feat(mam-api): add personal API token routes
This commit is contained in:
parent
d23ca9be73
commit
1e4c92c2df
1 changed files with 70 additions and 0 deletions
70
services/mam-api/src/routes/tokens.js
Normal file
70
services/mam-api/src/routes/tokens.js
Normal 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;
|
||||||
Loading…
Reference in a new issue