@actuate-media/cms-core 0.11.0 → 0.11.2
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/dist/__tests__/actions/document-crud.test.js +5 -1
- package/dist/__tests__/actions/document-crud.test.js.map +1 -1
- package/dist/__tests__/api/admin-contracts.test.js.map +1 -1
- package/dist/__tests__/api/public-globals.test.js.map +1 -1
- package/dist/__tests__/auth/password.test.js.map +1 -1
- package/dist/__tests__/auth/session.test.js.map +1 -1
- package/dist/__tests__/codegen/generate-types.test.js.map +1 -1
- package/dist/__tests__/next.test.js +1 -3
- package/dist/__tests__/next.test.js.map +1 -1
- package/dist/__tests__/scheduling/scheduling.test.js +28 -4
- package/dist/__tests__/scheduling/scheduling.test.js.map +1 -1
- package/dist/__tests__/security/access.test.js +1 -1
- package/dist/__tests__/security/access.test.js.map +1 -1
- package/dist/__tests__/security/audit.test.js.map +1 -1
- package/dist/__tests__/security/client-ip.test.js.map +1 -1
- package/dist/__tests__/security/csrf.test.js.map +1 -1
- package/dist/__tests__/security/ip-allowlist.test.js.map +1 -1
- package/dist/__tests__/security/rate-limit.test.js.map +1 -1
- package/dist/__tests__/security/reauth.test.js.map +1 -1
- package/dist/__tests__/security/redact.test.js.map +1 -1
- package/dist/__tests__/security/sanitize.test.js.map +1 -1
- package/dist/__tests__/security/secret-storage.test.js.map +1 -1
- package/dist/__tests__/security/upload-magic.test.js.map +1 -1
- package/dist/__tests__/server-site.test.js.map +1 -1
- package/dist/__tests__/site.test.js +5 -2
- package/dist/__tests__/site.test.js.map +1 -1
- package/dist/__tests__/webhooks/webhooks.test.js.map +1 -1
- package/dist/a11y/index.d.ts +1 -1
- package/dist/a11y/index.d.ts.map +1 -1
- package/dist/a11y/index.js +23 -20
- package/dist/a11y/index.js.map +1 -1
- package/dist/actions.d.ts +1 -1
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +45 -38
- package/dist/actions.js.map +1 -1
- package/dist/api/handler-factory.d.ts.map +1 -1
- package/dist/api/handler-factory.js +15 -8
- package/dist/api/handler-factory.js.map +1 -1
- package/dist/api/handlers.d.ts.map +1 -1
- package/dist/api/handlers.js +287 -112
- package/dist/api/handlers.js.map +1 -1
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js.map +1 -1
- package/dist/api/openapi.d.ts.map +1 -1
- package/dist/api/openapi.js +151 -30
- package/dist/api/openapi.js.map +1 -1
- package/dist/api/router.d.ts +6 -6
- package/dist/api/router.d.ts.map +1 -1
- package/dist/api/router.js +27 -10
- package/dist/api/router.js.map +1 -1
- package/dist/auth/index.d.ts +12 -12
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +9 -9
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/mfa-pending.d.ts.map +1 -1
- package/dist/auth/mfa-pending.js.map +1 -1
- package/dist/auth/oauth.d.ts.map +1 -1
- package/dist/auth/oauth.js +15 -7
- package/dist/auth/oauth.js.map +1 -1
- package/dist/auth/password.d.ts +1 -1
- package/dist/auth/password.d.ts.map +1 -1
- package/dist/auth/password.js +14 -14
- package/dist/auth/password.js.map +1 -1
- package/dist/auth/providers/github.d.ts +1 -1
- package/dist/auth/providers/github.d.ts.map +1 -1
- package/dist/auth/providers/github.js +2 -2
- package/dist/auth/providers/github.js.map +1 -1
- package/dist/auth/providers/google.d.ts +1 -1
- package/dist/auth/providers/google.d.ts.map +1 -1
- package/dist/auth/providers/google.js +2 -2
- package/dist/auth/providers/google.js.map +1 -1
- package/dist/auth/providers/microsoft.d.ts +1 -1
- package/dist/auth/providers/microsoft.d.ts.map +1 -1
- package/dist/auth/providers/microsoft.js +2 -2
- package/dist/auth/providers/microsoft.js.map +1 -1
- package/dist/auth/reset-email.d.ts.map +1 -1
- package/dist/auth/reset-email.js +1 -1
- package/dist/auth/reset-email.js.map +1 -1
- package/dist/auth/reset.d.ts.map +1 -1
- package/dist/auth/reset.js +9 -9
- package/dist/auth/reset.js.map +1 -1
- package/dist/auth/session.d.ts.map +1 -1
- package/dist/auth/session.js +6 -6
- package/dist/auth/session.js.map +1 -1
- package/dist/auth/totp.d.ts.map +1 -1
- package/dist/auth/totp.js +8 -2
- package/dist/auth/totp.js.map +1 -1
- package/dist/backup/index.d.ts +2 -2
- package/dist/backup/index.d.ts.map +1 -1
- package/dist/backup/index.js +5 -5
- package/dist/backup/index.js.map +1 -1
- package/dist/cache/index.d.ts +1 -1
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/cache/index.js.map +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +8 -8
- package/dist/client.js.map +1 -1
- package/dist/codegen/index.d.ts +1 -1
- package/dist/codegen/index.d.ts.map +1 -1
- package/dist/codegen/index.js +170 -174
- package/dist/codegen/index.js.map +1 -1
- package/dist/collections/index.d.ts +1 -1
- package/dist/collections/index.d.ts.map +1 -1
- package/dist/collections/index.js.map +1 -1
- package/dist/config/define.d.ts +2 -2
- package/dist/config/define.d.ts.map +1 -1
- package/dist/config/define.js +1 -1
- package/dist/config/define.js.map +1 -1
- package/dist/config/index.d.ts +3 -3
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +32 -18
- package/dist/config/index.js.map +1 -1
- package/dist/config/types.d.ts +26 -26
- package/dist/config/types.d.ts.map +1 -1
- package/dist/content/ai-api.d.ts.map +1 -1
- package/dist/content/ai-api.js +8 -2
- package/dist/content/ai-api.js.map +1 -1
- package/dist/content/content-graph.d.ts +1 -1
- package/dist/content/content-graph.d.ts.map +1 -1
- package/dist/content/content-graph.js +7 -7
- package/dist/content/content-graph.js.map +1 -1
- package/dist/content/extract.js +13 -13
- package/dist/content/extract.js.map +1 -1
- package/dist/content/index.d.ts +7 -7
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +4 -4
- package/dist/content/index.js.map +1 -1
- package/dist/content/structured-data.d.ts +3 -3
- package/dist/content/structured-data.d.ts.map +1 -1
- package/dist/content/structured-data.js +65 -67
- package/dist/content/structured-data.js.map +1 -1
- package/dist/db/adapters/mysql.d.ts.map +1 -1
- package/dist/db/adapters/mysql.js.map +1 -1
- package/dist/db/adapters/postgres.d.ts.map +1 -1
- package/dist/db/adapters/postgres.js.map +1 -1
- package/dist/db/adapters/sqlite.d.ts.map +1 -1
- package/dist/db/adapters/sqlite.js.map +1 -1
- package/dist/db/create-adapter.d.ts.map +1 -1
- package/dist/db/create-adapter.js.map +1 -1
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/index.js.map +1 -1
- package/dist/db.d.ts +1 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +1 -1
- package/dist/db.js.map +1 -1
- package/dist/fields/index.d.ts +2 -2
- package/dist/fields/index.d.ts.map +1 -1
- package/dist/fields/index.js +51 -47
- package/dist/fields/index.js.map +1 -1
- package/dist/forms/analytics.d.ts.map +1 -1
- package/dist/forms/analytics.js.map +1 -1
- package/dist/forms/attribution.d.ts.map +1 -1
- package/dist/forms/attribution.js +7 -2
- package/dist/forms/attribution.js.map +1 -1
- package/dist/forms/index.d.ts.map +1 -1
- package/dist/forms/index.js.map +1 -1
- package/dist/graphql/index.d.ts.map +1 -1
- package/dist/graphql/index.js.map +1 -1
- package/dist/graphql/resolvers.d.ts.map +1 -1
- package/dist/graphql/resolvers.js +17 -21
- package/dist/graphql/resolvers.js.map +1 -1
- package/dist/graphql/schema-builder.d.ts.map +1 -1
- package/dist/graphql/schema-builder.js.map +1 -1
- package/dist/health/index.d.ts +2 -2
- package/dist/health/index.d.ts.map +1 -1
- package/dist/health/index.js +9 -9
- package/dist/health/index.js.map +1 -1
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +2 -2
- package/dist/i18n/index.js.map +1 -1
- package/dist/index.d.ts +78 -78
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +43 -43
- package/dist/index.js.map +1 -1
- package/dist/media/index.d.ts +2 -2
- package/dist/media/index.d.ts.map +1 -1
- package/dist/media/index.js +1 -1
- package/dist/media/index.js.map +1 -1
- package/dist/media/optimize.d.ts.map +1 -1
- package/dist/media/optimize.js +7 -4
- package/dist/media/optimize.js.map +1 -1
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +3 -3
- package/dist/middleware.js.map +1 -1
- package/dist/multisite/index.d.ts.map +1 -1
- package/dist/multisite/index.js +4 -4
- package/dist/multisite/index.js.map +1 -1
- package/dist/next/preview.d.ts.map +1 -1
- package/dist/next/preview.js.map +1 -1
- package/dist/next.d.ts.map +1 -1
- package/dist/next.js +4 -5
- package/dist/next.js.map +1 -1
- package/dist/notifications/index.d.ts +1 -1
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +5 -5
- package/dist/notifications/index.js.map +1 -1
- package/dist/page-builder/__tests__/a11y-fix.test.js +1 -5
- package/dist/page-builder/__tests__/a11y-fix.test.js.map +1 -1
- package/dist/page-builder/__tests__/blocks.test.js +4 -0
- package/dist/page-builder/__tests__/blocks.test.js.map +1 -1
- package/dist/page-builder/__tests__/design-scorer.test.js +44 -11
- package/dist/page-builder/__tests__/design-scorer.test.js.map +1 -1
- package/dist/page-builder/__tests__/schema.test.js +12 -12
- package/dist/page-builder/__tests__/schema.test.js.map +1 -1
- package/dist/page-builder/__tests__/seo-analyzer.test.js +27 -13
- package/dist/page-builder/__tests__/seo-analyzer.test.js.map +1 -1
- package/dist/page-builder/ai-pipeline.d.ts.map +1 -1
- package/dist/page-builder/ai-pipeline.js +1 -3
- package/dist/page-builder/ai-pipeline.js.map +1 -1
- package/dist/page-builder/blocks.d.ts.map +1 -1
- package/dist/page-builder/blocks.js +45 -9
- package/dist/page-builder/blocks.js.map +1 -1
- package/dist/page-builder/design-scorer.d.ts.map +1 -1
- package/dist/page-builder/design-scorer.js +249 -41
- package/dist/page-builder/design-scorer.js.map +1 -1
- package/dist/page-builder/index.d.ts +3 -3
- package/dist/page-builder/index.d.ts.map +1 -1
- package/dist/page-builder/index.js +2 -2
- package/dist/page-builder/index.js.map +1 -1
- package/dist/page-builder/seo-analyzer.d.ts.map +1 -1
- package/dist/page-builder/seo-analyzer.js +252 -56
- package/dist/page-builder/seo-analyzer.js.map +1 -1
- package/dist/page-builder/templates.d.ts.map +1 -1
- package/dist/page-builder/templates.js +45 -16
- package/dist/page-builder/templates.js.map +1 -1
- package/dist/page-builder/tree.d.ts.map +1 -1
- package/dist/page-builder/tree.js.map +1 -1
- package/dist/page-builder/validate.js.map +1 -1
- package/dist/presence/index.d.ts.map +1 -1
- package/dist/presence/index.js +2 -2
- package/dist/presence/index.js.map +1 -1
- package/dist/preview/index.d.ts.map +1 -1
- package/dist/preview/index.js.map +1 -1
- package/dist/privacy/index.d.ts +1 -1
- package/dist/privacy/index.d.ts.map +1 -1
- package/dist/privacy/index.js +3 -3
- package/dist/privacy/index.js.map +1 -1
- package/dist/relationships/index.d.ts.map +1 -1
- package/dist/relationships/index.js +1 -1
- package/dist/relationships/index.js.map +1 -1
- package/dist/scheduling/index.d.ts +2 -2
- package/dist/scheduling/index.d.ts.map +1 -1
- package/dist/scheduling/index.js +3 -1
- package/dist/scheduling/index.js.map +1 -1
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/index.js +1 -3
- package/dist/search/index.js.map +1 -1
- package/dist/security/access.d.ts +4 -4
- package/dist/security/access.d.ts.map +1 -1
- package/dist/security/access.js +11 -15
- package/dist/security/access.js.map +1 -1
- package/dist/security/anomaly-detection.d.ts.map +1 -1
- package/dist/security/anomaly-detection.js +5 -5
- package/dist/security/anomaly-detection.js.map +1 -1
- package/dist/security/api-key-enhanced.d.ts +2 -2
- package/dist/security/api-key-enhanced.d.ts.map +1 -1
- package/dist/security/api-key-enhanced.js +5 -5
- package/dist/security/api-key-enhanced.js.map +1 -1
- package/dist/security/audit.d.ts.map +1 -1
- package/dist/security/audit.js.map +1 -1
- package/dist/security/breach-check.js.map +1 -1
- package/dist/security/captcha.d.ts.map +1 -1
- package/dist/security/captcha.js.map +1 -1
- package/dist/security/client-ip.d.ts.map +1 -1
- package/dist/security/client-ip.js +4 -1
- package/dist/security/client-ip.js.map +1 -1
- package/dist/security/cors.d.ts +1 -1
- package/dist/security/cors.d.ts.map +1 -1
- package/dist/security/cors.js +12 -12
- package/dist/security/cors.js.map +1 -1
- package/dist/security/csp-nonces.js +11 -11
- package/dist/security/csp-nonces.js.map +1 -1
- package/dist/security/csrf.js +2 -2
- package/dist/security/csrf.js.map +1 -1
- package/dist/security/encrypted-fields.d.ts.map +1 -1
- package/dist/security/encrypted-fields.js +7 -4
- package/dist/security/encrypted-fields.js.map +1 -1
- package/dist/security/headers.d.ts.map +1 -1
- package/dist/security/headers.js +12 -12
- package/dist/security/headers.js.map +1 -1
- package/dist/security/index.d.ts +39 -39
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +25 -25
- package/dist/security/index.js.map +1 -1
- package/dist/security/internal-keys.d.ts.map +1 -1
- package/dist/security/internal-keys.js.map +1 -1
- package/dist/security/ip-allowlist.js +2 -4
- package/dist/security/ip-allowlist.js.map +1 -1
- package/dist/security/middleware.d.ts +2 -2
- package/dist/security/middleware.d.ts.map +1 -1
- package/dist/security/middleware.js +11 -11
- package/dist/security/middleware.js.map +1 -1
- package/dist/security/rate-limit.d.ts +0 -4
- package/dist/security/rate-limit.d.ts.map +1 -1
- package/dist/security/rate-limit.js +33 -3
- package/dist/security/rate-limit.js.map +1 -1
- package/dist/security/reauth.d.ts +1 -1
- package/dist/security/reauth.d.ts.map +1 -1
- package/dist/security/reauth.js.map +1 -1
- package/dist/security/redact.d.ts.map +1 -1
- package/dist/security/redact.js +4 -1
- package/dist/security/redact.js.map +1 -1
- package/dist/security/safe-fetch.d.ts.map +1 -1
- package/dist/security/safe-fetch.js.map +1 -1
- package/dist/security/sanitize.d.ts.map +1 -1
- package/dist/security/sanitize.js +40 -8
- package/dist/security/sanitize.js.map +1 -1
- package/dist/security/secret-storage.js +6 -6
- package/dist/security/secret-storage.js.map +1 -1
- package/dist/security/security-txt.d.ts.map +1 -1
- package/dist/security/security-txt.js +2 -2
- package/dist/security/security-txt.js.map +1 -1
- package/dist/security/session-limits.d.ts +1 -1
- package/dist/security/session-limits.d.ts.map +1 -1
- package/dist/security/session-limits.js +1 -1
- package/dist/security/session-limits.js.map +1 -1
- package/dist/security/upload.d.ts.map +1 -1
- package/dist/security/upload.js +26 -20
- package/dist/security/upload.js.map +1 -1
- package/dist/security/webhook.d.ts.map +1 -1
- package/dist/security/webhook.js +12 -8
- package/dist/security/webhook.js.map +1 -1
- package/dist/seo/analysis.d.ts.map +1 -1
- package/dist/seo/analysis.js +25 -13
- package/dist/seo/analysis.js.map +1 -1
- package/dist/seo/index.d.ts +9 -9
- package/dist/seo/index.d.ts.map +1 -1
- package/dist/seo/index.js +4 -4
- package/dist/seo/index.js.map +1 -1
- package/dist/seo/llms-txt.js +1 -3
- package/dist/seo/llms-txt.js.map +1 -1
- package/dist/server-site.d.ts.map +1 -1
- package/dist/server-site.js +12 -14
- package/dist/server-site.js.map +1 -1
- package/dist/setup/index.d.ts.map +1 -1
- package/dist/setup/index.js.map +1 -1
- package/dist/site.d.ts.map +1 -1
- package/dist/site.js +7 -3
- package/dist/site.js.map +1 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js.map +1 -1
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +3 -3
- package/dist/templates/index.js.map +1 -1
- package/dist/upgrade/changelog.d.ts +1 -1
- package/dist/upgrade/changelog.d.ts.map +1 -1
- package/dist/upgrade/changelog.js +12 -12
- package/dist/upgrade/changelog.js.map +1 -1
- package/dist/upgrade/index.d.ts +6 -6
- package/dist/upgrade/index.d.ts.map +1 -1
- package/dist/upgrade/index.js +3 -3
- package/dist/upgrade/index.js.map +1 -1
- package/dist/upgrade/upgrade-pr.d.ts.map +1 -1
- package/dist/upgrade/upgrade-pr.js +36 -36
- package/dist/upgrade/upgrade-pr.js.map +1 -1
- package/dist/upgrade/version-check.d.ts +1 -1
- package/dist/upgrade/version-check.d.ts.map +1 -1
- package/dist/upgrade/version-check.js +13 -13
- package/dist/upgrade/version-check.js.map +1 -1
- package/dist/webhooks/index.d.ts +1 -1
- package/dist/webhooks/index.d.ts.map +1 -1
- package/dist/webhooks/index.js +4 -4
- package/dist/webhooks/index.js.map +1 -1
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflows/index.d.ts +1 -1
- package/dist/workflows/index.d.ts.map +1 -1
- package/dist/workflows/index.js +3 -3
- package/dist/workflows/index.js.map +1 -1
- package/package.json +1 -1
- package/prisma/seed.ts +31 -31
package/dist/api/handlers.js
CHANGED
|
@@ -16,7 +16,7 @@ import { verifyCaptcha, getCaptchaConfig } from '../security/captcha.js';
|
|
|
16
16
|
import { checkForUpdates } from '../upgrade/version-check.js';
|
|
17
17
|
import { createUpgradePR } from '../upgrade/upgrade-pr.js';
|
|
18
18
|
import { encryptField, decryptField } from '../security/encrypted-fields.js';
|
|
19
|
-
import { encryptSecret, decryptSecret, encryptStringArray } from '../security/secret-storage.js';
|
|
19
|
+
import { encryptSecret, decryptSecret, encryptStringArray, } from '../security/secret-storage.js';
|
|
20
20
|
import { createRateLimiter } from '../security/rate-limit.js';
|
|
21
21
|
import { generateOpenAPISpec } from './openapi.js';
|
|
22
22
|
import { createSSEPresenceAdapter } from '../presence/index.js';
|
|
@@ -71,7 +71,9 @@ function mediaUrl(storageKey) {
|
|
|
71
71
|
const value = String(storageKey ?? '');
|
|
72
72
|
if (!value)
|
|
73
73
|
return '';
|
|
74
|
-
return value.startsWith('http://') || value.startsWith('https://') || value.startsWith('/')
|
|
74
|
+
return value.startsWith('http://') || value.startsWith('https://') || value.startsWith('/')
|
|
75
|
+
? value
|
|
76
|
+
: '';
|
|
75
77
|
}
|
|
76
78
|
function normalizeMediaItem(media) {
|
|
77
79
|
const width = typeof media.width === 'number' ? media.width : null;
|
|
@@ -167,9 +169,9 @@ function hasModel(d, name) {
|
|
|
167
169
|
}
|
|
168
170
|
}
|
|
169
171
|
function modelNotAvailable(name) {
|
|
170
|
-
return errorResponse(`The "${name}" model is not available in your Prisma schema. `
|
|
171
|
-
|
|
172
|
-
|
|
172
|
+
return errorResponse(`The "${name}" model is not available in your Prisma schema. ` +
|
|
173
|
+
'Run `actuate db:init` for new schemas, or carefully update the existing Actuate block, create/apply a Prisma migration, then regenerate Prisma Client. ' +
|
|
174
|
+
'See https://actuatecms.dev/docs/database-setup for required models.', 501);
|
|
173
175
|
}
|
|
174
176
|
async function safeCount(model, where) {
|
|
175
177
|
try {
|
|
@@ -232,28 +234,34 @@ function isAllowedStorageUrl(url) {
|
|
|
232
234
|
}
|
|
233
235
|
}
|
|
234
236
|
const ALLOWED_SORT_FIELDS = new Set([
|
|
235
|
-
'createdAt',
|
|
237
|
+
'createdAt',
|
|
238
|
+
'updatedAt',
|
|
239
|
+
'publishedAt',
|
|
240
|
+
'status',
|
|
241
|
+
'collection',
|
|
236
242
|
]);
|
|
237
243
|
let _secretMissing = false;
|
|
238
244
|
let _secretWarningLogged = false;
|
|
239
245
|
function getSessionSecret() {
|
|
240
|
-
const secret = process.env.CMS_SECRET
|
|
241
|
-
|
|
242
|
-
|
|
246
|
+
const secret = process.env.CMS_SECRET ??
|
|
247
|
+
process.env.CMS_SESSION_SECRET ??
|
|
248
|
+
globalThis.__actuateConfig?.secret;
|
|
243
249
|
if (!secret) {
|
|
244
250
|
_secretMissing = true;
|
|
245
251
|
if (!_secretWarningLogged) {
|
|
246
252
|
_secretWarningLogged = true;
|
|
247
|
-
console.error('[Actuate CMS] Missing CMS secret. Set the CMS_SECRET environment variable (min 32 characters) '
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
253
|
+
console.error('[Actuate CMS] Missing CMS secret. Set the CMS_SECRET environment variable (min 32 characters) ' +
|
|
254
|
+
'or pass `secret` in your actuate.config.ts. ' +
|
|
255
|
+
"Generate one with: node -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\" " +
|
|
256
|
+
'-- All authenticated API routes will return 503 until this is configured.');
|
|
251
257
|
}
|
|
252
258
|
throw new Error('CMS secret not configured');
|
|
253
259
|
}
|
|
254
260
|
if (secret.length < 32) {
|
|
255
|
-
throw new Error('[Actuate CMS] CMS secret must be at least 32 characters (got ' +
|
|
256
|
-
+
|
|
261
|
+
throw new Error('[Actuate CMS] CMS secret must be at least 32 characters (got ' +
|
|
262
|
+
secret.length +
|
|
263
|
+
'). ' +
|
|
264
|
+
"Generate a secure value with: node -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\"");
|
|
257
265
|
}
|
|
258
266
|
_secretMissing = false;
|
|
259
267
|
return secret;
|
|
@@ -378,6 +386,14 @@ const formLimiterGlobal = createRateLimiter({ maxRequests: 10, windowMs: 60_000
|
|
|
378
386
|
const aiGenerateLimiter = createRateLimiter({ maxRequests: 20, windowMs: 60 * 60 * 1000 });
|
|
379
387
|
const linkHealthLimiter = createRateLimiter({ maxRequests: 4, windowMs: 60 * 60 * 1000 });
|
|
380
388
|
async function checkRateLimitAsync(limiter, key) {
|
|
389
|
+
// Explicit, environment-gated bypass for test harnesses. Production never
|
|
390
|
+
// sets this — Vercel + the deploy guide both omit it. We deliberately do
|
|
391
|
+
// NOT key off `NODE_ENV === 'test'` because Next.js builds run with
|
|
392
|
+
// NODE_ENV=test in some CI configurations and we want rate limiting
|
|
393
|
+
// exercised by unit tests that explicitly opt in.
|
|
394
|
+
if (process.env.ACTUATE_DISABLE_RATE_LIMIT === '1') {
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
381
397
|
const result = await limiter.check(key);
|
|
382
398
|
return result.allowed;
|
|
383
399
|
}
|
|
@@ -417,9 +433,7 @@ async function enforceSessionLimitsForUser(d, userId) {
|
|
|
417
433
|
}
|
|
418
434
|
}
|
|
419
435
|
function getAdminPath() {
|
|
420
|
-
return process.env.ACTUATE_ADMIN_PATH
|
|
421
|
-
?? globalThis.__actuateConfig?.admin?.path
|
|
422
|
-
?? '/admin';
|
|
436
|
+
return (process.env.ACTUATE_ADMIN_PATH ?? globalThis.__actuateConfig?.admin?.path ?? '/admin');
|
|
423
437
|
}
|
|
424
438
|
class ModelNotAvailableError extends Error {
|
|
425
439
|
model;
|
|
@@ -442,7 +456,9 @@ export function registerCMSRoutes(router) {
|
|
|
442
456
|
if (typeof prop !== 'string' || prop.startsWith('$') || prop === 'then')
|
|
443
457
|
return undefined;
|
|
444
458
|
return new Proxy({}, {
|
|
445
|
-
get() {
|
|
459
|
+
get() {
|
|
460
|
+
throw new ModelNotAvailableError(String(prop));
|
|
461
|
+
},
|
|
446
462
|
});
|
|
447
463
|
},
|
|
448
464
|
});
|
|
@@ -463,7 +479,9 @@ export function registerCMSRoutes(router) {
|
|
|
463
479
|
if (val !== undefined && val !== null)
|
|
464
480
|
return val;
|
|
465
481
|
return new Proxy({}, {
|
|
466
|
-
get() {
|
|
482
|
+
get() {
|
|
483
|
+
throw new ModelNotAvailableError(String(prop));
|
|
484
|
+
},
|
|
467
485
|
});
|
|
468
486
|
},
|
|
469
487
|
});
|
|
@@ -475,12 +493,7 @@ export function registerCMSRoutes(router) {
|
|
|
475
493
|
router.get('/auth/csrf', async () => {
|
|
476
494
|
const token = await generateCsrfToken();
|
|
477
495
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
478
|
-
const cookieFlags = [
|
|
479
|
-
`actuate_csrf=${token}`,
|
|
480
|
-
'Path=/',
|
|
481
|
-
'SameSite=Lax',
|
|
482
|
-
'Max-Age=86400',
|
|
483
|
-
];
|
|
496
|
+
const cookieFlags = [`actuate_csrf=${token}`, 'Path=/', 'SameSite=Lax', 'Max-Age=86400'];
|
|
484
497
|
if (isProduction)
|
|
485
498
|
cookieFlags.push('Secure');
|
|
486
499
|
const response = json({ data: { token } });
|
|
@@ -523,7 +536,7 @@ export function registerCMSRoutes(router) {
|
|
|
523
536
|
// ---------------------------------------------------------------------------
|
|
524
537
|
router.post('/auth/login', async (request) => {
|
|
525
538
|
try {
|
|
526
|
-
const body = await request.json();
|
|
539
|
+
const body = (await request.json());
|
|
527
540
|
const { email, password } = body;
|
|
528
541
|
if (!email || !password) {
|
|
529
542
|
return errorResponse('Email and password are required', 400);
|
|
@@ -607,12 +620,7 @@ export function registerCMSRoutes(router) {
|
|
|
607
620
|
if (isProduction)
|
|
608
621
|
sessionCookie.push('Secure');
|
|
609
622
|
const csrfToken = await generateCsrfToken();
|
|
610
|
-
const csrfCookie = [
|
|
611
|
-
`actuate_csrf=${csrfToken}`,
|
|
612
|
-
'Path=/',
|
|
613
|
-
'SameSite=Lax',
|
|
614
|
-
'Max-Age=86400',
|
|
615
|
-
];
|
|
623
|
+
const csrfCookie = [`actuate_csrf=${csrfToken}`, 'Path=/', 'SameSite=Lax', 'Max-Age=86400'];
|
|
616
624
|
if (isProduction)
|
|
617
625
|
csrfCookie.push('Secure');
|
|
618
626
|
response.headers.append('Set-Cookie', sessionCookie.join('; '));
|
|
@@ -651,7 +659,7 @@ export function registerCMSRoutes(router) {
|
|
|
651
659
|
});
|
|
652
660
|
router.post('/auth/forgot-password', async (request) => {
|
|
653
661
|
try {
|
|
654
|
-
const body = await request.json();
|
|
662
|
+
const body = (await request.json());
|
|
655
663
|
const { email } = body;
|
|
656
664
|
if (!email) {
|
|
657
665
|
return errorResponse('Email is required', 400);
|
|
@@ -684,7 +692,7 @@ export function registerCMSRoutes(router) {
|
|
|
684
692
|
});
|
|
685
693
|
router.post('/auth/reset-password', async (request) => {
|
|
686
694
|
try {
|
|
687
|
-
const body = await request.json();
|
|
695
|
+
const body = (await request.json());
|
|
688
696
|
const { token, password } = body;
|
|
689
697
|
if (!token || !password) {
|
|
690
698
|
return errorResponse('Token and new password are required', 400);
|
|
@@ -764,7 +772,10 @@ export function registerCMSRoutes(router) {
|
|
|
764
772
|
if (reauthErr)
|
|
765
773
|
return reauthErr;
|
|
766
774
|
const { generateTOTPSecret, generateTOTPUri, generateBackupCodes } = await import('../auth/totp.js');
|
|
767
|
-
const user = await db().user.findUnique({
|
|
775
|
+
const user = await db().user.findUnique({
|
|
776
|
+
where: { id: auth.session.userId },
|
|
777
|
+
select: { email: true, totpEnabled: true },
|
|
778
|
+
});
|
|
768
779
|
if (!user)
|
|
769
780
|
return errorResponse('User not found', 404);
|
|
770
781
|
if (user.totpEnabled)
|
|
@@ -791,11 +802,14 @@ export function registerCMSRoutes(router) {
|
|
|
791
802
|
const auth = await requireAuth(request);
|
|
792
803
|
if (auth.error)
|
|
793
804
|
return auth.error;
|
|
794
|
-
const body = await request.json();
|
|
805
|
+
const body = (await request.json());
|
|
795
806
|
if (!body.code)
|
|
796
807
|
return errorResponse('Code is required', 400);
|
|
797
808
|
const { verifyTOTP } = await import('../auth/totp.js');
|
|
798
|
-
const user = await db().user.findUnique({
|
|
809
|
+
const user = await db().user.findUnique({
|
|
810
|
+
where: { id: auth.session.userId },
|
|
811
|
+
select: { totpSecret: true },
|
|
812
|
+
});
|
|
799
813
|
if (!user?.totpSecret)
|
|
800
814
|
return errorResponse('TOTP not set up', 400);
|
|
801
815
|
const secret = await decryptSecret(user.totpSecret);
|
|
@@ -845,7 +859,7 @@ export function registerCMSRoutes(router) {
|
|
|
845
859
|
});
|
|
846
860
|
router.post('/auth/totp/login', async (request) => {
|
|
847
861
|
try {
|
|
848
|
-
const body = await request.json();
|
|
862
|
+
const body = (await request.json());
|
|
849
863
|
if (!body.mfaPendingToken || !body.code) {
|
|
850
864
|
return errorResponse('mfaPendingToken and code are required', 400);
|
|
851
865
|
}
|
|
@@ -883,7 +897,15 @@ export function registerCMSRoutes(router) {
|
|
|
883
897
|
const { verifyTOTP } = await import('../auth/totp.js');
|
|
884
898
|
const user = await db().user.findUnique({
|
|
885
899
|
where: { id: pending.userId },
|
|
886
|
-
select: {
|
|
900
|
+
select: {
|
|
901
|
+
id: true,
|
|
902
|
+
email: true,
|
|
903
|
+
name: true,
|
|
904
|
+
role: true,
|
|
905
|
+
totpSecret: true,
|
|
906
|
+
totpEnabled: true,
|
|
907
|
+
isActive: true,
|
|
908
|
+
},
|
|
887
909
|
});
|
|
888
910
|
if (!user || !user.isActive || !user.totpEnabled || !user.totpSecret) {
|
|
889
911
|
return errorResponse('Invalid request', 400);
|
|
@@ -1133,7 +1155,7 @@ export function registerCMSRoutes(router) {
|
|
|
1133
1155
|
const roleErr = requireRole(auth.session.role, WRITE_ROLES);
|
|
1134
1156
|
if (roleErr)
|
|
1135
1157
|
return roleErr;
|
|
1136
|
-
const body = await request.json();
|
|
1158
|
+
const body = (await request.json());
|
|
1137
1159
|
const ctx = buildActionContext(auth.session, db());
|
|
1138
1160
|
const doc = await createDocument(params.slug, body, ctx);
|
|
1139
1161
|
await logEvent({
|
|
@@ -1155,7 +1177,7 @@ export function registerCMSRoutes(router) {
|
|
|
1155
1177
|
const roleErr = requireRole(auth.session.role, WRITE_ROLES);
|
|
1156
1178
|
if (roleErr)
|
|
1157
1179
|
return roleErr;
|
|
1158
|
-
const body = await request.json();
|
|
1180
|
+
const body = (await request.json());
|
|
1159
1181
|
const ctx = buildActionContext(auth.session, db());
|
|
1160
1182
|
const doc = await updateDocument(params.slug, params.id, body, ctx);
|
|
1161
1183
|
await logEvent({
|
|
@@ -1241,7 +1263,7 @@ export function registerCMSRoutes(router) {
|
|
|
1241
1263
|
const auth = await requireAuth(request);
|
|
1242
1264
|
if (auth.error)
|
|
1243
1265
|
return auth.error;
|
|
1244
|
-
const body = await request.json();
|
|
1266
|
+
const body = (await request.json());
|
|
1245
1267
|
if (!body.filename || !body.contentType) {
|
|
1246
1268
|
return errorResponse('filename and contentType are required', 400);
|
|
1247
1269
|
}
|
|
@@ -1328,8 +1350,96 @@ export function registerCMSRoutes(router) {
|
|
|
1328
1350
|
// Strip <script>, on*, javascript: URLs, foreignObject, etc.
|
|
1329
1351
|
const xml = new TextDecoder('utf-8', { fatal: false }).decode(arrayBuffer);
|
|
1330
1352
|
const sanitized = sanitizeHtml(xml, {
|
|
1331
|
-
allowedTags: [
|
|
1332
|
-
|
|
1353
|
+
allowedTags: [
|
|
1354
|
+
'svg',
|
|
1355
|
+
'g',
|
|
1356
|
+
'path',
|
|
1357
|
+
'circle',
|
|
1358
|
+
'ellipse',
|
|
1359
|
+
'line',
|
|
1360
|
+
'polygon',
|
|
1361
|
+
'polyline',
|
|
1362
|
+
'rect',
|
|
1363
|
+
'text',
|
|
1364
|
+
'tspan',
|
|
1365
|
+
'defs',
|
|
1366
|
+
'use',
|
|
1367
|
+
'symbol',
|
|
1368
|
+
'title',
|
|
1369
|
+
'desc',
|
|
1370
|
+
'style',
|
|
1371
|
+
'linearGradient',
|
|
1372
|
+
'radialGradient',
|
|
1373
|
+
'stop',
|
|
1374
|
+
'mask',
|
|
1375
|
+
'clipPath',
|
|
1376
|
+
'pattern',
|
|
1377
|
+
'filter',
|
|
1378
|
+
'feGaussianBlur',
|
|
1379
|
+
'feColorMatrix',
|
|
1380
|
+
'feOffset',
|
|
1381
|
+
'feBlend',
|
|
1382
|
+
'feFlood',
|
|
1383
|
+
'feComposite',
|
|
1384
|
+
'feMerge',
|
|
1385
|
+
'feMergeNode',
|
|
1386
|
+
],
|
|
1387
|
+
allowedAttributes: {
|
|
1388
|
+
'*': [
|
|
1389
|
+
'id',
|
|
1390
|
+
'class',
|
|
1391
|
+
'fill',
|
|
1392
|
+
'stroke',
|
|
1393
|
+
'stroke-width',
|
|
1394
|
+
'stroke-linecap',
|
|
1395
|
+
'stroke-linejoin',
|
|
1396
|
+
'opacity',
|
|
1397
|
+
'transform',
|
|
1398
|
+
'd',
|
|
1399
|
+
'cx',
|
|
1400
|
+
'cy',
|
|
1401
|
+
'r',
|
|
1402
|
+
'rx',
|
|
1403
|
+
'ry',
|
|
1404
|
+
'x',
|
|
1405
|
+
'y',
|
|
1406
|
+
'x1',
|
|
1407
|
+
'y1',
|
|
1408
|
+
'x2',
|
|
1409
|
+
'y2',
|
|
1410
|
+
'width',
|
|
1411
|
+
'height',
|
|
1412
|
+
'viewBox',
|
|
1413
|
+
'xmlns',
|
|
1414
|
+
'xmlns:xlink',
|
|
1415
|
+
'preserveAspectRatio',
|
|
1416
|
+
'points',
|
|
1417
|
+
'points',
|
|
1418
|
+
'offset',
|
|
1419
|
+
'stop-color',
|
|
1420
|
+
'stop-opacity',
|
|
1421
|
+
'gradientUnits',
|
|
1422
|
+
'gradientTransform',
|
|
1423
|
+
'href',
|
|
1424
|
+
'xlink:href',
|
|
1425
|
+
'fill-rule',
|
|
1426
|
+
'clip-rule',
|
|
1427
|
+
'mask',
|
|
1428
|
+
'clip-path',
|
|
1429
|
+
'filter',
|
|
1430
|
+
'patternUnits',
|
|
1431
|
+
'patternContentUnits',
|
|
1432
|
+
'in',
|
|
1433
|
+
'in2',
|
|
1434
|
+
'result',
|
|
1435
|
+
'mode',
|
|
1436
|
+
'values',
|
|
1437
|
+
'type',
|
|
1438
|
+
'stdDeviation',
|
|
1439
|
+
'dx',
|
|
1440
|
+
'dy',
|
|
1441
|
+
],
|
|
1442
|
+
},
|
|
1333
1443
|
});
|
|
1334
1444
|
uploadBuffer = Buffer.from(sanitized, 'utf-8');
|
|
1335
1445
|
finalSize = uploadBuffer.byteLength;
|
|
@@ -1467,7 +1577,9 @@ export function registerCMSRoutes(router) {
|
|
|
1467
1577
|
try {
|
|
1468
1578
|
await blob.del(media.storageKey);
|
|
1469
1579
|
}
|
|
1470
|
-
catch {
|
|
1580
|
+
catch {
|
|
1581
|
+
/* best-effort */
|
|
1582
|
+
}
|
|
1471
1583
|
}
|
|
1472
1584
|
catch {
|
|
1473
1585
|
newPublicUrl = `/api/cms/media/file/${newStorageKey}`;
|
|
@@ -1511,7 +1623,7 @@ export function registerCMSRoutes(router) {
|
|
|
1511
1623
|
const roleErr = requireRole(auth.session.role, WRITE_ROLES);
|
|
1512
1624
|
if (roleErr)
|
|
1513
1625
|
return roleErr;
|
|
1514
|
-
const body = await request.json();
|
|
1626
|
+
const body = (await request.json());
|
|
1515
1627
|
const updated = await db().media.update({
|
|
1516
1628
|
where: { id: params.id },
|
|
1517
1629
|
data: {
|
|
@@ -1636,7 +1748,7 @@ export function registerCMSRoutes(router) {
|
|
|
1636
1748
|
if (!encKey) {
|
|
1637
1749
|
return errorResponse('CMS_ENCRYPTION_KEY is required to store encrypted credentials.', 400);
|
|
1638
1750
|
}
|
|
1639
|
-
const body = await request.json();
|
|
1751
|
+
const body = (await request.json());
|
|
1640
1752
|
if (body.githubRepo && !/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/.test(body.githubRepo)) {
|
|
1641
1753
|
return errorResponse('Invalid repository format. Use owner/repo.', 400);
|
|
1642
1754
|
}
|
|
@@ -1678,7 +1790,7 @@ export function registerCMSRoutes(router) {
|
|
|
1678
1790
|
if (!session || session.role !== 'admin') {
|
|
1679
1791
|
return errorResponse('Unauthorized — admin only', 403);
|
|
1680
1792
|
}
|
|
1681
|
-
const body = await request.json();
|
|
1793
|
+
const body = (await request.json());
|
|
1682
1794
|
if (!body.targetVersion) {
|
|
1683
1795
|
return errorResponse('targetVersion is required', 400);
|
|
1684
1796
|
}
|
|
@@ -1743,7 +1855,7 @@ export function registerCMSRoutes(router) {
|
|
|
1743
1855
|
if (!(await checkRateLimitAsync(loginLimiter, `setup:${clientIp}`))) {
|
|
1744
1856
|
return errorResponse('Too many setup attempts', 429);
|
|
1745
1857
|
}
|
|
1746
|
-
const body = await request.json();
|
|
1858
|
+
const body = (await request.json());
|
|
1747
1859
|
if (!body.name || !body.email || !body.password) {
|
|
1748
1860
|
return errorResponse('Name, email, and password are required', 400);
|
|
1749
1861
|
}
|
|
@@ -1768,9 +1880,17 @@ export function registerCMSRoutes(router) {
|
|
|
1768
1880
|
// Health endpoint -- reports available models and CMS version
|
|
1769
1881
|
// ---------------------------------------------------------------------------
|
|
1770
1882
|
const CMS_EXPECTED_MODELS = [
|
|
1771
|
-
'document',
|
|
1772
|
-
'
|
|
1773
|
-
'
|
|
1883
|
+
'document',
|
|
1884
|
+
'media',
|
|
1885
|
+
'user',
|
|
1886
|
+
'session',
|
|
1887
|
+
'version',
|
|
1888
|
+
'folder',
|
|
1889
|
+
'redirect',
|
|
1890
|
+
'formSubmission',
|
|
1891
|
+
'auditLog',
|
|
1892
|
+
'webhookEndpoint',
|
|
1893
|
+
'webhookDeliveryLog',
|
|
1774
1894
|
];
|
|
1775
1895
|
router.get('/health', async () => {
|
|
1776
1896
|
const cmsVersion = globalThis.__actuateCoreVersion ?? '0.0.0';
|
|
@@ -1889,8 +2009,12 @@ export function registerCMSRoutes(router) {
|
|
|
1889
2009
|
orderBy: { updatedAt: 'desc' },
|
|
1890
2010
|
take: 20,
|
|
1891
2011
|
select: {
|
|
1892
|
-
id: true,
|
|
1893
|
-
|
|
2012
|
+
id: true,
|
|
2013
|
+
title: true,
|
|
2014
|
+
status: true,
|
|
2015
|
+
collection: true,
|
|
2016
|
+
updatedAt: true,
|
|
2017
|
+
createdById: true,
|
|
1894
2018
|
createdBy: { select: { name: true, email: true } },
|
|
1895
2019
|
},
|
|
1896
2020
|
}),
|
|
@@ -1965,13 +2089,31 @@ export function registerCMSRoutes(router) {
|
|
|
1965
2089
|
const canSeeUserDirectory = auth.session.role === 'ADMIN' || auth.session.role === 'EDITOR';
|
|
1966
2090
|
const [documents, media, users] = await Promise.all([
|
|
1967
2091
|
safeFindMany(d.document, {
|
|
1968
|
-
where: {
|
|
2092
|
+
where: {
|
|
2093
|
+
deletedAt: null,
|
|
2094
|
+
OR: [
|
|
2095
|
+
{ title: { contains: q, mode: 'insensitive' } },
|
|
2096
|
+
{ plainText: { contains: q, mode: 'insensitive' } },
|
|
2097
|
+
],
|
|
2098
|
+
},
|
|
1969
2099
|
take: 10,
|
|
1970
2100
|
orderBy: { updatedAt: 'desc' },
|
|
1971
|
-
select: {
|
|
2101
|
+
select: {
|
|
2102
|
+
id: true,
|
|
2103
|
+
title: true,
|
|
2104
|
+
slug: true,
|
|
2105
|
+
collection: true,
|
|
2106
|
+
status: true,
|
|
2107
|
+
updatedAt: true,
|
|
2108
|
+
},
|
|
1972
2109
|
}),
|
|
1973
2110
|
safeFindMany(d.media, {
|
|
1974
|
-
where: {
|
|
2111
|
+
where: {
|
|
2112
|
+
OR: [
|
|
2113
|
+
{ filename: { contains: q, mode: 'insensitive' } },
|
|
2114
|
+
{ altText: { contains: q, mode: 'insensitive' } },
|
|
2115
|
+
],
|
|
2116
|
+
},
|
|
1975
2117
|
take: 5,
|
|
1976
2118
|
orderBy: { createdAt: 'desc' },
|
|
1977
2119
|
select: { id: true, filename: true, altText: true, mimeType: true, storageKey: true },
|
|
@@ -2143,7 +2285,7 @@ export function registerCMSRoutes(router) {
|
|
|
2143
2285
|
return errorResponse('Form not found', 404);
|
|
2144
2286
|
}
|
|
2145
2287
|
const formData = (form.data ?? {});
|
|
2146
|
-
const body = await request.json();
|
|
2288
|
+
const body = (await request.json());
|
|
2147
2289
|
if (!body.fields || typeof body.fields !== 'object') {
|
|
2148
2290
|
return errorResponse('Missing or invalid "fields" in request body', 400);
|
|
2149
2291
|
}
|
|
@@ -2163,14 +2305,10 @@ export function registerCMSRoutes(router) {
|
|
|
2163
2305
|
submittedAt: new Date(),
|
|
2164
2306
|
},
|
|
2165
2307
|
});
|
|
2166
|
-
// Fire form hooks asynchronously (email notification, webhooks)
|
|
2167
2308
|
(async () => {
|
|
2168
2309
|
try {
|
|
2169
2310
|
const config = globalThis.__actuateConfig;
|
|
2170
|
-
const hooks = [
|
|
2171
|
-
...(config?.plugins?.forms?.hooks ?? []),
|
|
2172
|
-
...(config?._pluginHooks ?? []),
|
|
2173
|
-
];
|
|
2311
|
+
const hooks = [...(config?.plugins?.forms?.hooks ?? []), ...(config?._pluginHooks ?? [])];
|
|
2174
2312
|
const formHooks = hooks.filter((h) => h.event === 'afterCreate:form-submissions');
|
|
2175
2313
|
for (const hook of formHooks) {
|
|
2176
2314
|
await hook.handler({ formId, data: body.fields });
|
|
@@ -2220,7 +2358,7 @@ export function registerCMSRoutes(router) {
|
|
|
2220
2358
|
return auth.error;
|
|
2221
2359
|
if (auth.session.role !== 'ADMIN')
|
|
2222
2360
|
return errorResponse('Admin access required', 403);
|
|
2223
|
-
const body = await request.json();
|
|
2361
|
+
const body = (await request.json());
|
|
2224
2362
|
const source = String(body.source ?? body.from ?? '').trim();
|
|
2225
2363
|
const destination = String(body.destination ?? body.to ?? '').trim();
|
|
2226
2364
|
const requestedStatus = Number(body.statusCode ?? body.type);
|
|
@@ -2255,7 +2393,9 @@ export function registerCMSRoutes(router) {
|
|
|
2255
2393
|
try {
|
|
2256
2394
|
allowed.add(new URL(siteUrl).hostname.toLowerCase());
|
|
2257
2395
|
}
|
|
2258
|
-
catch {
|
|
2396
|
+
catch {
|
|
2397
|
+
/* noop */
|
|
2398
|
+
}
|
|
2259
2399
|
}
|
|
2260
2400
|
if (!allowed.has(destUrl.hostname.toLowerCase())) {
|
|
2261
2401
|
return errorResponse('External redirect destinations must be to an allowlisted host. Add the host to `redirects.allowedExternalHosts` in your CMS config.', 400);
|
|
@@ -2389,7 +2529,9 @@ export function registerCMSRoutes(router) {
|
|
|
2389
2529
|
try {
|
|
2390
2530
|
await resp.body?.cancel();
|
|
2391
2531
|
}
|
|
2392
|
-
catch {
|
|
2532
|
+
catch {
|
|
2533
|
+
/* noop */
|
|
2534
|
+
}
|
|
2393
2535
|
}
|
|
2394
2536
|
catch (err) {
|
|
2395
2537
|
status = err instanceof SsrfBlockedError ? -1 : 0;
|
|
@@ -2428,7 +2570,14 @@ export function registerCMSRoutes(router) {
|
|
|
2428
2570
|
return roleErr;
|
|
2429
2571
|
const documents = await db().document.findMany({
|
|
2430
2572
|
where: { status: 'PUBLISHED', deletedAt: null },
|
|
2431
|
-
select: {
|
|
2573
|
+
select: {
|
|
2574
|
+
id: true,
|
|
2575
|
+
title: true,
|
|
2576
|
+
slug: true,
|
|
2577
|
+
collection: true,
|
|
2578
|
+
data: true,
|
|
2579
|
+
plainText: true,
|
|
2580
|
+
},
|
|
2432
2581
|
});
|
|
2433
2582
|
const issues = [];
|
|
2434
2583
|
for (const doc of documents) {
|
|
@@ -2445,7 +2594,11 @@ export function registerCMSRoutes(router) {
|
|
|
2445
2594
|
const plainText = (doc.plainText ?? '');
|
|
2446
2595
|
if (plainText.length > 0 && plainText.length < 300)
|
|
2447
2596
|
problems.push('Content is too short (< 300 characters)');
|
|
2448
|
-
const content = typeof data.body === 'string'
|
|
2597
|
+
const content = typeof data.body === 'string'
|
|
2598
|
+
? data.body
|
|
2599
|
+
: typeof data.content === 'string'
|
|
2600
|
+
? data.content
|
|
2601
|
+
: '';
|
|
2449
2602
|
if (content) {
|
|
2450
2603
|
const imgMatches = content.match(/<img\b[^>]*>/gi) ?? [];
|
|
2451
2604
|
const missingAlt = imgMatches.filter((img) => !img.includes('alt=')).length;
|
|
@@ -2455,7 +2608,12 @@ export function registerCMSRoutes(router) {
|
|
|
2455
2608
|
problems.push('No H1 heading found in content');
|
|
2456
2609
|
}
|
|
2457
2610
|
if (problems.length > 0) {
|
|
2458
|
-
issues.push({
|
|
2611
|
+
issues.push({
|
|
2612
|
+
documentId: doc.id,
|
|
2613
|
+
title: doc.title ?? 'Untitled',
|
|
2614
|
+
slug: doc.slug ?? '',
|
|
2615
|
+
problems,
|
|
2616
|
+
});
|
|
2459
2617
|
}
|
|
2460
2618
|
}
|
|
2461
2619
|
const total = documents.length;
|
|
@@ -2539,7 +2697,10 @@ export function registerCMSRoutes(router) {
|
|
|
2539
2697
|
return errorResponse('Not found', 404);
|
|
2540
2698
|
const data = doc.data || {};
|
|
2541
2699
|
const title = doc.title || data.title || '';
|
|
2542
|
-
const keywords = title
|
|
2700
|
+
const keywords = title
|
|
2701
|
+
.split(/\s+/)
|
|
2702
|
+
.filter((w) => w.length > 3)
|
|
2703
|
+
.slice(0, 5);
|
|
2543
2704
|
if (keywords.length === 0) {
|
|
2544
2705
|
return new Response(JSON.stringify({ suggestions: [] }), {
|
|
2545
2706
|
status: 200,
|
|
@@ -2702,12 +2863,12 @@ export function registerCMSRoutes(router) {
|
|
|
2702
2863
|
const auth = await requireAuth(request);
|
|
2703
2864
|
if (auth.error)
|
|
2704
2865
|
return auth.error;
|
|
2705
|
-
const docs = await safeFindMany(db().document, {
|
|
2866
|
+
const docs = (await safeFindMany(db().document, {
|
|
2706
2867
|
where: { deletedAt: null, status: 'PUBLISHED' },
|
|
2707
2868
|
select: { id: true, title: true, collection: true, data: true, plainText: true },
|
|
2708
2869
|
orderBy: { updatedAt: 'desc' },
|
|
2709
2870
|
take: 50,
|
|
2710
|
-
});
|
|
2871
|
+
}));
|
|
2711
2872
|
let missingMetaDescriptions = 0;
|
|
2712
2873
|
let missingAltText = 0;
|
|
2713
2874
|
const topContent = [];
|
|
@@ -2715,7 +2876,11 @@ export function registerCMSRoutes(router) {
|
|
|
2715
2876
|
const data = doc.data ?? {};
|
|
2716
2877
|
if (!data.metaDescription && !data.seoDescription)
|
|
2717
2878
|
missingMetaDescriptions++;
|
|
2718
|
-
const content = typeof data.body === 'string'
|
|
2879
|
+
const content = typeof data.body === 'string'
|
|
2880
|
+
? data.body
|
|
2881
|
+
: typeof data.content === 'string'
|
|
2882
|
+
? data.content
|
|
2883
|
+
: '';
|
|
2719
2884
|
if (content) {
|
|
2720
2885
|
const imgMatches = content.match(/<img\b[^>]*>/gi) ?? [];
|
|
2721
2886
|
const noAlt = imgMatches.filter((img) => !img.includes('alt=')).length;
|
|
@@ -2745,7 +2910,11 @@ export function registerCMSRoutes(router) {
|
|
|
2745
2910
|
continue;
|
|
2746
2911
|
seen.add(clean);
|
|
2747
2912
|
try {
|
|
2748
|
-
const resp = await fetch(clean, {
|
|
2913
|
+
const resp = await fetch(clean, {
|
|
2914
|
+
method: 'HEAD',
|
|
2915
|
+
redirect: 'manual',
|
|
2916
|
+
signal: AbortSignal.timeout(3000),
|
|
2917
|
+
});
|
|
2749
2918
|
if (resp.status >= 400)
|
|
2750
2919
|
brokenInternalLinks++;
|
|
2751
2920
|
}
|
|
@@ -2755,7 +2924,9 @@ export function registerCMSRoutes(router) {
|
|
|
2755
2924
|
}
|
|
2756
2925
|
}
|
|
2757
2926
|
}
|
|
2758
|
-
catch {
|
|
2927
|
+
catch {
|
|
2928
|
+
/* best effort */
|
|
2929
|
+
}
|
|
2759
2930
|
return json({
|
|
2760
2931
|
data: {
|
|
2761
2932
|
totalPages: docs.length,
|
|
@@ -2796,7 +2967,10 @@ export function registerCMSRoutes(router) {
|
|
|
2796
2967
|
}
|
|
2797
2968
|
}
|
|
2798
2969
|
if (unresolved.size > 0 && layoutConfig.inherit !== false) {
|
|
2799
|
-
const segments = path
|
|
2970
|
+
const segments = path
|
|
2971
|
+
.replace(/^\/|\/$/g, '')
|
|
2972
|
+
.split('/')
|
|
2973
|
+
.filter(Boolean);
|
|
2800
2974
|
const walkDepth = Math.min(segments.length - 1, MAX_RESOLVE_DEPTH);
|
|
2801
2975
|
for (let i = walkDepth; i > 0 && unresolved.size > 0; i--) {
|
|
2802
2976
|
const parentSlug = segments.slice(0, i).join('/');
|
|
@@ -2805,10 +2979,7 @@ export function registerCMSRoutes(router) {
|
|
|
2805
2979
|
collection: matchedCollection,
|
|
2806
2980
|
deletedAt: null,
|
|
2807
2981
|
status: 'PUBLISHED',
|
|
2808
|
-
OR: [
|
|
2809
|
-
{ data: { path: ['slug'], equals: parentSlug } },
|
|
2810
|
-
{ slug: parentSlug },
|
|
2811
|
-
],
|
|
2982
|
+
OR: [{ data: { path: ['slug'], equals: parentSlug } }, { slug: parentSlug }],
|
|
2812
2983
|
},
|
|
2813
2984
|
select: { data: true },
|
|
2814
2985
|
});
|
|
@@ -2865,14 +3036,17 @@ export function registerCMSRoutes(router) {
|
|
|
2865
3036
|
if (!pathParam) {
|
|
2866
3037
|
return errorResponse('Missing required "path" query parameter', 400);
|
|
2867
3038
|
}
|
|
2868
|
-
const segments = pathParam
|
|
3039
|
+
const segments = pathParam
|
|
3040
|
+
.replace(/^\/|\/$/g, '')
|
|
3041
|
+
.split('/')
|
|
3042
|
+
.filter(Boolean);
|
|
2869
3043
|
const configCollections = globalThis.__actuateConfig?.collections ?? {};
|
|
2870
3044
|
const collectionDefs = Object.values(configCollections);
|
|
2871
3045
|
let matchedCollection = null;
|
|
2872
3046
|
let docSlug = null;
|
|
2873
3047
|
if (segments.length === 0) {
|
|
2874
3048
|
// Root path — find a page-type collection and look for "home" or "index"
|
|
2875
|
-
const pageCol = collectionDefs.find((c) => c.type === 'page' && !(
|
|
3049
|
+
const pageCol = collectionDefs.find((c) => c.type === 'page' && !(c.urlPrefix ?? '').replace(/^\/|\/$/g, ''));
|
|
2876
3050
|
matchedCollection = pageCol?.slug ?? 'pages';
|
|
2877
3051
|
docSlug = 'home';
|
|
2878
3052
|
}
|
|
@@ -2918,16 +3092,13 @@ export function registerCMSRoutes(router) {
|
|
|
2918
3092
|
{ slug: 'home' },
|
|
2919
3093
|
{ slug: 'index' },
|
|
2920
3094
|
]
|
|
2921
|
-
: [
|
|
2922
|
-
{ data: { path: ['slug'], equals: docSlug } },
|
|
2923
|
-
{ slug: docSlug },
|
|
2924
|
-
],
|
|
3095
|
+
: [{ data: { path: ['slug'], equals: docSlug } }, { slug: docSlug }],
|
|
2925
3096
|
},
|
|
2926
3097
|
});
|
|
2927
3098
|
if (!doc) {
|
|
2928
3099
|
return errorResponse('Document not found', 404);
|
|
2929
3100
|
}
|
|
2930
|
-
const docData =
|
|
3101
|
+
const docData = doc.data && typeof doc.data === 'object' ? doc.data : {};
|
|
2931
3102
|
const layout = await resolveLayout(pathParam, docData, matchedCollection);
|
|
2932
3103
|
const { _layout: _omit, ...cleanData } = docData;
|
|
2933
3104
|
return json({
|
|
@@ -2979,7 +3150,7 @@ export function registerCMSRoutes(router) {
|
|
|
2979
3150
|
const roleErr = requireRole(auth.session.role, WRITE_ROLES);
|
|
2980
3151
|
if (roleErr)
|
|
2981
3152
|
return roleErr;
|
|
2982
|
-
const body = await request.json();
|
|
3153
|
+
const body = (await request.json());
|
|
2983
3154
|
if (!body.name || !body.scope)
|
|
2984
3155
|
return errorResponse('name and scope are required', 400);
|
|
2985
3156
|
// A child folder MUST live in the same scope as its parent — without
|
|
@@ -3014,7 +3185,7 @@ export function registerCMSRoutes(router) {
|
|
|
3014
3185
|
const roleErr = requireRole(auth.session.role, WRITE_ROLES);
|
|
3015
3186
|
if (roleErr)
|
|
3016
3187
|
return roleErr;
|
|
3017
|
-
const body = await request.json();
|
|
3188
|
+
const body = (await request.json());
|
|
3018
3189
|
const existing = await db().folder.findUnique({ where: { id: params.id } });
|
|
3019
3190
|
if (!existing)
|
|
3020
3191
|
return errorResponse('Folder not found', 404);
|
|
@@ -3064,7 +3235,9 @@ export function registerCMSRoutes(router) {
|
|
|
3064
3235
|
if (!folder)
|
|
3065
3236
|
return errorResponse('Folder not found', 404);
|
|
3066
3237
|
const [docCount, mediaCount, childCount] = await Promise.all([
|
|
3067
|
-
hasModel(d, 'document')
|
|
3238
|
+
hasModel(d, 'document')
|
|
3239
|
+
? d.document.count({ where: { folderId: params.id, deletedAt: null } })
|
|
3240
|
+
: 0,
|
|
3068
3241
|
hasModel(d, 'media') ? d.media.count({ where: { folderId: params.id } }) : 0,
|
|
3069
3242
|
d.folder.count({ where: { parentId: params.id } }),
|
|
3070
3243
|
]);
|
|
@@ -3086,7 +3259,7 @@ export function registerCMSRoutes(router) {
|
|
|
3086
3259
|
const roleErr = requireRole(auth.session.role, WRITE_ROLES);
|
|
3087
3260
|
if (roleErr)
|
|
3088
3261
|
return roleErr;
|
|
3089
|
-
const body = await request.json();
|
|
3262
|
+
const body = (await request.json());
|
|
3090
3263
|
// Confirm the target folder is in the documents scope before moving in.
|
|
3091
3264
|
if (body.folderId) {
|
|
3092
3265
|
const target = await db().folder.findUnique({ where: { id: body.folderId } });
|
|
@@ -3114,7 +3287,7 @@ export function registerCMSRoutes(router) {
|
|
|
3114
3287
|
const roleErr = requireRole(auth.session.role, WRITE_ROLES);
|
|
3115
3288
|
if (roleErr)
|
|
3116
3289
|
return roleErr;
|
|
3117
|
-
const body = await request.json();
|
|
3290
|
+
const body = (await request.json());
|
|
3118
3291
|
if (body.folderId) {
|
|
3119
3292
|
const target = await db().folder.findUnique({ where: { id: body.folderId } });
|
|
3120
3293
|
if (!target)
|
|
@@ -3141,7 +3314,7 @@ export function registerCMSRoutes(router) {
|
|
|
3141
3314
|
const auth = await requireAuth(request);
|
|
3142
3315
|
if (auth.error)
|
|
3143
3316
|
return auth.error;
|
|
3144
|
-
const body = await request.json();
|
|
3317
|
+
const body = (await request.json());
|
|
3145
3318
|
if (!body.collection || !body.documentId) {
|
|
3146
3319
|
return errorResponse('collection and documentId are required', 400);
|
|
3147
3320
|
}
|
|
@@ -3183,7 +3356,7 @@ export function registerCMSRoutes(router) {
|
|
|
3183
3356
|
const auth = await requireAuth(request);
|
|
3184
3357
|
if (auth.error)
|
|
3185
3358
|
return auth.error;
|
|
3186
|
-
const body = await request.json();
|
|
3359
|
+
const body = (await request.json());
|
|
3187
3360
|
if (!body.stage)
|
|
3188
3361
|
return errorResponse('Stage is required', 400);
|
|
3189
3362
|
const { transitionDocument } = await import('../workflow/index.js');
|
|
@@ -3210,7 +3383,9 @@ export function registerCMSRoutes(router) {
|
|
|
3210
3383
|
return errorResponse('Document not found', 404);
|
|
3211
3384
|
const stage = (doc.workflowStage ?? 'DRAFT');
|
|
3212
3385
|
const transitions = getAvailableTransitions(stage, auth.session.role);
|
|
3213
|
-
return json({
|
|
3386
|
+
return json({
|
|
3387
|
+
data: { stage, transitions, reviewerId: doc.reviewerId, reviewNote: doc.reviewNote },
|
|
3388
|
+
});
|
|
3214
3389
|
}
|
|
3215
3390
|
catch (err) {
|
|
3216
3391
|
return internalError(err);
|
|
@@ -3336,16 +3511,12 @@ export function registerCMSRoutes(router) {
|
|
|
3336
3511
|
// allowed. Integrators that want to gate a global must set
|
|
3337
3512
|
// `access.read` explicitly (returning `false` for public).
|
|
3338
3513
|
const readAccess = globalConfig.access?.read;
|
|
3339
|
-
const allowed = readAccess
|
|
3340
|
-
? await readAccess({ user: null, doc })
|
|
3341
|
-
: true;
|
|
3514
|
+
const allowed = readAccess ? await readAccess({ user: null, doc }) : true;
|
|
3342
3515
|
if (!allowed) {
|
|
3343
3516
|
return errorResponse('Forbidden', 403);
|
|
3344
3517
|
}
|
|
3345
3518
|
return json({
|
|
3346
|
-
data: doc.data && typeof doc.data === 'object'
|
|
3347
|
-
? doc.data
|
|
3348
|
-
: {},
|
|
3519
|
+
data: doc.data && typeof doc.data === 'object' ? doc.data : {},
|
|
3349
3520
|
});
|
|
3350
3521
|
}
|
|
3351
3522
|
catch (err) {
|
|
@@ -3376,7 +3547,7 @@ export function registerCMSRoutes(router) {
|
|
|
3376
3547
|
const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
|
|
3377
3548
|
if (roleErr)
|
|
3378
3549
|
return roleErr;
|
|
3379
|
-
const body = await request.json();
|
|
3550
|
+
const body = (await request.json());
|
|
3380
3551
|
const ctx = buildActionContext(auth.session, db());
|
|
3381
3552
|
const global = await updateGlobal(params.slug, body, ctx);
|
|
3382
3553
|
return json({ data: global });
|
|
@@ -3412,7 +3583,7 @@ export function registerCMSRoutes(router) {
|
|
|
3412
3583
|
const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
|
|
3413
3584
|
if (roleErr)
|
|
3414
3585
|
return roleErr;
|
|
3415
|
-
const body = await request.json();
|
|
3586
|
+
const body = (await request.json());
|
|
3416
3587
|
if (!body.url || !body.events?.length) {
|
|
3417
3588
|
return errorResponse('url and events are required', 400);
|
|
3418
3589
|
}
|
|
@@ -3444,7 +3615,7 @@ export function registerCMSRoutes(router) {
|
|
|
3444
3615
|
const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
|
|
3445
3616
|
if (roleErr)
|
|
3446
3617
|
return roleErr;
|
|
3447
|
-
const body = await request.json();
|
|
3618
|
+
const body = (await request.json());
|
|
3448
3619
|
const { updateEndpoint } = await import('../webhooks/index.js');
|
|
3449
3620
|
const updated = await updateEndpoint(params.id, body);
|
|
3450
3621
|
return json({ data: updated });
|
|
@@ -3599,7 +3770,7 @@ export function registerCMSRoutes(router) {
|
|
|
3599
3770
|
const d = db();
|
|
3600
3771
|
if (!hasModel(d, 'scriptTag'))
|
|
3601
3772
|
return modelNotAvailable('ScriptTag');
|
|
3602
|
-
const body = await request.json();
|
|
3773
|
+
const body = (await request.json());
|
|
3603
3774
|
if (!body.name || !body.code || !body.placement) {
|
|
3604
3775
|
return errorResponse('name, code, and placement are required', 400);
|
|
3605
3776
|
}
|
|
@@ -3648,7 +3819,7 @@ export function registerCMSRoutes(router) {
|
|
|
3648
3819
|
const existing = await d.scriptTag.findUnique({ where: { id: params.id } });
|
|
3649
3820
|
if (!existing)
|
|
3650
3821
|
return errorResponse('Script tag not found', 404);
|
|
3651
|
-
const body = await request.json();
|
|
3822
|
+
const body = (await request.json());
|
|
3652
3823
|
const update = {};
|
|
3653
3824
|
if (body.name !== undefined)
|
|
3654
3825
|
update.name = body.name;
|
|
@@ -3830,7 +4001,7 @@ export function registerCMSRoutes(router) {
|
|
|
3830
4001
|
const d = db();
|
|
3831
4002
|
if (!hasModel(d, 'pageTemplate'))
|
|
3832
4003
|
return modelNotAvailable('PageTemplate');
|
|
3833
|
-
const body = await request.json();
|
|
4004
|
+
const body = (await request.json());
|
|
3834
4005
|
if (!body.name)
|
|
3835
4006
|
return errorResponse('name is required', 400);
|
|
3836
4007
|
if (!body.tree)
|
|
@@ -3881,7 +4052,7 @@ export function registerCMSRoutes(router) {
|
|
|
3881
4052
|
return errorResponse('Template not found', 404);
|
|
3882
4053
|
if (existing.builtIn)
|
|
3883
4054
|
return errorResponse('Cannot update built-in templates', 403);
|
|
3884
|
-
const body = await request.json();
|
|
4055
|
+
const body = (await request.json());
|
|
3885
4056
|
const update = {};
|
|
3886
4057
|
if (body.name !== undefined)
|
|
3887
4058
|
update.name = body.name;
|
|
@@ -4029,7 +4200,7 @@ export function registerCMSRoutes(router) {
|
|
|
4029
4200
|
const d = db();
|
|
4030
4201
|
if (!hasModel(d, 'savedSection'))
|
|
4031
4202
|
return modelNotAvailable('SavedSection');
|
|
4032
|
-
const body = await request.json();
|
|
4203
|
+
const body = (await request.json());
|
|
4033
4204
|
if (!body.name)
|
|
4034
4205
|
return errorResponse('name is required', 400);
|
|
4035
4206
|
if (!body.tree)
|
|
@@ -4078,7 +4249,7 @@ export function registerCMSRoutes(router) {
|
|
|
4078
4249
|
const existing = await d.savedSection.findUnique({ where: { id: params.id } });
|
|
4079
4250
|
if (!existing)
|
|
4080
4251
|
return errorResponse('Saved section not found', 404);
|
|
4081
|
-
const body = await request.json();
|
|
4252
|
+
const body = (await request.json());
|
|
4082
4253
|
const update = {};
|
|
4083
4254
|
if (body.name !== undefined)
|
|
4084
4255
|
update.name = body.name;
|
|
@@ -4295,7 +4466,11 @@ export function registerCMSRoutes(router) {
|
|
|
4295
4466
|
await logEvent({
|
|
4296
4467
|
event: 'settings_changed',
|
|
4297
4468
|
userId: auth.session.userId,
|
|
4298
|
-
details: {
|
|
4469
|
+
details: {
|
|
4470
|
+
action: 'a11y_auto_fix',
|
|
4471
|
+
fixedCount: result.report.fixedCount,
|
|
4472
|
+
remainingCount: result.report.remainingCount,
|
|
4473
|
+
},
|
|
4299
4474
|
});
|
|
4300
4475
|
return json({ data: result });
|
|
4301
4476
|
}
|