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