@byline/admin 2.3.3 → 2.4.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.
Files changed (177) hide show
  1. package/dist/abilities.js +5 -24
  2. package/dist/index.js +8 -30
  3. package/dist/lib/assert-admin-actor.js +13 -74
  4. package/dist/lib/create-command.js +6 -16
  5. package/dist/modules/admin-account/commands.js +35 -24
  6. package/dist/modules/admin-account/components/change-password.d.ts +8 -0
  7. package/dist/modules/admin-account/components/change-password.js +192 -0
  8. package/dist/modules/admin-account/components/change-password.module.js +8 -0
  9. package/dist/modules/admin-account/components/change-password_module.css +27 -0
  10. package/dist/modules/admin-account/components/container.d.ts +29 -0
  11. package/dist/modules/admin-account/components/container.js +298 -0
  12. package/dist/modules/admin-account/components/container.module.js +28 -0
  13. package/dist/modules/admin-account/components/container_module.css +106 -0
  14. package/dist/modules/admin-account/components/update.d.ts +8 -0
  15. package/dist/modules/admin-account/components/update.js +207 -0
  16. package/dist/modules/admin-account/components/update.module.js +8 -0
  17. package/dist/modules/admin-account/components/update_module.css +27 -0
  18. package/dist/modules/admin-account/errors.js +14 -45
  19. package/dist/modules/admin-account/index.js +4 -34
  20. package/dist/modules/admin-account/schemas.js +25 -59
  21. package/dist/modules/admin-account/service.js +56 -61
  22. package/dist/modules/admin-permissions/abilities.js +6 -24
  23. package/dist/modules/admin-permissions/commands.js +42 -28
  24. package/dist/modules/admin-permissions/components/inspector.d.ts +4 -0
  25. package/dist/modules/admin-permissions/components/inspector.js +284 -0
  26. package/dist/modules/admin-permissions/components/inspector.module.js +56 -0
  27. package/dist/modules/admin-permissions/components/inspector_module.css +238 -0
  28. package/dist/modules/admin-permissions/dto.js +3 -16
  29. package/dist/modules/admin-permissions/errors.js +14 -27
  30. package/dist/modules/admin-permissions/index.js +6 -26
  31. package/dist/modules/admin-permissions/repository.js +1 -8
  32. package/dist/modules/admin-permissions/schemas.js +33 -70
  33. package/dist/modules/admin-permissions/service.js +88 -92
  34. package/dist/modules/admin-roles/abilities.js +8 -30
  35. package/dist/modules/admin-roles/commands.js +89 -55
  36. package/dist/modules/admin-roles/components/create.d.ts +7 -0
  37. package/dist/modules/admin-roles/components/create.js +177 -0
  38. package/dist/modules/admin-roles/components/create.module.js +8 -0
  39. package/dist/modules/admin-roles/components/create_module.css +27 -0
  40. package/dist/modules/admin-roles/components/permissions.d.ts +10 -0
  41. package/dist/modules/admin-roles/components/permissions.js +303 -0
  42. package/dist/modules/admin-roles/components/permissions.module.js +44 -0
  43. package/dist/modules/admin-roles/components/permissions_module.css +192 -0
  44. package/dist/modules/admin-roles/components/update.d.ts +8 -0
  45. package/dist/modules/admin-roles/components/update.js +166 -0
  46. package/dist/modules/admin-roles/components/update.module.js +8 -0
  47. package/dist/modules/admin-roles/components/update_module.css +27 -0
  48. package/dist/modules/admin-roles/dto.js +3 -16
  49. package/dist/modules/admin-roles/errors.js +16 -40
  50. package/dist/modules/admin-roles/index.js +6 -26
  51. package/dist/modules/admin-roles/repository.js +1 -8
  52. package/dist/modules/admin-roles/schemas.js +41 -71
  53. package/dist/modules/admin-roles/service.js +79 -82
  54. package/dist/modules/admin-users/abilities.js +9 -38
  55. package/dist/modules/admin-users/commands.js +92 -50
  56. package/dist/modules/admin-users/components/create.d.ts +8 -0
  57. package/dist/modules/admin-users/components/create.js +268 -0
  58. package/dist/modules/admin-users/components/create.module.js +10 -0
  59. package/dist/modules/admin-users/components/create_module.css +45 -0
  60. package/dist/modules/admin-users/components/roles.d.ts +11 -0
  61. package/dist/modules/admin-users/components/roles.js +148 -0
  62. package/dist/modules/admin-users/components/roles.module.js +18 -0
  63. package/dist/modules/admin-users/components/roles_module.css +75 -0
  64. package/dist/modules/admin-users/components/set-password.d.ts +8 -0
  65. package/dist/modules/admin-users/components/set-password.js +170 -0
  66. package/dist/modules/admin-users/components/set-password.module.js +9 -0
  67. package/dist/modules/admin-users/components/set-password_module.css +31 -0
  68. package/dist/modules/admin-users/components/update.d.ts +8 -0
  69. package/dist/modules/admin-users/components/update.js +254 -0
  70. package/dist/modules/admin-users/components/update.module.js +9 -0
  71. package/dist/modules/admin-users/components/update_module.css +34 -0
  72. package/dist/modules/admin-users/dto.js +3 -18
  73. package/dist/modules/admin-users/errors.js +17 -43
  74. package/dist/modules/admin-users/index.js +7 -27
  75. package/dist/modules/admin-users/repository.js +1 -8
  76. package/dist/modules/admin-users/schemas.js +44 -75
  77. package/dist/modules/admin-users/seed-super-admin.js +9 -34
  78. package/dist/modules/admin-users/service.js +76 -91
  79. package/dist/modules/auth/components/sign-in-form.d.ts +12 -0
  80. package/dist/modules/auth/components/sign-in-form.js +115 -0
  81. package/dist/modules/auth/components/sign-in-form.module.js +12 -0
  82. package/dist/modules/auth/components/sign-in-form_module.css +41 -0
  83. package/dist/modules/auth/index.js +3 -24
  84. package/dist/modules/auth/jwt-session-provider.js +179 -149
  85. package/dist/modules/auth/password.js +11 -53
  86. package/dist/modules/auth/phc.js +21 -54
  87. package/dist/modules/auth/refresh-tokens-repository.js +1 -8
  88. package/dist/modules/auth/resolve-actor.js +6 -28
  89. package/dist/services/admin-services-context.d.ts +16 -0
  90. package/dist/services/admin-services-context.js +13 -0
  91. package/dist/services/admin-services-types.d.ts +129 -0
  92. package/dist/services/admin-services-types.js +1 -0
  93. package/dist/store.js +1 -8
  94. package/dist/vendor/noble-argon2/_blake.js +277 -45
  95. package/dist/vendor/noble-argon2/_md.js +81 -136
  96. package/dist/vendor/noble-argon2/_u64.js +65 -67
  97. package/dist/vendor/noble-argon2/argon2.js +181 -342
  98. package/dist/vendor/noble-argon2/blake2.js +252 -327
  99. package/dist/vendor/noble-argon2/utils.js +110 -490
  100. package/dist/vendor/noble-argon2/utils.js.LICENSE.txt +1 -0
  101. package/package.json +89 -10
  102. package/src/abilities.ts +32 -0
  103. package/src/declarations.d.ts +4 -0
  104. package/src/index.ts +39 -0
  105. package/src/lib/assert-admin-actor.ts +90 -0
  106. package/src/lib/create-command.ts +109 -0
  107. package/src/modules/admin-account/commands.ts +76 -0
  108. package/src/modules/admin-account/components/change-password.module.css +40 -0
  109. package/src/modules/admin-account/components/change-password.tsx +232 -0
  110. package/src/modules/admin-account/components/container.module.css +158 -0
  111. package/src/modules/admin-account/components/container.tsx +229 -0
  112. package/src/modules/admin-account/components/update.module.css +40 -0
  113. package/src/modules/admin-account/components/update.tsx +263 -0
  114. package/src/modules/admin-account/errors.ts +75 -0
  115. package/src/modules/admin-account/index.ts +60 -0
  116. package/src/modules/admin-account/schemas.ts +84 -0
  117. package/src/modules/admin-account/service.ts +92 -0
  118. package/src/modules/admin-permissions/abilities.ts +46 -0
  119. package/src/modules/admin-permissions/commands.ts +103 -0
  120. package/src/modules/admin-permissions/components/inspector.module.css +326 -0
  121. package/src/modules/admin-permissions/components/inspector.tsx +298 -0
  122. package/src/modules/admin-permissions/dto.ts +28 -0
  123. package/src/modules/admin-permissions/errors.ts +57 -0
  124. package/src/modules/admin-permissions/index.ts +72 -0
  125. package/src/modules/admin-permissions/repository.ts +49 -0
  126. package/src/modules/admin-permissions/schemas.ts +128 -0
  127. package/src/modules/admin-permissions/service.ts +137 -0
  128. package/src/modules/admin-roles/abilities.ts +62 -0
  129. package/src/modules/admin-roles/commands.ts +161 -0
  130. package/src/modules/admin-roles/components/create.module.css +40 -0
  131. package/src/modules/admin-roles/components/create.tsx +218 -0
  132. package/src/modules/admin-roles/components/permissions.module.css +279 -0
  133. package/src/modules/admin-roles/components/permissions.tsx +396 -0
  134. package/src/modules/admin-roles/components/update.module.css +40 -0
  135. package/src/modules/admin-roles/components/update.tsx +218 -0
  136. package/src/modules/admin-roles/dto.ts +30 -0
  137. package/src/modules/admin-roles/errors.ts +76 -0
  138. package/src/modules/admin-roles/index.ts +81 -0
  139. package/src/modules/admin-roles/repository.ts +96 -0
  140. package/src/modules/admin-roles/schemas.ts +139 -0
  141. package/src/modules/admin-roles/service.ts +136 -0
  142. package/src/modules/admin-users/abilities.ts +76 -0
  143. package/src/modules/admin-users/commands.ts +157 -0
  144. package/src/modules/admin-users/components/create.module.css +63 -0
  145. package/src/modules/admin-users/components/create.tsx +323 -0
  146. package/src/modules/admin-users/components/roles.module.css +119 -0
  147. package/src/modules/admin-users/components/roles.tsx +172 -0
  148. package/src/modules/admin-users/components/set-password.module.css +46 -0
  149. package/src/modules/admin-users/components/set-password.tsx +199 -0
  150. package/src/modules/admin-users/components/update.module.css +49 -0
  151. package/src/modules/admin-users/components/update.tsx +328 -0
  152. package/src/modules/admin-users/dto.ts +39 -0
  153. package/src/modules/admin-users/errors.ts +84 -0
  154. package/src/modules/admin-users/index.ts +91 -0
  155. package/src/modules/admin-users/repository.ts +161 -0
  156. package/src/modules/admin-users/schemas.ts +168 -0
  157. package/src/modules/admin-users/seed-super-admin.ts +102 -0
  158. package/src/modules/admin-users/service.ts +166 -0
  159. package/src/modules/auth/components/sign-in-form.module.css +62 -0
  160. package/src/modules/auth/components/sign-in-form.tsx +132 -0
  161. package/src/modules/auth/index.ts +31 -0
  162. package/src/modules/auth/jwt-session-provider.ts +301 -0
  163. package/src/modules/auth/password.ts +94 -0
  164. package/src/modules/auth/phc.ts +121 -0
  165. package/src/modules/auth/refresh-tokens-repository.ts +74 -0
  166. package/src/modules/auth/resolve-actor.ts +42 -0
  167. package/src/services/admin-services-context.tsx +52 -0
  168. package/src/services/admin-services-types.ts +177 -0
  169. package/src/store.ts +32 -0
  170. package/src/vendor/noble-argon2/LICENSE +21 -0
  171. package/src/vendor/noble-argon2/README.md +87 -0
  172. package/src/vendor/noble-argon2/_blake.ts +58 -0
  173. package/src/vendor/noble-argon2/_md.ts +223 -0
  174. package/src/vendor/noble-argon2/_u64.ts +118 -0
  175. package/src/vendor/noble-argon2/argon2.ts +668 -0
  176. package/src/vendor/noble-argon2/blake2.ts +583 -0
  177. package/src/vendor/noble-argon2/utils.ts +849 -0
@@ -0,0 +1,218 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * This Source Code is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
+ *
8
+ * Copyright (c) Infonomic Company Limited
9
+ */
10
+
11
+ /**
12
+ * Role details drawer form.
13
+ *
14
+ * Mirrors the admin-users update pattern: TanStack Form + Zod, blur-then-
15
+ * change validation, diff-against-original patch, optimistic concurrency
16
+ * via `vid`. `machine_name` is read-only here — it is captured at create
17
+ * time and immutable thereafter.
18
+ */
19
+
20
+ import { useState } from 'react'
21
+ import { revalidateLogic, useForm } from '@tanstack/react-form-start'
22
+
23
+ import { Alert, Button, Input, LoaderEllipsis, TextArea } from '@byline/ui/react'
24
+ import cx from 'classnames'
25
+ import { z } from 'zod'
26
+
27
+ import { useBylineAdminServices } from '../../../services/admin-services-context.js'
28
+ import styles from './update.module.css'
29
+ import type { UpdateAdminRoleInput } from '../../../services/admin-services-types.js'
30
+ import type { AdminRoleResponse } from '../index.js'
31
+
32
+ const updateRoleSchema = z.object({
33
+ name: z.string().min(1, 'Name is required').max(128, 'Name must not exceed 128 characters'),
34
+ description: z.string().max(2000, 'Description must not exceed 2000 characters'),
35
+ })
36
+
37
+ type UpdateRoleValues = z.infer<typeof updateRoleSchema>
38
+
39
+ function defaultsFrom(role: AdminRoleResponse): UpdateRoleValues {
40
+ return {
41
+ name: role.name,
42
+ description: role.description ?? '',
43
+ }
44
+ }
45
+
46
+ /** Build a patch object containing only fields whose values differ from the original role. */
47
+ function buildPatch(
48
+ values: UpdateRoleValues,
49
+ role: AdminRoleResponse
50
+ ): UpdateAdminRoleInput['patch'] {
51
+ const patch: UpdateAdminRoleInput['patch'] = {}
52
+ const normaliseText = (value: string): string | null => (value.trim().length > 0 ? value : null)
53
+ const nextDescription = normaliseText(values.description)
54
+ const nextName = values.name.trim()
55
+
56
+ if (nextName !== role.name) patch.name = nextName
57
+ if (nextDescription !== role.description) patch.description = nextDescription
58
+ return patch
59
+ }
60
+
61
+ interface UpdateRoleProps {
62
+ role: AdminRoleResponse
63
+ onClose?: () => void
64
+ onSuccess?: (role: AdminRoleResponse) => void
65
+ }
66
+
67
+ export function UpdateRole({ role, onClose, onSuccess }: UpdateRoleProps) {
68
+ const { updateAdminRole } = useBylineAdminServices()
69
+ const [formError, setFormError] = useState<string | null>(null)
70
+ const [successMessage, setSuccessMessage] = useState<string | null>(null)
71
+
72
+ const form = useForm({
73
+ defaultValues: defaultsFrom(role),
74
+ validationLogic: revalidateLogic({
75
+ mode: 'blur',
76
+ modeAfterSubmission: 'change',
77
+ }),
78
+ validators: {
79
+ onDynamic: updateRoleSchema,
80
+ },
81
+ onSubmit: async ({ value }) => {
82
+ setFormError(null)
83
+ setSuccessMessage(null)
84
+ const patch = buildPatch(value, role)
85
+ if (Object.keys(patch).length === 0) {
86
+ setSuccessMessage('No changes to save.')
87
+ return
88
+ }
89
+
90
+ try {
91
+ const updated = await updateAdminRole({
92
+ data: { id: role.id, vid: role.vid, patch },
93
+ })
94
+ setSuccessMessage('Saved.')
95
+ onSuccess?.(updated)
96
+ } catch (err) {
97
+ const code = getErrorCode(err)
98
+ if (code === 'admin.roles.versionConflict') {
99
+ setFormError(
100
+ 'This role has been modified elsewhere since you opened this form. Reload to get the latest values and try again.'
101
+ )
102
+ return
103
+ }
104
+ if (code === 'admin.roles.notFound') {
105
+ setFormError('This role no longer exists.')
106
+ return
107
+ }
108
+ setFormError('Could not save changes. Please try again.')
109
+ }
110
+ },
111
+ })
112
+
113
+ return (
114
+ <div className={cx('byline-role-update-wrap', styles.wrap)}>
115
+ <form
116
+ noValidate
117
+ onSubmit={(event) => {
118
+ event.preventDefault()
119
+ event.stopPropagation()
120
+ void form.handleSubmit()
121
+ }}
122
+ className={cx('byline-role-update-form', styles.form)}
123
+ >
124
+ {formError ? <Alert intent="danger">{formError}</Alert> : null}
125
+ {successMessage ? <Alert intent="success">{successMessage}</Alert> : null}
126
+
127
+ <form.Field name="name">
128
+ {(field) => (
129
+ <Input
130
+ label="Name"
131
+ id="role-name"
132
+ name={field.name}
133
+ value={field.state.value}
134
+ onBlur={field.handleBlur}
135
+ onChange={(e) => field.handleChange(e.currentTarget.value)}
136
+ error={field.state.meta.errors.length > 0}
137
+ errorText={firstError(field.state.meta.errors)}
138
+ required
139
+ />
140
+ )}
141
+ </form.Field>
142
+
143
+ <Input
144
+ label="Machine name"
145
+ id="role-machine-name"
146
+ name="machine_name"
147
+ value={role.machine_name}
148
+ readOnly
149
+ disabled
150
+ helpText="The stable code-side handle. Cannot be changed after creation."
151
+ />
152
+
153
+ <form.Field name="description">
154
+ {(field) => (
155
+ <TextArea
156
+ label="Description"
157
+ id="role-description"
158
+ name={field.name}
159
+ value={field.state.value}
160
+ onBlur={field.handleBlur}
161
+ onChange={(e) => field.handleChange(e.currentTarget.value)}
162
+ error={field.state.meta.errors.length > 0}
163
+ errorText={firstError(field.state.meta.errors)}
164
+ rows={3}
165
+ />
166
+ )}
167
+ </form.Field>
168
+
169
+ <div className={cx('byline-role-update-actions', styles.actions)}>
170
+ <Button
171
+ type="button"
172
+ intent="secondary"
173
+ size="sm"
174
+ onClick={onClose}
175
+ className={cx('byline-role-update-action', styles.action)}
176
+ >
177
+ {successMessage ? 'Close' : 'Cancel'}
178
+ </Button>
179
+ <form.Subscribe
180
+ selector={(state) => ({
181
+ canSubmit: state.canSubmit,
182
+ isSubmitting: state.isSubmitting,
183
+ })}
184
+ >
185
+ {({ canSubmit, isSubmitting }) => (
186
+ <Button
187
+ size="sm"
188
+ intent="primary"
189
+ type="submit"
190
+ disabled={!canSubmit || isSubmitting}
191
+ className={cx('byline-role-update-action', styles.action)}
192
+ >
193
+ {isSubmitting === true ? <LoaderEllipsis size={42} /> : 'Save'}
194
+ </Button>
195
+ )}
196
+ </form.Subscribe>
197
+ </div>
198
+ </form>
199
+ </div>
200
+ )
201
+ }
202
+
203
+ function firstError(errors: readonly unknown[]): string | undefined {
204
+ for (const err of errors) {
205
+ if (typeof err === 'string') return err
206
+ if (err && typeof err === 'object' && 'message' in err) {
207
+ const msg = (err as { message?: unknown }).message
208
+ if (typeof msg === 'string') return msg
209
+ }
210
+ }
211
+ return undefined
212
+ }
213
+
214
+ function getErrorCode(err: unknown): string | null {
215
+ return typeof (err as { code?: unknown })?.code === 'string'
216
+ ? (err as { code: string }).code
217
+ : null
218
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ import type { AdminRoleRow } from './repository.js'
10
+ import type { AdminRoleResponse } from './schemas.js'
11
+
12
+ /**
13
+ * Shape an `AdminRoleRow` into its public `AdminRoleResponse` form.
14
+ *
15
+ * Effectively an identity map today — the indirection exists so future
16
+ * row-only fields (tenant id, soft-delete) stay opted out of the public
17
+ * shape by default.
18
+ */
19
+ export function toAdminRole(row: AdminRoleRow): AdminRoleResponse {
20
+ return {
21
+ id: row.id,
22
+ vid: row.vid,
23
+ name: row.name,
24
+ machine_name: row.machine_name,
25
+ description: row.description,
26
+ order: row.order,
27
+ created_at: row.created_at,
28
+ updated_at: row.updated_at,
29
+ }
30
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ /**
10
+ * Module-local error codes for admin-roles.
11
+ *
12
+ * Mirrors the `code + factory` shape used by `AdminUsersError`. Codes
13
+ * are dot-prefixed (`admin.roles.*`) so they sort alongside the matching
14
+ * ability keys in logs and admin UI messages.
15
+ */
16
+
17
+ export const AdminRolesErrorCodes = {
18
+ NOT_FOUND: 'admin.roles.notFound',
19
+ MACHINE_NAME_IN_USE: 'admin.roles.machineNameInUse',
20
+ VERSION_CONFLICT: 'admin.roles.versionConflict',
21
+ USER_NOT_FOUND: 'admin.roles.userNotFound',
22
+ } as const
23
+
24
+ export type AdminRolesErrorCode = (typeof AdminRolesErrorCodes)[keyof typeof AdminRolesErrorCodes]
25
+
26
+ export interface AdminRolesErrorOptions {
27
+ message?: string
28
+ cause?: unknown
29
+ }
30
+
31
+ export class AdminRolesError extends Error {
32
+ public readonly code: AdminRolesErrorCode
33
+
34
+ constructor(code: AdminRolesErrorCode, options: { message: string; cause?: unknown }) {
35
+ super(options.message, options.cause != null ? { cause: options.cause } : undefined)
36
+ this.name = 'AdminRolesError'
37
+ this.code = code
38
+ }
39
+ }
40
+
41
+ const make =
42
+ (code: AdminRolesErrorCode, defaultMessage: string) =>
43
+ (options?: AdminRolesErrorOptions): AdminRolesError =>
44
+ new AdminRolesError(code, {
45
+ message: options?.message ?? defaultMessage,
46
+ cause: options?.cause,
47
+ })
48
+
49
+ /** The referenced admin role id does not exist. */
50
+ export const ERR_ADMIN_ROLE_NOT_FOUND = make(AdminRolesErrorCodes.NOT_FOUND, 'admin role not found')
51
+
52
+ /** Creating a role conflicts with an existing `machine_name`. */
53
+ export const ERR_ADMIN_ROLE_MACHINE_NAME_IN_USE = make(
54
+ AdminRolesErrorCodes.MACHINE_NAME_IN_USE,
55
+ 'machine name already in use'
56
+ )
57
+
58
+ /**
59
+ * The stored `vid` does not match the client-supplied `expectedVid` —
60
+ * the caller is holding a stale version of the row. Typical admin-UI
61
+ * response is to reload the edit form with the current values.
62
+ */
63
+ export const ERR_ADMIN_ROLE_VERSION_CONFLICT = make(
64
+ AdminRolesErrorCodes.VERSION_CONFLICT,
65
+ 'admin role has been modified elsewhere — please reload and try again'
66
+ )
67
+
68
+ /**
69
+ * The admin user targeted by a role-assignment operation does not exist.
70
+ * Module-local rather than reaching into `@byline/admin/admin-users`'
71
+ * error codes — keeps the modules decoupled.
72
+ */
73
+ export const ERR_ADMIN_ROLE_USER_NOT_FOUND = make(
74
+ AdminRolesErrorCodes.USER_NOT_FOUND,
75
+ 'admin user not found'
76
+ )
@@ -0,0 +1,81 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ /**
10
+ * `@byline/admin/admin-roles` — role CRUD, reorder, and role ↔ user
11
+ * assignment.
12
+ *
13
+ * Exports the adapter-facing `AdminRolesRepository` contract, ability
14
+ * keys, transport-agnostic commands, the `AdminRolesService`, and the
15
+ * module's error types. Commands are the recommended entry point for
16
+ * any caller; the service is exposed for internal uses (other services,
17
+ * future seeds) that want to skip Zod/ability overhead.
18
+ *
19
+ * Per-role ability grants live on the sibling
20
+ * `@byline/admin/admin-permissions` module, not here.
21
+ */
22
+
23
+ export {
24
+ ADMIN_ROLES_ABILITIES,
25
+ type AdminRolesAbilityKey,
26
+ registerAdminRolesAbilities,
27
+ } from './abilities.js'
28
+ export {
29
+ createAdminRoleCommand,
30
+ deleteAdminRoleCommand,
31
+ getAdminRoleCommand,
32
+ getRolesForUserCommand,
33
+ listAdminRolesCommand,
34
+ reorderAdminRolesCommand,
35
+ setRolesForUserCommand,
36
+ updateAdminRoleCommand,
37
+ } from './commands.js'
38
+ export { toAdminRole } from './dto.js'
39
+ export {
40
+ AdminRolesError,
41
+ type AdminRolesErrorCode,
42
+ AdminRolesErrorCodes,
43
+ ERR_ADMIN_ROLE_MACHINE_NAME_IN_USE,
44
+ ERR_ADMIN_ROLE_NOT_FOUND,
45
+ ERR_ADMIN_ROLE_USER_NOT_FOUND,
46
+ ERR_ADMIN_ROLE_VERSION_CONFLICT,
47
+ } from './errors.js'
48
+ export {
49
+ adminRoleListResponseSchema,
50
+ adminRoleResponseSchema,
51
+ createAdminRoleRequestSchema,
52
+ deleteAdminRoleRequestSchema,
53
+ getAdminRoleRequestSchema,
54
+ getRolesForUserRequestSchema,
55
+ listAdminRolesRequestSchema,
56
+ reorderAdminRolesRequestSchema,
57
+ setRolesForUserRequestSchema,
58
+ updateAdminRoleRequestSchema,
59
+ userRolesResponseSchema,
60
+ } from './schemas.js'
61
+ export { AdminRolesService } from './service.js'
62
+ export type { AdminRolesCommandDeps } from './commands.js'
63
+ export type {
64
+ AdminRoleRow,
65
+ AdminRolesRepository,
66
+ CreateAdminRoleInput,
67
+ UpdateAdminRoleInput,
68
+ } from './repository.js'
69
+ export type {
70
+ AdminRoleListResponse,
71
+ AdminRoleResponse,
72
+ CreateAdminRoleRequest,
73
+ DeleteAdminRoleRequest,
74
+ GetAdminRoleRequest,
75
+ GetRolesForUserRequest,
76
+ ListAdminRolesRequest,
77
+ ReorderAdminRolesRequest,
78
+ SetRolesForUserRequest,
79
+ UpdateAdminRoleRequest,
80
+ UserRolesResponse,
81
+ } from './schemas.js'
@@ -0,0 +1,96 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ /**
10
+ * `AdminRolesRepository` — role CRUD plus role ↔ user assignments.
11
+ *
12
+ * Deliberately does *not* cover per-role ability grants — those live on
13
+ * `AdminPermissionsRepository` (`modules/admin-permissions/repository.ts`),
14
+ * which owns the `byline_admin_permissions` table. The split follows the
15
+ * admin UI: role identity and membership live together; ability grants
16
+ * are a separate editor surface driven by the ability registry.
17
+ *
18
+ * **Optimistic concurrency.** `update`, `delete`, and `reorder` take an
19
+ * `expectedVid` and bump the stored `vid` on success. Adapters throw
20
+ * `AdminRolesError(VERSION_CONFLICT)` when the stored vid differs from
21
+ * the expected one — typical client response is to reload the form.
22
+ *
23
+ * `machine_name` is **immutable post-create**. The `UpdateAdminRoleInput`
24
+ * type omits it deliberately so renaming the slug is a deliberate
25
+ * separate operation if it ever ships.
26
+ */
27
+
28
+ export interface AdminRoleRow {
29
+ id: string
30
+ vid: number
31
+ name: string
32
+ machine_name: string
33
+ description: string | null
34
+ order: number
35
+ created_at: Date
36
+ updated_at: Date
37
+ }
38
+
39
+ export interface CreateAdminRoleInput {
40
+ name: string
41
+ machine_name: string
42
+ description?: string | null
43
+ order?: number
44
+ }
45
+
46
+ export interface UpdateAdminRoleInput {
47
+ name?: string
48
+ description?: string | null
49
+ order?: number
50
+ }
51
+
52
+ export interface AdminRolesRepository {
53
+ create(input: CreateAdminRoleInput): Promise<AdminRoleRow>
54
+ getById(id: string): Promise<AdminRoleRow | null>
55
+ getByMachineName(machineName: string): Promise<AdminRoleRow | null>
56
+ /** Roles ordered by their `order` column then `created_at`. No paging — the list is small by design. */
57
+ list(): Promise<AdminRoleRow[]>
58
+ /**
59
+ * Content update with optimistic concurrency. Throws
60
+ * `AdminRolesError(VERSION_CONFLICT)` if the stored `vid` differs from
61
+ * `expectedVid`. Bumps `vid` on success and returns the fresh row.
62
+ */
63
+ update(id: string, expectedVid: number, patch: UpdateAdminRoleInput): Promise<AdminRoleRow>
64
+ /**
65
+ * Delete with optimistic concurrency. Version-gated on `expectedVid` to
66
+ * prevent races against a concurrent update.
67
+ */
68
+ delete(id: string, expectedVid: number): Promise<void>
69
+ /**
70
+ * Bulk reorder. The provided `ids` array fixes the new `order` value
71
+ * for each role to its index in the array. Runs in a single
72
+ * transaction. Roles not present in `ids` are left untouched.
73
+ *
74
+ * Vid-less by design — reorder mutates only the `order` column and
75
+ * the UX shape is always "drag, then save the whole list", which would
76
+ * pointlessly conflict with concurrent edits to other fields. Last
77
+ * writer on the order column wins.
78
+ */
79
+ reorder(ids: string[]): Promise<void>
80
+
81
+ /** Assign a role to a user. Idempotent via the composite primary key. */
82
+ assignToUser(roleId: string, userId: string): Promise<void>
83
+ unassignFromUser(roleId: string, userId: string): Promise<void>
84
+ listRolesForUser(userId: string): Promise<AdminRoleRow[]>
85
+ listUsersForRole(roleId: string): Promise<string[]>
86
+ /**
87
+ * Replace the role-set for a user wholesale. Runs in a single
88
+ * transaction so the user is never observed mid-edit. Vid-less by
89
+ * design — assignments live in the join table, not on the user row,
90
+ * and the editor UX is "drag, save the whole list" rather than
91
+ * field-level concurrency. Caller is responsible for validating that
92
+ * `userId` and every `roleId` exist; the repo trusts its inputs and
93
+ * lets the FK constraint surface as the ultimate backstop.
94
+ */
95
+ setRolesForUser(userId: string, roleIds: readonly string[]): Promise<void>
96
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ import { uuidSchema } from '@byline/core/validation'
10
+ import { z } from 'zod'
11
+
12
+ /**
13
+ * Zod request/response schemas for the admin-roles commands.
14
+ *
15
+ * `vid` gates writes for optimistic concurrency; `machine_name` is
16
+ * accepted only at create time and validated as a slug-shaped string.
17
+ *
18
+ * Reorder takes the full ordered id list — the index in the array
19
+ * becomes each role's new `order` value. The list-view UX is "drag,
20
+ * then save the whole order" so a partial-update payload would add no
21
+ * value and complicate atomicity.
22
+ */
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Field-level schemas
26
+ // ---------------------------------------------------------------------------
27
+
28
+ const idSchema = uuidSchema
29
+
30
+ const vidSchema = z
31
+ .number({ message: 'vid is required' })
32
+ .int({ message: 'vid must be an integer' })
33
+ .positive({ message: 'vid must be positive' })
34
+
35
+ const nameSchema = z.string().min(1).max(128)
36
+
37
+ const machineNameSchema = z
38
+ .string()
39
+ .min(1)
40
+ .max(128)
41
+ .regex(/^[a-z0-9][a-z0-9_-]*$/, {
42
+ message: 'machine_name may contain lowercase letters, numbers, hyphens, and underscores only',
43
+ })
44
+
45
+ const descriptionSchema = z.string().max(2000).nullish()
46
+
47
+ const orderSchema = z.number().int().min(0)
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Requests
51
+ // ---------------------------------------------------------------------------
52
+
53
+ export const listAdminRolesRequestSchema = z.object({}).optional()
54
+ export type ListAdminRolesRequest = z.infer<typeof listAdminRolesRequestSchema>
55
+
56
+ export const getAdminRoleRequestSchema = z.object({
57
+ id: idSchema,
58
+ })
59
+ export type GetAdminRoleRequest = z.infer<typeof getAdminRoleRequestSchema>
60
+
61
+ export const createAdminRoleRequestSchema = z.object({
62
+ name: nameSchema,
63
+ machine_name: machineNameSchema,
64
+ description: descriptionSchema,
65
+ order: orderSchema.optional(),
66
+ })
67
+ export type CreateAdminRoleRequest = z.infer<typeof createAdminRoleRequestSchema>
68
+
69
+ export const updateAdminRoleRequestSchema = z.object({
70
+ id: idSchema,
71
+ vid: vidSchema,
72
+ patch: z
73
+ .object({
74
+ name: nameSchema.optional(),
75
+ description: descriptionSchema,
76
+ order: orderSchema.optional(),
77
+ })
78
+ .refine((p) => Object.keys(p).length > 0, { message: 'patch cannot be empty' }),
79
+ })
80
+ export type UpdateAdminRoleRequest = z.infer<typeof updateAdminRoleRequestSchema>
81
+
82
+ export const deleteAdminRoleRequestSchema = z.object({
83
+ id: idSchema,
84
+ vid: vidSchema,
85
+ })
86
+ export type DeleteAdminRoleRequest = z.infer<typeof deleteAdminRoleRequestSchema>
87
+
88
+ export const reorderAdminRolesRequestSchema = z.object({
89
+ ids: z.array(idSchema).min(1, { message: 'at least one id is required' }),
90
+ })
91
+ export type ReorderAdminRolesRequest = z.infer<typeof reorderAdminRolesRequestSchema>
92
+
93
+ export const getRolesForUserRequestSchema = z.object({
94
+ userId: idSchema,
95
+ })
96
+ export type GetRolesForUserRequest = z.infer<typeof getRolesForUserRequestSchema>
97
+
98
+ export const setRolesForUserRequestSchema = z.object({
99
+ userId: idSchema,
100
+ roleIds: z.array(idSchema),
101
+ })
102
+ export type SetRolesForUserRequest = z.infer<typeof setRolesForUserRequestSchema>
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Responses
106
+ // ---------------------------------------------------------------------------
107
+
108
+ export const adminRoleResponseSchema = z.object({
109
+ id: z.string(),
110
+ vid: z.number().int(),
111
+ name: z.string(),
112
+ machine_name: z.string(),
113
+ description: z.string().nullable(),
114
+ order: z.number().int(),
115
+ created_at: z.date(),
116
+ updated_at: z.date(),
117
+ })
118
+ export type AdminRoleResponse = z.infer<typeof adminRoleResponseSchema>
119
+
120
+ export const adminRoleListResponseSchema = z.object({
121
+ roles: z.array(adminRoleResponseSchema),
122
+ })
123
+ export type AdminRoleListResponse = z.infer<typeof adminRoleListResponseSchema>
124
+
125
+ /**
126
+ * User-roles editor payload. `userId` is echoed back so the caller can
127
+ * match async writes; `roles` is the authoritative role-set after the
128
+ * write, shaped as full role rows so the drawer renders names without a
129
+ * second fetch.
130
+ */
131
+ export const userRolesResponseSchema = z.object({
132
+ userId: z.string(),
133
+ roles: z.array(adminRoleResponseSchema),
134
+ })
135
+ export type UserRolesResponse = z.infer<typeof userRolesResponseSchema>
136
+
137
+ /** Empty response for void-returning mutations (delete, reorder). */
138
+ export const okResponseSchema = z.object({ ok: z.literal(true) })
139
+ export type OkResponse = z.infer<typeof okResponseSchema>