@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,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>
|