mirror of
https://github.com/payloadcms/payload.git
synced 2026-09-14 20:07:19 +08:00
b77493d41a
## What
This completes the user-type cleanup planned for Payload 4.0:
- `UntypedUser` was deprecated for removal in 4.0.
- `TypedUser` was marked to be renamed to `User` in 4.0.
Previously, the public `User` type was the loose, deprecated
`UntypedUser`, while `TypedUser` was the generated type for auth-enabled
collections. `ClientUser` was also loose, and `req.user` did not include
the runtime auth fields `_strategy` and `_sid`.
The types now have clear roles:
| Type | Purpose |
| --- | --- |
| `User` | The generated user type for auth-enabled collections. This
replaces `TypedUser`. Without generated types, it falls back to a
documented shape containing Payload's built-in auth fields. Contains
both read and write fields. |
| `AuthenticatedUser` | `User` plus the optional runtime fields
`_strategy` and `_sid`. Used by `PayloadRequest.user`, `payload.auth()`,
auth strategy results, and auth internals. |
| `ClientUser` | The type used by `useAuth().user` and `me` responses.
It is now an alias of `AuthenticatedUser` |
I'm considering replacing `ClientUser` in favor of just
`AuthenticatedUser` in a separate PR.
## Breaking changes
- `TypedUser` has been removed. Use `User`.
- `UntypedUser` has been removed. Use `User` for a user document,
`AuthenticatedUser` for a signed-in request user, or `ClientUser` in
client code.
- `User` and `ClientUser` no longer have an `[key: string]: any` index
signature. Custom auth-collection fields require generated types or an
explicit augmented type.
- The Local API `user` option is now `User | null` instead of the loose
`Document` type for:
- collection `count`, `create`, `delete`, `duplicate`, `find`,
`findByID`, `findDistinct`, and `update`
- collection and global version count, find, find-by-ID, and restore
operations
- global `findOne` and `update`
- `UserSession.createdAt` is now optional and nullable: `createdAt?:
Date | null | string`. This matches generated session types, but callers
must handle a missing value.
`AuthenticatedUser` is assignable to `User`, so passing `req.user` to
these Local API operations continues to work.
## Other changes
- Adds a strict untyped fallback containing all built-in user fields,
without an index signature. A type test verifies that generated user
types are assignable to this fallback.
- Types `payload.auth()` and login results with the runtime auth fields,
and uses `AuthenticatedUser` while login, `me`, refresh, and session
code build signed-in users.
- Fixes the session lookup ID type and updates session handling for
nullable `createdAt` values.
- Hardens refresh/session handling when a user or session is missing and
avoids mutating the in-memory user's `updatedAt` solely to control
database timestamps.
- Updates Payload, the admin UI, Lexical, tests, and first-party plugins
to use the new types:
- `plugin-mcp` uses `User` for the authorized caller.
- `plugin-import-export` removes obsolete `req.user.user` handling
- `plugin-multi-tenant` explicitly casts accesses to plugin-defined user
fields.
- `plugin-ecommerce` adds `UserWithCart` for the optional reverse `cart`
join that projects may
define on their user collection.
## Migration
```diff
- import type { TypedUser, UntypedUser } from 'payload'
+ import type { User } from 'payload'
```
Use `User` for stored/read user documents and `AuthenticatedUser` when
code specifically receives the signed-in user from `req.user`,
`payload.auth()`, or an auth strategy.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1215866573673868
164 lines
4.4 KiB
TypeScript
164 lines
4.4 KiB
TypeScript
import type { Payload, RequestContext, TypedLocale, User } from '../index.js'
|
|
import type { PayloadRequest } from '../types/index.js'
|
|
|
|
import { getDataLoader } from '../collections/dataloader.js'
|
|
import { getLocalI18n } from '../translations/getLocalI18n.js'
|
|
import { sanitizeFallbackLocale } from '../utilities/sanitizeFallbackLocale.js'
|
|
|
|
function getRequestContext(
|
|
req: Partial<PayloadRequest> = { context: null } as unknown as PayloadRequest,
|
|
context: RequestContext = {},
|
|
): RequestContext {
|
|
if (req.context) {
|
|
if (Object.keys(req.context).length === 0 && req.context.constructor === Object) {
|
|
// if req.context is `{}` avoid unnecessary spread
|
|
return context
|
|
} else {
|
|
return { ...req.context, ...context }
|
|
}
|
|
} else {
|
|
return context
|
|
}
|
|
}
|
|
|
|
const attachFakeURLProperties = (req: Partial<PayloadRequest>, urlSuffix?: string) => {
|
|
/**
|
|
* *NOTE*
|
|
* If no URL is provided, the local API was called outside
|
|
* the context of a request. Therefore we create a fake URL object.
|
|
* `ts-expect-error` is used below for properties that are 'read-only'.
|
|
* Since they do not exist yet we can safely ignore the error.
|
|
*/
|
|
let urlObject: undefined | URL
|
|
|
|
function getURLObject() {
|
|
if (urlObject) {
|
|
return urlObject
|
|
}
|
|
|
|
const fallbackURL = `http://${req.host || 'localhost'}${urlSuffix || ''}`
|
|
|
|
const urlToUse =
|
|
req?.url ||
|
|
(req.payload?.config?.serverURL
|
|
? `${req.payload?.config.serverURL}${urlSuffix || ''}`
|
|
: fallbackURL)
|
|
|
|
try {
|
|
urlObject = new URL(urlToUse)
|
|
} catch (_err) {
|
|
req.payload?.logger.error(
|
|
`Failed to create URL object from URL: ${urlToUse}, falling back to ${fallbackURL}`,
|
|
)
|
|
|
|
urlObject = new URL(fallbackURL)
|
|
}
|
|
|
|
return urlObject
|
|
}
|
|
|
|
if (!req.host) {
|
|
req.host = getURLObject().host
|
|
}
|
|
|
|
if (!req.protocol) {
|
|
req.protocol = getURLObject().protocol
|
|
}
|
|
|
|
if (!req.pathname) {
|
|
req.pathname = getURLObject().pathname
|
|
}
|
|
|
|
if (!req.searchParams) {
|
|
// @ts-expect-error eslint-disable-next-line no-param-reassign
|
|
req.searchParams = getURLObject().searchParams
|
|
}
|
|
|
|
if (!req.origin) {
|
|
// @ts-expect-error eslint-disable-next-line no-param-reassign
|
|
req.origin = getURLObject().origin
|
|
}
|
|
|
|
if (!req?.url) {
|
|
// @ts-expect-error eslint-disable-next-line no-param-reassign
|
|
req.url = getURLObject().href
|
|
}
|
|
}
|
|
|
|
export type CreateLocalReqOptions = {
|
|
context?: RequestContext
|
|
depth?: number
|
|
fallbackLocale?: false | TypedLocale
|
|
locale?: string
|
|
req?: Partial<PayloadRequest>
|
|
urlSuffix?: string
|
|
user?: User
|
|
}
|
|
|
|
type CreateLocalReq = (options: CreateLocalReqOptions, payload: Payload) => Promise<PayloadRequest>
|
|
|
|
export const createLocalReq: CreateLocalReq = async (
|
|
{
|
|
context,
|
|
depth,
|
|
fallbackLocale,
|
|
locale: localeArg,
|
|
req = {} as PayloadRequest,
|
|
urlSuffix,
|
|
user,
|
|
},
|
|
payload,
|
|
): Promise<PayloadRequest> => {
|
|
const localization = payload.config?.localization
|
|
|
|
if (localization) {
|
|
const locale = localeArg === '*' ? 'all' : localeArg
|
|
const defaultLocale = localization.defaultLocale
|
|
const localeCandidate = locale || req?.locale || req?.query?.locale
|
|
|
|
req.locale =
|
|
localeCandidate && typeof localeCandidate === 'string' ? localeCandidate : defaultLocale
|
|
|
|
const sanitizedFallback = sanitizeFallbackLocale({
|
|
fallbackLocale: fallbackLocale!,
|
|
locale: req.locale,
|
|
localization,
|
|
})
|
|
|
|
req.fallbackLocale = sanitizedFallback!
|
|
}
|
|
|
|
const i18n =
|
|
req?.i18n ||
|
|
(await getLocalI18n({ config: payload.config, language: payload.config.i18n.fallbackLanguage }))
|
|
|
|
if (!req.headers) {
|
|
req.headers = new Headers()
|
|
}
|
|
|
|
req.context = getRequestContext(req, context)
|
|
req.payloadAPI = req?.payloadAPI || 'local'
|
|
req.payload = payload
|
|
req.i18n = i18n
|
|
req.t = i18n.t
|
|
req.user = user || req?.user || null
|
|
|
|
// Ensure user.collection is set for auth-related access control
|
|
// TODO (4.0): Instead of silently falling back, throw an error if user.collection is missing
|
|
if (req.user && !req.user.collection) {
|
|
req.user = { ...req.user, collection: payload.config.admin.user }
|
|
}
|
|
|
|
req.payloadDataLoader = req?.payloadDataLoader || getDataLoader(req as PayloadRequest)
|
|
req.routeParams = req?.routeParams || {}
|
|
req.query = req?.query || {}
|
|
|
|
if (typeof depth !== 'undefined') {
|
|
req.query.depth = depth
|
|
}
|
|
|
|
attachFakeURLProperties(req, urlSuffix)
|
|
|
|
return req as PayloadRequest
|
|
}
|