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==