Add recaptcha v3 for register form.
This commit is contained in:
parent
265992bc35
commit
65f7d41284
@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Recaptcha to prevent API abuse.
|
||||
|
||||
### Fixed
|
||||
|
||||
## [Winter 0.5.0-alpha] - 2023/01/17
|
||||
|
||||
### Added
|
||||
- Mentions: Mention channels, and users which selectively highlights associated users/channels.
|
||||
|
||||
|
@ -4,4 +4,5 @@ MONGO_URI="mongodb://localhost/accord"
|
||||
NODE_ENV="dev"
|
||||
PORT=3000
|
||||
WEBSITE_URL="http://localhost:4200"
|
||||
SESSION_SECRET="Please ⭐ this repository."
|
||||
SESSION_SECRET="Please ⭐ this repository."
|
||||
RECAPTCHA_SECRET=""
|
@ -7,14 +7,14 @@ const windowMs = 10 * 60 * 1000;
|
||||
// additional layer rate limits
|
||||
export const extraRateLimit = (maxRequests: number) => (req: Request, res: Response, next: NextFunction) => {
|
||||
if (process.env.NODE_ENV === 'dev') return next();
|
||||
|
||||
|
||||
return rateLimit({
|
||||
max: maxRequests,
|
||||
message: JSON.stringify({ message: 'You are being rate limited' }),
|
||||
store: new RateLimitStore({
|
||||
uri: process.env.MONGO_URI,
|
||||
collectionName: 'extraRateLimits',
|
||||
expireTimeMs: windowMs,
|
||||
expireTimeMs: windowMs / 2,
|
||||
}),
|
||||
windowMs: windowMs / 2,
|
||||
})(req, res, next);
|
||||
@ -25,7 +25,7 @@ export default (req: Request, res: Response, next: NextFunction) => {
|
||||
if (process.env.NODE_ENV === 'dev') return next();
|
||||
|
||||
return rateLimit({
|
||||
max: 5000,
|
||||
max: 3000,
|
||||
message: JSON.stringify({ message: 'You are being rate limited' }),
|
||||
store: new RateLimitStore({
|
||||
uri: process.env.MONGO_URI,
|
||||
|
@ -8,7 +8,7 @@ import { REST } from '@acrd/types';
|
||||
|
||||
export const router = Router();
|
||||
|
||||
router.post('/login', extraRateLimit(20), (req, res, next) => {
|
||||
router.post('/login', extraRateLimit(10), (req, res, next) => {
|
||||
req['flash'] = (_: string, message: string) => res.status(400).json({ message });
|
||||
next();
|
||||
}, passport.authenticate('local', {
|
||||
@ -30,7 +30,22 @@ router.post('/login', extraRateLimit(20), (req, res, next) => {
|
||||
res.status(201).json({ token: await deps.users.createToken(user) });
|
||||
});
|
||||
|
||||
router.post('/register', extraRateLimit(1), async (req, res) => {
|
||||
router.post('/register', extraRateLimit(10), async (req, res) => {
|
||||
if (process.env.RECAPTCHA_SECRET) {
|
||||
var response = await fetch('https://www.google.com/recaptcha/api/siteverify', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams(Object.entries({
|
||||
secret: process.env.RECAPTCHA_SECRET,
|
||||
response: req.query.recaptcha as string,
|
||||
})).toString(),
|
||||
});
|
||||
|
||||
var json = await response.json();
|
||||
if (!json.success)
|
||||
throw new APIError(400, 'Invalid captcha');
|
||||
}
|
||||
|
||||
const user = await deps.users.create({
|
||||
email: req.body.email,
|
||||
password: req.body.password,
|
||||
@ -42,7 +57,7 @@ router.post('/register', extraRateLimit(1), async (req, res) => {
|
||||
res.status(201).json(await deps.users.createToken(user));
|
||||
});
|
||||
|
||||
router.get('/verify', extraRateLimit(10), async (req, res) => {
|
||||
router.get('/verify', extraRateLimit(5), async (req, res) => {
|
||||
const email = deps.verification.getEmailFromCode(req.query.code as string);
|
||||
const user = await User.findOne({ email }) as any;
|
||||
if (!email || !user)
|
||||
@ -66,7 +81,7 @@ router.get('/verify', extraRateLimit(10), async (req, res) => {
|
||||
res.json({ token: await deps.users.createToken(user) });
|
||||
});
|
||||
|
||||
router.get('/email/forgot-password', extraRateLimit(10), async (req, res) => {
|
||||
router.get('/email/forgot-password', extraRateLimit(5), async (req, res) => {
|
||||
const email = req.query.email?.toString();
|
||||
if (!email)
|
||||
throw new APIError(400, 'Email not provided');
|
||||
@ -82,7 +97,7 @@ router.get('/email/forgot-password', extraRateLimit(10), async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/change-password', extraRateLimit(10), async (req, res) => {
|
||||
router.post('/change-password', extraRateLimit(5), async (req, res) => {
|
||||
const { email, oldPassword, newPassword }: REST.To.Post['/auth/change-password'] = req.body;
|
||||
|
||||
const user = await User.findOne({ email }) as any as SelfUserDocument;
|
||||
|
@ -6,4 +6,5 @@ NODE_ENV="dev"
|
||||
PORT=3001
|
||||
ROOT_ENDPOINT="http://localhost:3001"
|
||||
WEBSITE_URL="http://localhost:4200"
|
||||
SESSION_SECRET="Please ⭐ this repository."
|
||||
SESSION_SECRET="Please ⭐ this repository."
|
||||
RECAPTCHA_SECRET=""
|
1
backend/types/dotenv.d.ts
vendored
1
backend/types/dotenv.d.ts
vendored
@ -9,6 +9,7 @@ declare global {
|
||||
ROOT_ENDPOINT: string;
|
||||
WEBSITE_URL: string;
|
||||
SESSION_SECRET: string;
|
||||
RECAPTCHA_SECRET: string;
|
||||
/** Set during runtime. */
|
||||
SSH_KEY: string;
|
||||
}
|
||||
|
3
frontend/env/.env.dev
vendored
3
frontend/env/.env.dev
vendored
@ -6,4 +6,5 @@ REACT_APP_REPO_URL="https://github.com/theadamjr/acrd.app"
|
||||
REACT_APP_VERSION_NAME="Cyan"
|
||||
REACT_APP_VERSION_NUMBER="0.6.0-dev-release"
|
||||
REACT_APP_ROOT_API_URL="http://localhost:3000"
|
||||
REACT_APP_OWNER_USER_ID="450428712295456768"
|
||||
REACT_APP_OWNER_USER_ID="450428712295456768"
|
||||
REACT_APP_RECAPTCHA_SITE_KEY="6Ldzz2omAAAAAAZ7Ey6ZmDtCS5rMSxVYc3Uf5EpQ"
|
3
frontend/env/.env.prod
vendored
3
frontend/env/.env.prod
vendored
@ -6,4 +6,5 @@ REACT_APP_REPO_URL="https://github.com/theadamjr/acrd.app"
|
||||
REACT_APP_VERSION_NAME="Cyan"
|
||||
REACT_APP_VERSION_NUMBER="0.6.0-dev-release"
|
||||
REACT_APP_ROOT_API_URL="https://api.acrd.app"
|
||||
REACT_APP_OWNER_USER_ID="451453819310678016"
|
||||
REACT_APP_OWNER_USER_ID="451453819310678016"
|
||||
REACT_APP_RECAPTCHA_SITE_KEY="6Ldzz2omAAAAAAZ7Ey6ZmDtCS5rMSxVYc3Uf5EpQ"
|
40
frontend/package-lock.json
generated
40
frontend/package-lock.json
generated
@ -38,6 +38,7 @@
|
||||
"react-contextmenu": "^2.14.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-draggable": "^4.4.4",
|
||||
"react-google-recaptcha-v3": "^1.10.1",
|
||||
"react-hook-form": "^7.12.1",
|
||||
"react-modal": "^3.14.3",
|
||||
"react-number-format": "^4.7.3",
|
||||
@ -67,6 +68,7 @@
|
||||
"@types/javascript-time-ago": "^2.0.3",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/promise-timeout": "^1.3.0",
|
||||
"@types/react-google-recaptcha": "^2.1.5",
|
||||
"@types/react-modal": "^3.12.1",
|
||||
"@types/react-router-dom": "^5.1.8",
|
||||
"@types/react-select": "^4.0.17",
|
||||
@ -4122,6 +4124,15 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-google-recaptcha": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-google-recaptcha/-/react-google-recaptcha-2.1.5.tgz",
|
||||
"integrity": "sha512-iWTjmVttlNgp0teyh7eBXqNOQzVq2RWNiFROWjraOptRnb1OcHJehQnji0sjqIRAk9K0z8stjyhU+OLpPb0N6w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-modal": {
|
||||
"version": "3.13.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-modal/-/react-modal-3.13.1.tgz",
|
||||
@ -17495,6 +17506,18 @@
|
||||
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
|
||||
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg=="
|
||||
},
|
||||
"node_modules/react-google-recaptcha-v3": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/react-google-recaptcha-v3/-/react-google-recaptcha-v3-1.10.1.tgz",
|
||||
"integrity": "sha512-K3AYzSE0SasTn+XvV2tq+6YaxM+zQypk9rbCgG4OVUt7Rh4ze9basIKefoBz9sC0CNslJj9N1uwTTgRMJQbQJQ==",
|
||||
"dependencies": {
|
||||
"hoist-non-react-statics": "^3.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.3 || ^17.0 || ^18.0",
|
||||
"react-dom": "^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hook-form": {
|
||||
"version": "7.19.5",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.19.5.tgz",
|
||||
@ -25580,6 +25603,15 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-google-recaptcha": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-google-recaptcha/-/react-google-recaptcha-2.1.5.tgz",
|
||||
"integrity": "sha512-iWTjmVttlNgp0teyh7eBXqNOQzVq2RWNiFROWjraOptRnb1OcHJehQnji0sjqIRAk9K0z8stjyhU+OLpPb0N6w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-modal": {
|
||||
"version": "3.13.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-modal/-/react-modal-3.13.1.tgz",
|
||||
@ -35486,6 +35518,14 @@
|
||||
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
|
||||
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg=="
|
||||
},
|
||||
"react-google-recaptcha-v3": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/react-google-recaptcha-v3/-/react-google-recaptcha-v3-1.10.1.tgz",
|
||||
"integrity": "sha512-K3AYzSE0SasTn+XvV2tq+6YaxM+zQypk9rbCgG4OVUt7Rh4ze9basIKefoBz9sC0CNslJj9N1uwTTgRMJQbQJQ==",
|
||||
"requires": {
|
||||
"hoist-non-react-statics": "^3.3.2"
|
||||
}
|
||||
},
|
||||
"react-hook-form": {
|
||||
"version": "7.19.5",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.19.5.tgz",
|
||||
|
@ -34,6 +34,7 @@
|
||||
"react-contextmenu": "^2.14.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-draggable": "^4.4.4",
|
||||
"react-google-recaptcha-v3": "^1.10.1",
|
||||
"react-hook-form": "^7.12.1",
|
||||
"react-modal": "^3.14.3",
|
||||
"react-number-format": "^4.7.3",
|
||||
@ -95,6 +96,7 @@
|
||||
"@types/javascript-time-ago": "^2.0.3",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/promise-timeout": "^1.3.0",
|
||||
"@types/react-google-recaptcha": "^2.1.5",
|
||||
"@types/react-modal": "^3.12.1",
|
||||
"@types/react-router-dom": "^5.1.8",
|
||||
"@types/react-select": "^4.0.17",
|
||||
|
@ -5,8 +5,9 @@ import { registerUser } from '../../../store/auth';
|
||||
import NormalButton from '../../utils/buttons/normal-button';
|
||||
import PageWrapper from '../page-wrapper';
|
||||
import Input from '../../inputs/input';
|
||||
import { useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import FullParticles from '../../utils/full-particles';
|
||||
import { GoogleReCaptcha, GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
|
||||
|
||||
const RegisterPage: React.FunctionComponent = () => {
|
||||
const user = useSelector((s: Store.AppState) => s.auth.user);
|
||||
@ -14,45 +15,61 @@ const RegisterPage: React.FunctionComponent = () => {
|
||||
const { register, handleSubmit } = useForm();
|
||||
const query = new URLSearchParams(useLocation().search);
|
||||
const [email, setEmail] = useState(query.get('email') ?? '');
|
||||
const [token, setToken] = useState("");
|
||||
const [refreshReCaptcha, setRefreshReCaptcha] = useState(true);
|
||||
|
||||
const onSubmit = (data) => dispatch(registerUser(data));
|
||||
const doSomething = () => {
|
||||
/* do something like submit a form and then refresh recaptcha */
|
||||
setRefreshReCaptcha(r => !r);
|
||||
}
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
if (token)
|
||||
dispatch(registerUser(data, token));
|
||||
else throw new TypeError("Verify you are not a robot");
|
||||
}
|
||||
|
||||
return (user)
|
||||
? <Redirect to="/channels/@me" />
|
||||
: (
|
||||
<PageWrapper>
|
||||
<div className="flex items-center justify-center absolute top-[30%] left-[35%]">
|
||||
<form className="rounded-md shadow bg-bg-primary p-8 w-[480px]"
|
||||
onSubmit={handleSubmit(onSubmit)}>
|
||||
<h1 className="text-2xl font-bold mb-8 text-center">Create an account</h1>
|
||||
<GoogleReCaptchaProvider reCaptchaKey={process.env.REACT_APP_RECAPTCHA_SITE_KEY}>
|
||||
<PageWrapper>
|
||||
<div className="flex items-center justify-center absolute top-[30%] left-[35%]">
|
||||
<form className="rounded-md shadow bg-bg-primary p-8 w-[480px]"
|
||||
onSubmit={handleSubmit(onSubmit)}>
|
||||
<h1 className="text-2xl font-bold mb-8 text-center">Create an account</h1>
|
||||
|
||||
<Input
|
||||
label="Email"
|
||||
name="email"
|
||||
register={register}
|
||||
className="mt-3"
|
||||
defaultValue={email!}
|
||||
onInput={(e) => setEmail(e.currentTarget.value)} />
|
||||
<Input
|
||||
label="Username"
|
||||
name="username"
|
||||
register={register}
|
||||
className="mt-3" />
|
||||
<Input
|
||||
label="Password"
|
||||
name="password"
|
||||
type="password"
|
||||
register={register}
|
||||
className="mt-3" />
|
||||
<Input
|
||||
label="Email"
|
||||
name="email"
|
||||
register={register}
|
||||
className="mt-3"
|
||||
defaultValue={email!}
|
||||
onInput={(e) => setEmail(e.currentTarget.value)} />
|
||||
<Input
|
||||
label="Username"
|
||||
name="username"
|
||||
register={register}
|
||||
className="mt-3" />
|
||||
<Input
|
||||
label="Password"
|
||||
name="password"
|
||||
type="password"
|
||||
register={register}
|
||||
className="mt-3" />
|
||||
|
||||
<NormalButton className="bg-primary font w-full h-11 rounded-md mt-8">Register</NormalButton>
|
||||
<p className="mt-2">
|
||||
<Link to={`/login${email && `?email=${email}`}`}>Already have an account?</Link>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
<FullParticles />
|
||||
</PageWrapper>
|
||||
<GoogleReCaptcha onVerify={(token) => setToken(token)} />
|
||||
<NormalButton
|
||||
onClick={doSomething}
|
||||
className="bg-primary font w-full h-11 rounded-md mt-8">Register</NormalButton>
|
||||
<p className="mt-2">
|
||||
<Link to={`/login${email && `?email=${email}`}`}>Already have an account?</Link>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
<FullParticles />
|
||||
</PageWrapper>
|
||||
</GoogleReCaptchaProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ import configureStore from './store/configure-store';
|
||||
import { SnackbarProvider } from 'notistack';
|
||||
import { applyTheme } from './store/themes';
|
||||
import accordTheme from '!!raw-loader!./styles/theme/accord-theme.css';
|
||||
import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
|
||||
|
||||
applyTheme(accordTheme);
|
||||
|
||||
|
@ -79,12 +79,12 @@ export const logoutUser = () => (dispatch) => {
|
||||
localStorage.removeItem('token');
|
||||
}
|
||||
|
||||
export const registerUser = (data: REST.To.Post['/auth/register']) => (dispatch) => {
|
||||
export const registerUser = (data: REST.To.Post['/auth/register'], recaptcha: string) => (dispatch) => {
|
||||
dispatch(api.restCallBegan({
|
||||
onSuccess: [actions.loggedInAttempted.type],
|
||||
method: 'post',
|
||||
data,
|
||||
url: `/auth/register`,
|
||||
url: `/auth/register?recaptcha=${recaptcha}`,
|
||||
callback: (payload) => {
|
||||
localStorage.setItem('token', payload);
|
||||
dispatch(ready());
|
||||
|
1
frontend/types/dotenv.d.ts
vendored
1
frontend/types/dotenv.d.ts
vendored
@ -10,6 +10,7 @@ declare global {
|
||||
REACT_APP_VERSION_NUMBER: string;
|
||||
REACT_APP_ROOT_API_URL: string;
|
||||
REACT_APP_OWNER_USER_ID: string;
|
||||
REACT_APP_RECAPTCHA_SITE_KEY: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user