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

feat: add recaptcha #75

Merged
merged 12 commits into from
Mar 27, 2023
Merged
3 changes: 3 additions & 0 deletions .github/workflows/cdelivery-s3-caller.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,7 @@ jobs:
show-notifications: ${{ secrets.REACT_APP_SHOW_NOTIFICATIONS }}
graasp-compose-host: ${{ secrets.REACT_APP_GRAASP_COMPOSE_HOST_STAGE }}
graasp-explorer-host: ${{ secrets.REACT_APP_GRAASP_EXPLORE_HOST_STAGE }}
authentication-host: ${{ secrets.REACT_APP_AUTHENTICATION_HOST_STAGE }}
recaptcha-site-key: ${{ secrets.REACT_APP_RECAPTCHA_SITE_KEY }}
sentry-dsn: ${{ secrets.REACT_APP_SENTRY_DSN }}
domain: ${{ secrets.REACT_APP_DOMAIN_STAGE }}
3 changes: 3 additions & 0 deletions .github/workflows/cdeployment-s3-caller.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,7 @@ jobs:
show-notifications: ${{ secrets.REACT_APP_SHOW_NOTIFICATIONS }}
graasp-compose-host: ${{ secrets.REACT_APP_GRAASP_COMPOSE_HOST_PROD }}
graasp-explorer-host: ${{ secrets.REACT_APP_GRAASP_EXPLORE_HOST_PROD }}
authentication-host: ${{ secrets.REACT_APP_AUTHENTICATION_HOST_PROD }}
recaptcha-site-key: ${{ secrets.REACT_APP_RECAPTCHA_SITE_KEY }}
sentry-dsn: ${{ secrets.REACT_APP_SENTRY_DSN }}
domain: ${{ secrets.REACT_APP_DOMAIN_PROD }}
3 changes: 3 additions & 0 deletions .github/workflows/cintegration-s3-caller.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ jobs:
aws-s3-bucket-name: ${{ secrets.AWS_S3_BUCKET_NAME_GRAASP_AUTH_DEV }}
cloudfront-distribution-id: ${{ secrets.CLOUDFRONT_DISTRIBUTION_GRAASP_AUTH_DEV }}
api-host: ${{ secrets.REACT_APP_API_HOST_DEV }}
authentication-host: ${{ secrets.REACT_APP_AUTHENTICATION_HOST_DEV }}
show-notifications: ${{ secrets.REACT_APP_SHOW_NOTIFICATIONS }}
graasp-compose-host: ${{ secrets.REACT_APP_GRAASP_COMPOSE_HOST_DEV }}
graasp-explorer-host: ${{ secrets.REACT_APP_GRAASP_EXPLORE_HOST_DEV }}
recaptcha-site-key: ${{ secrets.REACT_APP_RECAPTCHA_SITE_KEY }}
sentry-dsn: ${{ secrets.REACT_APP_SENTRY_DSN }}
domain: ${{ secrets.REACT_APP_DOMAIN_DEV }}
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,15 @@
# graasp-auth

Create an `.env.local` file with:

```sh
REACT_APP_API_HOST=http://localhost:3000
PORT=3001
REACT_APP_DOMAIN=localhost:3001
REACT_APP_SHOW_NOTIFICATIONS=true
REACT_APP_AUTHENTICATION_HOST=http://localhost:3001

spaenleh marked this conversation as resolved.
Show resolved Hide resolved
REACT_APP_RECAPTCHA_SITE_KEY=
```

Generate your recaptcha key from [the reCAPTCHA admin console](https://www.google.com/recaptcha/admin/create)
13 changes: 10 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@
"@emotion/react": "11.10.5",
"@emotion/styled": "11.10.5",
"@graasp/query-client": "0.2.0",
"@graasp/sdk": "0.4.1",
"@graasp/sdk": "0.10.0",
"@graasp/translations": "1.8.0",
"@graasp/ui": "0.11.1",
"@mui/icons-material": "5.11.0",
"@mui/lab": "5.0.0-alpha.119",
"@mui/material": "5.11.8",
"@sentry/browser": "7.45.0",
"@sentry/react": "7.45.0",
"@sentry/tracing": "7.45.0",
"http-status-codes": "2.2.0",
"qs": "6.11.0",
"react": "17.0.2",
Expand All @@ -31,8 +34,8 @@
"validator": "13.7.0"
},
"scripts": {
"start": "env-cmd -f ./.env.local react-scripts start",
"start:test": "env-cmd -f ./.env.test react-scripts start",
"start": "env-cmd -f ./.env.local react-scripts -r @cypress/instrument-cra start",
"start:test": "env-cmd -f ./.env.test react-scripts -r @cypress/instrument-cra start",
"start:ci": "react-scripts -r @cypress/instrument-cra start",
"build": "react-scripts build",
"dist:dev": "env-cmd -f ./.env.development react-scripts build",
Expand Down Expand Up @@ -106,5 +109,9 @@
"typescript": "4.9.4",
"wait-on": "7.0.1"
},
"resolutions": {
"@graasp/sdk": "0.10.0",
"@types/react": "17.0.30"
},
"packageManager": "yarn@3.2.1"
}
6 changes: 6 additions & 0 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@
href="%PUBLIC_URL%/favicon-16x16.png"
/>

<!--
This is to load the reCAPTCHA script
The REACT_APP_RECAPTCHA_SITE_KEY value is replaced at build time by CRA
-->
<script src="https://www.google.com/recaptcha/api.js?render=%REACT_APP_RECAPTCHA_SITE_KEY%"></script>
spaenleh marked this conversation as resolved.
Show resolved Hide resolved

<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
Expand Down
22 changes: 13 additions & 9 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import * as Sentry from '@sentry/react';
import { Route, BrowserRouter as Router, Routes } from 'react-router-dom';

import { HOME_PATH, SIGN_UP_PATH, buildSignInPath } from '../config/paths';
import ErrorFallback from './ErrorFallback';
import Redirection from './Redirection';
import SignIn from './SignIn';
import SignUp from './SignUp';

const App = () => (
<Router>
<Redirection>
<Routes>
<Route path={buildSignInPath()} element={<SignIn />} />
<Route path={SIGN_UP_PATH} element={<SignUp />} />
<Route path={HOME_PATH} element={<SignIn />} />
</Routes>
</Redirection>
</Router>
<Sentry.ErrorBoundary fallback={<ErrorFallback />} showDialog>
<Router>
<Redirection>
<Routes>
<Route path={buildSignInPath()} element={<SignIn />} />
<Route path={SIGN_UP_PATH} element={<SignUp />} />
<Route path={HOME_PATH} element={<SignIn />} />
</Routes>
</Redirection>
</Router>
</Sentry.ErrorBoundary>
);

export default App;
11 changes: 11 additions & 0 deletions src/components/ErrorFallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { FAILURE_MESSAGES } from '@graasp/translations';

import { Alert } from '@mui/material';

import { useMessagesTranslation } from '../config/i18n';

const ErrorFallback = () => {
const { t } = useMessagesTranslation();
return <Alert severity="error">{t(FAILURE_MESSAGES.UNEXPECTED_ERROR)}</Alert>;
};
export default ErrorFallback;
7 changes: 5 additions & 2 deletions src/components/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,24 @@ import { theme } from '@graasp/ui';

import { ThemeProvider } from '@mui/material/styles';

import { SHOW_NOTIFICATIONS } from '../config/constants';
import { RECAPTCHA_SITE_KEY, SHOW_NOTIFICATIONS } from '../config/constants';
import i18nConfig from '../config/i18n';
import {
QueryClientProvider,
ReactQueryDevtools,
queryClient,
} from '../config/queryClient';
import { RecaptchaProvider } from '../context/RecaptchaContext';
import App from './App';

const Root = () => (
<QueryClientProvider client={queryClient}>
<I18nextProvider i18n={i18nConfig}>
<ThemeProvider theme={theme}>
{SHOW_NOTIFICATIONS && <ToastContainer />}
<App />
<RecaptchaProvider siteKey={RECAPTCHA_SITE_KEY}>
<App />
</RecaptchaProvider>
</ThemeProvider>
</I18nextProvider>
{process.env.NODE_ENV === 'development' && <ReactQueryDevtools />}
Expand Down
18 changes: 14 additions & 4 deletions src/components/SignIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { FC, useState } from 'react';
import { Link } from 'react-router-dom';

import { MUTATION_KEYS } from '@graasp/query-client';
import { RecaptchaAction } from '@graasp/sdk';
import { AUTH } from '@graasp/translations';
import { Button } from '@graasp/ui';

Expand All @@ -21,6 +22,7 @@ import {
SIGN_IN_BUTTON_ID,
SIGN_IN_HEADER_ID,
} from '../config/selectors';
import { useRecaptcha } from '../context/RecaptchaContext';
import { SIGN_IN_METHODS } from '../types/signInMethod';
import { emailValidator, passwordValidator } from '../utils/validation';
import EmailInput from './EmailInput';
Expand All @@ -41,6 +43,7 @@ const {

const SignIn: FC = () => {
const { t } = useAuthTranslation();
const { executeCaptcha } = useRecaptcha();

const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
Expand All @@ -53,15 +56,15 @@ const SignIn: FC = () => {
const { mutateAsync: signIn, isSuccess: signInSuccess } = useMutation<
unknown,
unknown,
{ email: string }
{ email: string; captcha: string }
>(MUTATION_KEYS.SIGN_IN);
const {
mutateAsync: signInWithPassword,
isSuccess: signInWithPasswordSuccess,
} = useMutation<
{ data: { resource: string } },
unknown,
{ email: string; password: string }
{ email: string; password: string; captcha: string }
>(MUTATION_KEYS.SIGN_IN_WITH_PASSWORD);

const handleSignIn = async () => {
Expand All @@ -70,8 +73,13 @@ const SignIn: FC = () => {
if (checkingEmail) {
setShouldValidate(true);
} else {
await signIn({ email: lowercaseEmail });
setSuccessView(true);
try {
const token = await executeCaptcha(RecaptchaAction.SignIn);
await signIn({ email: lowercaseEmail, captcha: token });
setSuccessView(true);
} catch (e) {
console.error(e);
}
}
};

Expand All @@ -85,9 +93,11 @@ const SignIn: FC = () => {
setPasswordError(checkingPassword);
}
} else {
const token = await executeCaptcha(RecaptchaAction.SignInWithPassword);
const { data } = await signInWithPassword({
email: lowercaseEmail,
password,
captcha: token,
});
if (data.resource) {
window.location.href = data.resource;
Expand Down
12 changes: 10 additions & 2 deletions src/components/SignUp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ChangeEventHandler, useEffect, useState } from 'react';
import { Link, useSearchParams } from 'react-router-dom';

import { MUTATION_KEYS } from '@graasp/query-client';
import { RecaptchaAction } from '@graasp/sdk';
import { AUTH } from '@graasp/translations';
import { Button, Loader } from '@graasp/ui';

Expand All @@ -17,6 +18,7 @@ import {
SIGN_UP_BUTTON_ID,
SIGN_UP_HEADER_ID,
} from '../config/selectors';
import { useRecaptcha } from '../context/RecaptchaContext';
import { emailValidator, nameValidator } from '../utils/validation';
import EmailInput from './EmailInput';
import FullscreenContainer from './FullscreenContainer';
Expand All @@ -29,6 +31,7 @@ const { SIGN_IN_LINK_TEXT, SIGN_UP_BUTTON, SIGN_UP_HEADER, NAME_FIELD_LABEL } =

const SignUp = () => {
const { t } = useAuthTranslation();
const { executeCaptcha } = useRecaptcha();

const [email, setEmail] = useState<string>('');
const [name, setName] = useState<string>('');
Expand All @@ -40,7 +43,7 @@ const SignUp = () => {
const { mutateAsync: signUp, isSuccess: signUpSuccess } = useMutation<
unknown,
unknown,
{ email: string; name: string }
{ email: string; name: string; captcha: string }
>(MUTATION_KEYS.SIGN_UP);
const [searchParams] = useSearchParams();

Expand Down Expand Up @@ -79,7 +82,12 @@ const SignUp = () => {
setNameError(checkingUsername);
setShouldValidate(true);
} else {
await signUp({ name, email: lowercaseEmail });
const token = await executeCaptcha(RecaptchaAction.SignUp);
await signUp({
name: name.trim(),
email: lowercaseEmail,
captcha: token,
});
setSuccessView(true);
}
};
Expand Down
14 changes: 10 additions & 4 deletions src/components/SuccessContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState } from 'react';
import { Trans } from 'react-i18next';

import { MUTATION_KEYS } from '@graasp/query-client';
import { RecaptchaAction } from '@graasp/sdk';
import { AUTH, namespaces } from '@graasp/translations';
import { Button } from '@graasp/ui';

Expand All @@ -12,6 +13,7 @@ import { useAuthTranslation } from '../config/i18n';
import { useMutation } from '../config/queryClient';
import { SUCCESS_CONTENT_ID } from '../config/selectors';
import { BACK_BUTTON_ID, RESEND_EMAIL_BUTTON_ID } from '../config/selectors';
import { useRecaptcha } from '../context/RecaptchaContext';

type Props = {
title: string;
Expand All @@ -25,17 +27,21 @@ const SuccessContent = ({
handleBackButtonClick = null,
}: Props) => {
const { t } = useAuthTranslation();
const { executeCaptcha } = useRecaptcha();
const [isEmailSent, setIsEmailSent] = useState(false);

// used for resend email
const { mutate: signIn } = useMutation<unknown, unknown, { email: string }>(
MUTATION_KEYS.SIGN_IN,
);
const { mutate: signIn } = useMutation<
unknown,
unknown,
{ email: string; captcha: string }
>(MUTATION_KEYS.SIGN_IN);

// used for resend email
const handleResendEmail = async () => {
const lowercaseEmail = email.toLowerCase();
signIn({ email: lowercaseEmail });
const token = await executeCaptcha(RecaptchaAction.SignIn);
signIn({ email: lowercaseEmail, captcha: token });
};

const onClickResendEmail = () => {
Expand Down
5 changes: 5 additions & 0 deletions src/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ export const GRAASP_COMPOSE_HOST =
export const AUTHENTICATION_HOST =
process.env.REACT_APP_AUTHENTICATION_HOST || 'http://localhost:3001';

export const SENRY_DSN = process.env.REACT_APP_SENTRY_DSN;
export const APP_VERSION = process.env.REACT_APP_VERSION;

export const RECAPTCHA_SITE_KEY = process.env.REACT_APP_RECAPTCHA_SITE_KEY;

export const NAME_MAXIMUM_LENGTH = 300;
export const NAME_MINIMUM_LENGTH = 2;

Expand Down
1 change: 1 addition & 0 deletions src/config/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ i18n.use(initReactI18next);
export const useAuthTranslation = () => useTranslation(namespaces.auth);
export const useBuilderTranslation = () => useTranslation(namespaces.builder);
export const useCommonTranslation = () => useTranslation(namespaces.common);
export const useMessagesTranslation = () => useTranslation(namespaces.messages);

export default i18n;
50 changes: 50 additions & 0 deletions src/context/RecaptchaContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { createContext, useContext } from 'react';

declare global {
interface Window {
grecaptcha: {
ready?: (callback: () => void) => void;
execute: (
siteKey: string,
{ action }: { action: string },
) => Promise<string>;
};
}
}

type RecaptchaContextType = {
executeCaptcha: (action: string) => Promise<string>;
};

const RecaptchaContext = createContext<RecaptchaContextType>({
executeCaptcha: () => Promise.reject('No Recaptcha context provider found'),
});

type Props = {
children: JSX.Element;
siteKey: string;
};

export const RecaptchaProvider = ({ children, siteKey }: Props) => {
const executeCaptcha = (action: string): Promise<string> => {
return new Promise<string>((resolve) => {
if (!window.grecaptcha) {
resolve(undefined);
} else {
window.grecaptcha.ready(async () => {
const token = await window.grecaptcha.execute(siteKey, { action });
resolve(token);
});
}
});
};

const value = { executeCaptcha };
return (
<RecaptchaContext.Provider value={value}>
{children}
</RecaptchaContext.Provider>
);
};

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