@actuate-media/cms-admin 0.80.0 → 0.80.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/views/settings/CaptchaStatusCard.d.ts +7 -0
- package/dist/views/settings/CaptchaStatusCard.d.ts.map +1 -0
- package/dist/views/settings/CaptchaStatusCard.js +49 -0
- package/dist/views/settings/CaptchaStatusCard.js.map +1 -0
- package/dist/views/settings/SecurityTab.d.ts.map +1 -1
- package/dist/views/settings/SecurityTab.js +2 -1
- package/dist/views/settings/SecurityTab.js.map +1 -1
- package/package.json +2 -2
- package/src/views/settings/CaptchaStatusCard.tsx +119 -0
- package/src/views/settings/SecurityTab.tsx +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @actuate-media/cms-admin
|
|
2
2
|
|
|
3
|
+
## 0.80.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 2f2fe29: First-class CAPTCHA policy on public form submit: enforce toggle, fail-open/closed on provider outage with timeout + audit logging, and Settings → Security status card (#676).
|
|
8
|
+
|
|
3
9
|
## 0.80.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only CAPTCHA status for Settings → Security (#676).
|
|
3
|
+
* Keys and policy are env / actuate.config driven — this surface shows what
|
|
4
|
+
* public form submit will enforce.
|
|
5
|
+
*/
|
|
6
|
+
export declare function CaptchaStatusCard(): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
//# sourceMappingURL=CaptchaStatusCard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CaptchaStatusCard.d.ts","sourceRoot":"","sources":["../../../src/views/settings/CaptchaStatusCard.tsx"],"names":[],"mappings":"AA2BA;;;;GAIG;AACH,wBAAgB,iBAAiB,4CAsFhC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { ShieldCheck } from 'lucide-react';
|
|
5
|
+
import { cmsApi } from '../../lib/api.js';
|
|
6
|
+
import { SettingsCard } from './components.js';
|
|
7
|
+
function providerLabel(provider) {
|
|
8
|
+
switch (provider) {
|
|
9
|
+
case 'turnstile':
|
|
10
|
+
return 'Cloudflare Turnstile';
|
|
11
|
+
case 'recaptcha':
|
|
12
|
+
return 'Google reCAPTCHA v3';
|
|
13
|
+
default:
|
|
14
|
+
return 'Not configured';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Read-only CAPTCHA status for Settings → Security (#676).
|
|
19
|
+
* Keys and policy are env / actuate.config driven — this surface shows what
|
|
20
|
+
* public form submit will enforce.
|
|
21
|
+
*/
|
|
22
|
+
export function CaptchaStatusCard() {
|
|
23
|
+
const [config, setConfig] = useState(null);
|
|
24
|
+
const [error, setError] = useState(null);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
let cancelled = false;
|
|
27
|
+
void cmsApi('/captcha/config')
|
|
28
|
+
.then((res) => {
|
|
29
|
+
if (cancelled)
|
|
30
|
+
return;
|
|
31
|
+
if (res.error || !res.data) {
|
|
32
|
+
setError(res.error ?? 'Unable to load captcha config');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
setConfig(res.data);
|
|
36
|
+
})
|
|
37
|
+
.catch((err) => {
|
|
38
|
+
if (!cancelled)
|
|
39
|
+
setError(err instanceof Error ? err.message : 'Unable to load captcha config');
|
|
40
|
+
});
|
|
41
|
+
return () => {
|
|
42
|
+
cancelled = true;
|
|
43
|
+
};
|
|
44
|
+
}, []);
|
|
45
|
+
return (_jsx("div", { id: "security-captcha", children: _jsx(SettingsCard, { title: "Form CAPTCHA", description: "Server-side Turnstile / reCAPTCHA verification on public form submit. Provider keys stay in environment variables.", children: error ? (_jsx("p", { className: "text-destructive text-sm", children: error })) : !config ? (_jsx("p", { className: "text-muted-foreground text-sm", children: "Loading captcha status\u2026" })) : (_jsxs("div", { className: "space-y-3 text-sm", children: [_jsxs("div", { className: "flex items-start gap-2", children: [_jsx(ShieldCheck, { size: 16, className: "text-muted-foreground mt-0.5 shrink-0", "aria-hidden": true }), _jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-foreground font-medium", children: providerLabel(config.provider) }), _jsx("p", { className: "text-muted-foreground", children: config.provider === 'none'
|
|
46
|
+
? 'Set TURNSTILE_* or RECAPTCHA_* env vars to enable. Honeypot spam protection still runs without captcha.'
|
|
47
|
+
: `Site key configured${config.siteKey ? ` (${config.siteKey.slice(0, 8)}…)` : ''}.` })] })] }), _jsxs("dl", { className: "border-border grid gap-2 rounded-md border p-3 sm:grid-cols-2", children: [_jsxs("div", { children: [_jsx("dt", { className: "text-muted-foreground", children: "Enforce on submit" }), _jsx("dd", { className: "text-foreground font-medium", children: config.enforce ? 'On' : 'Off' })] }), _jsxs("div", { children: [_jsx("dt", { className: "text-muted-foreground", children: "Provider outage policy" }), _jsx("dd", { className: "text-foreground font-medium", children: config.onProviderError })] }), _jsxs("div", { children: [_jsx("dt", { className: "text-muted-foreground", children: "Verify timeout" }), _jsxs("dd", { className: "text-foreground font-medium", children: [config.timeoutMs, " ms"] })] }), config.scoreThreshold != null && (_jsxs("div", { children: [_jsx("dt", { className: "text-muted-foreground", children: "reCAPTCHA score threshold" }), _jsx("dd", { className: "text-foreground font-medium", children: config.scoreThreshold })] }))] }), _jsxs("p", { className: "text-muted-foreground text-xs", children: ["Policy via ", _jsx("code", { className: "text-foreground", children: "CAPTCHA_ENFORCE" }), ",", ' ', _jsx("code", { className: "text-foreground", children: "CAPTCHA_ON_PROVIDER_ERROR" }), " (", _jsx("code", { className: "text-foreground", children: "fail-open" }), " /", ' ', _jsx("code", { className: "text-foreground", children: "fail-closed" }), "),", ' ', _jsx("code", { className: "text-foreground", children: "CAPTCHA_TIMEOUT_MS" }), ", or", ' ', _jsx("code", { className: "text-foreground", children: "forms.captcha" }), " in actuate.config. Token failures always reject when enforce is on; only provider outages follow the outage policy."] })] })) }) }));
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=CaptchaStatusCard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CaptchaStatusCard.js","sourceRoot":"","sources":["../../../src/views/settings/CaptchaStatusCard.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SecurityTab.d.ts","sourceRoot":"","sources":["../../../src/views/settings/SecurityTab.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"SecurityTab.d.ts","sourceRoot":"","sources":["../../../src/views/settings/SecurityTab.tsx"],"names":[],"mappings":"AA+BA,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EACJ,UAAU,GACX,EAAE;IACD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CACpC,2CAsJA"}
|
|
@@ -8,6 +8,7 @@ import { AccessProtectionCard } from './AccessProtectionCard.js';
|
|
|
8
8
|
import { AuthMfaCard } from './AuthMfaCard.js';
|
|
9
9
|
import { SecretsAuditCard } from './SecretsAuditCard.js';
|
|
10
10
|
import { SecurityStatusCard } from './SecurityStatusCard.js';
|
|
11
|
+
import { CaptchaStatusCard } from './CaptchaStatusCard.js';
|
|
11
12
|
import { SessionPolicyCard } from './SessionPolicyCard.js';
|
|
12
13
|
import { useSecuritySettings } from './useSecuritySettings.js';
|
|
13
14
|
function CardSkeleton() {
|
|
@@ -37,6 +38,6 @@ export function SecurityTab({ role, onNavigate, }) {
|
|
|
37
38
|
? 'Ask an administrator to review the Security tab.'
|
|
38
39
|
: (loadError ?? 'The security service could not be reached. Try again shortly.') })] })] }));
|
|
39
40
|
}
|
|
40
|
-
return (_jsxs("div", { className: "space-y-4 pb-4", children: [_jsx(SecurityStatusCard, { status: data.status, onNavigate: onNavigate }), issues.length > 0 && _jsx(SettingsValidationSummary, { issues: issues }), !canEdit && (_jsx("p", { className: "text-muted-foreground text-sm", children: "You have read-only access. Only administrators can change security settings." })), _jsx(AuthMfaCard, { form: form, setField: setField, canEdit: canEdit, viewerHasMfa: data.viewerHasMfa, withMfa: data.users.withMfa, totalAdmins: data.users.totalAdmins, onNavigate: onNavigate }), _jsx("div", { id: "security-sessions", children: _jsx(SessionPolicyCard, { form: form, setField: setField, canEdit: canEdit, activeSessions: data.activeSessions, revokeState: revokeState, onRevokeAll: () => void revokeAllOtherSessions() }) }), _jsx("div", { id: "security-ip-allowlist", children: _jsx(AccessProtectionCard, { form: form, setField: setField, canEdit: canEdit, ipAllowlist: data.ipAllowlist, secrets: data.secrets, csrfEnabled: data.csrfEnabled, uploadValidation: data.uploadValidation, sources: data.sources, onNavigate: onNavigate }) }), _jsx("div", { id: "security-audit", children: _jsx(SecretsAuditCard, { secrets: data.secrets, auditEnabled: data.auditEnabled, failedLogins24h: data.failedLogins24h, users: data.users, apiKeys: data.apiKeys, sources: data.sources, onNavigate: onNavigate }) }), saveState === 'error' && (_jsx("p", { className: "text-destructive text-sm", role: "alert", children: "Failed to save security settings. Please try again." })), _jsx(ConfirmDangerousSettingDialog, { open: lockoutPrompt !== null, title: "This allowlist may lock you out", description: lockoutPrompt ?? 'Your current IP is not covered by the allowlist you are about to save.', confirmLabel: "Save anyway", onConfirm: () => void save({ confirmLockout: true }), onCancel: dismissLockoutPrompt }), _jsx(SettingsSaveBar, { dirty: dirty, canEdit: canEdit, hasErrors: hasErrors, saveState: saveState, onSave: () => void save(), onReset: reset })] }));
|
|
41
|
+
return (_jsxs("div", { className: "space-y-4 pb-4", children: [_jsx(SecurityStatusCard, { status: data.status, onNavigate: onNavigate }), _jsx(CaptchaStatusCard, {}), issues.length > 0 && _jsx(SettingsValidationSummary, { issues: issues }), !canEdit && (_jsx("p", { className: "text-muted-foreground text-sm", children: "You have read-only access. Only administrators can change security settings." })), _jsx(AuthMfaCard, { form: form, setField: setField, canEdit: canEdit, viewerHasMfa: data.viewerHasMfa, withMfa: data.users.withMfa, totalAdmins: data.users.totalAdmins, onNavigate: onNavigate }), _jsx("div", { id: "security-sessions", children: _jsx(SessionPolicyCard, { form: form, setField: setField, canEdit: canEdit, activeSessions: data.activeSessions, revokeState: revokeState, onRevokeAll: () => void revokeAllOtherSessions() }) }), _jsx("div", { id: "security-ip-allowlist", children: _jsx(AccessProtectionCard, { form: form, setField: setField, canEdit: canEdit, ipAllowlist: data.ipAllowlist, secrets: data.secrets, csrfEnabled: data.csrfEnabled, uploadValidation: data.uploadValidation, sources: data.sources, onNavigate: onNavigate }) }), _jsx("div", { id: "security-audit", children: _jsx(SecretsAuditCard, { secrets: data.secrets, auditEnabled: data.auditEnabled, failedLogins24h: data.failedLogins24h, users: data.users, apiKeys: data.apiKeys, sources: data.sources, onNavigate: onNavigate }) }), saveState === 'error' && (_jsx("p", { className: "text-destructive text-sm", role: "alert", children: "Failed to save security settings. Please try again." })), _jsx(ConfirmDangerousSettingDialog, { open: lockoutPrompt !== null, title: "This allowlist may lock you out", description: lockoutPrompt ?? 'Your current IP is not covered by the allowlist you are about to save.', confirmLabel: "Save anyway", onConfirm: () => void save({ confirmLockout: true }), onCancel: dismissLockoutPrompt }), _jsx(SettingsSaveBar, { dirty: dirty, canEdit: canEdit, hasErrors: hasErrors, saveState: saveState, onSave: () => void save(), onReset: reset })] }));
|
|
41
42
|
}
|
|
42
43
|
//# sourceMappingURL=SecurityTab.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SecurityTab.js","sourceRoot":"","sources":["../../../src/views/settings/SecurityTab.tsx"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"SecurityTab.js","sourceRoot":"","sources":["../../../src/views/settings/SecurityTab.tsx"],"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.80.
|
|
3
|
+
"version": "0.80.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"tailwindcss": "^4.0.0",
|
|
99
99
|
"typescript": "^5.7.0",
|
|
100
100
|
"vitest": "^4.1.0",
|
|
101
|
-
"@actuate-media/cms-core": "0.
|
|
101
|
+
"@actuate-media/cms-core": "0.103.0",
|
|
102
102
|
"@actuate-media/component-blocks": "0.2.2",
|
|
103
103
|
"@actuate-media/plugin-ai": "0.5.6",
|
|
104
104
|
"@actuate-media/plugin-blocks": "0.1.0",
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { ShieldCheck } from 'lucide-react'
|
|
5
|
+
import { cmsApi } from '../../lib/api.js'
|
|
6
|
+
import { SettingsCard } from './components.js'
|
|
7
|
+
|
|
8
|
+
interface PublicCaptchaConfig {
|
|
9
|
+
provider: 'recaptcha' | 'turnstile' | 'none'
|
|
10
|
+
siteKey: string | null
|
|
11
|
+
enforce: boolean
|
|
12
|
+
onProviderError: 'fail-open' | 'fail-closed'
|
|
13
|
+
timeoutMs: number
|
|
14
|
+
scoreThreshold: number | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function providerLabel(provider: string): string {
|
|
18
|
+
switch (provider) {
|
|
19
|
+
case 'turnstile':
|
|
20
|
+
return 'Cloudflare Turnstile'
|
|
21
|
+
case 'recaptcha':
|
|
22
|
+
return 'Google reCAPTCHA v3'
|
|
23
|
+
default:
|
|
24
|
+
return 'Not configured'
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Read-only CAPTCHA status for Settings → Security (#676).
|
|
30
|
+
* Keys and policy are env / actuate.config driven — this surface shows what
|
|
31
|
+
* public form submit will enforce.
|
|
32
|
+
*/
|
|
33
|
+
export function CaptchaStatusCard() {
|
|
34
|
+
const [config, setConfig] = useState<PublicCaptchaConfig | null>(null)
|
|
35
|
+
const [error, setError] = useState<string | null>(null)
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
let cancelled = false
|
|
39
|
+
void cmsApi<PublicCaptchaConfig>('/captcha/config')
|
|
40
|
+
.then((res) => {
|
|
41
|
+
if (cancelled) return
|
|
42
|
+
if (res.error || !res.data) {
|
|
43
|
+
setError(res.error ?? 'Unable to load captcha config')
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
setConfig(res.data)
|
|
47
|
+
})
|
|
48
|
+
.catch((err) => {
|
|
49
|
+
if (!cancelled)
|
|
50
|
+
setError(err instanceof Error ? err.message : 'Unable to load captcha config')
|
|
51
|
+
})
|
|
52
|
+
return () => {
|
|
53
|
+
cancelled = true
|
|
54
|
+
}
|
|
55
|
+
}, [])
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div id="security-captcha">
|
|
59
|
+
<SettingsCard
|
|
60
|
+
title="Form CAPTCHA"
|
|
61
|
+
description="Server-side Turnstile / reCAPTCHA verification on public form submit. Provider keys stay in environment variables."
|
|
62
|
+
>
|
|
63
|
+
{error ? (
|
|
64
|
+
<p className="text-destructive text-sm">{error}</p>
|
|
65
|
+
) : !config ? (
|
|
66
|
+
<p className="text-muted-foreground text-sm">Loading captcha status…</p>
|
|
67
|
+
) : (
|
|
68
|
+
<div className="space-y-3 text-sm">
|
|
69
|
+
<div className="flex items-start gap-2">
|
|
70
|
+
<ShieldCheck
|
|
71
|
+
size={16}
|
|
72
|
+
className="text-muted-foreground mt-0.5 shrink-0"
|
|
73
|
+
aria-hidden
|
|
74
|
+
/>
|
|
75
|
+
<div className="space-y-1">
|
|
76
|
+
<p className="text-foreground font-medium">{providerLabel(config.provider)}</p>
|
|
77
|
+
<p className="text-muted-foreground">
|
|
78
|
+
{config.provider === 'none'
|
|
79
|
+
? 'Set TURNSTILE_* or RECAPTCHA_* env vars to enable. Honeypot spam protection still runs without captcha.'
|
|
80
|
+
: `Site key configured${config.siteKey ? ` (${config.siteKey.slice(0, 8)}…)` : ''}.`}
|
|
81
|
+
</p>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
<dl className="border-border grid gap-2 rounded-md border p-3 sm:grid-cols-2">
|
|
85
|
+
<div>
|
|
86
|
+
<dt className="text-muted-foreground">Enforce on submit</dt>
|
|
87
|
+
<dd className="text-foreground font-medium">{config.enforce ? 'On' : 'Off'}</dd>
|
|
88
|
+
</div>
|
|
89
|
+
<div>
|
|
90
|
+
<dt className="text-muted-foreground">Provider outage policy</dt>
|
|
91
|
+
<dd className="text-foreground font-medium">{config.onProviderError}</dd>
|
|
92
|
+
</div>
|
|
93
|
+
<div>
|
|
94
|
+
<dt className="text-muted-foreground">Verify timeout</dt>
|
|
95
|
+
<dd className="text-foreground font-medium">{config.timeoutMs} ms</dd>
|
|
96
|
+
</div>
|
|
97
|
+
{config.scoreThreshold != null && (
|
|
98
|
+
<div>
|
|
99
|
+
<dt className="text-muted-foreground">reCAPTCHA score threshold</dt>
|
|
100
|
+
<dd className="text-foreground font-medium">{config.scoreThreshold}</dd>
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
</dl>
|
|
104
|
+
<p className="text-muted-foreground text-xs">
|
|
105
|
+
Policy via <code className="text-foreground">CAPTCHA_ENFORCE</code>,{' '}
|
|
106
|
+
<code className="text-foreground">CAPTCHA_ON_PROVIDER_ERROR</code> (
|
|
107
|
+
<code className="text-foreground">fail-open</code> /{' '}
|
|
108
|
+
<code className="text-foreground">fail-closed</code>),{' '}
|
|
109
|
+
<code className="text-foreground">CAPTCHA_TIMEOUT_MS</code>, or{' '}
|
|
110
|
+
<code className="text-foreground">forms.captcha</code> in actuate.config. Token
|
|
111
|
+
failures always reject when enforce is on; only provider outages follow the outage
|
|
112
|
+
policy.
|
|
113
|
+
</p>
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
</SettingsCard>
|
|
117
|
+
</div>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
@@ -12,6 +12,7 @@ import { AccessProtectionCard } from './AccessProtectionCard.js'
|
|
|
12
12
|
import { AuthMfaCard } from './AuthMfaCard.js'
|
|
13
13
|
import { SecretsAuditCard } from './SecretsAuditCard.js'
|
|
14
14
|
import { SecurityStatusCard } from './SecurityStatusCard.js'
|
|
15
|
+
import { CaptchaStatusCard } from './CaptchaStatusCard.js'
|
|
15
16
|
import { SessionPolicyCard } from './SessionPolicyCard.js'
|
|
16
17
|
import { useSecuritySettings } from './useSecuritySettings.js'
|
|
17
18
|
|
|
@@ -98,6 +99,8 @@ export function SecurityTab({
|
|
|
98
99
|
<div className="space-y-4 pb-4">
|
|
99
100
|
<SecurityStatusCard status={data.status} onNavigate={onNavigate} />
|
|
100
101
|
|
|
102
|
+
<CaptchaStatusCard />
|
|
103
|
+
|
|
101
104
|
{issues.length > 0 && <SettingsValidationSummary issues={issues} />}
|
|
102
105
|
|
|
103
106
|
{!canEdit && (
|