@byline/admin 4.19.0 → 5.1.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/fields/field-services-context.d.ts +1 -1
- package/dist/fields/field-services-types.d.ts +5 -2
- package/dist/forms/available-locales-widget.d.ts +3 -2
- package/dist/forms/available-locales-widget.js +4 -2
- package/dist/forms/document-actions.d.ts +3 -2
- package/dist/forms/document-actions.js +32 -14
- package/dist/forms/form-renderer.d.ts +12 -3
- package/dist/forms/form-renderer.js +98 -8
- package/dist/forms/form-renderer.module.js +1 -0
- package/dist/forms/form-renderer_module.css +12 -0
- package/dist/forms/form-status-display.d.ts +3 -2
- package/dist/forms/form-status-display.js +5 -2
- package/dist/forms/path-widget.d.ts +2 -1
- package/dist/forms/path-widget.js +7 -2
- package/dist/forms/scheduled-publication-control.d.ts +2 -1
- package/dist/forms/scheduled-publication-control.js +12 -8
- package/dist/forms/tree-placement-widget.d.ts +5 -1
- package/dist/forms/tree-placement-widget.js +14 -7
- package/dist/forms/upload-executor.d.ts +2 -2
- package/dist/modules/admin-account/components/change-password.js +18 -0
- package/dist/modules/admin-account/service.d.ts +2 -6
- package/dist/modules/admin-account/service.js +2 -1
- package/dist/modules/admin-users/repository.d.ts +10 -1
- package/dist/modules/auth/index.d.ts +3 -1
- package/dist/modules/auth/index.js +1 -0
- package/dist/modules/auth/jwt-session-provider.d.ts +8 -2
- package/dist/modules/auth/jwt-session-provider.js +222 -70
- package/dist/modules/auth/login-sessions-repository.d.ts +23 -0
- package/dist/modules/auth/login-sessions-repository.js +1 -0
- package/dist/modules/auth/refresh-tokens-repository.d.ts +12 -9
- package/dist/modules/auth/resolve-actor.d.ts +3 -0
- package/dist/modules/auth/resolve-actor.js +5 -2
- package/dist/modules/auth/sign-in-rate-limiter.d.ts +48 -0
- package/dist/modules/auth/sign-in-rate-limiter.js +229 -0
- package/dist/store.d.ts +15 -2
- package/package.json +17 -17
- package/src/fields/field-services-types.ts +10 -2
- package/src/forms/available-locales-widget.tsx +5 -2
- package/src/forms/document-actions.tsx +55 -20
- package/src/forms/form-renderer-submit.test.tsx +131 -2
- package/src/forms/form-renderer.module.css +20 -0
- package/src/forms/form-renderer.tsx +122 -7
- package/src/forms/form-status-display.tsx +6 -1
- package/src/forms/path-widget.tsx +8 -3
- package/src/forms/scheduled-publication-control.tsx +16 -7
- package/src/forms/tree-placement-widget.tsx +34 -7
- package/src/modules/admin-account/components/change-password.test.tsx +72 -0
- package/src/modules/admin-account/components/change-password.tsx +15 -4
- package/src/modules/admin-account/service.ts +8 -7
- package/src/modules/admin-users/repository.ts +10 -1
- package/src/modules/auth/index.ts +13 -1
- package/src/modules/auth/jwt-session-provider.ts +276 -91
- package/src/modules/auth/login-sessions-repository.ts +25 -0
- package/src/modules/auth/refresh-tokens-repository.ts +12 -9
- package/src/modules/auth/resolve-actor.ts +10 -1
- package/src/modules/auth/sign-in-rate-limiter.ts +240 -0
- package/src/store.ts +22 -2
|
@@ -6,14 +6,15 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
import type { WorkflowStatus } from '@byline/core';
|
|
9
|
-
import type { PublishedVersionInfo } from './form-renderer';
|
|
9
|
+
import type { PublishedVersionInfo } from './form-renderer.js';
|
|
10
10
|
/**
|
|
11
11
|
* The status / metadata strip at the top of a document form — current
|
|
12
12
|
* workflow status (suppressed for single-status workflows), last-modified and
|
|
13
13
|
* created timestamps, and a "published live" indicator with an inline
|
|
14
14
|
* Unpublish action when a previously-published version is still live.
|
|
15
15
|
*/
|
|
16
|
-
export declare const FormStatusDisplay: ({ initialData, workflowStatuses, publishedVersion, onUnpublish, afterStatusCells, }: {
|
|
16
|
+
export declare const FormStatusDisplay: ({ disabled, initialData, workflowStatuses, publishedVersion, onUnpublish, afterStatusCells, }: {
|
|
17
|
+
disabled?: boolean;
|
|
17
18
|
initialData?: Record<string, any>;
|
|
18
19
|
workflowStatuses?: WorkflowStatus[];
|
|
19
20
|
publishedVersion?: PublishedVersionInfo | null;
|
|
@@ -4,7 +4,7 @@ import { useTranslation } from "@byline/i18n/react";
|
|
|
4
4
|
import clsx from "clsx";
|
|
5
5
|
import { LocalDateTime } from "../fields/local-date-time.js";
|
|
6
6
|
import form_renderer_module from "./form-renderer.module.js";
|
|
7
|
-
const FormStatusDisplay = ({ initialData, workflowStatuses, publishedVersion, onUnpublish, afterStatusCells })=>{
|
|
7
|
+
const FormStatusDisplay = ({ disabled = false, initialData, workflowStatuses, publishedVersion, onUnpublish, afterStatusCells })=>{
|
|
8
8
|
const { t } = useTranslation('byline-admin');
|
|
9
9
|
const statusCode = initialData?.status;
|
|
10
10
|
const statusLabel = workflowStatuses?.find((s)=>s.name === statusCode)?.label ?? statusCode;
|
|
@@ -82,7 +82,10 @@ const FormStatusDisplay = ({ initialData, workflowStatuses, publishedVersion, on
|
|
|
82
82
|
' ',
|
|
83
83
|
/*#__PURE__*/ jsx("button", {
|
|
84
84
|
type: "button",
|
|
85
|
-
|
|
85
|
+
disabled: disabled,
|
|
86
|
+
onClick: ()=>{
|
|
87
|
+
if (!disabled) onUnpublish().catch(()=>{});
|
|
88
|
+
},
|
|
86
89
|
className: clsx('byline-form-status-unpublish', form_renderer_module["status-unpublish"]),
|
|
87
90
|
children: t('common.actions.unpublish')
|
|
88
91
|
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SlugifierFn } from '@byline/core';
|
|
2
2
|
export interface PathWidgetProps {
|
|
3
|
+
disabled?: boolean;
|
|
3
4
|
/** The collection's `useAsPath` source field name, when configured. */
|
|
4
5
|
useAsPath: string | undefined;
|
|
5
6
|
/** Collection path, forwarded to the slugifier as context. */
|
|
@@ -51,4 +52,4 @@ export interface PathWidgetProps {
|
|
|
51
52
|
* Stable override handles: `.byline-form-path`, `.byline-form-path-header`,
|
|
52
53
|
* `.byline-form-path-regenerate`.
|
|
53
54
|
*/
|
|
54
|
-
export declare const PathWidget: ({ useAsPath, collectionPath, defaultLocale, activeLocale, mode, slugifier, sourceLocked, }: PathWidgetProps) => import("react").JSX.Element;
|
|
55
|
+
export declare const PathWidget: ({ useAsPath, collectionPath, defaultLocale, activeLocale, mode, slugifier, sourceLocked, disabled, }: PathWidgetProps) => import("react").JSX.Element;
|
|
@@ -12,7 +12,7 @@ function coerceToString(value) {
|
|
|
12
12
|
if (value instanceof Date) return value.toISOString();
|
|
13
13
|
return String(value);
|
|
14
14
|
}
|
|
15
|
-
const PathWidget = ({ useAsPath, collectionPath, defaultLocale, activeLocale, mode, slugifier, sourceLocked = false })=>{
|
|
15
|
+
const PathWidget = ({ useAsPath, collectionPath, defaultLocale, activeLocale, mode, slugifier, sourceLocked = false, disabled = false })=>{
|
|
16
16
|
const { setSystemPath } = useFormContext();
|
|
17
17
|
const { t } = useTranslation('byline-admin');
|
|
18
18
|
const systemPath = useSystemPath();
|
|
@@ -37,13 +37,16 @@ const PathWidget = ({ useAsPath, collectionPath, defaultLocale, activeLocale, mo
|
|
|
37
37
|
]);
|
|
38
38
|
const inputValue = systemPath ?? '';
|
|
39
39
|
const handleChange = useCallback((next)=>{
|
|
40
|
+
if (disabled) return;
|
|
40
41
|
setSystemPath(0 === next.length ? null : next);
|
|
41
42
|
}, [
|
|
43
|
+
disabled,
|
|
42
44
|
setSystemPath
|
|
43
45
|
]);
|
|
44
46
|
const handleRegenerate = useCallback(()=>{
|
|
45
|
-
if (livePreview.length > 0) setSystemPath(livePreview);
|
|
47
|
+
if (!disabled && livePreview.length > 0) setSystemPath(livePreview);
|
|
46
48
|
}, [
|
|
49
|
+
disabled,
|
|
47
50
|
livePreview,
|
|
48
51
|
setSystemPath
|
|
49
52
|
]);
|
|
@@ -90,6 +93,7 @@ const PathWidget = ({ useAsPath, collectionPath, defaultLocale, activeLocale, mo
|
|
|
90
93
|
type: "button",
|
|
91
94
|
onClick: handleRegenerate,
|
|
92
95
|
className: clsx('byline-form-path-regenerate', path_widget_module.regenerate),
|
|
96
|
+
disabled: disabled,
|
|
93
97
|
"aria-label": t('pathWidget.regenerateAriaLabel', {
|
|
94
98
|
field: useAsPath
|
|
95
99
|
}),
|
|
@@ -100,6 +104,7 @@ const PathWidget = ({ useAsPath, collectionPath, defaultLocale, activeLocale, mo
|
|
|
100
104
|
]
|
|
101
105
|
}),
|
|
102
106
|
/*#__PURE__*/ jsx(Input, {
|
|
107
|
+
disabled: disabled,
|
|
103
108
|
id: "system-path",
|
|
104
109
|
name: "__systemPath__",
|
|
105
110
|
value: inputValue,
|
|
@@ -5,6 +5,7 @@ export interface SchedulePublicationInput {
|
|
|
5
5
|
publishAt: string;
|
|
6
6
|
}
|
|
7
7
|
export interface UseScheduledPublicationArgs {
|
|
8
|
+
disabled?: boolean;
|
|
8
9
|
schedule: ScheduledPublicationInfo | null;
|
|
9
10
|
onSchedule?: (input: SchedulePublicationInput) => Promise<void>;
|
|
10
11
|
onConfirm?: () => Promise<void>;
|
|
@@ -24,7 +25,7 @@ export interface UseScheduledPublicationReturn {
|
|
|
24
25
|
/** The schedule / reschedule modal. Render once, anywhere in the form. */
|
|
25
26
|
modal: React.ReactNode;
|
|
26
27
|
}
|
|
27
|
-
export declare function useScheduledPublication({ schedule, onSchedule, onConfirm, onCancel, hasUnsavedChanges, onUnsavedChanges, }: UseScheduledPublicationArgs): UseScheduledPublicationReturn;
|
|
28
|
+
export declare function useScheduledPublication({ disabled, schedule, onSchedule, onConfirm, onCancel, hasUnsavedChanges, onUnsavedChanges, }: UseScheduledPublicationArgs): UseScheduledPublicationReturn;
|
|
28
29
|
/**
|
|
29
30
|
* One metadata cell for the form's status bar, matching the Status /
|
|
30
31
|
* Last modified / Created cells in scale, sitting immediately after the status
|
|
@@ -28,7 +28,7 @@ function seedScheduleInstant(schedule) {
|
|
|
28
28
|
seed.setSeconds(0, 0);
|
|
29
29
|
return seed;
|
|
30
30
|
}
|
|
31
|
-
function useScheduledPublication({ schedule, onSchedule, onConfirm, onCancel, hasUnsavedChanges, onUnsavedChanges }) {
|
|
31
|
+
function useScheduledPublication({ disabled = false, schedule, onSchedule, onConfirm, onCancel, hasUnsavedChanges, onUnsavedChanges }) {
|
|
32
32
|
const timeZone = useMemo(browserTimeZone, []);
|
|
33
33
|
const [now, setNow] = useState(()=>Date.now());
|
|
34
34
|
const [showSchedule, setShowSchedule] = useState(false);
|
|
@@ -55,52 +55,56 @@ function useScheduledPublication({ schedule, onSchedule, onConfirm, onCancel, ha
|
|
|
55
55
|
now
|
|
56
56
|
]);
|
|
57
57
|
const openSchedule = useCallback(()=>{
|
|
58
|
+
if (disabled) return;
|
|
58
59
|
if (hasUnsavedChanges) return void onUnsavedChanges();
|
|
59
60
|
setShowSchedule(true);
|
|
60
61
|
}, [
|
|
62
|
+
disabled,
|
|
61
63
|
hasUnsavedChanges,
|
|
62
64
|
onUnsavedChanges
|
|
63
65
|
]);
|
|
64
66
|
const confirm = useCallback(async ()=>{
|
|
65
|
-
if (null == onConfirm) return;
|
|
67
|
+
if (disabled || null == onConfirm) return;
|
|
66
68
|
if (hasUnsavedChanges) return void onUnsavedChanges();
|
|
67
69
|
setBusy(true);
|
|
68
70
|
try {
|
|
69
71
|
await onConfirm();
|
|
70
|
-
} finally{
|
|
72
|
+
} catch {} finally{
|
|
71
73
|
setBusy(false);
|
|
72
74
|
}
|
|
73
75
|
}, [
|
|
76
|
+
disabled,
|
|
74
77
|
onConfirm,
|
|
75
78
|
hasUnsavedChanges,
|
|
76
79
|
onUnsavedChanges
|
|
77
80
|
]);
|
|
78
81
|
const cancel = useCallback(async ()=>{
|
|
79
|
-
if (null == onCancel) return;
|
|
82
|
+
if (disabled || null == onCancel) return;
|
|
80
83
|
setBusy(true);
|
|
81
84
|
try {
|
|
82
85
|
await onCancel();
|
|
83
|
-
} finally{
|
|
86
|
+
} catch {} finally{
|
|
84
87
|
setBusy(false);
|
|
85
88
|
}
|
|
86
89
|
}, [
|
|
90
|
+
disabled,
|
|
87
91
|
onCancel
|
|
88
92
|
]);
|
|
89
93
|
const modal = showSchedule ? /*#__PURE__*/ jsx(ScheduleModal, {
|
|
90
94
|
schedule: schedule,
|
|
91
95
|
timeZone: timeZone,
|
|
92
96
|
onSubmit: async (input)=>{
|
|
93
|
-
if (null == onSchedule) return;
|
|
97
|
+
if (disabled || null == onSchedule) return;
|
|
94
98
|
setBusy(true);
|
|
95
99
|
try {
|
|
96
100
|
await onSchedule(input);
|
|
97
101
|
setShowSchedule(false);
|
|
98
|
-
} finally{
|
|
102
|
+
} catch {} finally{
|
|
99
103
|
setBusy(false);
|
|
100
104
|
}
|
|
101
105
|
},
|
|
102
106
|
onDismiss: ()=>setShowSchedule(false),
|
|
103
|
-
busy: busy
|
|
107
|
+
busy: disabled || busy
|
|
104
108
|
}) : null;
|
|
105
109
|
return {
|
|
106
110
|
state,
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export interface TreePlacementWidgetProps {
|
|
2
|
+
disabled?: boolean;
|
|
3
|
+
onMutationError?: (error: unknown) => 'blocked' | 'committed' | null | void;
|
|
4
|
+
onCommitted?: (receipt: import('@byline/core').StructuralMutationReceipt) => void;
|
|
5
|
+
expectedRevision: number;
|
|
2
6
|
/** The collection path (`tree: true`). */
|
|
3
7
|
collectionPath: string;
|
|
4
8
|
/** The logical id of the document being edited. */
|
|
@@ -20,4 +24,4 @@ export interface TreePlacementWidgetProps {
|
|
|
20
24
|
* Renders only in edit mode (placement needs a persisted document) and only when
|
|
21
25
|
* the host wires the tree services. Stable override handle: `.byline-form-tree`.
|
|
22
26
|
*/
|
|
23
|
-
export declare const TreePlacementWidget: ({ collectionPath, documentId, useAsTitle, }: TreePlacementWidgetProps) => import("react").JSX.Element | null;
|
|
27
|
+
export declare const TreePlacementWidget: ({ collectionPath, disabled, onMutationError, onCommitted, expectedRevision, documentId, useAsTitle, }: TreePlacementWidgetProps) => import("react").JSX.Element | null;
|
|
@@ -8,7 +8,7 @@ import clsx from "clsx";
|
|
|
8
8
|
import { useBylineFieldServices } from "../fields/field-services-context.js";
|
|
9
9
|
import { RelationPicker } from "../fields/relation/relation-picker.js";
|
|
10
10
|
import tree_placement_widget_module from "./tree-placement-widget.module.js";
|
|
11
|
-
const TreePlacementWidget = ({ collectionPath, documentId, useAsTitle })=>{
|
|
11
|
+
const TreePlacementWidget = ({ collectionPath, disabled = false, onMutationError, onCommitted, expectedRevision, documentId, useAsTitle })=>{
|
|
12
12
|
const { t } = useTranslation('byline-admin');
|
|
13
13
|
const { getTreeAncestors, getTreeParent, placeTreeNode } = useBylineFieldServices();
|
|
14
14
|
const definition = getCollectionDefinition(collectionPath);
|
|
@@ -60,7 +60,7 @@ const TreePlacementWidget = ({ collectionPath, documentId, useAsTitle })=>{
|
|
|
60
60
|
t
|
|
61
61
|
]);
|
|
62
62
|
const place = useCallback(async (parentDocumentId, optimistic)=>{
|
|
63
|
-
if (null == placeTreeNode || busy) return;
|
|
63
|
+
if (disabled || null == placeTreeNode || busy) return;
|
|
64
64
|
const previousParent = parent;
|
|
65
65
|
const previousPlaced = placed;
|
|
66
66
|
setError(null);
|
|
@@ -68,12 +68,15 @@ const TreePlacementWidget = ({ collectionPath, documentId, useAsTitle })=>{
|
|
|
68
68
|
setParent(optimistic);
|
|
69
69
|
setPlaced(true);
|
|
70
70
|
try {
|
|
71
|
-
await placeTreeNode({
|
|
71
|
+
const result = await placeTreeNode({
|
|
72
|
+
expectedRevision,
|
|
72
73
|
collection: collectionPath,
|
|
73
74
|
documentId,
|
|
74
75
|
parentDocumentId
|
|
75
76
|
});
|
|
76
|
-
|
|
77
|
+
onCommitted?.(result);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (onMutationError?.(error) === 'committed') return;
|
|
77
80
|
setParent(previousParent);
|
|
78
81
|
setPlaced(previousPlaced);
|
|
79
82
|
setError(t('treeWidget.error'));
|
|
@@ -81,6 +84,10 @@ const TreePlacementWidget = ({ collectionPath, documentId, useAsTitle })=>{
|
|
|
81
84
|
setBusy(false);
|
|
82
85
|
}
|
|
83
86
|
}, [
|
|
87
|
+
disabled,
|
|
88
|
+
onMutationError,
|
|
89
|
+
onCommitted,
|
|
90
|
+
expectedRevision,
|
|
84
91
|
placeTreeNode,
|
|
85
92
|
busy,
|
|
86
93
|
parent,
|
|
@@ -136,20 +143,20 @@ const TreePlacementWidget = ({ collectionPath, documentId, useAsTitle })=>{
|
|
|
136
143
|
size: "xs",
|
|
137
144
|
variant: "outlined",
|
|
138
145
|
intent: "noeffect",
|
|
139
|
-
disabled: loading || busy,
|
|
146
|
+
disabled: disabled || loading || busy,
|
|
140
147
|
onClick: ()=>setPickerOpen(true),
|
|
141
148
|
children: t('treeWidget.choose')
|
|
142
149
|
}),
|
|
143
150
|
placed ? null != parent && /*#__PURE__*/ jsx("button", {
|
|
144
151
|
type: "button",
|
|
145
152
|
className: clsx('byline-form-tree-link', tree_placement_widget_module.link),
|
|
146
|
-
disabled: busy,
|
|
153
|
+
disabled: disabled || busy,
|
|
147
154
|
onClick: ()=>place(null, null),
|
|
148
155
|
children: t('treeWidget.makeRoot')
|
|
149
156
|
}) : /*#__PURE__*/ jsx("button", {
|
|
150
157
|
type: "button",
|
|
151
158
|
className: clsx('byline-form-tree-link', tree_placement_widget_module.link),
|
|
152
|
-
disabled: loading || busy,
|
|
159
|
+
disabled: disabled || loading || busy,
|
|
153
160
|
onClick: ()=>place(null, null),
|
|
154
161
|
children: t('treeWidget.addToTree')
|
|
155
162
|
})
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* but only uploaded when the user clicks Save.
|
|
14
14
|
*/
|
|
15
15
|
import type { Field, StoredFileValue } from '@byline/core';
|
|
16
|
-
import type { UploadFieldFn } from '../fields/field-services-types';
|
|
17
|
-
import type { PendingUpload } from './form-context';
|
|
16
|
+
import type { UploadFieldFn } from '../fields/field-services-types.js';
|
|
17
|
+
import type { PendingUpload } from './form-context.js';
|
|
18
18
|
export interface UploadResult {
|
|
19
19
|
fieldPath: string;
|
|
20
20
|
success: boolean;
|
|
@@ -84,6 +84,24 @@ function ChangeAccountPassword({ account, onClose, onSuccess }) {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
});
|
|
87
|
+
if (successMessage) return /*#__PURE__*/ jsxs("div", {
|
|
88
|
+
className: clsx('byline-account-change-password-wrap', change_password_module.wrap),
|
|
89
|
+
children: [
|
|
90
|
+
/*#__PURE__*/ jsx("div", {
|
|
91
|
+
role: "status",
|
|
92
|
+
children: /*#__PURE__*/ jsx(Alert, {
|
|
93
|
+
intent: "success",
|
|
94
|
+
children: successMessage
|
|
95
|
+
})
|
|
96
|
+
}),
|
|
97
|
+
/*#__PURE__*/ jsx(Button, {
|
|
98
|
+
type: "button",
|
|
99
|
+
intent: "primary",
|
|
100
|
+
onClick: ()=>window.location.reload(),
|
|
101
|
+
children: t('auth.signIn.title')
|
|
102
|
+
})
|
|
103
|
+
]
|
|
104
|
+
});
|
|
87
105
|
return /*#__PURE__*/ jsx("div", {
|
|
88
106
|
className: clsx('byline-account-change-password-wrap', change_password_module.wrap),
|
|
89
107
|
children: /*#__PURE__*/ jsxs("form", {
|
|
@@ -25,12 +25,8 @@ import type { AccountResponse, ChangeAccountPasswordRequest, UpdateAccountReques
|
|
|
25
25
|
* in the new hash. A hijacked session cannot use this flow to lock
|
|
26
26
|
* out the legitimate owner.
|
|
27
27
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* valid until their 15-minute expiry, and other refresh tokens remain
|
|
31
|
-
* useable. A "sign out everywhere on password change" follow-up should
|
|
32
|
-
* call `RefreshTokensRepository.revokeAllExcept(adminUserId, currentJti)`
|
|
33
|
-
* once that lands.
|
|
28
|
+
* Native adapter password writes atomically advance the session generation and
|
|
29
|
+
* revoke every refresh session. External providers own their own revocation.
|
|
34
30
|
*/
|
|
35
31
|
export declare class AdminAccountService {
|
|
36
32
|
#private;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { toAdminUser } from "../admin-users/dto.js";
|
|
2
|
-
import { ERR_ADMIN_USER_EMAIL_IN_USE } from "../admin-users/errors.js";
|
|
2
|
+
import { ERR_ADMIN_USER_EMAIL_IN_USE, ERR_ADMIN_USER_VERSION_CONFLICT } from "../admin-users/errors.js";
|
|
3
3
|
import { hashPassword, verifyPassword } from "../auth/password.js";
|
|
4
4
|
import { ERR_ADMIN_ACCOUNT_INVALID_CURRENT_PASSWORD, ERR_ADMIN_ACCOUNT_NOT_FOUND } from "./errors.js";
|
|
5
5
|
function _check_private_redeclaration(obj, privateCollection) {
|
|
@@ -63,6 +63,7 @@ class AdminAccountService {
|
|
|
63
63
|
if (!withHash) throw ERR_ADMIN_ACCOUNT_NOT_FOUND();
|
|
64
64
|
const ok = await verifyPassword(request.currentPassword, withHash.password_hash);
|
|
65
65
|
if (!ok) throw ERR_ADMIN_ACCOUNT_INVALID_CURRENT_PASSWORD();
|
|
66
|
+
if (withHash.vid !== request.vid) throw ERR_ADMIN_USER_VERSION_CONFLICT();
|
|
66
67
|
const newHash = await hashPassword(request.newPassword);
|
|
67
68
|
const row = await _class_private_field_get(this, _repo).setPasswordHash(actorId, request.vid, newHash);
|
|
68
69
|
return toAdminUser(row);
|
|
@@ -65,6 +65,8 @@ export interface AdminUserRow {
|
|
|
65
65
|
*/
|
|
66
66
|
export interface AdminUserWithPasswordRow extends AdminUserRow {
|
|
67
67
|
password_hash: string;
|
|
68
|
+
/** Native session generation, independent of edit revisions. */
|
|
69
|
+
session_version: number;
|
|
68
70
|
}
|
|
69
71
|
export interface CreateAdminUserInput {
|
|
70
72
|
email: string;
|
|
@@ -143,16 +145,23 @@ export interface AdminUsersRepository {
|
|
|
143
145
|
* Content update with optimistic concurrency. Throws
|
|
144
146
|
* `AdminUsersError(VERSION_CONFLICT)` if the stored `vid` differs from
|
|
145
147
|
* `expectedVid`. Bumps `vid` on success and returns the fresh row.
|
|
148
|
+
* A false `is_enabled` patch must atomically advance the native session
|
|
149
|
+
* generation and revoke refresh sessions, under the account row lock.
|
|
146
150
|
*/
|
|
147
151
|
update(id: string, expectedVid: number, patch: UpdateAdminUserInput): Promise<AdminUserRow>;
|
|
148
152
|
/**
|
|
149
153
|
* Replace the stored password hash with optimistic concurrency.
|
|
150
154
|
* Version-gated on `expectedVid`. Caller supplies a pre-hashed PHC string.
|
|
155
|
+
* Atomically advance the native session generation and revoke every refresh
|
|
156
|
+
* session in the same transaction. Lock the account before refresh rows.
|
|
151
157
|
* Returns the updated row so callers holding the edit form can refresh
|
|
152
158
|
* their cached `vid` without a second round-trip.
|
|
153
159
|
*/
|
|
154
160
|
setPasswordHash(id: string, expectedVid: number, passwordHash: string): Promise<AdminUserRow>;
|
|
155
|
-
/**
|
|
161
|
+
/**
|
|
162
|
+
* Toggle enabled state. Disable must atomically advance the native session
|
|
163
|
+
* generation and revoke refresh sessions. Enable never resets the generation.
|
|
164
|
+
*/
|
|
156
165
|
setEnabled(id: string, enabled: boolean): Promise<void>;
|
|
157
166
|
/**
|
|
158
167
|
* Set the admin interface locale preference. Vid-less — user preference
|
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
* Clerk, institutional SSO) should be shipped as separate packages
|
|
20
20
|
* against `@byline/auth` rather than added here.
|
|
21
21
|
*/
|
|
22
|
-
export { JwtSessionProvider, type JwtSessionProviderConfig } from './jwt-session-provider.js';
|
|
22
|
+
export { JwtSessionProvider, type JwtSessionProviderConfig, type NativeSessionEvent, } from './jwt-session-provider.js';
|
|
23
23
|
export { hashPassword, verifyPassword } from './password.js';
|
|
24
24
|
export { resolveActor } from './resolve-actor.js';
|
|
25
|
+
export { createPasswordSignInLimiter, type SignInLimiterOptions, type SignInRateLimitPolicy, type SignInRateLimitStore, type SignInSecurityEvent, } from './sign-in-rate-limiter.js';
|
|
26
|
+
export type { LoginSessionRow, LoginSessionsRepository } from './login-sessions-repository.js';
|
|
25
27
|
export type { IssueRefreshTokenInput, RefreshTokenRow, RefreshTokensRepository, } from './refresh-tokens-repository.js';
|
|
@@ -5,9 +5,14 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
-
import { type AdminAuth, type RefreshSessionArgs, type SessionProvider, type SessionProviderCapabilities, type SessionTokens, type SignInResult, type SignInWithPasswordArgs } from '@byline/auth';
|
|
8
|
+
import { type AdminAuth, type RefreshSessionArgs, type RevokeSessionArgs, type SessionProvider, type SessionProviderCapabilities, type SessionTokens, type SignInResult, type SignInWithPasswordArgs } from '@byline/auth';
|
|
9
9
|
import type { AdminStore } from '../../store.js';
|
|
10
|
+
export interface NativeSessionEvent {
|
|
11
|
+
type: 'refresh_attempted' | 'refresh_completed' | 'refresh_contested';
|
|
12
|
+
}
|
|
10
13
|
export interface JwtSessionProviderConfig {
|
|
14
|
+
/** Best-effort, token-free renewal telemetry. Errors never change authentication outcomes. */
|
|
15
|
+
onEvent?: (event: NativeSessionEvent) => void;
|
|
11
16
|
/**
|
|
12
17
|
* Adapter-backed admin repositories. Construct via the DB adapter's
|
|
13
18
|
* admin-store factory (e.g. `createAdminStore(db)` from
|
|
@@ -39,8 +44,9 @@ export declare class JwtSessionProvider implements SessionProvider {
|
|
|
39
44
|
signInWithPassword(args: SignInWithPasswordArgs): Promise<SignInResult>;
|
|
40
45
|
verifyAccessToken(token: string): Promise<{
|
|
41
46
|
actor: AdminAuth;
|
|
47
|
+
sessionId: string;
|
|
42
48
|
}>;
|
|
43
49
|
refreshSession(args: RefreshSessionArgs): Promise<SessionTokens>;
|
|
44
|
-
revokeSession(
|
|
50
|
+
revokeSession(args: RevokeSessionArgs): Promise<void>;
|
|
45
51
|
resolveActor(adminUserId: string): Promise<AdminAuth | null>;
|
|
46
52
|
}
|