// Current-user API token CRUD. The raw token is returned exactly once at // creation time; only the SHA-256 hash and an 8-char display prefix are stored. import express from 'express'; import pool from '../db/pool.js'; import { generateToken, hashToken, tokenDisplayPrefix } from '../auth/tokens.js'; const router = express.Router(); // GET / — list current user's tokens (prefix only, never the raw token) router.get('/', async (req, res, next) => { try { const { rows } = await pool.query( `SELECT id, name, token_prefix AS prefix, bound_hostname, last_used_at, expires_at, created_at FROM api_tokens WHERE user_id = $1 ORDER BY created_at DESC`, [req.user.id]); res.json(rows); } catch (err) { next(err); } }); // POST / — create a new token. Optional bound_hostname binds the token to a // specific node-agent hostname (migration 019) — the /cluster/heartbeat handler // rejects heartbeats whose body.hostname doesn't match a bound token. router.post('/', async (req, res, next) => { try { const { name, expires_at, bound_hostname } = req.body || {}; if (!name || typeof name !== 'string') return res.status(400).json({ error: 'name required' }); if (bound_hostname !== undefined && bound_hostname !== null && typeof bound_hostname !== 'string') { return res.status(400).json({ error: 'bound_hostname must be a string' }); } const raw = generateToken(); const { rows } = await pool.query( `INSERT INTO api_tokens (user_id, name, token_hash, token_prefix, expires_at, bound_hostname) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, name, token_prefix AS prefix, bound_hostname, expires_at, created_at`, [req.user.id, name.trim(), hashToken(raw), tokenDisplayPrefix(raw), expires_at || null, bound_hostname?.trim() || null]); res.status(201).json({ ...rows[0], token: raw }); // raw token shown exactly once } catch (err) { next(err); } }); // DELETE /:id — revoke; only the owner can revoke router.delete('/:id', async (req, res, next) => { try { const { rowCount } = await pool.query( `DELETE FROM api_tokens WHERE id = $1 AND user_id = $2`, [req.params.id, req.user.id]); if (rowCount === 0) return res.status(404).json({ error: 'token not found' }); res.status(204).end(); } catch (err) { next(err); } }); export default router;