@byline/admin 2.4.0 → 2.4.2

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,263 @@
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
+ * Self-service profile form.
13
+ *
14
+ * Editable surface is intentionally narrower than the admin-users
15
+ * `update.tsx`: no `is_super_admin`, `is_enabled`, or
16
+ * `is_email_verified` toggles. A user cannot grant themselves
17
+ * super-admin or flip their own status — those flow through the
18
+ * admin-users module on a privileged admin's session.
19
+ *
20
+ * Patch is built diff-style against the loaded row and submitted with
21
+ * the row's `vid` so a concurrent edit elsewhere surfaces as
22
+ * `admin.users.versionConflict` and we prompt for reload.
23
+ */
24
+
25
+ import { useState } from 'react'
26
+ import { revalidateLogic, useForm } from '@tanstack/react-form-start'
27
+
28
+ import { Alert, Button, Input, LoaderEllipsis } from '@byline/ui/react'
29
+ import cx from 'classnames'
30
+ import { z } from 'zod'
31
+
32
+ import { useBylineAdminServices } from '../../../services/admin-services-context.js'
33
+ import styles from './update.module.css'
34
+ import type { AccountResponse } from '../index.js'
35
+
36
+ const updateAccountSchema = z.object({
37
+ given_name: z.string().max(100, 'Given name must not exceed 100 characters'),
38
+ family_name: z.string().max(100, 'Family name must not exceed 100 characters'),
39
+ username: z.string().max(100, 'Username must not exceed 100 characters'),
40
+ email: z
41
+ .email({ message: 'Enter a valid email address' })
42
+ .min(3)
43
+ .max(254, 'Email must not exceed 254 characters'),
44
+ })
45
+
46
+ type UpdateAccountValues = z.infer<typeof updateAccountSchema>
47
+
48
+ function defaultsFrom(account: AccountResponse): UpdateAccountValues {
49
+ return {
50
+ given_name: account.given_name ?? '',
51
+ family_name: account.family_name ?? '',
52
+ username: account.username ?? '',
53
+ email: account.email,
54
+ }
55
+ }
56
+
57
+ function buildPatch(values: UpdateAccountValues, account: AccountResponse) {
58
+ const patch: {
59
+ given_name?: string | null
60
+ family_name?: string | null
61
+ username?: string | null
62
+ email?: string
63
+ } = {}
64
+ const normaliseText = (value: string): string | null => (value.trim().length > 0 ? value : null)
65
+ const nextGiven = normaliseText(values.given_name)
66
+ const nextFamily = normaliseText(values.family_name)
67
+ const nextUsername = normaliseText(values.username)
68
+ if (nextGiven !== account.given_name) patch.given_name = nextGiven
69
+ if (nextFamily !== account.family_name) patch.family_name = nextFamily
70
+ if (nextUsername !== account.username) patch.username = nextUsername
71
+ if (values.email !== account.email) patch.email = values.email
72
+ return patch
73
+ }
74
+
75
+ interface UpdateAccountProps {
76
+ account: AccountResponse
77
+ onClose?: () => void
78
+ onSuccess?: (account: AccountResponse) => void
79
+ }
80
+
81
+ export function UpdateAccount({ account, onClose, onSuccess }: UpdateAccountProps) {
82
+ const { updateAccount } = useBylineAdminServices()
83
+ const [formError, setFormError] = useState<string | null>(null)
84
+ const [successMessage, setSuccessMessage] = useState<string | null>(null)
85
+
86
+ const form = useForm({
87
+ defaultValues: defaultsFrom(account),
88
+ validationLogic: revalidateLogic({
89
+ mode: 'blur',
90
+ modeAfterSubmission: 'change',
91
+ }),
92
+ validators: {
93
+ onDynamic: updateAccountSchema,
94
+ },
95
+ onSubmit: async ({ value }) => {
96
+ setFormError(null)
97
+ setSuccessMessage(null)
98
+ const patch = buildPatch(value, account)
99
+ if (Object.keys(patch).length === 0) {
100
+ setSuccessMessage('No changes to save.')
101
+ return
102
+ }
103
+ try {
104
+ const updated = await updateAccount({ data: { vid: account.vid, patch } })
105
+ setSuccessMessage('Saved.')
106
+ onSuccess?.(updated)
107
+ } catch (err) {
108
+ const code = getErrorCode(err)
109
+ if (code === 'admin.users.emailInUse') {
110
+ form.setFieldMeta('email', (meta) => ({
111
+ ...meta,
112
+ errorMap: { ...meta.errorMap, onServer: 'This email is already in use.' },
113
+ errors: ['This email is already in use.'],
114
+ }))
115
+ return
116
+ }
117
+ if (code === 'admin.users.versionConflict') {
118
+ setFormError(
119
+ 'Your account has been modified elsewhere since you opened this form. Reload to refresh and try again.'
120
+ )
121
+ return
122
+ }
123
+ if (code === 'admin.account.notFound') {
124
+ setFormError('Your admin account could not be found. Please sign in again.')
125
+ return
126
+ }
127
+ setFormError('Could not save changes. Please try again.')
128
+ }
129
+ },
130
+ })
131
+
132
+ return (
133
+ <div className={cx('byline-account-update-wrap', styles.wrap)}>
134
+ <form
135
+ noValidate
136
+ onSubmit={(event) => {
137
+ event.preventDefault()
138
+ event.stopPropagation()
139
+ void form.handleSubmit()
140
+ }}
141
+ className={cx('byline-account-update-form', styles.form)}
142
+ >
143
+ {formError ? <Alert intent="danger">{formError}</Alert> : null}
144
+ {successMessage ? <Alert intent="success">{successMessage}</Alert> : null}
145
+
146
+ <form.Field name="given_name">
147
+ {(field) => (
148
+ <Input
149
+ label="Given name"
150
+ id="given_name"
151
+ name={field.name}
152
+ value={field.state.value}
153
+ onBlur={field.handleBlur}
154
+ onChange={(e) => field.handleChange(e.currentTarget.value)}
155
+ error={field.state.meta.errors.length > 0}
156
+ errorText={firstError(field.state.meta.errors)}
157
+ autoComplete="given-name"
158
+ />
159
+ )}
160
+ </form.Field>
161
+
162
+ <form.Field name="family_name">
163
+ {(field) => (
164
+ <Input
165
+ label="Family name"
166
+ id="family_name"
167
+ name={field.name}
168
+ value={field.state.value}
169
+ onBlur={field.handleBlur}
170
+ onChange={(e) => field.handleChange(e.currentTarget.value)}
171
+ error={field.state.meta.errors.length > 0}
172
+ errorText={firstError(field.state.meta.errors)}
173
+ autoComplete="family-name"
174
+ />
175
+ )}
176
+ </form.Field>
177
+
178
+ <form.Field name="username">
179
+ {(field) => (
180
+ <Input
181
+ label="Username"
182
+ id="username"
183
+ name={field.name}
184
+ value={field.state.value}
185
+ onBlur={field.handleBlur}
186
+ onChange={(e) => field.handleChange(e.currentTarget.value)}
187
+ error={field.state.meta.errors.length > 0}
188
+ errorText={firstError(field.state.meta.errors)}
189
+ helpText="Optional. Leave blank to clear."
190
+ autoComplete="username"
191
+ />
192
+ )}
193
+ </form.Field>
194
+
195
+ <form.Field name="email">
196
+ {(field) => (
197
+ <Input
198
+ label="Email"
199
+ id="email"
200
+ name={field.name}
201
+ type="email"
202
+ value={field.state.value}
203
+ onBlur={field.handleBlur}
204
+ onChange={(e) => field.handleChange(e.currentTarget.value)}
205
+ error={field.state.meta.errors.length > 0}
206
+ errorText={firstError(field.state.meta.errors)}
207
+ autoComplete="email"
208
+ required
209
+ />
210
+ )}
211
+ </form.Field>
212
+
213
+ <div className={cx('byline-account-update-actions', styles.actions)}>
214
+ <Button
215
+ type="button"
216
+ intent="secondary"
217
+ size="sm"
218
+ onClick={onClose}
219
+ className={cx('byline-account-update-action', styles.action)}
220
+ >
221
+ {successMessage ? 'Close' : 'Cancel'}
222
+ </Button>
223
+ <form.Subscribe
224
+ selector={(state) => ({
225
+ canSubmit: state.canSubmit,
226
+ isSubmitting: state.isSubmitting,
227
+ isDirty: state.isDirty,
228
+ })}
229
+ >
230
+ {({ canSubmit, isSubmitting }) => (
231
+ <Button
232
+ size="sm"
233
+ intent="primary"
234
+ type="submit"
235
+ disabled={!canSubmit || isSubmitting}
236
+ className={cx('byline-account-update-action', styles.action)}
237
+ >
238
+ {isSubmitting === true ? <LoaderEllipsis size={42} /> : 'Save'}
239
+ </Button>
240
+ )}
241
+ </form.Subscribe>
242
+ </div>
243
+ </form>
244
+ </div>
245
+ )
246
+ }
247
+
248
+ function firstError(errors: readonly unknown[]): string | undefined {
249
+ for (const err of errors) {
250
+ if (typeof err === 'string') return err
251
+ if (err && typeof err === 'object' && 'message' in err) {
252
+ const msg = (err as { message?: unknown }).message
253
+ if (typeof msg === 'string') return msg
254
+ }
255
+ }
256
+ return undefined
257
+ }
258
+
259
+ function getErrorCode(err: unknown): string | null {
260
+ return typeof (err as { code?: unknown })?.code === 'string'
261
+ ? (err as { code: string }).code
262
+ : null
263
+ }
@@ -0,0 +1,75 @@
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-account self-service.
11
+ *
12
+ * Same `code + factory` shape as the other admin modules. The codes are
13
+ * prefixed `admin.account.*` so they sort alongside any future
14
+ * `admin.account` ability keys (today there are none — self-service is
15
+ * gated only by "you must be authenticated and you can only act on
16
+ * yourself") and so transport layers can branch on them distinctly
17
+ * from `admin.users.*`. Note that `admin.users.versionConflict` and
18
+ * `admin.users.emailInUse` are also reachable here because the service
19
+ * delegates to `AdminUsersRepository.update` / `setPasswordHash`; both
20
+ * are deliberately surfaced unmodified so the UI sees a single error
21
+ * code per condition.
22
+ */
23
+
24
+ export const AdminAccountErrorCodes = {
25
+ NOT_FOUND: 'admin.account.notFound',
26
+ INVALID_CURRENT_PASSWORD: 'admin.account.invalidCurrentPassword',
27
+ } as const
28
+
29
+ export type AdminAccountErrorCode =
30
+ (typeof AdminAccountErrorCodes)[keyof typeof AdminAccountErrorCodes]
31
+
32
+ export interface AdminAccountErrorOptions {
33
+ message?: string
34
+ cause?: unknown
35
+ }
36
+
37
+ export class AdminAccountError extends Error {
38
+ public readonly code: AdminAccountErrorCode
39
+
40
+ constructor(code: AdminAccountErrorCode, options: { message: string; cause?: unknown }) {
41
+ super(options.message, options.cause != null ? { cause: options.cause } : undefined)
42
+ this.name = 'AdminAccountError'
43
+ this.code = code
44
+ }
45
+ }
46
+
47
+ const make =
48
+ (code: AdminAccountErrorCode, defaultMessage: string) =>
49
+ (options?: AdminAccountErrorOptions): AdminAccountError =>
50
+ new AdminAccountError(code, {
51
+ message: options?.message ?? defaultMessage,
52
+ cause: options?.cause,
53
+ })
54
+
55
+ /**
56
+ * The actor's admin-user id no longer resolves to a row. Typically
57
+ * means the session refers to a user that has been deleted out of band
58
+ * — the transport handler should clear cookies and redirect to
59
+ * sign-in.
60
+ */
61
+ export const ERR_ADMIN_ACCOUNT_NOT_FOUND = make(
62
+ AdminAccountErrorCodes.NOT_FOUND,
63
+ 'admin account not found'
64
+ )
65
+
66
+ /**
67
+ * The supplied current password did not verify against the stored hash.
68
+ * Returned for the change-password flow — message is intentionally
69
+ * generic so it can be surfaced verbatim to end users without leaking
70
+ * timing or existence signals.
71
+ */
72
+ export const ERR_ADMIN_ACCOUNT_INVALID_CURRENT_PASSWORD = make(
73
+ AdminAccountErrorCodes.INVALID_CURRENT_PASSWORD,
74
+ 'current password is incorrect'
75
+ )
@@ -0,0 +1,60 @@
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-account` — self-service surfaces for the currently
11
+ * signed-in admin user.
12
+ *
13
+ * Distinct from `@byline/admin/admin-users` in two ways:
14
+ *
15
+ * 1. The actor IS the target. Commands take no `id` field — the
16
+ * target is sourced from `actor.id` on the authenticated
17
+ * `RequestContext`. There is no way at the command surface to
18
+ * ask "operate on someone else."
19
+ * 2. There is no ability gate. The other admin modules use
20
+ * `assertAdminActor(context, ability)`; this module uses
21
+ * `requireAdminActor(context)` — authn-only. "Anyone may change
22
+ * their own password" is the policy.
23
+ *
24
+ * Reuses `AdminUsersRepository` from `@byline/admin/admin-users` rather
25
+ * than introducing a parallel repo — the table is the same and the
26
+ * narrower self-service surface is structural rather than physical.
27
+ *
28
+ * Active-session listing / revocation is intentionally not included
29
+ * yet — that depends on `RefreshTokensRepository` semantics and a
30
+ * "sign out everywhere on password change" follow-up.
31
+ */
32
+
33
+ export {
34
+ changeAccountPasswordCommand,
35
+ getAccountCommand,
36
+ updateAccountCommand,
37
+ } from './commands.js'
38
+ export {
39
+ AdminAccountError,
40
+ type AdminAccountErrorCode,
41
+ AdminAccountErrorCodes,
42
+ ERR_ADMIN_ACCOUNT_INVALID_CURRENT_PASSWORD,
43
+ ERR_ADMIN_ACCOUNT_NOT_FOUND,
44
+ } from './errors.js'
45
+ export {
46
+ accountResponseSchema,
47
+ changeAccountPasswordRequestSchema,
48
+ getAccountRequestSchema,
49
+ okResponseSchema,
50
+ updateAccountRequestSchema,
51
+ } from './schemas.js'
52
+ export { AdminAccountService } from './service.js'
53
+ export type { AdminAccountCommandDeps } from './commands.js'
54
+ export type {
55
+ AccountResponse,
56
+ ChangeAccountPasswordRequest,
57
+ GetAccountRequest,
58
+ OkResponse,
59
+ UpdateAccountRequest,
60
+ } from './schemas.js'
@@ -0,0 +1,84 @@
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
+ * Zod schemas for the admin-account commands.
11
+ *
12
+ * Self-service is intentionally narrower than admin-users:
13
+ *
14
+ * - The actor IS the target. None of the request schemas accept an
15
+ * `id` field — the command resolves the target from
16
+ * `actor.id`. Persisting `id` in the request shape would
17
+ * immediately invite "but what if I pass someone else's id?"
18
+ * mistakes downstream.
19
+ * - The update patch excludes `is_super_admin`, `is_enabled`, and
20
+ * `is_email_verified`. Self-service must never let a user grant
21
+ * themselves super-admin or flip their own enabled state. Those
22
+ * fields stay editable through the admin-users module by an admin
23
+ * who holds the relevant ability.
24
+ * - `changePassword` requires the *current* password as a defence
25
+ * against session-hijack abuse: an attacker with a stolen session
26
+ * cookie still needs the password they don't have to swap it out.
27
+ *
28
+ * The response shape is the same as `adminUserResponseSchema` so the
29
+ * admin-account UI and the admin-users UI render the same row shape
30
+ * — re-exported here for convenience.
31
+ */
32
+
33
+ import { passwordSchema } from '@byline/core/validation'
34
+ import { z } from 'zod'
35
+
36
+ import { adminUserResponseSchema, okResponseSchema } from '../admin-users/schemas.js'
37
+
38
+ const vidSchema = z
39
+ .number({ message: 'vid is required' })
40
+ .int({ message: 'vid must be an integer' })
41
+ .positive({ message: 'vid must be positive' })
42
+
43
+ const emailSchema = z
44
+ .email({ message: 'email must be a valid address' })
45
+ .min(3)
46
+ .max(254)
47
+ .transform((v) => v.toLowerCase())
48
+
49
+ const nameSchema = z.string().min(1).max(100)
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Requests
53
+ // ---------------------------------------------------------------------------
54
+
55
+ /** No payload — target is the actor on context. */
56
+ export const getAccountRequestSchema = z.object({}).strict()
57
+ export type GetAccountRequest = z.infer<typeof getAccountRequestSchema>
58
+
59
+ export const updateAccountRequestSchema = z.object({
60
+ vid: vidSchema,
61
+ patch: z
62
+ .object({
63
+ email: emailSchema.optional(),
64
+ given_name: nameSchema.nullish(),
65
+ family_name: nameSchema.nullish(),
66
+ username: z.string().min(1).max(100).nullish(),
67
+ })
68
+ .refine((p) => Object.keys(p).length > 0, { message: 'patch cannot be empty' }),
69
+ })
70
+ export type UpdateAccountRequest = z.infer<typeof updateAccountRequestSchema>
71
+
72
+ export const changeAccountPasswordRequestSchema = z.object({
73
+ vid: vidSchema,
74
+ currentPassword: z.string().min(1, { message: 'current password is required' }),
75
+ newPassword: passwordSchema,
76
+ })
77
+ export type ChangeAccountPasswordRequest = z.infer<typeof changeAccountPasswordRequestSchema>
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Responses (re-exports — same shape as the admin-users module)
81
+ // ---------------------------------------------------------------------------
82
+
83
+ export { adminUserResponseSchema as accountResponseSchema, okResponseSchema }
84
+ export type { AdminUserResponse as AccountResponse, OkResponse } from '../admin-users/schemas.js'
@@ -0,0 +1,92 @@
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 { toAdminUser } from '../admin-users/dto.js'
10
+ import { ERR_ADMIN_USER_EMAIL_IN_USE } from '../admin-users/errors.js'
11
+ import { hashPassword, verifyPassword } from '../auth/password.js'
12
+ import {
13
+ ERR_ADMIN_ACCOUNT_INVALID_CURRENT_PASSWORD,
14
+ ERR_ADMIN_ACCOUNT_NOT_FOUND,
15
+ } from './errors.js'
16
+ import type { AdminUsersRepository } from '../admin-users/repository.js'
17
+ import type {
18
+ AccountResponse,
19
+ ChangeAccountPasswordRequest,
20
+ UpdateAccountRequest,
21
+ } from './schemas.js'
22
+
23
+ /**
24
+ * Self-service business logic for the currently signed-in admin user.
25
+ *
26
+ * Reuses `AdminUsersRepository` rather than introducing a parallel
27
+ * repository — the underlying table is the same, and self-service is
28
+ * just a narrower surface over it. The narrowing is structural:
29
+ *
30
+ * - Every method takes `actorId` (sourced server-side from the
31
+ * authenticated `RequestContext`) and uses it as the target id.
32
+ * Callers cannot supply a target id; commands look it up from
33
+ * `actor.id` and pass it in.
34
+ * - `updateAccount` excludes `is_super_admin`, `is_enabled`, and
35
+ * `is_email_verified` from the writable surface. The schema
36
+ * already strips them, but the service signature reinforces it.
37
+ * - `changePassword` verifies the *current* password before swapping
38
+ * in the new hash. A hijacked session cannot use this flow to lock
39
+ * out the legitimate owner.
40
+ *
41
+ * Note on session revocation: changing a password here does **not**
42
+ * currently revoke other refresh tokens — existing access tokens stay
43
+ * valid until their 15-minute expiry, and other refresh tokens remain
44
+ * useable. A "sign out everywhere on password change" follow-up should
45
+ * call `RefreshTokensRepository.revokeAllExcept(adminUserId, currentJti)`
46
+ * once that lands.
47
+ */
48
+ export class AdminAccountService {
49
+ readonly #repo: AdminUsersRepository
50
+
51
+ constructor(deps: { repo: AdminUsersRepository }) {
52
+ this.#repo = deps.repo
53
+ }
54
+
55
+ async getAccount(actorId: string): Promise<AccountResponse> {
56
+ const row = await this.#repo.getById(actorId)
57
+ if (!row) throw ERR_ADMIN_ACCOUNT_NOT_FOUND()
58
+ return toAdminUser(row)
59
+ }
60
+
61
+ async updateAccount(actorId: string, request: UpdateAccountRequest): Promise<AccountResponse> {
62
+ const current = await this.#repo.getById(actorId)
63
+ if (!current) throw ERR_ADMIN_ACCOUNT_NOT_FOUND()
64
+
65
+ if (request.patch.email != null && request.patch.email !== current.email) {
66
+ const owner = await this.#repo.getByEmail(request.patch.email)
67
+ if (owner && owner.id !== actorId) throw ERR_ADMIN_USER_EMAIL_IN_USE()
68
+ }
69
+
70
+ const row = await this.#repo.update(actorId, request.vid, request.patch)
71
+ return toAdminUser(row)
72
+ }
73
+
74
+ async changePassword(
75
+ actorId: string,
76
+ request: ChangeAccountPasswordRequest
77
+ ): Promise<AccountResponse> {
78
+ // Pull the row *with* the password hash so we can verify the
79
+ // supplied current password before persisting a new one. The
80
+ // sign-in-shaped row is treated as ephemeral here — the hash
81
+ // string is never propagated outside this method.
82
+ const withHash = await this.#repo.getByIdForSignIn(actorId)
83
+ if (!withHash) throw ERR_ADMIN_ACCOUNT_NOT_FOUND()
84
+
85
+ const ok = await verifyPassword(request.currentPassword, withHash.password_hash)
86
+ if (!ok) throw ERR_ADMIN_ACCOUNT_INVALID_CURRENT_PASSWORD()
87
+
88
+ const newHash = await hashPassword(request.newPassword)
89
+ const row = await this.#repo.setPasswordHash(actorId, request.vid, newHash)
90
+ return toAdminUser(row)
91
+ }
92
+ }
@@ -0,0 +1,46 @@
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 { AbilityRegistry } from '@byline/auth'
10
+
11
+ /**
12
+ * Ability keys for the admin-permissions module.
13
+ *
14
+ * `read` gates the inspector view (see docs/AUTHN-AUTHZ.md).
15
+ * `update` will gate the per-role ability editor mounted on the
16
+ * admin-roles role detail page — declared here so the role editor can
17
+ * assert against it once that surface lands. The per-role editor shares
18
+ * the `update` key rather than minting `grant` / `revoke` keys: granting
19
+ * abilities to a role is a single editorial operation from the admin's
20
+ * perspective, and a granular split would force a redundant key on
21
+ * every permission-managing role.
22
+ */
23
+ export const ADMIN_PERMISSIONS_ABILITIES = {
24
+ read: 'admin.permissions.read',
25
+ update: 'admin.permissions.update',
26
+ } as const
27
+
28
+ export type AdminPermissionsAbilityKey =
29
+ (typeof ADMIN_PERMISSIONS_ABILITIES)[keyof typeof ADMIN_PERMISSIONS_ABILITIES]
30
+
31
+ export function registerAdminPermissionsAbilities(registry: AbilityRegistry): void {
32
+ registry.register({
33
+ key: ADMIN_PERMISSIONS_ABILITIES.read,
34
+ label: 'Read admin permissions',
35
+ description: 'View the abilities inspector and per-role ability grants.',
36
+ group: 'admin.permissions',
37
+ source: 'admin',
38
+ })
39
+ registry.register({
40
+ key: ADMIN_PERMISSIONS_ABILITIES.update,
41
+ label: 'Update admin permissions',
42
+ description: "Edit a role's ability grants.",
43
+ group: 'admin.permissions',
44
+ source: 'admin',
45
+ })
46
+ }