fix(ui): undefined permissions passed in create-first-user view (#13671)

### What?

In the create-first-user view, fields like `richText` were being marked
as `readOnly: true` because they had no permissions entry in the
permissions map.

### Why?

The view was passing an incomplete `docPermissions` object. 

When a field had no entry in `docPermissions.fields`, `renderField`
received `permissions: undefined`, which was interpreted as denied
access.

This caused fields (notably `richText`) to default to read-only even
though the user should have full access when creating the first user.

### How?

- Updated the create-first-user view to always pass a complete
`docPermissions` object.
- Default all fields in the user collection to `{ create: true, read:
true, update: true }`.
- Ensures every field is explicitly granted full access during the
first-user flow.
- Keeps the `renderField` logic unchanged and aligned with Payload’s
permission model.

Fixes #13612 

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211211792037939
This commit is contained in:
Patrik
2025-09-03 17:16:49 -04:00
committed by GitHub
parent d9e183242c
commit 5146fc865f
4 changed files with 69 additions and 10 deletions

View File

@@ -1,11 +1,14 @@
import type { AdminViewServerProps } from 'payload' import type {
AdminViewServerProps,
SanitizedDocumentPermissions,
SanitizedFieldsPermissions,
} from 'payload'
import { buildFormState } from '@payloadcms/ui/utilities/buildFormState' import { buildFormState } from '@payloadcms/ui/utilities/buildFormState'
import React from 'react' import React from 'react'
import { getDocPreferences } from '../Document/getDocPreferences.js' import { getDocPreferences } from '../Document/getDocPreferences.js'
import { getDocumentData } from '../Document/getDocumentData.js' import { getDocumentData } from '../Document/getDocumentData.js'
import { getDocumentPermissions } from '../Document/getDocumentPermissions.js'
import { CreateFirstUserClient } from './index.client.js' import { CreateFirstUserClient } from './index.client.js'
import './index.scss' import './index.scss'
@@ -43,18 +46,27 @@ export async function CreateFirstUserView({ initPageResult }: AdminViewServerPro
user: req.user, user: req.user,
}) })
// Get permissions const baseFields: SanitizedFieldsPermissions = Object.fromEntries(
const { docPermissions } = await getDocumentPermissions({ collectionConfig.fields
collectionConfig, .filter((f): f is { name: string } & typeof f => 'name' in f && typeof f.name === 'string')
data, .map((f) => [f.name, { create: true, read: true, update: true }]),
req, )
})
// In create-first-user we should always allow all fields
const docPermissionsForForm: SanitizedDocumentPermissions = {
create: true,
delete: true,
fields: baseFields,
read: true,
readVersions: true,
update: true,
}
// Build initial form state from data // Build initial form state from data
const { state: formState } = await buildFormState({ const { state: formState } = await buildFormState({
collectionSlug: collectionConfig.slug, collectionSlug: collectionConfig.slug,
data, data,
docPermissions, docPermissions: docPermissionsForForm,
docPreferences, docPreferences,
locale: locale?.code, locale: locale?.code,
operation: 'create', operation: 'create',
@@ -69,7 +81,7 @@ export async function CreateFirstUserView({ initPageResult }: AdminViewServerPro
<h1>{req.t('general:welcome')}</h1> <h1>{req.t('general:welcome')}</h1>
<p>{req.t('authentication:beginCreateFirstUser')}</p> <p>{req.t('authentication:beginCreateFirstUser')}</p>
<CreateFirstUserClient <CreateFirstUserClient
docPermissions={docPermissions} docPermissions={docPermissionsForForm}
docPreferences={docPreferences} docPreferences={docPreferences}
initialState={formState} initialState={formState}
loginWithUsername={loginWithUsername} loginWithUsername={loginWithUsername}

View File

@@ -75,6 +75,10 @@ export default buildConfigWithDefaults({
label: 'Named Save To JWT', label: 'Named Save To JWT',
saveToJWT: saveToJWTKey, saveToJWT: saveToJWTKey,
}, },
{
name: 'richText',
type: 'richText',
},
{ {
name: 'group', name: 'group',
type: 'group', type: 'group',

View File

@@ -124,6 +124,33 @@ describe('Auth', () => {
.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }) .poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT })
.not.toContain('create-first-user') .not.toContain('create-first-user')
}) })
test('richText field should should not be readOnly in create first user view', async () => {
const {
admin: {
routes: { createFirstUser: createFirstUserRoute },
},
routes: { admin: adminRoute },
} = getRoutes({})
// wait for create first user route
await page.goto(serverURL + `${adminRoute}${createFirstUserRoute}`)
await expect(page.locator('.create-first-user')).toBeVisible()
await waitForVisibleAuthFields()
const richTextRoot = page
.locator('.rich-text-lexical .ContentEditable__root[data-lexical-editor="true"]')
.first()
// ensure editor is present
await expect(richTextRoot).toBeVisible()
// core read-only checks
await expect(richTextRoot).toHaveAttribute('contenteditable', 'true')
await expect(richTextRoot).not.toHaveAttribute('aria-readonly', 'true')
})
}) })
describe('non create first user', () => { describe('non create first user', () => {

View File

@@ -243,6 +243,21 @@ export interface User {
adminOnlyField?: string | null; adminOnlyField?: string | null;
roles: ('admin' | 'editor' | 'moderator' | 'user' | 'viewer')[]; roles: ('admin' | 'editor' | 'moderator' | 'user' | 'viewer')[];
namedSaveToJWT?: string | null; namedSaveToJWT?: string | null;
richText?: {
root: {
type: string;
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
} | null;
group?: { group?: {
liftedSaveToJWT?: string | null; liftedSaveToJWT?: string | null;
}; };
@@ -503,6 +518,7 @@ export interface UsersSelect<T extends boolean = true> {
adminOnlyField?: T; adminOnlyField?: T;
roles?: T; roles?: T;
namedSaveToJWT?: T; namedSaveToJWT?: T;
richText?: T;
group?: group?:
| T | T
| { | {