@byline/admin 6.7.1 → 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.
- package/dist/modules/admin-account/components/change-password.js +1 -1
- package/dist/modules/admin-account/components/container.js +18 -3
- package/dist/modules/admin-account/components/container.module.js +2 -0
- package/dist/modules/admin-account/components/container_module.css +7 -0
- package/dist/modules/admin-account/components/preferences.js +1 -1
- package/dist/modules/admin-account/components/update.js +1 -1
- package/dist/modules/admin-roles/components/create.js +1 -1
- package/dist/modules/admin-roles/components/permissions.js +1 -1
- package/dist/modules/admin-roles/components/update.js +1 -1
- package/dist/modules/admin-users/abilities.d.ts +1 -0
- package/dist/modules/admin-users/abilities.js +8 -1
- package/dist/modules/admin-users/components/create.js +1 -1
- package/dist/modules/admin-users/components/roles.js +1 -1
- package/dist/modules/admin-users/components/set-password.js +1 -1
- package/dist/modules/admin-users/components/update.js +1 -1
- package/dist/modules/auth/auth-maintenance-repository.d.ts +31 -0
- package/dist/modules/auth/auth-maintenance-repository.js +1 -0
- package/dist/modules/auth/auth-maintenance.d.ts +11 -0
- package/dist/modules/auth/auth-maintenance.js +39 -0
- package/dist/modules/auth/auth-maintenance.test.node.d.ts +1 -0
- package/dist/modules/auth/components/passkey-browser.d.ts +14 -0
- package/dist/modules/auth/components/passkey-browser.js +118 -0
- package/dist/modules/auth/components/passkey-browser.test.node.d.ts +1 -0
- package/dist/modules/auth/components/passkey-manager.d.ts +26 -0
- package/dist/modules/auth/components/passkey-manager.js +406 -0
- package/dist/modules/auth/components/passkey-manager.module.js +22 -0
- package/dist/modules/auth/components/passkey-manager_module.css +104 -0
- package/dist/modules/auth/components/sign-in-form.js +62 -3
- package/dist/modules/auth/components/sign-in-form.module.js +5 -1
- package/dist/modules/auth/components/sign-in-form_module.css +40 -0
- package/dist/modules/auth/index.d.ts +5 -0
- package/dist/modules/auth/index.js +3 -0
- package/dist/modules/auth/jwt-session-provider.d.ts +12 -4
- package/dist/modules/auth/jwt-session-provider.js +135 -94
- package/dist/modules/auth/native-passkey-provider.d.ts +72 -0
- package/dist/modules/auth/native-passkey-provider.js +618 -0
- package/dist/modules/auth/passkey-rate-limiter.d.ts +14 -0
- package/dist/modules/auth/passkey-rate-limiter.js +60 -0
- package/dist/modules/auth/passkey-rate-limiter.test.node.d.ts +8 -0
- package/dist/modules/auth/passkey-storage-validation.d.ts +14 -0
- package/dist/modules/auth/passkey-storage-validation.js +59 -0
- package/dist/modules/auth/passkeys-repository.d.ts +96 -0
- package/dist/modules/auth/passkeys-repository.js +1 -0
- package/dist/modules/auth/refresh-tokens-repository.d.ts +1 -1
- package/dist/modules/auth/sign-in-rate-limiter.d.ts +2 -0
- package/dist/modules/auth/sign-in-rate-limiter.js +1 -1
- package/dist/services/admin-services-context.d.ts +1 -1
- package/dist/services/admin-services-types.d.ts +60 -0
- package/dist/store.d.ts +4 -0
- package/package.json +31 -24
- package/src/modules/admin-account/components/change-password.tsx +1 -1
- package/src/modules/admin-account/components/container.module.css +9 -0
- package/src/modules/admin-account/components/container.tsx +12 -3
- package/src/modules/admin-account/components/preferences.tsx +1 -1
- package/src/modules/admin-account/components/update.tsx +1 -1
- package/src/modules/admin-roles/components/create.tsx +1 -1
- package/src/modules/admin-roles/components/permissions.tsx +1 -1
- package/src/modules/admin-roles/components/update.tsx +1 -1
- package/src/modules/admin-users/abilities.ts +7 -0
- package/src/modules/admin-users/components/create.tsx +1 -1
- package/src/modules/admin-users/components/roles.tsx +1 -1
- package/src/modules/admin-users/components/set-password.tsx +1 -1
- package/src/modules/admin-users/components/update.tsx +1 -1
- package/src/modules/auth/auth-maintenance-repository.ts +26 -0
- package/src/modules/auth/auth-maintenance.test.node.ts +110 -0
- package/src/modules/auth/auth-maintenance.ts +62 -0
- package/src/modules/auth/components/passkey-browser.test.node.ts +188 -0
- package/src/modules/auth/components/passkey-browser.ts +139 -0
- package/src/modules/auth/components/passkey-manager.module.css +100 -0
- package/src/modules/auth/components/passkey-manager.test.tsx +189 -0
- package/src/modules/auth/components/passkey-manager.tsx +386 -0
- package/src/modules/auth/components/sign-in-form.module.css +37 -0
- package/src/modules/auth/components/sign-in-form.test.tsx +92 -0
- package/src/modules/auth/components/sign-in-form.tsx +65 -4
- package/src/modules/auth/index.ts +15 -0
- package/src/modules/auth/jwt-session-provider.ts +116 -44
- package/src/modules/auth/native-passkey-provider.ts +710 -0
- package/src/modules/auth/passkey-rate-limiter.test.node.ts +83 -0
- package/src/modules/auth/passkey-rate-limiter.ts +76 -0
- package/src/modules/auth/passkey-storage-validation.ts +108 -0
- package/src/modules/auth/passkeys-repository.ts +97 -0
- package/src/modules/auth/refresh-tokens-repository.ts +1 -1
- package/src/modules/auth/sign-in-rate-limiter.ts +1 -1
- package/src/services/admin-services-context.tsx +2 -0
- package/src/services/admin-services-types.ts +60 -0
- package/src/store.ts +4 -0
- package/dist/exports-parity.test.node.js +0 -37
- package/dist/fields/array/array-field.jsx +0 -209
- package/dist/fields/blocks/blocks-field.jsx +0 -248
- package/dist/fields/checkbox/checkbox-field.jsx +0 -24
- package/dist/fields/code/code-editor.jsx +0 -211
- package/dist/fields/code/code-field.jsx +0 -109
- package/dist/fields/column-formatter.jsx +0 -25
- package/dist/fields/date-time-formatter.jsx +0 -18
- package/dist/fields/datetime/datetime-field.jsx +0 -22
- package/dist/fields/draggable-context-menu.jsx +0 -50
- package/dist/fields/field-admin.test.node.js +0 -40
- package/dist/fields/field-renderer.jsx +0 -136
- package/dist/fields/field-services-context.jsx +0 -17
- package/dist/fields/file/file-field.jsx +0 -176
- package/dist/fields/file/file-upload-field.jsx +0 -89
- package/dist/fields/group/group-field.jsx +0 -46
- package/dist/fields/image/image-field.jsx +0 -183
- package/dist/fields/image/image-upload-field.jsx +0 -157
- package/dist/fields/local-date-time.jsx +0 -69
- package/dist/fields/locale-badge.jsx +0 -19
- package/dist/fields/numerical/numerical-field.jsx +0 -93
- package/dist/fields/relation/relation-column-formatter.jsx +0 -74
- package/dist/fields/relation/relation-display.jsx +0 -118
- package/dist/fields/relation/relation-field.jsx +0 -108
- package/dist/fields/relation/relation-many-field.jsx +0 -142
- package/dist/fields/relation/relation-picker.jsx +0 -240
- package/dist/fields/relation/relation-summary.jsx +0 -45
- package/dist/fields/select/select-field.jsx +0 -27
- package/dist/fields/sortable-item.jsx +0 -54
- package/dist/fields/text/text-field.jsx +0 -79
- package/dist/fields/text-area/text-area-field.jsx +0 -79
- package/dist/forms/available-locales-reconcile.test.node.js +0 -43
- package/dist/forms/available-locales-widget.jsx +0 -63
- package/dist/forms/document-actions.jsx +0 -486
- package/dist/forms/form-context.jsx +0 -598
- package/dist/forms/form-modals.jsx +0 -128
- package/dist/forms/form-renderer.jsx +0 -532
- package/dist/forms/form-status-display.jsx +0 -69
- package/dist/forms/navigation-guard.jsx +0 -68
- package/dist/forms/nested-path.test.node.js +0 -115
- package/dist/forms/path-widget.jsx +0 -125
- package/dist/forms/pending-uploads.test.node.js +0 -19
- package/dist/forms/repeating-items.test.node.js +0 -29
- package/dist/forms/scheduled-publication-control.jsx +0 -389
- package/dist/forms/scheduled-publication-state.test.node.js +0 -121
- package/dist/forms/scheduled-publication-time.test.node.js +0 -70
- package/dist/forms/status-transitions.test.node.js +0 -76
- package/dist/forms/tree-placement-widget.jsx +0 -160
- package/dist/forms/upload-executor.test.node.js +0 -368
- package/dist/forms/use-form-layout.test.node.js +0 -69
- package/dist/modules/admin-account/components/change-password.jsx +0 -169
- package/dist/modules/admin-account/components/container.jsx +0 -164
- package/dist/modules/admin-account/components/preferences.jsx +0 -148
- package/dist/modules/admin-account/components/theme-switch.jsx +0 -52
- package/dist/modules/admin-account/components/update.jsx +0 -186
- package/dist/modules/admin-permissions/components/inspector.jsx +0 -200
- package/dist/modules/admin-preferences/schemas.test.node.js +0 -48
- package/dist/modules/admin-roles/components/create.jsx +0 -155
- package/dist/modules/admin-roles/components/permissions.jsx +0 -239
- package/dist/modules/admin-roles/components/update.jsx +0 -148
- package/dist/modules/admin-users/components/create.jsx +0 -206
- package/dist/modules/admin-users/components/roles.jsx +0 -126
- package/dist/modules/admin-users/components/set-password.jsx +0 -133
- package/dist/modules/admin-users/components/update.jsx +0 -215
- package/dist/modules/analytics/components/dashboard.jsx +0 -208
- package/dist/modules/analytics/components/dashboard.test.node.js +0 -162
- package/dist/modules/analytics/components/timeseries.jsx +0 -193
- package/dist/modules/auth/components/sign-in-form.jsx +0 -89
- package/dist/modules/auth/safe-redirect.test.node.js +0 -40
- package/dist/modules/auth/sign-in-form-props.test.node.js +0 -7
- package/dist/presentation/group.jsx +0 -26
- package/dist/presentation/row.jsx +0 -23
- package/dist/presentation/tabs.jsx +0 -39
- package/dist/services/admin-services-context.jsx +0 -17
- package/dist/widgets/diff-viewer/diff-modal.jsx +0 -110
- package/dist/widgets/source-locale-badge/source-locale-badge.jsx +0 -28
- package/dist/widgets/status-badge/status-badge.jsx +0 -46
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createHmac } from "node:crypto";
|
|
2
|
+
import { signInNetwork } from "./sign-in-rate-limiter.js";
|
|
3
|
+
function createPasskeyRequestLimiter(store, secret, now = Date.now) {
|
|
4
|
+
const bytes = 'string' == typeof secret ? new TextEncoder().encode(secret) : secret;
|
|
5
|
+
if (bytes.byteLength < 32) throw new Error('Passkey HMAC secret must contain at least 32 bytes');
|
|
6
|
+
const key = createHmac('sha256', bytes).update('byline:passkey-requests:v1').digest();
|
|
7
|
+
let active = 0;
|
|
8
|
+
const cleanupTask = {
|
|
9
|
+
name: 'auth.sign-in-counters.cleanup',
|
|
10
|
+
intervalMs: 60000,
|
|
11
|
+
leaseMs: 60000,
|
|
12
|
+
async run (context) {
|
|
13
|
+
const started = performance.now();
|
|
14
|
+
for(let n = 0; n < 32 && performance.now() - started < 30000; n++){
|
|
15
|
+
context.signal.throwIfAborted();
|
|
16
|
+
await context.heartbeat();
|
|
17
|
+
if (await store.purgeExpired(new Date(now() - 300000)) < 100) return {
|
|
18
|
+
workRemaining: false
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
workRemaining: true
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
requiredCleanupTask: cleanupTask.name,
|
|
28
|
+
cleanupTask,
|
|
29
|
+
async acquire () {
|
|
30
|
+
if (active >= 4) return null;
|
|
31
|
+
active++;
|
|
32
|
+
const started = performance.now();
|
|
33
|
+
let released = false;
|
|
34
|
+
return ()=>{
|
|
35
|
+
if (released) return;
|
|
36
|
+
released = true;
|
|
37
|
+
setTimeout(()=>{
|
|
38
|
+
active--;
|
|
39
|
+
}, Math.max(0, 100 - (performance.now() - started))).unref();
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
async consume ({ ip }) {
|
|
43
|
+
const network = signInNetwork(ip);
|
|
44
|
+
const time = now();
|
|
45
|
+
const end = (Math.floor(time / 60000) + 1) * 60000;
|
|
46
|
+
const digest = createHmac('sha256', key).update(JSON.stringify([
|
|
47
|
+
'network',
|
|
48
|
+
network,
|
|
49
|
+
end
|
|
50
|
+
])).digest('hex');
|
|
51
|
+
await store.purgeExpired(new Date(time - 300000));
|
|
52
|
+
const allowed = await store.consume(digest, 60, new Date(end));
|
|
53
|
+
return {
|
|
54
|
+
allowed,
|
|
55
|
+
retryAfterSeconds: allowed ? 0 : Math.max(1, Math.ceil((end - time) / 1000))
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export { createPasskeyRequestLimiter };
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
import type { PasskeyAttemptRow, PasskeyClaimInput } from './passkeys-repository.js';
|
|
9
|
+
/** Canonical encoding makes string identity exactly equivalent to WebAuthn byte identity. */
|
|
10
|
+
export declare function assertPasskeyBytes(value: string, maxBytes: number, exactBytes?: number): void;
|
|
11
|
+
export declare function assertPasskeyTokenHash(value: string): void;
|
|
12
|
+
export declare function assertPasskeyAttempt(input: Omit<PasskeyAttemptRow, 'consumed_at'>): void;
|
|
13
|
+
/** Runtime callers must obey the same purpose/action boundary as typed callers. */
|
|
14
|
+
export declare function assertPasskeyClaim(input: PasskeyClaimInput): void;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
function assertPasskeyBytes(value, maxBytes, exactBytes) {
|
|
2
|
+
if ('string' != typeof value || !/^[A-Za-z0-9_-]+$/.test(value)) throw new Error('Invalid passkey byte encoding');
|
|
3
|
+
const bytes = Buffer.from(value, 'base64url');
|
|
4
|
+
if (bytes.length > maxBytes || void 0 !== exactBytes && bytes.length !== exactBytes || bytes.toString('base64url') !== value) throw new Error('Invalid passkey byte encoding');
|
|
5
|
+
}
|
|
6
|
+
function assertPasskeyTokenHash(value) {
|
|
7
|
+
if (!/^[a-f0-9]{64}$/.test(value)) throw new Error('Invalid passkey token hash');
|
|
8
|
+
}
|
|
9
|
+
function assertPasskeyAttempt(input) {
|
|
10
|
+
assertPasskeyTokenHash(input.token_hash);
|
|
11
|
+
const lifetime = input.expires_at.getTime() - input.created_at.getTime();
|
|
12
|
+
if (!Number.isFinite(lifetime) || lifetime <= 0 || lifetime > 300000) throw new Error('Invalid passkey attempt lifetime');
|
|
13
|
+
if (![
|
|
14
|
+
'sign-in',
|
|
15
|
+
'fresh-authentication',
|
|
16
|
+
'enrollment',
|
|
17
|
+
'grant'
|
|
18
|
+
].includes(input.purpose)) throw new Error('Invalid passkey attempt purpose');
|
|
19
|
+
if ('grant' === input.purpose) {
|
|
20
|
+
if (null !== input.challenge) throw new Error('Grants cannot contain challenges');
|
|
21
|
+
} else assertPasskeyBytes(input.challenge ?? '', 32, 32);
|
|
22
|
+
if ('sign-in' === input.purpose) {
|
|
23
|
+
if ([
|
|
24
|
+
input.actor_user_id,
|
|
25
|
+
input.actor_login_id,
|
|
26
|
+
input.actor_generation,
|
|
27
|
+
input.action,
|
|
28
|
+
input.target_user_id,
|
|
29
|
+
input.credential_record_id,
|
|
30
|
+
input.proof_credential_id
|
|
31
|
+
].some((value)=>null !== value)) throw new Error('Sign-in attempts cannot carry management authority');
|
|
32
|
+
} else {
|
|
33
|
+
if (!input.actor_user_id || !input.actor_login_id || !Number.isSafeInteger(input.actor_generation) || (input.actor_generation ?? -1) < 0) throw new Error('Management attempts require an acting login and generation');
|
|
34
|
+
if ('enroll' === input.action) {
|
|
35
|
+
if (input.target_user_id !== input.actor_user_id || null !== input.credential_record_id) throw new Error('Enrollment must target the actor');
|
|
36
|
+
} else if ('remove' === input.action) {
|
|
37
|
+
if (!input.target_user_id || !input.credential_record_id || 'enrollment' === input.purpose) throw new Error('Removal must bind its target account and credential');
|
|
38
|
+
} else throw new Error('Management attempts require an action');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function assertPasskeyClaim(input) {
|
|
42
|
+
assertPasskeyTokenHash(input.tokenHash);
|
|
43
|
+
if ('sign-in' === input.purpose) {
|
|
44
|
+
if (void 0 !== input.action || [
|
|
45
|
+
input.binding.actor_user_id,
|
|
46
|
+
input.binding.actor_login_id,
|
|
47
|
+
input.binding.actor_generation
|
|
48
|
+
].some((value)=>null !== value)) throw new Error('Sign-in claims require anonymous binding and no action');
|
|
49
|
+
} else {
|
|
50
|
+
if (![
|
|
51
|
+
'fresh-authentication',
|
|
52
|
+
'enrollment',
|
|
53
|
+
'grant'
|
|
54
|
+
].includes(input.purpose)) throw new Error('Invalid passkey claim purpose');
|
|
55
|
+
if ('enroll' !== input.action && 'remove' !== input.action) throw new Error('Authenticated claims require an action');
|
|
56
|
+
if (!input.binding.actor_user_id || !input.binding.actor_login_id || !Number.isSafeInteger(input.binding.actor_generation) || input.binding.actor_generation < 0) throw new Error('Authenticated claims require an acting login binding');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export { assertPasskeyAttempt, assertPasskeyBytes, assertPasskeyClaim, assertPasskeyTokenHash };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code Form 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 https://mozilla.org/MPL/2.0/.
|
|
5
|
+
* Copyright (c) Infonomic Company Limited
|
|
6
|
+
*/
|
|
7
|
+
/** Internal persistence records. Never return these through account/editor DTOs. */
|
|
8
|
+
export interface PasskeyCredentialRow {
|
|
9
|
+
id: string;
|
|
10
|
+
/** Null after account deletion; the credential identity remains reserved. */
|
|
11
|
+
admin_user_id: string | null;
|
|
12
|
+
/** Canonical unpadded base64url; byte-exact uniqueness includes revoked records. */
|
|
13
|
+
credential_id: string;
|
|
14
|
+
public_key: string;
|
|
15
|
+
counter: number;
|
|
16
|
+
transports: string[];
|
|
17
|
+
backup_eligible: boolean;
|
|
18
|
+
backed_up: boolean;
|
|
19
|
+
name: string;
|
|
20
|
+
created_at: Date;
|
|
21
|
+
last_used_at: Date | null;
|
|
22
|
+
revoked_at: Date | null;
|
|
23
|
+
}
|
|
24
|
+
export interface PasskeyAttemptRow {
|
|
25
|
+
/** SHA-256 hex of the independent bearer identifier; never the identifier itself. */
|
|
26
|
+
token_hash: string;
|
|
27
|
+
purpose: 'sign-in' | 'fresh-authentication' | 'enrollment' | 'grant';
|
|
28
|
+
challenge: string | null;
|
|
29
|
+
actor_user_id: string | null;
|
|
30
|
+
actor_login_id: string | null;
|
|
31
|
+
actor_generation: number | null;
|
|
32
|
+
action: 'enroll' | 'remove' | null;
|
|
33
|
+
target_user_id: string | null;
|
|
34
|
+
credential_record_id: string | null;
|
|
35
|
+
/** Credential used for fresh proof, if any; revalidated at grant use. */
|
|
36
|
+
proof_credential_id: string | null;
|
|
37
|
+
created_at: Date;
|
|
38
|
+
expires_at: Date;
|
|
39
|
+
consumed_at: Date | null;
|
|
40
|
+
}
|
|
41
|
+
export type PasskeyClaimInput = {
|
|
42
|
+
tokenHash: string;
|
|
43
|
+
now: Date;
|
|
44
|
+
} & ({
|
|
45
|
+
purpose: 'sign-in';
|
|
46
|
+
binding: {
|
|
47
|
+
actor_user_id: null;
|
|
48
|
+
actor_login_id: null;
|
|
49
|
+
actor_generation: null;
|
|
50
|
+
};
|
|
51
|
+
action?: never;
|
|
52
|
+
} | {
|
|
53
|
+
purpose: 'fresh-authentication' | 'enrollment' | 'grant';
|
|
54
|
+
binding: {
|
|
55
|
+
actor_user_id: string;
|
|
56
|
+
actor_login_id: string;
|
|
57
|
+
actor_generation: number;
|
|
58
|
+
};
|
|
59
|
+
action: 'enroll' | 'remove';
|
|
60
|
+
});
|
|
61
|
+
export interface PasskeysRepository {
|
|
62
|
+
/** Target lock required; generation and native-login revocation commit together. */
|
|
63
|
+
invalidateAccountLogins(adminUserId: string, at: Date): Promise<void>;
|
|
64
|
+
/** Success requires both account locks; denial/failure records may be written separately. */
|
|
65
|
+
appendRemovalAudit(input: {
|
|
66
|
+
actorId: string;
|
|
67
|
+
targetId: string;
|
|
68
|
+
credentialId: string;
|
|
69
|
+
outcome: 'success' | 'denied' | 'failed';
|
|
70
|
+
}): Promise<void>;
|
|
71
|
+
getUserHandle(adminUserId: string): Promise<string | null>;
|
|
72
|
+
/** Requires the owning account lock. Allocates once; never resets on credential removal. */
|
|
73
|
+
ensureUserHandle(adminUserId: string, candidate: string): Promise<string>;
|
|
74
|
+
/** Includes tombstones: authentication must reject revoked or ownerless records. */
|
|
75
|
+
findCredential(id: string): Promise<PasskeyCredentialRow | null>;
|
|
76
|
+
/** Includes tombstones: authentication must reject revoked or ownerless records. */
|
|
77
|
+
findCredentialByCredentialId(credentialId: string): Promise<PasskeyCredentialRow | null>;
|
|
78
|
+
listCredentials(adminUserId: string): Promise<PasskeyCredentialRow[]>;
|
|
79
|
+
/** All credential writes require the owning account lock and share its transaction. */
|
|
80
|
+
createCredential(input: Omit<PasskeyCredentialRow, 'admin_user_id' | 'revoked_at'> & {
|
|
81
|
+
admin_user_id: string;
|
|
82
|
+
}): Promise<void>;
|
|
83
|
+
updateCredential(id: string, patch: Partial<Pick<PasskeyCredentialRow, 'counter' | 'backed_up' | 'name' | 'last_used_at'>>): Promise<void>;
|
|
84
|
+
revokeCredential(id: string, at: Date): Promise<void>;
|
|
85
|
+
/** Authenticated attempts require the actor lock; sign-in has no account yet. */
|
|
86
|
+
createAttempt(input: Omit<PasskeyAttemptRow, 'consumed_at'>): Promise<void>;
|
|
87
|
+
findAttempt(tokenHash: string): Promise<PasskeyAttemptRow | null>;
|
|
88
|
+
countPendingForLogin(loginId: string, now: Date): Promise<number>;
|
|
89
|
+
/**
|
|
90
|
+
* Root store only: commits consumption in its own short transaction BEFORE returning.
|
|
91
|
+
* Calling through an account-transaction store must throw, never create a savepoint.
|
|
92
|
+
* Exact purpose/binding, unconsumed state and expiry gate the sole winning claim.
|
|
93
|
+
* No automatic retries, even if commit acknowledgement is uncertain.
|
|
94
|
+
*/
|
|
95
|
+
claimAttempt(input: PasskeyClaimInput): Promise<PasskeyAttemptRow | null>;
|
|
96
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -62,7 +62,7 @@ export interface RefreshTokensRepository {
|
|
|
62
62
|
revokeChain(startId: string, at?: Date): Promise<number>;
|
|
63
63
|
/** Revoke every non-revoked token for a user. Used on password change / sign-out everywhere. */
|
|
64
64
|
revokeAllForUser(adminUserId: string, at?: Date): Promise<number>;
|
|
65
|
-
/**
|
|
65
|
+
/** Legacy housekeeping: at most 100 null-sid rows expired for 30 days. Native login members are retained. */
|
|
66
66
|
purgeExpired(now?: Date): Promise<number>;
|
|
67
67
|
/** All non-revoked tokens for a user. Primarily for tests. */
|
|
68
68
|
listActiveForUser(adminUserId: string): Promise<RefreshTokenRow[]>;
|
|
@@ -38,6 +38,8 @@ export interface SignInLimiterOptions {
|
|
|
38
38
|
/** Also bounds counter creation when the provider or a denial completes very quickly. */
|
|
39
39
|
minimumSlotMs?: number;
|
|
40
40
|
}
|
|
41
|
+
/** IPv4 uses one address; IPv6 uses its /64. Mapped IPv4 shares its native IPv4 bucket. */
|
|
42
|
+
export declare function signInNetwork(ip: string): string;
|
|
41
43
|
/**
|
|
42
44
|
* Instantiate once per process. Acquire before consume and release in finally after verification.
|
|
43
45
|
* Network admission precedes account-plus-network admission. There is no account-wide lockout.
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { type ReactNode } from 'react';
|
|
9
9
|
import type { BylineAdminServices } from './admin-services-types.js';
|
|
10
|
-
export type { AdminServiceCall, BylineAdminServices, ChangeAccountPasswordInput, CreateAdminRoleInput, CreateAdminUserInput, SetAdminUserPasswordInput, SetRoleAbilitiesInput, SetUserRolesInput, SignInInput, SignInResult, UpdateAccountInput, UpdateAdminRoleInput, UpdateAdminUserInput, WhoHasAbilityInput, } from './admin-services-types.js';
|
|
10
|
+
export type { AdminServiceCall, BylineAdminServices, ChangeAccountPasswordInput, CreateAdminRoleInput, CreateAdminUserInput, PasskeyAdminServices, PasskeyPageBinding, SetAdminUserPasswordInput, SetRoleAbilitiesInput, SetUserRolesInput, SignInInput, SignInResult, UpdateAccountInput, UpdateAdminRoleInput, UpdateAdminUserInput, WhoHasAbilityInput, } from './admin-services-types.js';
|
|
11
11
|
interface BylineAdminServicesProviderProps {
|
|
12
12
|
services: BylineAdminServices;
|
|
13
13
|
children: ReactNode;
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* server fns directly today and move into `@byline/host-tanstack-start`
|
|
24
24
|
* in Phase 3 along with the route factories.
|
|
25
25
|
*/
|
|
26
|
+
import type { PasskeyAuthenticationOptions, PasskeyCeremony, PasskeyCredentialSummary, PasskeyManagementAction, PasskeyManagementGrant, PasskeyRegistrationOptions } from '@byline/auth';
|
|
26
27
|
import type { AccountResponse, ChangeAccountPasswordRequest, UpdateAccountRequest } from '../modules/admin-account/index.js';
|
|
27
28
|
import type { SetRoleAbilitiesResponse, WhoHasAbilityResponse } from '../modules/admin-permissions/index.js';
|
|
28
29
|
import type { AdminRoleResponse, UserRolesResponse } from '../modules/admin-roles/index.js';
|
|
@@ -46,6 +47,63 @@ export interface SignInInput {
|
|
|
46
47
|
* supply whatever envelope it produces without forcing a public type.
|
|
47
48
|
*/
|
|
48
49
|
export type SignInResult = unknown;
|
|
50
|
+
/** Captured before any prompt; the host checks it again before submission. */
|
|
51
|
+
export interface PasskeyPageBinding {
|
|
52
|
+
expectedSessionId: string | null;
|
|
53
|
+
assertCurrent(): void;
|
|
54
|
+
}
|
|
55
|
+
interface PasskeyBoundInput {
|
|
56
|
+
expectedSessionId: string;
|
|
57
|
+
}
|
|
58
|
+
export interface PasskeyAdminServices {
|
|
59
|
+
capability(): Promise<{
|
|
60
|
+
enabled: boolean;
|
|
61
|
+
}>;
|
|
62
|
+
captureLogin(signIn?: boolean): PasskeyPageBinding;
|
|
63
|
+
managementContext(binding: PasskeyPageBinding): Promise<{
|
|
64
|
+
id: string;
|
|
65
|
+
isSuperAdmin: boolean;
|
|
66
|
+
canManagePasskeys: boolean;
|
|
67
|
+
}>;
|
|
68
|
+
signInOptions: AdminServiceCall<Record<string, never>, PasskeyCeremony<PasskeyAuthenticationOptions>>;
|
|
69
|
+
signIn(args: {
|
|
70
|
+
data: {
|
|
71
|
+
ceremonyId: string;
|
|
72
|
+
response: unknown;
|
|
73
|
+
};
|
|
74
|
+
}, binding: PasskeyPageBinding): Promise<unknown>;
|
|
75
|
+
passwordProof: AdminServiceCall<PasskeyBoundInput & {
|
|
76
|
+
password: string;
|
|
77
|
+
action: PasskeyManagementAction;
|
|
78
|
+
}, PasskeyManagementGrant>;
|
|
79
|
+
proofOptions: AdminServiceCall<PasskeyBoundInput & {
|
|
80
|
+
action: PasskeyManagementAction;
|
|
81
|
+
}, PasskeyCeremony<PasskeyAuthenticationOptions>>;
|
|
82
|
+
proof: AdminServiceCall<PasskeyBoundInput & {
|
|
83
|
+
ceremonyId: string;
|
|
84
|
+
response: unknown;
|
|
85
|
+
}, PasskeyManagementGrant>;
|
|
86
|
+
enrollmentOptions: AdminServiceCall<PasskeyBoundInput & {
|
|
87
|
+
grantId: string;
|
|
88
|
+
}, PasskeyCeremony<PasskeyRegistrationOptions>>;
|
|
89
|
+
enrollment: AdminServiceCall<PasskeyBoundInput & {
|
|
90
|
+
ceremonyId: string;
|
|
91
|
+
response: unknown;
|
|
92
|
+
name: string;
|
|
93
|
+
}, PasskeyCredentialSummary>;
|
|
94
|
+
list: AdminServiceCall<PasskeyBoundInput & {
|
|
95
|
+
targetUserId?: string;
|
|
96
|
+
}, PasskeyCredentialSummary[]>;
|
|
97
|
+
rename: AdminServiceCall<PasskeyBoundInput & {
|
|
98
|
+
credentialRecordId: string;
|
|
99
|
+
name: string;
|
|
100
|
+
}, PasskeyCredentialSummary>;
|
|
101
|
+
remove: AdminServiceCall<PasskeyBoundInput & {
|
|
102
|
+
grantId: string;
|
|
103
|
+
}, {
|
|
104
|
+
signedOut: boolean;
|
|
105
|
+
}>;
|
|
106
|
+
}
|
|
49
107
|
/** Same shape as `UpdateAccountRequest` from `@byline/admin/admin-account`. */
|
|
50
108
|
export type UpdateAccountInput = UpdateAccountRequest;
|
|
51
109
|
/** Same shape as `ChangeAccountPasswordRequest` from `@byline/admin/admin-account`. */
|
|
@@ -119,6 +177,7 @@ export interface WhoHasAbilityInput {
|
|
|
119
177
|
}
|
|
120
178
|
export interface BylineAdminServices {
|
|
121
179
|
adminSignIn: AdminServiceCall<SignInInput, SignInResult>;
|
|
180
|
+
passkeys?: PasskeyAdminServices;
|
|
122
181
|
updateAccount: AdminServiceCall<UpdateAccountInput, AccountResponse>;
|
|
123
182
|
changeAccountPassword: AdminServiceCall<ChangeAccountPasswordInput, AccountResponse>;
|
|
124
183
|
setAdminLocale: AdminServiceCall<SetAdminLocaleInput, SetAdminLocaleResult>;
|
|
@@ -143,3 +202,4 @@ export interface BylineAdminServices {
|
|
|
143
202
|
*/
|
|
144
203
|
getCollectionDocumentVersion: (collection: string, documentId: string, versionId: string, locale: string | undefined) => Promise<Record<string, unknown>>;
|
|
145
204
|
}
|
|
205
|
+
export {};
|
package/dist/store.d.ts
CHANGED
|
@@ -9,7 +9,9 @@ import type { AdminPermissionsRepository } from './modules/admin-permissions/rep
|
|
|
9
9
|
import type { AdminPreferencesRepository } from './modules/admin-preferences/repository.js';
|
|
10
10
|
import type { AdminRolesRepository } from './modules/admin-roles/repository.js';
|
|
11
11
|
import type { AdminUsersRepository, AdminUserWithPasswordRow } from './modules/admin-users/repository.js';
|
|
12
|
+
import type { AuthMaintenanceRepository } from './modules/auth/auth-maintenance-repository.js';
|
|
12
13
|
import type { LoginSessionsRepository } from './modules/auth/login-sessions-repository.js';
|
|
14
|
+
import type { PasskeysRepository } from './modules/auth/passkeys-repository.js';
|
|
13
15
|
import type { RefreshTokensRepository } from './modules/auth/refresh-tokens-repository.js';
|
|
14
16
|
import type { SignInRateLimitStore } from './modules/auth/sign-in-rate-limiter.js';
|
|
15
17
|
/**
|
|
@@ -35,6 +37,8 @@ export interface AdminStore {
|
|
|
35
37
|
withSessionLock<T>(adminUserId: string, work: (store: AdminStore, user: AdminUserWithPasswordRow | null) => Promise<T>): Promise<T>;
|
|
36
38
|
/** Lock distinct account IDs in sorted order in one transaction for account-switch sign-in. */
|
|
37
39
|
withSessionLocks<T>(adminUserIds: string[], work: (store: AdminStore) => Promise<T>): Promise<T>;
|
|
40
|
+
authMaintenance: AuthMaintenanceRepository;
|
|
41
|
+
passkeys: PasskeysRepository;
|
|
38
42
|
loginSessions: LoginSessionsRepository;
|
|
39
43
|
signInRateLimits: SignInRateLimitStore;
|
|
40
44
|
adminUsers: AdminUsersRepository;
|
package/package.json
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"name": "@byline/admin",
|
|
3
3
|
"private": false,
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
|
-
"version": "6.
|
|
5
|
+
"version": "6.8.0",
|
|
6
6
|
"engines": {
|
|
7
|
-
"node": ">=
|
|
7
|
+
"node": ">=22.22.2"
|
|
8
8
|
},
|
|
9
9
|
"description": "Byline CMS admin subsystem — admin users, roles, permissions, account self-service, and the built-in JWT session provider",
|
|
10
10
|
"keywords": [
|
|
@@ -157,13 +157,24 @@
|
|
|
157
157
|
"import": "./dist/modules/auth/components/sign-in-form.js",
|
|
158
158
|
"require": "./dist/modules/auth/components/sign-in-form.js"
|
|
159
159
|
},
|
|
160
|
-
"./package.json": "./package.json"
|
|
160
|
+
"./package.json": "./package.json",
|
|
161
|
+
"./auth/components/passkey-manager": {
|
|
162
|
+
"types": "./dist/modules/auth/components/passkey-manager.d.ts",
|
|
163
|
+
"import": "./dist/modules/auth/components/passkey-manager.js",
|
|
164
|
+
"require": "./dist/modules/auth/components/passkey-manager.js"
|
|
165
|
+
}
|
|
161
166
|
},
|
|
162
167
|
"files": [
|
|
163
168
|
"dist/",
|
|
164
169
|
"src/"
|
|
165
170
|
],
|
|
166
171
|
"dependencies": {
|
|
172
|
+
"@byline/analytics": "6.8.0",
|
|
173
|
+
"@byline/analytics-agent": "6.8.0",
|
|
174
|
+
"@byline/auth": "6.8.0",
|
|
175
|
+
"@byline/core": "6.8.0",
|
|
176
|
+
"@byline/i18n": "6.8.0",
|
|
177
|
+
"@byline/ui": "6.8.0",
|
|
167
178
|
"@codemirror/commands": "^6.11.1",
|
|
168
179
|
"@codemirror/lang-css": "^6.3.1",
|
|
169
180
|
"@codemirror/lang-html": "^6.4.12",
|
|
@@ -173,41 +184,37 @@
|
|
|
173
184
|
"@codemirror/lang-python": "^6.2.1",
|
|
174
185
|
"@codemirror/lang-sql": "^6.10.0",
|
|
175
186
|
"@codemirror/lang-yaml": "^6.1.3",
|
|
176
|
-
"@codemirror/language": "^6.
|
|
177
|
-
"@codemirror/state": "^6.7.
|
|
178
|
-
"@codemirror/view": "^6.43.
|
|
179
|
-
"@lezer/highlight": "^1.2.
|
|
187
|
+
"@codemirror/language": "^6.13.1",
|
|
188
|
+
"@codemirror/state": "^6.7.6",
|
|
189
|
+
"@codemirror/view": "^6.43.14",
|
|
190
|
+
"@lezer/highlight": "^1.2.5",
|
|
191
|
+
"@simplewebauthn/browser": "^14.0.0",
|
|
192
|
+
"@simplewebauthn/server": "^14.0.3",
|
|
180
193
|
"@tanstack/react-form-start": "^1.33.5",
|
|
181
194
|
"clsx": "^2.1.1",
|
|
182
195
|
"jose": "^6.2.12",
|
|
183
196
|
"react-diff-viewer-continued": "^4.4.0",
|
|
184
|
-
"uuid": "^14.0.
|
|
185
|
-
"zod": "^4.6.
|
|
186
|
-
"@byline/analytics": "6.7.1",
|
|
187
|
-
"@byline/auth": "6.7.1",
|
|
188
|
-
"@byline/core": "6.7.1",
|
|
189
|
-
"@byline/analytics-agent": "6.7.1",
|
|
190
|
-
"@byline/i18n": "6.7.1",
|
|
191
|
-
"@byline/ui": "6.7.1"
|
|
197
|
+
"uuid": "^14.0.3",
|
|
198
|
+
"zod": "^4.6.7"
|
|
192
199
|
},
|
|
193
200
|
"peerDependencies": {
|
|
194
201
|
"react": "^19.0.0",
|
|
195
202
|
"react-dom": "^19.0.0"
|
|
196
203
|
},
|
|
197
204
|
"devDependencies": {
|
|
198
|
-
"@biomejs/biome": "2.5.
|
|
199
|
-
"@rsbuild/plugin-react": "^2.1.
|
|
200
|
-
"@rslib/core": "^1.0.
|
|
201
|
-
"@types/node": "^26.6.
|
|
205
|
+
"@biomejs/biome": "2.5.15",
|
|
206
|
+
"@rsbuild/plugin-react": "^2.1.1",
|
|
207
|
+
"@rslib/core": "^1.0.3",
|
|
208
|
+
"@types/node": "^26.6.5",
|
|
202
209
|
"@types/react": "19.3.0",
|
|
203
210
|
"@types/react-dom": "19.3.0",
|
|
204
|
-
"@vitejs/plugin-react": "^6.1.
|
|
205
|
-
"jsdom": "^30.1.
|
|
206
|
-
"react": "^19.3.
|
|
207
|
-
"react-dom": "^19.3.
|
|
211
|
+
"@vitejs/plugin-react": "^6.1.2",
|
|
212
|
+
"jsdom": "^30.1.3",
|
|
213
|
+
"react": "^19.3.1",
|
|
214
|
+
"react-dom": "^19.3.1",
|
|
208
215
|
"typescript": "^7.0.2",
|
|
209
216
|
"typescript-plugin-css-modules": "^5.2.0",
|
|
210
|
-
"vitest": "^5.0.
|
|
217
|
+
"vitest": "^5.0.4"
|
|
211
218
|
},
|
|
212
219
|
"publishConfig": {
|
|
213
220
|
"access": "public",
|
|
@@ -215,7 +215,7 @@ export function ChangeAccountPassword({ account, onClose, onSuccess }: ChangePas
|
|
|
215
215
|
<div className={cx('byline-account-change-password-actions', styles.actions)}>
|
|
216
216
|
<Button
|
|
217
217
|
type="button"
|
|
218
|
-
intent="
|
|
218
|
+
intent="noeffect"
|
|
219
219
|
size="sm"
|
|
220
220
|
onClick={onClose}
|
|
221
221
|
className={cx('byline-account-change-password-action', styles.action)}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* .byline-account-status-on
|
|
14
14
|
* .byline-account-status-off
|
|
15
15
|
* .byline-account-status-help — italic helper line on Account Status card
|
|
16
|
+
* .byline-account-preferences-theme — colour theme block inside Preferences
|
|
16
17
|
* .byline-account-drawer — drawer responsive width override
|
|
17
18
|
* .byline-account-drawer-body — drawer content container
|
|
18
19
|
* .byline-account-drawer-scroll — drawer scrollable inner viewport
|
|
@@ -92,6 +93,14 @@
|
|
|
92
93
|
font-size: var(--font-size-xs);
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
.preferences-theme,
|
|
97
|
+
:global(.byline-account-preferences-theme) {
|
|
98
|
+
display: flex;
|
|
99
|
+
flex-direction: column;
|
|
100
|
+
gap: var(--spacing-8);
|
|
101
|
+
margin-top: var(--spacing-20);
|
|
102
|
+
}
|
|
103
|
+
|
|
95
104
|
/*
|
|
96
105
|
* Italic helper for "Not set" placeholders. Replaces the Tailwind
|
|
97
106
|
* `italic` utility we used pre-lift; with the component now in a
|
|
@@ -32,6 +32,7 @@ import { useTranslation } from '@byline/i18n/react'
|
|
|
32
32
|
import { CloseIcon, Drawer, EditIcon, IconButton } from '@byline/ui/react'
|
|
33
33
|
import cx from 'clsx'
|
|
34
34
|
|
|
35
|
+
import { PasskeyManager } from '../../auth/components/passkey-manager.js'
|
|
35
36
|
import { ChangeAccountPassword } from './change-password.js'
|
|
36
37
|
import styles from './container.module.css'
|
|
37
38
|
import { Preferences } from './preferences.js'
|
|
@@ -180,6 +181,12 @@ export function AccountSelfContainer({ account }: AccountSelfContainerProps) {
|
|
|
180
181
|
<p className={cx('muted', 'byline-account-status-help', styles['status-help'])}>
|
|
181
182
|
{t('account.preferences.help')}
|
|
182
183
|
</p>
|
|
184
|
+
<div className={cx('byline-account-preferences-theme', styles['preferences-theme'])}>
|
|
185
|
+
<p className={cx('byline-account-line', styles.line)}>
|
|
186
|
+
<span className="muted">{t('account.appearance.colorTheme')}</span>
|
|
187
|
+
</p>
|
|
188
|
+
<ThemeSwitch />
|
|
189
|
+
</div>
|
|
183
190
|
</ContainerSection>
|
|
184
191
|
</div>
|
|
185
192
|
|
|
@@ -192,9 +199,11 @@ export function AccountSelfContainer({ account }: AccountSelfContainerProps) {
|
|
|
192
199
|
<p className={cx('byline-account-line', styles.line)}>{t('account.password.intro')}</p>
|
|
193
200
|
</ContainerSection>
|
|
194
201
|
|
|
195
|
-
<
|
|
196
|
-
|
|
197
|
-
|
|
202
|
+
<PasskeyManager
|
|
203
|
+
targetUserId={currentAccount.id}
|
|
204
|
+
targetIsSuperAdmin={currentAccount.is_super_admin}
|
|
205
|
+
section={ContainerSection}
|
|
206
|
+
/>
|
|
198
207
|
|
|
199
208
|
<ContainerSection title={t('account.sections.status')}>
|
|
200
209
|
<p className={cx('byline-account-line', styles.line)}>
|
|
@@ -172,7 +172,7 @@ export function Preferences({ account, onClose, onSuccess }: PreferencesProps) {
|
|
|
172
172
|
<div className={cx('byline-account-preferences-actions', styles.actions)}>
|
|
173
173
|
<Button
|
|
174
174
|
type="button"
|
|
175
|
-
intent="
|
|
175
|
+
intent="noeffect"
|
|
176
176
|
size="sm"
|
|
177
177
|
onClick={onClose}
|
|
178
178
|
className={cx('byline-account-preferences-action', styles.action)}
|
|
@@ -240,7 +240,7 @@ export function UpdateAccount({ account, onClose, onSuccess }: UpdateAccountProp
|
|
|
240
240
|
<div className={cx('byline-account-update-actions', styles.actions)}>
|
|
241
241
|
<Button
|
|
242
242
|
type="button"
|
|
243
|
-
intent="
|
|
243
|
+
intent="noeffect"
|
|
244
244
|
size="sm"
|
|
245
245
|
onClick={onClose}
|
|
246
246
|
className={cx('byline-account-update-action', styles.action)}
|
|
@@ -192,7 +192,7 @@ export function CreateAdminRole({ onClose, onSuccess }: CreateAdminRoleProps) {
|
|
|
192
192
|
<div className={cx('byline-role-create-actions', styles.actions)}>
|
|
193
193
|
<Button
|
|
194
194
|
type="button"
|
|
195
|
-
intent="
|
|
195
|
+
intent="noeffect"
|
|
196
196
|
size="sm"
|
|
197
197
|
onClick={onClose}
|
|
198
198
|
className={cx('byline-role-create-action', styles.action)}
|
|
@@ -188,7 +188,7 @@ export function UpdateRole({ role, onClose, onSuccess }: UpdateRoleProps) {
|
|
|
188
188
|
<div className={cx('byline-role-update-actions', styles.actions)}>
|
|
189
189
|
<Button
|
|
190
190
|
type="button"
|
|
191
|
-
intent="
|
|
191
|
+
intent="noeffect"
|
|
192
192
|
size="sm"
|
|
193
193
|
onClick={onClose}
|
|
194
194
|
className={cx('byline-role-update-action', styles.action)}
|
|
@@ -32,6 +32,7 @@ export const ADMIN_USERS_ABILITIES = {
|
|
|
32
32
|
update: 'admin.users.update',
|
|
33
33
|
delete: 'admin.users.delete',
|
|
34
34
|
changePassword: 'admin.users.changePassword',
|
|
35
|
+
managePasskeys: 'admin.users.managePasskeys',
|
|
35
36
|
} as const
|
|
36
37
|
|
|
37
38
|
export type AdminUsersAbilityKey =
|
|
@@ -43,6 +44,12 @@ export type AdminUsersAbilityKey =
|
|
|
43
44
|
* the webapp wires into `initBylineCore()`.
|
|
44
45
|
*/
|
|
45
46
|
export function registerAdminUsersAbilities(registry: AbilityRegistry): void {
|
|
47
|
+
registry.register({
|
|
48
|
+
key: ADMIN_USERS_ABILITIES.managePasskeys,
|
|
49
|
+
label: 'Manage admin user passkeys',
|
|
50
|
+
group: 'admin.users',
|
|
51
|
+
source: 'admin',
|
|
52
|
+
})
|
|
46
53
|
registry.register({
|
|
47
54
|
key: ADMIN_USERS_ABILITIES.read,
|
|
48
55
|
label: 'Read admin users',
|
|
@@ -299,7 +299,7 @@ export function CreateAdminUser({ onClose, onSuccess }: CreateAdminUserProps) {
|
|
|
299
299
|
<div className={cx('byline-user-create-actions', styles.actions)}>
|
|
300
300
|
<Button
|
|
301
301
|
type="button"
|
|
302
|
-
intent="
|
|
302
|
+
intent="noeffect"
|
|
303
303
|
size="sm"
|
|
304
304
|
onClick={onClose}
|
|
305
305
|
className={cx('byline-user-create-action', styles.action)}
|
|
@@ -143,7 +143,7 @@ export function UserRoles({ user, allRoles, initialRoleIds, onClose, onSaved }:
|
|
|
143
143
|
<div className={cx('byline-user-roles-actions', styles.actions)}>
|
|
144
144
|
<Button
|
|
145
145
|
type="button"
|
|
146
|
-
intent="
|
|
146
|
+
intent="noeffect"
|
|
147
147
|
size="xs"
|
|
148
148
|
onClick={onClose}
|
|
149
149
|
disabled={saving}
|