@byline/admin 6.7.2 → 6.8.0

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 (86) hide show
  1. package/dist/modules/admin-account/components/change-password.js +1 -1
  2. package/dist/modules/admin-account/components/container.js +18 -3
  3. package/dist/modules/admin-account/components/container.module.js +2 -0
  4. package/dist/modules/admin-account/components/container_module.css +7 -0
  5. package/dist/modules/admin-account/components/preferences.js +1 -1
  6. package/dist/modules/admin-account/components/update.js +1 -1
  7. package/dist/modules/admin-roles/components/create.js +1 -1
  8. package/dist/modules/admin-roles/components/permissions.js +1 -1
  9. package/dist/modules/admin-roles/components/update.js +1 -1
  10. package/dist/modules/admin-users/abilities.d.ts +1 -0
  11. package/dist/modules/admin-users/abilities.js +8 -1
  12. package/dist/modules/admin-users/components/create.js +1 -1
  13. package/dist/modules/admin-users/components/roles.js +1 -1
  14. package/dist/modules/admin-users/components/set-password.js +1 -1
  15. package/dist/modules/admin-users/components/update.js +1 -1
  16. package/dist/modules/auth/auth-maintenance-repository.d.ts +31 -0
  17. package/dist/modules/auth/auth-maintenance-repository.js +1 -0
  18. package/dist/modules/auth/auth-maintenance.d.ts +11 -0
  19. package/dist/modules/auth/auth-maintenance.js +39 -0
  20. package/dist/modules/auth/auth-maintenance.test.node.d.ts +1 -0
  21. package/dist/modules/auth/components/passkey-browser.d.ts +14 -0
  22. package/dist/modules/auth/components/passkey-browser.js +118 -0
  23. package/dist/modules/auth/components/passkey-browser.test.node.d.ts +1 -0
  24. package/dist/modules/auth/components/passkey-manager.d.ts +26 -0
  25. package/dist/modules/auth/components/passkey-manager.js +406 -0
  26. package/dist/modules/auth/components/passkey-manager.module.js +22 -0
  27. package/dist/modules/auth/components/passkey-manager_module.css +104 -0
  28. package/dist/modules/auth/components/sign-in-form.js +62 -3
  29. package/dist/modules/auth/components/sign-in-form.module.js +5 -1
  30. package/dist/modules/auth/components/sign-in-form_module.css +40 -0
  31. package/dist/modules/auth/index.d.ts +5 -0
  32. package/dist/modules/auth/index.js +3 -0
  33. package/dist/modules/auth/jwt-session-provider.d.ts +12 -4
  34. package/dist/modules/auth/jwt-session-provider.js +135 -94
  35. package/dist/modules/auth/native-passkey-provider.d.ts +72 -0
  36. package/dist/modules/auth/native-passkey-provider.js +618 -0
  37. package/dist/modules/auth/passkey-rate-limiter.d.ts +14 -0
  38. package/dist/modules/auth/passkey-rate-limiter.js +60 -0
  39. package/dist/modules/auth/passkey-rate-limiter.test.node.d.ts +8 -0
  40. package/dist/modules/auth/passkey-storage-validation.d.ts +14 -0
  41. package/dist/modules/auth/passkey-storage-validation.js +59 -0
  42. package/dist/modules/auth/passkeys-repository.d.ts +96 -0
  43. package/dist/modules/auth/passkeys-repository.js +1 -0
  44. package/dist/modules/auth/refresh-tokens-repository.d.ts +1 -1
  45. package/dist/modules/auth/sign-in-rate-limiter.d.ts +2 -0
  46. package/dist/modules/auth/sign-in-rate-limiter.js +1 -1
  47. package/dist/services/admin-services-context.d.ts +1 -1
  48. package/dist/services/admin-services-types.d.ts +60 -0
  49. package/dist/store.d.ts +4 -0
  50. package/package.json +16 -9
  51. package/src/modules/admin-account/components/change-password.tsx +1 -1
  52. package/src/modules/admin-account/components/container.module.css +9 -0
  53. package/src/modules/admin-account/components/container.tsx +12 -3
  54. package/src/modules/admin-account/components/preferences.tsx +1 -1
  55. package/src/modules/admin-account/components/update.tsx +1 -1
  56. package/src/modules/admin-roles/components/create.tsx +1 -1
  57. package/src/modules/admin-roles/components/permissions.tsx +1 -1
  58. package/src/modules/admin-roles/components/update.tsx +1 -1
  59. package/src/modules/admin-users/abilities.ts +7 -0
  60. package/src/modules/admin-users/components/create.tsx +1 -1
  61. package/src/modules/admin-users/components/roles.tsx +1 -1
  62. package/src/modules/admin-users/components/set-password.tsx +1 -1
  63. package/src/modules/admin-users/components/update.tsx +1 -1
  64. package/src/modules/auth/auth-maintenance-repository.ts +26 -0
  65. package/src/modules/auth/auth-maintenance.test.node.ts +110 -0
  66. package/src/modules/auth/auth-maintenance.ts +62 -0
  67. package/src/modules/auth/components/passkey-browser.test.node.ts +188 -0
  68. package/src/modules/auth/components/passkey-browser.ts +139 -0
  69. package/src/modules/auth/components/passkey-manager.module.css +100 -0
  70. package/src/modules/auth/components/passkey-manager.test.tsx +189 -0
  71. package/src/modules/auth/components/passkey-manager.tsx +386 -0
  72. package/src/modules/auth/components/sign-in-form.module.css +37 -0
  73. package/src/modules/auth/components/sign-in-form.test.tsx +92 -0
  74. package/src/modules/auth/components/sign-in-form.tsx +65 -4
  75. package/src/modules/auth/index.ts +15 -0
  76. package/src/modules/auth/jwt-session-provider.ts +116 -44
  77. package/src/modules/auth/native-passkey-provider.ts +710 -0
  78. package/src/modules/auth/passkey-rate-limiter.test.node.ts +83 -0
  79. package/src/modules/auth/passkey-rate-limiter.ts +76 -0
  80. package/src/modules/auth/passkey-storage-validation.ts +108 -0
  81. package/src/modules/auth/passkeys-repository.ts +97 -0
  82. package/src/modules/auth/refresh-tokens-repository.ts +1 -1
  83. package/src/modules/auth/sign-in-rate-limiter.ts +1 -1
  84. package/src/services/admin-services-context.tsx +2 -0
  85. package/src/services/admin-services-types.ts +60 -0
  86. package/src/store.ts +4 -0
@@ -0,0 +1,406 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useRef, useState } from "react";
4
+ import { getAdminConfig } from "@byline/core";
5
+ import { useTranslation } from "@byline/i18n/react";
6
+ import { Alert, Button, CloseIcon, Drawer, IconButton, Input, InputPassword, Modal } from "@byline/ui/react";
7
+ import { useBylineAdminServices } from "../../../services/admin-services-context.js";
8
+ import { enrollPasskey, passkeyErrorKey, provePasskeyAction } from "./passkey-browser.js";
9
+ import passkey_manager_module from "./passkey-manager.module.js";
10
+ function PasskeyManager({ targetUserId, targetIsSuperAdmin = false, section: Section, ownAccountNotice }) {
11
+ const { passkeys } = useBylineAdminServices();
12
+ const { t } = useTranslation('byline-admin');
13
+ const id = useId();
14
+ const binding = useRef(null);
15
+ const [enabled, setEnabled] = useState(false);
16
+ const [loading, setLoading] = useState(true);
17
+ const [allowed, setAllowed] = useState(false);
18
+ const [self, setSelf] = useState(false);
19
+ const [credentials, setCredentials] = useState([]);
20
+ const [edit, setEdit] = useState(null);
21
+ const [name, setName] = useState('');
22
+ const [password, setPassword] = useState('');
23
+ const [pending, setPending] = useState(false);
24
+ const [error, setError] = useState(null);
25
+ const [notice, setNotice] = useState(null);
26
+ const mounted = useRef(false);
27
+ useEffect(()=>{
28
+ let active = true;
29
+ mounted.current = true;
30
+ setAllowed(false);
31
+ setEnabled(false);
32
+ setLoading(true);
33
+ setError(null);
34
+ if (passkeys) (async ()=>{
35
+ if (!(await passkeys.capability()).enabled || !active) return;
36
+ setEnabled(true);
37
+ const captured = passkeys.captureLogin();
38
+ const page = {
39
+ ...captured,
40
+ assertCurrent: ()=>{
41
+ if (!active) throw new Error('Passkey page closed');
42
+ captured.assertCurrent();
43
+ }
44
+ };
45
+ binding.current = page;
46
+ const actor = await passkeys.managementContext(page);
47
+ const isSelf = actor.id === targetUserId;
48
+ if (!isSelf && (!actor.canManagePasskeys || targetIsSuperAdmin && !actor.isSuperAdmin)) return;
49
+ page.assertCurrent();
50
+ setSelf(isSelf);
51
+ setAllowed(true);
52
+ const rows = await passkeys.list({
53
+ data: {
54
+ expectedSessionId: page.expectedSessionId,
55
+ targetUserId
56
+ }
57
+ });
58
+ page.assertCurrent();
59
+ setCredentials(rows);
60
+ setLoading(false);
61
+ })().catch((error)=>{
62
+ if (active) setError(passkeyErrorKey(error));
63
+ });
64
+ return ()=>{
65
+ active = false;
66
+ mounted.current = false;
67
+ binding.current = null;
68
+ };
69
+ }, [
70
+ passkeys,
71
+ targetUserId,
72
+ targetIsSuperAdmin
73
+ ]);
74
+ function open(value) {
75
+ setEdit(value);
76
+ setName('rename' === value.kind ? value.credential.name : '');
77
+ setPassword('');
78
+ setError(null);
79
+ setNotice(null);
80
+ }
81
+ function close() {
82
+ if (pending) return;
83
+ setEdit(null);
84
+ setPassword('');
85
+ setError(null);
86
+ }
87
+ async function perform(usePassword) {
88
+ const page = binding.current;
89
+ if (!passkeys || !edit || !page?.expectedSessionId || pending) return;
90
+ setPending(true);
91
+ setError(null);
92
+ const suppliedPassword = usePassword ? password : void 0;
93
+ setPassword('');
94
+ try {
95
+ page.assertCurrent();
96
+ const expectedSessionId = page.expectedSessionId;
97
+ if ('rename' === edit.kind) {
98
+ const updated = await passkeys.rename({
99
+ data: {
100
+ expectedSessionId,
101
+ credentialRecordId: edit.credential.id,
102
+ name: name.trim()
103
+ }
104
+ });
105
+ page.assertCurrent();
106
+ setCredentials((rows)=>rows.map((row)=>row.id === updated.id ? updated : row));
107
+ setNotice('passkeys.renamed');
108
+ } else if ('enroll' === edit.kind) {
109
+ const created = await enrollPasskey(passkeys, page, name.trim(), suppliedPassword);
110
+ page.assertCurrent();
111
+ setCredentials((rows)=>[
112
+ ...rows,
113
+ created
114
+ ]);
115
+ setNotice('passkeys.added');
116
+ } else {
117
+ const grant = await provePasskeyAction(passkeys, page, {
118
+ purpose: 'remove',
119
+ targetUserId,
120
+ credentialRecordId: edit.credential.id
121
+ }, suppliedPassword);
122
+ page.assertCurrent();
123
+ const result = await passkeys.remove({
124
+ data: {
125
+ expectedSessionId,
126
+ grantId: grant.grantId
127
+ }
128
+ });
129
+ if (result.signedOut) return void window.location.assign(getAdminConfig().routes.signIn);
130
+ page.assertCurrent();
131
+ setCredentials((rows)=>rows.filter((row)=>row.id !== edit.credential.id));
132
+ setNotice('passkeys.removed');
133
+ }
134
+ setEdit(null);
135
+ } catch (error) {
136
+ if (mounted.current) setError(passkeyErrorKey(error));
137
+ } finally{
138
+ if (mounted.current) setPending(false);
139
+ }
140
+ }
141
+ if (!passkeys || !enabled) return null;
142
+ if (!allowed) return error ? /*#__PURE__*/ jsx(Alert, {
143
+ intent: "danger",
144
+ children: t(error)
145
+ }) : null;
146
+ const selfService = self && void 0 === ownAccountNotice;
147
+ const fields = /*#__PURE__*/ jsxs(Fragment, {
148
+ children: [
149
+ error && /*#__PURE__*/ jsx(Alert, {
150
+ intent: "danger",
151
+ children: t(error)
152
+ }),
153
+ edit?.kind === 'remove' ? /*#__PURE__*/ jsx("p", {
154
+ children: t('passkeys.removeWarning', {
155
+ name: edit.credential.name
156
+ })
157
+ }) : /*#__PURE__*/ jsx(Input, {
158
+ id: `${id}-name`,
159
+ name: "passkey-name",
160
+ label: t('passkeys.name'),
161
+ value: name,
162
+ maxLength: 80,
163
+ disabled: pending,
164
+ onChange: (event)=>setName(event.currentTarget.value)
165
+ }),
166
+ edit?.kind !== 'rename' && /*#__PURE__*/ jsxs(Fragment, {
167
+ children: [
168
+ /*#__PURE__*/ jsx("p", {
169
+ children: t("passkeys.proofDescription")
170
+ }),
171
+ /*#__PURE__*/ jsx(InputPassword, {
172
+ id: `${id}-password`,
173
+ name: "passkey-password",
174
+ label: t('common.fields.password'),
175
+ autoComplete: "current-password",
176
+ value: password,
177
+ disabled: pending,
178
+ onChange: (event)=>setPassword(event.currentTarget.value),
179
+ showPasswordLabel: t('common.actions.showPassword'),
180
+ hidePasswordLabel: t('common.actions.hidePassword')
181
+ })
182
+ ]
183
+ }),
184
+ pending && /*#__PURE__*/ jsx("p", {
185
+ role: "status",
186
+ children: t('passkeys.waiting')
187
+ })
188
+ ]
189
+ });
190
+ const actions = /*#__PURE__*/ jsxs(Fragment, {
191
+ children: [
192
+ /*#__PURE__*/ jsx(Button, {
193
+ type: "button",
194
+ size: "sm",
195
+ intent: "noeffect",
196
+ disabled: pending,
197
+ onClick: close,
198
+ children: t('common.actions.cancel')
199
+ }),
200
+ edit?.kind === 'rename' ? /*#__PURE__*/ jsx(Button, {
201
+ type: "button",
202
+ size: "sm",
203
+ disabled: pending || !name.trim(),
204
+ onClick: ()=>void perform(false),
205
+ children: t('common.actions.save')
206
+ }) : /*#__PURE__*/ jsxs(Fragment, {
207
+ children: [
208
+ /*#__PURE__*/ jsx(Button, {
209
+ type: "button",
210
+ size: "sm",
211
+ disabled: pending || !password || edit?.kind === 'enroll' && !name.trim(),
212
+ onClick: ()=>void perform(true),
213
+ children: t('passkeys.usePassword')
214
+ }),
215
+ /*#__PURE__*/ jsx(Button, {
216
+ type: "button",
217
+ size: "sm",
218
+ disabled: pending || edit?.kind === 'enroll' && !name.trim(),
219
+ onClick: ()=>void perform(false),
220
+ children: t('passkeys.usePasskey')
221
+ })
222
+ ]
223
+ })
224
+ ]
225
+ });
226
+ const body = /*#__PURE__*/ jsxs(Fragment, {
227
+ children: [
228
+ self && void 0 !== ownAccountNotice ? ownAccountNotice : /*#__PURE__*/ jsx("p", {
229
+ children: t(selfService ? "passkeys.description" : "passkeys.recoveryDescription")
230
+ }),
231
+ !edit && error && /*#__PURE__*/ jsx(Alert, {
232
+ intent: "danger",
233
+ children: t(error)
234
+ }),
235
+ notice && /*#__PURE__*/ jsx("p", {
236
+ role: "status",
237
+ children: t(notice)
238
+ }),
239
+ loading ? /*#__PURE__*/ jsx("p", {
240
+ role: "status",
241
+ children: t('common.loading')
242
+ }) : 0 === credentials.length ? /*#__PURE__*/ jsx("p", {
243
+ className: passkey_manager_module.empty,
244
+ children: t('passkeys.empty')
245
+ }) : /*#__PURE__*/ jsx("ul", {
246
+ className: passkey_manager_module.list,
247
+ children: credentials.map((credential)=>/*#__PURE__*/ jsxs("li", {
248
+ className: passkey_manager_module.row,
249
+ children: [
250
+ /*#__PURE__*/ jsxs("div", {
251
+ children: [
252
+ /*#__PURE__*/ jsx("strong", {
253
+ children: credential.name
254
+ }),
255
+ /*#__PURE__*/ jsxs("p", {
256
+ children: [
257
+ t('passkeys.lastUsed'),
258
+ ":",
259
+ ' ',
260
+ credential.lastUsedAt ? new Date(credential.lastUsedAt).toLocaleString() : t('common.never')
261
+ ]
262
+ })
263
+ ]
264
+ }),
265
+ /*#__PURE__*/ jsxs("div", {
266
+ className: passkey_manager_module.actions,
267
+ children: [
268
+ selfService && /*#__PURE__*/ jsx(Button, {
269
+ type: "button",
270
+ size: "sm",
271
+ variant: "text",
272
+ onClick: ()=>open({
273
+ kind: 'rename',
274
+ credential
275
+ }),
276
+ "aria-label": t('passkeys.renameNamed', {
277
+ name: credential.name
278
+ }),
279
+ children: t('passkeys.rename')
280
+ }),
281
+ /*#__PURE__*/ jsx(Button, {
282
+ type: "button",
283
+ size: "sm",
284
+ intent: "danger",
285
+ variant: "text",
286
+ onClick: ()=>open({
287
+ kind: 'remove',
288
+ credential
289
+ }),
290
+ "aria-label": t('passkeys.removeNamed', {
291
+ name: credential.name
292
+ }),
293
+ children: t('passkeys.remove')
294
+ })
295
+ ]
296
+ })
297
+ ]
298
+ }, credential.id))
299
+ }),
300
+ selfService && /*#__PURE__*/ jsx(Button, {
301
+ type: "button",
302
+ size: "sm",
303
+ disabled: loading,
304
+ onClick: ()=>open({
305
+ kind: 'enroll'
306
+ }),
307
+ className: passkey_manager_module.add,
308
+ children: t('passkeys.add')
309
+ }),
310
+ /*#__PURE__*/ jsx(Modal, {
311
+ isOpen: edit?.kind === 'remove',
312
+ onDismiss: close,
313
+ closeOnOverlayClick: false,
314
+ ariaLabel: t('passkeys.remove'),
315
+ children: /*#__PURE__*/ jsxs(Modal.Container, {
316
+ className: `byline-passkey-modal ${passkey_manager_module.modal}`,
317
+ children: [
318
+ /*#__PURE__*/ jsx(Modal.Header, {
319
+ children: /*#__PURE__*/ jsx("h2", {
320
+ children: t('passkeys.remove')
321
+ })
322
+ }),
323
+ /*#__PURE__*/ jsx(Modal.Content, {
324
+ children: fields
325
+ }),
326
+ /*#__PURE__*/ jsx(Modal.Actions, {
327
+ className: `byline-passkey-modal-actions ${passkey_manager_module["modal-actions"]}`,
328
+ children: actions
329
+ })
330
+ ]
331
+ })
332
+ }),
333
+ /*#__PURE__*/ jsx(Drawer, {
334
+ id: `${id}-passkey-drawer`,
335
+ closeOnOverlayClick: false,
336
+ width: "medium",
337
+ topOffset: "46px",
338
+ isOpen: null !== edit && 'remove' !== edit.kind,
339
+ onDismiss: close,
340
+ className: `byline-passkey-drawer ${passkey_manager_module.drawer}`,
341
+ children: /*#__PURE__*/ jsxs(Drawer.Container, {
342
+ "aria-hidden": null === edit || 'remove' === edit.kind,
343
+ className: `byline-passkey-drawer-body ${passkey_manager_module["drawer-body"]}`,
344
+ children: [
345
+ /*#__PURE__*/ jsxs(Drawer.TopActions, {
346
+ children: [
347
+ /*#__PURE__*/ jsx("button", {
348
+ type: "button",
349
+ tabIndex: 0,
350
+ className: passkey_manager_module["drawer-skip"],
351
+ children: "no action"
352
+ }),
353
+ /*#__PURE__*/ jsx(IconButton, {
354
+ "aria-label": t('common.actions.close'),
355
+ size: "sm",
356
+ onClick: close,
357
+ children: /*#__PURE__*/ jsx(CloseIcon, {
358
+ width: "14px",
359
+ height: "14px",
360
+ svgClassName: "white-icon stroke-white"
361
+ })
362
+ })
363
+ ]
364
+ }),
365
+ /*#__PURE__*/ jsx(Drawer.Header, {
366
+ children: /*#__PURE__*/ jsx("h2", {
367
+ children: t(edit?.kind === 'rename' ? 'passkeys.rename' : 'passkeys.add')
368
+ })
369
+ }),
370
+ /*#__PURE__*/ jsx(Drawer.Content, {
371
+ children: /*#__PURE__*/ jsxs("div", {
372
+ className: `byline-passkey-drawer-form ${passkey_manager_module["drawer-form"]}`,
373
+ children: [
374
+ fields,
375
+ /*#__PURE__*/ jsx("div", {
376
+ className: `byline-passkey-drawer-actions ${passkey_manager_module["drawer-actions"]} ${passkey_manager_module["modal-actions"]}`,
377
+ children: actions
378
+ })
379
+ ]
380
+ })
381
+ })
382
+ ]
383
+ })
384
+ })
385
+ ]
386
+ });
387
+ if (Section) return /*#__PURE__*/ jsx(Section, {
388
+ title: t('passkeys.title'),
389
+ children: /*#__PURE__*/ jsx("div", {
390
+ className: "byline-passkey-manager",
391
+ children: body
392
+ })
393
+ });
394
+ return /*#__PURE__*/ jsxs("section", {
395
+ className: `byline-passkey-manager ${passkey_manager_module.manager}`,
396
+ "aria-labelledby": `${id}-title`,
397
+ children: [
398
+ /*#__PURE__*/ jsx("h2", {
399
+ id: `${id}-title`,
400
+ children: t('passkeys.title')
401
+ }),
402
+ body
403
+ ]
404
+ });
405
+ }
406
+ export { PasskeyManager };
@@ -0,0 +1,22 @@
1
+ import "./passkey-manager_module.css";
2
+ const passkey_manager_module = {
3
+ manager: "manager-mPrpUs",
4
+ list: "list-Mb1OlA",
5
+ empty: "empty-pDRaVX",
6
+ add: "add-Mmfyxh",
7
+ row: "row-We5Ooj",
8
+ actions: "actions-Cu2Zmf",
9
+ modal: "modal-T_EV6A",
10
+ "drawer-body": "drawer-body-GlBpJK",
11
+ drawerBody: "drawer-body-GlBpJK",
12
+ "drawer-form": "drawer-form-Qi9gs0",
13
+ drawerForm: "drawer-form-Qi9gs0",
14
+ "drawer-actions": "drawer-actions-wJGh5j",
15
+ drawerActions: "drawer-actions-wJGh5j",
16
+ "drawer-skip": "drawer-skip-Vzeo1m",
17
+ drawerSkip: "drawer-skip-Vzeo1m",
18
+ "modal-actions": "modal-actions-j4wk2d",
19
+ modalActions: "modal-actions-j4wk2d",
20
+ drawer: "drawer-pc4pSE"
21
+ };
22
+ export default passkey_manager_module;
@@ -0,0 +1,104 @@
1
+ .manager-mPrpUs {
2
+ border: 1px solid var(--border-color, #8885);
3
+ border-radius: .5rem;
4
+ padding: 1.25rem;
5
+ }
6
+
7
+ .list-Mb1OlA {
8
+ margin: 1rem 0;
9
+ padding: 0;
10
+ list-style: none;
11
+ }
12
+
13
+ .empty-pDRaVX + .add-Mmfyxh {
14
+ margin-top: .25rem;
15
+ }
16
+
17
+ .row-We5Ooj {
18
+ border-bottom: 1px solid var(--border-color, #8885);
19
+ overflow-wrap: anywhere;
20
+ flex-wrap: wrap;
21
+ justify-content: space-between;
22
+ align-items: center;
23
+ gap: .75rem;
24
+ padding: .75rem 0;
25
+ display: flex;
26
+ }
27
+
28
+ .row-We5Ooj p {
29
+ margin: .25rem 0;
30
+ font-size: .875rem;
31
+ }
32
+
33
+ .actions-Cu2Zmf {
34
+ flex-wrap: wrap;
35
+ gap: .5rem;
36
+ display: flex;
37
+ }
38
+
39
+ .modal-T_EV6A {
40
+ width: calc(100% - 2 * var(--spacing-8));
41
+ max-width: 540px;
42
+ margin-bottom: var(--spacing-8);
43
+ }
44
+
45
+ .drawer-body-GlBpJK {
46
+ padding: var(--spacing-8);
47
+ }
48
+
49
+ .drawer-form-Qi9gs0 {
50
+ gap: var(--spacing-16);
51
+ padding-top: var(--spacing-8);
52
+ flex-direction: column;
53
+ display: flex;
54
+ }
55
+
56
+ .drawer-actions-wJGh5j {
57
+ justify-content: flex-end;
58
+ gap: var(--spacing-12);
59
+ margin-top: var(--spacing-8);
60
+ display: flex;
61
+ }
62
+
63
+ .drawer-skip-Vzeo1m {
64
+ clip: rect(0, 0, 0, 0);
65
+ white-space: nowrap;
66
+ border: 0;
67
+ width: 1px;
68
+ height: 1px;
69
+ margin: -1px;
70
+ padding: 0;
71
+ position: absolute;
72
+ overflow: hidden;
73
+ }
74
+
75
+ .modal-actions-j4wk2d {
76
+ flex-direction: column-reverse;
77
+ align-items: stretch;
78
+ }
79
+
80
+ @media (max-width: 39.99rem) {
81
+ .modal-actions-j4wk2d > * {
82
+ min-height: 2.5rem;
83
+ }
84
+ }
85
+
86
+ @media (min-width: 40rem) {
87
+ .modal-T_EV6A {
88
+ width: 100%;
89
+ margin-bottom: 0;
90
+ }
91
+
92
+ .modal-actions-j4wk2d {
93
+ align-items: center;
94
+ gap: var(--spacing-8);
95
+ flex-flow: wrap;
96
+ }
97
+ }
98
+
99
+ @media (min-width: 48rem) {
100
+ .drawer-pc4pSE {
101
+ width: 540px;
102
+ }
103
+ }
104
+
@@ -1,20 +1,52 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { useState } from "react";
3
+ import { useEffect, useRef, useState } from "react";
4
4
  import { getAdminConfig } from "@byline/core";
5
5
  import { useTranslation } from "@byline/i18n/react";
6
- import { Alert, Button, Card, Input, InputPassword, LoaderEllipsis } from "@byline/ui/react";
6
+ import { Alert, Button, Card, Input, InputPassword, LoaderEllipsis, PasskeyIcon } from "@byline/ui/react";
7
7
  import clsx from "clsx";
8
8
  import { useBylineAdminServices } from "../../../services/admin-services-context.js";
9
9
  import { normalizeRootRelativeRedirect, resolveSignInFormRedirect } from "../safe-redirect.js";
10
+ import { passkeyErrorKey, signInWithPasskey } from "./passkey-browser.js";
10
11
  import sign_in_form_module from "./sign-in-form.module.js";
11
12
  function SignInForm({ redirectTo, homeUrl }) {
12
- const { adminSignIn } = useBylineAdminServices();
13
+ const { adminSignIn, passkeys } = useBylineAdminServices();
13
14
  const { t } = useTranslation('byline-admin');
14
15
  const [email, setEmail] = useState('');
15
16
  const [password, setPassword] = useState('');
16
17
  const [pending, setPending] = useState(false);
17
18
  const [error, setError] = useState(null);
19
+ const mounted = useRef(false);
20
+ const [passkeysEnabled, setPasskeysEnabled] = useState(false);
21
+ useEffect(()=>{
22
+ let active = true;
23
+ mounted.current = true;
24
+ passkeys?.capability().then(({ enabled })=>{
25
+ if (active) setPasskeysEnabled(enabled);
26
+ }).catch(()=>{});
27
+ return ()=>{
28
+ active = false;
29
+ mounted.current = false;
30
+ };
31
+ }, [
32
+ passkeys
33
+ ]);
34
+ async function handlePasskey() {
35
+ if (!passkeys || pending) return;
36
+ setPending(true);
37
+ setError(null);
38
+ try {
39
+ await signInWithPasskey(passkeys, ()=>mounted.current);
40
+ } catch (error) {
41
+ if (mounted.current) {
42
+ setError(t(passkeyErrorKey(error)));
43
+ setPending(false);
44
+ }
45
+ return;
46
+ }
47
+ const destination = resolveSignInFormRedirect(redirectTo, getAdminConfig().routes.admin);
48
+ window.location.assign(normalizeRootRelativeRedirect(destination) ?? '/');
49
+ }
18
50
  async function handleSubmit(event) {
19
51
  event.preventDefault();
20
52
  if (pending) return;
@@ -115,6 +147,33 @@ function SignInForm({ redirectTo, homeUrl }) {
115
147
  })
116
148
  ]
117
149
  })
150
+ }),
151
+ passkeysEnabled && /*#__PURE__*/ jsxs(Card.Footer, {
152
+ className: clsx('byline-sign-in-alternate', sign_in_form_module.alternate),
153
+ children: [
154
+ /*#__PURE__*/ jsx("div", {
155
+ className: clsx('byline-sign-in-divider', sign_in_form_module.divider),
156
+ children: /*#__PURE__*/ jsx("span", {
157
+ children: t('auth.signIn.alternate')
158
+ })
159
+ }),
160
+ /*#__PURE__*/ jsxs(Button, {
161
+ type: "button",
162
+ variant: "outlined",
163
+ disabled: pending,
164
+ onClick: handlePasskey,
165
+ className: clsx('byline-passkey-sign-in-button', sign_in_form_module["passkey-button"]),
166
+ children: [
167
+ /*#__PURE__*/ jsx(PasskeyIcon, {
168
+ width: "20px",
169
+ height: "20px"
170
+ }),
171
+ /*#__PURE__*/ jsx("span", {
172
+ children: t('passkeys.signIn')
173
+ })
174
+ ]
175
+ })
176
+ ]
118
177
  })
119
178
  ]
120
179
  });
@@ -7,6 +7,10 @@ const sign_in_form_module = {
7
7
  actions: "actions-bVKfoJ",
8
8
  "home-link": "home-link-YfGVyd",
9
9
  homeLink: "home-link-YfGVyd",
10
- button: "button-paNjW_"
10
+ button: "button-paNjW_",
11
+ alternate: "alternate-bC6QfW",
12
+ divider: "divider-QyDRIH",
13
+ "passkey-button": "passkey-button-LpEiWc",
14
+ passkeyButton: "passkey-button-LpEiWc"
11
15
  };
12
16
  export default sign_in_form_module;
@@ -39,3 +39,43 @@
39
39
  margin-left: auto;
40
40
  }
41
41
 
42
+ :is(.alternate-bC6QfW, .byline-sign-in-alternate) {
43
+ align-items: stretch;
44
+ gap: var(--spacing-16);
45
+ padding-bottom: var(--spacing-24);
46
+ flex-direction: column;
47
+ }
48
+
49
+ :is(.divider-QyDRIH, .byline-sign-in-divider) {
50
+ align-items: center;
51
+ gap: var(--spacing-12);
52
+ color: var(--gray-500);
53
+ font-size: var(--font-size-xs);
54
+ text-transform: uppercase;
55
+ letter-spacing: .04em;
56
+ display: flex;
57
+ }
58
+
59
+ .divider-QyDRIH:before, .divider-QyDRIH:after {
60
+ content: "";
61
+ border-top: var(--border-width-thin) var(--border-style-solid) var(--border-color, #8885);
62
+ flex: 1;
63
+ }
64
+
65
+ .byline-sign-in-divider:before {
66
+ content: "";
67
+ border-top: var(--border-width-thin) var(--border-style-solid) var(--border-color, #8885);
68
+ flex: 1;
69
+ }
70
+
71
+ .byline-sign-in-divider:after {
72
+ content: "";
73
+ border-top: var(--border-width-thin) var(--border-style-solid) var(--border-color, #8885);
74
+ flex: 1;
75
+ }
76
+
77
+ :is(.passkey-button-LpEiWc, .byline-passkey-sign-in-button) {
78
+ gap: var(--spacing-8);
79
+ width: 100%;
80
+ }
81
+