UNPKG

@accounter/scraper-app

Version:
145 lines (133 loc) • 5.04 kB
import { readFile, rename, writeFile } from 'node:fs/promises'; import type { FastifyInstance } from 'fastify'; import { registerAccountsRoutes } from './accounts-routes.js'; import { registerSettingsRoutes } from './settings-routes.js'; import { registerSourcesRoutes } from './sources-routes.js'; import { getVault, hasVaultFile, isLocked, lockVault, unlockVault } from './vault-store.js'; import { defaultVault, getVaultPath, saveVaultFile } from './vault.js'; type UnlockBody = { password: string }; type CreateBody = { password: string; serverUrl: string; apiKey: string }; export async function registerVaultRoutes(app: FastifyInstance): Promise<void> { app.get('/api/vault/status', async () => ({ locked: isLocked(), hasFile: await hasVaultFile(), })); app.post<{ Body: UnlockBody }>( '/api/vault/unlock', { schema: { body: { type: 'object', required: ['password'], properties: { password: { type: 'string' } }, }, }, }, async (req, reply) => { const result = await unlockVault(req.body.password); if (result === 'ok') return { ok: true }; const status = result === 'not-found' ? 404 : 401; return reply.status(status).send({ error: result }); }, ); app.post<{ Body: CreateBody }>( '/api/vault/create', { schema: { body: { type: 'object', required: ['password', 'serverUrl', 'apiKey'], properties: { password: { type: 'string' }, serverUrl: { type: 'string' }, apiKey: { type: 'string' }, }, }, }, }, async (req, reply) => { if (await hasVaultFile()) return reply.status(409).send({ error: 'vault-already-exists' }); const { password, serverUrl, apiKey } = req.body; lockVault(); const vault = defaultVault(); vault.settings.serverUrl = serverUrl; vault.settings.apiKey = apiKey; await saveVaultFile(getVaultPath(), vault, password); const result = await unlockVault(password); if (result !== 'ok') return reply.status(500).send({ error: result }); return reply.status(201).send({ ok: true }); }, ); app.get('/api/vault/env-path', async () => ({ path: getVaultPath(), })); app.get('/api/vault/download', async (_req, reply) => { try { const fileBuffer = await readFile(getVaultPath()); reply.header('Content-Type', 'application/octet-stream'); reply.header('Content-Disposition', 'attachment; filename=".vault"'); return reply.send(fileBuffer); } catch (err) { if ((err as NodeJS.ErrnoException).code === 'ENOENT') { return reply.status(404).send({ error: 'no-vault-file' }); } throw err; } }); app.addContentTypeParser('application/octet-stream', { parseAs: 'buffer' }, (_req, body, done) => done(null, body), ); app.post<{ Querystring: { force?: string }; Body: Buffer }>( '/api/vault/upload', { config: { rawBody: false } }, async (req, reply) => { const force = req.query.force === 'true'; const vaultPath = getVaultPath(); try { const exists = await hasVaultFile(); if (exists && !force) { return reply.status(409).send({ error: 'vault-already-exists' }); } await writeFile(vaultPath + '.tmp', req.body as Buffer); await rename(vaultPath + '.tmp', vaultPath); lockVault(); return reply.status(200).send({ ok: true }); } catch (err) { req.log.error(err, 'Vault upload failed'); return reply.status(500).send({ error: 'write-failed' }); } }, ); app.get('/api/vault/path', async (_req, reply) => { if (isLocked()) return reply.status(401).send({ error: 'vault-locked' }); return { path: getVaultPath() }; }); app.get('/api/vault/test-connection', async (_req, reply) => { if (isLocked()) return reply.status(401).send({ error: 'vault-locked' }); const { serverUrl, apiKey } = getVault().settings; if (!serverUrl || !apiKey) { return reply.status(400).send({ error: 'Server URL and API key are not configured' }); } const t0 = Date.now(); try { const res = await fetch(serverUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` }, body: JSON.stringify({ query: '{ __typename }' }), signal: AbortSignal.timeout(10_000), }); const latencyMs = Date.now() - t0; if (!res.ok) { const text = await res.text().catch(() => res.statusText); return { ok: false, error: `HTTP ${res.status}: ${text}`, latencyMs }; } return { ok: true, latencyMs }; } catch (err) { const latencyMs = Date.now() - t0; return { ok: false, error: err instanceof Error ? err.message : String(err), latencyMs }; } }); await registerSourcesRoutes(app); await registerSettingsRoutes(app); await registerAccountsRoutes(app); }