Add recaptcha for login page and forgotten password.
This commit is contained in:
parent
380558eba2
commit
ff6daf58ab
@ -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
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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),
|
||||
|
@ -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 } }),
|
||||
|
2
frontend/env/.env.dev
vendored
2
frontend/env/.env.dev
vendored
@ -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"
|
2
frontend/env/.env.prod
vendored
2
frontend/env/.env.prod
vendored
@ -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"
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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%]">
|
||||
|
@ -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',
|
||||
|
Loading…
x
Reference in New Issue
Block a user