@composius/payload-plugin-auth 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # @composius/payload-plugin-auth
2
+
3
+ A [Payload CMS](https://payloadcms.com) plugin that turns a `users` collection into a
4
+ role-based auth collection — configurable roles, sensible auth hardening, and exported
5
+ access helpers (`hasRole`, `hasRoleOrOwner`) for use in your other collections.
6
+
7
+ If the collection already exists in your config it is extended (your explicit settings
8
+ win); otherwise it is created.
9
+
10
+ ## Fields
11
+
12
+ | Field | Type | Notes |
13
+ | ------ | -------- | ---------------------------------------------------------------- |
14
+ | `name` | `text` | required, used as admin title |
15
+ | `role` | `select` | required, saved to the JWT; only admins can set or change it |
16
+
17
+ Email and password are added by Payload's auth. The collection also sets:
18
+
19
+ - Cookies: `secure` in production only (plain-http localhost logins keep working in dev), `sameSite: 'Lax'`.
20
+ - Brute-force protection: 5 max login attempts, 15 minutes lock time.
21
+ - Admin UI: the collection is hidden from users without the admin role.
22
+ - Access: `create`/`delete` admin only, `read`/`update` admin or the user themselves.
23
+
24
+ ## Admin lockout protection
25
+
26
+ - The **first user created is always given the admin role**, whatever was submitted —
27
+ otherwise the default role would apply (role changes are admin-only) and nobody could
28
+ ever manage users.
29
+ - The **last admin cannot be deleted or demoted**; such operations fail with a 403.
30
+ - `adminRole`/`defaultRole` values that are not in `roles` throw at config build time.
31
+
32
+ ## Requirements
33
+
34
+ The following dependencies are required to be installed in your project before using this plugin:
35
+
36
+ - `payload` (`^3.84.1`)
37
+
38
+ ```bash
39
+ pnpm add payload
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ ```ts
45
+ import { buildConfig } from 'payload'
46
+ import { ComposiusPayloadPluginAuth, hasRole, hasRoleOrOwner } from '@composius/payload-plugin-auth'
47
+
48
+ export default buildConfig({
49
+ plugins: [
50
+ ComposiusPayloadPluginAuth({
51
+ // Custom roles: the role select offers these instead of the defaults
52
+ roles: [
53
+ { label: 'Admin', value: 'admin' },
54
+ { label: 'Author', value: 'author' },
55
+ { label: 'Member', value: 'member' },
56
+ ],
57
+ // New users become members; the very first user still becomes admin
58
+ defaultRole: 'member',
59
+ }),
60
+ ],
61
+ collections: [
62
+ {
63
+ slug: 'articles',
64
+ access: {
65
+ read: () => true,
66
+ create: hasRole('admin', 'author'),
67
+ // admins see everything, authors only the articles they authored
68
+ update: hasRoleOrOwner(['admin'], 'author'),
69
+ delete: hasRole('admin'),
70
+ },
71
+ fields: [{ name: 'author', type: 'relationship', relationTo: 'users' }],
72
+ },
73
+ ],
74
+ // ...
75
+ })
76
+ ```
77
+
78
+ ### Access helpers
79
+
80
+ - `hasRole(...roles)` — collection access allowing users whose `role` is one of the given values.
81
+ - `hasRoleFieldLevel(...roles)` — the same check as field-level access (field access cannot return a query).
82
+ - `hasRoleOrOwner(roles, ownerField = 'id')` — allows the given roles, and otherwise
83
+ restricts the operation to documents whose `ownerField` equals the user's id. The
84
+ default `'id'` gives "self" access on the users collection; pass a relationship field
85
+ name (`'author'`, `'owner'`, …) for other collections.
86
+
87
+ ## Options
88
+
89
+ All optional — defaults shown as comments:
90
+
91
+ ```ts
92
+ ComposiusPayloadPluginAuth({
93
+ // Access per operation. Defaults: create/delete = admin role,
94
+ // read/update = admin role or the user themselves.
95
+ access: { read, create, update, delete },
96
+
97
+ // Role value with full access to the users collection and admin UI.
98
+ adminRole: 'admin',
99
+
100
+ // Role value assigned to new users (except the very first, who becomes admin).
101
+ defaultRole: 'viewer',
102
+
103
+ // Selectable roles. Default: Admin, Editor, Viewer.
104
+ roles: [
105
+ { label: 'Admin', value: 'admin' },
106
+ { label: 'Editor', value: 'editor' },
107
+ { label: 'Viewer', value: 'viewer' },
108
+ ],
109
+
110
+ // Slug of the auth collection to extend or create.
111
+ slug: 'users',
112
+
113
+ // Keeps the collection schema but disables runtime behavior (default: false).
114
+ disabled: false,
115
+ })
116
+ ```
117
+
118
+ ## Development
119
+
120
+ From the monorepo root:
121
+
122
+ ```bash
123
+ pnpm install
124
+ pnpm dev:auth # dev Payload app with this plugin
125
+ pnpm vitest run packages/payload-plugin-auth/test # unit tests
126
+ pnpm vitest run dev/configs/auth # integration tests
127
+ pnpm --filter @composius/payload-plugin-auth build # build to dist/
128
+ ```
129
+
130
+ See the [root README](../../README.md) for the release flow.
@@ -0,0 +1,58 @@
1
+ import { Access, FieldAccess, Config } from 'payload';
2
+
3
+ /** Collection access allowing only users whose `role` is one of the given values. */
4
+ declare const hasRole: (...roles: string[]) => Access;
5
+ /** Field-level variant of {@link hasRole} (field access cannot return a query). */
6
+ declare const hasRoleFieldLevel: (...roles: string[]) => FieldAccess;
7
+ /**
8
+ * Collection access allowing users whose `role` is one of the given values, and
9
+ * otherwise restricting the operation to documents the user owns: those whose
10
+ * `ownerField` equals the user's id. Use `'id'` (the default) for the users
11
+ * collection itself, or a relationship field name (e.g. `'owner'`, `'author'`)
12
+ * for other collections.
13
+ */
14
+ declare const hasRoleOrOwner: (roles: string[], ownerField?: string) => Access;
15
+
16
+ type Role = {
17
+ label: Record<string, string> | string;
18
+ value: string;
19
+ };
20
+ type UsersAccess = {
21
+ create?: Access;
22
+ delete?: Access;
23
+ read?: Access;
24
+ update?: Access;
25
+ };
26
+ type ComposiusPayloadPluginAuthConfig = {
27
+ /**
28
+ * Access control for the users collection, per operation.
29
+ * Defaults: `create`/`delete` require the admin role, `read`/`update` allow
30
+ * the admin role or the user themselves.
31
+ */
32
+ access?: UsersAccess;
33
+ /**
34
+ * Role value granting full access to the users collection and admin UI.
35
+ * Must be one of `roles`. Defaults to `'admin'`.
36
+ */
37
+ adminRole?: string;
38
+ /**
39
+ * Role value assigned to new users by default. Must be one of `roles`.
40
+ * Defaults to `'viewer'`.
41
+ */
42
+ defaultRole?: string;
43
+ disabled?: boolean;
44
+ /**
45
+ * Selectable roles for the `role` field.
46
+ * Defaults to Admin (`admin`), Editor (`editor`) and Viewer (`viewer`).
47
+ */
48
+ roles?: Role[];
49
+ /**
50
+ * Slug of the auth users collection to extend (created when it does not
51
+ * exist yet). Defaults to `'users'`.
52
+ */
53
+ slug?: string;
54
+ };
55
+
56
+ declare const ComposiusPayloadPluginAuth: (pluginOptions?: ComposiusPayloadPluginAuthConfig) => (config: Config) => Config;
57
+
58
+ export { ComposiusPayloadPluginAuth, type ComposiusPayloadPluginAuthConfig, type Role, type UsersAccess, hasRole, hasRoleFieldLevel, hasRoleOrOwner };
package/dist/index.js ADDED
@@ -0,0 +1,252 @@
1
+ // src/access.ts
2
+ var roleOf = (user) => {
3
+ const role = user?.role;
4
+ return typeof role === "string" ? role : void 0;
5
+ };
6
+ var hasRole = (...roles) => ({ req: { user } }) => {
7
+ const role = roleOf(user);
8
+ return role !== void 0 && roles.includes(role);
9
+ };
10
+ var hasRoleFieldLevel = (...roles) => ({ req: { user } }) => {
11
+ const role = roleOf(user);
12
+ return role !== void 0 && roles.includes(role);
13
+ };
14
+ var hasRoleOrOwner = (roles, ownerField = "id") => ({ req: { user } }) => {
15
+ if (!user) {
16
+ return false;
17
+ }
18
+ const role = roleOf(user);
19
+ if (role !== void 0 && roles.includes(role)) {
20
+ return true;
21
+ }
22
+ return { [ownerField]: { equals: user.id } };
23
+ };
24
+
25
+ // src/collections/Users.ts
26
+ import { APIError } from "payload";
27
+
28
+ // src/translations/en.ts
29
+ var en = {
30
+ errors: {
31
+ lastAdminDelete: "The last admin user cannot be deleted.",
32
+ lastAdminRole: "The last admin user cannot lose the admin role."
33
+ },
34
+ fields: {
35
+ name: "Name",
36
+ role: "Role"
37
+ },
38
+ roles: {
39
+ admin: "Admin",
40
+ editor: "Editor",
41
+ viewer: "Viewer"
42
+ },
43
+ users: {
44
+ plural: "Users",
45
+ singular: "User"
46
+ }
47
+ };
48
+
49
+ // src/translations/fr.ts
50
+ var fr = {
51
+ errors: {
52
+ lastAdminDelete: "Le dernier utilisateur admin ne peut pas \xEAtre supprim\xE9.",
53
+ lastAdminRole: "Le dernier utilisateur admin ne peut pas perdre le r\xF4le d'admin."
54
+ },
55
+ fields: {
56
+ name: "Nom",
57
+ role: "R\xF4le"
58
+ },
59
+ roles: {
60
+ admin: "Admin",
61
+ editor: "\xC9diteur",
62
+ viewer: "Lecteur"
63
+ },
64
+ users: {
65
+ plural: "Utilisateurs",
66
+ singular: "Utilisateur"
67
+ }
68
+ };
69
+
70
+ // src/translations/index.ts
71
+ var translations = { en, fr };
72
+ var label = (pick) => ({
73
+ en: pick(en),
74
+ fr: pick(fr)
75
+ });
76
+ var translate = (language, pick) => pick(translations[language ?? "en"] ?? en);
77
+
78
+ // src/collections/Users.ts
79
+ var fieldNames = (fields) => fields.map((field) => field.name).filter(
80
+ (name) => Boolean(name)
81
+ );
82
+ var withUsersAuth = (existing, { access, adminRole, defaultRole, roles, slug }) => {
83
+ const collection = slug;
84
+ const adminFieldLevel = hasRoleFieldLevel(adminRole);
85
+ const countAdmins = (req) => req.payload.count({
86
+ collection,
87
+ req,
88
+ where: { role: { equals: adminRole } }
89
+ });
90
+ const existingAuth = typeof existing?.auth === "object" ? existing.auth : {};
91
+ const existingFieldNames = fieldNames(existing?.fields ?? []);
92
+ const addedFields = [
93
+ // Email added by default
94
+ {
95
+ name: "name",
96
+ type: "text",
97
+ label: label((t) => t.fields.name),
98
+ required: true
99
+ },
100
+ {
101
+ name: "role",
102
+ type: "select",
103
+ label: label((t) => t.fields.role),
104
+ options: roles,
105
+ defaultValue: defaultRole,
106
+ required: true,
107
+ saveToJWT: true,
108
+ access: {
109
+ // Users can update their own profile, but only admins can change roles
110
+ create: adminFieldLevel,
111
+ update: adminFieldLevel
112
+ }
113
+ }
114
+ ];
115
+ return {
116
+ ...existing,
117
+ slug,
118
+ labels: existing?.labels ?? {
119
+ singular: label((t) => t.users.singular),
120
+ plural: label((t) => t.users.plural)
121
+ },
122
+ admin: {
123
+ useAsTitle: "name",
124
+ defaultColumns: ["name", "email", "role", "createdAt"],
125
+ hidden: ({ user }) => user?.role !== adminRole,
126
+ ...existing?.admin
127
+ },
128
+ auth: {
129
+ ...existingAuth,
130
+ cookies: {
131
+ // secure: true would break plain-http localhost logins in dev
132
+ secure: process.env.NODE_ENV === "production",
133
+ sameSite: "Lax",
134
+ ...existingAuth.cookies
135
+ },
136
+ maxLoginAttempts: existingAuth.maxLoginAttempts ?? 5,
137
+ lockTime: existingAuth.lockTime ?? 15 * 60 * 1e3
138
+ // 15 minutes
139
+ },
140
+ access: {
141
+ create: access.create,
142
+ read: access.read,
143
+ update: access.update,
144
+ delete: access.delete,
145
+ ...existing?.access
146
+ },
147
+ hooks: {
148
+ ...existing?.hooks,
149
+ beforeChange: [
150
+ ...existing?.hooks?.beforeChange ?? [],
151
+ async ({ data, operation, originalDoc, req }) => {
152
+ if (operation === "create") {
153
+ const { totalDocs } = await req.payload.count({ collection, req });
154
+ if (totalDocs === 0) {
155
+ data.role = adminRole;
156
+ }
157
+ return data;
158
+ }
159
+ if (operation === "update" && originalDoc?.role === adminRole && typeof data?.role === "string" && data.role !== adminRole) {
160
+ const { totalDocs } = await countAdmins(req);
161
+ if (totalDocs <= 1) {
162
+ throw new APIError(
163
+ translate(req.i18n?.language, (t) => t.errors.lastAdminRole),
164
+ 403
165
+ );
166
+ }
167
+ }
168
+ return data;
169
+ }
170
+ ],
171
+ beforeDelete: [
172
+ ...existing?.hooks?.beforeDelete ?? [],
173
+ async ({ id, req }) => {
174
+ const doc = await req.payload.findByID({ collection, id, depth: 0, req });
175
+ if (doc?.role === adminRole) {
176
+ const { totalDocs } = await countAdmins(req);
177
+ if (totalDocs <= 1) {
178
+ throw new APIError(
179
+ translate(req.i18n?.language, (t) => t.errors.lastAdminDelete),
180
+ 403
181
+ );
182
+ }
183
+ }
184
+ }
185
+ ]
186
+ },
187
+ fields: [
188
+ ...existing?.fields ?? [],
189
+ ...addedFields.filter(
190
+ (field) => !existingFieldNames.includes(field.name ?? "")
191
+ )
192
+ ]
193
+ };
194
+ };
195
+
196
+ // src/index.ts
197
+ var defaultRoles = [
198
+ { label: label((t) => t.roles.admin), value: "admin" },
199
+ { label: label((t) => t.roles.editor), value: "editor" },
200
+ { label: label((t) => t.roles.viewer), value: "viewer" }
201
+ ];
202
+ var ComposiusPayloadPluginAuth = (pluginOptions = {}) => (config) => {
203
+ const slug = pluginOptions.slug ?? "users";
204
+ const roles = pluginOptions.roles ?? defaultRoles;
205
+ const adminRole = pluginOptions.adminRole ?? "admin";
206
+ const defaultRole = pluginOptions.defaultRole ?? "viewer";
207
+ const roleValues = roles.map((role) => role.value);
208
+ if (!roleValues.includes(adminRole)) {
209
+ throw new Error(
210
+ `ComposiusPayloadPluginAuth: adminRole "${adminRole}" is not one of the configured roles (${roleValues.join(", ")})`
211
+ );
212
+ }
213
+ if (!roleValues.includes(defaultRole)) {
214
+ throw new Error(
215
+ `ComposiusPayloadPluginAuth: defaultRole "${defaultRole}" is not one of the configured roles (${roleValues.join(", ")})`
216
+ );
217
+ }
218
+ const admin = hasRole(adminRole);
219
+ const adminOrSelf = hasRoleOrOwner([adminRole], "id");
220
+ const access = {
221
+ create: pluginOptions.access?.create ?? admin,
222
+ delete: pluginOptions.access?.delete ?? admin,
223
+ read: pluginOptions.access?.read ?? adminOrSelf,
224
+ update: pluginOptions.access?.update ?? adminOrSelf
225
+ };
226
+ if (!config.collections) {
227
+ config.collections = [];
228
+ }
229
+ const existingIndex = config.collections.findIndex(
230
+ (collection) => collection.slug === slug
231
+ );
232
+ const users = withUsersAuth(
233
+ existingIndex === -1 ? void 0 : config.collections[existingIndex],
234
+ { access, adminRole, defaultRole, roles, slug }
235
+ );
236
+ if (existingIndex === -1) {
237
+ config.collections.push(users);
238
+ } else {
239
+ config.collections[existingIndex] = users;
240
+ }
241
+ if (pluginOptions.disabled) {
242
+ return config;
243
+ }
244
+ return config;
245
+ };
246
+ export {
247
+ ComposiusPayloadPluginAuth,
248
+ hasRole,
249
+ hasRoleFieldLevel,
250
+ hasRoleOrOwner
251
+ };
252
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/access.ts","../src/collections/Users.ts","../src/translations/en.ts","../src/translations/fr.ts","../src/translations/index.ts","../src/index.ts"],"sourcesContent":["import type { Access, FieldAccess } from 'payload'\n\nconst roleOf = (user: unknown): string | undefined => {\n const role = (user as { role?: unknown } | null | undefined)?.role\n return typeof role === 'string' ? role : undefined\n}\n\n/** Collection access allowing only users whose `role` is one of the given values. */\nexport const hasRole =\n (...roles: string[]): Access =>\n ({ req: { user } }) => {\n const role = roleOf(user)\n return role !== undefined && roles.includes(role)\n }\n\n/** Field-level variant of {@link hasRole} (field access cannot return a query). */\nexport const hasRoleFieldLevel =\n (...roles: string[]): FieldAccess =>\n ({ req: { user } }) => {\n const role = roleOf(user)\n return role !== undefined && roles.includes(role)\n }\n\n/**\n * Collection access allowing users whose `role` is one of the given values, and\n * otherwise restricting the operation to documents the user owns: those whose\n * `ownerField` equals the user's id. Use `'id'` (the default) for the users\n * collection itself, or a relationship field name (e.g. `'owner'`, `'author'`)\n * for other collections.\n */\nexport const hasRoleOrOwner =\n (roles: string[], ownerField = 'id'): Access =>\n ({ req: { user } }) => {\n if (!user) {\n return false\n }\n\n const role = roleOf(user)\n if (role !== undefined && roles.includes(role)) {\n return true\n }\n\n return { [ownerField]: { equals: user.id } }\n }\n","import type { CollectionConfig, CollectionSlug, Field, PayloadRequest } from 'payload'\n\nimport { APIError } from 'payload'\n\nimport { hasRoleFieldLevel } from '../access.js'\nimport { label, translate } from '../translations/index.js'\nimport type { UsersOptions } from '../types.js'\n\nconst fieldNames = (fields: Field[]): string[] =>\n fields.map((field) => (field as { name?: string }).name).filter((name): name is string =>\n Boolean(name),\n )\n\n/**\n * Extends an existing users collection with auth settings, role field, access\n * control and last-admin protections. Explicit settings already present on the\n * existing collection win over the plugin's; when there is no existing\n * collection, pass `undefined` to build one from scratch.\n */\nexport const withUsersAuth = (\n existing: CollectionConfig | undefined,\n { access, adminRole, defaultRole, roles, slug }: UsersOptions,\n): CollectionConfig => {\n const collection = slug as CollectionSlug\n const adminFieldLevel = hasRoleFieldLevel(adminRole)\n\n const countAdmins = (req: PayloadRequest) =>\n req.payload.count({\n collection,\n req,\n where: { role: { equals: adminRole } },\n })\n\n const existingAuth = typeof existing?.auth === 'object' ? existing.auth : {}\n const existingFieldNames = fieldNames(existing?.fields ?? [])\n\n const addedFields: Field[] = [\n // Email added by default\n {\n name: 'name',\n type: 'text',\n label: label((t) => t.fields.name),\n required: true,\n },\n {\n name: 'role',\n type: 'select',\n label: label((t) => t.fields.role),\n options: roles,\n defaultValue: defaultRole,\n required: true,\n saveToJWT: true,\n access: {\n // Users can update their own profile, but only admins can change roles\n create: adminFieldLevel,\n update: adminFieldLevel,\n },\n },\n ]\n\n return {\n ...existing,\n slug,\n labels: existing?.labels ?? {\n singular: label((t) => t.users.singular),\n plural: label((t) => t.users.plural),\n },\n admin: {\n useAsTitle: 'name',\n defaultColumns: ['name', 'email', 'role', 'createdAt'],\n hidden: ({ user }) => (user as { role?: string } | null)?.role !== adminRole,\n ...existing?.admin,\n },\n auth: {\n ...existingAuth,\n cookies: {\n // secure: true would break plain-http localhost logins in dev\n secure: process.env.NODE_ENV === 'production',\n sameSite: 'Lax',\n ...existingAuth.cookies,\n },\n maxLoginAttempts: existingAuth.maxLoginAttempts ?? 5,\n lockTime: existingAuth.lockTime ?? 15 * 60 * 1000, // 15 minutes\n },\n access: {\n create: access.create,\n read: access.read,\n update: access.update,\n delete: access.delete,\n ...existing?.access,\n },\n hooks: {\n ...existing?.hooks,\n beforeChange: [\n ...(existing?.hooks?.beforeChange ?? []),\n async ({ data, operation, originalDoc, req }) => {\n if (operation === 'create') {\n // The very first user must get the admin role no matter what was\n // submitted: role creation is admin-only, so a non-admin first\n // user would leave the panel without anyone able to manage users.\n const { totalDocs } = await req.payload.count({ collection, req })\n if (totalDocs === 0) {\n data.role = adminRole\n }\n return data\n }\n\n if (\n operation === 'update' &&\n originalDoc?.role === adminRole &&\n typeof data?.role === 'string' &&\n data.role !== adminRole\n ) {\n const { totalDocs } = await countAdmins(req)\n if (totalDocs <= 1) {\n throw new APIError(\n translate(req.i18n?.language, (t) => t.errors.lastAdminRole),\n 403,\n )\n }\n }\n\n return data\n },\n ],\n beforeDelete: [\n ...(existing?.hooks?.beforeDelete ?? []),\n async ({ id, req }) => {\n const doc = await req.payload.findByID({ collection, id, depth: 0, req })\n if ((doc as { role?: string } | null)?.role === adminRole) {\n const { totalDocs } = await countAdmins(req)\n if (totalDocs <= 1) {\n throw new APIError(\n translate(req.i18n?.language, (t) => t.errors.lastAdminDelete),\n 403,\n )\n }\n }\n },\n ],\n },\n fields: [\n ...(existing?.fields ?? []),\n ...addedFields.filter(\n (field) => !existingFieldNames.includes((field as { name?: string }).name ?? ''),\n ),\n ],\n }\n}\n","export const en = {\n errors: {\n lastAdminDelete: 'The last admin user cannot be deleted.',\n lastAdminRole: 'The last admin user cannot lose the admin role.',\n },\n fields: {\n name: 'Name',\n role: 'Role',\n },\n roles: {\n admin: 'Admin',\n editor: 'Editor',\n viewer: 'Viewer',\n },\n users: {\n plural: 'Users',\n singular: 'User',\n },\n}\n","import type { Translation } from './index.js'\n\nexport const fr: Translation = {\n errors: {\n lastAdminDelete: 'Le dernier utilisateur admin ne peut pas être supprimé.',\n lastAdminRole: \"Le dernier utilisateur admin ne peut pas perdre le rôle d'admin.\",\n },\n fields: {\n name: 'Nom',\n role: 'Rôle',\n },\n roles: {\n admin: 'Admin',\n editor: 'Éditeur',\n viewer: 'Lecteur',\n },\n users: {\n plural: 'Utilisateurs',\n singular: 'Utilisateur',\n },\n}\n","import { en } from './en.js'\nimport { fr } from './fr.js'\n\nexport type Translation = typeof en\n\nconst translations: Record<string, Translation> = { en, fr }\n\n/** Builds a Payload label record ({ en, fr }) from a translation key selector. */\nexport const label = (pick: (t: Translation) => string): Record<string, string> => ({\n en: pick(en),\n fr: pick(fr),\n})\n\n/** Resolves a translation for a runtime language (e.g. `req.i18n.language`), falling back to English. */\nexport const translate = (\n language: string | undefined,\n pick: (t: Translation) => string,\n): string => pick(translations[language ?? 'en'] ?? en)\n","import type { Config } from 'payload'\n\nimport { hasRole, hasRoleFieldLevel, hasRoleOrOwner } from './access.js'\nimport { withUsersAuth } from './collections/Users.js'\nimport { label } from './translations/index.js'\nimport type { Role, UsersAccess, ComposiusPayloadPluginAuthConfig } from './types.js'\n\nexport { hasRole, hasRoleFieldLevel, hasRoleOrOwner }\nexport type { Role, UsersAccess, ComposiusPayloadPluginAuthConfig }\n\nconst defaultRoles: Role[] = [\n { label: label((t) => t.roles.admin), value: 'admin' },\n { label: label((t) => t.roles.editor), value: 'editor' },\n { label: label((t) => t.roles.viewer), value: 'viewer' },\n]\n\nexport const ComposiusPayloadPluginAuth =\n (pluginOptions: ComposiusPayloadPluginAuthConfig = {}) =>\n (config: Config): Config => {\n const slug = pluginOptions.slug ?? 'users'\n const roles = pluginOptions.roles ?? defaultRoles\n const adminRole = pluginOptions.adminRole ?? 'admin'\n const defaultRole = pluginOptions.defaultRole ?? 'viewer'\n\n // A wrong role value here would lock everyone out of the users collection\n // (or hand new users an invalid role), so fail at config build time.\n const roleValues = roles.map((role) => role.value)\n if (!roleValues.includes(adminRole)) {\n throw new Error(\n `ComposiusPayloadPluginAuth: adminRole \"${adminRole}\" is not one of the configured roles (${roleValues.join(', ')})`,\n )\n }\n if (!roleValues.includes(defaultRole)) {\n throw new Error(\n `ComposiusPayloadPluginAuth: defaultRole \"${defaultRole}\" is not one of the configured roles (${roleValues.join(', ')})`,\n )\n }\n\n const admin = hasRole(adminRole)\n const adminOrSelf = hasRoleOrOwner([adminRole], 'id')\n\n const access = {\n create: pluginOptions.access?.create ?? admin,\n delete: pluginOptions.access?.delete ?? admin,\n read: pluginOptions.access?.read ?? adminOrSelf,\n update: pluginOptions.access?.update ?? adminOrSelf,\n }\n\n if (!config.collections) {\n config.collections = []\n }\n\n const existingIndex = config.collections.findIndex(\n (collection) => collection.slug === slug,\n )\n const users = withUsersAuth(\n existingIndex === -1 ? undefined : config.collections[existingIndex],\n { access, adminRole, defaultRole, roles, slug },\n )\n\n if (existingIndex === -1) {\n config.collections.push(users)\n } else {\n config.collections[existingIndex] = users\n }\n\n /**\n * If the plugin is disabled, we still want to keep added collections/fields so the database schema is consistent which is important for migrations.\n */\n if (pluginOptions.disabled) {\n return config\n }\n\n return config\n }\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@composius/payload-plugin-auth",
3
+ "version": "0.1.1",
4
+ "description": "Users auth collection with configurable roles and role-based access helpers",
5
+ "license": "MIT",
6
+ "author": {
7
+ "name": "Composius"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/Composius/payload-plugins",
12
+ "directory": "packages/payload-plugin-auth"
13
+ },
14
+ "type": "module",
15
+ "exports": {
16
+ ".": {
17
+ "import": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ }
21
+ },
22
+ "main": "./dist/index.js",
23
+ "types": "./dist/index.d.ts",
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "devDependencies": {
28
+ "payload": "3.84.1",
29
+ "rimraf": "3.0.2",
30
+ "tsup": "8.5.0",
31
+ "typescript": "5.7.3"
32
+ },
33
+ "peerDependencies": {
34
+ "payload": "^3.84.1"
35
+ },
36
+ "engines": {
37
+ "node": ">=20.9.0",
38
+ "pnpm": "^9 || ^10 || ^11"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public",
42
+ "registry": "https://registry.npmjs.org/"
43
+ },
44
+ "scripts": {
45
+ "build": "tsup",
46
+ "clean": "rimraf {dist,*.tsbuildinfo}"
47
+ }
48
+ }