Adding migrations and lot of work. Starting work on the demo scope
This commit is contained in:
+78
-4
@@ -11,6 +11,7 @@ import type { CaseState } from '../src/types.js'
|
||||
import { authenticateJwt, createDevelopmentAdminToken, hasAdminClaim, requireAdmin, resolveUserId } from './auth.js'
|
||||
import { createLevelRepository } from './levelRepository.js'
|
||||
import { createNarrativeRepository } from './narrativeRepository.js'
|
||||
import { createTextExtractorFromEnv } from './ocr.js'
|
||||
import { createStoryGraphRepository, type StoryNodeType } from './storyGraphRepository.js'
|
||||
import { createObjectStorageFromEnv } from './objectStorage.js'
|
||||
|
||||
@@ -25,6 +26,7 @@ export const pool = new Pool({ connectionString: databaseUrl })
|
||||
const editingEnabled = process.env.LEVEL_EDITING_ENABLED === 'true'
|
||||
const objectStorage = createObjectStorageFromEnv()
|
||||
await objectStorage.initialize()
|
||||
const textExtractor = createTextExtractorFromEnv()
|
||||
const levels = createLevelRepository(pool, editingEnabled, objectStorage)
|
||||
const narrative = createNarrativeRepository(pool, objectStorage)
|
||||
const storyGraph = createStoryGraphRepository(pool)
|
||||
@@ -49,7 +51,7 @@ const upload = multer({
|
||||
})
|
||||
|
||||
app.get('/api/health', async (_req, res) => {
|
||||
try { await pool.query('SELECT 1'); res.json({ ok: true, database: 'connected', objectStorage: objectStorage.provider, schema: 'osint', editingEnabled }) }
|
||||
try { await pool.query('SELECT 1'); res.json({ ok: true, database: 'connected', objectStorage: objectStorage.provider, textExtraction: textExtractor.provider, schema: 'osint', editingEnabled }) }
|
||||
catch { res.status(503).json({ ok: false, database: 'unavailable' }) }
|
||||
})
|
||||
app.get('/api/session', (req, res) => res.json({ authenticated: Boolean(req.authClaims), isAdmin: hasAdminClaim(req) }))
|
||||
@@ -106,14 +108,41 @@ app.get('/api/assets/:id', async (req, res, next) => {
|
||||
asset.stream.pipe(res)
|
||||
} catch (error) { next(error) }
|
||||
})
|
||||
app.post('/api/levels/:id/documents', requireAdmin, upload.single('file'), async (req, res, next) => {
|
||||
app.post('/api/levels/:id/documents', upload.single('file'), async (req, res, next) => {
|
||||
try {
|
||||
if (!wantsEdit(req)) return res.status(403).json({ error: 'Level editing is disabled' })
|
||||
if (!req.file) return res.status(400).json({ error: 'A file is required' })
|
||||
const document = await levels.uploadDocument(String(req.params.id), req.file)
|
||||
const extraction = await textExtractor.extract(req.file)
|
||||
const x = Number(req.body?.x); const y = Number(req.body?.y)
|
||||
const placement = Number.isFinite(x) && Number.isFinite(y) ? { x, y } : undefined
|
||||
const document = await levels.uploadDocument(String(req.params.id), req.file, extraction, placement)
|
||||
document ? res.status(201).json(document) : res.status(404).json({ error: 'Level not found' })
|
||||
} catch (error) { next(error) }
|
||||
})
|
||||
app.post('/api/levels/:id/reveals/seen', async (req, res, next) => {
|
||||
try {
|
||||
const ids = Array.isArray(req.body?.documentIds) ? req.body.documentIds.map(String) : []
|
||||
const acknowledged = await levels.acknowledgeRevealedDocuments(String(req.params.id), ids)
|
||||
acknowledged === null ? res.status(404).json({ error: 'Level not found' }) : res.json({ acknowledged })
|
||||
} catch (error) { next(error) }
|
||||
})
|
||||
app.get('/api/levels/:id/flags', requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const flags = await levels.listFlags(String(req.params.id))
|
||||
flags ? res.json(flags) : res.status(404).json({ error: 'Level not found' })
|
||||
} catch (error) { next(error) }
|
||||
})
|
||||
app.put('/api/levels/:id/flags/:key', requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const updated = await levels.setFlag(String(req.params.id), String(req.params.key), true)
|
||||
updated ? res.json({ ok: true }) : res.status(404).json({ error: 'Level not found' })
|
||||
} catch (error) { next(error) }
|
||||
})
|
||||
app.delete('/api/levels/:id/flags/:key', requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const updated = await levels.setFlag(String(req.params.id), String(req.params.key), false)
|
||||
updated ? res.json({ ok: true }) : res.status(404).json({ error: 'Level not found' })
|
||||
} catch (error) { next(error) }
|
||||
})
|
||||
app.get('/api/levels/:id', async (req, res, next) => {
|
||||
try {
|
||||
const level = await levels.getLevel(req.params.id, wantsEdit(req))
|
||||
@@ -142,6 +171,34 @@ function requireEditing(res: express.Response) {
|
||||
if (!editingEnabled) { res.status(403).json({ error: 'Level editing is disabled' }); return false }
|
||||
return true
|
||||
}
|
||||
app.get('/api/levels/:id/evidence-match-rules', requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const rules = await levels.listEvidenceMatchRules(String(req.params.id))
|
||||
rules ? res.json(rules) : res.status(404).json({ error: 'Level not found' })
|
||||
} catch (error) { next(error) }
|
||||
})
|
||||
app.post('/api/levels/:id/evidence-match-rules', requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
if (!requireEditing(res)) return
|
||||
const rule = await levels.createEvidenceMatchRule(String(req.params.id), req.body)
|
||||
rule ? res.status(201).json(rule) : res.status(404).json({ error: 'Level not found' })
|
||||
} catch (error) { next(error) }
|
||||
})
|
||||
app.put('/api/levels/:id/evidence-match-rules/:ruleId', requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
if (!requireEditing(res)) return
|
||||
const rule = await levels.updateEvidenceMatchRule(String(req.params.id), String(req.params.ruleId), req.body)
|
||||
rule ? res.json(rule) : res.status(404).json({ error: 'Level or evidence match rule not found' })
|
||||
} catch (error) { next(error) }
|
||||
})
|
||||
app.delete('/api/levels/:id/evidence-match-rules/:ruleId', requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
if (!requireEditing(res)) return
|
||||
const removed = await levels.deleteEvidenceMatchRule(String(req.params.id), String(req.params.ruleId))
|
||||
if (removed === null) return res.status(404).json({ error: 'Level not found' })
|
||||
removed ? res.json({ ok: true }) : res.status(404).json({ error: 'Evidence match rule not found' })
|
||||
} catch (error) { next(error) }
|
||||
})
|
||||
app.get('/api/admin/mysteries', requireAdmin, async (_req, res, next) => {
|
||||
try { res.json(await narrative.listMysteries()) } catch (error) { next(error) }
|
||||
})
|
||||
@@ -350,6 +407,23 @@ app.post('/api/playthroughs/:id/advance', async (req, res, next) => {
|
||||
} catch (error) { next(error) }
|
||||
})
|
||||
|
||||
// The playthrough case-state (achievements). Read is open; granting is a dev-only
|
||||
// stand-in until the server-side achievement rule engine drives awards from play.
|
||||
app.get('/api/playthroughs/:id/achievements', async (req, res, next) => {
|
||||
try {
|
||||
const flags = await narrative.listAchievements(String(req.params.id))
|
||||
flags ? res.json(flags) : res.status(404).json({ error: 'Playthrough not found' })
|
||||
} catch (error) { next(error) }
|
||||
})
|
||||
app.post('/api/playthroughs/:id/achievements', async (req, res, next) => {
|
||||
try {
|
||||
if (process.env.NODE_ENV === 'production') return res.status(403).json({ error: 'Manual grants are disabled' })
|
||||
if (!req.body?.flagKey) return res.status(400).json({ error: 'A flagKey is required' })
|
||||
const result = await narrative.awardAchievement(String(req.params.id), String(req.body.flagKey), req.body.nodeId ? String(req.body.nodeId) : null)
|
||||
result.ok ? res.json({ earned: result.earned }) : res.status(result.error === 'Playthrough not found' ? 404 : 400).json({ error: result.error })
|
||||
} catch (error) { next(error) }
|
||||
})
|
||||
|
||||
app.use((error: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
|
||||
if (error instanceof multer.MulterError) {
|
||||
return res.status(error.code === 'LIMIT_FILE_SIZE' ? 413 : 400).json({ error: error.code === 'LIMIT_FILE_SIZE' ? 'Document exceeds the upload limit' : error.message })
|
||||
|
||||
Reference in New Issue
Block a user