123 lines
5.6 KiB
JavaScript
123 lines
5.6 KiB
JavaScript
import { Router } from 'express';
|
|
import crypto from 'node:crypto';
|
|
import db from '../db/index.js';
|
|
import { HttpError } from '../middleware/errorHandler.js';
|
|
|
|
const router = Router();
|
|
|
|
const KEY_PREFIX_LEN = 12; // ex: "clk_live_ab3" — assez pour identifier sans exposer le secret
|
|
|
|
function generateKey() {
|
|
const secret = crypto.randomBytes(24).toString('hex'); // 48 caractères hex
|
|
const full = `clk_live_${secret}`;
|
|
const hash = crypto.createHash('sha256').update(full).digest('hex');
|
|
return { full, hash, prefix: full.slice(0, KEY_PREFIX_LEN) };
|
|
}
|
|
|
|
/* ── GET /api/api-keys ── liste des clés de l'utilisateur connecté ──────── */
|
|
router.get('/', (req, res) => {
|
|
const rows = db.prepare(`
|
|
SELECT k.id, k.nom, k.key_prefix, k.scopes, k.investisseur_id, k.scope_all,
|
|
i.nom AS investisseur_nom, k.created_at, k.last_used_at, k.revoked_at
|
|
FROM api_keys k
|
|
JOIN investisseurs i ON i.id = k.investisseur_id
|
|
WHERE k.user_id = ?
|
|
ORDER BY k.revoked_at IS NOT NULL, k.created_at DESC
|
|
`).all(req.user.id);
|
|
res.json(rows);
|
|
});
|
|
|
|
/* ── POST /api/api-keys ── créer une nouvelle clé (nom + investisseur, ou
|
|
scope_all pour "Famille et entreprises") ──────────────────────────────
|
|
scope_all=true n'est autorisé que si investisseur_id désigne le profil
|
|
principal — enforcement serveur, indépendant de ce que montre l'UI, pour
|
|
qu'un appel direct à l'API ne puisse pas contourner cette règle. */
|
|
router.post('/', (req, res, next) => {
|
|
try {
|
|
const nom = (req.body?.nom || '').trim();
|
|
const investisseur_id = Number(req.body?.investisseur_id);
|
|
const scope_all = !!req.body?.scope_all;
|
|
|
|
if (!nom) throw new HttpError(400, 'Le nom de la clé est requis');
|
|
if (nom.length > 100) throw new HttpError(400, 'Le nom de la clé est trop long (100 caractères max)');
|
|
if (!Number.isInteger(investisseur_id)) throw new HttpError(400, 'investisseur_id est requis');
|
|
|
|
const inv = db.prepare('SELECT id, is_principal FROM investisseurs WHERE id = ? AND user_id = ?')
|
|
.get(investisseur_id, req.user.id);
|
|
if (!inv) throw new HttpError(404, 'Investisseur introuvable');
|
|
if (scope_all && !inv.is_principal) {
|
|
throw new HttpError(403, 'Seul le profil principal peut créer une clé « Famille et entreprises »');
|
|
}
|
|
|
|
const { full, hash, prefix } = generateKey();
|
|
|
|
const info = db.prepare(`
|
|
INSERT INTO api_keys (user_id, investisseur_id, nom, key_prefix, key_hash, scopes, scope_all)
|
|
VALUES (?, ?, ?, ?, ?, 'read', ?)
|
|
`).run(req.user.id, investisseur_id, nom, prefix, hash, scope_all ? 1 : 0);
|
|
|
|
const saved = db.prepare(`
|
|
SELECT k.id, k.nom, k.key_prefix, k.scopes, k.investisseur_id, k.scope_all,
|
|
i.nom AS investisseur_nom, k.created_at, k.last_used_at, k.revoked_at
|
|
FROM api_keys k JOIN investisseurs i ON i.id = k.investisseur_id
|
|
WHERE k.id = ?
|
|
`).get(info.lastInsertRowid);
|
|
|
|
// La valeur en clair n'est renvoyée qu'ici, une seule fois.
|
|
res.status(201).json({ ...saved, key: full });
|
|
} catch (e) { next(e); }
|
|
});
|
|
|
|
/* ── PATCH /api/api-keys/:id ── renommer une clé ─────────────────────────── */
|
|
router.patch('/:id', (req, res, next) => {
|
|
try {
|
|
const nom = (req.body?.nom || '').trim();
|
|
if (!nom) throw new HttpError(400, 'Le nom de la clé est requis');
|
|
if (nom.length > 100) throw new HttpError(400, 'Le nom de la clé est trop long (100 caractères max)');
|
|
|
|
const existing = db.prepare('SELECT id FROM api_keys WHERE id = ? AND user_id = ?')
|
|
.get(req.params.id, req.user.id);
|
|
if (!existing) throw new HttpError(404, 'Clé API introuvable');
|
|
|
|
db.prepare('UPDATE api_keys SET nom = ? WHERE id = ?').run(nom, req.params.id);
|
|
|
|
const saved = db.prepare(`
|
|
SELECT k.id, k.nom, k.key_prefix, k.scopes, k.investisseur_id, k.scope_all,
|
|
i.nom AS investisseur_nom, k.created_at, k.last_used_at, k.revoked_at
|
|
FROM api_keys k JOIN investisseurs i ON i.id = k.investisseur_id
|
|
WHERE k.id = ?
|
|
`).get(req.params.id);
|
|
res.json(saved);
|
|
} catch (e) { next(e); }
|
|
});
|
|
|
|
/* ── DELETE /api/api-keys/:id ── révoque la clé (soft-delete) ───────────── */
|
|
router.delete('/:id', (req, res, next) => {
|
|
try {
|
|
const existing = db.prepare('SELECT id, revoked_at FROM api_keys WHERE id = ? AND user_id = ?')
|
|
.get(req.params.id, req.user.id);
|
|
if (!existing) throw new HttpError(404, 'Clé API introuvable');
|
|
if (existing.revoked_at) return res.json({ revoked: true });
|
|
|
|
db.prepare(`UPDATE api_keys SET revoked_at = datetime('now') WHERE id = ?`).run(req.params.id);
|
|
res.json({ revoked: true });
|
|
} catch (e) { next(e); }
|
|
});
|
|
|
|
/* ── DELETE /api/api-keys/:id/purge ── suppression définitive de la ligne ──
|
|
Distinct de la révocation ci-dessus : ici la clé disparaît complètement
|
|
(active ou déjà révoquée). Le frontend affiche un avertissement avant
|
|
d'appeler cette route si la clé est encore active. ─────────────────── */
|
|
router.delete('/:id/purge', (req, res, next) => {
|
|
try {
|
|
const existing = db.prepare('SELECT id FROM api_keys WHERE id = ? AND user_id = ?')
|
|
.get(req.params.id, req.user.id);
|
|
if (!existing) throw new HttpError(404, 'Clé API introuvable');
|
|
|
|
db.prepare('DELETE FROM api_keys WHERE id = ?').run(req.params.id);
|
|
res.json({ deleted: true });
|
|
} catch (e) { next(e); }
|
|
});
|
|
|
|
export default router;
|