Add recaptcha for login page and forgotten password.

This commit is contained in:
theADAMJR 2023-06-07 06:27:05 +01:00
parent 380558eba2
commit ff6daf58ab
11 changed files with 87 additions and 56 deletions

View File

@ -6,11 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
Bug fixes and stability updates to make accord a more smooth experience.
### Added
- Recaptcha to prevent API abuse.
- Recaptcha to prevent API abuse to registration and login form.
### Fixed
- Invite page now loads invite when not in guild.
## [Winter 0.5.0-alpha] - 2023/01/17

View File

@ -1,7 +1,7 @@
import { Guild, GuildDocument } from './models/guild';
import DBWrapper from './db-wrapper';
import { generateSnowflake } from './snowflake-entity';
import { SelfUserDocument, User } from './models/user';
import { User } from './models/user';
import { Invite } from './models/invite';
import { APIError } from '../rest/modules/api-error';
import { Channel } from './models/channel';

View File

@ -8,16 +8,17 @@ import validateHasPermission from '../middleware/validate-has-permission';
export const router = Router();
// NOTE: Basic guild metadata is 'unlisted' and can be accessed by anyone with the URL.
router.get('/:id', async (req, res) => {
const guild = await deps.guilds.get(req.params.id);
res.json(guild);
});
router.get('/', updateUser, validateUser, async (req, res) => {
const guilds = await Guild.find({ _id: { $in: res.locals.guildIds } });
res.json(guilds);
});
router.get('/:id', updateUser, validateUser, async (req, res) => {
const guild = await deps.guilds.get(req.params.id);
res.json(guild);
});
router.get('/:id/channels',
updateUser, validateUser, updateGuild,
validateHasPermission(PermissionTypes.General.VIEW_CHANNELS),

View File

@ -44,9 +44,7 @@ router.get('/self', updateUser, validateUser, async (req, res) => res.json(res.l
router.get('/entities', updateUser, validateUser, async (req, res) => {
const guildIds: string[] = req.params.guildIds as any;
const user: UserTypes.Self = res.locals.user;
const $in = (guildIds)
? user.guildIds.concat(guildIds)
: user.guildIds;
const $in = user.guildIds.concat(guildIds);
const [channels, guilds, members, roles, themes, unsecureUsers] = await Promise.all([
Channel.find({ guildId: { $in } }),

View File

@ -4,7 +4,7 @@ REACT_APP_CDN_URL="http://localhost:3000/assets"
REACT_APP_WEBSITE_URL="http://localhost:4200"
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_VERSION_NUMBER="0.6.0-in-development"
REACT_APP_ROOT_API_URL="http://localhost:3000"
REACT_APP_OWNER_USER_ID="450428712295456768"
REACT_APP_RECAPTCHA_SITE_KEY="6Ldzz2omAAAAAAZ7Ey6ZmDtCS5rMSxVYc3Uf5EpQ"

View File

@ -4,7 +4,7 @@ REACT_APP_CDN_URL="https://api.acrd.app/assets"
REACT_APP_WEBSITE_URL="https://acrd.app"
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_VERSION_NUMBER="0.6.0-in-development"
REACT_APP_ROOT_API_URL="https://api.acrd.app"
REACT_APP_OWNER_USER_ID="451453819310678016"
REACT_APP_RECAPTCHA_SITE_KEY="6Ldzz2omAAAAAAZ7Ey6ZmDtCS5rMSxVYc3Uf5EpQ"

View File

@ -27,17 +27,20 @@ export default function App() {
return (
<Router basename={process.env.PUBLIC_URL}>
<Switch>
{/* These routes are public and can be accessed by anyone. */}
<Route exact path="/" component={HomePage} />
<Route exact path="/login" component={LoginPage} />
<Route exact path="/register" component={RegisterPage} />
<Route exact path="/logout" component={LogoutPage} />
<Route exact path="/verify" component={VerifyPage} />
<Route exact path="/join/:inviteId" component={InvitePage} />
<PrivateRoute exact path="/join/:inviteId" component={InvitePage} />
{/* Users must be logged in to use private routes. */}
<PrivateRoute exact path="/themes/:themeCode" component={ThemePage} />
<PrivateRoute exact path="/channels/@me" component={OverviewPage} />
<PrivateRoute exact path="/channels/:guildId/:channelId?" component={GuildPage} />
{/* This route is a catch-all for any other routes that don't exist. */}
<Route path="*" component={NotFoundPage} />
</Switch>
</Router>

View File

@ -8,6 +8,7 @@ import { loginUser, forgotPasswordEmail } from '../../../store/auth';
import { useEffect, useState } from 'react';
import VerifyCodeInput from './verify-code-input';
import FullParticles from '../../utils/full-particles';
import { GoogleReCaptcha, GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
const LoginPage: React.FunctionComponent = () => {
const dispatch = useDispatch();
@ -16,46 +17,61 @@ const LoginPage: React.FunctionComponent = () => {
const shouldVerify = useSelector((s: Store.AppState) => s.auth.shouldVerify);
const query = new URLSearchParams(useLocation().search);
const [email, setEmail] = useState(query.get('email') ?? '');
const [token, setToken] = useState('');
const [refreshReCaptcha, setRefreshReCaptcha] = useState(true);
const onLogin = (data) => dispatch(loginUser(data));
const resetPassword = () => dispatch(forgotPasswordEmail(getValues().email));
const onLogin = async (data) => {
if (token)
dispatch(loginUser(data));
else throw new TypeError('Beep Boop. If you are reading this, you are probably a robot');
}
const resetPassword = () => {
if (token)
dispatch(forgotPasswordEmail(getValues().email));
else throw new TypeError('Beep Boop. If you are reading this, you are probably a robot');
}
return (user)
? <Redirect to={query.get('redirect') ?? '/channels/@me'} />
: (
<PageWrapper pageTitle="acrd.app | Login">
<div className="flex items-center absolute justify-center top-[30%] left-[35%]">
<form
className="rounded-md shadow bg-bg-primary p-8 w-[478px]"
onSubmit={handleSubmit(onLogin)}>
<h1 className="text-3xl font-bold">Welcome back!</h1>
<p className="lead">We're so excited to see you again!</p>
<GoogleReCaptchaProvider reCaptchaKey={process.env.REACT_APP_RECAPTCHA_SITE_KEY}>
<PageWrapper pageTitle="acrd.app | Login">
<div className="flex items-center absolute justify-center top-[30%] left-[35%]">
<form
className="rounded-md shadow bg-bg-primary p-8 w-[478px]"
onSubmit={handleSubmit(onLogin)}>
<h1 className="text-3xl font-bold">Welcome back!</h1>
<p className="lead">We're so excited to see you again!</p>
<Input
label="Email"
name="email"
register={register}
className="mt-3"
defaultValue={email!}
onInput={(e) => setEmail(e.currentTarget.value)} />
<Input
label="Password"
name="password"
type="password"
register={register}
className="mt-3" />
<Link to="#" onClick={resetPassword}>Forgot your password?</Link>
<Input
label="Email"
name="email"
register={register}
className="mt-3"
defaultValue={email!}
onInput={(e) => setEmail(e.currentTarget.value)} />
<Input
label="Password"
name="password"
type="password"
register={register}
className="mt-3" />
<Link to="#" onClick={resetPassword}>Forgot your password?</Link>
{shouldVerify && <VerifyCodeInput />}
{shouldVerify && <VerifyCodeInput />}
<NormalButton className="bg-primary font w-full h-11 rounded-md mt-8">
{(shouldVerify) ? 'Resend Code' : 'Login'}
</NormalButton>
<p className="mt-2">Need an account? <Link to={`/register${email && `?email=${email}`}`}>Register</Link></p>
</form>
</div>
<FullParticles />
</PageWrapper>
<GoogleReCaptcha onVerify={(token) => setToken(token)} />
<NormalButton
onClick={() => setRefreshReCaptcha(r => !r)}
className="bg-primary font w-full h-11 rounded-md mt-8">
{(shouldVerify) ? 'Resend Code' : 'Login'}
</NormalButton>
<p className="mt-2">Need an account? <Link to={`/register${email && `?email=${email}`}`}>Register</Link></p>
</form>
</div>
<FullParticles />
</PageWrapper>
</GoogleReCaptchaProvider>
);
}

View File

@ -15,18 +15,13 @@ 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 [token, setToken] = useState('');
const [refreshReCaptcha, setRefreshReCaptcha] = useState(true);
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");
else throw new TypeError("Beep Boop. If you are reading this, you are probably a robot");
}
return (user)
@ -60,7 +55,7 @@ const RegisterPage: React.FunctionComponent = () => {
<GoogleReCaptcha onVerify={(token) => setToken(token)} />
<NormalButton
onClick={doSomething}
onClick={() => setRefreshReCaptcha(r => !r)}
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>

View File

@ -5,7 +5,7 @@ import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Link, useHistory, useParams } from 'react-router-dom';
import fetchEntities from '../../store/actions/fetch-entities';
import { getGuild, getGuildMembers } from '../../store/guilds';
import { fetchGuild, getGuild, getGuildMembers } from '../../store/guilds';
import { fetchInvite, getInvite } from '../../store/invites';
import { joinGuild } from '../../store/members';
import { getTag, getUser } from '../../store/users';
@ -29,9 +29,13 @@ const InvitePage: React.FunctionComponent<InvitePageProps> = () => {
useEffect(() => {
dispatch(fetchInvite(inviteId));
if (invite) dispatch(fetchEntities([invite.guildId]));
}, []);
useEffect(() => {
if (invite?.guildId)
dispatch(fetchGuild(invite?.guildId));
}, [invite?.guildId]);
const Wrapper: React.FunctionComponent = ({ children }) => (
<PageWrapper pageTitle={`acrd.app | Invite to '${guild?.name}'`}>
<div className="flex items-center absolute justify-center h-screen left-[35%]">

View File

@ -50,7 +50,7 @@ export const uploadGuildIcon = (guildId: string, file: File) => (dispatch) => {
dispatch(uploadFile(file, uploadCallback));
}
export const fetchGuildInvites = (id: string) => (dispatch, getStore: () => Store.AppState) => {
export const fetchGuildInvites = (id: string) => (dispatch) => {
dispatch(api.restCallBegan({
url: `/guilds/${id}/invites`,
headers: getHeaders(),
@ -58,6 +58,18 @@ export const fetchGuildInvites = (id: string) => (dispatch, getStore: () => Stor
}));
}
export const fetchGuild = (id: string) => (dispatch, getStore: () => Store.AppState) => {
const guilds = getStore().entities.guilds;
const isCached = guilds.some(g => g.id === id);
if (isCached) return;
dispatch(api.restCallBegan({
url: `/guilds/${id}`,
headers: getHeaders(),
callback: (guild: Entity.Guild) => dispatch(actions.fetched([guild])),
}));
}
export const deleteGuild = (guildId: string) => (dispatch) => {
dispatch(api.wsCallBegan({
event: 'GUILD_DELETE',