@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.
- package/dist/admin-shell/admin-roles/container_module.css +1 -1
- package/dist/admin-shell/admin-roles/delete.js +1 -1
- package/dist/admin-shell/admin-users/container.js +181 -159
- package/dist/admin-shell/admin-users/container.module.js +2 -1
- package/dist/admin-shell/admin-users/container_module.css +5 -1
- package/dist/admin-shell/admin-users/delete.js +1 -1
- package/dist/admin-shell/chrome/admin-app-bar.d.ts +0 -7
- package/dist/admin-shell/collections/api.d.ts +6 -2
- package/dist/admin-shell/collections/api.js +109 -3
- package/dist/admin-shell/collections/api.test.d.ts +8 -0
- package/dist/admin-shell/collections/edit.js +2 -0
- package/dist/admin-shell/collections/preview-link.js +17 -13
- package/dist/admin-shell/collections/view-menu.d.ts +8 -1
- package/dist/admin-shell/collections/view-menu.js +124 -107
- package/dist/admin-shell/collections/view-menu.module.js +1 -0
- package/dist/admin-shell/collections/view-menu_module.css +15 -1
- package/dist/admin-shell/scheduled-publications/list.js +2 -2
- package/dist/admin-shell/singletons/preview-link.js +17 -13
- package/dist/admin-shell/singletons/view-menu.js +78 -63
- package/dist/admin-shell/singletons/view.js +2 -0
- package/dist/integrations/byline-admin-services.js +2 -0
- package/dist/integrations/passkey-admin-services.d.ts +9 -0
- package/dist/integrations/passkey-admin-services.js +53 -0
- package/dist/integrations/session-coordination.d.ts +3 -1
- package/dist/integrations/session-coordination.js +3 -1
- package/dist/integrations/session-middleware.d.ts +3 -1
- package/dist/integrations/session-middleware.js +87 -79
- package/dist/integrations/sign-in-body.d.ts +3 -0
- package/dist/integrations/sign-in-body.js +19 -4
- package/dist/integrations/sign-in-middleware.js +30 -5
- package/dist/server-fns/auth/passkey-admission.d.ts +24 -0
- package/dist/server-fns/auth/passkey-admission.js +88 -0
- package/dist/server-fns/auth/passkeys.d.ts +96 -0
- package/dist/server-fns/auth/passkeys.js +238 -0
- package/dist/server-fns/auth/reconcile-locale.d.ts +20 -0
- package/dist/server-fns/auth/reconcile-locale.js +16 -0
- package/dist/server-fns/auth/sign-in.js +1 -15
- package/dist/server-fns/collections/get.d.ts +1 -0
- package/dist/server-fns/collections/get.js +4 -0
- package/package.json +11 -11
- package/src/admin-shell/admin-roles/container.module.css +1 -1
- package/src/admin-shell/admin-roles/delete.tsx +1 -1
- package/src/admin-shell/admin-users/container.module.css +7 -1
- package/src/admin-shell/admin-users/container.tsx +22 -2
- package/src/admin-shell/admin-users/delete.tsx +1 -1
- package/src/admin-shell/chrome/admin-app-bar.tsx +2 -2
- package/src/admin-shell/chrome/dashboard.test.tsx +2 -2
- package/src/admin-shell/collections/api.test.tsx +223 -0
- package/src/admin-shell/collections/api.tsx +98 -3
- package/src/admin-shell/collections/edit.tsx +9 -1
- package/src/admin-shell/collections/preview-link.tsx +13 -12
- package/src/admin-shell/collections/view-menu.module.css +29 -3
- package/src/admin-shell/collections/view-menu.tsx +114 -96
- package/src/admin-shell/scheduled-publications/list.tsx +3 -4
- package/src/admin-shell/singletons/history.test.tsx +1 -1
- package/src/admin-shell/singletons/preview-link.tsx +13 -12
- package/src/admin-shell/singletons/view-menu.tsx +65 -59
- package/src/admin-shell/singletons/view.test.tsx +4 -4
- package/src/admin-shell/singletons/view.tsx +3 -1
- package/src/integrations/byline-admin-services.ts +3 -0
- package/src/integrations/passkey-admin-services.ts +85 -0
- package/src/integrations/session-client.test.node.ts +73 -0
- package/src/integrations/session-coordination.ts +3 -1
- package/src/integrations/session-middleware.ts +86 -72
- package/src/integrations/sign-in-body.test.node.ts +31 -1
- package/src/integrations/sign-in-body.ts +27 -3
- package/src/integrations/sign-in-middleware.ts +42 -9
- package/src/server-fns/auth/passkey-admission.test.node.ts +173 -0
- package/src/server-fns/auth/passkey-admission.ts +125 -0
- package/src/server-fns/auth/passkeys.test.node.ts +247 -0
- package/src/server-fns/auth/passkeys.ts +252 -0
- package/src/server-fns/auth/reconcile-locale.ts +40 -0
- package/src/server-fns/auth/sign-in.ts +1 -32
- 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.
|
|
6
|
+
"version": "6.9.0",
|
|
7
7
|
"engines": {
|
|
8
|
-
"node": ">=
|
|
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.
|
|
123
|
-
"@byline/ai": "6.
|
|
124
|
-
"@byline/analytics": "6.
|
|
125
|
-
"@byline/analytics-agent": "6.
|
|
126
|
-
"@byline/auth": "6.
|
|
127
|
-
"@byline/client": "6.
|
|
128
|
-
"@byline/core": "6.
|
|
129
|
-
"@byline/i18n": "6.
|
|
130
|
-
"@byline/ui": "6.
|
|
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:
|
|
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="
|
|
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:
|
|
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="
|
|
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
|
-
{
|
|
133
|
-
{ singleton: true,
|
|
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: {
|