@byline/host-tanstack-start 6.7.2 → 6.9.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 (74) hide show
  1. package/dist/admin-shell/admin-roles/container_module.css +1 -1
  2. package/dist/admin-shell/admin-roles/delete.js +1 -1
  3. package/dist/admin-shell/admin-users/container.js +181 -159
  4. package/dist/admin-shell/admin-users/container.module.js +2 -1
  5. package/dist/admin-shell/admin-users/container_module.css +5 -1
  6. package/dist/admin-shell/admin-users/delete.js +1 -1
  7. package/dist/admin-shell/chrome/admin-app-bar.d.ts +0 -7
  8. package/dist/admin-shell/collections/api.d.ts +6 -2
  9. package/dist/admin-shell/collections/api.js +109 -3
  10. package/dist/admin-shell/collections/api.test.d.ts +8 -0
  11. package/dist/admin-shell/collections/edit.js +2 -0
  12. package/dist/admin-shell/collections/preview-link.js +17 -13
  13. package/dist/admin-shell/collections/view-menu.d.ts +8 -1
  14. package/dist/admin-shell/collections/view-menu.js +124 -107
  15. package/dist/admin-shell/collections/view-menu.module.js +1 -0
  16. package/dist/admin-shell/collections/view-menu_module.css +15 -1
  17. package/dist/admin-shell/scheduled-publications/list.js +2 -2
  18. package/dist/admin-shell/singletons/preview-link.js +17 -13
  19. package/dist/admin-shell/singletons/view-menu.js +78 -63
  20. package/dist/admin-shell/singletons/view.js +2 -0
  21. package/dist/integrations/byline-admin-services.js +2 -0
  22. package/dist/integrations/passkey-admin-services.d.ts +9 -0
  23. package/dist/integrations/passkey-admin-services.js +53 -0
  24. package/dist/integrations/session-coordination.d.ts +3 -1
  25. package/dist/integrations/session-coordination.js +3 -1
  26. package/dist/integrations/session-middleware.d.ts +3 -1
  27. package/dist/integrations/session-middleware.js +87 -79
  28. package/dist/integrations/sign-in-body.d.ts +3 -0
  29. package/dist/integrations/sign-in-body.js +19 -4
  30. package/dist/integrations/sign-in-middleware.js +30 -5
  31. package/dist/server-fns/auth/passkey-admission.d.ts +24 -0
  32. package/dist/server-fns/auth/passkey-admission.js +88 -0
  33. package/dist/server-fns/auth/passkeys.d.ts +96 -0
  34. package/dist/server-fns/auth/passkeys.js +238 -0
  35. package/dist/server-fns/auth/reconcile-locale.d.ts +20 -0
  36. package/dist/server-fns/auth/reconcile-locale.js +16 -0
  37. package/dist/server-fns/auth/sign-in.js +1 -15
  38. package/dist/server-fns/collections/get.d.ts +1 -0
  39. package/dist/server-fns/collections/get.js +4 -0
  40. package/package.json +11 -11
  41. package/src/admin-shell/admin-roles/container.module.css +1 -1
  42. package/src/admin-shell/admin-roles/delete.tsx +1 -1
  43. package/src/admin-shell/admin-users/container.module.css +7 -1
  44. package/src/admin-shell/admin-users/container.tsx +22 -2
  45. package/src/admin-shell/admin-users/delete.tsx +1 -1
  46. package/src/admin-shell/chrome/admin-app-bar.tsx +2 -2
  47. package/src/admin-shell/chrome/dashboard.test.tsx +2 -2
  48. package/src/admin-shell/collections/api.test.tsx +223 -0
  49. package/src/admin-shell/collections/api.tsx +98 -3
  50. package/src/admin-shell/collections/edit.tsx +9 -1
  51. package/src/admin-shell/collections/preview-link.tsx +13 -12
  52. package/src/admin-shell/collections/view-menu.module.css +29 -3
  53. package/src/admin-shell/collections/view-menu.tsx +114 -96
  54. package/src/admin-shell/scheduled-publications/list.tsx +3 -4
  55. package/src/admin-shell/singletons/history.test.tsx +1 -1
  56. package/src/admin-shell/singletons/preview-link.tsx +13 -12
  57. package/src/admin-shell/singletons/view-menu.tsx +65 -59
  58. package/src/admin-shell/singletons/view.test.tsx +4 -4
  59. package/src/admin-shell/singletons/view.tsx +3 -1
  60. package/src/integrations/byline-admin-services.ts +3 -0
  61. package/src/integrations/passkey-admin-services.ts +85 -0
  62. package/src/integrations/session-client.test.node.ts +73 -0
  63. package/src/integrations/session-coordination.ts +3 -1
  64. package/src/integrations/session-middleware.ts +86 -72
  65. package/src/integrations/sign-in-body.test.node.ts +31 -1
  66. package/src/integrations/sign-in-body.ts +27 -3
  67. package/src/integrations/sign-in-middleware.ts +42 -9
  68. package/src/server-fns/auth/passkey-admission.test.node.ts +173 -0
  69. package/src/server-fns/auth/passkey-admission.ts +125 -0
  70. package/src/server-fns/auth/passkeys.test.node.ts +247 -0
  71. package/src/server-fns/auth/passkeys.ts +252 -0
  72. package/src/server-fns/auth/reconcile-locale.ts +40 -0
  73. package/src/server-fns/auth/sign-in.ts +1 -32
  74. package/src/server-fns/collections/get.ts +3 -0
@@ -0,0 +1,238 @@
1
+ import { createServerFn } from "@tanstack/react-start";
2
+ import { getRequest, setResponseHeader } from "@tanstack/react-start/server";
3
+ import { ERR_SESSION_CHANGED, ERR_UNAUTHENTICATED, isAdminAuth } from "@byline/auth";
4
+ import { clearPreviewCookie, clearSessionCookies, getAdminRequestContext, readAccessTokenCookie, readRefreshTokenCookie, setSessionCookies } from "@byline/client/server";
5
+ import { getServerConfig } from "@byline/core";
6
+ import { z } from "zod";
7
+ import { bylineCore } from "../../integrations/byline-core.js";
8
+ import { adminSessionMiddleware, adminSessionMutationMiddleware } from "../../integrations/session-middleware.js";
9
+ import { wasPasskeyBodyChecked } from "../../integrations/sign-in-body.js";
10
+ import { withPasskeyAdmission } from "./passkey-admission.js";
11
+ import { reconcileLocaleAfterSignIn } from "./reconcile-locale.js";
12
+ const session = z.object({
13
+ expectedSessionId: z.uuid()
14
+ });
15
+ const bearer = z.string().regex(/^[A-Za-z0-9_-]{43}$/);
16
+ const action = z.discriminatedUnion('purpose', [
17
+ z.object({
18
+ purpose: z.literal('enroll')
19
+ }).strict(),
20
+ z.object({
21
+ purpose: z.literal('remove'),
22
+ targetUserId: z.uuid(),
23
+ credentialRecordId: z.uuid()
24
+ }).strict()
25
+ ]);
26
+ const passkeys_name = z.string().trim().min(1).max(80);
27
+ function validate(schema) {
28
+ return (input)=>{
29
+ const parsed = schema.safeParse(input);
30
+ if (!parsed.success) throw new Response('Invalid passkey input', {
31
+ status: 400,
32
+ headers: {
33
+ 'Cache-Control': 'no-store'
34
+ }
35
+ });
36
+ return parsed.data;
37
+ };
38
+ }
39
+ function configuredProvider() {
40
+ setResponseHeader('Cache-Control', 'no-store');
41
+ const config = getServerConfig();
42
+ const provider = config.sessionProvider;
43
+ if (!config.passkeys || provider?.capabilities?.passkeys !== true || !provider.passkeys) throw new Response('Passkeys are not enabled', {
44
+ status: 404,
45
+ headers: {
46
+ 'Cache-Control': 'no-store'
47
+ }
48
+ });
49
+ return provider.passkeys;
50
+ }
51
+ async function currentSession(expectedSessionId) {
52
+ const auth = await getAdminRequestContext();
53
+ if (auth.sessionId !== expectedSessionId) throw ERR_SESSION_CHANGED({
54
+ message: 'session changed before passkey operation'
55
+ });
56
+ const accessToken = readAccessTokenCookie();
57
+ if (!accessToken) throw ERR_UNAUTHENTICATED({
58
+ message: 'access credential missing'
59
+ });
60
+ return {
61
+ accessToken,
62
+ expectedSessionId
63
+ };
64
+ }
65
+ function admitted(purpose, work, expectedSessionId) {
66
+ const provider = configuredProvider();
67
+ const config = getServerConfig();
68
+ const request = getRequest();
69
+ return withPasskeyAdmission({
70
+ provider,
71
+ purpose,
72
+ request,
73
+ bodyChecked: wasPasskeyBodyChecked(request, 'password-proof' === purpose),
74
+ protection: config.passkeyRequests,
75
+ ...'password-proof' === purpose ? {
76
+ password: {
77
+ protection: config.passwordSignIn,
78
+ resolveActorEmail: async ()=>{
79
+ const auth = await getAdminRequestContext();
80
+ if (!auth.actor || !expectedSessionId || auth.sessionId !== expectedSessionId) throw ERR_SESSION_CHANGED({
81
+ message: 'session changed before password proof'
82
+ });
83
+ const user = await bylineCore().adminStore?.adminUsers.getById(auth.actor.id);
84
+ if (!user) throw ERR_UNAUTHENTICATED({
85
+ message: 'account unavailable'
86
+ });
87
+ return user.email;
88
+ }
89
+ }
90
+ } : {},
91
+ work: ({ ip })=>work(provider, {
92
+ ip,
93
+ userAgent: request.headers.get('user-agent')?.slice(0, 512)
94
+ })
95
+ });
96
+ }
97
+ const getPasskeyCapability = createServerFn({
98
+ method: 'GET'
99
+ }).handler(async ()=>{
100
+ setResponseHeader('Cache-Control', 'no-store');
101
+ const config = getServerConfig();
102
+ return {
103
+ enabled: !!(config.passkeys && config.sessionProvider?.capabilities?.passkeys && config.sessionProvider.passkeys)
104
+ };
105
+ });
106
+ const passkeySignInOptions = createServerFn({
107
+ method: 'POST'
108
+ }).validator(validate(z.object({}).strict())).handler(async ()=>admitted('sign-in-options', (provider, metadata)=>provider.signInOptions(metadata)));
109
+ const passkeySignIn = createServerFn({
110
+ method: 'POST'
111
+ }).validator(validate(z.object({
112
+ ceremonyId: bearer,
113
+ response: z.unknown()
114
+ }).strict())).handler(async ({ data })=>admitted('sign-in', async (provider, metadata)=>{
115
+ const result = await provider.verifySignIn({
116
+ ...data,
117
+ ...metadata,
118
+ previousAccessToken: readAccessTokenCookie(),
119
+ previousRefreshToken: readRefreshTokenCookie()
120
+ });
121
+ setSessionCookies(result);
122
+ try {
123
+ await reconcileLocaleAfterSignIn(result.actor.id);
124
+ } catch {}
125
+ return {
126
+ userId: result.actor.id,
127
+ sessionId: result.sessionId
128
+ };
129
+ }));
130
+ const passkeyPasswordProof = createServerFn({
131
+ method: 'POST'
132
+ }).middleware([
133
+ adminSessionMiddleware
134
+ ]).validator(validate(session.extend({
135
+ password: z.string().min(1).max(128),
136
+ action: action
137
+ }).strict())).handler(async ({ data })=>admitted('password-proof', async (provider)=>provider.passwordProof({
138
+ ...data,
139
+ ...await currentSession(data.expectedSessionId)
140
+ }), data.expectedSessionId));
141
+ const passkeyProofOptions = createServerFn({
142
+ method: 'POST'
143
+ }).middleware([
144
+ adminSessionMiddleware
145
+ ]).validator(validate(session.extend({
146
+ action: action
147
+ }).strict())).handler(async ({ data })=>admitted('proof-options', async (provider)=>provider.proofOptions({
148
+ ...data,
149
+ ...await currentSession(data.expectedSessionId)
150
+ })));
151
+ const passkeyProof = createServerFn({
152
+ method: 'POST'
153
+ }).middleware([
154
+ adminSessionMiddleware
155
+ ]).validator(validate(session.extend({
156
+ ceremonyId: bearer,
157
+ response: z.unknown()
158
+ }).strict())).handler(async ({ data })=>admitted('proof', async (provider)=>provider.verifyProof({
159
+ ...data,
160
+ ...await currentSession(data.expectedSessionId)
161
+ })));
162
+ const passkeyEnrollmentOptions = createServerFn({
163
+ method: 'POST'
164
+ }).middleware([
165
+ adminSessionMiddleware
166
+ ]).validator(validate(session.extend({
167
+ grantId: bearer
168
+ }).strict())).handler(async ({ data })=>admitted('enrollment-options', async (provider)=>provider.enrollmentOptions({
169
+ ...data,
170
+ ...await currentSession(data.expectedSessionId)
171
+ })));
172
+ const passkeyEnrollment = createServerFn({
173
+ method: 'POST'
174
+ }).middleware([
175
+ adminSessionMiddleware
176
+ ]).validator(validate(session.extend({
177
+ ceremonyId: bearer,
178
+ response: z.unknown(),
179
+ name: passkeys_name
180
+ }).strict())).handler(async ({ data })=>admitted('enrollment', async (provider)=>provider.verifyEnrollment({
181
+ ...data,
182
+ ...await currentSession(data.expectedSessionId)
183
+ })));
184
+ const passkeyList = createServerFn({
185
+ method: 'POST'
186
+ }).middleware([
187
+ adminSessionMiddleware
188
+ ]).validator(validate(session.extend({
189
+ targetUserId: z.uuid().optional()
190
+ }).strict())).handler(async ({ data })=>admitted('list', async (provider)=>provider.listCredentials({
191
+ ...data,
192
+ ...await currentSession(data.expectedSessionId)
193
+ })));
194
+ const passkeyRename = createServerFn({
195
+ method: 'POST'
196
+ }).middleware([
197
+ adminSessionMiddleware
198
+ ]).validator(validate(session.extend({
199
+ credentialRecordId: z.uuid(),
200
+ name: passkeys_name
201
+ }).strict())).handler(async ({ data })=>admitted('rename', async (provider)=>provider.renameCredential({
202
+ ...data,
203
+ ...await currentSession(data.expectedSessionId)
204
+ })));
205
+ const passkeyRemove = createServerFn({
206
+ method: 'POST'
207
+ }).middleware([
208
+ adminSessionMutationMiddleware
209
+ ]).validator(validate(session.extend({
210
+ grantId: bearer
211
+ }).strict())).handler(async ({ data })=>admitted('remove', async (provider)=>{
212
+ const result = await provider.removeCredential({
213
+ ...data,
214
+ ...await currentSession(data.expectedSessionId)
215
+ });
216
+ if (result.signedOut) {
217
+ clearSessionCookies();
218
+ clearPreviewCookie();
219
+ }
220
+ return result;
221
+ }));
222
+ const passkeyManagementContext = createServerFn({
223
+ method: 'POST'
224
+ }).middleware([
225
+ adminSessionMiddleware
226
+ ]).validator(validate(session.strict())).handler(async ({ data })=>admitted('list', async ()=>{
227
+ await currentSession(data.expectedSessionId);
228
+ const { actor } = await getAdminRequestContext();
229
+ if (!isAdminAuth(actor)) throw ERR_UNAUTHENTICATED({
230
+ message: 'admin login required'
231
+ });
232
+ return {
233
+ id: actor.id,
234
+ isSuperAdmin: actor.isSuperAdmin,
235
+ canManagePasskeys: actor.hasAbility('admin.users.managePasskeys')
236
+ };
237
+ }));
238
+ export { getPasskeyCapability, passkeyEnrollment, passkeyEnrollmentOptions, passkeyList, passkeyManagementContext, passkeyPasswordProof, passkeyProof, passkeyProofOptions, passkeyRemove, passkeyRename, passkeySignIn, passkeySignInOptions };
@@ -0,0 +1,20 @@
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
+ * Apply the `byline_admin_lng` cookie to `admin_users.preferred_locale`
10
+ * when the two diverge after sign-in. No-op when:
11
+ *
12
+ * - The cookie is unset (the cascade falls through to the existing
13
+ * column / Accept-Language / default anyway).
14
+ * - The cookie carries a locale outside `i18n.admin.locales`
15
+ * (stale value pointing at a removed locale — let the resolver
16
+ * fall through cleanly).
17
+ * - No admin store is configured (headless tooling paths).
18
+ * - The stored value already matches the cookie.
19
+ */
20
+ export declare function reconcileLocaleAfterSignIn(adminUserId: string): Promise<void>;
@@ -0,0 +1,16 @@
1
+ import { readAdminLocaleCookie } from "../../i18n/locale-cookie.js";
2
+ import { bylineCore } from "../../integrations/byline-core.js";
3
+ async function reconcileLocaleAfterSignIn(adminUserId) {
4
+ const cookieLocale = readAdminLocaleCookie();
5
+ if (null == cookieLocale) return;
6
+ const core = bylineCore();
7
+ const locales = core.config.i18n.admin.locales;
8
+ if (!locales.includes(cookieLocale)) return;
9
+ const adminStore = core.adminStore;
10
+ if (null == adminStore) return;
11
+ const row = await adminStore.adminUsers.getById(adminUserId);
12
+ if (!row) return;
13
+ if (row.preferred_locale === cookieLocale) return;
14
+ await adminStore.adminUsers.setPreferredLocale(adminUserId, cookieLocale);
15
+ }
16
+ export { reconcileLocaleAfterSignIn };
@@ -3,10 +3,9 @@ import { getRequest, getRequestHeader } from "@tanstack/react-start/server";
3
3
  import { readAccessTokenCookie, readRefreshTokenCookie, setSessionCookies } from "@byline/client/server";
4
4
  import { getServerConfig } from "@byline/core";
5
5
  import { passwordSignInSchema } from "@byline/core/validation";
6
- import { readAdminLocaleCookie } from "../../i18n/locale-cookie.js";
7
- import { bylineCore } from "../../integrations/byline-core.js";
8
6
  import { normalizeClientIp } from "../../integrations/client-ip.js";
9
7
  import { wasSignInBodyChecked } from "../../integrations/sign-in-body.js";
8
+ import { reconcileLocaleAfterSignIn } from "./reconcile-locale.js";
10
9
  const adminSignIn = createServerFn({
11
10
  method: 'POST'
12
11
  }).validator((input)=>{
@@ -104,17 +103,4 @@ const adminSignIn = createServerFn({
104
103
  sessionId: result.sessionId
105
104
  };
106
105
  });
107
- async function reconcileLocaleAfterSignIn(adminUserId) {
108
- const cookieLocale = readAdminLocaleCookie();
109
- if (null == cookieLocale) return;
110
- const core = bylineCore();
111
- const locales = core.config.i18n.admin.locales;
112
- if (!locales.includes(cookieLocale)) return;
113
- const adminStore = core.adminStore;
114
- if (null == adminStore) return;
115
- const row = await adminStore.adminUsers.getById(adminUserId);
116
- if (!row) return;
117
- if (row.preferred_locale === cookieLocale) return;
118
- await adminStore.adminUsers.setPreferredLocale(adminUserId, cookieLocale);
119
- }
120
106
  export { adminSignIn };
@@ -18,6 +18,7 @@
18
18
  export declare function getCollectionDocument(collection: string, id: string, locale?: string, depth?: number, populateRelations?: boolean): Promise<{
19
19
  revision: number;
20
20
  _publishedVersion: Record<string, any> | null;
21
+ _unavailableBlocks?: import("@byline/core").UnavailableBlock[] | undefined;
21
22
  _restoreWarnings?: string[] | undefined;
22
23
  availableLocales?: string[] | undefined;
23
24
  _availableVersionLocales?: string[] | undefined;
@@ -57,6 +57,7 @@ const getDocumentFn = createServerFn({
57
57
  }) : null;
58
58
  }
59
59
  const restoreWarnings = serialised._restoreWarnings;
60
+ const unavailableBlocks = document._unavailableBlocks;
60
61
  const availableLocales = serialised.availableLocales;
61
62
  const availableVersionLocales = serialised._availableVersionLocales;
62
63
  const scheduledPublicationEnabled = getServerConfig().scheduledPublication?.enabled === true;
@@ -71,6 +72,9 @@ const getDocumentFn = createServerFn({
71
72
  return {
72
73
  ...withEditableRevision(parsed, document),
73
74
  _publishedVersion: publishedVersion,
75
+ ...unavailableBlocks?.length ? {
76
+ _unavailableBlocks: unavailableBlocks
77
+ } : {},
74
78
  ...restoreWarnings && restoreWarnings.length > 0 ? {
75
79
  _restoreWarnings: restoreWarnings
76
80
  } : {},
package/package.json CHANGED
@@ -3,9 +3,9 @@
3
3
  "private": false,
4
4
  "type": "module",
5
5
  "license": "MPL-2.0",
6
- "version": "6.7.2",
6
+ "version": "6.9.0",
7
7
  "engines": {
8
- "node": ">=20.9.0"
8
+ "node": ">=22.22.2"
9
9
  },
10
10
  "description": "TanStack Start host adapter for Byline CMS — server fns, auth context, integration glue, admin shell, and route factories",
11
11
  "keywords": [
@@ -119,15 +119,15 @@
119
119
  }
120
120
  },
121
121
  "dependencies": {
122
- "@byline/admin": "6.7.2",
123
- "@byline/ai": "6.7.2",
124
- "@byline/analytics": "6.7.2",
125
- "@byline/analytics-agent": "6.7.2",
126
- "@byline/auth": "6.7.2",
127
- "@byline/client": "6.7.2",
128
- "@byline/core": "6.7.2",
129
- "@byline/i18n": "6.7.2",
130
- "@byline/ui": "6.7.2",
122
+ "@byline/admin": "6.9.0",
123
+ "@byline/ai": "6.9.0",
124
+ "@byline/analytics": "6.9.0",
125
+ "@byline/analytics-agent": "6.9.0",
126
+ "@byline/auth": "6.9.0",
127
+ "@byline/client": "6.9.0",
128
+ "@byline/core": "6.9.0",
129
+ "@byline/i18n": "6.9.0",
130
+ "@byline/ui": "6.9.0",
131
131
  "@dnd-kit/core": "^6.3.1",
132
132
  "@dnd-kit/sortable": "^10.0.0",
133
133
  "clsx": "^2.1.1",
@@ -17,7 +17,7 @@
17
17
  :global(.byline-role-detail-card) {
18
18
  margin-bottom: 1.5rem;
19
19
  border: var(--border-width-thin) var(--border-style-solid) var(--gray-100);
20
- border-radius: 0.125rem;
20
+ border-radius: var(--border-radius-sm);
21
21
  background-color: var(--canvas-25);
22
22
  padding: 1rem;
23
23
  }
@@ -88,7 +88,7 @@ export function DeleteRole({ role, onClose }: DeleteRoleProps) {
88
88
  <div className={cx('byline-role-delete-actions', styles.actions)}>
89
89
  <Button
90
90
  type="button"
91
- intent="secondary"
91
+ intent="noeffect"
92
92
  size="sm"
93
93
  onClick={onClose}
94
94
  disabled={pending}
@@ -18,6 +18,7 @@
18
18
  * .byline-admin-user-drawer-body — drawer content container
19
19
  * .byline-admin-user-drawer-scroll — drawer scrollable inner viewport
20
20
  * .byline-admin-user-modal — modal container sizing
21
+ * .byline-admin-user-account-link — own-account link in the Passkeys card
21
22
  * .byline-admin-user-modal-head — modal header row
22
23
  */
23
24
 
@@ -50,7 +51,7 @@
50
51
  border: var(--border-width-thin) var(--border-style-solid) var(--gray-100);
51
52
  background-color: var(--canvas-25);
52
53
  padding: 1rem;
53
- border-radius: 0.125rem;
54
+ border-radius: var(--border-radius-sm);
54
55
  }
55
56
 
56
57
  :is([data-theme="dark"], :global(.dark)) .section,
@@ -197,3 +198,8 @@
197
198
  justify-content: space-between;
198
199
  margin-bottom: 1rem;
199
200
  }
201
+
202
+ .accountLink,
203
+ :global(.byline-admin-user-account-link) {
204
+ text-decoration: underline;
205
+ }
@@ -15,6 +15,7 @@ import { useRouter } from '@tanstack/react-router'
15
15
  import { UserRoles } from '@byline/admin/admin-users/components/roles'
16
16
  import { SetPassword } from '@byline/admin/admin-users/components/set-password'
17
17
  import { UpdateUser } from '@byline/admin/admin-users/components/update'
18
+ import { PasskeyManager } from '@byline/admin/auth/components/passkey-manager'
18
19
  import { LocalDateTime } from '@byline/admin/react'
19
20
  import { useTranslation } from '@byline/i18n/react'
20
21
  import {
@@ -28,6 +29,8 @@ import {
28
29
  } from '@byline/ui/react'
29
30
  import cx from 'clsx'
30
31
 
32
+ import { getAdminRoutePath } from '../../routes/admin-path.js'
33
+ import { Link } from '../chrome/loose-router.js'
31
34
  import styles from './container.module.css'
32
35
  import { DeleteUser } from './delete.js'
33
36
  import type { AdminRoleResponse } from '../../server-fns/admin-roles/index.js'
@@ -266,9 +269,7 @@ export function AccountContainer({ user, allRoles, initialUserRoles }: AccountCo
266
269
  </p>
267
270
  </div>
268
271
  </ContainerSection>
269
- </div>
270
272
 
271
- <div className={cx('byline-admin-user-column', styles.column)}>
272
273
  <ContainerSection
273
274
  title={t('adminUsers.detail.sections.roles')}
274
275
  onEdit={openDrawer('roles')}
@@ -289,7 +290,9 @@ export function AccountContainer({ user, allRoles, initialUserRoles }: AccountCo
289
290
  {t('adminUsers.detail.editRolesButton')}
290
291
  </Button>
291
292
  </ContainerSection>
293
+ </div>
292
294
 
295
+ <div className={cx('byline-admin-user-column', styles.column)}>
293
296
  <ContainerSection
294
297
  title={t('adminUsers.detail.sections.password')}
295
298
  onEdit={openDrawer('set_password')}
@@ -303,6 +306,23 @@ export function AccountContainer({ user, allRoles, initialUserRoles }: AccountCo
303
306
  </Button>
304
307
  </ContainerSection>
305
308
 
309
+ <PasskeyManager
310
+ targetUserId={currentUser.id}
311
+ targetIsSuperAdmin={currentUser.is_super_admin}
312
+ section={ContainerSection}
313
+ ownAccountNotice={
314
+ <p className={cx('byline-admin-user-line-spaced', styles.lineSpaced)}>
315
+ {t('adminUsers.detail.passkeys.ownAccount')}{' '}
316
+ <Link
317
+ to={getAdminRoutePath('account')}
318
+ className={cx('byline-admin-user-account-link', styles.accountLink)}
319
+ >
320
+ {t('adminUsers.detail.passkeys.openAccount')}
321
+ </Link>
322
+ </p>
323
+ }
324
+ />
325
+
306
326
  <ContainerSection title={t('adminUsers.detail.sections.delete')}>
307
327
  <p className={cx('byline-admin-user-line-spaced', styles.lineSpaced)}>
308
328
  {t('adminUsers.detail.delete.intro')}
@@ -100,7 +100,7 @@ export function DeleteUser({ user, onClose }: DeleteUserProps) {
100
100
  <div className={cx('byline-admin-user-delete-actions', styles.actions)}>
101
101
  <Button
102
102
  type="button"
103
- intent="secondary"
103
+ intent="noeffect"
104
104
  size="sm"
105
105
  onClick={onClose}
106
106
  disabled={pending}
@@ -1,3 +1,5 @@
1
+ 'use client'
2
+
1
3
  /**
2
4
  * This Source Code is subject to the terms of the Mozilla Public
3
5
  * License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -20,8 +22,6 @@ import { Branding } from './branding.js'
20
22
  import { Breadcrumbs } from './breadcrumbs/breadcrumbs.js'
21
23
  import { useBreadcrumbs } from './breadcrumbs/breadcrumbs-provider.js'
22
24
 
23
- ;('use client')
24
-
25
25
  interface AdminAppBarProps {
26
26
  user: CurrentAdminUser
27
27
  }
@@ -129,8 +129,8 @@ describe('AdminDashboard document resources', () => {
129
129
  routes: { admin: '/internal/cms' },
130
130
  collections: [articles, settings],
131
131
  admin: [
132
- { slug: articles.path, group: 'content' },
133
- { singleton: true, slug: settings.path, group: 'content' },
132
+ { path: articles.path, group: 'content' },
133
+ { singleton: true, path: settings.path, group: 'content' },
134
134
  ],
135
135
  collectionGroups: [{ name: 'content', label: 'Content' }],
136
136
  i18n: {