@actuate-media/cms-admin 0.26.0 → 0.28.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 (105) hide show
  1. package/dist/__tests__/views/appearance-settings.render.test.d.ts +2 -0
  2. package/dist/__tests__/views/appearance-settings.render.test.d.ts.map +1 -0
  3. package/dist/__tests__/views/appearance-settings.render.test.js +95 -0
  4. package/dist/__tests__/views/appearance-settings.render.test.js.map +1 -0
  5. package/dist/__tests__/views/security-settings.render.test.d.ts +2 -0
  6. package/dist/__tests__/views/security-settings.render.test.d.ts.map +1 -0
  7. package/dist/__tests__/views/security-settings.render.test.js +175 -0
  8. package/dist/__tests__/views/security-settings.render.test.js.map +1 -0
  9. package/dist/actuate-admin.css +1 -1
  10. package/dist/components/ThemeProvider.d.ts +47 -4
  11. package/dist/components/ThemeProvider.d.ts.map +1 -1
  12. package/dist/components/ThemeProvider.js +173 -27
  13. package/dist/components/ThemeProvider.js.map +1 -1
  14. package/dist/components/ui/Badge.d.ts +1 -1
  15. package/dist/components/ui/Button.d.ts +1 -1
  16. package/dist/components/ui/Card.d.ts +2 -2
  17. package/dist/components/ui/Input.d.ts +1 -1
  18. package/dist/components/ui/Select.d.ts +1 -1
  19. package/dist/layout/Sidebar.d.ts.map +1 -1
  20. package/dist/layout/Sidebar.js +47 -8
  21. package/dist/layout/Sidebar.js.map +1 -1
  22. package/dist/views/Settings.d.ts.map +1 -1
  23. package/dist/views/Settings.js +5 -17
  24. package/dist/views/Settings.js.map +1 -1
  25. package/dist/views/settings/AccessProtectionCard.d.ts +10 -0
  26. package/dist/views/settings/AccessProtectionCard.d.ts.map +1 -0
  27. package/dist/views/settings/AccessProtectionCard.js +20 -0
  28. package/dist/views/settings/AccessProtectionCard.js.map +1 -0
  29. package/dist/views/settings/AdminThemeCard.d.ts +6 -0
  30. package/dist/views/settings/AdminThemeCard.d.ts.map +1 -0
  31. package/dist/views/settings/AdminThemeCard.js +31 -0
  32. package/dist/views/settings/AdminThemeCard.js.map +1 -0
  33. package/dist/views/settings/AppearanceTab.d.ts +7 -0
  34. package/dist/views/settings/AppearanceTab.d.ts.map +1 -0
  35. package/dist/views/settings/AppearanceTab.js +24 -0
  36. package/dist/views/settings/AppearanceTab.js.map +1 -0
  37. package/dist/views/settings/AuthMfaCard.d.ts +10 -0
  38. package/dist/views/settings/AuthMfaCard.d.ts.map +1 -0
  39. package/dist/views/settings/AuthMfaCard.js +33 -0
  40. package/dist/views/settings/AuthMfaCard.js.map +1 -0
  41. package/dist/views/settings/BrandPreviewCard.d.ts +5 -0
  42. package/dist/views/settings/BrandPreviewCard.d.ts.map +1 -0
  43. package/dist/views/settings/BrandPreviewCard.js +18 -0
  44. package/dist/views/settings/BrandPreviewCard.js.map +1 -0
  45. package/dist/views/settings/BrandingCard.d.ts +6 -0
  46. package/dist/views/settings/BrandingCard.d.ts.map +1 -0
  47. package/dist/views/settings/BrandingCard.js +38 -0
  48. package/dist/views/settings/BrandingCard.js.map +1 -0
  49. package/dist/views/settings/SecretsAuditCard.d.ts +11 -0
  50. package/dist/views/settings/SecretsAuditCard.d.ts.map +1 -0
  51. package/dist/views/settings/SecretsAuditCard.js +22 -0
  52. package/dist/views/settings/SecretsAuditCard.js.map +1 -0
  53. package/dist/views/settings/SecurityStatusCard.d.ts +6 -0
  54. package/dist/views/settings/SecurityStatusCard.d.ts.map +1 -0
  55. package/dist/views/settings/SecurityStatusCard.js +35 -0
  56. package/dist/views/settings/SecurityStatusCard.js.map +1 -0
  57. package/dist/views/settings/SecurityTab.d.ts +5 -0
  58. package/dist/views/settings/SecurityTab.d.ts.map +1 -0
  59. package/dist/views/settings/SecurityTab.js +41 -0
  60. package/dist/views/settings/SecurityTab.js.map +1 -0
  61. package/dist/views/settings/SessionPolicyCard.d.ts +11 -0
  62. package/dist/views/settings/SessionPolicyCard.d.ts.map +1 -0
  63. package/dist/views/settings/SessionPolicyCard.js +21 -0
  64. package/dist/views/settings/SessionPolicyCard.js.map +1 -0
  65. package/dist/views/settings/TypographyMotionCard.d.ts +6 -0
  66. package/dist/views/settings/TypographyMotionCard.d.ts.map +1 -0
  67. package/dist/views/settings/TypographyMotionCard.js +8 -0
  68. package/dist/views/settings/TypographyMotionCard.js.map +1 -0
  69. package/dist/views/settings/securityPrimitives.d.ts +21 -0
  70. package/dist/views/settings/securityPrimitives.d.ts.map +1 -0
  71. package/dist/views/settings/securityPrimitives.js +31 -0
  72. package/dist/views/settings/securityPrimitives.js.map +1 -0
  73. package/dist/views/settings/useAppearanceSettings.d.ts +69 -0
  74. package/dist/views/settings/useAppearanceSettings.d.ts.map +1 -0
  75. package/dist/views/settings/useAppearanceSettings.js +344 -0
  76. package/dist/views/settings/useAppearanceSettings.js.map +1 -0
  77. package/dist/views/settings/useGeneralSettings.d.ts +4 -1
  78. package/dist/views/settings/useGeneralSettings.d.ts.map +1 -1
  79. package/dist/views/settings/useGeneralSettings.js.map +1 -1
  80. package/dist/views/settings/useSecuritySettings.d.ts +37 -0
  81. package/dist/views/settings/useSecuritySettings.d.ts.map +1 -0
  82. package/dist/views/settings/useSecuritySettings.js +154 -0
  83. package/dist/views/settings/useSecuritySettings.js.map +1 -0
  84. package/package.json +2 -2
  85. package/src/__tests__/views/appearance-settings.render.test.tsx +132 -0
  86. package/src/__tests__/views/security-settings.render.test.tsx +212 -0
  87. package/src/components/ThemeProvider.tsx +249 -36
  88. package/src/layout/Sidebar.tsx +81 -13
  89. package/src/styles/theme.css +32 -2
  90. package/src/views/Settings.tsx +5 -68
  91. package/src/views/settings/AccessProtectionCard.tsx +143 -0
  92. package/src/views/settings/AdminThemeCard.tsx +145 -0
  93. package/src/views/settings/AppearanceTab.tsx +81 -0
  94. package/src/views/settings/AuthMfaCard.tsx +118 -0
  95. package/src/views/settings/BrandPreviewCard.tsx +125 -0
  96. package/src/views/settings/BrandingCard.tsx +332 -0
  97. package/src/views/settings/SecretsAuditCard.tsx +129 -0
  98. package/src/views/settings/SecurityStatusCard.tsx +110 -0
  99. package/src/views/settings/SecurityTab.tsx +162 -0
  100. package/src/views/settings/SessionPolicyCard.tsx +123 -0
  101. package/src/views/settings/TypographyMotionCard.tsx +65 -0
  102. package/src/views/settings/securityPrimitives.tsx +75 -0
  103. package/src/views/settings/useAppearanceSettings.ts +513 -0
  104. package/src/views/settings/useGeneralSettings.ts +4 -1
  105. package/src/views/settings/useSecuritySettings.ts +197 -0
@@ -0,0 +1,197 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useMemo, useState } from 'react'
4
+ import {
5
+ validateSecuritySettings,
6
+ type SecurityCenterData,
7
+ type SecuritySettings,
8
+ } from '@actuate-media/cms-core/security-center'
9
+ import { cmsApi } from '../../lib/api.js'
10
+ import type { ValidationIssue } from './useGeneralSettings.js'
11
+
12
+ /** The subset of security policy the tab can actually edit + enforce. */
13
+ export interface SecurityForm {
14
+ mfaRequired: boolean
15
+ mfaGracePeriodDays: number
16
+ maxConcurrentSessions: number
17
+ }
18
+
19
+ type SaveState = 'idle' | 'saving' | 'success' | 'error'
20
+
21
+ const EMPTY_FORM: SecurityForm = {
22
+ mfaRequired: false,
23
+ mfaGracePeriodDays: 7,
24
+ maxConcurrentSessions: 5,
25
+ }
26
+
27
+ function formFromSettings(s: SecuritySettings): SecurityForm {
28
+ return {
29
+ mfaRequired: s.mfa.required,
30
+ mfaGracePeriodDays: s.mfa.gracePeriodDays,
31
+ maxConcurrentSessions: s.session.maxConcurrentSessions,
32
+ }
33
+ }
34
+
35
+ function formToSettings(f: SecurityForm): SecuritySettings {
36
+ return {
37
+ mfa: { required: f.mfaRequired, gracePeriodDays: f.mfaGracePeriodDays },
38
+ session: { maxConcurrentSessions: f.maxConcurrentSessions },
39
+ }
40
+ }
41
+
42
+ export interface UseSecuritySettings {
43
+ loading: boolean
44
+ loadError: string | null
45
+ data: SecurityCenterData | null
46
+ form: SecurityForm
47
+ setField: <K extends keyof SecurityForm>(key: K, value: SecurityForm[K]) => void
48
+ dirty: boolean
49
+ issues: ValidationIssue[]
50
+ hasErrors: boolean
51
+ canEdit: boolean
52
+ saveState: SaveState
53
+ saveError: string | null
54
+ save: () => Promise<void>
55
+ reset: () => void
56
+ refetch: () => void
57
+ revokeState: 'idle' | 'working' | 'done' | 'error'
58
+ revokeAllOtherSessions: () => Promise<number | null>
59
+ }
60
+
61
+ /**
62
+ * Loads the Security Center (`GET /security`) — computed posture, cross-module
63
+ * summaries, secret/encryption verdicts — and tracks the small editable policy
64
+ * (MFA requirement + grace, concurrent-session cap). Save is lockout-safe: the
65
+ * client mirrors the server's MFA guard so the operator gets immediate feedback,
66
+ * and the server re-checks authoritatively.
67
+ */
68
+ export function useSecuritySettings(canEdit: boolean): UseSecuritySettings {
69
+ const [loading, setLoading] = useState(true)
70
+ const [loadError, setLoadError] = useState<string | null>(null)
71
+ const [data, setData] = useState<SecurityCenterData | null>(null)
72
+ const [baseline, setBaseline] = useState<SecurityForm>(EMPTY_FORM)
73
+ const [form, setForm] = useState<SecurityForm>(EMPTY_FORM)
74
+ const [saveState, setSaveState] = useState<SaveState>('idle')
75
+ const [saveError, setSaveError] = useState<string | null>(null)
76
+ const [revokeState, setRevokeState] = useState<'idle' | 'working' | 'done' | 'error'>('idle')
77
+ const [reloadKey, setReloadKey] = useState(0)
78
+
79
+ useEffect(() => {
80
+ let cancelled = false
81
+ setLoading(true)
82
+ setLoadError(null)
83
+ cmsApi<SecurityCenterData>('/security')
84
+ .then((res) => {
85
+ if (cancelled) return
86
+ if (res.error || !res.data) {
87
+ setLoadError(res.error ?? 'Failed to load security settings.')
88
+ return
89
+ }
90
+ setData(res.data)
91
+ const f = formFromSettings(res.data.settings)
92
+ setBaseline(f)
93
+ setForm(f)
94
+ })
95
+ .catch(() => {
96
+ if (!cancelled) setLoadError('Failed to load security settings.')
97
+ })
98
+ .finally(() => {
99
+ if (!cancelled) setLoading(false)
100
+ })
101
+ return () => {
102
+ cancelled = true
103
+ }
104
+ }, [reloadKey])
105
+
106
+ const setField = useCallback(<K extends keyof SecurityForm>(key: K, value: SecurityForm[K]) => {
107
+ setForm((prev) => ({ ...prev, [key]: value }))
108
+ setSaveState('idle')
109
+ }, [])
110
+
111
+ const dirty = useMemo(
112
+ () => (Object.keys(form) as Array<keyof SecurityForm>).some((k) => form[k] !== baseline[k]),
113
+ [form, baseline],
114
+ )
115
+
116
+ const issues = useMemo<ValidationIssue[]>(() => {
117
+ const list = validateSecuritySettings(formToSettings(form))
118
+ // Client mirror of the server's lockout-safe MFA guard: block enabling
119
+ // org-wide MFA when the current admin has not set up MFA themselves.
120
+ if (form.mfaRequired && !baseline.mfaRequired && data && !data.viewerHasMfa) {
121
+ list.push({
122
+ field: 'mfaRequired',
123
+ message:
124
+ 'Set up MFA for your own account before requiring it for everyone — otherwise you would lock yourself out.',
125
+ severity: 'error',
126
+ })
127
+ }
128
+ return list
129
+ }, [form, baseline.mfaRequired, data])
130
+
131
+ const hasErrors = issues.some((i) => i.severity === 'error')
132
+
133
+ const save = useCallback(async () => {
134
+ if (!canEdit || hasErrors || !dirty) return
135
+ setSaveState('saving')
136
+ setSaveError(null)
137
+ try {
138
+ const res = await cmsApi<SecurityCenterData>('/security', {
139
+ method: 'PUT',
140
+ body: JSON.stringify(formToSettings(form)),
141
+ })
142
+ if (res.error || !res.data) throw new Error(res.error ?? 'Failed to save security settings.')
143
+ setData(res.data)
144
+ const f = formFromSettings(res.data.settings)
145
+ setBaseline(f)
146
+ setForm(f)
147
+ setSaveState('success')
148
+ } catch (err) {
149
+ setSaveState('error')
150
+ setSaveError(err instanceof Error ? err.message : 'Failed to save security settings.')
151
+ }
152
+ }, [canEdit, hasErrors, dirty, form])
153
+
154
+ const reset = useCallback(() => {
155
+ setForm(baseline)
156
+ setSaveState('idle')
157
+ setSaveError(null)
158
+ }, [baseline])
159
+
160
+ const refetch = useCallback(() => setReloadKey((k) => k + 1), [])
161
+
162
+ const revokeAllOtherSessions = useCallback(async (): Promise<number | null> => {
163
+ setRevokeState('working')
164
+ try {
165
+ const res = await cmsApi<{ revoked: number }>('/security/sessions/revoke-all', {
166
+ method: 'POST',
167
+ })
168
+ if (res.error) throw new Error(res.error)
169
+ setRevokeState('done')
170
+ // Refresh the active-session count.
171
+ setReloadKey((k) => k + 1)
172
+ return res.data?.revoked ?? 0
173
+ } catch {
174
+ setRevokeState('error')
175
+ return null
176
+ }
177
+ }, [])
178
+
179
+ return {
180
+ loading,
181
+ loadError,
182
+ data,
183
+ form,
184
+ setField,
185
+ dirty,
186
+ issues,
187
+ hasErrors,
188
+ canEdit,
189
+ saveState,
190
+ saveError,
191
+ save,
192
+ reset,
193
+ refetch,
194
+ revokeState,
195
+ revokeAllOtherSessions,
196
+ }
197
+ }