@actuate-media/cms-admin 0.67.0 → 0.69.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 (180) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/AdminRoot.d.ts.map +1 -1
  3. package/dist/AdminRoot.js +59 -15
  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 +119 -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__/router/query-string.test.d.ts +2 -0
  14. package/dist/__tests__/router/query-string.test.d.ts.map +1 -0
  15. package/dist/__tests__/router/query-string.test.js +44 -0
  16. package/dist/__tests__/router/query-string.test.js.map +1 -0
  17. package/dist/__tests__/views/collection-list-nav.render.test.js +43 -5
  18. package/dist/__tests__/views/collection-list-nav.render.test.js.map +1 -1
  19. package/dist/__tests__/views/forms-list.render.test.js +25 -6
  20. package/dist/__tests__/views/forms-list.render.test.js.map +1 -1
  21. package/dist/__tests__/views/page-builder-sections-panel.render.test.d.ts +2 -0
  22. package/dist/__tests__/views/page-builder-sections-panel.render.test.d.ts.map +1 -0
  23. package/dist/__tests__/views/page-builder-sections-panel.render.test.js +121 -0
  24. package/dist/__tests__/views/page-builder-sections-panel.render.test.js.map +1 -0
  25. package/dist/__tests__/views/page-templates.render.test.d.ts +2 -0
  26. package/dist/__tests__/views/page-templates.render.test.d.ts.map +1 -0
  27. package/dist/__tests__/views/page-templates.render.test.js +105 -0
  28. package/dist/__tests__/views/page-templates.render.test.js.map +1 -0
  29. package/dist/__tests__/views/post-type-editor.render.test.d.ts +2 -0
  30. package/dist/__tests__/views/post-type-editor.render.test.d.ts.map +1 -0
  31. package/dist/__tests__/views/post-type-editor.render.test.js +102 -0
  32. package/dist/__tests__/views/post-type-editor.render.test.js.map +1 -0
  33. package/dist/__tests__/views/security-settings.render.test.js +85 -4
  34. package/dist/__tests__/views/security-settings.render.test.js.map +1 -1
  35. package/dist/__tests__/views/seo-content-pane.render.test.d.ts +2 -0
  36. package/dist/__tests__/views/seo-content-pane.render.test.d.ts.map +1 -0
  37. package/dist/__tests__/views/seo-content-pane.render.test.js +117 -0
  38. package/dist/__tests__/views/seo-content-pane.render.test.js.map +1 -0
  39. package/dist/__tests__/views/users-selected-deeplink.render.test.d.ts +2 -0
  40. package/dist/__tests__/views/users-selected-deeplink.render.test.d.ts.map +1 -0
  41. package/dist/__tests__/views/users-selected-deeplink.render.test.js +91 -0
  42. package/dist/__tests__/views/users-selected-deeplink.render.test.js.map +1 -0
  43. package/dist/actuate-admin.css +1 -1
  44. package/dist/components/collections/EntryDetailPane.d.ts +10 -0
  45. package/dist/components/collections/EntryDetailPane.d.ts.map +1 -0
  46. package/dist/components/collections/EntryDetailPane.js +92 -0
  47. package/dist/components/collections/EntryDetailPane.js.map +1 -0
  48. package/dist/components/forms/FieldsPanel.js +1 -1
  49. package/dist/components/forms/FieldsPanel.js.map +1 -1
  50. package/dist/components/forms/FormSchemaPane.d.ts +8 -0
  51. package/dist/components/forms/FormSchemaPane.d.ts.map +1 -0
  52. package/dist/components/forms/FormSchemaPane.js +95 -0
  53. package/dist/components/forms/FormSchemaPane.js.map +1 -0
  54. package/dist/components/seo/SeoEditorPane.d.ts +9 -0
  55. package/dist/components/seo/SeoEditorPane.d.ts.map +1 -0
  56. package/dist/components/seo/SeoEditorPane.js +175 -0
  57. package/dist/components/seo/SeoEditorPane.js.map +1 -0
  58. package/dist/components/ui/Badge.d.ts +1 -1
  59. package/dist/components/ui/Button.d.ts +1 -1
  60. package/dist/components/ui/StatusBadge.d.ts +1 -1
  61. package/dist/hooks/useIdleLogout.d.ts +33 -0
  62. package/dist/hooks/useIdleLogout.d.ts.map +1 -0
  63. package/dist/hooks/useIdleLogout.js +99 -0
  64. package/dist/hooks/useIdleLogout.js.map +1 -0
  65. package/dist/lib/api.d.ts +1 -0
  66. package/dist/lib/api.d.ts.map +1 -1
  67. package/dist/lib/api.js +7 -1
  68. package/dist/lib/api.js.map +1 -1
  69. package/dist/lib/post-types-service.d.ts +44 -0
  70. package/dist/lib/post-types-service.d.ts.map +1 -0
  71. package/dist/lib/post-types-service.js +83 -0
  72. package/dist/lib/post-types-service.js.map +1 -0
  73. package/dist/router/index.d.ts +7 -0
  74. package/dist/router/index.d.ts.map +1 -1
  75. package/dist/router/index.js +23 -4
  76. package/dist/router/index.js.map +1 -1
  77. package/dist/views/CollectionList.d.ts.map +1 -1
  78. package/dist/views/CollectionList.js +23 -4
  79. package/dist/views/CollectionList.js.map +1 -1
  80. package/dist/views/Forms.d.ts.map +1 -1
  81. package/dist/views/Forms.js +25 -21
  82. package/dist/views/Forms.js.map +1 -1
  83. package/dist/views/MediaBrowser.d.ts +4 -1
  84. package/dist/views/MediaBrowser.d.ts.map +1 -1
  85. package/dist/views/MediaBrowser.js +27 -5
  86. package/dist/views/MediaBrowser.js.map +1 -1
  87. package/dist/views/Posts/PostTypeEditor.d.ts +16 -0
  88. package/dist/views/Posts/PostTypeEditor.d.ts.map +1 -0
  89. package/dist/views/Posts/PostTypeEditor.js +159 -0
  90. package/dist/views/Posts/PostTypeEditor.js.map +1 -0
  91. package/dist/views/Posts/PostTypesView.d.ts +3 -2
  92. package/dist/views/Posts/PostTypesView.d.ts.map +1 -1
  93. package/dist/views/Posts/PostTypesView.js +1 -1
  94. package/dist/views/Posts/PostTypesView.js.map +1 -1
  95. package/dist/views/SEO.d.ts.map +1 -1
  96. package/dist/views/SEO.js +2 -1
  97. package/dist/views/SEO.js.map +1 -1
  98. package/dist/views/Settings.d.ts +4 -1
  99. package/dist/views/Settings.d.ts.map +1 -1
  100. package/dist/views/Settings.js +4 -3
  101. package/dist/views/Settings.js.map +1 -1
  102. package/dist/views/Users.d.ts +4 -1
  103. package/dist/views/Users.d.ts.map +1 -1
  104. package/dist/views/Users.js +22 -2
  105. package/dist/views/Users.js.map +1 -1
  106. package/dist/views/page-builder/BuilderSectionsPanel.d.ts +20 -0
  107. package/dist/views/page-builder/BuilderSectionsPanel.d.ts.map +1 -0
  108. package/dist/views/page-builder/BuilderSectionsPanel.js +105 -0
  109. package/dist/views/page-builder/BuilderSectionsPanel.js.map +1 -0
  110. package/dist/views/page-builder/PageBuilder.d.ts.map +1 -1
  111. package/dist/views/page-builder/PageBuilder.js +51 -2
  112. package/dist/views/page-builder/PageBuilder.js.map +1 -1
  113. package/dist/views/page-builder/PageTemplates.d.ts.map +1 -1
  114. package/dist/views/page-builder/PageTemplates.js +113 -6
  115. package/dist/views/page-builder/PageTemplates.js.map +1 -1
  116. package/dist/views/seo/ContentTab.d.ts.map +1 -1
  117. package/dist/views/seo/ContentTab.js +22 -6
  118. package/dist/views/seo/ContentTab.js.map +1 -1
  119. package/dist/views/settings/AccessProtectionCard.d.ts +5 -1
  120. package/dist/views/settings/AccessProtectionCard.d.ts.map +1 -1
  121. package/dist/views/settings/AccessProtectionCard.js +13 -10
  122. package/dist/views/settings/AccessProtectionCard.js.map +1 -1
  123. package/dist/views/settings/SecurityTab.d.ts.map +1 -1
  124. package/dist/views/settings/SecurityTab.js +5 -4
  125. package/dist/views/settings/SecurityTab.js.map +1 -1
  126. package/dist/views/settings/SessionPolicyCard.d.ts +1 -2
  127. package/dist/views/settings/SessionPolicyCard.d.ts.map +1 -1
  128. package/dist/views/settings/SessionPolicyCard.js +12 -5
  129. package/dist/views/settings/SessionPolicyCard.js.map +1 -1
  130. package/dist/views/settings/useSecuritySettings.d.ts +13 -1
  131. package/dist/views/settings/useSecuritySettings.d.ts.map +1 -1
  132. package/dist/views/settings/useSecuritySettings.js +47 -4
  133. package/dist/views/settings/useSecuritySettings.js.map +1 -1
  134. package/package.json +2 -2
  135. package/src/AdminRoot.tsx +88 -18
  136. package/src/__tests__/hooks/useIdleLogout.test.tsx +188 -0
  137. package/src/__tests__/lib/post-types-service.test.ts +112 -0
  138. package/src/__tests__/router/query-string.test.ts +54 -0
  139. package/src/__tests__/views/collection-list-nav.render.test.tsx +51 -5
  140. package/src/__tests__/views/forms-list.render.test.tsx +34 -6
  141. package/src/__tests__/views/page-builder-sections-panel.render.test.tsx +138 -0
  142. package/src/__tests__/views/page-templates.render.test.tsx +142 -0
  143. package/src/__tests__/views/post-type-editor.render.test.tsx +116 -0
  144. package/src/__tests__/views/security-settings.render.test.tsx +111 -4
  145. package/src/__tests__/views/seo-content-pane.render.test.tsx +142 -0
  146. package/src/__tests__/views/users-selected-deeplink.render.test.tsx +102 -0
  147. package/src/components/collections/EntryDetailPane.tsx +182 -0
  148. package/src/components/forms/FieldsPanel.tsx +3 -1
  149. package/src/components/forms/FormSchemaPane.tsx +192 -0
  150. package/src/components/seo/SeoEditorPane.tsx +533 -0
  151. package/src/hooks/useIdleLogout.ts +129 -0
  152. package/src/lib/api.ts +8 -2
  153. package/src/lib/post-types-service.ts +129 -0
  154. package/src/router/index.ts +25 -4
  155. package/src/views/CollectionList.tsx +158 -110
  156. package/src/views/Forms.tsx +154 -126
  157. package/src/views/MediaBrowser.tsx +28 -5
  158. package/src/views/Posts/PostTypeEditor.tsx +568 -0
  159. package/src/views/Posts/PostTypesView.tsx +5 -4
  160. package/src/views/SEO.tsx +2 -1
  161. package/src/views/Settings.tsx +7 -2
  162. package/src/views/Users.tsx +23 -2
  163. package/src/views/page-builder/BuilderSectionsPanel.tsx +304 -0
  164. package/src/views/page-builder/PageBuilder.tsx +69 -2
  165. package/src/views/page-builder/PageTemplates.tsx +340 -57
  166. package/src/views/seo/ContentTab.tsx +113 -80
  167. package/src/views/settings/AccessProtectionCard.tsx +83 -53
  168. package/src/views/settings/SecurityTab.tsx +25 -3
  169. package/src/views/settings/SessionPolicyCard.tsx +45 -9
  170. package/src/views/settings/useSecuritySettings.ts +78 -22
  171. package/dist/components/forms/FormSchemaDrawer.d.ts +0 -9
  172. package/dist/components/forms/FormSchemaDrawer.d.ts.map +0 -1
  173. package/dist/components/forms/FormSchemaDrawer.js +0 -99
  174. package/dist/components/forms/FormSchemaDrawer.js.map +0 -1
  175. package/dist/components/seo/SeoEditorDrawer.d.ts +0 -8
  176. package/dist/components/seo/SeoEditorDrawer.d.ts.map +0 -1
  177. package/dist/components/seo/SeoEditorDrawer.js +0 -172
  178. package/dist/components/seo/SeoEditorDrawer.js.map +0 -1
  179. package/src/components/forms/FormSchemaDrawer.tsx +0 -192
  180. package/src/components/seo/SeoEditorDrawer.tsx +0 -529
@@ -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":"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"}
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.69.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.89.0",
99
99
  "@actuate-media/component-blocks": "0.2.1"
100
100
  },
101
101
  "scripts": {
package/src/AdminRoot.tsx CHANGED
@@ -5,7 +5,7 @@ import * as Dialog from '@radix-ui/react-dialog'
5
5
  import { toast } from 'sonner'
6
6
  import { Layout } from './layout/Layout.js'
7
7
  import { adminPortalContainer } from './lib/portal-container.js'
8
- import { useAdminRouter } from './router/index.js'
8
+ import { useAdminRouter, parseRouteQuery } from './router/index.js'
9
9
  import { cmsApi, ensureCsrfToken } from './lib/api.js'
10
10
  import { setDocumentFavicon, BRAND_UPDATED_EVENT } from './lib/favicon.js'
11
11
  import {
@@ -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,55 @@ 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
+ const { stayActive } = useIdleLogout({
212
+ enabled: Boolean(session && sessionPolicy?.idleLogoutEnabled),
213
+ timeoutMs: (sessionPolicy?.timeoutHours ?? 0) * 3_600_000,
214
+ onIdle: handleLogout,
215
+ // One-minute heads-up so the sign-out is never a surprise. Any activity
216
+ // (or the toast action) resets the clock and dismisses the warning.
217
+ warningMs: 60_000,
218
+ onWarn: () => {
219
+ toast.warning('You will be signed out in about a minute due to inactivity.', {
220
+ id: 'idle-logout-warning',
221
+ duration: 60_000,
222
+ action: {
223
+ label: 'Stay signed in',
224
+ onClick: () => stayActive(),
225
+ },
226
+ })
227
+ },
228
+ onWarnCancelled: () => {
229
+ toast.dismiss('idle-logout-warning')
230
+ },
231
+ })
232
+
182
233
  useBranding(config)
183
234
  useAppearanceBootstrap(session)
184
235
  useAdminPageTitle(config, currentPath)
@@ -292,10 +343,11 @@ function AdminShell({
292
343
 
293
344
  const resetMatch = matchRoute('/reset-password')
294
345
  if (resetMatch) {
295
- const params = new URLSearchParams(
296
- typeof window !== 'undefined' ? window.location.search : '',
346
+ // currentPath carries the query string (router syncs it from the URL on
347
+ // mount/popstate), so the emailed `?token=` deep link survives reloads.
348
+ return (
349
+ <ResetPassword onNavigate={navigate} token={parseRouteQuery(currentPath).get('token')} />
297
350
  )
298
- return <ResetPassword onNavigate={navigate} token={params.get('token')} />
299
351
  }
300
352
 
301
353
  if (onLogin) {
@@ -332,7 +384,7 @@ function AdminShell({
332
384
  // a collection slugged `media`, `users`, or `settings`).
333
385
 
334
386
  if (matchRoute('/media')) {
335
- return <MediaBrowser onNavigate={navigate} />
387
+ return <MediaBrowser onNavigate={navigate} currentPath={currentPath} />
336
388
  }
337
389
 
338
390
  // ─── Posts area ────────────────────────────────────────────────
@@ -358,13 +410,7 @@ function AdminShell({
358
410
  }
359
411
  const postTypeEdit = matchRoute('/posts/types/:slug/edit')
360
412
  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
- )
413
+ return <PostTypeEditor slug={postTypeEdit.slug} onNavigate={navigate} />
368
414
  }
369
415
  if (matchRoute('/posts/types')) {
370
416
  return <PostTypesView onNavigate={navigate} />
@@ -434,7 +480,7 @@ function AdminShell({
434
480
  // "Add sub-page" in the Pages tree view arrives with the parent id
435
481
  // in the query string (`/pages/new?parent=<id>`); matchRoute ignores
436
482
  // the query so it's parsed here and preselected in the editor.
437
- const newPageQuery = new URLSearchParams(currentPath.split('?')[1] ?? '')
483
+ const newPageQuery = parseRouteQuery(currentPath)
438
484
  return (
439
485
  <PageSectionEditor
440
486
  initialParentId={newPageQuery.get('parent')}
@@ -538,17 +584,38 @@ function AdminShell({
538
584
  // Users + API Keys moved into the Settings view as tabs. The legacy
539
585
  // top-level routes still resolve — they just land on the matching tab.
540
586
  if (matchRoute('/users')) {
541
- return <Settings onNavigate={navigate} config={config} session={session} initialTab="users" />
587
+ return (
588
+ <Settings
589
+ onNavigate={navigate}
590
+ config={config}
591
+ session={session}
592
+ initialTab="users"
593
+ currentPath={currentPath}
594
+ />
595
+ )
542
596
  }
543
597
 
544
598
  if (matchRoute('/api-keys')) {
545
599
  return (
546
- <Settings onNavigate={navigate} config={config} session={session} initialTab="api-keys" />
600
+ <Settings
601
+ onNavigate={navigate}
602
+ config={config}
603
+ session={session}
604
+ initialTab="api-keys"
605
+ currentPath={currentPath}
606
+ />
547
607
  )
548
608
  }
549
609
 
550
610
  if (matchRoute('/settings')) {
551
- return <Settings onNavigate={navigate} config={config} session={session} />
611
+ return (
612
+ <Settings
613
+ onNavigate={navigate}
614
+ config={config}
615
+ session={session}
616
+ currentPath={currentPath}
617
+ />
618
+ )
552
619
  }
553
620
 
554
621
  if (matchRoute('/profile')) {
@@ -687,7 +754,8 @@ function AdminShell({
687
754
  // Preview routes render as standalone pages (no sidebar/header) so a
688
755
  // new-tab preview looks like the real published page rather than an
689
756
  // embedded admin view. Everything else gets the full admin chrome.
690
- if (currentPath.endsWith('/preview')) {
757
+ // Query string is stripped — currentPath carries it after reloads.
758
+ if (currentPath.split('?')[0]!.endsWith('/preview')) {
691
759
  return (
692
760
  <div className="bg-background h-full min-h-screen">
693
761
  <ErrorBoundary>{renderView()}</ErrorBoundary>
@@ -838,7 +906,9 @@ function useAdminPageTitle(config: any, currentPath: string) {
838
906
  const baseName = branding?.title ?? (branding?.name ? `${branding.name} Admin` : null)
839
907
  if (!baseName) return
840
908
 
841
- const segment = currentPath === '/' ? 'Dashboard' : currentPath.split('/').filter(Boolean)[0]
909
+ // Strip the query so `/settings?tab=users` titles as "Settings".
910
+ const pathname = currentPath.split('?')[0]!.split('#')[0]!
911
+ const segment = pathname === '/' ? 'Dashboard' : pathname.split('/').filter(Boolean)[0]
842
912
  const pageName = segment ? segment.charAt(0).toUpperCase() + segment.slice(1) : 'Dashboard'
843
913
  document.title = `${pageName} — ${baseName}`
844
914
  }, [config, currentPath])
@@ -0,0 +1,188 @@
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
+ warningMs,
12
+ onWarn,
13
+ onWarnCancelled,
14
+ stayActiveRef,
15
+ }: {
16
+ enabled: boolean
17
+ timeoutMs: number
18
+ onIdle: () => void
19
+ warningMs?: number
20
+ onWarn?: () => void
21
+ onWarnCancelled?: () => void
22
+ stayActiveRef?: { current: (() => void) | null }
23
+ }) {
24
+ const { stayActive } = useIdleLogout({
25
+ enabled,
26
+ timeoutMs,
27
+ onIdle,
28
+ warningMs,
29
+ onWarn,
30
+ onWarnCancelled,
31
+ })
32
+ if (stayActiveRef) stayActiveRef.current = stayActive
33
+ return null
34
+ }
35
+
36
+ const TIMEOUT_MS = 60_000
37
+
38
+ beforeEach(() => {
39
+ vi.useFakeTimers()
40
+ })
41
+
42
+ afterEach(() => {
43
+ vi.useRealTimers()
44
+ })
45
+
46
+ describe('useIdleLogout', () => {
47
+ it('fires onIdle exactly once after the timeout with no activity', () => {
48
+ const onIdle = vi.fn()
49
+ render(<Harness enabled timeoutMs={TIMEOUT_MS} onIdle={onIdle} />)
50
+
51
+ vi.advanceTimersByTime(TIMEOUT_MS - 1_000)
52
+ expect(onIdle).not.toHaveBeenCalled()
53
+
54
+ vi.advanceTimersByTime(TIMEOUT_MS)
55
+ expect(onIdle).toHaveBeenCalledTimes(1)
56
+
57
+ // The idle callback must not re-fire on subsequent ticks.
58
+ vi.advanceTimersByTime(TIMEOUT_MS * 3)
59
+ expect(onIdle).toHaveBeenCalledTimes(1)
60
+ })
61
+
62
+ it('resets the idle deadline on user activity', () => {
63
+ const onIdle = vi.fn()
64
+ render(<Harness enabled timeoutMs={TIMEOUT_MS} onIdle={onIdle} />)
65
+
66
+ // Just before the deadline the user types — the clock restarts.
67
+ vi.advanceTimersByTime(TIMEOUT_MS - 5_000)
68
+ window.dispatchEvent(new Event('keydown'))
69
+ vi.advanceTimersByTime(TIMEOUT_MS - 5_000)
70
+ expect(onIdle).not.toHaveBeenCalled()
71
+
72
+ // A full quiet timeout after the last activity signs the user out.
73
+ vi.advanceTimersByTime(TIMEOUT_MS)
74
+ expect(onIdle).toHaveBeenCalledTimes(1)
75
+ })
76
+
77
+ it('does nothing when disabled', () => {
78
+ const onIdle = vi.fn()
79
+ render(<Harness enabled={false} timeoutMs={TIMEOUT_MS} onIdle={onIdle} />)
80
+ vi.advanceTimersByTime(TIMEOUT_MS * 10)
81
+ expect(onIdle).not.toHaveBeenCalled()
82
+ })
83
+
84
+ it('tears everything down on unmount', () => {
85
+ const onIdle = vi.fn()
86
+ const { unmount } = render(<Harness enabled timeoutMs={TIMEOUT_MS} onIdle={onIdle} />)
87
+ unmount()
88
+ vi.advanceTimersByTime(TIMEOUT_MS * 10)
89
+ expect(onIdle).not.toHaveBeenCalled()
90
+ })
91
+
92
+ it('checks the deadline immediately when a hidden tab becomes visible', () => {
93
+ const onIdle = vi.fn()
94
+ render(<Harness enabled timeoutMs={TIMEOUT_MS} onIdle={onIdle} />)
95
+
96
+ // Simulate the deadline passing while the tab was hidden (the interval is
97
+ // shared with visibility, so freeze it by only moving the mocked clock).
98
+ vi.setSystemTime(Date.now() + TIMEOUT_MS + 1_000)
99
+ document.dispatchEvent(new Event('visibilitychange'))
100
+ expect(onIdle).toHaveBeenCalledTimes(1)
101
+ })
102
+
103
+ describe('warning window', () => {
104
+ const WARNING_MS = 20_000
105
+
106
+ it('fires onWarn once inside the warning window, then onIdle at the deadline', () => {
107
+ const onIdle = vi.fn()
108
+ const onWarn = vi.fn()
109
+ render(
110
+ <Harness
111
+ enabled
112
+ timeoutMs={TIMEOUT_MS}
113
+ warningMs={WARNING_MS}
114
+ onIdle={onIdle}
115
+ onWarn={onWarn}
116
+ />,
117
+ )
118
+
119
+ // Before the warning window: nothing.
120
+ vi.advanceTimersByTime(TIMEOUT_MS - WARNING_MS - 5_000)
121
+ expect(onWarn).not.toHaveBeenCalled()
122
+
123
+ // Inside the window: warn exactly once, no logout yet.
124
+ vi.advanceTimersByTime(10_000)
125
+ expect(onWarn).toHaveBeenCalledTimes(1)
126
+ expect(onIdle).not.toHaveBeenCalled()
127
+
128
+ // Deadline: logout, and the warning doesn't re-fire.
129
+ vi.advanceTimersByTime(TIMEOUT_MS)
130
+ expect(onIdle).toHaveBeenCalledTimes(1)
131
+ expect(onWarn).toHaveBeenCalledTimes(1)
132
+ })
133
+
134
+ it('activity after the warning cancels it and resets the clock', () => {
135
+ const onIdle = vi.fn()
136
+ const onWarn = vi.fn()
137
+ const onWarnCancelled = vi.fn()
138
+ render(
139
+ <Harness
140
+ enabled
141
+ timeoutMs={TIMEOUT_MS}
142
+ warningMs={WARNING_MS}
143
+ onIdle={onIdle}
144
+ onWarn={onWarn}
145
+ onWarnCancelled={onWarnCancelled}
146
+ />,
147
+ )
148
+
149
+ vi.advanceTimersByTime(TIMEOUT_MS - 10_000)
150
+ expect(onWarn).toHaveBeenCalledTimes(1)
151
+
152
+ window.dispatchEvent(new Event('keydown'))
153
+ expect(onWarnCancelled).toHaveBeenCalledTimes(1)
154
+
155
+ // The clock restarted: warn again on the next quiet cycle, no logout yet.
156
+ vi.advanceTimersByTime(TIMEOUT_MS - 10_000)
157
+ expect(onWarn).toHaveBeenCalledTimes(2)
158
+ expect(onIdle).not.toHaveBeenCalled()
159
+ })
160
+
161
+ it('stayActive resets the clock like real activity', () => {
162
+ const onIdle = vi.fn()
163
+ const onWarn = vi.fn()
164
+ const onWarnCancelled = vi.fn()
165
+ const stayActiveRef: { current: (() => void) | null } = { current: null }
166
+ render(
167
+ <Harness
168
+ enabled
169
+ timeoutMs={TIMEOUT_MS}
170
+ warningMs={WARNING_MS}
171
+ onIdle={onIdle}
172
+ onWarn={onWarn}
173
+ onWarnCancelled={onWarnCancelled}
174
+ stayActiveRef={stayActiveRef}
175
+ />,
176
+ )
177
+
178
+ vi.advanceTimersByTime(TIMEOUT_MS - 10_000)
179
+ expect(onWarn).toHaveBeenCalledTimes(1)
180
+
181
+ stayActiveRef.current?.()
182
+ expect(onWarnCancelled).toHaveBeenCalledTimes(1)
183
+
184
+ vi.advanceTimersByTime(TIMEOUT_MS - 15_000)
185
+ expect(onIdle).not.toHaveBeenCalled()
186
+ })
187
+ })
188
+ })
@@ -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
+ })