@setzkasten-cms/astro-admin 0.8.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +22 -6
- package/src/admin-page.astro +1 -1
- package/src/api-routes/__tests__/auth-guard.test.ts +134 -0
- package/src/api-routes/__tests__/feature-gate.test.ts +60 -0
- package/src/api-routes/__tests__/github-token-for-request.test.ts +112 -0
- package/src/api-routes/__tests__/github-token.test.ts +78 -0
- package/src/api-routes/__tests__/global-config-theme.test.ts +71 -0
- package/src/api-routes/__tests__/history-rollback.test.ts +196 -0
- package/src/api-routes/__tests__/history.test.ts +168 -0
- package/src/api-routes/__tests__/init-scan-page-resolve-config.test.ts +61 -0
- package/src/api-routes/__tests__/license-tier.test.ts +45 -0
- package/src/api-routes/__tests__/migrate-to-multi.test.ts +189 -0
- package/src/api-routes/__tests__/pages-meta-store.test.ts +179 -0
- package/src/api-routes/__tests__/route-registry.test.ts +120 -0
- package/src/api-routes/__tests__/session-cookie.test.ts +67 -0
- package/src/api-routes/__tests__/setup-github-app-callback.test.ts +152 -0
- package/src/api-routes/__tests__/setup-github-app-repos.test.ts +192 -0
- package/src/api-routes/__tests__/setup-github-app.test.ts +107 -0
- package/src/api-routes/__tests__/storage-config-for-request.test.ts +78 -0
- package/src/api-routes/__tests__/webhook-signing.test.ts +39 -0
- package/src/api-routes/__tests__/webhooks.test.ts +219 -0
- package/src/api-routes/__tests__/website-resolver-bootstrap-standalone.test.ts +85 -0
- package/src/api-routes/__tests__/website-resolver-bootstrap.test.ts +108 -0
- package/src/api-routes/__tests__/website-resolver.test.ts +123 -0
- package/src/api-routes/__tests__/websites-add.test.ts +305 -0
- package/src/api-routes/__tests__/websites-list.test.ts +112 -0
- package/src/api-routes/__tests__/websites-remove.test.ts +155 -0
- package/src/api-routes/_auth-guard.ts +134 -13
- package/src/api-routes/_feature-gate.ts +39 -0
- package/src/api-routes/_github-token.ts +64 -0
- package/src/api-routes/_license-tier.ts +25 -0
- package/src/api-routes/_pages-meta-store.ts +134 -0
- package/src/api-routes/_role-resolver.ts +60 -0
- package/src/api-routes/_session-cookie.ts +42 -0
- package/src/api-routes/_storage-config.ts +77 -4
- package/src/api-routes/_vercel-origin.ts +22 -0
- package/src/api-routes/_webhook-dispatcher.ts +120 -0
- package/src/api-routes/_webhook-signing.ts +13 -0
- package/src/api-routes/_webhook-status-store.ts +31 -0
- package/src/api-routes/_website-resolver.ts +243 -0
- package/src/api-routes/_websites-store.ts +120 -0
- package/src/api-routes/asset-proxy.ts +6 -4
- package/src/api-routes/auth-callback.ts +8 -7
- package/src/api-routes/auth-logout.ts +5 -1
- package/src/api-routes/auth-setzkasten-login.ts +37 -11
- package/src/api-routes/catalog-add.ts +9 -5
- package/src/api-routes/catalog-export.ts +8 -4
- package/src/api-routes/config.ts +12 -5
- package/src/api-routes/editors.ts +94 -10
- package/src/api-routes/github-proxy.ts +5 -5
- package/src/api-routes/global-config.ts +23 -6
- package/src/api-routes/history-rollback.ts +144 -0
- package/src/api-routes/history-version.ts +57 -0
- package/src/api-routes/history.ts +119 -0
- package/src/api-routes/init-add-section.ts +13 -5
- package/src/api-routes/init-apply.ts +5 -3
- package/src/api-routes/init-migrate.ts +7 -5
- package/src/api-routes/init-scan-page.ts +26 -6
- package/src/api-routes/init-scan.ts +5 -3
- package/src/api-routes/migrate-to-multi.ts +255 -0
- package/src/api-routes/pages.ts +118 -4
- package/src/api-routes/section-add.ts +15 -5
- package/src/api-routes/section-commit-pending.ts +117 -5
- package/src/api-routes/section-delete.ts +29 -5
- package/src/api-routes/section-duplicate.ts +15 -5
- package/src/api-routes/section-prepare-copy.ts +15 -4
- package/src/api-routes/section-prepare.ts +9 -5
- package/src/api-routes/setup-github-app-bounce.ts +52 -0
- package/src/api-routes/setup-github-app-branches.ts +63 -0
- package/src/api-routes/setup-github-app-callback.ts +71 -0
- package/src/api-routes/setup-github-app-installed.ts +44 -0
- package/src/api-routes/setup-github-app-repos.ts +46 -0
- package/src/api-routes/setup-github-app.ts +58 -0
- package/src/api-routes/updater-register.ts +37 -25
- package/src/api-routes/updater-transfer.ts +1 -12
- package/src/api-routes/webhooks-status.ts +17 -0
- package/src/api-routes/webhooks-test.ts +134 -0
- package/src/api-routes/webhooks.ts +163 -0
- package/src/api-routes/websites-add.ts +113 -0
- package/src/api-routes/websites-list.ts +40 -0
- package/src/api-routes/websites-remove.ts +74 -0
- package/src/init/__tests__/patcher-edge-cases.test.ts +34 -1
- package/src/init/__tests__/patcher-mixed-content-wrapper.test.ts +90 -0
- package/src/init/template-patcher-v2.ts +42 -4
- package/LICENSE +0 -37
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro'
|
|
2
|
+
import { resolveStorageConfigForRequest } from './_storage-config'
|
|
3
|
+
import { resolveGitHubTokenForRequest } from './_github-token'
|
|
4
|
+
import { parseSession, requireAdmin } from './_auth-guard'
|
|
5
|
+
import { withTrailers } from './_commit-trailers'
|
|
6
|
+
import { invalidateCache } from './_github-cache'
|
|
7
|
+
|
|
8
|
+
interface RollbackBody {
|
|
9
|
+
path?: string
|
|
10
|
+
sha?: string
|
|
11
|
+
/** Optional ETag-style guard: client sends the SHA they think is HEAD;
|
|
12
|
+
* if HEAD has moved, we 409 to prevent stomping live edits. */
|
|
13
|
+
expectedHeadSha?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* POST /api/setzkasten/history/rollback
|
|
18
|
+
*
|
|
19
|
+
* Body: { path, sha, expectedHeadSha? }
|
|
20
|
+
*
|
|
21
|
+
* Restores `path` to the contents from `sha` by writing a new commit
|
|
22
|
+
* (no `git revert` — JSON content is set wholesale). The original SHA
|
|
23
|
+
* stays in history so users can roll forward again.
|
|
24
|
+
*
|
|
25
|
+
* Conflict semantics: the client passes the SHA they currently render in
|
|
26
|
+
* the file picker. If the file's HEAD has moved between page-load and the
|
|
27
|
+
* rollback click, we return 409 with `code: 'head-moved'` so the UI can
|
|
28
|
+
* tell the user to refresh.
|
|
29
|
+
*
|
|
30
|
+
* Admin-only — editors can edit, but rollback is destructive enough to
|
|
31
|
+
* warrant the audit-log control.
|
|
32
|
+
*/
|
|
33
|
+
export const POST: APIRoute = async ({ request, cookies }) => {
|
|
34
|
+
const denied = requireAdmin(cookies.get('setzkasten_session')?.value)
|
|
35
|
+
if (denied) return denied
|
|
36
|
+
|
|
37
|
+
const session = parseSession(cookies.get('setzkasten_session')?.value)
|
|
38
|
+
if (!session) return new Response('Unauthorized', { status: 401 })
|
|
39
|
+
|
|
40
|
+
let body: RollbackBody
|
|
41
|
+
try {
|
|
42
|
+
body = (await request.json()) as RollbackBody
|
|
43
|
+
} catch {
|
|
44
|
+
return Response.json({ error: 'Invalid JSON body' }, { status: 400 })
|
|
45
|
+
}
|
|
46
|
+
const { path, sha, expectedHeadSha } = body
|
|
47
|
+
if (!path || !sha) {
|
|
48
|
+
return Response.json({ error: 'path and sha are required' }, { status: 400 })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const tokenResult = await resolveGitHubTokenForRequest(request)
|
|
52
|
+
if (!tokenResult.ok) return new Response(tokenResult.error.message, { status: 500 })
|
|
53
|
+
|
|
54
|
+
const storage = await resolveStorageConfigForRequest(request)
|
|
55
|
+
if (!storage) {
|
|
56
|
+
return Response.json({ error: 'Could not resolve owner/repo' }, { status: 400 })
|
|
57
|
+
}
|
|
58
|
+
const { owner, repo, branch } = storage
|
|
59
|
+
|
|
60
|
+
const headers = {
|
|
61
|
+
Authorization: `Bearer ${tokenResult.value}`,
|
|
62
|
+
Accept: 'application/vnd.github+json',
|
|
63
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
64
|
+
'Content-Type': 'application/json',
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 1. Fetch contents of the file at the target sha
|
|
68
|
+
const versionRes = await fetch(
|
|
69
|
+
`https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${sha}`,
|
|
70
|
+
{ headers },
|
|
71
|
+
)
|
|
72
|
+
if (versionRes.status === 404) {
|
|
73
|
+
return Response.json(
|
|
74
|
+
{ error: 'File did not exist at the requested sha', code: 'version-not-found' },
|
|
75
|
+
{ status: 404 },
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
if (!versionRes.ok) {
|
|
79
|
+
return Response.json(
|
|
80
|
+
{ error: `Failed to read version: ${versionRes.status}` },
|
|
81
|
+
{ status: 502 },
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
const versionData = (await versionRes.json()) as {
|
|
85
|
+
content: string
|
|
86
|
+
encoding: string
|
|
87
|
+
}
|
|
88
|
+
const targetContent =
|
|
89
|
+
versionData.encoding === 'base64'
|
|
90
|
+
? Buffer.from(versionData.content, 'base64').toString('utf-8')
|
|
91
|
+
: versionData.content
|
|
92
|
+
|
|
93
|
+
// 2. Fetch current HEAD SHA of the file (for conflict detection + PUT sha param)
|
|
94
|
+
const headRes = await fetch(
|
|
95
|
+
`https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`,
|
|
96
|
+
{ headers },
|
|
97
|
+
)
|
|
98
|
+
let currentSha: string | null = null
|
|
99
|
+
if (headRes.ok) {
|
|
100
|
+
const data = (await headRes.json()) as { sha: string }
|
|
101
|
+
currentSha = data.sha
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (expectedHeadSha && currentSha && expectedHeadSha !== currentSha) {
|
|
105
|
+
return Response.json(
|
|
106
|
+
{
|
|
107
|
+
error: 'Datei wurde inzwischen geändert. Bitte den Verlauf neu laden.',
|
|
108
|
+
code: 'head-moved',
|
|
109
|
+
},
|
|
110
|
+
{ status: 409 },
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 3. Write new commit with the historical content
|
|
115
|
+
const shortSha = sha.slice(0, 7)
|
|
116
|
+
const fileName = path.split('/').pop() ?? path
|
|
117
|
+
const message = withTrailers(
|
|
118
|
+
`revert(${fileName}): rollback to ${shortSha}`,
|
|
119
|
+
session.user.email,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
const putBody: Record<string, unknown> = {
|
|
123
|
+
message,
|
|
124
|
+
content: Buffer.from(targetContent).toString('base64'),
|
|
125
|
+
branch,
|
|
126
|
+
}
|
|
127
|
+
if (currentSha) putBody.sha = currentSha
|
|
128
|
+
|
|
129
|
+
const putRes = await fetch(
|
|
130
|
+
`https://api.github.com/repos/${owner}/${repo}/contents/${path}`,
|
|
131
|
+
{ method: 'PUT', headers, body: JSON.stringify(putBody) },
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if (!putRes.ok) {
|
|
135
|
+
const text = await putRes.text()
|
|
136
|
+
return Response.json({ error: `Rollback write failed: ${text}` }, { status: 502 })
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Invalidate history cache for this path — fresh commit shows up in lists.
|
|
140
|
+
invalidateCache(`history:${owner}/${repo}:${branch}:${path}:head`)
|
|
141
|
+
|
|
142
|
+
const putData = (await putRes.json()) as { commit: { sha: string } }
|
|
143
|
+
return Response.json({ ok: true, commitSha: putData.commit.sha })
|
|
144
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro'
|
|
2
|
+
import { resolveStorageConfigForRequest } from './_storage-config'
|
|
3
|
+
import { resolveGitHubTokenForRequest } from './_github-token'
|
|
4
|
+
import { requireAdmin } from './_auth-guard'
|
|
5
|
+
import { cachedFetch } from './_github-cache'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* GET /api/setzkasten/history/version?path=<file>&sha=<commit-sha>
|
|
9
|
+
*
|
|
10
|
+
* Returns the file content at a specific commit (for diff rendering).
|
|
11
|
+
* Cached per (path, sha) for 5 minutes — historical content is immutable.
|
|
12
|
+
*/
|
|
13
|
+
export const GET: APIRoute = async ({ request, url, cookies }) => {
|
|
14
|
+
const denied = requireAdmin(cookies.get('setzkasten_session')?.value)
|
|
15
|
+
if (denied) return denied
|
|
16
|
+
|
|
17
|
+
const path = url.searchParams.get('path')
|
|
18
|
+
const sha = url.searchParams.get('sha')
|
|
19
|
+
if (!path || !sha) {
|
|
20
|
+
return Response.json({ error: 'Missing required `path` or `sha`.' }, { status: 400 })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const tokenResult = await resolveGitHubTokenForRequest(request)
|
|
24
|
+
if (!tokenResult.ok) return new Response(tokenResult.error.message, { status: 500 })
|
|
25
|
+
|
|
26
|
+
const storage = await resolveStorageConfigForRequest(request)
|
|
27
|
+
if (!storage) {
|
|
28
|
+
return Response.json({ error: 'Could not resolve owner/repo' }, { status: 400 })
|
|
29
|
+
}
|
|
30
|
+
const { owner, repo } = storage
|
|
31
|
+
|
|
32
|
+
const cacheKey = `history-version:${owner}/${repo}:${path}:${sha}`
|
|
33
|
+
const result = await cachedFetch(cacheKey, 5 * 60_000, async () => {
|
|
34
|
+
const u = new URL(
|
|
35
|
+
`https://api.github.com/repos/${owner}/${repo}/contents/${path}`,
|
|
36
|
+
)
|
|
37
|
+
u.searchParams.set('ref', sha)
|
|
38
|
+
const res = await fetch(u, {
|
|
39
|
+
headers: {
|
|
40
|
+
Authorization: `Bearer ${tokenResult.value}`,
|
|
41
|
+
Accept: 'application/vnd.github+json',
|
|
42
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
if (res.status === 404) return { ok: false as const, status: 404, error: 'File not found at given sha' }
|
|
46
|
+
if (!res.ok) return { ok: false as const, status: 502, error: `GitHub returned ${res.status}` }
|
|
47
|
+
const data = (await res.json()) as { content: string; encoding: string; sha: string }
|
|
48
|
+
const raw =
|
|
49
|
+
data.encoding === 'base64'
|
|
50
|
+
? Buffer.from(data.content, 'base64').toString('utf-8')
|
|
51
|
+
: data.content
|
|
52
|
+
return { ok: true as const, value: { content: raw, sha: data.sha } }
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
if (!result.ok) return Response.json({ error: result.error }, { status: result.status })
|
|
56
|
+
return Response.json(result.value)
|
|
57
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro'
|
|
2
|
+
import { resolveStorageConfigForRequest } from './_storage-config'
|
|
3
|
+
import { resolveGitHubTokenForRequest } from './_github-token'
|
|
4
|
+
import { requireAdmin } from './_auth-guard'
|
|
5
|
+
import { parseCoAuthorTrailers, type CommitInfo } from '@setzkasten-cms/core'
|
|
6
|
+
import { cachedFetch } from './_github-cache'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* GET /api/setzkasten/history?path=<contentPath>&before=<sha>
|
|
10
|
+
*
|
|
11
|
+
* Returns up to 5 most recent commits affecting the given file. Pagination
|
|
12
|
+
* via `before=<sha>` returns 10 more older commits — clients call this on
|
|
13
|
+
* "Mehr laden". Admin-only — editors can read content but not the audit
|
|
14
|
+
* trail (and certainly not roll back).
|
|
15
|
+
*/
|
|
16
|
+
export const GET: APIRoute = async ({ request, url, cookies }) => {
|
|
17
|
+
const denied = requireAdmin(cookies.get('setzkasten_session')?.value)
|
|
18
|
+
if (denied) return denied
|
|
19
|
+
|
|
20
|
+
const path = url.searchParams.get('path')
|
|
21
|
+
const before = url.searchParams.get('before')
|
|
22
|
+
if (!path) {
|
|
23
|
+
return Response.json({ error: 'Missing required `path` parameter.' }, { status: 400 })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const tokenResult = await resolveGitHubTokenForRequest(request)
|
|
27
|
+
if (!tokenResult.ok) return new Response(tokenResult.error.message, { status: 500 })
|
|
28
|
+
|
|
29
|
+
const storage = await resolveStorageConfigForRequest(request)
|
|
30
|
+
if (!storage) {
|
|
31
|
+
return Response.json({ error: 'Could not resolve owner/repo' }, { status: 400 })
|
|
32
|
+
}
|
|
33
|
+
const { owner, repo, branch } = storage
|
|
34
|
+
const perPage = before ? 10 : 5
|
|
35
|
+
|
|
36
|
+
// Cache history per (path, before) for 60s — invalidated by rollback.
|
|
37
|
+
const cacheKey = `history:${owner}/${repo}:${branch}:${path}:${before ?? 'head'}`
|
|
38
|
+
const commits = await cachedFetch(cacheKey, 60_000, () =>
|
|
39
|
+
fetchCommits(owner, repo, branch, path, perPage, before, tokenResult.value),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if (!commits.ok) {
|
|
43
|
+
return Response.json({ error: commits.error }, { status: commits.status })
|
|
44
|
+
}
|
|
45
|
+
return Response.json({ commits: commits.value })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface CommitsListSuccess {
|
|
49
|
+
ok: true
|
|
50
|
+
value: readonly CommitInfo[]
|
|
51
|
+
}
|
|
52
|
+
interface CommitsListFailure {
|
|
53
|
+
ok: false
|
|
54
|
+
status: number
|
|
55
|
+
error: string
|
|
56
|
+
}
|
|
57
|
+
type CommitsResult = CommitsListSuccess | CommitsListFailure
|
|
58
|
+
|
|
59
|
+
async function fetchCommits(
|
|
60
|
+
owner: string,
|
|
61
|
+
repo: string,
|
|
62
|
+
branch: string,
|
|
63
|
+
path: string,
|
|
64
|
+
perPage: number,
|
|
65
|
+
before: string | null,
|
|
66
|
+
token: string,
|
|
67
|
+
): Promise<CommitsResult> {
|
|
68
|
+
// GitHub paginates by `?sha=<commit>` — passing `before` as `sha` gets
|
|
69
|
+
// commits older than (and including) that SHA. We start one before
|
|
70
|
+
// requested SHA so the same commit doesn't appear twice. The simplest
|
|
71
|
+
// way: pass sha=<before>, request perPage+1, and skip the first.
|
|
72
|
+
const sha = before ?? branch
|
|
73
|
+
const u = new URL(`https://api.github.com/repos/${owner}/${repo}/commits`)
|
|
74
|
+
u.searchParams.set('path', path)
|
|
75
|
+
u.searchParams.set('sha', sha)
|
|
76
|
+
u.searchParams.set('per_page', String(before ? perPage + 1 : perPage))
|
|
77
|
+
|
|
78
|
+
const res = await fetch(u, {
|
|
79
|
+
headers: {
|
|
80
|
+
Authorization: `Bearer ${token}`,
|
|
81
|
+
Accept: 'application/vnd.github+json',
|
|
82
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
if (res.status === 404) return { ok: true, value: [] }
|
|
87
|
+
if (!res.ok) {
|
|
88
|
+
return { ok: false, status: 502, error: `GitHub returned ${res.status}` }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const data = (await res.json()) as Array<{
|
|
92
|
+
sha: string
|
|
93
|
+
commit: {
|
|
94
|
+
author: { name: string; email: string; date: string }
|
|
95
|
+
message: string
|
|
96
|
+
}
|
|
97
|
+
author: { avatar_url?: string } | null
|
|
98
|
+
}>
|
|
99
|
+
|
|
100
|
+
// Skip first if we paginated (the `before` SHA itself).
|
|
101
|
+
const start = before ? 1 : 0
|
|
102
|
+
const slice = data.slice(start, start + perPage)
|
|
103
|
+
const commits: CommitInfo[] = slice.map((c) => {
|
|
104
|
+
const [firstLine, ...rest] = c.commit.message.split('\n')
|
|
105
|
+
const body = rest.join('\n')
|
|
106
|
+
return {
|
|
107
|
+
sha: c.sha,
|
|
108
|
+
shortSha: c.sha.slice(0, 7),
|
|
109
|
+
authoredAt: c.commit.author.date,
|
|
110
|
+
authorName: c.commit.author.name,
|
|
111
|
+
authorEmail: c.commit.author.email,
|
|
112
|
+
authorAvatarUrl: c.author?.avatar_url,
|
|
113
|
+
coAuthors: parseCoAuthorTrailers(body),
|
|
114
|
+
message: firstLine ?? '',
|
|
115
|
+
body,
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
return { ok: true, value: commits }
|
|
119
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { APIRoute } from 'astro'
|
|
2
2
|
import type { InferredSection } from '@setzkasten-cms/core/init'
|
|
3
3
|
import { addSectionToConfig } from '@setzkasten-cms/core/init'
|
|
4
|
-
import {
|
|
4
|
+
import { prefixPath, resolveStorageConfigForRequest } from './_storage-config'
|
|
5
5
|
import { patchTemplateForFields, stripTemplateFallbacks } from '../init/template-patcher-v2'
|
|
6
6
|
import type { RepeatedGroup } from '../init/analyzer-types'
|
|
7
7
|
import { withTrailers } from './_commit-trailers'
|
|
8
|
+
import { resolveGitHubTokenForRequest } from './_github-token'
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* POST /api/setzkasten/init/add-section
|
|
@@ -21,10 +22,11 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
21
22
|
return new Response('Unauthorized', { status: 401 })
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
const
|
|
25
|
-
if (!
|
|
26
|
-
return new Response(
|
|
25
|
+
const tokenResult = await resolveGitHubTokenForRequest(request)
|
|
26
|
+
if (!tokenResult.ok) {
|
|
27
|
+
return new Response(tokenResult.error.message, { status: 500 })
|
|
27
28
|
}
|
|
29
|
+
const githubToken = tokenResult.value
|
|
28
30
|
|
|
29
31
|
try {
|
|
30
32
|
const body = await request.json() as {
|
|
@@ -38,7 +40,7 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
38
40
|
contentPath?: string
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
const storage =
|
|
43
|
+
const storage = await resolveStorageConfigForRequest(request, body)
|
|
42
44
|
if (!storage) {
|
|
43
45
|
return Response.json({ error: 'Could not resolve owner/repo. Set SETZKASTEN_OWNER and SETZKASTEN_REPO env vars.' }, { status: 400 })
|
|
44
46
|
}
|
|
@@ -292,6 +294,12 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
292
294
|
return Response.json({ error: commitResult.error }, { status: 500 })
|
|
293
295
|
}
|
|
294
296
|
|
|
297
|
+
const { recordPageEdit } = await import('./_pages-meta-store.js')
|
|
298
|
+
await recordPageEdit(
|
|
299
|
+
{ owner, repo, branch, contentPath, token: tokenResult.value },
|
|
300
|
+
pageKey,
|
|
301
|
+
).catch(() => {})
|
|
302
|
+
|
|
295
303
|
return Response.json({
|
|
296
304
|
success: true,
|
|
297
305
|
commitSha: commitResult.sha,
|
|
@@ -3,6 +3,7 @@ import { generateConfigFile, type InferredSection, type ConfigGeneratorInput } f
|
|
|
3
3
|
import { patchAstroConfig } from '../init/astro-config-patcher'
|
|
4
4
|
import { patchTemplateForFields } from '../init/template-patcher-v2'
|
|
5
5
|
import { withTrailers } from './_commit-trailers'
|
|
6
|
+
import { resolveGitHubTokenForRequest } from './_github-token'
|
|
6
7
|
|
|
7
8
|
interface ApplyRequest {
|
|
8
9
|
owner: string
|
|
@@ -36,10 +37,11 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
36
37
|
return new Response('Unauthorized', { status: 401 })
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
const
|
|
40
|
-
if (!
|
|
41
|
-
return new Response(
|
|
40
|
+
const tokenResult = await resolveGitHubTokenForRequest(request)
|
|
41
|
+
if (!tokenResult.ok) {
|
|
42
|
+
return new Response(tokenResult.error.message, { status: 500 })
|
|
42
43
|
}
|
|
44
|
+
const githubToken = tokenResult.value
|
|
43
45
|
|
|
44
46
|
try {
|
|
45
47
|
const body = await request.json() as ApplyRequest
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { APIRoute } from 'astro'
|
|
2
2
|
import type { SetzKastenConfig } from '@setzkasten-cms/core'
|
|
3
|
-
import {
|
|
3
|
+
import { prefixPath, resolveStorageConfigForRequest } from './_storage-config'
|
|
4
4
|
import { patchTemplateForFields } from '../init/template-patcher-v2'
|
|
5
5
|
import { withTrailers } from './_commit-trailers'
|
|
6
|
+
import { resolveGitHubTokenForRequest } from './_github-token'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* POST /api/setzkasten/init/migrate
|
|
@@ -21,10 +22,11 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
21
22
|
return new Response('Unauthorized', { status: 401 })
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
const
|
|
25
|
-
if (!
|
|
26
|
-
return new Response(
|
|
25
|
+
const tokenResult = await resolveGitHubTokenForRequest(request)
|
|
26
|
+
if (!tokenResult.ok) {
|
|
27
|
+
return new Response(tokenResult.error.message, { status: 500 })
|
|
27
28
|
}
|
|
29
|
+
const githubToken = tokenResult.value
|
|
28
30
|
|
|
29
31
|
try {
|
|
30
32
|
const body = await request.json() as {
|
|
@@ -35,7 +37,7 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
35
37
|
componentPath?: string
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
const storage =
|
|
40
|
+
const storage = await resolveStorageConfigForRequest(request, body)
|
|
39
41
|
if (!storage) {
|
|
40
42
|
return Response.json({ error: 'Could not resolve owner/repo. Set SETZKASTEN_OWNER and SETZKASTEN_REPO env vars.' }, { status: 400 })
|
|
41
43
|
}
|
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
import type { APIRoute } from 'astro'
|
|
2
2
|
import type { SetzKastenConfig } from '@setzkasten-cms/core'
|
|
3
|
-
import {
|
|
3
|
+
import { prefixPath, resolveStorageConfigForRequest } from './_storage-config'
|
|
4
4
|
import { extractSectionImports, extractLayoutImport } from '../init/astro-detector'
|
|
5
5
|
import { analyzeAstroSection } from '../init/astro-section-analyzer-v2'
|
|
6
6
|
import type { RepoFile } from '@setzkasten-cms/core/init'
|
|
7
|
+
import { resolveGitHubTokenForRequest } from './_github-token'
|
|
8
|
+
|
|
9
|
+
// Build-time constant injected by the Vite define plugin — always available in
|
|
10
|
+
// compiled API routes (unlike page-ssr injectScript which only runs for SSR pages).
|
|
11
|
+
declare const __SETZKASTEN_FULL_CONFIG__: SetzKastenConfig | null | undefined
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolves the full Setzkasten config.
|
|
15
|
+
* Reads the Vite build-time constant first; falls back to globalThis for
|
|
16
|
+
* local dev / test environments where the define is not applied.
|
|
17
|
+
*
|
|
18
|
+
* Without the build-time fallback, cold-start Vercel function invocations of
|
|
19
|
+
* this API route see managedSections={} and offer every adopted section
|
|
20
|
+
* (including _layout_header / _layout_footer) for re-adoption.
|
|
21
|
+
*/
|
|
22
|
+
export function resolveFullConfig(): SetzKastenConfig | undefined {
|
|
23
|
+
const buildConfig = typeof __SETZKASTEN_FULL_CONFIG__ !== 'undefined' ? __SETZKASTEN_FULL_CONFIG__ : null
|
|
24
|
+
return (buildConfig ?? (globalThis as any).__SETZKASTEN_FULL_CONFIG__) as SetzKastenConfig | undefined
|
|
25
|
+
}
|
|
7
26
|
|
|
8
27
|
/**
|
|
9
28
|
* POST /api/setzkasten/init/scan-page
|
|
@@ -21,10 +40,11 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
21
40
|
return new Response('Unauthorized', { status: 401 })
|
|
22
41
|
}
|
|
23
42
|
|
|
24
|
-
const
|
|
25
|
-
if (!
|
|
26
|
-
return new Response(
|
|
43
|
+
const tokenResult = await resolveGitHubTokenForRequest(request)
|
|
44
|
+
if (!tokenResult.ok) {
|
|
45
|
+
return new Response(tokenResult.error.message, { status: 500 })
|
|
27
46
|
}
|
|
47
|
+
const githubToken = tokenResult.value
|
|
28
48
|
|
|
29
49
|
try {
|
|
30
50
|
const body = await request.json() as {
|
|
@@ -35,7 +55,7 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
35
55
|
projectRoot?: string
|
|
36
56
|
}
|
|
37
57
|
|
|
38
|
-
const storage =
|
|
58
|
+
const storage = await resolveStorageConfigForRequest(request, body)
|
|
39
59
|
if (!storage) {
|
|
40
60
|
return Response.json({ error: 'Could not resolve owner/repo. Set SETZKASTEN_OWNER and SETZKASTEN_REPO env vars.' }, { status: 400 })
|
|
41
61
|
}
|
|
@@ -47,7 +67,7 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
47
67
|
}
|
|
48
68
|
|
|
49
69
|
// Get current schema to know which sections are already managed + their fields
|
|
50
|
-
const config = (
|
|
70
|
+
const config = resolveFullConfig()
|
|
51
71
|
const managedSections = new Map<string, Set<string>>() // key → field keys
|
|
52
72
|
if (config) {
|
|
53
73
|
for (const product of Object.values(config.products)) {
|
|
@@ -2,6 +2,7 @@ import type { APIRoute } from 'astro'
|
|
|
2
2
|
import { analyzeProject, type RepoFile } from '@setzkasten-cms/core/init'
|
|
3
3
|
import { findAstroPages, extractSectionImports } from '../init/astro-detector'
|
|
4
4
|
import { analyzeAstroSection } from '../init/astro-section-analyzer-v2'
|
|
5
|
+
import { resolveGitHubTokenForRequest } from './_github-token'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* POST /api/setzkasten/init/scan
|
|
@@ -16,10 +17,11 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
16
17
|
return new Response('Unauthorized', { status: 401 })
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
const
|
|
20
|
-
if (!
|
|
21
|
-
return new Response(
|
|
20
|
+
const tokenResult = await resolveGitHubTokenForRequest(request)
|
|
21
|
+
if (!tokenResult.ok) {
|
|
22
|
+
return new Response(tokenResult.error.message, { status: 500 })
|
|
22
23
|
}
|
|
24
|
+
const githubToken = tokenResult.value
|
|
23
25
|
|
|
24
26
|
try {
|
|
25
27
|
const body = await request.json() as { owner: string; repo: string; branch?: string }
|