@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,323 @@
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
+ * Create-admin-user drawer form.
13
+ *
14
+ * Rendered inside the list-view drawer. Same TanStack Form + Zod shape
15
+ * as the AccountDetails form — fields, blur-then-change validation,
16
+ * form-level alerts for errors that don't map onto a single field. On
17
+ * success the parent list-view navigates to the new user's detail page
18
+ * so the admin can finish configuring (roles, initial enablement).
19
+ *
20
+ * Extension point: future props (e.g. a `roles` array once the
21
+ * admin-roles module lands) plug in here — the list-route loader
22
+ * already runs in parallel, so adding a side-data fetch is a one-line
23
+ * change in the loader and a prop here.
24
+ */
25
+
26
+ import { useState } from 'react'
27
+ import { revalidateLogic, useForm } from '@tanstack/react-form-start'
28
+
29
+ import { passwordSchema } from '@byline/core/validation'
30
+ import { Alert, Button, Checkbox, Input, LoaderEllipsis } from '@byline/ui/react'
31
+ import cx from 'classnames'
32
+ import { z } from 'zod'
33
+
34
+ import { useBylineAdminServices } from '../../../services/admin-services-context.js'
35
+ import styles from './create.module.css'
36
+ import type { AdminUserResponse } from '../index.js'
37
+
38
+ const createAdminUserFormSchema = z.object({
39
+ email: z
40
+ .email({ message: 'Enter a valid email address' })
41
+ .min(3)
42
+ .max(254, 'Email must not exceed 254 characters'),
43
+ password: passwordSchema,
44
+ given_name: z.string().max(100, 'Given name must not exceed 100 characters'),
45
+ family_name: z.string().max(100, 'Family name must not exceed 100 characters'),
46
+ username: z.string().max(100, 'Username must not exceed 100 characters'),
47
+ is_super_admin: z.boolean(),
48
+ is_enabled: z.boolean(),
49
+ is_email_verified: z.boolean(),
50
+ })
51
+
52
+ type CreateAdminUserValues = z.infer<typeof createAdminUserFormSchema>
53
+
54
+ const initialValues: CreateAdminUserValues = {
55
+ email: '',
56
+ password: '',
57
+ given_name: '',
58
+ family_name: '',
59
+ username: '',
60
+ is_super_admin: false,
61
+ // Sensible defaults for an admin-created row. A brand-new admin user
62
+ // is almost always meant to sign in straight away; the admin picks
63
+ // whether they arrive pre-verified.
64
+ is_enabled: true,
65
+ is_email_verified: false,
66
+ }
67
+
68
+ function normaliseText(value: string): string | null {
69
+ return value.trim().length > 0 ? value : null
70
+ }
71
+
72
+ interface CreateAdminUserProps {
73
+ onClose?: () => void
74
+ /** Called on successful create with the new user so the parent can navigate. */
75
+ onSuccess?: (user: AdminUserResponse) => void
76
+ }
77
+
78
+ export function CreateAdminUser({ onClose, onSuccess }: CreateAdminUserProps) {
79
+ const { createAdminUser } = useBylineAdminServices()
80
+ const [formError, setFormError] = useState<string | null>(null)
81
+
82
+ const form = useForm({
83
+ defaultValues: initialValues,
84
+ validationLogic: revalidateLogic({
85
+ mode: 'blur',
86
+ modeAfterSubmission: 'change',
87
+ }),
88
+ validators: {
89
+ onDynamic: createAdminUserFormSchema,
90
+ },
91
+ onSubmit: async ({ value }) => {
92
+ setFormError(null)
93
+ try {
94
+ const created = await createAdminUser({
95
+ data: {
96
+ email: value.email,
97
+ password: value.password,
98
+ given_name: normaliseText(value.given_name),
99
+ family_name: normaliseText(value.family_name),
100
+ username: normaliseText(value.username),
101
+ is_super_admin: value.is_super_admin,
102
+ is_enabled: value.is_enabled,
103
+ is_email_verified: value.is_email_verified,
104
+ },
105
+ })
106
+ // Clear the form so the next drawer open starts fresh — the
107
+ // list-view shows the success as a toast and keeps the drawer
108
+ // closed until the user clicks "+" again.
109
+ form.reset(initialValues)
110
+ onSuccess?.(created)
111
+ } catch (err) {
112
+ const code = getErrorCode(err)
113
+ if (code === 'admin.users.emailInUse') {
114
+ form.setFieldMeta('email', (meta) => ({
115
+ ...meta,
116
+ errorMap: { ...meta.errorMap, onServer: 'This email is already in use.' },
117
+ errors: ['This email is already in use.'],
118
+ }))
119
+ return
120
+ }
121
+ setFormError('Could not create this admin user. Please try again.')
122
+ }
123
+ },
124
+ })
125
+
126
+ return (
127
+ <div className={cx('byline-user-create-wrap', styles.wrap)}>
128
+ <form
129
+ noValidate
130
+ onSubmit={(event) => {
131
+ event.preventDefault()
132
+ event.stopPropagation()
133
+ void form.handleSubmit()
134
+ }}
135
+ className={cx('byline-user-create-form', styles.form)}
136
+ >
137
+ {formError ? <Alert intent="danger">{formError}</Alert> : null}
138
+
139
+ <div className={cx('byline-user-create-grid', styles.grid)}>
140
+ <form.Field name="given_name">
141
+ {(field) => (
142
+ <Input
143
+ label="Given name"
144
+ id="new-given-name"
145
+ name={field.name}
146
+ value={field.state.value}
147
+ onBlur={field.handleBlur}
148
+ onChange={(e) => field.handleChange(e.currentTarget.value)}
149
+ error={field.state.meta.errors.length > 0}
150
+ errorText={firstError(field.state.meta.errors)}
151
+ autoComplete="given-name"
152
+ />
153
+ )}
154
+ </form.Field>
155
+
156
+ <form.Field name="family_name">
157
+ {(field) => (
158
+ <Input
159
+ label="Family name"
160
+ id="new-family-name"
161
+ name={field.name}
162
+ value={field.state.value}
163
+ onBlur={field.handleBlur}
164
+ onChange={(e) => field.handleChange(e.currentTarget.value)}
165
+ error={field.state.meta.errors.length > 0}
166
+ errorText={firstError(field.state.meta.errors)}
167
+ autoComplete="family-name"
168
+ />
169
+ )}
170
+ </form.Field>
171
+ </div>
172
+
173
+ <form.Field name="username">
174
+ {(field) => (
175
+ <Input
176
+ label="Username"
177
+ id="new-username"
178
+ name={field.name}
179
+ value={field.state.value}
180
+ onBlur={field.handleBlur}
181
+ onChange={(e) => field.handleChange(e.currentTarget.value)}
182
+ error={field.state.meta.errors.length > 0}
183
+ errorText={firstError(field.state.meta.errors)}
184
+ helpText="Optional."
185
+ autoComplete="username"
186
+ />
187
+ )}
188
+ </form.Field>
189
+
190
+ <form.Field name="email">
191
+ {(field) => (
192
+ <Input
193
+ label="Email"
194
+ id="new-email"
195
+ name={field.name}
196
+ type="email"
197
+ value={field.state.value}
198
+ onBlur={field.handleBlur}
199
+ onChange={(e) => field.handleChange(e.currentTarget.value)}
200
+ error={field.state.meta.errors.length > 0}
201
+ errorText={firstError(field.state.meta.errors)}
202
+ autoComplete="email"
203
+ required
204
+ />
205
+ )}
206
+ </form.Field>
207
+
208
+ <form.Field name="password">
209
+ {(field) => (
210
+ <Input
211
+ label="Initial password"
212
+ id="new-password"
213
+ name={field.name}
214
+ type="password"
215
+ value={field.state.value}
216
+ onBlur={field.handleBlur}
217
+ onChange={(e) => field.handleChange(e.currentTarget.value)}
218
+ error={field.state.meta.errors.length > 0}
219
+ errorText={firstError(field.state.meta.errors)}
220
+ helpText="The user can change it from their own account after signing in."
221
+ autoComplete="new-password"
222
+ required
223
+ />
224
+ )}
225
+ </form.Field>
226
+
227
+ <div className={cx('byline-user-create-flags', styles.flags)}>
228
+ <form.Field name="is_enabled">
229
+ {(field) => (
230
+ <Checkbox
231
+ id="new-is-enabled"
232
+ name={field.name}
233
+ label="Enabled"
234
+ checked={field.state.value}
235
+ onCheckedChange={(checked) => field.handleChange(checked === true)}
236
+ helpText="Disabled accounts cannot sign in."
237
+ />
238
+ )}
239
+ </form.Field>
240
+
241
+ <form.Field name="is_email_verified">
242
+ {(field) => (
243
+ <Checkbox
244
+ id="new-is-email-verified"
245
+ name={field.name}
246
+ label="Email verified"
247
+ checked={field.state.value}
248
+ onCheckedChange={(checked) => field.handleChange(checked === true)}
249
+ helpText="Skip the verification flow for this account."
250
+ />
251
+ )}
252
+ </form.Field>
253
+
254
+ <form.Field name="is_super_admin">
255
+ {(field) => (
256
+ <Checkbox
257
+ id="new-is-super-admin"
258
+ name={field.name}
259
+ label="Super admin"
260
+ checked={field.state.value}
261
+ onCheckedChange={(checked) => field.handleChange(checked === true)}
262
+ helpText="Super admins bypass every ability check — grant with care."
263
+ />
264
+ )}
265
+ </form.Field>
266
+ </div>
267
+
268
+ <div className={cx('byline-user-create-actions', styles.actions)}>
269
+ <Button
270
+ type="button"
271
+ intent="secondary"
272
+ size="sm"
273
+ onClick={onClose}
274
+ className={cx('byline-user-create-action', styles.action)}
275
+ >
276
+ Cancel
277
+ </Button>
278
+ <form.Subscribe
279
+ selector={(state) => ({
280
+ canSubmit: state.canSubmit,
281
+ isSubmitting: state.isSubmitting,
282
+ })}
283
+ >
284
+ {({ canSubmit, isSubmitting }) => (
285
+ <Button
286
+ size="sm"
287
+ intent="primary"
288
+ type="submit"
289
+ disabled={!canSubmit || isSubmitting}
290
+ className={cx('byline-user-create-action', styles.action)}
291
+ >
292
+ {isSubmitting === true ? <LoaderEllipsis size={42} /> : 'Save'}
293
+ </Button>
294
+ )}
295
+ </form.Subscribe>
296
+ </div>
297
+ </form>
298
+ </div>
299
+ )
300
+ }
301
+
302
+ function firstError(errors: readonly unknown[]): string | undefined {
303
+ for (const err of errors) {
304
+ if (typeof err === 'string') return err
305
+ if (err && typeof err === 'object' && 'message' in err) {
306
+ const msg = (err as { message?: unknown }).message
307
+ if (typeof msg === 'string') return msg
308
+ }
309
+ }
310
+ return undefined
311
+ }
312
+
313
+ function getErrorCode(err: unknown): string | null {
314
+ if (err && typeof err === 'object') {
315
+ const e = err as { code?: unknown; cause?: unknown }
316
+ if (typeof e.code === 'string') return e.code
317
+ if (e.cause && typeof e.cause === 'object' && 'code' in e.cause) {
318
+ const cause = e.cause as { code?: unknown }
319
+ if (typeof cause.code === 'string') return cause.code
320
+ }
321
+ }
322
+ return null
323
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * UserRoles — drawer for assigning roles to a user.
3
+ *
4
+ * Override handles:
5
+ * .byline-user-roles-wrap — outer container
6
+ * .byline-user-roles-empty — "no roles created yet" empty-state
7
+ * .byline-user-roles-empty-hint — inline machine-name hint
8
+ * .byline-user-roles-list — flat checkbox list
9
+ * .byline-user-roles-row — single role row
10
+ * .byline-user-roles-label — clickable label
11
+ * .byline-user-roles-label-head — name + machine-name pill row
12
+ * .byline-user-roles-name — role display name
13
+ * .byline-user-roles-machine — inline code pill (machine name)
14
+ * .byline-user-roles-description — role description
15
+ * .byline-user-roles-actions — Cancel/Save row
16
+ * .byline-user-roles-action — buttons in the actions row
17
+ */
18
+
19
+ .wrap,
20
+ :global(.byline-user-roles-wrap) {
21
+ display: flex;
22
+ flex-direction: column;
23
+ gap: var(--spacing-12);
24
+ padding: var(--spacing-4);
25
+ margin-top: var(--spacing-4);
26
+ }
27
+
28
+ .empty,
29
+ :global(.byline-user-roles-empty) {
30
+ margin: 0;
31
+ font-style: italic;
32
+ }
33
+
34
+ .empty-hint,
35
+ :global(.byline-user-roles-empty-hint) {
36
+ /* Inherits muted colour from the parent .muted utility. */
37
+ }
38
+
39
+ .list,
40
+ :global(.byline-user-roles-list) {
41
+ display: flex;
42
+ flex-direction: column;
43
+ gap: var(--spacing-4);
44
+ }
45
+
46
+ .row,
47
+ :global(.byline-user-roles-row) {
48
+ display: flex;
49
+ align-items: flex-start;
50
+ gap: var(--spacing-8);
51
+ padding: var(--spacing-4) 0;
52
+ }
53
+
54
+ .label,
55
+ :global(.byline-user-roles-label) {
56
+ min-width: 0;
57
+ flex: 1 1 0;
58
+ cursor: pointer;
59
+ }
60
+
61
+ .label-head,
62
+ :global(.byline-user-roles-label-head) {
63
+ display: flex;
64
+ align-items: center;
65
+ gap: var(--spacing-8);
66
+ }
67
+
68
+ .name,
69
+ :global(.byline-user-roles-name) {
70
+ font-size: var(--font-size-sm);
71
+ font-weight: var(--font-weight-medium);
72
+ }
73
+
74
+ .machine,
75
+ :global(.byline-user-roles-machine) {
76
+ background-color: var(--gray-100);
77
+ padding: 0.125rem 0.375rem;
78
+ font-size: var(--font-size-xs);
79
+ border-radius: var(--border-radius-sm);
80
+ }
81
+
82
+ .description,
83
+ :global(.byline-user-roles-description) {
84
+ margin-bottom: 0;
85
+ font-size: var(--font-size-xs);
86
+ }
87
+
88
+ .actions,
89
+ :global(.byline-user-roles-actions) {
90
+ display: flex;
91
+ align-items: center;
92
+ justify-content: flex-end;
93
+ gap: var(--spacing-8);
94
+ margin-top: var(--spacing-16);
95
+ }
96
+
97
+ .action,
98
+ :global(.byline-user-roles-action) {
99
+ min-width: 4rem;
100
+ }
101
+
102
+ /*
103
+ * Checkbox shrink-fit — see admin-roles/permissions.module.css for the
104
+ * rationale; the same override is needed here for the per-user role
105
+ * checkbox list. `!important` is required to win over the uikit
106
+ * Checkbox's `width: 100%` default at equal specificity.
107
+ */
108
+ .checkbox-auto,
109
+ :global(.byline-user-roles-checkbox-auto) {
110
+ /* biome-ignore lint/complexity/noImportantStyles: see admin-roles/permissions.module.css */
111
+ width: auto !important;
112
+ }
113
+
114
+ :is([data-theme="dark"], :global(.dark)) {
115
+ .machine,
116
+ :global(.byline-user-roles-machine) {
117
+ background-color: var(--canvas-800);
118
+ }
119
+ }
@@ -0,0 +1,172 @@
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
+ * User-roles drawer.
13
+ *
14
+ * Renders a flat checkbox list of every available role, pre-checked
15
+ * from the user's current assignments. On save we wholesale-replace
16
+ * the user's role-set via `setUserRoles`; the response carries the
17
+ * authoritative stored set so the editor's "initial" baseline resets
18
+ * cleanly.
19
+ *
20
+ * Standard drawer pattern (no view/edit mode toggle) — role lists are
21
+ * short by design and the drawer is a short-lived edit context, not
22
+ * a steady-state inspector. Save + Cancel are always visible; Save
23
+ * is disabled until dirty.
24
+ */
25
+
26
+ import { useState } from 'react'
27
+
28
+ import { Alert, Button, Checkbox, LoaderEllipsis } from '@byline/ui/react'
29
+ import cx from 'classnames'
30
+
31
+ import { useBylineAdminServices } from '../../../services/admin-services-context.js'
32
+ import styles from './roles.module.css'
33
+ import type { AdminRoleResponse, UserRolesResponse } from '../../admin-roles/index.js'
34
+ import type { AdminUserResponse } from '../index.js'
35
+
36
+ interface UserRolesProps {
37
+ user: AdminUserResponse
38
+ allRoles: AdminRoleResponse[]
39
+ initialRoleIds: string[]
40
+ onClose?: () => void
41
+ onSaved?: (response: UserRolesResponse) => void
42
+ }
43
+
44
+ function setsEqual(a: ReadonlySet<string>, b: ReadonlySet<string>): boolean {
45
+ if (a.size !== b.size) return false
46
+ for (const item of a) if (!b.has(item)) return false
47
+ return true
48
+ }
49
+
50
+ export function UserRoles({ user, allRoles, initialRoleIds, onClose, onSaved }: UserRolesProps) {
51
+ const { setUserRoles } = useBylineAdminServices()
52
+ const [initialSet, setInitialSet] = useState<ReadonlySet<string>>(() => new Set(initialRoleIds))
53
+ const [selected, setSelected] = useState<Set<string>>(() => new Set(initialRoleIds))
54
+ const [saving, setSaving] = useState(false)
55
+ const [error, setError] = useState<string | null>(null)
56
+ const [successMessage, setSuccessMessage] = useState<string | null>(null)
57
+
58
+ const isDirty = !setsEqual(selected, initialSet)
59
+
60
+ function handleToggle(roleId: string, checked: boolean): void {
61
+ setSelected((current) => {
62
+ const next = new Set(current)
63
+ if (checked) next.add(roleId)
64
+ else next.delete(roleId)
65
+ return next
66
+ })
67
+ setSuccessMessage(null)
68
+ }
69
+
70
+ async function handleSave(): Promise<void> {
71
+ if (saving) return
72
+ setSaving(true)
73
+ setError(null)
74
+ setSuccessMessage(null)
75
+ try {
76
+ const response = await setUserRoles({
77
+ data: { userId: user.id, roleIds: Array.from(selected) },
78
+ })
79
+ const storedSet = new Set(response.roles.map((r) => r.id))
80
+ setInitialSet(storedSet)
81
+ setSelected(new Set(storedSet))
82
+ setSuccessMessage('Saved.')
83
+ onSaved?.(response)
84
+ } catch (err) {
85
+ const code = getErrorCode(err)
86
+ if (code === 'admin.roles.userNotFound') {
87
+ setError('This user no longer exists.')
88
+ } else if (code === 'admin.roles.notFound') {
89
+ setError('One or more selected roles no longer exist. Reload the page and try again.')
90
+ } else {
91
+ setError('Could not save roles. Please try again.')
92
+ }
93
+ } finally {
94
+ setSaving(false)
95
+ }
96
+ }
97
+
98
+ return (
99
+ <div className={cx('byline-user-roles-wrap', styles.wrap)}>
100
+ {error ? <Alert intent="danger">{error}</Alert> : null}
101
+ {successMessage ? <Alert intent="success">{successMessage}</Alert> : null}
102
+
103
+ {allRoles.length === 0 ? (
104
+ <p className={cx('muted', 'byline-user-roles-empty', styles.empty)}>
105
+ No roles have been created yet. Create roles in{' '}
106
+ <span className="muted">/admin/roles</span> first.
107
+ </p>
108
+ ) : (
109
+ <div className={cx('byline-user-roles-list', styles.list)}>
110
+ {allRoles.map((role) => (
111
+ <div key={role.id} className={cx('byline-user-roles-row', styles.row)}>
112
+ <Checkbox
113
+ id={`role-${role.id}`}
114
+ name={`role-${role.id}`}
115
+ checked={selected.has(role.id)}
116
+ disabled={saving}
117
+ onCheckedChange={(checked) => handleToggle(role.id, checked === true)}
118
+ containerClasses={cx('byline-user-roles-checkbox-auto', styles['checkbox-auto'])}
119
+ componentClasses={cx('byline-user-roles-checkbox-auto', styles['checkbox-auto'])}
120
+ />
121
+ <label
122
+ htmlFor={`role-${role.id}`}
123
+ className={cx('byline-user-roles-label', styles.label)}
124
+ >
125
+ <div className={cx('byline-user-roles-label-head', styles['label-head'])}>
126
+ <span className={cx('byline-user-roles-name', styles.name)}>{role.name}</span>
127
+ <code className={cx('byline-user-roles-machine', styles.machine)}>
128
+ {role.machine_name}
129
+ </code>
130
+ </div>
131
+ {role.description ? (
132
+ <p className={cx('muted', 'byline-user-roles-description', styles.description)}>
133
+ {role.description}
134
+ </p>
135
+ ) : null}
136
+ </label>
137
+ </div>
138
+ ))}
139
+ </div>
140
+ )}
141
+
142
+ <div className={cx('byline-user-roles-actions', styles.actions)}>
143
+ <Button
144
+ type="button"
145
+ intent="secondary"
146
+ size="xs"
147
+ onClick={onClose}
148
+ disabled={saving}
149
+ className={cx('byline-user-roles-action', styles.action)}
150
+ >
151
+ {successMessage ? 'Close' : 'Cancel'}
152
+ </Button>
153
+ <Button
154
+ type="button"
155
+ intent="primary"
156
+ size="xs"
157
+ onClick={() => void handleSave()}
158
+ disabled={saving || !isDirty}
159
+ className={cx('byline-user-roles-action', styles.action)}
160
+ >
161
+ {saving ? <LoaderEllipsis size={30} /> : 'Save'}
162
+ </Button>
163
+ </div>
164
+ </div>
165
+ )
166
+ }
167
+
168
+ function getErrorCode(err: unknown): string | null {
169
+ return typeof (err as { code?: unknown })?.code === 'string'
170
+ ? (err as { code: string }).code
171
+ : null
172
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * SetAdminUserPassword — privileged "set password" drawer form.
3
+ *
4
+ * Override handles:
5
+ * .byline-user-set-password-wrap — outer container
6
+ * .byline-user-set-password-form — vertical-stack form element
7
+ * .byline-user-set-password-target — bold target email span in the lead-in
8
+ * .byline-user-set-password-actions — Cancel/Save row
9
+ * .byline-user-set-password-action — buttons in the actions row
10
+ */
11
+
12
+ .wrap,
13
+ :global(.byline-user-set-password-wrap) {
14
+ display: flex;
15
+ flex-direction: column;
16
+ gap: var(--spacing-8);
17
+ padding: var(--spacing-4);
18
+ margin-top: var(--spacing-4);
19
+ }
20
+
21
+ .form,
22
+ :global(.byline-user-set-password-form) {
23
+ display: flex;
24
+ flex-direction: column;
25
+ gap: var(--spacing-16);
26
+ padding-top: var(--spacing-8);
27
+ }
28
+
29
+ .target,
30
+ :global(.byline-user-set-password-target) {
31
+ font-weight: var(--font-weight-semibold);
32
+ }
33
+
34
+ .actions,
35
+ :global(.byline-user-set-password-actions) {
36
+ display: flex;
37
+ align-items: center;
38
+ justify-content: flex-end;
39
+ gap: var(--spacing-8);
40
+ margin-top: var(--spacing-16);
41
+ }
42
+
43
+ .action,
44
+ :global(.byline-user-set-password-action) {
45
+ min-width: 4rem;
46
+ }