Gate admin menu and authoring with shared JWT

This commit is contained in:
2026-08-15 09:29:08 +02:00
parent eeaa4138fa
commit 8f1f5a8743
16 changed files with 366 additions and 52 deletions
+10 -8
View File
@@ -33,29 +33,31 @@ function documentKind(type: SourceFileType) {
return type === 'web_capture' ? 'WEB CAPTURE' : type.toUpperCase()
}
async function uploadAsset(baseUrl: string, levelId: string, manifestDir: string, document: MysteryDocument) {
async function uploadAsset(baseUrl: string, levelId: string, manifestDir: string, document: MysteryDocument, authorization?: string) {
if (!document.asset) return undefined
const assetPath = path.resolve(manifestDir, document.asset)
const form = new FormData()
form.append('file', new Blob([await readFile(assetPath)]), path.basename(assetPath))
const response = await requireOk(await fetch(`${baseUrl}/api/levels/${levelId}/documents?edit=1`, { method: 'POST', body: form }), `Upload ${document.asset}`)
const response = await requireOk(await fetch(`${baseUrl}/api/levels/${levelId}/documents?edit=1`, { method: 'POST', headers: authorization ? { authorization } : undefined, body: form }), `Upload ${document.asset}`)
return await response.json() as CaseDocument
}
export async function importMysteryTemplate(manifestPath: string, baseUrl = 'http://localhost:8787') {
export async function importMysteryTemplate(manifestPath: string, baseUrl = 'http://localhost:8787', adminJwt = process.env.OSINT_ADMIN_JWT) {
const absoluteManifest = path.resolve(manifestPath)
const manifest = JSON.parse(await readFile(absoluteManifest, 'utf8')) as MysteryManifest
const manifestDir = path.dirname(absoluteManifest)
const authoringId = `${manifest.slug}-authoring-${Date.now()}`
const authorization = adminJwt ? `Bearer ${adminJwt}` : undefined
const headers = { 'content-type': 'application/json', ...(authorization ? { authorization } : {}) }
const createdResponse = await requireOk(await fetch(`${baseUrl}/api/levels`, {
method: 'POST', headers: { 'content-type': 'application/json' },
method: 'POST', headers,
body: JSON.stringify({ id: authoringId, title: manifest.title, subtitle: manifest.subtitle }),
}), 'Create authoring level')
const state = await createdResponse.json() as CaseState
const documents = new Map<string, CaseDocument>()
for (const source of manifest.documents) {
const uploaded = await uploadAsset(baseUrl, state.id, manifestDir, source)
const uploaded = await uploadAsset(baseUrl, state.id, manifestDir, source, authorization)
documents.set(source.key, {
id: uploaded?.id || randomUUID(), title: source.title, kind: documentKind(source.fileType),
date: source.publishedAt.slice(0, 10), publishedAt: source.publishedAt,
@@ -87,15 +89,15 @@ export async function importMysteryTemplate(manifestPath: string, baseUrl = 'htt
state.viewport = { x: 0, y: 28, zoom: 0.7 }
await requireOk(await fetch(`${baseUrl}/api/levels/${state.id}?edit=1`, {
method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify(state),
method: 'PUT', headers, body: JSON.stringify(state),
}), 'Save authored mystery')
const templateResponse = await requireOk(await fetch(`${baseUrl}/api/levels/${state.id}/templates?edit=1`, {
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ slug: manifest.slug, name: manifest.name }),
method: 'POST', headers, body: JSON.stringify({ slug: manifest.slug, name: manifest.name }),
}), 'Freeze mystery template')
const template = await templateResponse.json() as { slug: string; currentVersion: number }
const playableId = `${manifest.slug}-case-${Date.now()}`
const playableResponse = await requireOk(await fetch(`${baseUrl}/api/templates/${manifest.slug}/levels?edit=1`, {
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: playableId, title: manifest.title }),
method: 'POST', headers, body: JSON.stringify({ id: playableId, title: manifest.title }),
}), 'Instantiate playable mystery')
const playable = await playableResponse.json() as CaseState
return { manifest, template, authoringLevelId: state.id, playableLevel: playable }