import { Router } from 'express'; import { isValidToken } from './db.js'; import { listArticles, getArticle, updateArticle, deleteArticle } from './db.js'; const router = Router(); function escapeHtml(s: unknown): string { return String(s ?? '') .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } // Same dark, monospace-accented theme as flit/wisp — same CSS vars, same // card/button/input vocabulary — so stash reads as part of the same family // of tools rather than a bare database inspector. function page(title: string, body: string): string { return ` ${escapeHtml(title)}
${body}
`; } // Browser navigation can't set an Authorization header, so the web UI takes // the token as a query param instead. The API (used by the extension) keeps // using the Bearer header — see auth.ts. function requireTokenQuery(req: any, res: any, next: any) { const token = typeof req.query.t === 'string' ? req.query.t : ''; if (!token || !isValidToken(token)) { res.status(401).send(page('Unauthorized', '

stash

Missing or invalid ?t= token.

')); return; } req.token = token; next(); } router.get('/', requireTokenQuery, (req: any, res) => { const unread = req.query.unread === 'true'; const q = typeof req.query.q === 'string' ? req.query.q.trim() : ''; const articles = listArticles({ unread, q: q || undefined, limit: 100 }); const t = req.token; const items = articles.map(a => `
  • ${escapeHtml(a.title)}
  • `).join(''); res.send(page('stash', `

    stash

    `)); }); router.get('/read/:id', requireTokenQuery, (req: any, res) => { let article = getArticle(Number(req.params.id)); if (!article) { res.status(404).send(page('Not found', '

    stash

    Not found.

    ')); return; } if (!article.read_at) { article = updateArticle(article.id, { read_at: new Date().toISOString() }) ?? article; } const t = req.token; res.send(page(article.title, `
    ← back to stash

    ${escapeHtml(article.title)}

    ${escapeHtml(new URL(article.url).hostname)} ${article.author ? `— ${escapeHtml(article.author)}` : ''}
    ${article.content_html}
    `)); }); router.post('/toggle-read/:id', requireTokenQuery, (req: any, res) => { const article = getArticle(Number(req.params.id)); if (!article) { res.status(404).send(page('Not found', '

    stash

    Not found.

    ')); return; } updateArticle(article.id, { read_at: article.read_at ? null : new Date().toISOString() }); const redirect = typeof req.body?.redirect === 'string' ? req.body.redirect : `/?t=${encodeURIComponent(req.token)}`; res.redirect(redirect); }); router.post('/delete/:id', requireTokenQuery, (req: any, res) => { deleteArticle(Number(req.params.id)); const redirect = typeof req.body?.redirect === 'string' ? req.body.redirect : `/?t=${encodeURIComponent(req.token)}`; res.redirect(redirect); }); export default router;