Création de la feature API V1
This commit is contained in:
@@ -2254,4 +2254,29 @@ db.exec('CREATE INDEX IF NOT EXISTS idx_objectifs_investisseur ON objectifs(inve
|
||||
}
|
||||
}
|
||||
|
||||
// ── Migration : table api_keys ───────────────────────────────────────────
|
||||
// Clés API générées par l'utilisateur pour un accès programmatique/MCP.
|
||||
// Chaque clé est scopée à un investisseur précis (usage single-user
|
||||
// multi-investisseur — pas de notion multi-tenant ici). Seul le hash SHA-256
|
||||
// est stocké ; la valeur en clair n'est montrée qu'une fois à la création.
|
||||
// `key_prefix` (8 premiers caractères) permet d'identifier une clé dans
|
||||
// l'UI sans jamais réafficher la valeur complète.
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS api_keys (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
investisseur_id INTEGER NOT NULL REFERENCES investisseurs(id) ON DELETE CASCADE,
|
||||
nom TEXT NOT NULL,
|
||||
key_prefix TEXT NOT NULL,
|
||||
key_hash TEXT NOT NULL UNIQUE,
|
||||
scopes TEXT NOT NULL DEFAULT 'read',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
last_used_at TEXT,
|
||||
revoked_at TEXT
|
||||
)
|
||||
`);
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_api_keys_user ON api_keys(user_id)');
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_api_keys_inv ON api_keys(investisseur_id)');
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash)');
|
||||
|
||||
export default db;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import crypto from 'node:crypto';
|
||||
import db from '../db/index.js';
|
||||
|
||||
/**
|
||||
* Authentification par clé API (X-API-Key), distincte du JWT utilisé par le
|
||||
* frontend (requireAuth). Une clé API est toujours scopée à un seul
|
||||
* investisseur — pas de notion de "scope=all" ici, contrairement aux routes
|
||||
* internes. Réservé aux routes /api/v1 (lecture seule, Phase 1).
|
||||
*/
|
||||
export function requireApiKey(req, res, next) {
|
||||
const key = req.header('X-API-Key');
|
||||
if (!key) {
|
||||
return res.status(401).json({ error: 'Missing X-API-Key header' });
|
||||
}
|
||||
|
||||
const hash = crypto.createHash('sha256').update(key).digest('hex');
|
||||
const row = db.prepare(`
|
||||
SELECT k.id, k.investisseur_id, k.scopes, k.revoked_at
|
||||
FROM api_keys k
|
||||
WHERE k.key_hash = ?
|
||||
`).get(hash);
|
||||
|
||||
if (!row || row.revoked_at) {
|
||||
return res.status(401).json({ error: 'Invalid or revoked API key' });
|
||||
}
|
||||
|
||||
db.prepare(`UPDATE api_keys SET last_used_at = datetime('now') WHERE id = ?`).run(row.id);
|
||||
|
||||
req.apiKeyId = row.id;
|
||||
req.investisseurId = row.investisseur_id;
|
||||
req.apiScopes = (row.scopes || 'read').split(',').map(s => s.trim());
|
||||
next();
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
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,
|
||||
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) ──── */
|
||||
router.post('/', (req, res, next) => {
|
||||
try {
|
||||
const nom = (req.body?.nom || '').trim();
|
||||
const investisseur_id = Number(req.body?.investisseur_id);
|
||||
|
||||
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 FROM investisseurs WHERE id = ? AND user_id = ?')
|
||||
.get(investisseur_id, req.user.id);
|
||||
if (!inv) throw new HttpError(404, 'Investisseur introuvable');
|
||||
|
||||
const { full, hash, prefix } = generateKey();
|
||||
|
||||
const info = db.prepare(`
|
||||
INSERT INTO api_keys (user_id, investisseur_id, nom, key_prefix, key_hash, scopes)
|
||||
VALUES (?, ?, ?, ?, ?, 'read')
|
||||
`).run(req.user.id, investisseur_id, nom, prefix, hash);
|
||||
|
||||
const saved = db.prepare(`
|
||||
SELECT k.id, k.nom, k.key_prefix, k.scopes, k.investisseur_id,
|
||||
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,
|
||||
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;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Router } from 'express';
|
||||
import db from '../../db/index.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /dashboard:
|
||||
* get:
|
||||
* summary: Synthèse du portefeuille (KPIs)
|
||||
* tags: [Dashboard]
|
||||
* security: [{ ApiKeyAuth: [] }]
|
||||
* responses:
|
||||
* 200: { description: Synthèse KPI }
|
||||
*/
|
||||
router.get('/', (req, res) => {
|
||||
const invId = req.investisseurId;
|
||||
|
||||
const investissements = db.prepare(`
|
||||
SELECT
|
||||
COUNT(*) AS nb_investissements,
|
||||
COALESCE(SUM(montant_investi), 0) AS total_investi,
|
||||
COALESCE(SUM(CASE WHEN statut='en_cours' THEN montant_investi END), 0) AS encours,
|
||||
COALESCE(SUM(CASE WHEN statut='rembourse' THEN montant_investi END), 0) AS rembourse
|
||||
FROM investissements WHERE investisseur_id = ?
|
||||
`).get(invId);
|
||||
|
||||
const interets = db.prepare(`
|
||||
SELECT
|
||||
COALESCE(SUM(r.interets_bruts), 0) AS interets_bruts,
|
||||
COALESCE(SUM(r.interets_nets), 0) AS interets_nets,
|
||||
COALESCE(SUM(r.capital), 0) AS capital_recu,
|
||||
COALESCE(SUM(r.net_recu), 0) AS net_recu_total
|
||||
FROM remboursements r
|
||||
JOIN investissements i ON i.id = r.investissement_id
|
||||
WHERE i.investisseur_id = ?
|
||||
`).get(invId);
|
||||
|
||||
const cash = db.prepare(`
|
||||
SELECT
|
||||
COALESCE(SUM(CASE WHEN type='depot' THEN montant END), 0) AS total_depots,
|
||||
COALESCE(SUM(CASE WHEN type='retrait' THEN montant END), 0) AS total_retraits
|
||||
FROM depots_retraits WHERE investisseur_id = ?
|
||||
`).get(invId);
|
||||
|
||||
res.json({ investissements, interets, cash });
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Router } from 'express';
|
||||
import db from '../../db/index.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /depots-retraits:
|
||||
* get:
|
||||
* summary: Liste des mouvements de cash (dépôts / retraits)
|
||||
* tags: [Dépôts / Retraits]
|
||||
* security: [{ ApiKeyAuth: [] }]
|
||||
* responses:
|
||||
* 200: { description: Liste des mouvements }
|
||||
*/
|
||||
router.get('/', (req, res) => {
|
||||
const rows = db.prepare(`
|
||||
SELECT dr.id, dr.date_operation, p.nom AS plateforme_nom, dr.type,
|
||||
dr.montant, dr.libelle
|
||||
FROM depots_retraits dr
|
||||
JOIN plateformes p ON p.id = dr.plateforme_id
|
||||
WHERE dr.investisseur_id = ?
|
||||
ORDER BY dr.date_operation DESC
|
||||
`).all(req.investisseurId);
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Router } from 'express';
|
||||
import investisseurRouter from './investisseur.js';
|
||||
import investissementsRouter from './investissements.js';
|
||||
import remboursementsRouter from './remboursements.js';
|
||||
import depotsRetraitsRouter from './depotsRetraits.js';
|
||||
import dashboardRouter from './dashboard.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use('/investisseur', investisseurRouter);
|
||||
router.use('/investissements', investissementsRouter);
|
||||
router.use('/remboursements', remboursementsRouter);
|
||||
router.use('/depots-retraits', depotsRetraitsRouter);
|
||||
router.use('/dashboard', dashboardRouter);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Router } from 'express';
|
||||
import db from '../../db/index.js';
|
||||
import { HttpError } from '../../middleware/errorHandler.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
const LIST_COLUMNS = `
|
||||
i.id, i.nom_projet, i.emetteur, p.nom AS plateforme_nom,
|
||||
i.date_souscription, i.date_premiere_echeance, i.date_cible,
|
||||
i.montant_investi, i.taux_interet, i.duree_mois,
|
||||
i.type_remb, i.freq_interets, i.statut, i.reference
|
||||
`;
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /investissements:
|
||||
* get:
|
||||
* summary: Liste des investissements de l'investisseur
|
||||
* tags: [Investissements]
|
||||
* security: [{ ApiKeyAuth: [] }]
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: statut
|
||||
* schema: { type: string, enum: [en_cours, rembourse, en_retard, procedure, cloture] }
|
||||
* responses:
|
||||
* 200: { description: Liste des investissements }
|
||||
*/
|
||||
router.get('/', (req, res) => {
|
||||
const { statut } = req.query;
|
||||
const conds = ['i.investisseur_id = ?'];
|
||||
const args = [req.investisseurId];
|
||||
if (statut) { conds.push('i.statut = ?'); args.push(statut); }
|
||||
|
||||
const rows = db.prepare(`
|
||||
SELECT ${LIST_COLUMNS}
|
||||
FROM investissements i
|
||||
JOIN plateformes p ON p.id = i.plateforme_id
|
||||
WHERE ${conds.join(' AND ')}
|
||||
ORDER BY i.date_souscription DESC
|
||||
`).all(...args);
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /investissements/{id}:
|
||||
* get:
|
||||
* summary: Détail d'un investissement, avec ses remboursements réels
|
||||
* tags: [Investissements]
|
||||
* security: [{ ApiKeyAuth: [] }]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema: { type: integer }
|
||||
* responses:
|
||||
* 200: { description: Détail de l'investissement }
|
||||
* 404: { description: Investissement introuvable }
|
||||
*/
|
||||
router.get('/:id', (req, res, next) => {
|
||||
try {
|
||||
const inv = db.prepare(`
|
||||
SELECT ${LIST_COLUMNS}, i.notes
|
||||
FROM investissements i
|
||||
JOIN plateformes p ON p.id = i.plateforme_id
|
||||
WHERE i.id = ? AND i.investisseur_id = ?
|
||||
`).get(req.params.id, req.investisseurId);
|
||||
if (!inv) throw new HttpError(404, 'Investissement introuvable');
|
||||
|
||||
const remboursements = db.prepare(`
|
||||
SELECT id, date_remb, capital, interets_bruts, prelev_sociaux,
|
||||
prelev_forfaitaire, cashback, interets_nets, net_recu, statut
|
||||
FROM remboursements
|
||||
WHERE investissement_id = ?
|
||||
ORDER BY date_remb
|
||||
`).all(inv.id);
|
||||
|
||||
res.json({ ...inv, remboursements });
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Router } from 'express';
|
||||
import db from '../../db/index.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /investisseur:
|
||||
* get:
|
||||
* summary: Profil de l'investisseur lié à la clé API
|
||||
* tags: [Investisseur]
|
||||
* security: [{ ApiKeyAuth: [] }]
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Profil investisseur
|
||||
* 401:
|
||||
* description: Clé API invalide ou manquante
|
||||
*/
|
||||
router.get('/', (req, res) => {
|
||||
const inv = db.prepare(`
|
||||
SELECT id, nom, prenom, type, type_fiscal, notes, created_at
|
||||
FROM investisseurs WHERE id = ?
|
||||
`).get(req.investisseurId);
|
||||
res.json(inv);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Router } from 'express';
|
||||
import db from '../../db/index.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /remboursements:
|
||||
* get:
|
||||
* summary: Liste des remboursements réels perçus
|
||||
* tags: [Remboursements]
|
||||
* security: [{ ApiKeyAuth: [] }]
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: date_debut
|
||||
* schema: { type: string, format: date }
|
||||
* - in: query
|
||||
* name: date_fin
|
||||
* schema: { type: string, format: date }
|
||||
* responses:
|
||||
* 200: { description: Liste des remboursements }
|
||||
*/
|
||||
router.get('/', (req, res) => {
|
||||
const { date_debut, date_fin } = req.query;
|
||||
const conds = ['i.investisseur_id = ?'];
|
||||
const args = [req.investisseurId];
|
||||
if (date_debut) { conds.push('r.date_remb >= ?'); args.push(date_debut); }
|
||||
if (date_fin) { conds.push('r.date_remb <= ?'); args.push(date_fin); }
|
||||
|
||||
const rows = db.prepare(`
|
||||
SELECT r.id, r.date_remb, i.nom_projet, p.nom AS plateforme_nom,
|
||||
r.capital, r.interets_bruts, r.prelev_sociaux, r.prelev_forfaitaire,
|
||||
r.cashback, r.interets_nets, r.net_recu, r.statut
|
||||
FROM remboursements r
|
||||
JOIN investissements i ON i.id = r.investissement_id
|
||||
JOIN plateformes p ON p.id = i.plateforme_id
|
||||
WHERE ${conds.join(' AND ')}
|
||||
ORDER BY r.date_remb DESC
|
||||
`).all(...args);
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -52,6 +52,10 @@ import secteursInvRouter from './routes/secteurs-inv.js';
|
||||
import associationsInvRouter from './routes/associations-inv.js';
|
||||
import notificationsRouter from './routes/notifications.js';
|
||||
import ticketsRouter from './routes/tickets.js';
|
||||
import apiKeysRouter from './routes/apiKeys.js';
|
||||
import v1Router from './routes/v1/index.js';
|
||||
import { requireApiKey } from './middleware/apiKey.js';
|
||||
import { swaggerSpec, swaggerUi } from './swagger.js';
|
||||
import db from './db/index.js';
|
||||
import { getSmtpConfig } from './utils/mailer.js';
|
||||
|
||||
@@ -104,6 +108,15 @@ app.get('/api/app-info', (_, res) => {
|
||||
|
||||
app.use('/api/auth', authLimiter, authRouter);
|
||||
|
||||
// ── API publique v1 (lecture seule, authentification par clé API) ────────
|
||||
// Montée ici, avant les routes internes ci-dessous : la ligne
|
||||
// `app.use('/api', requireAuth, associationsInvRouter)` plus bas capte tout
|
||||
// préfixe /api/* avec le JWT interne, ce qui intercepterait /api/v1/* et
|
||||
// /api/docs si elles étaient montées après.
|
||||
app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
|
||||
app.get('/api/openapi.json', (_, res) => res.json(swaggerSpec));
|
||||
app.use('/api/v1', requireApiKey, v1Router);
|
||||
|
||||
// All routes below require authentication
|
||||
app.use('/api/investisseurs', requireAuth, investisseursRouter);
|
||||
app.use('/api/plateformes', requireAuth, plateformesRouter);
|
||||
@@ -142,6 +155,7 @@ app.use('/api/secteurs-inv', requireAuth, secteursInvRouter);
|
||||
app.use('/api', requireAuth, associationsInvRouter);
|
||||
app.use('/api/notifications', notificationsRouter);
|
||||
app.use('/api/tickets', requireAuth, ticketsRouter);
|
||||
app.use('/api/api-keys', requireAuth, apiKeysRouter);
|
||||
|
||||
app.use(errorHandler);
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import swaggerJsdoc from 'swagger-jsdoc';
|
||||
import swaggerUi from 'swagger-ui-express';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
// swagger-jsdoc résout ses motifs `apis` avec `glob`, qui n'interprète pas
|
||||
// les antislashs Windows comme séparateurs — on force donc des slashs.
|
||||
const toGlobPath = (p) => p.split(path.sep).join('/');
|
||||
|
||||
/**
|
||||
* Documentation OpenAPI de l'API publique v1 (lecture seule).
|
||||
* Générée depuis les annotations JSDoc `@openapi` dans backend/src/routes/v1/*.js.
|
||||
* Servie sur /api/docs (public, pas d'authentification pour consulter la doc —
|
||||
* seules les requêtes réelles vers /api/v1/... nécessitent une clé API).
|
||||
*/
|
||||
const swaggerSpec = swaggerJsdoc({
|
||||
definition: {
|
||||
openapi: '3.0.3',
|
||||
info: {
|
||||
title: 'Crowdlending Tracker API',
|
||||
version: 'v1',
|
||||
description:
|
||||
"API publique en lecture seule du portefeuille de crowdlending. " +
|
||||
"Authentification par clé API (header `X-API-Key`), générée depuis Mon compte → Clés API. " +
|
||||
"Chaque clé est scopée à un seul investisseur.",
|
||||
},
|
||||
servers: [{ url: '/api/v1' }],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
ApiKeyAuth: {
|
||||
type: 'apiKey',
|
||||
in: 'header',
|
||||
name: 'X-API-Key',
|
||||
},
|
||||
},
|
||||
},
|
||||
security: [{ ApiKeyAuth: [] }],
|
||||
},
|
||||
apis: [toGlobPath(path.join(__dirname, 'routes/v1/*.js'))],
|
||||
});
|
||||
|
||||
const nbPaths = Object.keys(swaggerSpec.paths || {}).length;
|
||||
console.log(`[Swagger] ${nbPaths} route(s) documentée(s) sur /api/docs`);
|
||||
|
||||
export { swaggerSpec, swaggerUi };
|
||||
Reference in New Issue
Block a user