Skip to content
This repository has been archived by the owner on Nov 21, 2024. It is now read-only.

feat: use recaptcha v2 (invisible) #322

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 5 additions & 9 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
Expand All @@ -8,14 +8,10 @@
name="description"
content="Authentication portal for the Graasp ecosystem."
/>
<meta name="version-info" content="%VITE_VERSION%" />

<!--
This is to load the reCAPTCHA script
The VITE_RECAPTCHA_SITE_KEY value is replaced at build time by vite
-->
<script src="https://www.google.com/recaptcha/api.js?render=%VITE_RECAPTCHA_SITE_KEY%"></script>

<meta
name="version-info"
content="%VITE_VERSION% @ %VITE_BUILD_TIMESTAMP%"
/>
<!-- Load Roboto font from Google fonts -->
<link
rel="stylesheet"
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-ga4": "2.1.0",
"react-google-recaptcha": "3.1.0",
"react-i18next": "14.0.5",
"react-router": "6.22.1",
"react-router-dom": "6.22.1",
Expand Down Expand Up @@ -85,6 +86,7 @@
"@types/node": "20.11.20",
"@types/react": "^18.2.60",
"@types/react-dom": "18.2.19",
"@types/react-google-recaptcha": "2.1.9",
"@types/react-router-dom": "5.3.3",
"@types/validator": "13.11.9",
"@typescript-eslint/eslint-plugin": "7.1.0",
Expand Down
9 changes: 5 additions & 4 deletions src/components/FullscreenContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { Box } from '@mui/material';
import { Box, Stack } from '@mui/material';

import Footer from './Footer';
import ReCAPTCHANotice from './ReCAPTCHANotice';

type Props = {
children: JSX.Element | JSX.Element[];
};

const FullscreenContainer = ({ children }: Props): JSX.Element => (
<Box
<Stack
margin="auto"
textAlign="center"
display="flex"
alignItems="center"
justifyContent="center"
bgcolor="#f6f7fb"
Expand All @@ -30,8 +30,9 @@ const FullscreenContainer = ({ children }: Props): JSX.Element => (
>
{children}
</Box>
<ReCAPTCHANotice />
<Footer />
</Box>
</Stack>
);

export default FullscreenContainer;
31 changes: 31 additions & 0 deletions src/components/ReCAPTCHANotice.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Trans } from 'react-i18next';

import { Box, Link, Typography } from '@mui/material';

import { useAuthTranslation } from '../config/i18n';
import { AUTH } from '../langs/constants';

const ReCAPTCHANotice = () => {
const { t } = useAuthTranslation();
return (
<Box>
<Typography color="text.secondary" fontSize="0.5rem">
{t(AUTH.SITE_PROTECTED_BY_RECAPTCHA)}
</Typography>
<Typography color="text.secondary" fontSize="0.5rem">
<Trans i18nKey={AUTH.GOOGLE_PRIVACY_AND_TERMS} t={t}>
<Link
color="text.secondary"
href="https://policies.google.com/privacy"
>
Privacy Policy
</Link>
<Link color="text.secondary" href="https://policies.google.com/terms">
Terms of Service
</Link>
</Trans>
</Typography>
</Box>
);
};
export default ReCAPTCHANotice;
29 changes: 16 additions & 13 deletions src/components/SignIn.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import React, { FC, useState } from 'react';
import React, { FC, useRef, useState } from 'react';
import ReCAPTCHA from 'react-google-recaptcha';
import { Link, useLocation } from 'react-router-dom';

import { RecaptchaAction } from '@graasp/sdk';
import { Button } from '@graasp/ui';

import { Stack } from '@mui/material';
import Box from '@mui/material/Box';
import FormControl from '@mui/material/FormControl';
import Typography from '@mui/material/Typography';

import { RECAPTCHA_SITE_KEY } from '../config/env';
import { useAuthTranslation } from '../config/i18n';
import { SIGN_UP_PATH } from '../config/paths';
import { mutations } from '../config/queryClient';
Expand All @@ -21,7 +22,6 @@ import {
SIGN_IN_BUTTON_ID,
SIGN_IN_HEADER_ID,
} from '../config/selectors';
import { useRecaptcha } from '../context/RecaptchaContext';
import { useMobileAppLogin } from '../hooks/mobile';
import { useRedirection } from '../hooks/searchParams';
import { AUTH } from '../langs/constants';
Expand All @@ -42,8 +42,8 @@ const {
} = AUTH;

const SignIn: FC = () => {
const { t } = useAuthTranslation();
const { executeCaptcha } = useRecaptcha();
const { t, i18n } = useAuthTranslation();
const reCAPTCHARef = useRef<ReCAPTCHA>();

const { isMobile, challenge } = useMobileAppLogin();
const { search } = useLocation();
Expand Down Expand Up @@ -77,9 +77,7 @@ const SignIn: FC = () => {
setShouldValidate(true);
} else {
try {
const token = await executeCaptcha(
isMobile ? RecaptchaAction.SignInMobile : RecaptchaAction.SignIn,
);
const token = await reCAPTCHARef.current.executeAsync();
await (isMobile
? mobileSignIn({ email: lowercaseEmail, captcha: token, challenge })
: signIn({
Expand All @@ -104,11 +102,7 @@ const SignIn: FC = () => {
setPasswordError(checkingPassword);
}
} else {
const token = await executeCaptcha(
isMobile
? RecaptchaAction.SignInWithPasswordMobile
: RecaptchaAction.SignInWithPassword,
);
const token = await reCAPTCHARef.current.executeAsync();
const result = await (isMobile
? mobileSignInWithPassword({
email: lowercaseEmail,
Expand Down Expand Up @@ -211,6 +205,15 @@ const SignIn: FC = () => {
</Button>
</>
)}
<ReCAPTCHA
style={{ display: 'none' }}
ref={reCAPTCHARef}
sitekey={RECAPTCHA_SITE_KEY}
size="invisible"
hl={i18n.language}
tabIndex={-1}
badge="inline"
/>
{signInMethod === SIGN_IN_METHODS.EMAIL && (
<Button onClick={handleSignIn} id={SIGN_IN_BUTTON_ID}>
{t(SIGN_IN_BUTTON)}
Expand Down
57 changes: 35 additions & 22 deletions src/components/SignUp.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { ChangeEventHandler, useEffect, useState } from 'react';
import { ChangeEventHandler, useEffect, useRef, useState } from 'react';
import ReCAPTCHA from 'react-google-recaptcha';
import { Link, useLocation, useSearchParams } from 'react-router-dom';

import { RecaptchaAction } from '@graasp/sdk';
import { Button, Loader } from '@graasp/ui';

import { Stack } from '@mui/material';
import FormControl from '@mui/material/FormControl';
import Typography from '@mui/material/Typography';

import { SIGN_IN_PATH } from '../config/constants';
import { RECAPTCHA_SITE_KEY } from '../config/env';
import { useAuthTranslation } from '../config/i18n';
import { hooks, mutations } from '../config/queryClient';
import {
Expand All @@ -17,7 +18,6 @@ import {
SIGN_UP_BUTTON_ID,
SIGN_UP_HEADER_ID,
} from '../config/selectors';
import { useRecaptcha } from '../context/RecaptchaContext';
import { useMobileAppLogin } from '../hooks/mobile';
import { useRedirection } from '../hooks/searchParams';
import { AUTH } from '../langs/constants';
Expand All @@ -31,8 +31,8 @@ const { SIGN_IN_LINK_TEXT, SIGN_UP_BUTTON, SIGN_UP_HEADER, NAME_FIELD_LABEL } =
AUTH;

const SignUp = () => {
const { t } = useAuthTranslation();
const { executeCaptcha } = useRecaptcha();
const { t, i18n } = useAuthTranslation();
const reCAPTCHARef = useRef<ReCAPTCHA>();

const { isMobile, challenge } = useMobileAppLogin();
const redirect = useRedirection();
Expand Down Expand Up @@ -86,23 +86,28 @@ const SignUp = () => {
setNameError(checkingUsername);
setShouldValidate(true);
} else {
const token = await executeCaptcha(
isMobile ? RecaptchaAction.SignUpMobile : RecaptchaAction.SignUp,
);
await (isMobile
? mobileSignUp({
name: name.trim(),
email: lowercaseEmail,
captcha: token,
challenge,
})
: signUp({
name: name.trim(),
email: lowercaseEmail,
captcha: token,
url: redirect.url,
}));
setSuccessView(true);
try {
const token = await reCAPTCHARef.current.executeAsync();
// const token = await executeCaptcha(
// isMobile ? RecaptchaAction.SignUpMobile : RecaptchaAction.SignUp,
// );
await (isMobile
? mobileSignUp({
name: name.trim(),
email: lowercaseEmail,
captcha: token,
challenge,
})
: signUp({
name: name.trim(),
email: lowercaseEmail,
captcha: token,
url: redirect.url,
}));
setSuccessView(true);
} catch (err) {
console.error(err);
}
}
};

Expand Down Expand Up @@ -132,6 +137,14 @@ const SignUp = () => {
disabled={Boolean(invitation?.email)}
shouldValidate={shouldValidate}
/>
<ReCAPTCHA
style={{ display: 'none' }}
ref={reCAPTCHARef}
sitekey={RECAPTCHA_SITE_KEY}
size="invisible"
hl={i18n.language}
badge="inline"
/>
<Button onClick={handleRegister} id={SIGN_UP_BUTTON_ID} fullWidth>
{t(SIGN_UP_BUTTON)}
</Button>
Expand Down
16 changes: 16 additions & 0 deletions src/context/RecaptchaContext.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { createContext, useContext } from 'react';

import { RECAPTCHA_SITE_KEY } from '../config/env';

declare global {
interface Window {
grecaptcha: {
Expand Down Expand Up @@ -48,3 +50,17 @@ export const RecaptchaProvider = ({ children, siteKey }: Props) => {
};

export const useRecaptcha = () => useContext(RecaptchaContext);

export const RecaptchaComponent = () => {
return (
<div
className="g-recaptcha"
data-sitekey={RECAPTCHA_SITE_KEY}
// eslint-disable-next-line no-console
data-callback={(token) => console.log('got token', token)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe use console.debug?

data-size="invisible"
>
hello
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have a better text here?

</div>
);
};
2 changes: 2 additions & 0 deletions src/langs/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ export const AUTH = {
SIGN_UP_SAVE_ACTIONS_TOOLTIP: 'SIGN_UP_SAVE_ACTIONS_TOOLTIP',
SIGN_UP_SUCCESS_TITLE: 'SIGN_UP_SUCCESS_TITLE',
SWITCH_ACCOUNT_TEXT: 'SWITCH_ACCOUNT_TEXT',
SITE_PROTECTED_BY_RECAPTCHA: 'SITE_PROTECTED_BY_RECAPTCHA',
GOOGLE_PRIVACY_AND_TERMS: 'GOOGLE_PRIVACY_AND_TERMS',
};
4 changes: 3 additions & 1 deletion src/langs/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,7 @@
"SIGN_UP_SAVE_ACTIONS_LABEL": "Enable Analytics",
"SIGN_UP_SAVE_ACTIONS_TOOLTIP": "Coming Soon!",
"SIGN_UP_SUCCESS_TITLE": "Welcome!",
"SWITCH_ACCOUNT_TEXT": "Switch to another account"
"SWITCH_ACCOUNT_TEXT": "Switch to another account",
"SITE_PROTECTED_BY_RECAPTCHA": "This site is protected by reCAPTCHA.",
"GOOGLE_PRIVACY_AND_TERMS": "The Google <0>Privacy Policy</0> and <1>Terms of Service</1> apply."
}
4 changes: 3 additions & 1 deletion src/langs/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,7 @@
"SIGN_UP_SAVE_ACTIONS_LABEL": "Activer les statistiques d'utilisation",
"SIGN_UP_SAVE_ACTIONS_TOOLTIP": "Prochainement!",
"SIGN_UP_SUCCESS_TITLE": "Bienvenue !",
"SWITCH_ACCOUNT_TEXT": "Utiliser un autre compte"
"SWITCH_ACCOUNT_TEXT": "Utiliser un autre compte",
"SITE_PROTECTED_BY_RECAPTCHA": "Ce site est protégé par Google reCAPTCHA.",
"GOOGLE_PRIVACY_AND_TERMS": "La <0>Politique de protection des données</0> et les <1>Terms du service</1> de Google s'appliquent."
}
8 changes: 7 additions & 1 deletion vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import istanbul from 'vite-plugin-istanbul';

// https://vitejs.dev/config/
const config = ({ mode }: { mode: string }): UserConfigExport => {
process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };
process.env = {
VITE_VERSION: 'default',
VITE_BUILD_TIMESTAMP: new Date().toISOString(),
...process.env,
...loadEnv(mode, process.cwd()),
};

return defineConfig({
base: '',
Expand All @@ -29,6 +34,7 @@ const config = ({ mode }: { mode: string }): UserConfigExport => {
checker({
typescript: true,
eslint: { lintCommand: 'eslint "./**/*.{ts,tsx}"' },
overlay: { initialIsOpen: false },
}),
react(),
istanbul({
Expand Down
Loading
Loading