From d728ccd4e05e17665f535e605c19510d8dd2df21 Mon Sep 17 00:00:00 2001 From: Yash Singh Date: Thu, 18 Jun 2026 18:45:43 +0530 Subject: [PATCH] fix: avoid duplicating current user in query preset access list --- .../payload/src/query-presets/constraints.ts | 7 +- .../query-presets/duplicate-users.int.spec.ts | 98 +++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 test/query-presets/duplicate-users.int.spec.ts diff --git a/packages/payload/src/query-presets/constraints.ts b/packages/payload/src/query-presets/constraints.ts index b932ce8499d..21cbfa5a2b9 100644 --- a/packages/payload/src/query-presets/constraints.ts +++ b/packages/payload/src/query-presets/constraints.ts @@ -82,7 +82,12 @@ export const getConstraints = (config: Config): Field => ({ data?.access?.[constraintOperation]?.constraint === 'specificUsers' && req.user ) { - return [...(data?.access?.[constraintOperation]?.users || []), req.user.id] + const users = data?.access?.[constraintOperation]?.users || [] + const isUserPresent = users.some( + (user) => (typeof user === 'object' ? user?.id : user) === req.user!.id, + ) + + return isUserPresent ? users : [...users, req.user.id] } }, ], diff --git a/test/query-presets/duplicate-users.int.spec.ts b/test/query-presets/duplicate-users.int.spec.ts new file mode 100644 index 00000000000..51a9abb7b17 --- /dev/null +++ b/test/query-presets/duplicate-users.int.spec.ts @@ -0,0 +1,98 @@ +import type { Payload, User } from 'payload' + +import path from 'path' +import { fileURLToPath } from 'url' +import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest' + +import { initPayloadInt } from '../__helpers/shared/initPayloadInt.js' +import { devUser, regularUser } from '../credentials.js' + +const queryPresetsCollectionSlug = 'payload-query-presets' + +const filename = fileURLToPath(import.meta.url) +const dirname = path.dirname(filename) + +const countUser = (users: unknown, id: User['id']) => + (Array.isArray(users) ? users : []).filter( + (user) => (typeof user === 'object' && user ? user.id : user) === id, + ).length + +describe('Query Presets - duplicate users', () => { + let payload: Payload + let adminUser: User + let editorUser: User + + const createdIDs: (number | string)[] = [] + + beforeAll(async () => { + // @ts-expect-error: initPayloadInt does not have a proper type definition + ;({ payload } = await initPayloadInt(dirname)) + + adminUser = await payload + .login({ + collection: 'users', + data: { + email: devUser.email, + password: devUser.password, + }, + }) + ?.then((result) => result.user) + + editorUser = await payload + .login({ + collection: 'users', + data: { + email: regularUser.email, + password: regularUser.password, + }, + }) + ?.then((result) => result.user) + }) + + afterEach(async () => { + for (const id of createdIDs) { + await payload.delete({ id, collection: queryPresetsCollectionSlug }) + } + createdIDs.length = 0 + }) + + afterAll(async () => { + await payload.destroy() + }) + + it('should not duplicate the current user in the access list when re-saving', async () => { + const preset = await payload.create({ + collection: queryPresetsCollectionSlug, + data: { + access: { + read: { + constraint: 'specificUsers', + users: [editorUser.id], + }, + }, + relatedCollection: 'pages', + title: 'No Duplicate Users', + }, + overrideAccess: false, + user: adminUser, + }) + + createdIDs.push(preset.id) + + expect(countUser(preset.access?.read?.users, adminUser.id)).toBe(1) + + const updatedPreset = await payload.update({ + id: preset.id, + collection: queryPresetsCollectionSlug, + data: { + title: 'No Duplicate Users (Updated)', + }, + overrideAccess: false, + user: adminUser, + }) + + // re-saving must not append the current user again + expect(countUser(updatedPreset.access?.read?.users, adminUser.id)).toBe(1) + expect(countUser(updatedPreset.access?.read?.users, editorUser.id)).toBe(1) + }) +})