@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.
- package/dist/abilities.js +5 -24
- package/dist/index.js +8 -30
- package/dist/lib/assert-admin-actor.js +13 -74
- package/dist/lib/create-command.js +6 -16
- package/dist/modules/admin-account/commands.js +35 -24
- package/dist/modules/admin-account/components/change-password.d.ts +8 -0
- package/dist/modules/admin-account/components/change-password.js +192 -0
- package/dist/modules/admin-account/components/change-password.module.js +8 -0
- package/dist/modules/admin-account/components/change-password_module.css +27 -0
- package/dist/modules/admin-account/components/container.d.ts +29 -0
- package/dist/modules/admin-account/components/container.js +298 -0
- package/dist/modules/admin-account/components/container.module.js +28 -0
- package/dist/modules/admin-account/components/container_module.css +106 -0
- package/dist/modules/admin-account/components/update.d.ts +8 -0
- package/dist/modules/admin-account/components/update.js +207 -0
- package/dist/modules/admin-account/components/update.module.js +8 -0
- package/dist/modules/admin-account/components/update_module.css +27 -0
- package/dist/modules/admin-account/errors.js +14 -45
- package/dist/modules/admin-account/index.js +4 -34
- package/dist/modules/admin-account/schemas.js +25 -59
- package/dist/modules/admin-account/service.js +56 -61
- package/dist/modules/admin-permissions/abilities.js +6 -24
- package/dist/modules/admin-permissions/commands.js +42 -28
- package/dist/modules/admin-permissions/components/inspector.d.ts +4 -0
- package/dist/modules/admin-permissions/components/inspector.js +284 -0
- package/dist/modules/admin-permissions/components/inspector.module.js +56 -0
- package/dist/modules/admin-permissions/components/inspector_module.css +238 -0
- package/dist/modules/admin-permissions/dto.js +3 -16
- package/dist/modules/admin-permissions/errors.js +14 -27
- package/dist/modules/admin-permissions/index.js +6 -26
- package/dist/modules/admin-permissions/repository.js +1 -8
- package/dist/modules/admin-permissions/schemas.js +33 -70
- package/dist/modules/admin-permissions/service.js +88 -92
- package/dist/modules/admin-roles/abilities.js +8 -30
- package/dist/modules/admin-roles/commands.js +89 -55
- package/dist/modules/admin-roles/components/create.d.ts +7 -0
- package/dist/modules/admin-roles/components/create.js +177 -0
- package/dist/modules/admin-roles/components/create.module.js +8 -0
- package/dist/modules/admin-roles/components/create_module.css +27 -0
- package/dist/modules/admin-roles/components/permissions.d.ts +10 -0
- package/dist/modules/admin-roles/components/permissions.js +303 -0
- package/dist/modules/admin-roles/components/permissions.module.js +44 -0
- package/dist/modules/admin-roles/components/permissions_module.css +192 -0
- package/dist/modules/admin-roles/components/update.d.ts +8 -0
- package/dist/modules/admin-roles/components/update.js +166 -0
- package/dist/modules/admin-roles/components/update.module.js +8 -0
- package/dist/modules/admin-roles/components/update_module.css +27 -0
- package/dist/modules/admin-roles/dto.js +3 -16
- package/dist/modules/admin-roles/errors.js +16 -40
- package/dist/modules/admin-roles/index.js +6 -26
- package/dist/modules/admin-roles/repository.js +1 -8
- package/dist/modules/admin-roles/schemas.js +41 -71
- package/dist/modules/admin-roles/service.js +79 -82
- package/dist/modules/admin-users/abilities.js +9 -38
- package/dist/modules/admin-users/commands.js +92 -50
- package/dist/modules/admin-users/components/create.d.ts +8 -0
- package/dist/modules/admin-users/components/create.js +268 -0
- package/dist/modules/admin-users/components/create.module.js +10 -0
- package/dist/modules/admin-users/components/create_module.css +45 -0
- package/dist/modules/admin-users/components/roles.d.ts +11 -0
- package/dist/modules/admin-users/components/roles.js +148 -0
- package/dist/modules/admin-users/components/roles.module.js +18 -0
- package/dist/modules/admin-users/components/roles_module.css +75 -0
- package/dist/modules/admin-users/components/set-password.d.ts +8 -0
- package/dist/modules/admin-users/components/set-password.js +170 -0
- package/dist/modules/admin-users/components/set-password.module.js +9 -0
- package/dist/modules/admin-users/components/set-password_module.css +31 -0
- package/dist/modules/admin-users/components/update.d.ts +8 -0
- package/dist/modules/admin-users/components/update.js +254 -0
- package/dist/modules/admin-users/components/update.module.js +9 -0
- package/dist/modules/admin-users/components/update_module.css +34 -0
- package/dist/modules/admin-users/dto.js +3 -18
- package/dist/modules/admin-users/errors.js +17 -43
- package/dist/modules/admin-users/index.js +7 -27
- package/dist/modules/admin-users/repository.js +1 -8
- package/dist/modules/admin-users/schemas.js +44 -75
- package/dist/modules/admin-users/seed-super-admin.js +9 -34
- package/dist/modules/admin-users/service.js +76 -91
- package/dist/modules/auth/components/sign-in-form.d.ts +12 -0
- package/dist/modules/auth/components/sign-in-form.js +115 -0
- package/dist/modules/auth/components/sign-in-form.module.js +12 -0
- package/dist/modules/auth/components/sign-in-form_module.css +41 -0
- package/dist/modules/auth/index.js +3 -24
- package/dist/modules/auth/jwt-session-provider.js +179 -149
- package/dist/modules/auth/password.js +11 -53
- package/dist/modules/auth/phc.js +21 -54
- package/dist/modules/auth/refresh-tokens-repository.js +1 -8
- package/dist/modules/auth/resolve-actor.js +6 -28
- package/dist/services/admin-services-context.d.ts +16 -0
- package/dist/services/admin-services-context.js +13 -0
- package/dist/services/admin-services-types.d.ts +129 -0
- package/dist/services/admin-services-types.js +1 -0
- package/dist/store.js +1 -8
- package/dist/vendor/noble-argon2/_blake.js +277 -45
- package/dist/vendor/noble-argon2/_md.js +81 -136
- package/dist/vendor/noble-argon2/_u64.js +65 -67
- package/dist/vendor/noble-argon2/argon2.js +181 -342
- package/dist/vendor/noble-argon2/blake2.js +252 -327
- package/dist/vendor/noble-argon2/utils.js +110 -490
- package/dist/vendor/noble-argon2/utils.js.LICENSE.txt +1 -0
- package/package.json +89 -10
- package/src/abilities.ts +32 -0
- package/src/declarations.d.ts +4 -0
- package/src/index.ts +39 -0
- package/src/lib/assert-admin-actor.ts +90 -0
- package/src/lib/create-command.ts +109 -0
- package/src/modules/admin-account/commands.ts +76 -0
- package/src/modules/admin-account/components/change-password.module.css +40 -0
- package/src/modules/admin-account/components/change-password.tsx +232 -0
- package/src/modules/admin-account/components/container.module.css +158 -0
- package/src/modules/admin-account/components/container.tsx +229 -0
- package/src/modules/admin-account/components/update.module.css +40 -0
- package/src/modules/admin-account/components/update.tsx +263 -0
- package/src/modules/admin-account/errors.ts +75 -0
- package/src/modules/admin-account/index.ts +60 -0
- package/src/modules/admin-account/schemas.ts +84 -0
- package/src/modules/admin-account/service.ts +92 -0
- package/src/modules/admin-permissions/abilities.ts +46 -0
- package/src/modules/admin-permissions/commands.ts +103 -0
- package/src/modules/admin-permissions/components/inspector.module.css +326 -0
- package/src/modules/admin-permissions/components/inspector.tsx +298 -0
- package/src/modules/admin-permissions/dto.ts +28 -0
- package/src/modules/admin-permissions/errors.ts +57 -0
- package/src/modules/admin-permissions/index.ts +72 -0
- package/src/modules/admin-permissions/repository.ts +49 -0
- package/src/modules/admin-permissions/schemas.ts +128 -0
- package/src/modules/admin-permissions/service.ts +137 -0
- package/src/modules/admin-roles/abilities.ts +62 -0
- package/src/modules/admin-roles/commands.ts +161 -0
- package/src/modules/admin-roles/components/create.module.css +40 -0
- package/src/modules/admin-roles/components/create.tsx +218 -0
- package/src/modules/admin-roles/components/permissions.module.css +279 -0
- package/src/modules/admin-roles/components/permissions.tsx +396 -0
- package/src/modules/admin-roles/components/update.module.css +40 -0
- package/src/modules/admin-roles/components/update.tsx +218 -0
- package/src/modules/admin-roles/dto.ts +30 -0
- package/src/modules/admin-roles/errors.ts +76 -0
- package/src/modules/admin-roles/index.ts +81 -0
- package/src/modules/admin-roles/repository.ts +96 -0
- package/src/modules/admin-roles/schemas.ts +139 -0
- package/src/modules/admin-roles/service.ts +136 -0
- package/src/modules/admin-users/abilities.ts +76 -0
- package/src/modules/admin-users/commands.ts +157 -0
- package/src/modules/admin-users/components/create.module.css +63 -0
- package/src/modules/admin-users/components/create.tsx +323 -0
- package/src/modules/admin-users/components/roles.module.css +119 -0
- package/src/modules/admin-users/components/roles.tsx +172 -0
- package/src/modules/admin-users/components/set-password.module.css +46 -0
- package/src/modules/admin-users/components/set-password.tsx +199 -0
- package/src/modules/admin-users/components/update.module.css +49 -0
- package/src/modules/admin-users/components/update.tsx +328 -0
- package/src/modules/admin-users/dto.ts +39 -0
- package/src/modules/admin-users/errors.ts +84 -0
- package/src/modules/admin-users/index.ts +91 -0
- package/src/modules/admin-users/repository.ts +161 -0
- package/src/modules/admin-users/schemas.ts +168 -0
- package/src/modules/admin-users/seed-super-admin.ts +102 -0
- package/src/modules/admin-users/service.ts +166 -0
- package/src/modules/auth/components/sign-in-form.module.css +62 -0
- package/src/modules/auth/components/sign-in-form.tsx +132 -0
- package/src/modules/auth/index.ts +31 -0
- package/src/modules/auth/jwt-session-provider.ts +301 -0
- package/src/modules/auth/password.ts +94 -0
- package/src/modules/auth/phc.ts +121 -0
- package/src/modules/auth/refresh-tokens-repository.ts +74 -0
- package/src/modules/auth/resolve-actor.ts +42 -0
- package/src/services/admin-services-context.tsx +52 -0
- package/src/services/admin-services-types.ts +177 -0
- package/src/store.ts +32 -0
- package/src/vendor/noble-argon2/LICENSE +21 -0
- package/src/vendor/noble-argon2/README.md +87 -0
- package/src/vendor/noble-argon2/_blake.ts +58 -0
- package/src/vendor/noble-argon2/_md.ts +223 -0
- package/src/vendor/noble-argon2/_u64.ts +118 -0
- package/src/vendor/noble-argon2/argon2.ts +668 -0
- package/src/vendor/noble-argon2/blake2.ts +583 -0
- 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
|
+
}
|