feat(mam-api): add personal API token routes

This commit is contained in:
Zac Gaetano 2026-05-18 12:50:58 -04:00
parent d23ca9be73
commit 1e4c92c2df

View file

@ -0,0 +1,70 @@
/**
* Personal API token routes (requires authentication)
*
* GET /api/v1/tokens list current user's tokens (no raw values)
* POST /api/v1/tokens create token, returns raw value ONCE
* DELETE /api/v1/tokens/:id revoke token
*/
import express from 'express';
import crypto from 'crypto';
import pool from '../db/pool.js';
const router = express.Router();
// Helper: get current user ID from session or req.user
const userId = req => req.user?.id || req.session?.userId;
// ── List ──────────────────────────────────────────────────────
router.get('/', async (req, res, next) => {
try {
const { rows } = await pool.query(
`SELECT id, name, token_prefix, last_used_at, expires_at, created_at
FROM api_tokens
WHERE user_id = $1
ORDER BY created_at DESC`,
[userId(req)]
);
res.json(rows);
} catch (err) { next(err); }
});
// ── Create ────────────────────────────────────────────────────
router.post('/', async (req, res, next) => {
try {
const { name, expires_in_days } = req.body;
if (!name) return res.status(400).json({ error: 'name required' });
// Generate: wd_ + 40 random hex chars = 43 chars total
const raw = 'wd_' + crypto.randomBytes(20).toString('hex');
const hash = crypto.createHash('sha256').update(raw).digest('hex');
const prefix = raw.slice(0, 10); // "wd_" + first 7 hex chars
const expiresAt = expires_in_days
? new Date(Date.now() + parseInt(expires_in_days, 10) * 86400000)
: null;
const { rows } = await pool.query(
`INSERT INTO api_tokens (user_id, name, token_hash, token_prefix, expires_at)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, name, token_prefix, last_used_at, expires_at, created_at`,
[userId(req), name.trim(), hash, prefix, expiresAt]
);
// Return raw token ONCE — it is never stored in plaintext
res.status(201).json({ ...rows[0], token: raw });
} catch (err) { next(err); }
});
// ── 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, userId(req)]
);
if (!rowCount) return res.status(404).json({ error: 'Token not found' });
res.json({ message: 'Token revoked' });
} catch (err) { next(err); }
});
export default router;