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:
@@ -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}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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
|
||||||
| {
|
| {
|
||||||
|
|||||||
Reference in New Issue
Block a user