@byline/admin 6.7.2 → 6.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/dist/fields/blocks/blocks-field.js +33 -3
  2. package/dist/fields/blocks/blocks-field.module.js +1 -0
  3. package/dist/fields/blocks/blocks-field_module.css +19 -0
  4. package/dist/forms/form-page-chrome.d.ts +3 -2
  5. package/dist/forms/form-page-chrome.js +13 -1
  6. package/dist/forms/form-renderer.d.ts +2 -1
  7. package/dist/forms/form-renderer.js +23 -17
  8. package/dist/forms/unavailable-blocks.d.ts +15 -0
  9. package/dist/forms/unavailable-blocks.js +38 -0
  10. package/dist/modules/admin-account/components/change-password.js +1 -1
  11. package/dist/modules/admin-account/components/container.js +18 -3
  12. package/dist/modules/admin-account/components/container.module.js +2 -0
  13. package/dist/modules/admin-account/components/container_module.css +7 -0
  14. package/dist/modules/admin-account/components/preferences.js +1 -1
  15. package/dist/modules/admin-account/components/update.js +1 -1
  16. package/dist/modules/admin-roles/components/create.js +1 -1
  17. package/dist/modules/admin-roles/components/permissions.js +1 -1
  18. package/dist/modules/admin-roles/components/update.js +1 -1
  19. package/dist/modules/admin-users/abilities.d.ts +1 -0
  20. package/dist/modules/admin-users/abilities.js +8 -1
  21. package/dist/modules/admin-users/components/create.js +1 -1
  22. package/dist/modules/admin-users/components/roles.js +1 -1
  23. package/dist/modules/admin-users/components/set-password.js +1 -1
  24. package/dist/modules/admin-users/components/update.js +1 -1
  25. package/dist/modules/auth/auth-maintenance-repository.d.ts +31 -0
  26. package/dist/modules/auth/auth-maintenance-repository.js +1 -0
  27. package/dist/modules/auth/auth-maintenance.d.ts +11 -0
  28. package/dist/modules/auth/auth-maintenance.js +39 -0
  29. package/dist/modules/auth/auth-maintenance.test.node.d.ts +1 -0
  30. package/dist/modules/auth/components/passkey-browser.d.ts +14 -0
  31. package/dist/modules/auth/components/passkey-browser.js +118 -0
  32. package/dist/modules/auth/components/passkey-browser.test.node.d.ts +1 -0
  33. package/dist/modules/auth/components/passkey-manager.d.ts +26 -0
  34. package/dist/modules/auth/components/passkey-manager.js +406 -0
  35. package/dist/modules/auth/components/passkey-manager.module.js +22 -0
  36. package/dist/modules/auth/components/passkey-manager_module.css +104 -0
  37. package/dist/modules/auth/components/sign-in-form.js +62 -3
  38. package/dist/modules/auth/components/sign-in-form.module.js +5 -1
  39. package/dist/modules/auth/components/sign-in-form_module.css +40 -0
  40. package/dist/modules/auth/index.d.ts +5 -0
  41. package/dist/modules/auth/index.js +3 -0
  42. package/dist/modules/auth/jwt-session-provider.d.ts +12 -4
  43. package/dist/modules/auth/jwt-session-provider.js +135 -94
  44. package/dist/modules/auth/native-passkey-provider.d.ts +72 -0
  45. package/dist/modules/auth/native-passkey-provider.js +618 -0
  46. package/dist/modules/auth/passkey-rate-limiter.d.ts +14 -0
  47. package/dist/modules/auth/passkey-rate-limiter.js +60 -0
  48. package/dist/modules/auth/passkey-rate-limiter.test.node.d.ts +8 -0
  49. package/dist/modules/auth/passkey-storage-validation.d.ts +14 -0
  50. package/dist/modules/auth/passkey-storage-validation.js +59 -0
  51. package/dist/modules/auth/passkeys-repository.d.ts +96 -0
  52. package/dist/modules/auth/passkeys-repository.js +1 -0
  53. package/dist/modules/auth/refresh-tokens-repository.d.ts +1 -1
  54. package/dist/modules/auth/sign-in-rate-limiter.d.ts +2 -0
  55. package/dist/modules/auth/sign-in-rate-limiter.js +1 -1
  56. package/dist/services/admin-services-context.d.ts +1 -1
  57. package/dist/services/admin-services-types.d.ts +60 -0
  58. package/dist/store.d.ts +4 -0
  59. package/package.json +16 -9
  60. package/src/fields/blocks/blocks-field.module.css +23 -0
  61. package/src/fields/blocks/blocks-field.tsx +26 -2
  62. package/src/forms/form-page-chrome.tsx +19 -1
  63. package/src/forms/form-renderer-capabilities.test.tsx +6 -6
  64. package/src/forms/form-renderer.tsx +24 -15
  65. package/src/forms/unavailable-blocks.test.tsx +152 -0
  66. package/src/forms/unavailable-blocks.tsx +58 -0
  67. package/src/modules/admin-account/components/change-password.tsx +1 -1
  68. package/src/modules/admin-account/components/container.module.css +9 -0
  69. package/src/modules/admin-account/components/container.tsx +12 -3
  70. package/src/modules/admin-account/components/preferences.tsx +1 -1
  71. package/src/modules/admin-account/components/update.tsx +1 -1
  72. package/src/modules/admin-roles/components/create.tsx +1 -1
  73. package/src/modules/admin-roles/components/permissions.tsx +1 -1
  74. package/src/modules/admin-roles/components/update.tsx +1 -1
  75. package/src/modules/admin-users/abilities.ts +7 -0
  76. package/src/modules/admin-users/components/create.tsx +1 -1
  77. package/src/modules/admin-users/components/roles.tsx +1 -1
  78. package/src/modules/admin-users/components/set-password.tsx +1 -1
  79. package/src/modules/admin-users/components/update.tsx +1 -1
  80. package/src/modules/auth/auth-maintenance-repository.ts +26 -0
  81. package/src/modules/auth/auth-maintenance.test.node.ts +110 -0
  82. package/src/modules/auth/auth-maintenance.ts +62 -0
  83. package/src/modules/auth/components/passkey-browser.test.node.ts +188 -0
  84. package/src/modules/auth/components/passkey-browser.ts +139 -0
  85. package/src/modules/auth/components/passkey-manager.module.css +100 -0
  86. package/src/modules/auth/components/passkey-manager.test.tsx +189 -0
  87. package/src/modules/auth/components/passkey-manager.tsx +386 -0
  88. package/src/modules/auth/components/sign-in-form.module.css +37 -0
  89. package/src/modules/auth/components/sign-in-form.test.tsx +92 -0
  90. package/src/modules/auth/components/sign-in-form.tsx +65 -4
  91. package/src/modules/auth/index.ts +15 -0
  92. package/src/modules/auth/jwt-session-provider.ts +116 -44
  93. package/src/modules/auth/native-passkey-provider.ts +710 -0
  94. package/src/modules/auth/passkey-rate-limiter.test.node.ts +83 -0
  95. package/src/modules/auth/passkey-rate-limiter.ts +76 -0
  96. package/src/modules/auth/passkey-storage-validation.ts +108 -0
  97. package/src/modules/auth/passkeys-repository.ts +97 -0
  98. package/src/modules/auth/refresh-tokens-repository.ts +1 -1
  99. package/src/modules/auth/sign-in-rate-limiter.ts +1 -1
  100. package/src/services/admin-services-context.tsx +2 -0
  101. package/src/services/admin-services-types.ts +60 -0
  102. package/src/store.ts +4 -0
@@ -1,8 +1,8 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { useEffect, useMemo, useState } from "react";
2
+ import { Fragment, useEffect, useMemo, useState } from "react";
3
3
  import { getAdminConfig } from "@byline/core";
4
4
  import { useTranslation } from "@byline/i18n/react";
5
- import { Card, CloseIcon, DraggableSortable, ErrorText, IconButton, Modal, PlusIcon, moveItem } from "@byline/ui/react";
5
+ import { Card, CloseIcon, DraggableSortable, ErrorText, IconButton, Modal, PlusIcon, WarningIcon, moveItem } from "@byline/ui/react";
6
6
  import clsx from "clsx";
7
7
  import { defaultScalarForField } from "../field-helpers.js";
8
8
  import { GroupField } from "../group/group-field.js";
@@ -11,10 +11,12 @@ import { useFieldError, useFormContext } from "../../forms/form-context.js";
11
11
  import { useScopedDomId } from "../../forms/form-dom-scope.js";
12
12
  import { hasExistingIdTargets } from "../../forms/nested-path.js";
13
13
  import { moveRepeatingItems, repeatingItemId, repeatingItemPath } from "../../forms/repeating-items.js";
14
+ import { useUnavailableBlocks } from "../../forms/unavailable-blocks.js";
14
15
  import blocks_field_module from "./blocks-field.module.js";
15
16
  const BlocksField = ({ field, defaultValue, path, contentLocale })=>{
16
17
  const { appendPatch, getFieldValue, getFieldValues, removePendingUploadsUnder, setFieldStore } = useFormContext();
17
18
  const { t } = useTranslation('byline-admin');
19
+ const unavailableBlocks = useUnavailableBlocks(path);
18
20
  const fieldError = useFieldError(path);
19
21
  const htmlId = useScopedDomId(path);
20
22
  const [items, setItems] = useState([]);
@@ -170,7 +172,35 @@ const BlocksField = ({ field, defaultValue, path, contentLocale })=>{
170
172
  onDragEnd: handleDragEnd,
171
173
  className: clsx('byline-field-blocks-stack', blocks_field_module.stack),
172
174
  children: [
173
- items.map((item, index)=>renderItem(item, index)),
175
+ Array.from({
176
+ length: items.length + 1
177
+ }, (_, index)=>/*#__PURE__*/ jsxs(Fragment, {
178
+ children: [
179
+ unavailableBlocks.map((issue, removedBefore)=>Math.min(issue.storedIndex - removedBefore, items.length) === index ? /*#__PURE__*/ jsxs("div", {
180
+ role: "note",
181
+ className: clsx('byline-field-blocks-unavailable', blocks_field_module.unavailable),
182
+ children: [
183
+ /*#__PURE__*/ jsx(WarningIcon, {
184
+ width: "24px",
185
+ height: "24px"
186
+ }),
187
+ /*#__PURE__*/ jsxs("div", {
188
+ children: [
189
+ /*#__PURE__*/ jsx("strong", {
190
+ children: t('fields.blocks.unavailableTitle', {
191
+ blockType: issue.blockType
192
+ })
193
+ }),
194
+ /*#__PURE__*/ jsx("p", {
195
+ children: t('fields.blocks.unavailableBody')
196
+ })
197
+ ]
198
+ })
199
+ ]
200
+ }, `${issue.storedIndex}-${issue.blockType}`) : null),
201
+ index < items.length && renderItem(items[index], index)
202
+ ]
203
+ }, items[index]?.id ?? 'end')),
174
204
  /*#__PURE__*/ jsxs("div", {
175
205
  className: clsx('byline-field-blocks-add-row', blocks_field_module["add-row"]),
176
206
  children: [
@@ -2,6 +2,7 @@ import "./blocks-field_module.css";
2
2
  const blocks_field_module = {
3
3
  title: "title-lxXfG6",
4
4
  stack: "stack-Xxa89J",
5
+ unavailable: "unavailable-B9h_EU",
5
6
  "add-row": "add-row-gEqjM_",
6
7
  addRow: "add-row-gEqjM_",
7
8
  "add-label": "add-label-jiOURu",
@@ -10,6 +10,25 @@
10
10
  display: flex;
11
11
  }
12
12
 
13
+ :is(.unavailable-B9h_EU, .byline-field-blocks-unavailable) {
14
+ align-items: flex-start;
15
+ gap: var(--spacing-8);
16
+ padding: var(--spacing-16);
17
+ border: 1px solid var(--stroke-warning);
18
+ border-radius: var(--border-radius-md);
19
+ background-color: oklch(from var(--fill-warning-weak) l c h / calc(alpha * .5));
20
+ color: var(--foreground);
21
+ display: flex;
22
+ }
23
+
24
+ :is(.unavailable-B9h_EU > :first-child, .byline-field-blocks-unavailable > :first-child) {
25
+ flex-shrink: 0;
26
+ }
27
+
28
+ :is(.unavailable-B9h_EU p, .byline-field-blocks-unavailable p) {
29
+ margin: var(--spacing-4) 0 0;
30
+ }
31
+
13
32
  :is(.add-row-gEqjM_, .byline-field-blocks-add-row) {
14
33
  color: var(--muted);
15
34
  align-items: center;
@@ -14,7 +14,7 @@
14
14
  * visible at its call site.
15
15
  */
16
16
  import type { ReactNode, RefObject } from 'react';
17
- import type { SlugifierFn, StructuralMutationReceipt, WorkflowStatus } from '@byline/core';
17
+ import type { SlugifierFn, StructuralMutationReceipt, UnavailableBlock, WorkflowStatus } from '@byline/core';
18
18
  import { type DocumentActionsLocaleOption } from './document-actions.js';
19
19
  import { type UseScheduledPublicationReturn } from './scheduled-publication-control.js';
20
20
  import type { PublishedVersionInfo } from './form-renderer.js';
@@ -78,8 +78,9 @@ export interface FormConcurrencyNoticesProps {
78
78
  onDiscardRequested: () => void;
79
79
  scheduling: UseScheduledPublicationReturn;
80
80
  restoreWarnings?: string[];
81
+ unavailableBlocks?: UnavailableBlock[];
81
82
  }
82
- export declare const FormConcurrencyNotices: ({ disabled, mutationIssue, scheduledPublicationsNeedReconfirmation, scheduledPublicationsHref, warningRef, discarding, reloadFailed, onDiscardRequested, scheduling, restoreWarnings, }: FormConcurrencyNoticesProps) => ReactNode;
83
+ export declare const FormConcurrencyNotices: ({ disabled, mutationIssue, scheduledPublicationsNeedReconfirmation, scheduledPublicationsHref, warningRef, discarding, reloadFailed, onDiscardRequested, scheduling, restoreWarnings, unavailableBlocks, }: FormConcurrencyNoticesProps) => ReactNode;
83
84
  export interface FormSidebarWidgetsProps {
84
85
  /** Mutations blocked or a discard in flight. */
85
86
  disabled: boolean;
@@ -132,7 +132,7 @@ const FormStatusBar = ({ disabled, initialData, workflowStatuses, publishedVersi
132
132
  ]
133
133
  });
134
134
  };
135
- const FormConcurrencyNotices = ({ disabled, mutationIssue, scheduledPublicationsNeedReconfirmation, scheduledPublicationsHref, warningRef, discarding, reloadFailed, onDiscardRequested, scheduling, restoreWarnings })=>{
135
+ const FormConcurrencyNotices = ({ disabled, mutationIssue, scheduledPublicationsNeedReconfirmation, scheduledPublicationsHref, warningRef, discarding, reloadFailed, onDiscardRequested, scheduling, restoreWarnings, unavailableBlocks })=>{
136
136
  const { t } = useTranslation('byline-admin');
137
137
  return /*#__PURE__*/ jsxs(Fragment, {
138
138
  children: [
@@ -189,6 +189,18 @@ const FormConcurrencyNotices = ({ disabled, mutationIssue, scheduledPublications
189
189
  onCancel: scheduling.cancel
190
190
  }),
191
191
  scheduling.modal,
192
+ unavailableBlocks && unavailableBlocks.length > 0 && /*#__PURE__*/ jsx(Alert, {
193
+ className: "m-0 mt-4",
194
+ intent: "warning",
195
+ icon: true,
196
+ close: false,
197
+ title: t('forms.unavailableBlocks.title'),
198
+ children: /*#__PURE__*/ jsx("p", {
199
+ children: t('forms.unavailableBlocks.body', {
200
+ count: unavailableBlocks.length
201
+ })
202
+ })
203
+ }),
192
204
  restoreWarnings && restoreWarnings.length > 0 && /*#__PURE__*/ jsxs(Alert, {
193
205
  className: "m-0 mt-4",
194
206
  intent: "warning",
@@ -6,7 +6,7 @@
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
8
  import { type ReactNode } from 'react';
9
- import type { AdminResourceConfig, Field, WorkflowStatus } from '@byline/core';
9
+ import type { AdminResourceConfig, Field, UnavailableBlock, WorkflowStatus } from '@byline/core';
10
10
  import { type ScheduledPublicationInfo, type SchedulePublicationInput } from './scheduled-publication-control.js';
11
11
  import { type SystemFieldsSubmitPayload } from './use-form-submission.js';
12
12
  import type { DocumentActionsLocaleOption } from './document-actions.js';
@@ -142,6 +142,7 @@ export interface FormRendererProps {
142
142
  * them with the new shape.
143
143
  */
144
144
  restoreWarnings?: string[];
145
+ unavailableBlocks?: UnavailableBlock[];
145
146
  /**
146
147
  * Default content locale used when no `initialLocale` is supplied and as the
147
148
  * fallback inside `PathWidget`. Hosts typically pass their app-wide
@@ -13,9 +13,10 @@ import form_renderer_module from "./form-renderer.module.js";
13
13
  import { useNavigationGuardAdapter } from "./navigation-guard.js";
14
14
  import { useScheduledPublication } from "./scheduled-publication-control.js";
15
15
  import { computeStatusTransitions } from "./status-transitions.js";
16
+ import { UnavailableBlocksProvider } from "./unavailable-blocks.js";
16
17
  import { useDocumentReloadRecovery } from "./use-document-reload-recovery.js";
17
18
  import { useFormSubmission } from "./use-form-submission.js";
18
- const FormContent = ({ mode, mutationIssue, mutationsBlocked = false, observedRevision, onMutationError, onTreeMutationCommitted, scheduledPublicationsNeedReconfirmation = false, scheduledPublicationsHref, onReloadDocument, fields, onSubmit, onCancel, onStatusChange, onUnpublish, scheduledPublication, onSchedulePublication, onConfirmScheduledPublication, onCancelScheduledPublication, onDelete, onDuplicate, onCopyToLocale, onDeleteLocale, contentLocales, nextStatus, workflowStatuses, publishedVersion, initialData, adminConfig, useAsTitle, useAsPath, showPath = true, heading, advertiseLocales, tree, headingLabel, headerSlot, collectionPath, initialLocale, onLocaleChange, defaultLocale = 'en', useNavigationGuard: useNavigationGuardProp, restoreWarnings, _activeTabBySet, _onTabChange })=>{
19
+ const FormContent = ({ mode, mutationIssue, mutationsBlocked = false, observedRevision, onMutationError, onTreeMutationCommitted, scheduledPublicationsNeedReconfirmation = false, scheduledPublicationsHref, onReloadDocument, fields, onSubmit, onCancel, onStatusChange, onUnpublish, scheduledPublication, onSchedulePublication, onConfirmScheduledPublication, onCancelScheduledPublication, onDelete, onDuplicate, onCopyToLocale, onDeleteLocale, contentLocales, nextStatus, workflowStatuses, publishedVersion, initialData, adminConfig, useAsTitle, useAsPath, showPath = true, heading, advertiseLocales, tree, headingLabel, headerSlot, collectionPath, initialLocale, onLocaleChange, defaultLocale = 'en', useNavigationGuard: useNavigationGuardProp, restoreWarnings, unavailableBlocks, _activeTabBySet, _onTabChange })=>{
19
20
  const { hasChanges: hasChangesFn, subscribeMeta } = useFormContext();
20
21
  const { t } = useTranslation('byline-admin');
21
22
  const [hasChanges, setHasChanges] = useState(hasChangesFn());
@@ -177,7 +178,8 @@ const FormContent = ({ mode, mutationIssue, mutationsBlocked = false, observedRe
177
178
  reloadFailed: reloadFailed,
178
179
  onDiscardRequested: requestDiscard,
179
180
  scheduling: scheduling,
180
- restoreWarnings: restoreWarnings
181
+ restoreWarnings: restoreWarnings,
182
+ unavailableBlocks: unavailableBlocks
181
183
  }),
182
184
  /*#__PURE__*/ jsx(FormLayout, {
183
185
  fields: fields,
@@ -233,20 +235,24 @@ const FormContent = ({ mode, mutationIssue, mutationsBlocked = false, observedRe
233
235
  const FormRenderer = (props)=>{
234
236
  const { mode, initialData, initialLocale, collectionPath } = props;
235
237
  const savedTabsRef = useRef({});
236
- return /*#__PURE__*/ jsx(FormProvider, {
237
- initialData: initialData,
238
- documentId: 'edit' === mode && 'string' == typeof initialData?.id ? initialData.id : null,
239
- collectionPath: collectionPath ?? null,
240
- children: /*#__PURE__*/ jsx(FormContent, {
241
- ...props,
242
- _activeTabBySet: savedTabsRef.current,
243
- _onTabChange: (tabSetName, tabName)=>{
244
- savedTabsRef.current = {
245
- ...savedTabsRef.current,
246
- [tabSetName]: tabName
247
- };
248
- }
249
- })
250
- }, `${initialLocale ?? 'default'}-${initialData?.versionId ?? ''}`);
238
+ return /*#__PURE__*/ jsx(UnavailableBlocksProvider, {
239
+ issues: props.unavailableBlocks,
240
+ fields: initialData?.fields ?? initialData ?? {},
241
+ children: /*#__PURE__*/ jsx(FormProvider, {
242
+ initialData: initialData,
243
+ documentId: 'edit' === mode && 'string' == typeof initialData?.id ? initialData.id : null,
244
+ collectionPath: collectionPath ?? null,
245
+ children: /*#__PURE__*/ jsx(FormContent, {
246
+ ...props,
247
+ _activeTabBySet: savedTabsRef.current,
248
+ _onTabChange: (tabSetName, tabName)=>{
249
+ savedTabsRef.current = {
250
+ ...savedTabsRef.current,
251
+ [tabSetName]: tabName
252
+ };
253
+ }
254
+ })
255
+ }, `${initialLocale ?? 'default'}-${initialData?.versionId ?? ''}`)
256
+ });
251
257
  };
252
258
  export { FormRenderer };
@@ -0,0 +1,15 @@
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 { ReactNode } from 'react';
9
+ import { type UnavailableBlock } from '@byline/core';
10
+ export declare function UnavailableBlocksProvider({ issues, fields, children, }: {
11
+ issues?: readonly UnavailableBlock[];
12
+ fields: Record<string, any>;
13
+ children: ReactNode;
14
+ }): import("react").JSX.Element;
15
+ export declare function useUnavailableBlocks(path: string): UnavailableBlock[];
@@ -0,0 +1,38 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useMemo } from "react";
3
+ import { parseInstancePath } from "@byline/core";
4
+ import { repeatingItemPath } from "./repeating-items.js";
5
+ const UnavailableBlocksContext = /*#__PURE__*/ createContext([]);
6
+ function bindUnavailableBlocks(issues, fields) {
7
+ return issues.map((issue)=>{
8
+ const parsed = parseInstancePath(issue.fieldPath);
9
+ if (!parsed.ok) return issue;
10
+ let value = fields;
11
+ let path = '';
12
+ for (const segment of parsed.segments)if ('field' === segment.kind) {
13
+ path = path ? `${path}.${segment.name}` : segment.name;
14
+ value = value?.[segment.name];
15
+ } else if ('index' === segment.kind) {
16
+ value = value?.[segment.index];
17
+ path = repeatingItemPath(path, value, segment.index);
18
+ }
19
+ return {
20
+ ...issue,
21
+ fieldPath: path
22
+ };
23
+ });
24
+ }
25
+ function UnavailableBlocksProvider({ issues, fields, children }) {
26
+ const bound = useMemo(()=>bindUnavailableBlocks(issues ?? [], fields), [
27
+ issues,
28
+ fields
29
+ ]);
30
+ return /*#__PURE__*/ jsx(UnavailableBlocksContext.Provider, {
31
+ value: bound,
32
+ children: children
33
+ });
34
+ }
35
+ function useUnavailableBlocks(path) {
36
+ return useContext(UnavailableBlocksContext).filter((issue)=>issue.fieldPath === path);
37
+ }
38
+ export { UnavailableBlocksProvider, useUnavailableBlocks };
@@ -182,7 +182,7 @@ function ChangeAccountPassword({ account, onClose, onSuccess }) {
182
182
  children: [
183
183
  /*#__PURE__*/ jsx(Button, {
184
184
  type: "button",
185
- intent: "secondary",
185
+ intent: "noeffect",
186
186
  size: "sm",
187
187
  onClick: onClose,
188
188
  className: clsx('byline-account-change-password-action', change_password_module.action),
@@ -5,6 +5,7 @@ import { LocalDateTime } from "@byline/admin/react";
5
5
  import { useTranslation } from "@byline/i18n/react";
6
6
  import { CloseIcon, Drawer, EditIcon, IconButton } from "@byline/ui/react";
7
7
  import clsx from "clsx";
8
+ import { PasskeyManager } from "../../auth/components/passkey-manager.js";
8
9
  import { ChangeAccountPassword } from "./change-password.js";
9
10
  import container_module from "./container.module.js";
10
11
  import { Preferences } from "./preferences.js";
@@ -208,6 +209,19 @@ function AccountSelfContainer({ account }) {
208
209
  /*#__PURE__*/ jsx("p", {
209
210
  className: clsx('muted', 'byline-account-status-help', container_module["status-help"]),
210
211
  children: t('account.preferences.help')
212
+ }),
213
+ /*#__PURE__*/ jsxs("div", {
214
+ className: clsx('byline-account-preferences-theme', container_module["preferences-theme"]),
215
+ children: [
216
+ /*#__PURE__*/ jsx("p", {
217
+ className: clsx('byline-account-line', container_module.line),
218
+ children: /*#__PURE__*/ jsx("span", {
219
+ className: "muted",
220
+ children: t('account.appearance.colorTheme')
221
+ })
222
+ }),
223
+ /*#__PURE__*/ jsx(ThemeSwitch, {})
224
+ ]
211
225
  })
212
226
  ]
213
227
  })
@@ -225,9 +239,10 @@ function AccountSelfContainer({ account }) {
225
239
  children: t('account.password.intro')
226
240
  })
227
241
  }),
228
- /*#__PURE__*/ jsx(ContainerSection, {
229
- title: t('account.sections.appearance'),
230
- children: /*#__PURE__*/ jsx(ThemeSwitch, {})
242
+ /*#__PURE__*/ jsx(PasskeyManager, {
243
+ targetUserId: currentAccount.id,
244
+ targetIsSuperAdmin: currentAccount.is_super_admin,
245
+ section: ContainerSection
231
246
  }),
232
247
  /*#__PURE__*/ jsxs(ContainerSection, {
233
248
  title: t('account.sections.status'),
@@ -15,6 +15,8 @@ const container_module = {
15
15
  statusOff: "status-off-WJAcBy",
16
16
  "status-help": "status-help-GVOlMV",
17
17
  statusHelp: "status-help-GVOlMV",
18
+ "preferences-theme": "preferences-theme-SHTZmr",
19
+ preferencesTheme: "preferences-theme-SHTZmr",
18
20
  "not-set": "not-set-Ek2yXe",
19
21
  notSet: "not-set-Ek2yXe",
20
22
  drawer: "drawer-dDlpic",
@@ -60,6 +60,13 @@
60
60
  margin-bottom: 0;
61
61
  }
62
62
 
63
+ :is(.preferences-theme-SHTZmr, .byline-account-preferences-theme) {
64
+ gap: var(--spacing-8);
65
+ margin-top: var(--spacing-20);
66
+ flex-direction: column;
67
+ display: flex;
68
+ }
69
+
63
70
  :is(.not-set-Ek2yXe, .byline-account-not-set) {
64
71
  font-style: italic;
65
72
  }
@@ -124,7 +124,7 @@ function Preferences({ account, onClose, onSuccess }) {
124
124
  children: [
125
125
  /*#__PURE__*/ jsx(Button, {
126
126
  type: "button",
127
- intent: "secondary",
127
+ intent: "noeffect",
128
128
  size: "sm",
129
129
  onClick: onClose,
130
130
  className: clsx('byline-account-preferences-action', preferences_module.action),
@@ -183,7 +183,7 @@ function UpdateAccount({ account, onClose, onSuccess }) {
183
183
  children: [
184
184
  /*#__PURE__*/ jsx(Button, {
185
185
  type: "button",
186
- intent: "secondary",
186
+ intent: "noeffect",
187
187
  size: "sm",
188
188
  onClick: onClose,
189
189
  className: clsx('byline-account-update-action', update_module.action),
@@ -144,7 +144,7 @@ function CreateAdminRole({ onClose, onSuccess }) {
144
144
  children: [
145
145
  /*#__PURE__*/ jsx(Button, {
146
146
  type: "button",
147
- intent: "secondary",
147
+ intent: "noeffect",
148
148
  size: "sm",
149
149
  onClick: onClose,
150
150
  className: clsx('byline-role-create-action', create_module.action),
@@ -207,7 +207,7 @@ function RolePermissions({ role, registered, initialAbilities, onSaved }) {
207
207
  children: [
208
208
  /*#__PURE__*/ jsx(Button, {
209
209
  type: "button",
210
- intent: "secondary",
210
+ intent: "noeffect",
211
211
  size: "xs",
212
212
  onClick: handleCancel,
213
213
  disabled: saving,
@@ -134,7 +134,7 @@ function UpdateRole({ role, onClose, onSuccess }) {
134
134
  children: [
135
135
  /*#__PURE__*/ jsx(Button, {
136
136
  type: "button",
137
- intent: "secondary",
137
+ intent: "noeffect",
138
138
  size: "sm",
139
139
  onClick: onClose,
140
140
  className: clsx('byline-role-update-action', update_module.action),
@@ -30,6 +30,7 @@ export declare const ADMIN_USERS_ABILITIES: {
30
30
  readonly update: 'admin.users.update';
31
31
  readonly delete: 'admin.users.delete';
32
32
  readonly changePassword: 'admin.users.changePassword';
33
+ readonly managePasskeys: 'admin.users.managePasskeys';
33
34
  };
34
35
  export type AdminUsersAbilityKey = (typeof ADMIN_USERS_ABILITIES)[keyof typeof ADMIN_USERS_ABILITIES];
35
36
  /**
@@ -3,9 +3,16 @@ const ADMIN_USERS_ABILITIES = {
3
3
  create: 'admin.users.create',
4
4
  update: 'admin.users.update',
5
5
  delete: 'admin.users.delete',
6
- changePassword: 'admin.users.changePassword'
6
+ changePassword: 'admin.users.changePassword',
7
+ managePasskeys: 'admin.users.managePasskeys'
7
8
  };
8
9
  function registerAdminUsersAbilities(registry) {
10
+ registry.register({
11
+ key: ADMIN_USERS_ABILITIES.managePasskeys,
12
+ label: 'Manage admin user passkeys',
13
+ group: 'admin.users',
14
+ source: 'admin'
15
+ });
9
16
  registry.register({
10
17
  key: ADMIN_USERS_ABILITIES.read,
11
18
  label: 'Read admin users',
@@ -239,7 +239,7 @@ function CreateAdminUser({ onClose, onSuccess }) {
239
239
  children: [
240
240
  /*#__PURE__*/ jsx(Button, {
241
241
  type: "button",
242
- intent: "secondary",
242
+ intent: "noeffect",
243
243
  size: "sm",
244
244
  onClick: onClose,
245
245
  className: clsx('byline-user-create-action', create_module.action),
@@ -113,7 +113,7 @@ function UserRoles({ user, allRoles, initialRoleIds, onClose, onSaved }) {
113
113
  children: [
114
114
  /*#__PURE__*/ jsx(Button, {
115
115
  type: "button",
116
- intent: "secondary",
116
+ intent: "noeffect",
117
117
  size: "xs",
118
118
  onClick: onClose,
119
119
  disabled: saving,
@@ -130,7 +130,7 @@ function SetPassword({ user, onClose, onSuccess }) {
130
130
  children: [
131
131
  /*#__PURE__*/ jsx(Button, {
132
132
  type: "button",
133
- intent: "secondary",
133
+ intent: "noeffect",
134
134
  size: "sm",
135
135
  onClick: onClose,
136
136
  className: clsx('byline-user-set-password-action', set_password_module.action),
@@ -230,7 +230,7 @@ function UpdateUser({ user, onClose, onSuccess }) {
230
230
  children: [
231
231
  /*#__PURE__*/ jsx(Button, {
232
232
  type: "button",
233
- intent: "secondary",
233
+ intent: "noeffect",
234
234
  size: "sm",
235
235
  onClick: onClose,
236
236
  className: clsx('byline-user-update-action', update_module.action),
@@ -0,0 +1,31 @@
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
+ /** Bounded housekeeping; expiry and revocation remain independent authorization checks. */
9
+ export interface AuthMaintenanceRepository {
10
+ /** Remove at most 100 attempts expired or consumed on/before the supplied grace cutoff. */
11
+ purgeAttempts(before: Date): Promise<number>;
12
+ /** Candidate only: eligibility must be checked again under its owning account lock. */
13
+ findTerminalLogin(before: Date): Promise<{
14
+ id: string;
15
+ adminUserId: string;
16
+ } | null>;
17
+ /**
18
+ * Requires the owning account lock. Re-read eligibility, delete at most 100 refresh
19
+ * children, then delete the terminal parent only if no children remain. Never cascade.
20
+ */
21
+ pruneTerminalLogin(input: {
22
+ id: string;
23
+ adminUserId: string;
24
+ before: Date;
25
+ }): Promise<{
26
+ refreshDeleted: number;
27
+ loginDeleted: boolean;
28
+ }>;
29
+ /** Remove at most 100 legacy null-sid rows expired on/before the retention cutoff. */
30
+ purgeLegacyRefreshTokens(before: Date): Promise<number>;
31
+ }
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,11 @@
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 { RecurringTaskDefinition } from '@byline/core';
9
+ import type { AdminStore } from '../../store.js';
10
+ /** Register for native sessions even on password-only sites; configuration never runs cleanup. */
11
+ export declare function createAuthMaintenanceTasks(store: AdminStore, now?: () => Date): RecurringTaskDefinition[];
@@ -0,0 +1,39 @@
1
+ const DAY = 86400000;
2
+ function createAuthMaintenanceTasks(store, now = ()=>new Date()) {
3
+ const task = (name, retention, batch)=>({
4
+ name: `auth.maintenance.${name}`,
5
+ intervalMs: 60000,
6
+ leaseMs: 60000,
7
+ async run (context) {
8
+ const started = performance.now();
9
+ const cutoff = new Date(now().getTime() - retention);
10
+ for(let n = 0; n < 32 && performance.now() - started < 30000; n++){
11
+ context.signal.throwIfAborted();
12
+ await context.heartbeat();
13
+ context.signal.throwIfAborted();
14
+ if (!await batch(cutoff)) return {
15
+ workRemaining: false
16
+ };
17
+ }
18
+ return {
19
+ workRemaining: true
20
+ };
21
+ }
22
+ });
23
+ return [
24
+ task('attempts', 300000, async (cutoff)=>await store.authMaintenance.purgeAttempts(cutoff) === 100),
25
+ task('logins', 30 * DAY, async (cutoff)=>{
26
+ const candidate = await store.authMaintenance.findTerminalLogin(cutoff);
27
+ if (!candidate) return false;
28
+ await store.withSessionLock(candidate.adminUserId, async (scoped)=>{
29
+ await scoped.authMaintenance.pruneTerminalLogin({
30
+ ...candidate,
31
+ before: cutoff
32
+ });
33
+ });
34
+ return true;
35
+ }),
36
+ task('legacy-refresh', 30 * DAY, async (cutoff)=>await store.authMaintenance.purgeLegacyRefreshTokens(cutoff) === 100)
37
+ ];
38
+ }
39
+ export { createAuthMaintenanceTasks };
@@ -0,0 +1 @@
1
+ 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 { PasskeyManagementAction } from '@byline/auth';
9
+ import type { PasskeyAdminServices, PasskeyPageBinding } from '../../../services/admin-services-types.js';
10
+ export declare function signInWithPasskey(services: PasskeyAdminServices, isActive?: () => boolean): Promise<unknown>;
11
+ export declare function provePasskeyAction(services: PasskeyAdminServices, binding: PasskeyPageBinding, action: PasskeyManagementAction, password?: string): Promise<import("@byline/auth").PasskeyManagementGrant>;
12
+ export declare function enrollPasskey(services: PasskeyAdminServices, binding: PasskeyPageBinding, name: string, password?: string): Promise<import("@byline/auth").PasskeyCredentialSummary>;
13
+ /** Render fixed translated messages, never provider payloads or authenticator identifiers. */
14
+ export declare function passkeyErrorKey(error: unknown): string;