const express = require('express'); const fs = require('fs'); const path = require('path'); const multer = require('multer'); const db = require('../db'); const router = express.Router(); const DB_PATH = path.resolve(__dirname, '..', '..', 'data', 'studyos.db'); const SQLITE_MAGIC = Buffer.from('SQLite format 3\0', 'utf8'); const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 500 * 1024 * 1024 } }); function checkAdminKey(req, res) { const adminKey = process.env.ADMIN_KEY || 'studyos-admin'; const headerKey = req.headers['x-admin-key']; if (!headerKey || headerKey !== adminKey) { res.status(403).json({ error: 'Forbidden: invalid or missing admin key' }); return false; } return true; } // GET /api/config — all key-value pairs router.get('/', (req, res) => { try { const rows = db.prepare('SELECT key, value FROM config ORDER BY key').all(); const result = {}; for (const row of rows) { result[row.key] = row.value; } res.json(result); } catch (err) { console.error('[config] list error:', err.message); res.status(500).json({ error: err.message }); } }); // PUT /api/config — upsert by key // Body: { key, value } router.put('/', (req, res) => { const { key, value } = req.body; if (key === undefined || value === undefined) { return res.status(400).json({ error: 'key and value are required' }); } try { const existing = db.prepare('SELECT key FROM config WHERE key = ?').get(key); if (existing) { db.prepare('UPDATE config SET value = ? WHERE key = ?').run(value, key); } else { db.prepare('INSERT INTO config (key, value) VALUES (?, ?)').run(key, value); } res.json({ key, value }); } catch (err) { console.error('[config] upsert error:', err.message); res.status(500).json({ error: err.message }); } }); // POST /api/config/test-vlm — proxy VLM test through backend to avoid CORS router.post('/test-vlm', async (req, res) => { const { url } = req.body; if (!url) { return res.status(400).json({ error: 'url is required' }); } // SSRF protection: only allow URLs matching the configured VLM endpoint const vlmConfig = db.prepare("SELECT value FROM config WHERE key = 'vlm_endpoint'").get(); const allowed = (vlmConfig?.value || '').replace(/\/+$/, ''); if (!url.startsWith(allowed)) { return res.status(400).json({ error: 'URL must match configured VLM endpoint' }); } const start = Date.now(); try { const resp = await fetch(url, { method: 'HEAD', signal: AbortSignal.timeout(5000) }); res.json({ success: resp.ok, latency_ms: Date.now() - start, message: resp.ok ? 'Endpoint responde' : `HTTP ${resp.status}`, }); } catch (err) { res.json({ success: false, latency_ms: Date.now() - start, message: err.message, }); } }); router.get('/backup', (req, res) => { if (!checkAdminKey(req, res)) return; if (!fs.existsSync(DB_PATH)) return res.status(404).json({ error: 'No database' }); res.setHeader('Content-Type', 'application/octet-stream'); res.setHeader('Content-Disposition', 'attachment; filename="studyos.db"'); fs.createReadStream(DB_PATH).pipe(res); }); router.post('/restore', upload.single('file'), (req, res) => { if (!checkAdminKey(req, res)) return; if (!req.file) return res.status(400).json({ error: 'No file uploaded' }); const buf = req.file.buffer; if (buf.length < 16 || !buf.slice(0, 16).equals(SQLITE_MAGIC)) { return res.status(400).json({ error: 'Not a valid SQLite database' }); } const tmpPath = DB_PATH + '.tmp'; fs.writeFileSync(tmpPath, buf); fs.renameSync(tmpPath, DB_PATH); res.json({ ok: true, message: 'Restore complete — reload required' }); }); module.exports = router;