@actuate-media/cms-admin 0.67.0 → 0.68.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 (146) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/AdminRoot.d.ts.map +1 -1
  3. package/dist/AdminRoot.js +29 -5
  4. package/dist/AdminRoot.js.map +1 -1
  5. package/dist/__tests__/hooks/useIdleLogout.test.d.ts +2 -0
  6. package/dist/__tests__/hooks/useIdleLogout.test.d.ts.map +1 -0
  7. package/dist/__tests__/hooks/useIdleLogout.test.js +64 -0
  8. package/dist/__tests__/hooks/useIdleLogout.test.js.map +1 -0
  9. package/dist/__tests__/lib/post-types-service.test.d.ts +2 -0
  10. package/dist/__tests__/lib/post-types-service.test.d.ts.map +1 -0
  11. package/dist/__tests__/lib/post-types-service.test.js +88 -0
  12. package/dist/__tests__/lib/post-types-service.test.js.map +1 -0
  13. package/dist/__tests__/views/collection-list-nav.render.test.js +43 -5
  14. package/dist/__tests__/views/collection-list-nav.render.test.js.map +1 -1
  15. package/dist/__tests__/views/forms-list.render.test.js +25 -6
  16. package/dist/__tests__/views/forms-list.render.test.js.map +1 -1
  17. package/dist/__tests__/views/page-builder-sections-panel.render.test.d.ts +2 -0
  18. package/dist/__tests__/views/page-builder-sections-panel.render.test.d.ts.map +1 -0
  19. package/dist/__tests__/views/page-builder-sections-panel.render.test.js +121 -0
  20. package/dist/__tests__/views/page-builder-sections-panel.render.test.js.map +1 -0
  21. package/dist/__tests__/views/page-templates.render.test.d.ts +2 -0
  22. package/dist/__tests__/views/page-templates.render.test.d.ts.map +1 -0
  23. package/dist/__tests__/views/page-templates.render.test.js +105 -0
  24. package/dist/__tests__/views/page-templates.render.test.js.map +1 -0
  25. package/dist/__tests__/views/post-type-editor.render.test.d.ts +2 -0
  26. package/dist/__tests__/views/post-type-editor.render.test.d.ts.map +1 -0
  27. package/dist/__tests__/views/post-type-editor.render.test.js +102 -0
  28. package/dist/__tests__/views/post-type-editor.render.test.js.map +1 -0
  29. package/dist/__tests__/views/security-settings.render.test.js +85 -4
  30. package/dist/__tests__/views/security-settings.render.test.js.map +1 -1
  31. package/dist/__tests__/views/seo-content-pane.render.test.d.ts +2 -0
  32. package/dist/__tests__/views/seo-content-pane.render.test.d.ts.map +1 -0
  33. package/dist/__tests__/views/seo-content-pane.render.test.js +117 -0
  34. package/dist/__tests__/views/seo-content-pane.render.test.js.map +1 -0
  35. package/dist/actuate-admin.css +1 -1
  36. package/dist/components/collections/EntryDetailPane.d.ts +10 -0
  37. package/dist/components/collections/EntryDetailPane.d.ts.map +1 -0
  38. package/dist/components/collections/EntryDetailPane.js +92 -0
  39. package/dist/components/collections/EntryDetailPane.js.map +1 -0
  40. package/dist/components/forms/FieldsPanel.js +1 -1
  41. package/dist/components/forms/FieldsPanel.js.map +1 -1
  42. package/dist/components/forms/FormSchemaPane.d.ts +8 -0
  43. package/dist/components/forms/FormSchemaPane.d.ts.map +1 -0
  44. package/dist/components/forms/FormSchemaPane.js +95 -0
  45. package/dist/components/forms/FormSchemaPane.js.map +1 -0
  46. package/dist/components/seo/SeoEditorPane.d.ts +9 -0
  47. package/dist/components/seo/SeoEditorPane.d.ts.map +1 -0
  48. package/dist/components/seo/SeoEditorPane.js +175 -0
  49. package/dist/components/seo/SeoEditorPane.js.map +1 -0
  50. package/dist/components/ui/Badge.d.ts +1 -1
  51. package/dist/components/ui/Button.d.ts +1 -1
  52. package/dist/components/ui/StatusBadge.d.ts +1 -1
  53. package/dist/hooks/useIdleLogout.d.ts +18 -0
  54. package/dist/hooks/useIdleLogout.d.ts.map +1 -0
  55. package/dist/hooks/useIdleLogout.js +66 -0
  56. package/dist/hooks/useIdleLogout.js.map +1 -0
  57. package/dist/lib/api.d.ts +1 -0
  58. package/dist/lib/api.d.ts.map +1 -1
  59. package/dist/lib/api.js +7 -1
  60. package/dist/lib/api.js.map +1 -1
  61. package/dist/lib/post-types-service.d.ts +44 -0
  62. package/dist/lib/post-types-service.d.ts.map +1 -0
  63. package/dist/lib/post-types-service.js +83 -0
  64. package/dist/lib/post-types-service.js.map +1 -0
  65. package/dist/views/CollectionList.d.ts.map +1 -1
  66. package/dist/views/CollectionList.js +23 -4
  67. package/dist/views/CollectionList.js.map +1 -1
  68. package/dist/views/Forms.d.ts.map +1 -1
  69. package/dist/views/Forms.js +25 -21
  70. package/dist/views/Forms.js.map +1 -1
  71. package/dist/views/Posts/PostTypeEditor.d.ts +16 -0
  72. package/dist/views/Posts/PostTypeEditor.d.ts.map +1 -0
  73. package/dist/views/Posts/PostTypeEditor.js +159 -0
  74. package/dist/views/Posts/PostTypeEditor.js.map +1 -0
  75. package/dist/views/Posts/PostTypesView.d.ts +3 -2
  76. package/dist/views/Posts/PostTypesView.d.ts.map +1 -1
  77. package/dist/views/Posts/PostTypesView.js +1 -1
  78. package/dist/views/Posts/PostTypesView.js.map +1 -1
  79. package/dist/views/page-builder/BuilderSectionsPanel.d.ts +20 -0
  80. package/dist/views/page-builder/BuilderSectionsPanel.d.ts.map +1 -0
  81. package/dist/views/page-builder/BuilderSectionsPanel.js +105 -0
  82. package/dist/views/page-builder/BuilderSectionsPanel.js.map +1 -0
  83. package/dist/views/page-builder/PageBuilder.d.ts.map +1 -1
  84. package/dist/views/page-builder/PageBuilder.js +51 -2
  85. package/dist/views/page-builder/PageBuilder.js.map +1 -1
  86. package/dist/views/page-builder/PageTemplates.d.ts.map +1 -1
  87. package/dist/views/page-builder/PageTemplates.js +113 -6
  88. package/dist/views/page-builder/PageTemplates.js.map +1 -1
  89. package/dist/views/seo/ContentTab.d.ts.map +1 -1
  90. package/dist/views/seo/ContentTab.js +22 -6
  91. package/dist/views/seo/ContentTab.js.map +1 -1
  92. package/dist/views/settings/AccessProtectionCard.d.ts +5 -1
  93. package/dist/views/settings/AccessProtectionCard.d.ts.map +1 -1
  94. package/dist/views/settings/AccessProtectionCard.js +13 -10
  95. package/dist/views/settings/AccessProtectionCard.js.map +1 -1
  96. package/dist/views/settings/SecurityTab.d.ts.map +1 -1
  97. package/dist/views/settings/SecurityTab.js +3 -3
  98. package/dist/views/settings/SecurityTab.js.map +1 -1
  99. package/dist/views/settings/SessionPolicyCard.d.ts +1 -2
  100. package/dist/views/settings/SessionPolicyCard.d.ts.map +1 -1
  101. package/dist/views/settings/SessionPolicyCard.js +12 -5
  102. package/dist/views/settings/SessionPolicyCard.js.map +1 -1
  103. package/dist/views/settings/useSecuritySettings.d.ts +13 -1
  104. package/dist/views/settings/useSecuritySettings.d.ts.map +1 -1
  105. package/dist/views/settings/useSecuritySettings.js +47 -4
  106. package/dist/views/settings/useSecuritySettings.js.map +1 -1
  107. package/package.json +2 -2
  108. package/src/AdminRoot.tsx +36 -7
  109. package/src/__tests__/hooks/useIdleLogout.test.tsx +86 -0
  110. package/src/__tests__/lib/post-types-service.test.ts +112 -0
  111. package/src/__tests__/views/collection-list-nav.render.test.tsx +51 -5
  112. package/src/__tests__/views/forms-list.render.test.tsx +34 -6
  113. package/src/__tests__/views/page-builder-sections-panel.render.test.tsx +138 -0
  114. package/src/__tests__/views/page-templates.render.test.tsx +142 -0
  115. package/src/__tests__/views/post-type-editor.render.test.tsx +116 -0
  116. package/src/__tests__/views/security-settings.render.test.tsx +111 -4
  117. package/src/__tests__/views/seo-content-pane.render.test.tsx +142 -0
  118. package/src/components/collections/EntryDetailPane.tsx +182 -0
  119. package/src/components/forms/FieldsPanel.tsx +3 -1
  120. package/src/components/forms/FormSchemaPane.tsx +192 -0
  121. package/src/components/seo/SeoEditorPane.tsx +533 -0
  122. package/src/hooks/useIdleLogout.ts +77 -0
  123. package/src/lib/api.ts +8 -2
  124. package/src/lib/post-types-service.ts +129 -0
  125. package/src/views/CollectionList.tsx +158 -110
  126. package/src/views/Forms.tsx +154 -126
  127. package/src/views/Posts/PostTypeEditor.tsx +568 -0
  128. package/src/views/Posts/PostTypesView.tsx +5 -4
  129. package/src/views/page-builder/BuilderSectionsPanel.tsx +304 -0
  130. package/src/views/page-builder/PageBuilder.tsx +69 -2
  131. package/src/views/page-builder/PageTemplates.tsx +340 -57
  132. package/src/views/seo/ContentTab.tsx +113 -80
  133. package/src/views/settings/AccessProtectionCard.tsx +83 -53
  134. package/src/views/settings/SecurityTab.tsx +23 -2
  135. package/src/views/settings/SessionPolicyCard.tsx +45 -9
  136. package/src/views/settings/useSecuritySettings.ts +78 -22
  137. package/dist/components/forms/FormSchemaDrawer.d.ts +0 -9
  138. package/dist/components/forms/FormSchemaDrawer.d.ts.map +0 -1
  139. package/dist/components/forms/FormSchemaDrawer.js +0 -99
  140. package/dist/components/forms/FormSchemaDrawer.js.map +0 -1
  141. package/dist/components/seo/SeoEditorDrawer.d.ts +0 -8
  142. package/dist/components/seo/SeoEditorDrawer.d.ts.map +0 -1
  143. package/dist/components/seo/SeoEditorDrawer.js +0 -172
  144. package/dist/components/seo/SeoEditorDrawer.js.map +0 -1
  145. package/src/components/forms/FormSchemaDrawer.tsx +0 -192
  146. package/src/components/seo/SeoEditorDrawer.tsx +0 -529
@@ -1 +1 @@
1
- {"version":3,"file":"SessionPolicyCard.js","sourceRoot":"","sources":["../../../src/views/settings/SessionPolicyCard.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"SessionPolicyCard.js","sourceRoot":"","sources":["../../../src/views/settings/SessionPolicyCard.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,EACL,8BAA8B,EAC9B,6BAA6B,GAC9B,MAAM,yCAAyC,CAAA;AAChD,OAAO,EAAE,6BAA6B,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAClF,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AAGjE,SAAS,aAAa,CAAC,CAAS;IAC9B,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;AAC1C,CAAC;AAED,SAAS,YAAY,CAAC,KAAa;IACjC,IAAI,KAAK,KAAK,CAAC;QAAE,OAAO,QAAQ,CAAA;IAChC,IAAI,KAAK,KAAK,GAAG;QAAE,OAAO,QAAQ,CAAA;IAClC,OAAO,GAAG,KAAK,QAAQ,CAAA;AACzB,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,EAChC,IAAI,EACJ,QAAQ,EACR,OAAO,EACP,cAAc,EACd,WAAW,EACX,WAAW,GAQZ;IACC,MAAM,SAAS,GAAG,KAAK,EAAE,CAAA;IACzB,MAAM,KAAK,GAAG,KAAK,EAAE,CAAA;IACrB,MAAM,SAAS,GAAG,KAAK,EAAE,CAAA;IACzB,MAAM,CAAC,aAAa,EAAE,gBAAgB,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAEzD,OAAO,CACL,MAAC,YAAY,IACX,SAAS,EAAE,SAAS,EACpB,KAAK,EAAC,UAAU,EAChB,WAAW,EAAC,qFAAqF,aAEjG,eAAK,SAAS,EAAC,WAAW,aACxB,eAAK,SAAS,EAAC,yCAAyC,aACtD,iBAAO,OAAO,EAAE,SAAS,EAAE,SAAS,EAAC,QAAQ,aAC3C,eAAM,SAAS,EAAC,uCAAuC,gCAAuB,EAC9E,YAAG,SAAS,EAAC,sCAAsC,sFAE/C,IACE,EACR,iBACE,EAAE,EAAE,SAAS,EACb,KAAK,EAAE,IAAI,CAAC,YAAY,EACxB,QAAQ,EAAE,CAAC,OAAO,EAClB,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EACjE,SAAS,EAAC,+MAA+M,YAExN,6BAA6B,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CACxC,iBAAgB,KAAK,EAAE,CAAC,YACrB,YAAY,CAAC,CAAC,CAAC,IADL,CAAC,CAEL,CACV,CAAC,GACK,IACL,EAEN,KAAC,iBAAiB,IAChB,KAAK,EAAC,kBAAkB,EACxB,WAAW,EAAC,mEAAmE,EAC/E,OAAO,EAAE,IAAI,CAAC,iBAAiB,EAC/B,QAAQ,EAAE,CAAC,OAAO,EAClB,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,mBAAmB,EAAE,KAAK,CAAC,GACzD,EAEF,eAAK,SAAS,EAAC,yCAAyC,aACtD,iBAAO,OAAO,EAAE,KAAK,EAAE,SAAS,EAAC,QAAQ,aACvC,eAAM,SAAS,EAAC,uCAAuC,4CAEhD,EACP,YAAG,SAAS,EAAC,sCAAsC,8GAG/C,IACE,EACR,iBACE,EAAE,EAAE,KAAK,EACT,KAAK,EAAE,IAAI,CAAC,qBAAqB,EACjC,QAAQ,EAAE,CAAC,OAAO,EAClB,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,uBAAuB,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAC1E,SAAS,EAAC,+MAA+M,YAExN,8BAA8B,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CACzC,iBAAgB,KAAK,EAAE,CAAC,YACrB,aAAa,CAAC,CAAC,CAAC,IADN,CAAC,CAEL,CACV,CAAC,GACK,IACL,IACF,EAEN,eAAK,SAAS,EAAC,4CAA4C,aACzD,KAAC,SAAS,IAAC,MAAM,EAAC,MAAM,EAAC,KAAK,EAAC,uBAAuB,EAAC,KAAK,EAAE,MAAM,CAAC,cAAc,CAAC,GAAI,EACxF,KAAC,SAAS,IACR,MAAM,EAAC,MAAM,EACb,KAAK,EAAC,qBAAqB,EAC3B,KAAK,EAAE,YAAY,CAAC,IAAI,CAAC,YAAY,CAAC,GACtC,EACF,KAAC,SAAS,IAAC,MAAM,EAAC,MAAM,EAAC,KAAK,EAAC,oCAAoC,EAAC,KAAK,EAAC,SAAS,GAAG,IAClF,EAEN,eAAK,SAAS,EAAC,0EAA0E,aACvF,0BACE,YAAG,SAAS,EAAC,qCAAqC,sCAA0B,EAC5E,YAAG,SAAS,EAAC,+BAA+B,6FAExC,EACH,WAAW,KAAK,MAAM,IAAI,CACzB,YAAG,SAAS,EAAC,2BAA2B,eAAW,QAAQ,wCAEvD,CACL,EACA,WAAW,KAAK,OAAO,IAAI,CAC1B,YAAG,SAAS,EAAC,+BAA+B,eAAW,QAAQ,sDAE3D,CACL,IACG,EACN,kBACE,IAAI,EAAC,QAAQ,EACb,QAAQ,EAAE,CAAC,OAAO,IAAI,WAAW,KAAK,SAAS,EAC/C,OAAO,EAAE,GAAG,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,EACrC,SAAS,EAAC,yMAAyM,aAElN,WAAW,KAAK,SAAS,IAAI,CAC5B,KAAC,OAAO,IAAC,IAAI,EAAE,EAAE,EAAE,SAAS,EAAC,cAAc,iBAAa,MAAM,GAAG,CAClE,6BAEM,IACL,EAEN,KAAC,6BAA6B,IAC5B,IAAI,EAAE,aAAa,EACnB,KAAK,EAAC,4BAA4B,EAClC,WAAW,EAAC,+IAA+I,EAC3J,YAAY,EAAC,iBAAiB,EAC9B,SAAS,EAAE,GAAG,EAAE;oBACd,gBAAgB,CAAC,KAAK,CAAC,CAAA;oBACvB,WAAW,EAAE,CAAA;gBACf,CAAC,EACD,QAAQ,EAAE,GAAG,EAAE,CAAC,gBAAgB,CAAC,KAAK,CAAC,GACvC,IACW,CAChB,CAAA;AACH,CAAC"}
@@ -5,8 +5,15 @@ export interface SecurityForm {
5
5
  mfaRequired: boolean;
6
6
  mfaGracePeriodDays: number;
7
7
  maxConcurrentSessions: number;
8
+ timeoutHours: number;
9
+ idleLogoutEnabled: boolean;
10
+ /** Raw textarea contents — one IP/CIDR entry per line. */
11
+ ipAllowlistText: string;
12
+ auditLoggingEnabled: boolean;
8
13
  }
9
14
  type SaveState = 'idle' | 'saving' | 'success' | 'error';
15
+ /** Split the textarea into trimmed, de-duplicated entries (one per line). */
16
+ export declare function parseAllowlistText(text: string): string[];
10
17
  export interface UseSecuritySettings {
11
18
  loading: boolean;
12
19
  loadError: string | null;
@@ -19,9 +26,14 @@ export interface UseSecuritySettings {
19
26
  canEdit: boolean;
20
27
  saveState: SaveState;
21
28
  saveError: string | null;
22
- save: () => Promise<void>;
29
+ save: (options?: {
30
+ confirmLockout?: boolean;
31
+ }) => Promise<void>;
23
32
  reset: () => void;
24
33
  refetch: () => void;
34
+ /** Server rejected the save with IP_LOCKOUT_RISK — message to confirm. */
35
+ lockoutPrompt: string | null;
36
+ dismissLockoutPrompt: () => void;
25
37
  revokeState: 'idle' | 'working' | 'done' | 'error';
26
38
  revokeAllOtherSessions: () => Promise<number | null>;
27
39
  }
@@ -1 +1 @@
1
- {"version":3,"file":"useSecuritySettings.d.ts","sourceRoot":"","sources":["../../../src/views/settings/useSecuritySettings.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,kBAAkB,EAExB,MAAM,yCAAyC,CAAA;AAEhD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAE9D,yEAAyE;AACzE,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,OAAO,CAAA;IACpB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,qBAAqB,EAAE,MAAM,CAAA;CAC9B;AAED,KAAK,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAA;AAuBxD,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,IAAI,EAAE,kBAAkB,GAAG,IAAI,CAAA;IAC/B,IAAI,EAAE,YAAY,CAAA;IAClB,QAAQ,EAAE,CAAC,CAAC,SAAS,MAAM,YAAY,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,KAAK,IAAI,CAAA;IAChF,KAAK,EAAE,OAAO,CAAA;IACd,MAAM,EAAE,eAAe,EAAE,CAAA;IACzB,SAAS,EAAE,OAAO,CAAA;IAClB,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,EAAE,SAAS,CAAA;IACpB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IACzB,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,CAAA;IAClD,sBAAsB,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;CACrD;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,mBAAmB,CAiIzE"}
1
+ {"version":3,"file":"useSecuritySettings.d.ts","sourceRoot":"","sources":["../../../src/views/settings/useSecuritySettings.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,kBAAkB,EAExB,MAAM,yCAAyC,CAAA;AAEhD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAE9D,yEAAyE;AACzE,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,OAAO,CAAA;IACpB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,qBAAqB,EAAE,MAAM,CAAA;IAC7B,YAAY,EAAE,MAAM,CAAA;IACpB,iBAAiB,EAAE,OAAO,CAAA;IAC1B,0DAA0D;IAC1D,eAAe,EAAE,MAAM,CAAA;IACvB,mBAAmB,EAAE,OAAO,CAAA;CAC7B;AAED,KAAK,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAA;AAYxD,6EAA6E;AAC7E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAOzD;AA6BD,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,IAAI,EAAE,kBAAkB,GAAG,IAAI,CAAA;IAC/B,IAAI,EAAE,YAAY,CAAA;IAClB,QAAQ,EAAE,CAAC,CAAC,SAAS,MAAM,YAAY,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,KAAK,IAAI,CAAA;IAChF,KAAK,EAAE,OAAO,CAAA;IACd,MAAM,EAAE,eAAe,EAAE,CAAA;IACzB,SAAS,EAAE,OAAO,CAAA;IAClB,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,EAAE,SAAS,CAAA;IACpB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,IAAI,EAAE,CAAC,OAAO,CAAC,EAAE;QAAE,cAAc,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/D,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,0EAA0E;IAC1E,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,oBAAoB,EAAE,MAAM,IAAI,CAAA;IAChC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,CAAA;IAClD,sBAAsB,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;CACrD;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,mBAAmB,CAuJzE"}
@@ -6,18 +6,44 @@ const EMPTY_FORM = {
6
6
  mfaRequired: false,
7
7
  mfaGracePeriodDays: 7,
8
8
  maxConcurrentSessions: 5,
9
+ timeoutHours: 168,
10
+ idleLogoutEnabled: false,
11
+ ipAllowlistText: '',
12
+ auditLoggingEnabled: true,
9
13
  };
14
+ /** Split the textarea into trimmed, de-duplicated entries (one per line). */
15
+ export function parseAllowlistText(text) {
16
+ const out = [];
17
+ for (const line of text.split('\n')) {
18
+ const entry = line.trim();
19
+ if (entry && !out.includes(entry))
20
+ out.push(entry);
21
+ }
22
+ return out;
23
+ }
10
24
  function formFromSettings(s) {
11
25
  return {
12
26
  mfaRequired: s.mfa.required,
13
27
  mfaGracePeriodDays: s.mfa.gracePeriodDays,
14
28
  maxConcurrentSessions: s.session.maxConcurrentSessions,
29
+ timeoutHours: s.session.timeoutHours ?? 168,
30
+ idleLogoutEnabled: s.session.idleLogoutEnabled ?? false,
31
+ ipAllowlistText: (s.access?.ipAllowlist ?? []).join('\n'),
32
+ auditLoggingEnabled: s.access?.auditLoggingEnabled ?? true,
15
33
  };
16
34
  }
17
35
  function formToSettings(f) {
18
36
  return {
19
37
  mfa: { required: f.mfaRequired, gracePeriodDays: f.mfaGracePeriodDays },
20
- session: { maxConcurrentSessions: f.maxConcurrentSessions },
38
+ session: {
39
+ maxConcurrentSessions: f.maxConcurrentSessions,
40
+ timeoutHours: f.timeoutHours,
41
+ idleLogoutEnabled: f.idleLogoutEnabled,
42
+ },
43
+ access: {
44
+ ipAllowlist: parseAllowlistText(f.ipAllowlistText),
45
+ auditLoggingEnabled: f.auditLoggingEnabled,
46
+ },
21
47
  };
22
48
  }
23
49
  /**
@@ -35,6 +61,7 @@ export function useSecuritySettings(canEdit) {
35
61
  const [form, setForm] = useState(EMPTY_FORM);
36
62
  const [saveState, setSaveState] = useState('idle');
37
63
  const [saveError, setSaveError] = useState(null);
64
+ const [lockoutPrompt, setLockoutPrompt] = useState(null);
38
65
  const [revokeState, setRevokeState] = useState('idle');
39
66
  const [reloadKey, setReloadKey] = useState(0);
40
67
  useEffect(() => {
@@ -85,18 +112,30 @@ export function useSecuritySettings(canEdit) {
85
112
  return list;
86
113
  }, [form, baseline.mfaRequired, data]);
87
114
  const hasErrors = issues.some((i) => i.severity === 'error');
88
- const save = useCallback(async () => {
115
+ const save = useCallback(async (options) => {
89
116
  if (!canEdit || hasErrors || !dirty)
90
117
  return;
91
118
  setSaveState('saving');
92
119
  setSaveError(null);
120
+ setLockoutPrompt(null);
93
121
  try {
94
122
  const res = await cmsApi('/security', {
95
123
  method: 'PUT',
96
- body: JSON.stringify(formToSettings(form)),
124
+ body: JSON.stringify({
125
+ ...formToSettings(form),
126
+ ...(options?.confirmLockout ? { confirmLockout: true } : {}),
127
+ }),
97
128
  });
98
- if (res.error || !res.data)
129
+ // The server's lockout guard: surface a confirm dialog instead of a
130
+ // generic failure so the operator can retry with confirmLockout.
131
+ if (res.code === 'IP_LOCKOUT_RISK') {
132
+ setSaveState('idle');
133
+ setLockoutPrompt(res.error ?? 'This allowlist may lock you out. Save anyway?');
134
+ return;
135
+ }
136
+ if (res.error || !res.data) {
99
137
  throw new Error(res.error ?? 'Failed to save security settings.');
138
+ }
100
139
  setData(res.data);
101
140
  const f = formFromSettings(res.data.settings);
102
141
  setBaseline(f);
@@ -108,10 +147,12 @@ export function useSecuritySettings(canEdit) {
108
147
  setSaveError(err instanceof Error ? err.message : 'Failed to save security settings.');
109
148
  }
110
149
  }, [canEdit, hasErrors, dirty, form]);
150
+ const dismissLockoutPrompt = useCallback(() => setLockoutPrompt(null), []);
111
151
  const reset = useCallback(() => {
112
152
  setForm(baseline);
113
153
  setSaveState('idle');
114
154
  setSaveError(null);
155
+ setLockoutPrompt(null);
115
156
  }, [baseline]);
116
157
  const refetch = useCallback(() => setReloadKey((k) => k + 1), []);
117
158
  const revokeAllOtherSessions = useCallback(async () => {
@@ -147,6 +188,8 @@ export function useSecuritySettings(canEdit) {
147
188
  save,
148
189
  reset,
149
190
  refetch,
191
+ lockoutPrompt,
192
+ dismissLockoutPrompt,
150
193
  revokeState,
151
194
  revokeAllOtherSessions,
152
195
  };
@@ -1 +1 @@
1
- {"version":3,"file":"useSecuritySettings.js","sourceRoot":"","sources":["../../../src/views/settings/useSecuritySettings.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useSecuritySettings.js","sourceRoot":"","sources":["../../../src/views/settings/useSecuritySettings.ts"],"names":[],"mappings":"AAAA,YAAY,CAAA;AAEZ,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AACjE,OAAO,EACL,wBAAwB,GAGzB,MAAM,yCAAyC,CAAA;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAiBzC,MAAM,UAAU,GAAiB;IAC/B,WAAW,EAAE,KAAK;IAClB,kBAAkB,EAAE,CAAC;IACrB,qBAAqB,EAAE,CAAC;IACxB,YAAY,EAAE,GAAG;IACjB,iBAAiB,EAAE,KAAK;IACxB,eAAe,EAAE,EAAE;IACnB,mBAAmB,EAAE,IAAI;CAC1B,CAAA;AAED,6EAA6E;AAC7E,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,MAAM,GAAG,GAAa,EAAE,CAAA;IACxB,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACpC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAA;QACzB,IAAI,KAAK,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACpD,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,SAAS,gBAAgB,CAAC,CAAmB;IAC3C,OAAO;QACL,WAAW,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ;QAC3B,kBAAkB,EAAE,CAAC,CAAC,GAAG,CAAC,eAAe;QACzC,qBAAqB,EAAE,CAAC,CAAC,OAAO,CAAC,qBAAqB;QACtD,YAAY,EAAE,CAAC,CAAC,OAAO,CAAC,YAAY,IAAI,GAAG;QAC3C,iBAAiB,EAAE,CAAC,CAAC,OAAO,CAAC,iBAAiB,IAAI,KAAK;QACvD,eAAe,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,WAAW,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;QACzD,mBAAmB,EAAE,CAAC,CAAC,MAAM,EAAE,mBAAmB,IAAI,IAAI;KAC3D,CAAA;AACH,CAAC;AAED,SAAS,cAAc,CAAC,CAAe;IACrC,OAAO;QACL,GAAG,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC,WAAW,EAAE,eAAe,EAAE,CAAC,CAAC,kBAAkB,EAAE;QACvE,OAAO,EAAE;YACP,qBAAqB,EAAE,CAAC,CAAC,qBAAqB;YAC9C,YAAY,EAAE,CAAC,CAAC,YAAY;YAC5B,iBAAiB,EAAE,CAAC,CAAC,iBAAiB;SACvC;QACD,MAAM,EAAE;YACN,WAAW,EAAE,kBAAkB,CAAC,CAAC,CAAC,eAAe,CAAC;YAClD,mBAAmB,EAAE,CAAC,CAAC,mBAAmB;SAC3C;KACF,CAAA;AACH,CAAC;AAwBD;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAgB;IAClD,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAA;IAC5C,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAA;IAC/D,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAA4B,IAAI,CAAC,CAAA;IACjE,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAe,UAAU,CAAC,CAAA;IAClE,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAe,UAAU,CAAC,CAAA;IAC1D,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAY,MAAM,CAAC,CAAA;IAC7D,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAA;IAC/D,MAAM,CAAC,aAAa,EAAE,gBAAgB,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAA;IACvE,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAwC,MAAM,CAAC,CAAA;IAC7F,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAA;IAE7C,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,SAAS,GAAG,KAAK,CAAA;QACrB,UAAU,CAAC,IAAI,CAAC,CAAA;QAChB,YAAY,CAAC,IAAI,CAAC,CAAA;QAClB,MAAM,CAAqB,WAAW,CAAC;aACpC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE;YACZ,IAAI,SAAS;gBAAE,OAAM;YACrB,IAAI,GAAG,CAAC,KAAK,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;gBAC3B,YAAY,CAAC,GAAG,CAAC,KAAK,IAAI,mCAAmC,CAAC,CAAA;gBAC9D,OAAM;YACR,CAAC;YACD,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;YACjB,MAAM,CAAC,GAAG,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;YAC7C,WAAW,CAAC,CAAC,CAAC,CAAA;YACd,OAAO,CAAC,CAAC,CAAC,CAAA;QACZ,CAAC,CAAC;aACD,KAAK,CAAC,GAAG,EAAE;YACV,IAAI,CAAC,SAAS;gBAAE,YAAY,CAAC,mCAAmC,CAAC,CAAA;QACnE,CAAC,CAAC;aACD,OAAO,CAAC,GAAG,EAAE;YACZ,IAAI,CAAC,SAAS;gBAAE,UAAU,CAAC,KAAK,CAAC,CAAA;QACnC,CAAC,CAAC,CAAA;QACJ,OAAO,GAAG,EAAE;YACV,SAAS,GAAG,IAAI,CAAA;QAClB,CAAC,CAAA;IACH,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAA;IAEf,MAAM,QAAQ,GAAG,WAAW,CAAC,CAA+B,GAAM,EAAE,KAAsB,EAAE,EAAE;QAC5F,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAA;QAC9C,YAAY,CAAC,MAAM,CAAC,CAAA;IACtB,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAA+B,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,EAC3F,CAAC,IAAI,EAAE,QAAQ,CAAC,CACjB,CAAA;IAED,MAAM,MAAM,GAAG,OAAO,CAAoB,GAAG,EAAE;QAC7C,MAAM,IAAI,GAAG,wBAAwB,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAA;QAC3D,uEAAuE;QACvE,qEAAqE;QACrE,IAAI,IAAI,CAAC,WAAW,IAAI,CAAC,QAAQ,CAAC,WAAW,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;YAC5E,IAAI,CAAC,IAAI,CAAC;gBACR,KAAK,EAAE,aAAa;gBACpB,OAAO,EACL,2GAA2G;gBAC7G,QAAQ,EAAE,OAAO;aAClB,CAAC,CAAA;QACJ,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC,CAAA;IAEtC,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAA;IAE5D,MAAM,IAAI,GAAG,WAAW,CACtB,KAAK,EAAE,OAAsC,EAAE,EAAE;QAC/C,IAAI,CAAC,OAAO,IAAI,SAAS,IAAI,CAAC,KAAK;YAAE,OAAM;QAC3C,YAAY,CAAC,QAAQ,CAAC,CAAA;QACtB,YAAY,CAAC,IAAI,CAAC,CAAA;QAClB,gBAAgB,CAAC,IAAI,CAAC,CAAA;QACtB,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,MAAM,CAAqB,WAAW,EAAE;gBACxD,MAAM,EAAE,KAAK;gBACb,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;oBACnB,GAAG,cAAc,CAAC,IAAI,CAAC;oBACvB,GAAG,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;iBAC7D,CAAC;aACH,CAAC,CAAA;YACF,oEAAoE;YACpE,iEAAiE;YACjE,IAAI,GAAG,CAAC,IAAI,KAAK,iBAAiB,EAAE,CAAC;gBACnC,YAAY,CAAC,MAAM,CAAC,CAAA;gBACpB,gBAAgB,CAAC,GAAG,CAAC,KAAK,IAAI,+CAA+C,CAAC,CAAA;gBAC9E,OAAM;YACR,CAAC;YACD,IAAI,GAAG,CAAC,KAAK,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;gBAC3B,MAAM,IAAI,KAAK,CAAC,GAAG,CAAC,KAAK,IAAI,mCAAmC,CAAC,CAAA;YACnE,CAAC;YACD,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;YACjB,MAAM,CAAC,GAAG,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;YAC7C,WAAW,CAAC,CAAC,CAAC,CAAA;YACd,OAAO,CAAC,CAAC,CAAC,CAAA;YACV,YAAY,CAAC,SAAS,CAAC,CAAA;QACzB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,YAAY,CAAC,OAAO,CAAC,CAAA;YACrB,YAAY,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,mCAAmC,CAAC,CAAA;QACxF,CAAC;IACH,CAAC,EACD,CAAC,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,CAAC,CAClC,CAAA;IAED,MAAM,oBAAoB,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAA;IAE1E,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;QAC7B,OAAO,CAAC,QAAQ,CAAC,CAAA;QACjB,YAAY,CAAC,MAAM,CAAC,CAAA;QACpB,YAAY,CAAC,IAAI,CAAC,CAAA;QAClB,gBAAgB,CAAC,IAAI,CAAC,CAAA;IACxB,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAA;IAEd,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IAEjE,MAAM,sBAAsB,GAAG,WAAW,CAAC,KAAK,IAA4B,EAAE;QAC5E,cAAc,CAAC,SAAS,CAAC,CAAA;QACzB,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,MAAM,CAAsB,+BAA+B,EAAE;gBAC7E,MAAM,EAAE,MAAM;aACf,CAAC,CAAA;YACF,IAAI,GAAG,CAAC,KAAK;gBAAE,MAAM,IAAI,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;YACzC,cAAc,CAAC,MAAM,CAAC,CAAA;YACtB,oCAAoC;YACpC,YAAY,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;YAC1B,OAAO,GAAG,CAAC,IAAI,EAAE,OAAO,IAAI,CAAC,CAAA;QAC/B,CAAC;QAAC,MAAM,CAAC;YACP,cAAc,CAAC,OAAO,CAAC,CAAA;YACvB,OAAO,IAAI,CAAA;QACb,CAAC;IACH,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,OAAO;QACL,OAAO;QACP,SAAS;QACT,IAAI;QACJ,IAAI;QACJ,QAAQ;QACR,KAAK;QACL,MAAM;QACN,SAAS;QACT,OAAO;QACP,SAAS;QACT,SAAS;QACT,IAAI;QACJ,KAAK;QACL,OAAO;QACP,aAAa;QACb,oBAAoB;QACpB,WAAW;QACX,sBAAsB;KACvB,CAAA;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actuate-media/cms-admin",
3
- "version": "0.67.0",
3
+ "version": "0.68.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/actuate-media/actuatecms.git",
@@ -95,7 +95,7 @@
95
95
  "tailwindcss": "^4.0.0",
96
96
  "typescript": "^5.7.0",
97
97
  "vitest": "^4.1.0",
98
- "@actuate-media/cms-core": "0.87.0",
98
+ "@actuate-media/cms-core": "0.88.0",
99
99
  "@actuate-media/component-blocks": "0.2.1"
100
100
  },
101
101
  "scripts": {
package/src/AdminRoot.tsx CHANGED
@@ -38,6 +38,7 @@ import { ThemeProvider } from './components/ThemeProvider.js'
38
38
  import { LocaleProvider } from './components/LocaleProvider.js'
39
39
  import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts.js'
40
40
  import { useAppearanceBootstrap } from './hooks/useAppearanceBootstrap.js'
41
+ import { useIdleLogout } from './hooks/useIdleLogout.js'
41
42
  import { PageBuilder } from './views/page-builder/PageBuilder.js'
42
43
  import { PageSectionEditor } from './views/page-editor/PageSectionEditor.js'
43
44
  import { PagePreview } from './views/page-editor/PagePreview.js'
@@ -52,6 +53,7 @@ import { PagesListView } from './views/Pages/PagesListView.js'
52
53
  import { PageTagManager } from './views/Pages/PageTagManager.js'
53
54
  import { PostsListView } from './views/Posts/PostsListView.js'
54
55
  import { PostTypesView } from './views/Posts/PostTypesView.js'
56
+ import { PostTypeEditor } from './views/Posts/PostTypeEditor.js'
55
57
  import { NewPostPicker } from './views/Posts/NewPostPicker.js'
56
58
  import { NewPostTypeInstructions } from './views/Posts/NewPostTypeInstructions.js'
57
59
  import { PostSectionEditor } from './views/post-editor/PostSectionEditor.js'
@@ -179,6 +181,39 @@ function AdminShell({
179
181
  [buildHref],
180
182
  )
181
183
 
184
+ // Idle auto-logout (Settings → Security). The policy is read from the
185
+ // non-admin-gated /auth/session-policy endpoint so every member's client
186
+ // enforces it, not just admins. When enabled, `timeoutHours` of inactivity
187
+ // signs the user out through the normal logout flow (server revocation +
188
+ // cookie clear + local state reset).
189
+ const [sessionPolicy, setSessionPolicy] = useState<{
190
+ timeoutHours: number
191
+ idleLogoutEnabled: boolean
192
+ } | null>(null)
193
+ useEffect(() => {
194
+ if (!session) {
195
+ setSessionPolicy(null)
196
+ return
197
+ }
198
+ let cancelled = false
199
+ void cmsApi<{ timeoutHours: number; idleLogoutEnabled: boolean }>('/auth/session-policy').then(
200
+ (res) => {
201
+ if (!cancelled && res.data && typeof res.data.timeoutHours === 'number') {
202
+ setSessionPolicy(res.data)
203
+ }
204
+ },
205
+ )
206
+ return () => {
207
+ cancelled = true
208
+ }
209
+ }, [session])
210
+
211
+ useIdleLogout({
212
+ enabled: Boolean(session && sessionPolicy?.idleLogoutEnabled),
213
+ timeoutMs: (sessionPolicy?.timeoutHours ?? 0) * 3_600_000,
214
+ onIdle: handleLogout,
215
+ })
216
+
182
217
  useBranding(config)
183
218
  useAppearanceBootstrap(session)
184
219
  useAdminPageTitle(config, currentPath)
@@ -358,13 +393,7 @@ function AdminShell({
358
393
  }
359
394
  const postTypeEdit = matchRoute('/posts/types/:slug/edit')
360
395
  if (postTypeEdit?.slug) {
361
- // V1 falls through to the per-collection list view; future
362
- // PR introduces a dedicated PostTypeEditor with field-level
363
- // metadata. The list view is the natural per-type management
364
- // surface today (it already shows posts in that type).
365
- return (
366
- <CollectionList collectionSlug={postTypeEdit.slug} config={config} onNavigate={navigate} />
367
- )
396
+ return <PostTypeEditor slug={postTypeEdit.slug} onNavigate={navigate} />
368
397
  }
369
398
  if (matchRoute('/posts/types')) {
370
399
  return <PostTypesView onNavigate={navigate} />
@@ -0,0 +1,86 @@
1
+ // @vitest-environment happy-dom
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3
+ import { render } from '@testing-library/react'
4
+
5
+ import { useIdleLogout } from '../../hooks/useIdleLogout.js'
6
+
7
+ function Harness({
8
+ enabled,
9
+ timeoutMs,
10
+ onIdle,
11
+ }: {
12
+ enabled: boolean
13
+ timeoutMs: number
14
+ onIdle: () => void
15
+ }) {
16
+ useIdleLogout({ enabled, timeoutMs, onIdle })
17
+ return null
18
+ }
19
+
20
+ const TIMEOUT_MS = 60_000
21
+
22
+ beforeEach(() => {
23
+ vi.useFakeTimers()
24
+ })
25
+
26
+ afterEach(() => {
27
+ vi.useRealTimers()
28
+ })
29
+
30
+ describe('useIdleLogout', () => {
31
+ it('fires onIdle exactly once after the timeout with no activity', () => {
32
+ const onIdle = vi.fn()
33
+ render(<Harness enabled timeoutMs={TIMEOUT_MS} onIdle={onIdle} />)
34
+
35
+ vi.advanceTimersByTime(TIMEOUT_MS - 1_000)
36
+ expect(onIdle).not.toHaveBeenCalled()
37
+
38
+ vi.advanceTimersByTime(TIMEOUT_MS)
39
+ expect(onIdle).toHaveBeenCalledTimes(1)
40
+
41
+ // The idle callback must not re-fire on subsequent ticks.
42
+ vi.advanceTimersByTime(TIMEOUT_MS * 3)
43
+ expect(onIdle).toHaveBeenCalledTimes(1)
44
+ })
45
+
46
+ it('resets the idle deadline on user activity', () => {
47
+ const onIdle = vi.fn()
48
+ render(<Harness enabled timeoutMs={TIMEOUT_MS} onIdle={onIdle} />)
49
+
50
+ // Just before the deadline the user types — the clock restarts.
51
+ vi.advanceTimersByTime(TIMEOUT_MS - 5_000)
52
+ window.dispatchEvent(new Event('keydown'))
53
+ vi.advanceTimersByTime(TIMEOUT_MS - 5_000)
54
+ expect(onIdle).not.toHaveBeenCalled()
55
+
56
+ // A full quiet timeout after the last activity signs the user out.
57
+ vi.advanceTimersByTime(TIMEOUT_MS)
58
+ expect(onIdle).toHaveBeenCalledTimes(1)
59
+ })
60
+
61
+ it('does nothing when disabled', () => {
62
+ const onIdle = vi.fn()
63
+ render(<Harness enabled={false} timeoutMs={TIMEOUT_MS} onIdle={onIdle} />)
64
+ vi.advanceTimersByTime(TIMEOUT_MS * 10)
65
+ expect(onIdle).not.toHaveBeenCalled()
66
+ })
67
+
68
+ it('tears everything down on unmount', () => {
69
+ const onIdle = vi.fn()
70
+ const { unmount } = render(<Harness enabled timeoutMs={TIMEOUT_MS} onIdle={onIdle} />)
71
+ unmount()
72
+ vi.advanceTimersByTime(TIMEOUT_MS * 10)
73
+ expect(onIdle).not.toHaveBeenCalled()
74
+ })
75
+
76
+ it('checks the deadline immediately when a hidden tab becomes visible', () => {
77
+ const onIdle = vi.fn()
78
+ render(<Harness enabled timeoutMs={TIMEOUT_MS} onIdle={onIdle} />)
79
+
80
+ // Simulate the deadline passing while the tab was hidden (the interval is
81
+ // shared with visibility, so freeze it by only moving the mocked clock).
82
+ vi.setSystemTime(Date.now() + TIMEOUT_MS + 1_000)
83
+ document.dispatchEvent(new Event('visibilitychange'))
84
+ expect(onIdle).toHaveBeenCalledTimes(1)
85
+ })
86
+ })
@@ -0,0 +1,112 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ const cmsApiMock = vi.fn()
4
+ vi.mock('../../lib/api.js', () => ({
5
+ cmsApi: (...args: unknown[]) => cmsApiMock(...args),
6
+ }))
7
+
8
+ const {
9
+ fetchPostTypeDetail,
10
+ fieldKeyFromName,
11
+ fieldTypeLabel,
12
+ humanizeFieldName,
13
+ renderFieldSnippet,
14
+ } = await import('../../lib/post-types-service.js')
15
+
16
+ const COLLECTIONS = [
17
+ {
18
+ slug: 'blog',
19
+ labels: { singular: 'Blog Post', plural: 'Blog' },
20
+ type: 'post',
21
+ urlPrefix: 'blog',
22
+ description: 'Long-form articles',
23
+ icon: 'file-text',
24
+ color: 'purple',
25
+ hidden: false,
26
+ fields: [
27
+ { name: 'title', type: 'text', required: true },
28
+ { name: 'body', type: 'richText', required: false },
29
+ { name: 'author', type: 'relationship', required: false, relationTo: 'users' },
30
+ ],
31
+ },
32
+ ]
33
+
34
+ describe('fetchPostTypeDetail', () => {
35
+ beforeEach(() => {
36
+ cmsApiMock.mockReset()
37
+ cmsApiMock.mockResolvedValue({ data: COLLECTIONS })
38
+ })
39
+
40
+ it('maps the matching collection row onto PostTypeDetail', async () => {
41
+ const detail = await fetchPostTypeDetail('blog')
42
+ expect(detail).toEqual({
43
+ slug: 'blog',
44
+ singular: 'Blog Post',
45
+ plural: 'Blog',
46
+ description: 'Long-form articles',
47
+ urlPrefix: 'blog',
48
+ icon: 'file-text',
49
+ color: 'purple',
50
+ fields: COLLECTIONS[0]!.fields,
51
+ })
52
+ })
53
+
54
+ it('returns null when the slug is unknown', async () => {
55
+ expect(await fetchPostTypeDetail('missing')).toBeNull()
56
+ })
57
+
58
+ it('throws on an API error envelope', async () => {
59
+ cmsApiMock.mockResolvedValue({ error: 'boom' })
60
+ await expect(fetchPostTypeDetail('blog')).rejects.toThrow('boom')
61
+ })
62
+ })
63
+
64
+ describe('field name helpers', () => {
65
+ it('humanizes camelCase and snake_case keys', () => {
66
+ expect(humanizeFieldName('clientName')).toBe('Client Name')
67
+ expect(humanizeFieldName('meta_description')).toBe('Meta Description')
68
+ expect(humanizeFieldName('seo-score')).toBe('Seo Score')
69
+ })
70
+
71
+ it('derives camelCase keys from display names', () => {
72
+ expect(fieldKeyFromName('Client Name')).toBe('clientName')
73
+ expect(fieldKeyFromName(' Featured Image! ')).toBe('featuredImage')
74
+ expect(fieldKeyFromName('')).toBe('')
75
+ })
76
+
77
+ it('labels known field types and passes through unknown ones', () => {
78
+ expect(fieldTypeLabel('text')).toBe('Short Text')
79
+ expect(fieldTypeLabel('relationship')).toBe('Relation')
80
+ expect(fieldTypeLabel('customThing')).toBe('customThing')
81
+ })
82
+ })
83
+
84
+ describe('renderFieldSnippet', () => {
85
+ it('renders a minimal optional field', () => {
86
+ expect(
87
+ renderFieldSnippet({ key: 'subtitle', label: 'Subtitle', type: 'text', required: false }),
88
+ ).toBe("subtitle: { type: 'text', label: 'Subtitle' },")
89
+ })
90
+
91
+ it('includes required, help text, and escapes quotes', () => {
92
+ const snippet = renderFieldSnippet({
93
+ key: 'clientName',
94
+ label: "Client's Name",
95
+ type: 'text',
96
+ required: true,
97
+ helpText: "Editor's hint",
98
+ })
99
+ expect(snippet).toContain("label: 'Client\\'s Name'")
100
+ expect(snippet).toContain('required: true')
101
+ expect(snippet).toContain("admin: { description: 'Editor\\'s hint' }")
102
+ })
103
+
104
+ it('seeds placeholder options for select and relationTo for relationship', () => {
105
+ expect(
106
+ renderFieldSnippet({ key: 'tone', label: 'Tone', type: 'select', required: false }),
107
+ ).toContain("options: [{ label: 'Option A', value: 'option-a' }]")
108
+ expect(
109
+ renderFieldSnippet({ key: 'owner', label: 'Owner', type: 'relationship', required: false }),
110
+ ).toContain("relationTo: 'posts'")
111
+ })
112
+ })
@@ -3,9 +3,14 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
3
3
  import { fireEvent, render, screen, within } from '@testing-library/react'
4
4
 
5
5
  /**
6
- * Regression for the post-list navigation bug: CollectionList must open
7
- * post-type rows in the section editor (`/posts/:type/:id/edit`), not the
8
- * generic `/:collection/:id` route which 404s when the slug is `posts`.
6
+ * CollectionList row interactions with the inline entry detail pane
7
+ * (design Entries split-pane):
8
+ * - row / title click toggles the 400px detail pane (no navigation)
9
+ * - "Open full editor" and row double-click open the correct editor route
10
+ *
11
+ * Regression (post-list navigation bug): post-type rows must open the section
12
+ * editor (`/posts/:type/:id/edit`), never the generic `/:collection/:id`
13
+ * route — which 404s when the slug is `posts`.
9
14
  */
10
15
 
11
16
  vi.mock('../../lib/api.js', () => ({
@@ -40,17 +45,58 @@ beforeEach(() => {
40
45
  listDocs = []
41
46
  })
42
47
 
43
- describe('CollectionList navigation (post collection)', () => {
44
- it('opens a post row in the section editor, not the generic route', () => {
48
+ describe('CollectionList entry detail pane + navigation (post collection)', () => {
49
+ it('row click opens the detail pane instead of navigating', () => {
50
+ listDocs = [
51
+ {
52
+ id: '1',
53
+ title: 'Hello',
54
+ status: 'PUBLISHED',
55
+ updatedAt: '2026-06-01T00:00:00Z',
56
+ data: { title: 'Hello' },
57
+ },
58
+ ]
59
+ const onNavigate = vi.fn()
60
+ render(<CollectionList collectionSlug="posts" config={POST_CONFIG} onNavigate={onNavigate} />)
61
+
62
+ const titleButton = within(screen.getByRole('table')).getByRole('button', { name: 'Hello' })
63
+ expect(titleButton.getAttribute('aria-expanded')).toBe('false')
64
+ fireEvent.click(titleButton)
65
+
66
+ expect(onNavigate).not.toHaveBeenCalled()
67
+ expect(titleButton.getAttribute('aria-expanded')).toBe('true')
68
+ expect(screen.getByRole('region', { name: 'Post details' })).toBeTruthy()
69
+
70
+ // Clicking again toggles the pane closed.
71
+ fireEvent.click(titleButton)
72
+ expect(screen.queryByRole('region', { name: 'Post details' })).toBeNull()
73
+ })
74
+
75
+ it('"Open full editor" opens the section editor, not the generic route', () => {
45
76
  listDocs = [{ id: '1', title: 'Hello', status: 'PUBLISHED', updatedAt: '2026-06-01T00:00:00Z' }]
46
77
  const onNavigate = vi.fn()
47
78
  render(<CollectionList collectionSlug="posts" config={POST_CONFIG} onNavigate={onNavigate} />)
48
79
 
49
80
  fireEvent.click(within(screen.getByRole('table')).getByRole('button', { name: 'Hello' }))
81
+ fireEvent.click(screen.getByRole('button', { name: 'Open full editor' }))
82
+
50
83
  expect(onNavigate).toHaveBeenCalledWith('/posts/posts/1/edit')
51
84
  expect(onNavigate).not.toHaveBeenCalledWith('/posts/1')
52
85
  })
53
86
 
87
+ it('row double-click opens the section editor directly', () => {
88
+ listDocs = [{ id: '1', title: 'Hello', status: 'PUBLISHED', updatedAt: '2026-06-01T00:00:00Z' }]
89
+ const onNavigate = vi.fn()
90
+ render(<CollectionList collectionSlug="posts" config={POST_CONFIG} onNavigate={onNavigate} />)
91
+
92
+ const row = within(screen.getByRole('table'))
93
+ .getByRole('button', { name: 'Hello' })
94
+ .closest('tr')!
95
+ fireEvent.doubleClick(row)
96
+
97
+ expect(onNavigate).toHaveBeenCalledWith('/posts/posts/1/edit')
98
+ })
99
+
54
100
  it('starts a new post in the section editor', () => {
55
101
  const onNavigate = vi.fn()
56
102
  render(<CollectionList collectionSlug="posts" config={POST_CONFIG} onNavigate={onNavigate} />)
@@ -3,8 +3,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
3
3
  import { fireEvent, render, screen, waitFor } from '@testing-library/react'
4
4
 
5
5
  // Drive the Forms list through a mocked forms-service so the test owns the
6
- // data and can assert filtering, search, the notify toggle, and the schema
7
- // drawer without a real backend.
6
+ // data and can assert filtering, search, the notify toggle, and the inline
7
+ // schema pane without a real backend.
8
8
  function makeForm(over: Record<string, unknown> = {}) {
9
9
  return {
10
10
  id: 'f1',
@@ -56,6 +56,7 @@ const fetchFormSchema = vi.fn(async () => ({
56
56
  activeVersionId: null,
57
57
  versions: [],
58
58
  }))
59
+ const saveFormSchema = vi.fn(async (_id: string, _fields: unknown[]) => ({ data: {} }))
59
60
  const fetchEmbedSettings = vi.fn(async () => ({}))
60
61
  const fetchNotificationSettings = vi.fn(async () => ({ enabled: true, recipients: [] }))
61
62
 
@@ -64,7 +65,7 @@ vi.mock('../../lib/forms-service.js', () => ({
64
65
  fetchFormStats: () => fetchFormStats(),
65
66
  setNotifyEnabled: (id: string, v: boolean) => setNotifyEnabled(id, v),
66
67
  fetchFormSchema: () => fetchFormSchema(),
67
- saveFormSchema: vi.fn(async () => ({ data: {} })),
68
+ saveFormSchema: (id: string, fields: unknown[]) => saveFormSchema(id, fields),
68
69
  fetchEmbedSettings: () => fetchEmbedSettings(),
69
70
  updateEmbedSettings: vi.fn(async () => ({ data: {} })),
70
71
  fetchNotificationSettings: () => fetchNotificationSettings(),
@@ -79,6 +80,8 @@ beforeEach(() => {
79
80
  fetchForms.mockClear()
80
81
  fetchFormStats.mockClear()
81
82
  setNotifyEnabled.mockClear()
83
+ fetchFormSchema.mockClear()
84
+ saveFormSchema.mockClear()
82
85
  })
83
86
 
84
87
  describe('Forms list', () => {
@@ -128,14 +131,39 @@ describe('Forms list', () => {
128
131
  await waitFor(() => expect(setNotifyEnabled).toHaveBeenCalledWith('f1', false))
129
132
  })
130
133
 
131
- it('opens the schema drawer from the Schema action', async () => {
134
+ it('opens the inline schema pane from the Schema action and saves through the service', async () => {
132
135
  render(<Forms onNavigate={() => {}} />)
133
136
  await screen.findByText('Contact Us')
134
137
 
135
- fireEvent.click(screen.getAllByRole('button', { name: 'Schema' })[0]!)
138
+ const schemaButton = screen.getAllByRole('button', { name: 'Schema' })[0]!
139
+ expect(schemaButton.getAttribute('aria-expanded')).toBe('false')
140
+ fireEvent.click(schemaButton)
136
141
 
137
142
  await waitFor(() => expect(fetchFormSchema).toHaveBeenCalled())
138
- // Drawer tabs are present.
143
+ // Pane renders as a labelled inline region (not an overlay dialog).
144
+ expect(screen.getByRole('region', { name: 'Form schema' })).toBeTruthy()
145
+ expect(schemaButton.getAttribute('aria-expanded')).toBe('true')
139
146
  expect(await screen.findByRole('tab', { name: 'Fields' })).toBeTruthy()
147
+
148
+ // Saving flows through the same saveFormSchema service call.
149
+ fireEvent.click(await screen.findByRole('button', { name: 'Save Schema' }))
150
+ await waitFor(() =>
151
+ expect(saveFormSchema).toHaveBeenCalledWith(
152
+ 'f1',
153
+ expect.arrayContaining([expect.objectContaining({ key: 'name' })]),
154
+ ),
155
+ )
156
+ })
157
+
158
+ it('clicking Schema again toggles the pane closed', async () => {
159
+ render(<Forms onNavigate={() => {}} />)
160
+ await screen.findByText('Contact Us')
161
+
162
+ const schemaButton = screen.getAllByRole('button', { name: 'Schema' })[0]!
163
+ fireEvent.click(schemaButton)
164
+ await waitFor(() => expect(screen.getByRole('region', { name: 'Form schema' })).toBeTruthy())
165
+
166
+ fireEvent.click(schemaButton)
167
+ await waitFor(() => expect(screen.queryByRole('region', { name: 'Form schema' })).toBeNull())
140
168
  })
141
169
  })