@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,232 @@
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 change-password drawer form.
13
+ *
14
+ * Distinct from the admin-users `set-password.tsx`:
15
+ *
16
+ * - Requires the current password as defence against
17
+ * session-hijack abuse. The server verifies it against the
18
+ * stored hash before swapping in the new one — wrong current
19
+ * password surfaces as `admin.account.invalidCurrentPassword`.
20
+ * - Confirmation field catches typos before round-trip.
21
+ *
22
+ * Caveat: changing the password here does not revoke other active
23
+ * sessions today. Existing access tokens stay valid until expiry
24
+ * (~15 min); a "sign out everywhere on password change" follow-up
25
+ * will close that gap.
26
+ */
27
+
28
+ import { useState } from 'react'
29
+ import { revalidateLogic, useForm } from '@tanstack/react-form-start'
30
+
31
+ import { passwordSchema } from '@byline/core/validation'
32
+ import { Alert, Button, InputPassword, LoaderEllipsis } from '@byline/ui/react'
33
+ import cx from 'classnames'
34
+ import { z } from 'zod'
35
+
36
+ import { useBylineAdminServices } from '../../../services/admin-services-context.js'
37
+ import styles from './change-password.module.css'
38
+ import type { AccountResponse } from '../index.js'
39
+
40
+ const changePasswordFormSchema = z
41
+ .object({
42
+ currentPassword: z.string().min(1, { message: 'Please enter your current password' }),
43
+ newPassword: passwordSchema,
44
+ confirm: z.string({ message: 'Please confirm the new password' }),
45
+ })
46
+ .refine((v) => v.newPassword === v.confirm, {
47
+ message: 'New passwords do not match',
48
+ path: ['confirm'],
49
+ })
50
+
51
+ type ChangePasswordValues = z.infer<typeof changePasswordFormSchema>
52
+
53
+ interface ChangePasswordProps {
54
+ account: AccountResponse
55
+ onClose?: () => void
56
+ onSuccess?: (account: AccountResponse) => void
57
+ }
58
+
59
+ export function ChangeAccountPassword({ account, onClose, onSuccess }: ChangePasswordProps) {
60
+ const { changeAccountPassword } = useBylineAdminServices()
61
+ const [formError, setFormError] = useState<string | null>(null)
62
+ const [successMessage, setSuccessMessage] = useState<string | null>(null)
63
+
64
+ const form = useForm({
65
+ defaultValues: { currentPassword: '', newPassword: '', confirm: '' } as ChangePasswordValues,
66
+ validationLogic: revalidateLogic({
67
+ mode: 'blur',
68
+ modeAfterSubmission: 'change',
69
+ }),
70
+ validators: {
71
+ onDynamic: changePasswordFormSchema,
72
+ },
73
+ onSubmit: async ({ value }) => {
74
+ setFormError(null)
75
+ setSuccessMessage(null)
76
+ try {
77
+ const updated = await changeAccountPassword({
78
+ data: {
79
+ vid: account.vid,
80
+ currentPassword: value.currentPassword,
81
+ newPassword: value.newPassword,
82
+ },
83
+ })
84
+ setSuccessMessage('Password updated.')
85
+ form.reset({ currentPassword: '', newPassword: '', confirm: '' })
86
+ onSuccess?.(updated)
87
+ } catch (err) {
88
+ const code = getErrorCode(err)
89
+ if (code === 'admin.account.invalidCurrentPassword') {
90
+ form.setFieldMeta('currentPassword', (meta) => ({
91
+ ...meta,
92
+ errorMap: { ...meta.errorMap, onServer: 'Current password is incorrect.' },
93
+ errors: ['Current password is incorrect.'],
94
+ }))
95
+ return
96
+ }
97
+ if (code === 'admin.users.versionConflict') {
98
+ setFormError(
99
+ 'Your account has been modified elsewhere since you opened this form. Reload to refresh and try again.'
100
+ )
101
+ return
102
+ }
103
+ if (code === 'admin.account.notFound') {
104
+ setFormError('Your admin account could not be found. Please sign in again.')
105
+ return
106
+ }
107
+ setFormError('Could not change the password. Please try again.')
108
+ }
109
+ },
110
+ })
111
+
112
+ return (
113
+ <div className={cx('byline-account-change-password-wrap', styles.wrap)}>
114
+ <form
115
+ noValidate
116
+ onSubmit={(event) => {
117
+ event.preventDefault()
118
+ event.stopPropagation()
119
+ void form.handleSubmit()
120
+ }}
121
+ className={cx('byline-account-change-password-form', styles.form)}
122
+ >
123
+ {formError ? <Alert intent="danger">{formError}</Alert> : null}
124
+ {successMessage ? <Alert intent="success">{successMessage}</Alert> : null}
125
+
126
+ <p className="muted">
127
+ Other active sessions will continue to work until their tokens expire. Sign out elsewhere
128
+ if you suspect another device has been compromised.
129
+ </p>
130
+
131
+ <form.Field name="currentPassword">
132
+ {(field) => (
133
+ <InputPassword
134
+ label="Current password"
135
+ id="currentPassword"
136
+ name={field.name}
137
+ value={field.state.value}
138
+ onBlur={field.handleBlur}
139
+ onChange={(e) => field.handleChange(e.currentTarget.value)}
140
+ error={field.state.meta.errors.length > 0}
141
+ errorText={firstError(field.state.meta.errors)}
142
+ autoComplete="current-password"
143
+ required
144
+ />
145
+ )}
146
+ </form.Field>
147
+
148
+ <form.Field name="newPassword">
149
+ {(field) => (
150
+ <InputPassword
151
+ label="New password"
152
+ id="newPassword"
153
+ name={field.name}
154
+ value={field.state.value}
155
+ onBlur={field.handleBlur}
156
+ onChange={(e) => field.handleChange(e.currentTarget.value)}
157
+ error={field.state.meta.errors.length > 0}
158
+ errorText={firstError(field.state.meta.errors)}
159
+ autoComplete="new-password"
160
+ required
161
+ />
162
+ )}
163
+ </form.Field>
164
+
165
+ <form.Field name="confirm">
166
+ {(field) => (
167
+ <InputPassword
168
+ label="Confirm new password"
169
+ id="confirm"
170
+ name={field.name}
171
+ value={field.state.value}
172
+ onBlur={field.handleBlur}
173
+ onChange={(e) => field.handleChange(e.currentTarget.value)}
174
+ error={field.state.meta.errors.length > 0}
175
+ errorText={firstError(field.state.meta.errors)}
176
+ autoComplete="new-password"
177
+ required
178
+ />
179
+ )}
180
+ </form.Field>
181
+
182
+ <div className={cx('byline-account-change-password-actions', styles.actions)}>
183
+ <Button
184
+ type="button"
185
+ intent="secondary"
186
+ size="sm"
187
+ onClick={onClose}
188
+ className={cx('byline-account-change-password-action', styles.action)}
189
+ >
190
+ {successMessage ? 'Close' : 'Cancel'}
191
+ </Button>
192
+ <form.Subscribe
193
+ selector={(state) => ({
194
+ canSubmit: state.canSubmit,
195
+ isSubmitting: state.isSubmitting,
196
+ isDirty: state.isDirty,
197
+ })}
198
+ >
199
+ {({ canSubmit, isSubmitting }) => (
200
+ <Button
201
+ size="sm"
202
+ intent="primary"
203
+ type="submit"
204
+ disabled={!canSubmit || isSubmitting}
205
+ className={cx('byline-account-change-password-action', styles.action)}
206
+ >
207
+ {isSubmitting === true ? <LoaderEllipsis size={42} /> : 'Save'}
208
+ </Button>
209
+ )}
210
+ </form.Subscribe>
211
+ </div>
212
+ </form>
213
+ </div>
214
+ )
215
+ }
216
+
217
+ function firstError(errors: readonly unknown[]): string | undefined {
218
+ for (const err of errors) {
219
+ if (typeof err === 'string') return err
220
+ if (err && typeof err === 'object' && 'message' in err) {
221
+ const msg = (err as { message?: unknown }).message
222
+ if (typeof msg === 'string') return msg
223
+ }
224
+ }
225
+ return undefined
226
+ }
227
+
228
+ function getErrorCode(err: unknown): string | null {
229
+ return typeof (err as { code?: unknown })?.code === 'string'
230
+ ? (err as { code: string }).code
231
+ : null
232
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * AccountSelfContainer — self-service account dashboard with edit drawers.
3
+ *
4
+ * Override handles:
5
+ * .byline-account-grid — outer two-column grid
6
+ * .byline-account-column — column wrapper (vertical card stack)
7
+ * .byline-account-section — single bordered section card
8
+ * .byline-account-section-head — section title + edit-icon row
9
+ * .byline-account-line — single key/value text line
10
+ * .byline-account-cta-line — paragraph above the in-card primary CTA
11
+ * .byline-account-meta — small metadata block (created/updated/last login)
12
+ * .byline-account-status — status colour modifier (enabled / disabled)
13
+ * .byline-account-status-on
14
+ * .byline-account-status-off
15
+ * .byline-account-status-help — italic helper line on Account Status card
16
+ * .byline-account-drawer — drawer responsive width override
17
+ * .byline-account-drawer-body — drawer content container
18
+ * .byline-account-drawer-scroll — drawer scrollable inner viewport
19
+ * .byline-account-drawer-skip — sr-only "no action" focus shim
20
+ */
21
+
22
+ .grid,
23
+ :global(.byline-account-grid) {
24
+ display: flex;
25
+ flex-direction: column;
26
+ gap: var(--spacing-16);
27
+ margin-bottom: var(--spacing-48);
28
+ }
29
+
30
+ @media (min-width: 40rem) {
31
+ .grid,
32
+ :global(.byline-account-grid) {
33
+ display: grid;
34
+ grid-template-columns: 1fr 1fr;
35
+ }
36
+ }
37
+
38
+ .column,
39
+ :global(.byline-account-column) {
40
+ display: flex;
41
+ flex-direction: column;
42
+ gap: var(--spacing-16);
43
+ margin-bottom: var(--spacing-16);
44
+ }
45
+
46
+ .section,
47
+ :global(.byline-account-section) {
48
+ padding: var(--spacing-16);
49
+ border: var(--border-width-thin) var(--border-style-solid) var(--gray-100);
50
+ background-color: var(--canvas-25);
51
+ border-radius: var(--border-radius-sm);
52
+ }
53
+
54
+ .section-head,
55
+ :global(.byline-account-section-head) {
56
+ display: flex;
57
+ align-items: center;
58
+ justify-content: space-between;
59
+ margin-bottom: var(--spacing-8);
60
+ }
61
+
62
+ .line,
63
+ :global(.byline-account-line) {
64
+ margin-bottom: 0;
65
+ }
66
+
67
+ .cta-line,
68
+ :global(.byline-account-cta-line) {
69
+ margin-bottom: var(--spacing-12);
70
+ }
71
+
72
+ .meta,
73
+ :global(.byline-account-meta) {
74
+ margin-top: var(--spacing-16);
75
+ font-size: var(--font-size-xs);
76
+ }
77
+
78
+ .status-on,
79
+ :global(.byline-account-status-on) {
80
+ color: var(--green-600);
81
+ }
82
+
83
+ .status-off,
84
+ :global(.byline-account-status-off) {
85
+ color: var(--red-600);
86
+ }
87
+
88
+ .status-help,
89
+ :global(.byline-account-status-help) {
90
+ margin-top: var(--spacing-12);
91
+ margin-bottom: 0;
92
+ font-size: var(--font-size-xs);
93
+ }
94
+
95
+ /*
96
+ * Italic helper for "Not set" placeholders. Replaces the Tailwind
97
+ * `italic` utility we used pre-lift; with the component now in a
98
+ * published package, Tailwind's source scanner doesn't see the class
99
+ * string in the host so the rule was never generated.
100
+ */
101
+ .not-set,
102
+ :global(.byline-account-not-set) {
103
+ font-style: italic;
104
+ }
105
+
106
+ .drawer,
107
+ :global(.byline-account-drawer) {
108
+ /* Width handled by uikit Drawer prop; no additional class needed today. */
109
+ }
110
+
111
+ @media (min-width: 48rem) {
112
+ .drawer,
113
+ :global(.byline-account-drawer) {
114
+ width: 500px;
115
+ }
116
+ }
117
+
118
+ .drawer-body,
119
+ :global(.byline-account-drawer-body) {
120
+ padding: var(--spacing-8);
121
+ }
122
+
123
+ .drawer-scroll,
124
+ :global(.byline-account-drawer-scroll) {
125
+ max-height: calc(100vh - 160px);
126
+ overflow-y: auto;
127
+ }
128
+
129
+ .drawer-skip,
130
+ :global(.byline-account-drawer-skip) {
131
+ position: absolute;
132
+ width: 1px;
133
+ height: 1px;
134
+ padding: 0;
135
+ margin: -1px;
136
+ overflow: hidden;
137
+ clip: rect(0, 0, 0, 0);
138
+ white-space: nowrap;
139
+ border: 0;
140
+ }
141
+
142
+ :is([data-theme="dark"], :global(.dark)) {
143
+ .section,
144
+ :global(.byline-account-section) {
145
+ border-color: var(--gray-700);
146
+ background-color: var(--canvas-800);
147
+ }
148
+
149
+ .status-on,
150
+ :global(.byline-account-status-on) {
151
+ color: var(--green-400);
152
+ }
153
+
154
+ .status-off,
155
+ :global(.byline-account-status-off) {
156
+ color: var(--red-400);
157
+ }
158
+ }
@@ -0,0 +1,229 @@
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 account container.
13
+ *
14
+ * Same drawer pattern as `admin-users/ui/container.tsx` but
15
+ * narrower: only Profile and Password sections (no Roles, no
16
+ * Delete) — those are admin-only actions on someone else, not
17
+ * self-service. Each card surfaces the read-only summary plus an
18
+ * "Edit" affordance that opens the matching drawer.
19
+ *
20
+ * Forms lift the fresh `AccountResponse` back into local state on
21
+ * success so the container's bumped `vid` is in hand for any
22
+ * subsequent edit without a refetch.
23
+ *
24
+ * Stable override handles: see `container.module.css`.
25
+ */
26
+
27
+ import type React from 'react'
28
+ import { useState } from 'react'
29
+
30
+ import { Button, CloseIcon, Drawer, EditIcon, IconButton, LocalDateTime } from '@byline/ui/react'
31
+ import cx from 'classnames'
32
+
33
+ import { ChangeAccountPassword } from './change-password.js'
34
+ import styles from './container.module.css'
35
+ import { UpdateAccount } from './update.js'
36
+ import type { AccountResponse } from '../index.js'
37
+
38
+ type ComponentKey = 'update' | 'change_password' | 'empty'
39
+
40
+ interface PanelProps {
41
+ account: AccountResponse
42
+ onClose?: () => void
43
+ onSuccess?: (account: AccountResponse) => void
44
+ }
45
+
46
+ const panels: Record<ComponentKey, { title: string; component: React.ComponentType<PanelProps> }> =
47
+ {
48
+ update: { title: 'Profile', component: UpdateAccount },
49
+ change_password: { title: 'Change Password', component: ChangeAccountPassword },
50
+ empty: { title: '', component: () => null },
51
+ }
52
+
53
+ function ContainerSection({
54
+ title,
55
+ onEdit,
56
+ children,
57
+ }: {
58
+ title: string
59
+ onEdit?: () => void
60
+ children: React.ReactNode
61
+ }) {
62
+ return (
63
+ <div className={cx('byline-account-section', styles.section)}>
64
+ <div className={cx('byline-account-section-head', styles['section-head'])}>
65
+ <h2>{title}</h2>
66
+ {onEdit ? (
67
+ <IconButton variant="text" onClick={onEdit} aria-label={`Edit ${title}`}>
68
+ <EditIcon width="20px" height="20px" />
69
+ </IconButton>
70
+ ) : null}
71
+ </div>
72
+ <div>{children}</div>
73
+ </div>
74
+ )
75
+ }
76
+
77
+ interface AccountSelfContainerProps {
78
+ account: AccountResponse
79
+ }
80
+
81
+ export function AccountSelfContainer({ account }: AccountSelfContainerProps) {
82
+ const [currentAccount, setCurrentAccount] = useState<AccountResponse>(account)
83
+ const [current, setCurrent] = useState<ComponentKey>('empty')
84
+ const [isDrawerOpen, setIsDrawerOpen] = useState(false)
85
+
86
+ const openDrawer = (key: ComponentKey) => () => {
87
+ setCurrent(key)
88
+ setIsDrawerOpen(true)
89
+ }
90
+ const closeDrawer = () => {
91
+ setCurrent('empty')
92
+ setIsDrawerOpen(false)
93
+ }
94
+ const handleSuccess = (updated: AccountResponse) => {
95
+ setCurrentAccount(updated)
96
+ }
97
+
98
+ const Panel = panels[current].component
99
+
100
+ return (
101
+ <>
102
+ <div className={cx('byline-account-grid', styles.grid)}>
103
+ <div className={cx('byline-account-column', styles.column)}>
104
+ <ContainerSection title="Profile" onEdit={openDrawer('update')}>
105
+ <p className={cx('byline-account-line', styles.line)}>
106
+ <span className="muted">Email:</span> {currentAccount.email}
107
+ </p>
108
+ <p className={cx('byline-account-line', styles.line)}>
109
+ <span className="muted">Given name:</span>{' '}
110
+ {currentAccount.given_name ?? (
111
+ <span className={cx('muted', 'byline-account-not-set', styles['not-set'])}>
112
+ Not set
113
+ </span>
114
+ )}
115
+ </p>
116
+ <p className={cx('byline-account-line', styles.line)}>
117
+ <span className="muted">Family name:</span>{' '}
118
+ {currentAccount.family_name ?? (
119
+ <span className={cx('muted', 'byline-account-not-set', styles['not-set'])}>
120
+ Not set
121
+ </span>
122
+ )}
123
+ </p>
124
+ <p className={cx('byline-account-cta-line', styles['cta-line'])}>
125
+ <span className="muted">Username:</span>{' '}
126
+ {currentAccount.username ?? (
127
+ <span className={cx('muted', 'byline-account-not-set', styles['not-set'])}>
128
+ Not set
129
+ </span>
130
+ )}
131
+ </p>
132
+ <Button size="sm" onClick={openDrawer('update')}>
133
+ Edit Profile
134
+ </Button>
135
+ <div className={cx('muted', 'byline-account-meta', styles.meta)}>
136
+ <p>
137
+ <span className="font-bold">Created:&nbsp;</span>
138
+ <LocalDateTime value={currentAccount.created_at} />
139
+ </p>
140
+ <p>
141
+ <span className="font-bold">Updated:&nbsp;</span>
142
+ <LocalDateTime value={currentAccount.updated_at} />
143
+ </p>
144
+ <p className={cx('byline-account-line', styles.line)}>
145
+ <span className="font-bold">Last login:&nbsp;</span>
146
+ <LocalDateTime value={currentAccount.last_login} fallback="Never" />
147
+ </p>
148
+ </div>
149
+ </ContainerSection>
150
+ </div>
151
+
152
+ <div className={cx('byline-account-column', styles.column)}>
153
+ <ContainerSection title="Password" onEdit={openDrawer('change_password')}>
154
+ <p className={cx('byline-account-cta-line', styles['cta-line'])}>
155
+ Change the password used to sign in to the admin. You'll need to enter your current
156
+ password to confirm the change.
157
+ </p>
158
+ <Button size="sm" onClick={openDrawer('change_password')}>
159
+ Change Password
160
+ </Button>
161
+ </ContainerSection>
162
+
163
+ <ContainerSection title="Account Status">
164
+ <p className={cx('byline-account-line', styles.line)}>
165
+ <span className="muted">Super admin:</span>{' '}
166
+ {currentAccount.is_super_admin ? 'Yes' : 'No'}
167
+ </p>
168
+ <p className={cx('byline-account-line', styles.line)}>
169
+ <span className="muted">Email verified:</span>{' '}
170
+ {currentAccount.is_email_verified ? 'Yes' : 'No'}
171
+ </p>
172
+ <p className={cx('byline-account-line', styles.line)}>
173
+ <span className="muted">Status:</span>{' '}
174
+ <span
175
+ className={
176
+ currentAccount.is_enabled
177
+ ? cx('byline-account-status-on', styles['status-on'])
178
+ : cx('byline-account-status-off', styles['status-off'])
179
+ }
180
+ >
181
+ {currentAccount.is_enabled ? 'Enabled' : 'Disabled'}
182
+ </span>
183
+ </p>
184
+ <p className={cx('muted', 'byline-account-status-help', styles['status-help'])}>
185
+ These flags are managed by an admin with the appropriate permissions and are not
186
+ self-editable.
187
+ </p>
188
+ </ContainerSection>
189
+ </div>
190
+ </div>
191
+
192
+ <Drawer
193
+ id="admin-account-drawer"
194
+ closeOnOverlayClick={false}
195
+ width="medium"
196
+ topOffset="46px"
197
+ isOpen={isDrawerOpen}
198
+ onDismiss={closeDrawer}
199
+ className={cx('byline-account-drawer', styles.drawer)}
200
+ >
201
+ <Drawer.Container
202
+ aria-hidden={!isDrawerOpen}
203
+ className={cx('byline-account-drawer-body', styles['drawer-body'])}
204
+ >
205
+ <Drawer.TopActions>
206
+ <button
207
+ type="button"
208
+ tabIndex={0}
209
+ className={cx('byline-account-drawer-skip', styles['drawer-skip'])}
210
+ >
211
+ no action
212
+ </button>
213
+ <IconButton aria-label="Close" size="sm" onClick={closeDrawer}>
214
+ <CloseIcon width="14px" height="14px" svgClassName="white-icon stroke-white" />
215
+ </IconButton>
216
+ </Drawer.TopActions>
217
+ <Drawer.Header>
218
+ <h2>{panels[current].title}</h2>
219
+ </Drawer.Header>
220
+ <Drawer.Content>
221
+ <div className={cx('byline-account-drawer-scroll', styles['drawer-scroll'])}>
222
+ <Panel account={currentAccount} onClose={closeDrawer} onSuccess={handleSuccess} />
223
+ </div>
224
+ </Drawer.Content>
225
+ </Drawer.Container>
226
+ </Drawer>
227
+ </>
228
+ )
229
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * UpdateAccount — self-service profile form (drawer body).
3
+ *
4
+ * Override handles:
5
+ * .byline-account-update-wrap — outer container
6
+ * .byline-account-update-form — vertical-stack form element
7
+ * .byline-account-update-actions — Cancel/Save row
8
+ * .byline-account-update-action — buttons in the actions row
9
+ */
10
+
11
+ .wrap,
12
+ :global(.byline-account-update-wrap) {
13
+ display: flex;
14
+ flex-direction: column;
15
+ gap: var(--spacing-8);
16
+ padding: var(--spacing-4);
17
+ margin-top: var(--spacing-4);
18
+ }
19
+
20
+ .form,
21
+ :global(.byline-account-update-form) {
22
+ display: flex;
23
+ flex-direction: column;
24
+ gap: var(--spacing-16);
25
+ padding-top: var(--spacing-8);
26
+ }
27
+
28
+ .actions,
29
+ :global(.byline-account-update-actions) {
30
+ display: flex;
31
+ align-items: center;
32
+ justify-content: flex-end;
33
+ gap: var(--spacing-8);
34
+ margin-top: var(--spacing-16);
35
+ }
36
+
37
+ .action,
38
+ :global(.byline-account-update-action) {
39
+ min-width: 4rem;
40
+ }