From 09d793926dbb642bbcb6ab975735d069df355a8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20S=C3=B6derling?= Date: Mon, 24 Oct 2022 18:05:12 +0200 Subject: [PATCH] feat: added beforeLogin hook (#1289) --- docs/hooks/collections.mdx | 5 +-- src/auth/operations/login.ts | 11 ++++++- src/collections/config/types.ts | 5 +-- test/credentials.ts | 5 +++ test/hooks/collections/Users/index.ts | 46 +++++++++++++++++++++++++++ test/hooks/config.ts | 3 ++ test/hooks/int.spec.ts | 19 +++++++++++ 7 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 test/hooks/collections/Users/index.ts diff --git a/docs/hooks/collections.mdx b/docs/hooks/collections.mdx index 042ad97a0..b9bc580c9 100644 --- a/docs/hooks/collections.mdx +++ b/docs/hooks/collections.mdx @@ -190,7 +190,7 @@ const afterDeleteHook: CollectionAfterDeleteHook = async ({ ### beforeLogin -For auth-enabled Collections, this hook runs after successful `login` operations. You can optionally modify the user that is returned. +For auth-enabled Collections, this hook runs during `login` operations where a user with the provided credentials exist, but before a token is generated and added too the response. You can optionally modify the user that is returned, or throw an error in order to deny the login operation. ```ts import { CollectionBeforeLoginHook } from 'payload/types'; @@ -198,7 +198,6 @@ import { CollectionBeforeLoginHook } from 'payload/types'; const beforeLoginHook: CollectionBeforeLoginHook = async ({ req, // full express request user, // user being logged in - token, // user token }) => { return user; } @@ -213,6 +212,8 @@ import { CollectionAfterLoginHook } from 'payload/types'; const afterLoginHook: CollectionAfterLoginHook = async ({ req, // full express request + user, // user that was logged in + token, // user token }) => {...} ``` diff --git a/src/auth/operations/login.ts b/src/auth/operations/login.ts index fcc8879e1..ccc6ab6ef 100644 --- a/src/auth/operations/login.ts +++ b/src/auth/operations/login.ts @@ -133,6 +133,15 @@ async function login(incomingArgs: Arguments): Promise { collection: collectionConfig.slug, }); + await collectionConfig.hooks.beforeLogin.reduce(async (priorHook, hook) => { + await priorHook; + + user = (await hook({ + user, + req: args.req, + })) || user; + }, Promise.resolve()); + const token = jwt.sign( fieldsToSign, secret, @@ -166,7 +175,7 @@ async function login(incomingArgs: Arguments): Promise { await priorHook; user = await hook({ - doc: user, + user, req: args.req, token, }) || user; diff --git a/src/collections/config/types.ts b/src/collections/config/types.ts index d75722d5e..3341c996d 100644 --- a/src/collections/config/types.ts +++ b/src/collections/config/types.ts @@ -112,13 +112,14 @@ export type AfterDeleteHook = (args: { export type AfterErrorHook = (err: Error, res: unknown) => { response: any, status: number } | void; -export type BeforeLoginHook = (args: { +export type BeforeLoginHook = (args: { req: PayloadRequest; + user: T }) => any; export type AfterLoginHook = (args: { req: PayloadRequest; - doc: T; + user: T; token: string; }) => any; diff --git a/test/credentials.ts b/test/credentials.ts index c68708538..bddad85f8 100644 --- a/test/credentials.ts +++ b/test/credentials.ts @@ -3,3 +3,8 @@ export const devUser = { password: 'test', roles: ['admin'], }; +export const regularUser = { + email: 'user@payloadcms.com', + password: 'test2', + roles: ['user'], +}; diff --git a/test/hooks/collections/Users/index.ts b/test/hooks/collections/Users/index.ts new file mode 100644 index 000000000..32dfe35cb --- /dev/null +++ b/test/hooks/collections/Users/index.ts @@ -0,0 +1,46 @@ +import { Payload } from '../../../../src'; +import { BeforeLoginHook, CollectionConfig } from '../../../../src/collections/config/types'; +import { AuthenticationError } from '../../../../src/errors'; +import { devUser, regularUser } from '../../../credentials'; + +const beforeLoginHook: BeforeLoginHook = ({ user }) => { + const isAdmin = user.roles.includes('admin') ? user : undefined; + if (!isAdmin) { + throw new AuthenticationError(); + } + return user; +}; + +export const seedHooksUsers = async (payload: Payload) => { + await payload.create({ + collection: hooksUsersSlug, + data: devUser, + }); + await payload.create({ + collection: hooksUsersSlug, + data: regularUser, + }); +}; + +export const hooksUsersSlug = 'hooks-users'; +const Users: CollectionConfig = { + slug: hooksUsersSlug, + auth: true, + fields: [ + { + name: 'roles', + label: 'Role', + type: 'select', + options: ['admin', 'user'], + defaultValue: 'user', + required: true, + saveToJWT: true, + hasMany: true, + }, + ], + hooks: { + beforeLogin: [beforeLoginHook], + }, +}; + +export default Users; diff --git a/test/hooks/config.ts b/test/hooks/config.ts index 3d334a53f..92b99737a 100644 --- a/test/hooks/config.ts +++ b/test/hooks/config.ts @@ -3,6 +3,7 @@ import TransformHooks from './collections/Transform'; import Hooks, { hooksSlug } from './collections/Hook'; import NestedAfterReadHooks from './collections/NestedAfterReadHooks'; import Relations from './collections/Relations'; +import Users, { seedHooksUsers } from './collections/Users'; export default buildConfig({ collections: [ @@ -10,8 +11,10 @@ export default buildConfig({ Hooks, NestedAfterReadHooks, Relations, + Users, ], onInit: async (payload) => { + await seedHooksUsers(payload); await payload.create({ collection: hooksSlug, data: { diff --git a/test/hooks/int.spec.ts b/test/hooks/int.spec.ts index 5a28082e6..4d0f12070 100644 --- a/test/hooks/int.spec.ts +++ b/test/hooks/int.spec.ts @@ -8,6 +8,9 @@ import { hooksSlug } from './collections/Hook'; import { generatedAfterReadText, nestedAfterReadHooksSlug } from './collections/NestedAfterReadHooks'; import { relationsSlug } from './collections/Relations'; import type { NestedAfterReadHook } from './payload-types'; +import { hooksUsersSlug } from './collections/Users'; +import { devUser, regularUser } from '../credentials'; +import { AuthenticationError } from '../../src/errors'; let client: RESTClient; @@ -117,4 +120,20 @@ describe('Hooks', () => { expect(retrievedDoc.group.subGroup.shouldPopulate.title).toEqual(relation.title); }); }); + describe('auth collection hooks', () => { + it('allow admin login', async () => { + const { user } = await payload.login({ + collection: hooksUsersSlug, + data: { + email: devUser.email, + password: devUser.password, + }, + }); + expect(user).toBeDefined(); + }); + + it('deny user login', async () => { + await expect(() => payload.login({ collection: hooksUsersSlug, data: { email: regularUser.email, password: regularUser.password } })).rejects.toThrow(AuthenticationError); + }); + }); });