From 4055908bc885ec1b2d69817a9937e4591d099fa1 Mon Sep 17 00:00:00 2001 From: Dan Ribbens Date: Sat, 9 Jul 2022 19:29:00 -0400 Subject: [PATCH] feat: add afterMe afterLogout and afterRefresh --- docs/hooks/collections.mdx | 35 +++++++++++++++++--- src/admin/components/Routes.tsx | 6 ++-- src/auth/operations/logout.ts | 20 +++++++++--- src/auth/operations/me.ts | 24 ++++++++++---- src/auth/operations/refresh.ts | 17 +++++++++- src/collections/buildEndpoints.ts | 20 ++++++------ src/collections/config/defaults.ts | 3 ++ src/collections/config/schema.ts | 3 ++ src/collections/config/types.ts | 20 ++++++++++++ src/collections/graphql/init.ts | 52 +++++++++++++++--------------- yarn.lock | 4 +-- 11 files changed, 147 insertions(+), 57 deletions(-) diff --git a/docs/hooks/collections.mdx b/docs/hooks/collections.mdx index bb35c9b6f2..10a6053c07 100644 --- a/docs/hooks/collections.mdx +++ b/docs/hooks/collections.mdx @@ -19,7 +19,9 @@ Collections feature the ability to define the following hooks: Additionally, `auth`-enabled collections feature the following hooks: +- [beforeLogin](#beforelogin) - [afterLogin](#afterlogin) +- [afterLogout](#afterlogout) - [afterForgotPassword](#afterforgotpassword) ## Config @@ -45,7 +47,9 @@ module.exports = { afterDelete: [(args) => {...}], // Auth-enabled hooks + beforeLogin: [(args) => {...}], afterLogin: [(args) => {...}], + afterLogout: [(args) => {...}], afterForgotPassword: [(args) => {...}], } } @@ -162,6 +166,20 @@ const afterDeleteHook = async ({ }) => {...} ``` +### beforeLogin + +For auth-enabled Collections, this hook runs after successful `login` operations. You can optionally modify the user that is returned. + +```js +const beforeLoginHook = async ({ + req, // full express request + user, // user being logged in + token, // user token +}) => { + return user; +} +``` + ### afterLogin For auth-enabled Collections, this hook runs after successful `login` operations. You can optionally modify the user that is returned. @@ -169,11 +187,17 @@ For auth-enabled Collections, this hook runs after successful `login` operations ```js const afterLoginHook = async ({ req, // full express request - user, // user being logged in - token, // user token -}) => { - return user; -} +}) => {...} +``` + +### afterLogout + +For auth-enabled Collections, this hook runs after before `logout` operations. + +```js +const afterLoginHook = async ({ + req, // full express request +}) => {...} ``` ### afterForgotPassword @@ -206,6 +230,7 @@ import type { CollectionAfterDeleteHook, CollectionBeforeLoginHook, CollectionAfterLoginHook, + CollectionAfterLogoutHook, CollectionAfterForgotPasswordHook, } from 'payload/types'; diff --git a/src/admin/components/Routes.tsx b/src/admin/components/Routes.tsx index 5d0e5467da..f642aca114 100644 --- a/src/admin/components/Routes.tsx +++ b/src/admin/components/Routes.tsx @@ -103,11 +103,11 @@ const Routes = () => { + + + {!userCollection.auth.disableLocalStrategy && ( - - - diff --git a/src/auth/operations/logout.ts b/src/auth/operations/logout.ts index f8a83b16a1..b417d05930 100644 --- a/src/auth/operations/logout.ts +++ b/src/auth/operations/logout.ts @@ -10,21 +10,25 @@ export type Arguments = { collection: Collection } -async function logout(args: Arguments): Promise { +async function logout(incomingArgs: Arguments): Promise { + let args = incomingArgs; const { res, req: { payload: { config, }, + user, }, + req, collection: { config: collectionConfig, }, - } = args; + collection, + } = incomingArgs; - if (!args.req.user) throw new APIError('No User', httpStatus.BAD_REQUEST); - if (args.req.user.collection !== collectionConfig.slug) throw new APIError('Incorrect collection', httpStatus.FORBIDDEN); + if (!user) throw new APIError('No User', httpStatus.BAD_REQUEST); + if (user.collection !== collectionConfig.slug) throw new APIError('Incorrect collection', httpStatus.FORBIDDEN); const cookieOptions = { path: '/', @@ -36,6 +40,14 @@ async function logout(args: Arguments): Promise { if (collectionConfig.auth.cookies.domain) cookieOptions.domain = collectionConfig.auth.cookies.domain; + await collection.config.hooks.afterLogout.reduce(async (priorHook, hook) => { + await priorHook; + + args = (await hook({ + req, + })) || args; + }, Promise.resolve()); + res.clearCookie(`${config.cookiePrefix}-token`, cookieOptions); return 'Logged out successfully.'; diff --git a/src/auth/operations/me.ts b/src/auth/operations/me.ts index c822d71d5a..b1c271729f 100644 --- a/src/auth/operations/me.ts +++ b/src/auth/operations/me.ts @@ -21,6 +21,9 @@ async function me({ collection, }: Arguments): Promise { const extractJWT = getExtractJWT(req.payload.config); + let response: Result = { + user: null, + }; if (req.user) { const user = { ...req.user }; @@ -33,7 +36,7 @@ async function me({ delete user.collection; - const response: Result = { + response = { user, collection: req.user.collection, }; @@ -45,13 +48,22 @@ async function me({ const decoded = jwt.decode(token) as jwt.JwtPayload; if (decoded) response.exp = decoded.exp; } - - return response; } - return { - user: null, - }; + // ///////////////////////////////////// + // After Me - Collection + // ///////////////////////////////////// + + await collection.config.hooks.afterMe.reduce(async (priorHook, hook) => { + await priorHook; + + response = await hook({ + req, + response, + }) || response; + }, Promise.resolve()); + + return response; } export default me; diff --git a/src/auth/operations/refresh.ts b/src/auth/operations/refresh.ts index a9dc101e8b..5ca6bf3679 100644 --- a/src/auth/operations/refresh.ts +++ b/src/auth/operations/refresh.ts @@ -61,6 +61,7 @@ async function refresh(incomingArgs: Arguments): Promise { delete payload.iat; delete payload.exp; const refreshedToken = jwt.sign(payload, secret, opts); + const exp = (jwt.decode(refreshedToken) as Record).exp as number; if (args.res) { const cookieOptions = { @@ -77,13 +78,27 @@ async function refresh(incomingArgs: Arguments): Promise { args.res.cookie(`${config.cookiePrefix}-token`, refreshedToken, cookieOptions); } + // ///////////////////////////////////// + // After Refresh - Collection + // ///////////////////////////////////// + + await collectionConfig.hooks.afterRefresh.reduce(async (priorHook, hook) => { + await priorHook; + + args = (await hook({ + req: args.req, + exp, + token: refreshedToken, + })) || args; + }, Promise.resolve()); + // ///////////////////////////////////// // Return results // ///////////////////////////////////// return { refreshedToken, - exp: (jwt.decode(refreshedToken) as Record).exp as number, + exp, user: payload, }; } diff --git a/src/collections/buildEndpoints.ts b/src/collections/buildEndpoints.ts index 2f0352e567..2d03fd146a 100644 --- a/src/collections/buildEndpoints.ts +++ b/src/collections/buildEndpoints.ts @@ -46,16 +46,6 @@ const buildEndpoints = (collection: SanitizedCollectionConfig): Endpoint[] => { method: 'post', handler: loginHandler, }, - { - path: '/logout', - method: 'post', - handler: logoutHandler, - }, - { - path: '/refresh-token', - method: 'post', - handler: refreshHandler, - }, { path: '/first-register', method: 'post', @@ -85,6 +75,16 @@ const buildEndpoints = (collection: SanitizedCollectionConfig): Endpoint[] => { method: 'get', handler: meHandler, }, + { + path: '/logout', + method: 'post', + handler: logoutHandler, + }, + { + path: '/refresh-token', + method: 'post', + handler: refreshHandler, + }, ]); } diff --git a/src/collections/config/defaults.ts b/src/collections/config/defaults.ts index 6895de33de..96625710b3 100644 --- a/src/collections/config/defaults.ts +++ b/src/collections/config/defaults.ts @@ -30,6 +30,9 @@ export const defaults = { afterDelete: [], beforeLogin: [], afterLogin: [], + afterLogout: [], + afterRefresh: [], + afterMe: [], afterForgotPassword: [], }, endpoints: [], diff --git a/src/collections/config/schema.ts b/src/collections/config/schema.ts index fc7958f778..3175e5e669 100644 --- a/src/collections/config/schema.ts +++ b/src/collections/config/schema.ts @@ -51,6 +51,9 @@ const collectionSchema = joi.object().keys({ afterDelete: joi.array().items(joi.func()), beforeLogin: joi.array().items(joi.func()), afterLogin: joi.array().items(joi.func()), + afterLogout: joi.array().items(joi.func()), + afterMe: joi.array().items(joi.func()), + afterRefresh: joi.array().items(joi.func()), afterForgotPassword: joi.array().items(joi.func()), }), endpoints: joi.array().items(joi.object({ diff --git a/src/collections/config/types.ts b/src/collections/config/types.ts index 6ce1982010..22e2645cfd 100644 --- a/src/collections/config/types.ts +++ b/src/collections/config/types.ts @@ -33,6 +33,8 @@ export type HookOperationType = | 'delete' | 'refresh' | 'login' +| 'logout' +| 'me' | 'forgotPassword'; type CreateOrUpdateOperation = Extract; @@ -120,6 +122,21 @@ export type AfterLoginHook = (args: { token: string; }) => any; +export type AfterLogoutHook = (args: { + req: PayloadRequest; +}) => any; + +export type AfterMeHook = (args: { + req: PayloadRequest; + response: unknown; +}) => any; + +export type AfterRefreshHook = (args: { + req: PayloadRequest; + token: string; + exp: number; +}) => any; + export type AfterForgotPasswordHook = (args: { args?: any; }) => any; @@ -191,6 +208,9 @@ export type CollectionConfig = { afterError?: AfterErrorHook; beforeLogin?: BeforeLoginHook[]; afterLogin?: AfterLoginHook[]; + afterLogout?: AfterLogoutHook[]; + afterMe?: AfterMeHook[]; + afterRefresh?: AfterRefreshHook[]; afterForgotPassword?: AfterForgotPasswordHook[]; }; /** diff --git a/src/collections/graphql/init.ts b/src/collections/graphql/init.ts index e5cbdebb33..da44b549c8 100644 --- a/src/collections/graphql/init.ts +++ b/src/collections/graphql/init.ts @@ -307,6 +307,32 @@ function initCollectionsGraphQL(payload: Payload): void { resolve: init(collection), }; + payload.Mutation.fields[`refreshToken${singularLabel}`] = { + type: new GraphQLObjectType({ + name: formatName(`${slug}Refreshed${singularLabel}`), + fields: { + user: { + type: collection.graphQL.JWT, + }, + refreshedToken: { + type: GraphQLString, + }, + exp: { + type: GraphQLInt, + }, + }, + }), + args: { + token: { type: GraphQLString }, + }, + resolve: refresh(collection), + }; + + payload.Mutation.fields[`logout${singularLabel}`] = { + type: GraphQLString, + resolve: logout(collection), + }; + if (!collection.config.auth.disableLocalStrategy) { if (collection.config.auth.maxLoginAttempts > 0) { payload.Mutation.fields[`unlock${singularLabel}`] = { @@ -340,11 +366,6 @@ function initCollectionsGraphQL(payload: Payload): void { resolve: login(collection), }; - payload.Mutation.fields[`logout${singularLabel}`] = { - type: GraphQLString, - resolve: logout(collection), - }; - payload.Mutation.fields[`forgotPassword${singularLabel}`] = { type: new GraphQLNonNull(GraphQLBoolean), args: { @@ -377,27 +398,6 @@ function initCollectionsGraphQL(payload: Payload): void { }, resolve: verifyEmail(collection), }; - - payload.Mutation.fields[`refreshToken${singularLabel}`] = { - type: new GraphQLObjectType({ - name: formatName(`${slug}Refreshed${singularLabel}`), - fields: { - user: { - type: collection.graphQL.JWT, - }, - refreshedToken: { - type: GraphQLString, - }, - exp: { - type: GraphQLInt, - }, - }, - }), - args: { - token: { type: GraphQLString }, - }, - resolve: refresh(collection), - }; } } }); diff --git a/yarn.lock b/yarn.lock index 31984bd410..d2071e3209 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9669,7 +9669,7 @@ passport-anonymous@^1.0.1: dependencies: passport-strategy "1.x.x" -passport-headerapikey@^1.2.2: +passport-headerapikey@^1.2.1: version "1.2.2" resolved "https://registry.npmjs.org/passport-headerapikey/-/passport-headerapikey-1.2.2.tgz#b71960523999c9864151b8535c919e3ff5ba75ce" integrity sha512-4BvVJRrWsNJPrd3UoZfcnnl4zvUWYKEtfYkoDsaOKBsrWHYmzTApCjs7qUbncOLexE9ul0IRiYBFfBG0y9IVQA== @@ -9685,7 +9685,7 @@ passport-jwt@^4.0.0: jsonwebtoken "^8.2.0" passport-strategy "^1.0.0" -passport-local-mongoose@^7.1.2: +passport-local-mongoose@^7.0.0: version "7.1.2" resolved "https://registry.npmjs.org/passport-local-mongoose/-/passport-local-mongoose-7.1.2.tgz#0a89876ef8a8e18787e59a39740e61c5653eb25e" integrity sha512-hNLIKi/6IhElr/PhOze8wLDh7T4+ZYhc8GFWYApLgG7FrjI55tuGZELPtsUBqODz77OwlUUf+ngPgHN09zxGLg==