@setzkasten-cms/astro 0.4.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/LICENSE ADDED
@@ -0,0 +1,37 @@
1
+ Setzkasten Community License
2
+
3
+ Copyright (c) 2026 Lilapixel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to use,
7
+ copy, modify, merge, publish, and distribute the Software, subject to the
8
+ following conditions:
9
+
10
+ 1. The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ 2. The Software may not be used for commercial purposes without a separate
14
+ commercial license from the copyright holder. "Commercial purposes" means
15
+ any use of the Software that is primarily intended for or directed toward
16
+ commercial advantage or monetary compensation. This includes, but is not
17
+ limited to:
18
+ - Using the Software to manage content for a commercial website or product
19
+ - Offering the Software as part of a paid service
20
+ - Using the Software within a for-profit organization
21
+
22
+ 3. Non-commercial use is permitted without restriction. This includes:
23
+ - Personal projects
24
+ - Open source projects
25
+ - Educational and academic use
26
+ - Non-profit organizations
27
+
28
+ 4. A commercial license ("Enterprise License") may be obtained by contacting
29
+ Lilapixel at hello@lilapixel.de.
30
+
31
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
32
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
33
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
34
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
35
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
36
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
37
+ SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@setzkasten-cms/astro",
3
+ "version": "0.4.2",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./src/index.ts",
7
+ "./auth-login": "./src/api-routes/auth-login.ts",
8
+ "./auth-callback": "./src/api-routes/auth-callback.ts",
9
+ "./auth-logout": "./src/api-routes/auth-logout.ts",
10
+ "./auth-session": "./src/api-routes/auth-session.ts",
11
+ "./github-proxy": "./src/api-routes/github-proxy.ts",
12
+ "./asset-proxy": "./src/api-routes/asset-proxy.ts",
13
+ "./draft": "./src/api-routes/draft.ts",
14
+ "./preview-middleware": "./src/preview-middleware.ts",
15
+ "./config": "./src/api-routes/config.ts",
16
+ "./pages": "./src/api-routes/pages.ts",
17
+ "./admin-page": "./src/admin-page.astro"
18
+ },
19
+ "dependencies": {
20
+ "@setzkasten-cms/core": "0.4.2",
21
+ "@setzkasten-cms/ui": "0.4.2",
22
+ "@setzkasten-cms/github-adapter": "0.4.2",
23
+ "@setzkasten-cms/auth": "0.4.2"
24
+ },
25
+ "peerDependencies": {
26
+ "astro": "^5.0.0",
27
+ "react": "^19.0.0",
28
+ "react-dom": "^19.0.0"
29
+ },
30
+ "scripts": {
31
+ "typecheck": "tsc --noEmit"
32
+ }
33
+ }
@@ -0,0 +1,146 @@
1
+ ---
2
+ /**
3
+ * Admin SPA shell – served at /admin/[...path]
4
+ * Mounts the React-based AdminApp as a client-side SPA.
5
+ */
6
+ ---
7
+
8
+ <!doctype html>
9
+ <html lang="de">
10
+ <head>
11
+ <meta charset="UTF-8" />
12
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
13
+ <meta name="robots" content="noindex, nofollow" />
14
+ <title>Setzkasten Admin</title>
15
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
16
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
17
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
18
+ <link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;0,9..40,800;1,9..40,400&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
19
+ <style>
20
+ * { box-sizing: border-box; margin: 0; padding: 0; }
21
+ body {
22
+ font-family: 'DM Sans', system-ui, sans-serif;
23
+ color: #1a1a2e;
24
+ background: #faf9f7;
25
+ -webkit-font-smoothing: antialiased;
26
+ }
27
+ .sk-boot-loading {
28
+ display: flex;
29
+ align-items: center;
30
+ justify-content: center;
31
+ min-height: 100vh;
32
+ flex-direction: column;
33
+ gap: 16px;
34
+ }
35
+ .sk-boot-spinner {
36
+ width: 32px;
37
+ height: 32px;
38
+ border: 3px solid #e2ddd7;
39
+ border-top-color: #c45d3e;
40
+ border-radius: 50%;
41
+ animation: sk-boot-spin 0.8s linear infinite;
42
+ }
43
+ @keyframes sk-boot-spin {
44
+ to { transform: rotate(360deg); }
45
+ }
46
+ </style>
47
+ </head>
48
+
49
+ <body>
50
+ <div id="setzkasten-admin">
51
+ <div class="sk-boot-loading">
52
+ <div class="sk-boot-spinner"></div>
53
+ <div style="font-size: 14px; color: #64748b;">Lade Setzkasten...</div>
54
+ </div>
55
+ </div>
56
+
57
+ <script>
58
+ import { createElement } from 'react'
59
+ import { createRoot } from 'react-dom/client'
60
+ import { SetzKastenProvider, AdminApp, ProxyContentRepository, ProxyAssetStore } from '@setzkasten-cms/ui'
61
+ import '@setzkasten-cms/ui/styles/admin.css'
62
+
63
+ async function boot() {
64
+ const root = document.getElementById('setzkasten-admin')
65
+ if (!root) return
66
+
67
+ try {
68
+ const injected = (globalThis as any).__SETZKASTEN_CONFIG__ ?? {}
69
+
70
+ const providers: Array<'github' | 'google'> = []
71
+ if (injected.hasGitHub) providers.push('github')
72
+ if (injected.hasGoogle) providers.push('google')
73
+ if (providers.length === 0) providers.push('github')
74
+
75
+ // Fetch the full user config from server
76
+ let userConfig: any = null
77
+ try {
78
+ const res = await fetch('/api/setzkasten/config')
79
+ if (res.ok) userConfig = await res.json()
80
+ } catch {}
81
+
82
+ const skConfig = userConfig ?? {
83
+ storage: { kind: 'github' as const },
84
+ auth: { providers },
85
+ theme: {},
86
+ products: {},
87
+ collections: {},
88
+ }
89
+
90
+ // Storage params come from the config API (server-injected via SSR)
91
+ const storage = userConfig?._storage ?? injected.storage ?? {}
92
+ const owner = storage.owner ?? ''
93
+ const repo = storage.repo ?? ''
94
+ const branch = storage.branch ?? 'main'
95
+ const contentPath = storage.contentPath ?? 'content'
96
+
97
+ const repository = new ProxyContentRepository({
98
+ proxyBaseUrl: '/api/setzkasten/github',
99
+ owner,
100
+ repo,
101
+ branch,
102
+ contentPath,
103
+ })
104
+
105
+ const assetsPath = storage.assetsPath ?? 'public/images'
106
+ const assets = new ProxyAssetStore({
107
+ proxyBaseUrl: '/api/setzkasten/github',
108
+ owner,
109
+ repo,
110
+ branch,
111
+ assetsPath,
112
+ publicUrlPrefix: '/images',
113
+ })
114
+
115
+ const auth = {
116
+ async login() { window.location.href = '/api/setzkasten/auth/login?provider=github' },
117
+ async logout() { window.location.href = '/api/setzkasten/auth/logout' },
118
+ async getSession() {
119
+ const res = await fetch('/api/setzkasten/auth/session')
120
+ return res.json()
121
+ },
122
+ }
123
+
124
+ const reactRoot = createRoot(root)
125
+ reactRoot.render(
126
+ createElement(
127
+ SetzKastenProvider,
128
+ { config: skConfig, repository, auth, assets },
129
+ createElement(AdminApp)
130
+ )
131
+ )
132
+ } catch (error) {
133
+ console.error('[setzkasten] Boot failed:', error)
134
+ root.innerHTML = `
135
+ <div style="display:flex;align-items:center;justify-content:center;min-height:100vh;flex-direction:column;gap:16px;">
136
+ <p style="color:#ef4444;font-size:14px;">Fehler beim Laden des Admin-Panels.</p>
137
+ <a href="/" style="color:#64748b;font-size:13px;">Zur Startseite</a>
138
+ </div>
139
+ `
140
+ }
141
+ }
142
+
143
+ boot()
144
+ </script>
145
+ </body>
146
+ </html>
@@ -0,0 +1,76 @@
1
+ import type { APIRoute } from 'astro'
2
+
3
+ /**
4
+ * Asset proxy – serves images from the private GitHub repo.
5
+ * Used by the admin UI to display image thumbnails without exposing the GitHub token.
6
+ *
7
+ * GET /api/setzkasten/asset/public/images/about/LP_Logo.png
8
+ * → fetches from GitHub API and returns the raw binary with correct Content-Type.
9
+ */
10
+ export const GET: APIRoute = async ({ params, cookies }) => {
11
+ const session = cookies.get('setzkasten_session')?.value
12
+ if (!session) {
13
+ return new Response('Unauthorized', { status: 401 })
14
+ }
15
+
16
+ const assetPath = params.path
17
+ if (!assetPath) {
18
+ return new Response('Missing path', { status: 400 })
19
+ }
20
+
21
+ const githubToken = import.meta.env.GITHUB_TOKEN ?? process.env.GITHUB_TOKEN
22
+ if (!githubToken) {
23
+ return new Response('GitHub token not configured', { status: 500 })
24
+ }
25
+
26
+ const config = (globalThis as any).__SETZKASTEN_CONFIG__
27
+ if (!config?.storage) {
28
+ return new Response('Storage not configured', { status: 500 })
29
+ }
30
+
31
+ const { owner, repo, branch } = config.storage
32
+ const githubUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${assetPath}?ref=${branch}`
33
+
34
+ try {
35
+ const response = await fetch(githubUrl, {
36
+ headers: {
37
+ Authorization: `Bearer ${githubToken}`,
38
+ Accept: 'application/vnd.github.raw+json',
39
+ 'X-GitHub-Api-Version': '2022-11-28',
40
+ },
41
+ })
42
+
43
+ if (!response.ok) {
44
+ return new Response('Asset not found', { status: response.status })
45
+ }
46
+
47
+ const contentType = guessMimeType(assetPath)
48
+ const body = await response.arrayBuffer()
49
+
50
+ return new Response(body, {
51
+ status: 200,
52
+ headers: {
53
+ 'Content-Type': contentType,
54
+ 'Cache-Control': 'private, max-age=300',
55
+ },
56
+ })
57
+ } catch (error) {
58
+ console.error('[setzkasten] Asset proxy error:', error)
59
+ return new Response('Failed to fetch asset', { status: 502 })
60
+ }
61
+ }
62
+
63
+ function guessMimeType(path: string): string {
64
+ const ext = path.split('.').pop()?.toLowerCase()
65
+ const types: Record<string, string> = {
66
+ jpg: 'image/jpeg',
67
+ jpeg: 'image/jpeg',
68
+ png: 'image/png',
69
+ gif: 'image/gif',
70
+ webp: 'image/webp',
71
+ avif: 'image/avif',
72
+ svg: 'image/svg+xml',
73
+ pdf: 'application/pdf',
74
+ }
75
+ return types[ext ?? ''] ?? 'application/octet-stream'
76
+ }
@@ -0,0 +1,105 @@
1
+ import type { APIRoute } from 'astro'
2
+ import { createGitHubAuth } from '@setzkasten-cms/auth'
3
+ import { createGoogleAuth } from '@setzkasten-cms/auth'
4
+
5
+ /**
6
+ * OAuth callback handler.
7
+ * Receives the authorization code from GitHub/Google, exchanges it for a
8
+ * session via the auth adapter, and sets a signed session cookie.
9
+ */
10
+ export const GET: APIRoute = async ({ request, url, cookies, redirect }) => {
11
+ const code = url.searchParams.get('code')
12
+ const state = url.searchParams.get('state')
13
+
14
+ if (!code) {
15
+ return new Response('Missing authorization code', { status: 400 })
16
+ }
17
+
18
+ // Parse stored state (may contain provider info)
19
+ const storedRaw = cookies.get('setzkasten_oauth_state')?.value
20
+ let storedState: string | undefined
21
+ let provider = 'github'
22
+ if (storedRaw) {
23
+ try {
24
+ const parsed = JSON.parse(storedRaw)
25
+ storedState = parsed.state
26
+ provider = parsed.provider ?? 'github'
27
+ } catch {
28
+ storedState = storedRaw
29
+ }
30
+ }
31
+
32
+ // CSRF: verify state matches stored state
33
+ if (state && storedState && state !== storedState) {
34
+ return new Response('Invalid state parameter', { status: 400 })
35
+ }
36
+
37
+ // Clear the state cookie
38
+ cookies.delete('setzkasten_oauth_state', { path: '/' })
39
+
40
+ const config = (globalThis as Record<string, unknown>).__SETZKASTEN_CONFIG__ as
41
+ | { adminPath: string }
42
+ | undefined
43
+
44
+ const adminPath = config?.adminPath ?? '/admin'
45
+
46
+ // On Vercel, url.origin may resolve to localhost. Use the Host header instead.
47
+ const host = request.headers.get('x-forwarded-host') ?? request.headers.get('host') ?? url.host
48
+ const protocol = request.headers.get('x-forwarded-proto') ?? (url.protocol === 'https:' ? 'https' : 'http')
49
+ const origin = `${protocol}://${host}`
50
+ const redirectUri = `${origin}/api/setzkasten/auth/callback`
51
+
52
+ // Read allowed emails from env (comma-separated)
53
+ const allowedEmailsRaw = import.meta.env.SETZKASTEN_ALLOWED_EMAILS ?? process.env.SETZKASTEN_ALLOWED_EMAILS ?? ''
54
+ const allowedEmails = allowedEmailsRaw
55
+ ? allowedEmailsRaw.split(',').map((e: string) => e.trim())
56
+ : undefined
57
+
58
+ try {
59
+ let sessionResult
60
+
61
+ if (provider === 'google') {
62
+ const auth = createGoogleAuth({
63
+ clientId: import.meta.env.GOOGLE_CLIENT_ID ?? process.env.GOOGLE_CLIENT_ID ?? '',
64
+ clientSecret: import.meta.env.GOOGLE_CLIENT_SECRET ?? process.env.GOOGLE_CLIENT_SECRET ?? '',
65
+ redirectUri,
66
+ allowedEmails,
67
+ })
68
+ sessionResult = await auth.handleCallback(code, 'google')
69
+ } else {
70
+ const auth = createGitHubAuth({
71
+ clientId: import.meta.env.GITHUB_CLIENT_ID ?? process.env.GITHUB_CLIENT_ID ?? '',
72
+ clientSecret: import.meta.env.GITHUB_CLIENT_SECRET ?? process.env.GITHUB_CLIENT_SECRET ?? '',
73
+ redirectUri,
74
+ allowedEmails,
75
+ })
76
+ sessionResult = await auth.handleCallback(code, 'github')
77
+ }
78
+
79
+ if (!sessionResult.ok) {
80
+ console.error('[setzkasten] Auth failed:', sessionResult.error.message)
81
+ return new Response(`Authentication failed: ${sessionResult.error.message}`, { status: 403 })
82
+ }
83
+
84
+ const session = sessionResult.value
85
+
86
+ // Set session cookie with user info (HMAC-signed in production)
87
+ const sessionPayload = JSON.stringify({
88
+ user: session.user,
89
+ expiresAt: session.expiresAt,
90
+ })
91
+
92
+ cookies.set('setzkasten_session', sessionPayload, {
93
+ httpOnly: true,
94
+ secure: import.meta.env.PROD,
95
+ sameSite: 'lax',
96
+ path: '/',
97
+ maxAge: 60 * 60 * 24 * 7, // 7 days
98
+ })
99
+
100
+ return redirect(adminPath)
101
+ } catch (error) {
102
+ console.error('[setzkasten] Auth callback error:', error)
103
+ return new Response('Authentication failed', { status: 500 })
104
+ }
105
+ }
@@ -0,0 +1,87 @@
1
+ import type { APIRoute } from 'astro'
2
+ import { createGitHubAuth } from '@setzkasten-cms/auth'
3
+ import { createGoogleAuth } from '@setzkasten-cms/auth'
4
+
5
+ /**
6
+ * Login initiation – redirects the user to the chosen OAuth provider.
7
+ *
8
+ * GET /api/setzkasten/auth/login?provider=github|google
9
+ */
10
+ export const GET: APIRoute = async ({ request, url, cookies, redirect }) => {
11
+ const provider = url.searchParams.get('provider') ?? 'github'
12
+
13
+ const config = (globalThis as Record<string, unknown>).__SETZKASTEN_CONFIG__ as
14
+ | { adminPath: string; hasGitHub: boolean; hasGoogle: boolean }
15
+ | undefined
16
+
17
+ const adminPath = config?.adminPath ?? '/admin'
18
+
19
+ // On Vercel, url.origin may resolve to localhost. Use the Host header instead.
20
+ const host = request.headers.get('x-forwarded-host') ?? request.headers.get('host') ?? url.host
21
+ const protocol = request.headers.get('x-forwarded-proto') ?? (url.protocol === 'https:' ? 'https' : 'http')
22
+ const origin = `${protocol}://${host}`
23
+ const redirectUri = `${origin}/api/setzkasten/auth/callback`
24
+
25
+ let loginUrl: string
26
+
27
+ if (provider === 'google') {
28
+ const googleClientId = import.meta.env.GOOGLE_CLIENT_ID ?? process.env.GOOGLE_CLIENT_ID ?? ''
29
+ const googleClientSecret = import.meta.env.GOOGLE_CLIENT_SECRET ?? process.env.GOOGLE_CLIENT_SECRET ?? ''
30
+
31
+ if (!googleClientId || !googleClientSecret) {
32
+ return new Response('Google OAuth not configured', { status: 500 })
33
+ }
34
+
35
+ const auth = createGoogleAuth({
36
+ clientId: googleClientId,
37
+ clientSecret: googleClientSecret,
38
+ redirectUri,
39
+ })
40
+
41
+ loginUrl = auth.getLoginUrl('google')
42
+
43
+ // Store the PKCE code verifier in a cookie for the callback
44
+ // Arctic generates it internally – we extract it from the URL
45
+ const urlObj = new URL(loginUrl)
46
+ const state = urlObj.searchParams.get('state')
47
+ if (state) {
48
+ cookies.set('setzkasten_oauth_state', JSON.stringify({ state, provider: 'google' }), {
49
+ httpOnly: true,
50
+ secure: true,
51
+ sameSite: 'lax',
52
+ path: '/',
53
+ maxAge: 600, // 10 min
54
+ })
55
+ }
56
+ } else {
57
+ // Default: GitHub
58
+ const ghClientId = import.meta.env.GITHUB_CLIENT_ID ?? process.env.GITHUB_CLIENT_ID ?? ''
59
+ const ghClientSecret = import.meta.env.GITHUB_CLIENT_SECRET ?? process.env.GITHUB_CLIENT_SECRET ?? ''
60
+
61
+ if (!ghClientId || !ghClientSecret) {
62
+ return new Response('GitHub OAuth not configured', { status: 500 })
63
+ }
64
+
65
+ const auth = createGitHubAuth({
66
+ clientId: ghClientId,
67
+ clientSecret: ghClientSecret,
68
+ redirectUri,
69
+ })
70
+
71
+ loginUrl = auth.getLoginUrl('github')
72
+
73
+ const urlObj = new URL(loginUrl)
74
+ const state = urlObj.searchParams.get('state')
75
+ if (state) {
76
+ cookies.set('setzkasten_oauth_state', JSON.stringify({ state, provider: 'github' }), {
77
+ httpOnly: true,
78
+ secure: true,
79
+ sameSite: 'lax',
80
+ path: '/',
81
+ maxAge: 600,
82
+ })
83
+ }
84
+ }
85
+
86
+ return redirect(loginUrl)
87
+ }
@@ -0,0 +1,9 @@
1
+ import type { APIRoute } from 'astro'
2
+
3
+ /**
4
+ * Logout – clears the session cookie and redirects to home.
5
+ */
6
+ export const GET: APIRoute = async ({ cookies, redirect }) => {
7
+ cookies.delete('setzkasten_session', { path: '/' })
8
+ return redirect('/')
9
+ }
@@ -0,0 +1,36 @@
1
+ import type { APIRoute } from 'astro'
2
+
3
+ /**
4
+ * Session check – returns current user info or 401.
5
+ * Used by the admin SPA to check if the user is logged in.
6
+ */
7
+ export const GET: APIRoute = async ({ cookies }) => {
8
+ const session = cookies.get('setzkasten_session')?.value
9
+ if (!session) {
10
+ return new Response(JSON.stringify({ authenticated: false }), {
11
+ status: 401,
12
+ headers: { 'Content-Type': 'application/json' },
13
+ })
14
+ }
15
+
16
+ try {
17
+ const parsed = JSON.parse(session) as { user: unknown; expiresAt: number }
18
+
19
+ if (parsed.expiresAt < Date.now()) {
20
+ cookies.delete('setzkasten_session', { path: '/' })
21
+ return new Response(JSON.stringify({ authenticated: false, reason: 'expired' }), {
22
+ status: 401,
23
+ headers: { 'Content-Type': 'application/json' },
24
+ })
25
+ }
26
+
27
+ return new Response(JSON.stringify({ authenticated: true, user: parsed.user }), {
28
+ headers: { 'Content-Type': 'application/json' },
29
+ })
30
+ } catch {
31
+ return new Response(JSON.stringify({ authenticated: false }), {
32
+ status: 401,
33
+ headers: { 'Content-Type': 'application/json' },
34
+ })
35
+ }
36
+ }
@@ -0,0 +1,30 @@
1
+ import type { APIRoute } from 'astro'
2
+
3
+ /**
4
+ * Returns the full SetzKastenConfig as JSON.
5
+ * The config is injected into globalThis by the integration at build time.
6
+ *
7
+ * GET /api/setzkasten/config
8
+ */
9
+ export const GET: APIRoute = async () => {
10
+ const config = (globalThis as Record<string, unknown>).__SETZKASTEN_FULL_CONFIG__ ?? {}
11
+ const ssrConfig = (globalThis as Record<string, unknown>).__SETZKASTEN_CONFIG__ as Record<string, unknown> | undefined
12
+
13
+ const result = {
14
+ storage: { kind: 'github' },
15
+ auth: { providers: ['github'] },
16
+ theme: {},
17
+ products: {},
18
+ collections: {},
19
+ ...config,
20
+ // Include storage params so the client can create ProxyContentRepository
21
+ _storage: ssrConfig?.storage ?? undefined,
22
+ _hasGitHub: ssrConfig?.hasGitHub ?? false,
23
+ _hasGoogle: ssrConfig?.hasGoogle ?? false,
24
+ }
25
+
26
+ return new Response(JSON.stringify(result), {
27
+ status: 200,
28
+ headers: { 'Content-Type': 'application/json' },
29
+ })
30
+ }
@@ -0,0 +1,61 @@
1
+ import type { APIRoute } from 'astro'
2
+ import { setDraft, clearDraft } from '../draft-store'
3
+
4
+ /**
5
+ * Draft content API for Live SSR Preview.
6
+ *
7
+ * POST /api/setzkasten/draft
8
+ * Body: { section: string, values: Record<string, unknown> }
9
+ * Stores draft section content for the current session.
10
+ *
11
+ * DELETE /api/setzkasten/draft
12
+ * Body: { section?: string }
13
+ * Clears draft(s) for the current session.
14
+ */
15
+ export const POST: APIRoute = async ({ request, cookies }) => {
16
+ const session = cookies.get('setzkasten_session')?.value
17
+ if (!session) {
18
+ return new Response('Unauthorized', { status: 401 })
19
+ }
20
+
21
+ try {
22
+ const body = (await request.json()) as {
23
+ section: string
24
+ values: Record<string, unknown>
25
+ }
26
+
27
+ if (!body.section || typeof body.values !== 'object') {
28
+ return new Response('Bad Request: section and values required', { status: 400 })
29
+ }
30
+
31
+ setDraft(session, body.section, body.values)
32
+
33
+ return new Response(JSON.stringify({ ok: true }), {
34
+ status: 200,
35
+ headers: { 'Content-Type': 'application/json' },
36
+ })
37
+ } catch (error) {
38
+ console.error('[setzkasten] Draft API error:', error)
39
+ return new Response('Internal error', { status: 500 })
40
+ }
41
+ }
42
+
43
+ export const DELETE: APIRoute = async ({ request, cookies }) => {
44
+ const session = cookies.get('setzkasten_session')?.value
45
+ if (!session) {
46
+ return new Response('Unauthorized', { status: 401 })
47
+ }
48
+
49
+ try {
50
+ const body = (await request.json().catch(() => ({}))) as { section?: string }
51
+ clearDraft(session, body.section)
52
+
53
+ return new Response(JSON.stringify({ ok: true }), {
54
+ status: 200,
55
+ headers: { 'Content-Type': 'application/json' },
56
+ })
57
+ } catch (error) {
58
+ console.error('[setzkasten] Draft clear error:', error)
59
+ return new Response('Internal error', { status: 500 })
60
+ }
61
+ }