@beechcms/api 0.4.3 → 0.6.0-preview.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/assets/dashboard/Searching.svg +1 -0
- package/assets/dashboard/assets/index-BSjk0Sx9.js +634 -0
- package/assets/dashboard/assets/index-kRCYVN2B.css +1 -0
- package/assets/dashboard/index.html +2 -2
- package/assets/dashboard/noResult.svg +43 -0
- package/assets/dashboard/working.svg +1 -0
- package/migrations/0000_v040_base.sql +27 -0
- package/migrations/0030_test_seeds.sql +125 -0
- package/package.json +14 -9
- package/src/auth/{in-memory-hash-provider.ts → __fixtures__/in-memory-hash-provider.ts} +4 -0
- package/src/auth/{static-token-service.ts → __fixtures__/static-token-service.ts} +4 -0
- package/src/auth/bcrypt-hash-provider.ts +6 -2
- package/src/auth/constants.ts +4 -0
- package/src/auth/generate-refresh-token.test.ts +8 -4
- package/src/auth/hash-provider.test.ts +14 -5
- package/src/auth/jose-token-service.ts +7 -0
- package/src/auth/jwt-claims-passthrough.test.ts +87 -0
- package/src/auth/login.test.ts +9 -1
- package/src/auth/login.ts +5 -1
- package/src/auth/refresh.ts +7 -1
- package/src/auth/token-service.test.ts +9 -1
- package/src/factory.ts +37 -16
- package/src/features/automations/__tests__/action-executors.test.ts +59 -72
- package/src/features/automations/__tests__/automation-runner.test.ts +4 -0
- package/src/features/automations/__tests__/automation-runner.utils.test.ts +4 -0
- package/src/features/automations/__tests__/automations.handler.test.ts +7 -3
- package/src/features/automations/__tests__/automations.repository.test.ts +4 -0
- package/src/features/automations/__tests__/automations.schema.test.ts +92 -2
- package/src/features/automations/__tests__/context-resolver.test.ts +4 -0
- package/src/features/automations/__tests__/cron-runner.test.ts +11 -7
- package/src/features/automations/__tests__/cron-runner.utils.test.ts +4 -0
- package/src/features/automations/__tests__/set-variable.executor.test.ts +77 -0
- package/src/features/automations/__tests__/template-grammar.test.ts +4 -0
- package/src/features/automations/__tests__/webhook.executor.test.ts +142 -0
- package/src/features/automations/__tests__/when-evaluator.test.ts +4 -0
- package/src/features/automations/__tests__/when-pushdown.test.ts +5 -1
- package/src/features/automations/action-executors/create-entry.executor.ts +4 -0
- package/src/features/automations/action-executors/edit-field.executor.ts +4 -0
- package/src/features/automations/action-executors/index.ts +5 -1
- package/src/features/automations/action-executors/send-mail.executor.ts +13 -2
- package/src/features/automations/action-executors/set-variable.executor.ts +15 -2
- package/src/features/automations/action-executors/webhook.executor.ts +55 -13
- package/src/features/automations/automation-runner.ts +4 -0
- package/src/features/automations/automation-runner.utils.ts +4 -0
- package/src/features/automations/automations.handler.ts +4 -0
- package/src/features/automations/automations.schema.ts +19 -2
- package/src/features/automations/context-resolver.ts +4 -0
- package/src/features/automations/cron-runner.ts +4 -0
- package/src/features/automations/cron-runner.utils.ts +4 -0
- package/src/features/automations/filter-translation.ts +4 -0
- package/src/features/automations/index.ts +4 -0
- package/src/features/automations/template-grammar.ts +4 -0
- package/src/features/automations/var-access-resolver.ts +4 -0
- package/src/features/automations/when-evaluator.ts +4 -0
- package/src/features/automations/when-pushdown.ts +4 -0
- package/src/features/backrefs/__tests__/backrefs.handler.test.ts +359 -0
- package/src/features/backrefs/backrefs.handler.ts +168 -0
- package/src/features/backrefs/d1-backref.repository.ts +84 -0
- package/src/features/backrefs/index.ts +5 -0
- package/src/features/content/constants.ts +6 -0
- package/src/features/content/handlers/bulk.handler.ts +185 -0
- package/src/features/content/handlers/create.ts +19 -55
- package/src/features/content/handlers/delete.ts +12 -37
- package/src/features/content/handlers/facets.ts +4 -0
- package/src/features/content/handlers/get.ts +4 -0
- package/src/features/content/handlers/helpers.test.ts +180 -0
- package/src/features/content/handlers/helpers.ts +93 -0
- package/src/features/content/handlers/list.ts +105 -19
- package/src/features/content/handlers/update.ts +19 -64
- package/src/features/content/index.ts +6 -0
- package/src/features/draft/draft.handler.ts +33 -8
- package/src/features/draft/draft.middleware.ts +4 -0
- package/src/features/draft/index.ts +4 -0
- package/src/features/email/email.provider.ts +4 -0
- package/src/features/email/email.service.ts +50 -45
- package/src/features/email/email.types.ts +12 -1
- package/src/features/email/index.ts +4 -0
- package/src/features/email/providers/resend.ts +4 -0
- package/src/features/email/providers/smtp.ts +55 -0
- package/src/features/email/templates/automation-mail.ts +4 -0
- package/src/features/email/templates/password-changed.ts +4 -0
- package/src/features/email/templates/password-reset.ts +4 -0
- package/src/features/email/templates/shell.ts +4 -0
- package/src/features/notifications/index.ts +4 -0
- package/src/features/notifications/notifications.handler.ts +4 -0
- package/src/features/password-reset/index.ts +5 -1
- package/src/features/password-reset/request.ts +12 -2
- package/src/features/password-reset/reset.ts +12 -2
- package/src/features/rotate-field/index.ts +4 -0
- package/src/features/rotate-field/rotate-field.handler.ts +4 -0
- package/src/features/rotate-field/rotate-field.schema.ts +4 -0
- package/src/features/schema/schema.handler.ts +118 -3
- package/src/features/seeds/index.ts +5 -0
- package/src/features/seeds/seeds.handler.test.ts +632 -0
- package/src/features/seeds/seeds.handler.ts +693 -0
- package/src/features/settings/__tests__/settings.handler.test.ts +555 -0
- package/src/features/settings/settings.handler.ts +98 -8
- package/src/features/setup/index.ts +124 -11
- package/src/features/stats/index.ts +4 -0
- package/src/features/stats/stats.handler.ts +11 -21
- package/src/features/webhooks/index.ts +63 -0
- package/src/index.ts +28 -19
- package/src/media-utils.ts +4 -0
- package/src/middleware/auth-providers.middleware.ts +13 -0
- package/src/middleware/observability.middleware.ts +21 -3
- package/src/middleware/rate-limit.middleware.ts +4 -0
- package/src/middleware/repository.middleware.ts +25 -2
- package/src/middleware/seed-registry.middleware.test.ts +97 -0
- package/src/middleware/seed-registry.middleware.ts +21 -0
- package/src/middleware/storage.middleware.ts +4 -0
- package/src/middleware.ts +5 -7
- package/src/public/access-policy.ts +4 -0
- package/src/public/api-key-middleware.ts +4 -0
- package/src/public/cache-utils.ts +38 -34
- package/src/public/entry-projection.ts +46 -42
- package/src/public/idempotency.ts +23 -19
- package/src/public/index.ts +4 -0
- package/src/public/problem-details.ts +71 -0
- package/src/public/public-add.ts +4 -0
- package/src/public/public-edit.ts +4 -0
- package/src/public/public-errors.ts +4 -0
- package/src/public/public-read.ts +4 -0
- package/src/public/public-routes.ts +4 -0
- package/src/public/query-builder.test.ts +4 -0
- package/src/public/query-builder.ts +4 -0
- package/src/public/rate-limit-middleware.ts +4 -0
- package/src/public/read-list.ts +54 -50
- package/src/public/read-single.ts +48 -44
- package/src/public/response-builder.ts +4 -0
- package/src/public/sanitize.ts +4 -0
- package/src/public/slug-utils.ts +4 -0
- package/src/rate-limit/cloudflare-rate-limiter.test.ts +4 -0
- package/src/rate-limit/cloudflare-rate-limiter.ts +4 -0
- package/src/rate-limit/in-memory-rate-limiter.test.ts +4 -0
- package/src/rate-limit/in-memory-rate-limiter.ts +4 -0
- package/src/rate-limit/no-op-rate-limiter.test.ts +4 -0
- package/src/rate-limit/no-op-rate-limiter.ts +4 -0
- package/src/search-utils.test.ts +4 -0
- package/src/search-utils.ts +5 -1
- package/src/search.ts +5 -1
- package/src/shared/apply-policies.test.ts +4 -0
- package/src/shared/apply-policies.ts +4 -0
- package/src/shared/automations.repository.d1.ts +4 -0
- package/src/shared/background-notification-service.test.ts +4 -0
- package/src/shared/background-notification-service.ts +4 -0
- package/src/shared/base.repository.d1.ts +4 -0
- package/src/shared/content-utils.test.ts +4 -0
- package/src/shared/content-utils.ts +4 -0
- package/src/shared/content.repository.d1.test.ts +147 -1
- package/src/shared/content.repository.d1.ts +535 -66
- package/src/shared/d1-activity-log.repository.test.ts +4 -0
- package/src/shared/d1-activity-log.repository.ts +4 -0
- package/src/shared/d1-activity-logger.test.ts +4 -0
- package/src/shared/d1-activity-logger.ts +4 -0
- package/src/shared/d1-analytics.repository.test.ts +4 -0
- package/src/shared/d1-analytics.repository.ts +4 -0
- package/src/shared/d1-content-scan.repository.test.ts +9 -5
- package/src/shared/d1-content-scan.repository.ts +15 -7
- package/src/shared/d1-notification.repository.test.ts +4 -0
- package/src/shared/d1-notification.repository.ts +4 -0
- package/src/shared/d1-password-reset-token.repository.test.ts +4 -0
- package/src/shared/d1-password-reset-token.repository.ts +4 -0
- package/src/shared/d1-search.repository.test.ts +4 -0
- package/src/shared/d1-search.repository.ts +4 -0
- package/src/shared/d1-session.repository.test.ts +4 -0
- package/src/shared/d1-session.repository.ts +4 -0
- package/src/shared/d1-setup-checklist.repository.ts +36 -0
- package/src/shared/d1-user.repository.test.ts +8 -4
- package/src/shared/d1-user.repository.ts +15 -5
- package/src/shared/d1-widget.repository.test.ts +4 -0
- package/src/shared/d1-widget.repository.ts +4 -0
- package/src/shared/demo-data-sql.ts +54 -0
- package/src/shared/demo-data.repository.d1.ts +20 -0
- package/src/shared/execution-context-scheduler.ts +4 -0
- package/src/shared/fixed-clock.ts +4 -0
- package/src/shared/fts-sync.ts +4 -0
- package/src/shared/idempotency.repository.d1.test.ts +4 -0
- package/src/shared/idempotency.repository.d1.ts +4 -0
- package/src/shared/in-memory-activity-logger.ts +4 -0
- package/src/shared/in-memory-notification-service.ts +4 -0
- package/src/shared/in-memory-seed.repository.ts +51 -0
- package/src/shared/media.repository.d1.test.ts +4 -0
- package/src/shared/media.repository.d1.ts +4 -0
- package/src/shared/qstash-notification-service.test.ts +67 -0
- package/src/shared/qstash-notification-service.ts +55 -0
- package/src/shared/query-utils.ts +4 -0
- package/src/shared/request-utils.ts +4 -0
- package/src/shared/schema-mutator.d1.ts +64 -0
- package/src/shared/seed-layout.repository.d1.test.ts +114 -0
- package/src/shared/seed-layout.repository.d1.ts +62 -0
- package/src/shared/seed-registry-cache.test.ts +68 -0
- package/src/shared/seed-registry-cache.ts +49 -0
- package/src/shared/seed.repository.d1.test.ts +143 -0
- package/src/shared/seed.repository.d1.ts +98 -0
- package/src/shared/sequential-id-generator.ts +11 -0
- package/src/shared/site-settings.repository.d1.ts +53 -0
- package/src/shared/storage/factory.ts +16 -15
- package/src/shared/storage/s3-bucket.ts +34 -2
- package/src/shared/storage-utils.ts +4 -0
- package/src/shared/system-stats.repository.d1.test.ts +4 -0
- package/src/shared/system-stats.repository.d1.ts +5 -1
- package/src/types.ts +23 -3
- package/src/upload.ts +134 -123
- package/src/widget.ts +6 -2
- package/assets/dashboard/assets/index-BKWnlvnV.css +0 -1
- package/assets/dashboard/assets/index-BMkd1Irh.js +0 -629
- package/src/shared/storage/r2-binding-bucket.ts +0 -81
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
|
|
3
|
+
// See LICENSE in the repository root for license terms.
|
|
4
|
+
|
|
1
5
|
/// <reference types="@cloudflare/workers-types" />
|
|
2
6
|
import { Hono } from 'hono'
|
|
3
7
|
import type { Env, Variables } from '../../types'
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
|
|
3
|
+
// See LICENSE in the repository root for license terms.
|
|
4
|
+
|
|
1
5
|
/// <reference types="@cloudflare/workers-types" />
|
|
2
6
|
import { Hono } from 'hono'
|
|
3
7
|
import type { Env, Variables } from '../../types'
|
|
@@ -8,7 +12,7 @@ export const passwordResetApp = new Hono<{ Bindings: Env; Variables: Variables }
|
|
|
8
12
|
|
|
9
13
|
// Feature flag: lets the dashboard know whether to show the forgot-password link
|
|
10
14
|
passwordResetApp.get('/auth/features', (context) => {
|
|
11
|
-
return context.json({ passwordReset: Boolean(context.env.RESEND_API_KEY) })
|
|
15
|
+
return context.json({ passwordReset: context.env.EMAIL_PROVIDER === 'smtp' || Boolean(context.env.RESEND_API_KEY) })
|
|
12
16
|
})
|
|
13
17
|
|
|
14
18
|
passwordResetApp.post('/auth/forgot-password', requestPasswordReset)
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
|
|
3
|
+
// See LICENSE in the repository root for license terms.
|
|
4
|
+
|
|
1
5
|
/// <reference types="@cloudflare/workers-types" />
|
|
2
6
|
import type { Context } from 'hono'
|
|
3
7
|
import type { Env, Variables } from '../../types'
|
|
@@ -16,7 +20,8 @@ export async function requestPasswordReset(
|
|
|
16
20
|
): Promise<Response> {
|
|
17
21
|
const { env, req } = context
|
|
18
22
|
|
|
19
|
-
|
|
23
|
+
const useSmtp = env.EMAIL_PROVIDER === 'smtp'
|
|
24
|
+
if (!useSmtp && !env.RESEND_API_KEY) {
|
|
20
25
|
return context.json({ error: 'Service not available' }, 503)
|
|
21
26
|
}
|
|
22
27
|
|
|
@@ -63,14 +68,19 @@ export async function requestPasswordReset(
|
|
|
63
68
|
const baseUrl = (env.APP_URL ?? new URL(req.url).origin).replace(/\/$/, '')
|
|
64
69
|
const resetUrl = `${baseUrl}/admin/reset-password?token=${resetToken}`
|
|
65
70
|
|
|
71
|
+
const smtpBaseUrl = env.SMTP_HOST
|
|
72
|
+
? `http://${env.SMTP_HOST}:${env.SMTP_PORT ?? '8025'}`
|
|
73
|
+
: undefined
|
|
66
74
|
try {
|
|
67
75
|
await sendPasswordResetEmail({
|
|
68
76
|
to: normalizedEmail,
|
|
69
77
|
resetUrl,
|
|
70
78
|
locale: emailLocale,
|
|
71
|
-
apiKey: env.RESEND_API_KEY,
|
|
79
|
+
apiKey: env.RESEND_API_KEY ?? '',
|
|
72
80
|
from: env.EMAIL_FROM,
|
|
73
81
|
isDev: env.ENV !== 'production',
|
|
82
|
+
provider: env.EMAIL_PROVIDER as 'smtp' | 'resend' | undefined,
|
|
83
|
+
smtpBaseUrl,
|
|
74
84
|
})
|
|
75
85
|
} catch (error) {
|
|
76
86
|
if (env.ENV !== 'production') {
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
|
|
3
|
+
// See LICENSE in the repository root for license terms.
|
|
4
|
+
|
|
1
5
|
/// <reference types="@cloudflare/workers-types" />
|
|
2
6
|
import type { Context } from 'hono'
|
|
3
7
|
import type { Env, Variables } from '../../types'
|
|
@@ -16,7 +20,8 @@ export async function resetPassword(
|
|
|
16
20
|
): Promise<Response> {
|
|
17
21
|
const { env, req, executionCtx } = context
|
|
18
22
|
|
|
19
|
-
|
|
23
|
+
const useSmtp = env.EMAIL_PROVIDER === 'smtp'
|
|
24
|
+
if (!useSmtp && !env.RESEND_API_KEY) {
|
|
20
25
|
return context.json({ error: 'Service not available' }, 503)
|
|
21
26
|
}
|
|
22
27
|
|
|
@@ -65,14 +70,19 @@ export async function resetPassword(
|
|
|
65
70
|
await context.get('userRepository').updatePasswordHash(tokenRecord.userId, hashedNewPassword)
|
|
66
71
|
await context.get('sessionRepository').revokeAllForUser(tokenRecord.userId, nowTimestamp)
|
|
67
72
|
|
|
73
|
+
const smtpBaseUrl = env.SMTP_HOST
|
|
74
|
+
? `http://${env.SMTP_HOST}:${env.SMTP_PORT ?? '8025'}`
|
|
75
|
+
: undefined
|
|
68
76
|
const sendNotification = async () => {
|
|
69
77
|
try {
|
|
70
78
|
await sendPasswordChangedEmail({
|
|
71
79
|
to: tokenRecord.email,
|
|
72
80
|
locale: emailLocale,
|
|
73
|
-
apiKey: env.RESEND_API_KEY
|
|
81
|
+
apiKey: env.RESEND_API_KEY ?? '',
|
|
74
82
|
from: env.EMAIL_FROM,
|
|
75
83
|
isDev: env.ENV !== 'production',
|
|
84
|
+
provider: env.EMAIL_PROVIDER as 'smtp' | 'resend' | undefined,
|
|
85
|
+
smtpBaseUrl,
|
|
76
86
|
})
|
|
77
87
|
} catch (error) {
|
|
78
88
|
if (env.ENV !== 'production') {
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
|
|
3
|
+
// See LICENSE in the repository root for license terms.
|
|
4
|
+
|
|
1
5
|
/// <reference types="@cloudflare/workers-types" />
|
|
2
6
|
import { Hono } from 'hono'
|
|
3
7
|
import { resolvePolicies, verifyHashField, sha256hex, validateAndSanitizeSeedPayload, EntryNotFoundError } from '@beechcms/core'
|
|
@@ -1,16 +1,131 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
|
|
3
|
+
// See LICENSE in the repository root for license terms.
|
|
4
|
+
|
|
1
5
|
/// <reference types="@cloudflare/workers-types" />
|
|
2
6
|
import { Hono } from 'hono'
|
|
7
|
+
import { canEditLayout, formLayoutSchema, validateLayoutAgainstSeed } from '@beechcms/core'
|
|
8
|
+
import { publicProblem } from '../../public/problem-details'
|
|
9
|
+
import type { Context } from 'hono'
|
|
3
10
|
import type { Env, Variables } from '../../types'
|
|
4
11
|
|
|
12
|
+
type AppContext = Context<{ Bindings: Env; Variables: Variables }>
|
|
13
|
+
|
|
14
|
+
/** Returns a 403 response if the caller lacks layout-edit permission, otherwise undefined. */
|
|
15
|
+
function requireLayoutEditPermission(context: AppContext) {
|
|
16
|
+
const role = context.get('jwtPayload')?.role
|
|
17
|
+
if (!canEditLayout(role)) {
|
|
18
|
+
return publicProblem(context, {
|
|
19
|
+
type: 'forbidden',
|
|
20
|
+
title: 'Forbidden',
|
|
21
|
+
status: 403,
|
|
22
|
+
detail: 'Layout edit requires admin role.',
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
return undefined
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Resolves the seed by slug. Returns a 404 response if not found, otherwise the seed. */
|
|
29
|
+
function requireSeedBySlug(context: AppContext, slug: string) {
|
|
30
|
+
const seed = context.get('seedRegistry').get(slug)
|
|
31
|
+
if (!seed) {
|
|
32
|
+
return {
|
|
33
|
+
seed: null,
|
|
34
|
+
error: publicProblem(context, {
|
|
35
|
+
type: 'seed-not-found',
|
|
36
|
+
title: 'Seed not found',
|
|
37
|
+
status: 404,
|
|
38
|
+
detail: `No seed with slug '${slug}'.`,
|
|
39
|
+
}),
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { seed, error: null }
|
|
43
|
+
}
|
|
44
|
+
|
|
5
45
|
const schemaApp = new Hono<{ Bindings: Env; Variables: Variables }>()
|
|
6
46
|
|
|
7
47
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
48
|
+
* Returns the full CMS schema (all configured Seeds), each optionally enriched
|
|
49
|
+
* with a stored custom FormLayout. Used by the Dashboard to drive menus and forms.
|
|
10
50
|
*/
|
|
11
51
|
schemaApp.get('/', async (context) => {
|
|
12
52
|
const registry = context.get('seedRegistry')
|
|
13
|
-
|
|
53
|
+
const layouts = await context.get('seedLayoutRepository').getAllAsMap()
|
|
54
|
+
|
|
55
|
+
const enriched = registry.all().map((seed) => {
|
|
56
|
+
const stored = layouts.get(seed.slug)
|
|
57
|
+
if (!stored) return seed
|
|
58
|
+
const result = validateLayoutAgainstSeed(stored, seed)
|
|
59
|
+
return { ...seed, layout: result.cleaned }
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
return context.json(enriched)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Upserts a custom FormLayout for a Seed.
|
|
67
|
+
* Admin-only. Validates shape (Zod) then semantic constraints against the Seed.
|
|
68
|
+
*/
|
|
69
|
+
schemaApp.put('/:slug/layout', async (context) => {
|
|
70
|
+
const forbidden = requireLayoutEditPermission(context)
|
|
71
|
+
if (forbidden) return forbidden
|
|
72
|
+
|
|
73
|
+
const slug = context.req.param('slug')
|
|
74
|
+
const { seed, error: seedError } = requireSeedBySlug(context, slug)
|
|
75
|
+
if (seedError) return seedError
|
|
76
|
+
|
|
77
|
+
let body: unknown
|
|
78
|
+
try {
|
|
79
|
+
body = await context.req.json()
|
|
80
|
+
} catch {
|
|
81
|
+
return publicProblem(context, {
|
|
82
|
+
type: 'invalid-json',
|
|
83
|
+
title: 'Invalid JSON',
|
|
84
|
+
status: 400,
|
|
85
|
+
detail: 'Body must be valid JSON.',
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const parsed = formLayoutSchema.safeParse(body)
|
|
90
|
+
if (!parsed.success) {
|
|
91
|
+
return publicProblem(context, {
|
|
92
|
+
type: 'invalid-layout',
|
|
93
|
+
title: 'Invalid layout',
|
|
94
|
+
status: 422,
|
|
95
|
+
detail: parsed.error.message,
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const semantic = validateLayoutAgainstSeed(parsed.data, seed!)
|
|
100
|
+
if (!semantic.ok) {
|
|
101
|
+
return publicProblem(context, {
|
|
102
|
+
type: 'invalid-layout',
|
|
103
|
+
title: 'Invalid layout',
|
|
104
|
+
status: 422,
|
|
105
|
+
detail: semantic.errors.join('; '),
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const userId = context.get('jwtPayload')?.sub ?? 'unknown'
|
|
110
|
+
await context.get('seedLayoutRepository').upsert(slug, semantic.cleaned, userId)
|
|
111
|
+
|
|
112
|
+
return context.json({ ok: true, layout: semantic.cleaned })
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Removes the stored layout for a Seed, reverting to the generated default.
|
|
117
|
+
* Admin-only. The "Reset" action in the Layout Builder calls this endpoint.
|
|
118
|
+
*/
|
|
119
|
+
schemaApp.delete('/:slug/layout', async (context) => {
|
|
120
|
+
const forbidden = requireLayoutEditPermission(context)
|
|
121
|
+
if (forbidden) return forbidden
|
|
122
|
+
|
|
123
|
+
const slug = context.req.param('slug')
|
|
124
|
+
const { error: seedError } = requireSeedBySlug(context, slug)
|
|
125
|
+
if (seedError) return seedError
|
|
126
|
+
|
|
127
|
+
await context.get('seedLayoutRepository').remove(slug)
|
|
128
|
+
return context.json({ ok: true })
|
|
14
129
|
})
|
|
15
130
|
|
|
16
131
|
export { schemaApp }
|