init commit for iPAL-kit plugin
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
import type { Access, FieldAccess } from 'payload'
|
||||
|
||||
import type { Role } from './types.js'
|
||||
|
||||
import { hasMinimumRole, isAdmin } from './predicates.js'
|
||||
|
||||
/**
|
||||
* Collection-level access (returns boolean | Where).
|
||||
* Use in collection `access.read/create/update/delete`.
|
||||
*/
|
||||
export const adminOnly: Access = ({ req: { user } }) => isAdmin(user)
|
||||
|
||||
export const adminOrEditor: Access = ({ req: { user } }) => hasMinimumRole(user, 'editor')
|
||||
|
||||
export const authenticated: Access = ({ req: { user } }) => Boolean(user)
|
||||
|
||||
/** Requires at least the given role. */
|
||||
export const requireRole =
|
||||
(minimum: Role): Access =>
|
||||
({ req: { user } }) =>
|
||||
hasMinimumRole(user, minimum)
|
||||
|
||||
/** Admins see all; others are constrained to their own document. */
|
||||
export const adminOrSelf: Access = ({ req: { user } }) => {
|
||||
if (isAdmin(user)) {return true}
|
||||
if (!user) {return false}
|
||||
return { id: { equals: user.id } }
|
||||
}
|
||||
|
||||
/**
|
||||
* Field-level access (returns boolean only — no Where support).
|
||||
* Use in field `access.read/update`.
|
||||
*/
|
||||
export const adminOnlyField: FieldAccess = ({ req: { user } }) => isAdmin(user)
|
||||
|
||||
export const adminOrEditorField: FieldAccess = ({ req: { user } }) => hasMinimumRole(user, 'editor')
|
||||
|
||||
/** Requires at least the given role, for field-level access. */
|
||||
export const requireRoleField =
|
||||
(minimum: Role): FieldAccess =>
|
||||
({ req: { user } }) =>
|
||||
hasMinimumRole(user, minimum)
|
||||
@@ -0,0 +1,15 @@
|
||||
export {
|
||||
adminOnly,
|
||||
adminOnlyField,
|
||||
adminOrEditor,
|
||||
adminOrEditorField,
|
||||
adminOrSelf,
|
||||
authenticated,
|
||||
requireRole,
|
||||
requireRoleField,
|
||||
} from './access.js'
|
||||
export { injectRoles } from './injectRoles.js'
|
||||
export { hasMinimumRole, isAdmin, isEditor } from './predicates.js'
|
||||
export { buildRolesField } from './rolesField.js'
|
||||
export type { AccessOption, Role } from './types.js'
|
||||
export { ROLE_HIERARCHY } from './types.js'
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Config } from 'payload'
|
||||
|
||||
import type { AccessOption } from './types.js'
|
||||
|
||||
import { buildRolesField } from './rolesField.js'
|
||||
|
||||
/**
|
||||
* Injects the fixed `roles` field into the client's auth collection.
|
||||
*
|
||||
* The plugin owns the role definition; the client owns the collection. This
|
||||
* finds the collection by slug and appends the field. We control the whole
|
||||
* stack, so no conflict handling is needed — the field is simply added.
|
||||
*/
|
||||
export function injectRoles(config: Config, access: AccessOption): Config {
|
||||
const rolesField = buildRolesField(access.defaultRole)
|
||||
|
||||
return {
|
||||
...config,
|
||||
collections: (config.collections ?? []).map((collection) => {
|
||||
if (collection.slug !== access.authCollection) {
|
||||
return collection
|
||||
}
|
||||
return {
|
||||
...collection,
|
||||
fields: [...collection.fields, rolesField],
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { Role } from './types.js'
|
||||
|
||||
import { ROLE_HIERARCHY } from './types.js'
|
||||
|
||||
/**
|
||||
* The plugin can't know the client's generated User type, and Payload types
|
||||
* `req.user` loosely (UntypedUser | null). Predicates therefore accept an
|
||||
* unknown-ish user and read `roles` defensively — no assumptions about shape
|
||||
* beyond an optional roles array.
|
||||
*/
|
||||
type MaybeUser = { roles?: null | Role[] } | null | Record<string, unknown> | undefined
|
||||
|
||||
/** Safely extracts the roles array from a loosely-typed user. */
|
||||
function getRoles(user: MaybeUser): Role[] {
|
||||
if (!user || typeof user !== 'object') {return []}
|
||||
const roles = (user as { roles?: unknown }).roles
|
||||
if (!Array.isArray(roles)) {return []}
|
||||
return roles.filter((role): role is Role => ROLE_HIERARCHY.includes(role as Role))
|
||||
}
|
||||
|
||||
/** Highest-privilege role index the user holds, or -1 if none. */
|
||||
function highestRoleIndex(user: MaybeUser): number {
|
||||
const roles = getRoles(user)
|
||||
if (!roles.length) {return -1}
|
||||
return Math.max(...roles.map((role) => ROLE_HIERARCHY.indexOf(role)))
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the user holds at least the given role in the hierarchy.
|
||||
* admin satisfies 'editor' and 'user'; editor satisfies 'user'.
|
||||
*/
|
||||
export function hasMinimumRole(user: MaybeUser, minimum: Role): boolean {
|
||||
return highestRoleIndex(user) >= ROLE_HIERARCHY.indexOf(minimum)
|
||||
}
|
||||
|
||||
/** True if the user is an admin. */
|
||||
export function isAdmin(user: MaybeUser): boolean {
|
||||
return hasMinimumRole(user, 'admin')
|
||||
}
|
||||
|
||||
/** True if the user is an editor or higher (editor, admin). */
|
||||
export function isEditor(user: MaybeUser): boolean {
|
||||
return hasMinimumRole(user, 'editor')
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { Field } from 'payload'
|
||||
|
||||
import type { Role } from './types.js'
|
||||
|
||||
import { isAdmin } from './predicates.js'
|
||||
import { ROLE_HIERARCHY } from './types.js'
|
||||
|
||||
/**
|
||||
* Builds the fixed `roles` field the plugin injects into the auth collection.
|
||||
*
|
||||
* Saved to the JWT so role checks avoid a database lookup. Only admins can
|
||||
* change roles, preventing privilege escalation by lower-privilege users.
|
||||
*/
|
||||
export function buildRolesField(defaultRole: Role = 'user'): Field {
|
||||
return {
|
||||
name: 'roles',
|
||||
type: 'select',
|
||||
access: {
|
||||
// Only admins may assign or change roles
|
||||
update: ({ req: { user } }) => isAdmin(user),
|
||||
},
|
||||
admin: {
|
||||
description: 'Role hierarchy: admin > editor > user.',
|
||||
},
|
||||
defaultValue: [defaultRole],
|
||||
hasMany: true,
|
||||
options: ROLE_HIERARCHY.map((role) => ({
|
||||
label: role.charAt(0).toUpperCase() + role.slice(1),
|
||||
value: role,
|
||||
})),
|
||||
required: true,
|
||||
saveToJWT: true,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Role hierarchy, lowest to highest privilege.
|
||||
* A higher role satisfies any requirement met by a lower one.
|
||||
*/
|
||||
export const ROLE_HIERARCHY = ['user', 'editor', 'admin'] as const
|
||||
|
||||
export type Role = (typeof ROLE_HIERARCHY)[number]
|
||||
|
||||
/**
|
||||
* Access-control options.
|
||||
*
|
||||
* The plugin injects a fixed `roles` field into the client's auth collection
|
||||
* — the collection itself belongs to the client (create-payload-app), the
|
||||
* role definition belongs to the plugin.
|
||||
*/
|
||||
export type AccessOption = {
|
||||
/** Slug of the client's auth collection, e.g. 'users'. */
|
||||
authCollection: string
|
||||
/** Role assigned to new users. Defaults to 'user'. */
|
||||
defaultRole?: Role
|
||||
}
|
||||
Reference in New Issue
Block a user