Merge branch 'master' of github.com:keen-studio/payload
This commit is contained in:
23
src/auth/graphql/resolvers/verifyEmail.js
Normal file
23
src/auth/graphql/resolvers/verifyEmail.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
function verifyEmail(collection) {
|
||||
async function resolver(_, args, context) {
|
||||
if (args.locale) context.req.locale = args.locale;
|
||||
if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale;
|
||||
|
||||
const options = {
|
||||
collection,
|
||||
token: args.token,
|
||||
req: context.req,
|
||||
res: context.res,
|
||||
api: 'GraphQL',
|
||||
};
|
||||
|
||||
const success = await this.operations.collections.auth.verifyEmail(options);
|
||||
return success;
|
||||
}
|
||||
|
||||
const verifyEmailResolver = resolver.bind(this);
|
||||
return verifyEmailResolver;
|
||||
}
|
||||
|
||||
module.exports = verifyEmail;
|
||||
@@ -35,9 +35,15 @@ async function login(args) {
|
||||
if (!userDoc || (args.collection.config.auth.emailVerification && !userDoc._verified)) {
|
||||
throw new AuthenticationError();
|
||||
}
|
||||
if (userDoc && userDoc.isLocked) {
|
||||
throw new AuthenticationError();
|
||||
}
|
||||
const authResult = await userDoc.authenticate(password);
|
||||
|
||||
if (!authResult.user) {
|
||||
if (authResult.user) {
|
||||
await authResult.user.resetLoginAttempts();
|
||||
} else {
|
||||
await userDoc.incLoginAttempts();
|
||||
throw new AuthenticationError();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,17 +10,17 @@ async function verifyEmail(args) {
|
||||
// 2. Perform password reset
|
||||
// /////////////////////////////////////
|
||||
|
||||
// TODO: How do we know which collection this is?
|
||||
const user = await args.collection.Model.findOne({
|
||||
_verificationToken: args.token,
|
||||
});
|
||||
|
||||
if (!user) throw new APIError('User not found.', httpStatus.BAD_REQUEST);
|
||||
if (user && user._verified === true) throw new APIError('Already activated', httpStatus.ACCEPTED);
|
||||
|
||||
user._verified = true;
|
||||
user._verificationToken = null;
|
||||
|
||||
await user.save();
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = verifyEmail;
|
||||
|
||||
@@ -11,6 +11,7 @@ import ForgotPassword from './views/ForgotPassword';
|
||||
import Login from './views/Login';
|
||||
import Logout from './views/Logout';
|
||||
import NotFound from './views/NotFound';
|
||||
import Verify from './views/Verify';
|
||||
import CreateFirstUser from './views/CreateFirstUser';
|
||||
import Edit from './views/collections/Edit';
|
||||
import EditGlobal from './views/Global';
|
||||
@@ -77,6 +78,21 @@ const Routes = () => {
|
||||
<ResetPassword />
|
||||
</Route>
|
||||
|
||||
{collections.map((collection) => {
|
||||
if (collection?.auth?.emailVerification) {
|
||||
return (
|
||||
<Route
|
||||
key={`${collection.slug}-verify`}
|
||||
path={`${match.url}/${collection.slug}/verify/:token`}
|
||||
exact
|
||||
>
|
||||
<Verify />
|
||||
</Route>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
|
||||
<Route
|
||||
render={() => {
|
||||
if (user) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import Meta from '../../utilities/Meta';
|
||||
|
||||
const NotFound = () => {
|
||||
const { setStepNav } = useStepNav();
|
||||
const { routes: { admin } } = useConfig;
|
||||
const { routes: { admin } } = useConfig();
|
||||
|
||||
useEffect(() => {
|
||||
setStepNav([{
|
||||
|
||||
68
src/client/components/views/Verify/index.js
Normal file
68
src/client/components/views/Verify/index.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams, useLocation } from 'react-router-dom';
|
||||
import Logo from '../../graphics/Logo';
|
||||
import MinimalTemplate from '../../templates/Minimal';
|
||||
import Button from '../../elements/Button';
|
||||
import Meta from '../../utilities/Meta';
|
||||
|
||||
import { useConfig } from '../../providers/Config';
|
||||
import { useAuthentication } from '../../providers/Authentication';
|
||||
import Login from '../Login';
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'verify';
|
||||
|
||||
const Verify = () => {
|
||||
const { user } = useAuthentication();
|
||||
const { token } = useParams();
|
||||
const { pathname } = useLocation();
|
||||
const { serverURL, routes: { admin: adminRoute }, admin: { user: adminUser } } = useConfig();
|
||||
|
||||
const collectionToVerify = pathname.split('/')?.[2];
|
||||
const isAdminUser = collectionToVerify === adminUser;
|
||||
const [verifyResult, setVerifyResult] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function verifyToken() {
|
||||
const result = await fetch(`${serverURL}/api/${collectionToVerify}/verify/${token}`, { method: 'POST' });
|
||||
setVerifyResult(result);
|
||||
}
|
||||
verifyToken();
|
||||
}, [setVerifyResult, collectionToVerify, pathname, serverURL, token]);
|
||||
|
||||
if (user) {
|
||||
return <Login />;
|
||||
}
|
||||
|
||||
const getText = () => {
|
||||
if (verifyResult?.status === 200) return 'Verified Successfully';
|
||||
if (verifyResult?.status === 202) return 'Already Activated';
|
||||
return 'Unable To Verify';
|
||||
};
|
||||
|
||||
return (
|
||||
<MinimalTemplate className={baseClass}>
|
||||
<Meta
|
||||
title="Verify"
|
||||
description="Verify user"
|
||||
keywords="Verify, Payload, CMS"
|
||||
/>
|
||||
<div className={`${baseClass}__brand`}>
|
||||
<Logo />
|
||||
</div>
|
||||
<h2>
|
||||
{getText()}
|
||||
</h2>
|
||||
{isAdminUser && verifyResult?.status === 200 && (
|
||||
<Button
|
||||
el="link"
|
||||
buttonStyle="secondary"
|
||||
to={`${adminRoute}/login`}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
)}
|
||||
</MinimalTemplate>
|
||||
);
|
||||
};
|
||||
export default Verify;
|
||||
16
src/client/components/views/Verify/index.scss
Normal file
16
src/client/components/views/Verify/index.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
@import '../../../scss/styles';
|
||||
|
||||
.verify {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
flex-wrap: wrap;
|
||||
min-height: 100vh;
|
||||
|
||||
&__brand {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin-bottom: base(2);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ function registerCollections() {
|
||||
} = this.graphQL.resolvers.collections;
|
||||
|
||||
const {
|
||||
login, logout, me, init, refresh, forgotPassword, resetPassword,
|
||||
login, logout, me, init, refresh, forgotPassword, resetPassword, verifyEmail,
|
||||
} = this.graphQL.resolvers.collections.auth;
|
||||
|
||||
Object.keys(this.collections).forEach((slug) => {
|
||||
@@ -266,6 +266,14 @@ function registerCollections() {
|
||||
resolve: resetPassword(collection),
|
||||
};
|
||||
|
||||
this.Mutation.fields[`verifyEmail${singularLabel}`] = {
|
||||
type: GraphQLBoolean,
|
||||
args: {
|
||||
token: { type: GraphQLString },
|
||||
},
|
||||
resolve: verifyEmail(collection),
|
||||
};
|
||||
|
||||
this.Mutation.fields[`refreshToken${singularLabel}`] = {
|
||||
type: new GraphQLObjectType({
|
||||
name: formatName(`${slug}Refreshed${singularLabel}`),
|
||||
|
||||
@@ -20,7 +20,46 @@ function registerCollections() {
|
||||
const schema = buildSchema(formattedCollection, this.config);
|
||||
|
||||
if (collection.auth) {
|
||||
schema.plugin(passportLocalMongoose, { usernameField: 'email' });
|
||||
schema.plugin(passportLocalMongoose, {
|
||||
usernameField: 'email',
|
||||
});
|
||||
|
||||
// Check if collection is the admin user set in config
|
||||
if (collection.slug === this.config.admin.user) {
|
||||
schema.add({ loginAttempts: { type: Number, hide: true, default: 0 } });
|
||||
schema.add({ lockUntil: { type: Date, hide: true } });
|
||||
|
||||
schema.virtual('isLocked').get(() => !!(this.lockUntil && this.lockUntil > Date.now()));
|
||||
|
||||
const { maxLoginAttempts, lockTime } = this.config.admin;
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
schema.methods.incLoginAttempts = function (cb) {
|
||||
// Expired lock, restart count at 1
|
||||
if (this.lockUntil && this.lockUntil < Date.now()) {
|
||||
return this.updateOne({
|
||||
$set: { loginAttempts: 1 },
|
||||
$unset: { lockUntil: 1 },
|
||||
}, cb);
|
||||
}
|
||||
|
||||
const updates = { $inc: { loginAttempts: 1 } };
|
||||
// Lock the account if at max attempts and not already locked
|
||||
if (this.loginAttempts + 1 >= maxLoginAttempts && !this.isLocked) {
|
||||
updates.$set = { lockUntil: Date.now() + lockTime };
|
||||
}
|
||||
return this.updateOne(updates, cb);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
schema.methods.resetLoginAttempts = function (cb) {
|
||||
return this.updateOne({
|
||||
$set: { loginAttempts: 0 },
|
||||
$unset: { lockUntil: 1 },
|
||||
}, cb);
|
||||
};
|
||||
}
|
||||
|
||||
schema.path('hash').options.hide = true;
|
||||
schema.path('salt').options.hide = true;
|
||||
if (collection.auth.emailVerification) {
|
||||
|
||||
@@ -3,6 +3,7 @@ require('isomorphic-fetch');
|
||||
|
||||
const express = require('express');
|
||||
const graphQLPlayground = require('graphql-playground-middleware-express').default;
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const logger = require('./utilities/logger')();
|
||||
const bindOperations = require('./init/bindOperations');
|
||||
const bindRequestHandlers = require('./init/bindRequestHandlers');
|
||||
@@ -98,8 +99,12 @@ class Payload {
|
||||
},
|
||||
}));
|
||||
|
||||
// Bind router to API
|
||||
this.express.use(this.config.routes.api, this.router);
|
||||
const apiLimiter = rateLimit({
|
||||
windowMs: this.config.rateLimit.window,
|
||||
max: this.config.rateLimit.max,
|
||||
});
|
||||
// Bind router to API and add rate limiter
|
||||
this.express.use(this.config.routes.api, apiLimiter, this.router);
|
||||
|
||||
// Enable static routes for all collections permitting upload
|
||||
this.initStatic();
|
||||
|
||||
@@ -6,7 +6,7 @@ const logout = require('../auth/graphql/resolvers/logout');
|
||||
const me = require('../auth/graphql/resolvers/me');
|
||||
const refresh = require('../auth/graphql/resolvers/refresh');
|
||||
const resetPassword = require('../auth/graphql/resolvers/resetPassword');
|
||||
// const verifyEmail = require('../auth/resolvers/verifyEmail');
|
||||
const verifyEmail = require('../auth/graphql/resolvers/verifyEmail');
|
||||
|
||||
const create = require('../collections/graphql/resolvers/create');
|
||||
const find = require('../collections/graphql/resolvers/find');
|
||||
@@ -37,7 +37,7 @@ function bindResolvers(ctx) {
|
||||
me: me.bind(ctx),
|
||||
refresh: refresh.bind(ctx),
|
||||
resetPassword: resetPassword.bind(ctx),
|
||||
// verifyEmail: verifyEmail.bind(ctx),
|
||||
verifyEmail: verifyEmail.bind(ctx),
|
||||
},
|
||||
},
|
||||
globals: {
|
||||
|
||||
@@ -29,6 +29,9 @@ const sanitizeConfig = (config) => {
|
||||
sanitizedConfig.collections.push(defaultUser);
|
||||
}
|
||||
|
||||
sanitizedConfig.maxLoginAttempts = sanitizedConfig.maxLoginAttempts || 3;
|
||||
sanitizedConfig.lockTime = sanitizedConfig.lockTime || 600000; // 10 minutes
|
||||
|
||||
sanitizedConfig.email = config.email || {};
|
||||
sanitizedConfig.email.fromName = sanitizedConfig.email.fromName || 'Payload';
|
||||
sanitizedConfig.email.fromAddress = sanitizedConfig.email.fromName || 'hello@payloadcms.com';
|
||||
@@ -44,6 +47,10 @@ const sanitizeConfig = (config) => {
|
||||
graphQLPlayground: (config.routes && config.routes.graphQLPlayground) ? config.routes.graphQLPlayground : '/graphql-playground',
|
||||
};
|
||||
|
||||
sanitizedConfig.rateLimit = config.rateLimit || {};
|
||||
sanitizedConfig.rateLimit.window = sanitizedConfig.rateLimit.window || 15 * 60 * 100; // 15min default
|
||||
sanitizedConfig.rateLimit.max = sanitizedConfig.rateLimit.max || 100;
|
||||
|
||||
sanitizedConfig.components = { ...(config.components || {}) };
|
||||
sanitizedConfig.hooks = { ...(config.hooks || {}) };
|
||||
sanitizedConfig.admin = { ...(config.admin || {}) };
|
||||
|
||||
Reference in New Issue
Block a user