diff --git a/config/database.js b/config/database.js index fdc4980..f5749d9 100644 --- a/config/database.js +++ b/config/database.js @@ -97,10 +97,21 @@ async function initDatabase() { username VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, is_admin BOOLEAN DEFAULT FALSE, + api_key VARCHAR(64) UNIQUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); + // Migration: ajouter la colonne api_key si elle n'existe pas + await client.query(` + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'api_key') THEN + ALTER TABLE users ADD COLUMN api_key VARCHAR(64) UNIQUE; + END IF; + END $$; + `); + // Table notes await client.query(` CREATE TABLE IF NOT EXISTS notes ( diff --git a/middleware/auth.js b/middleware/auth.js index 52f6761..59a52c9 100644 --- a/middleware/auth.js +++ b/middleware/auth.js @@ -1,6 +1,7 @@ -// Middleware d'authentification JWT +// Middleware d'authentification JWT et API Key const jwt = require('jsonwebtoken'); const logger = require('../config/logger'); +const { getOne } = require('../config/database'); const JWT_SECRET = process.env.JWT_SECRET || 'change_me_in_production_please_use_strong_secret'; @@ -9,14 +10,42 @@ if (JWT_SECRET === 'change_me_in_production_please_use_strong_secret' && process } /** - * Middleware pour vérifier le token JWT + * Middleware pour vérifier le token JWT ou la clé API + * Supporte: + * - Header "Authorization: Bearer TOKEN" (JWT, expire après 24h) + * - Header "X-API-Key: API_KEY" (clé permanente) */ -function authenticateToken(req, res, next) { +async function authenticateToken(req, res, next) { const authHeader = req.headers['authorization']; + const apiKey = req.headers['x-api-key']; + + // Option 1: Clé API permanente (prioritaire) + if (apiKey) { + try { + const user = await getOne( + 'SELECT id, username, is_admin FROM users WHERE api_key = $1', + [apiKey] + ); + + if (!user) { + logger.warn(`Tentative d'accès avec clé API invalide`); + return res.status(403).json({ error: 'Clé API invalide' }); + } + + req.user = user; + req.authType = 'api_key'; + return next(); + } catch (error) { + logger.error('Erreur lors de la vérification de la clé API:', error); + return res.status(500).json({ error: 'Erreur serveur' }); + } + } + + // Option 2: Token JWT const token = authHeader && authHeader.split(' ')[1]; // Format: "Bearer TOKEN" if (!token) { - return res.status(401).json({ error: 'Token d\'authentification manquant' }); + return res.status(401).json({ error: 'Token d\'authentification ou clé API manquant' }); } jwt.verify(token, JWT_SECRET, (err, user) => { @@ -27,6 +56,7 @@ function authenticateToken(req, res, next) { // Ajouter les informations utilisateur à la requête req.user = user; + req.authType = 'jwt'; next(); }); } diff --git a/routes/users.routes.js b/routes/users.routes.js index 4d6cc7a..08c1950 100644 --- a/routes/users.routes.js +++ b/routes/users.routes.js @@ -2,32 +2,124 @@ const express = require('express'); const router = express.Router(); const bcrypt = require('bcrypt'); +const crypto = require('crypto'); const { body, validationResult } = require('express-validator'); const { getAll, getOne, runQuery } = require('../config/database'); const { authenticateToken, requireAdmin } = require('../middleware/auth'); const logger = require('../config/logger'); +/** + * Génère une clé API sécurisée + * Format: nf_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (48 caractères hex) + */ +function generateApiKey() { + return 'nf_' + crypto.randomBytes(24).toString('hex'); +} + // Toutes les routes nécessitent authentification et droits admin router.use(authenticateToken); router.use(requireAdmin); /** * GET /api/users - * Liste tous les utilisateurs + * Liste tous les utilisateurs (avec indication si clé API existe) */ router.get('/', async (req, res) => { try { const users = await getAll( - 'SELECT id, username, is_admin, created_at FROM users ORDER BY created_at DESC' + 'SELECT id, username, is_admin, api_key, created_at FROM users ORDER BY created_at DESC' ); - res.json(users); + // Ne pas exposer la clé complète, juste indiquer si elle existe + const usersWithApiKeyStatus = users.map(u => ({ + ...u, + has_api_key: !!u.api_key, + api_key: undefined // Ne pas envoyer la clé dans la liste + })); + res.json(usersWithApiKeyStatus); } catch (error) { logger.error('Erreur lors de la récupération des utilisateurs:', error); res.status(500).json({ error: 'Erreur serveur' }); } }); +/** + * GET /api/users/:id/api-key + * Récupérer la clé API d'un utilisateur (génère si n'existe pas) + */ +router.get('/:id/api-key', async (req, res) => { + try { + const userId = req.params.id; + + const user = await getOne('SELECT id, username, api_key FROM users WHERE id = $1', [userId]); + if (!user) { + return res.status(404).json({ error: 'Utilisateur non trouvé' }); + } + + // Si pas de clé API, en générer une + if (!user.api_key) { + const newApiKey = generateApiKey(); + await runQuery('UPDATE users SET api_key = $1 WHERE id = $2', [newApiKey, userId]); + logger.info(`[API KEY] Clé API générée pour ${user.username} (ID: ${userId})`); + return res.json({ api_key: newApiKey, generated: true }); + } + + res.json({ api_key: user.api_key, generated: false }); + } catch (error) { + logger.error('Erreur lors de la récupération de la clé API:', error); + res.status(500).json({ error: 'Erreur serveur' }); + } +}); + +/** + * POST /api/users/:id/api-key/regenerate + * Régénérer la clé API d'un utilisateur + */ +router.post('/:id/api-key/regenerate', async (req, res) => { + try { + const userId = req.params.id; + + const user = await getOne('SELECT id, username FROM users WHERE id = $1', [userId]); + if (!user) { + return res.status(404).json({ error: 'Utilisateur non trouvé' }); + } + + const newApiKey = generateApiKey(); + await runQuery('UPDATE users SET api_key = $1 WHERE id = $2', [newApiKey, userId]); + + logger.info(`[API KEY] Clé API régénérée pour ${user.username} (ID: ${userId})`); + + res.json({ api_key: newApiKey, message: 'Clé API régénérée avec succès' }); + } catch (error) { + logger.error('Erreur lors de la régénération de la clé API:', error); + res.status(500).json({ error: 'Erreur serveur' }); + } +}); + +/** + * DELETE /api/users/:id/api-key + * Révoquer la clé API d'un utilisateur + */ +router.delete('/:id/api-key', async (req, res) => { + try { + const userId = req.params.id; + + const user = await getOne('SELECT id, username FROM users WHERE id = $1', [userId]); + if (!user) { + return res.status(404).json({ error: 'Utilisateur non trouvé' }); + } + + await runQuery('UPDATE users SET api_key = NULL WHERE id = $1', [userId]); + + logger.info(`[API KEY] Clé API révoquée pour ${user.username} (ID: ${userId})`); + + res.json({ message: 'Clé API révoquée avec succès' }); + } catch (error) { + logger.error('Erreur lors de la révocation de la clé API:', error); + res.status(500).json({ error: 'Erreur serveur' }); + } +}); + /** * POST /api/users * Créer un nouvel utilisateur diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 601a139..4152632 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -194,7 +194,7 @@ const Index = () => { const [showKeyboardHelp, setShowKeyboardHelp] = useState(false); const [showStatsDashboard, setShowStatsDashboard] = useState(false); const [showPomodoroModal, setShowPomodoroModal] = useState(false); - const [apiInfoModal, setApiInfoModal] = useState<{ open: boolean; user?: UserType }>({ open: false }); + const [apiInfoModal, setApiInfoModal] = useState<{ open: boolean; user?: UserType; apiKey?: string; loading?: boolean }>({ open: false }); // Pomodoro timer states const [pomodoroRunning, setPomodoroRunning] = useState(false); @@ -2262,7 +2262,15 @@ const Index = () => { + + + + + ) : ( +
+

+ Aucune clé API. Cliquez pour en générer une. +

+ +
+ )} +
+

Utilisation avec curl :

+
{`curl ${window.location.origin}/api/notes \\
+  -H "X-API-Key: ${apiInfoModal.apiKey || 'VOTRE_CLE_API'}"`}
+
+ + + {/* Alternative JWT */} +
+

Alternative : Token JWT (expire après 24h)

+
{`curl -X POST ${window.location.origin}/api/auth/login \\
   -H "Content-Type: application/json" \\
-  -d '{"username": "${apiInfoModal.user?.username}", "password": "VOTRE_MOT_DE_PASSE"}'`}
+ -d '{"username": "${apiInfoModal.user?.username}", "password": "MOT_DE_PASSE"}'`}
-

- Réponse : {`{"token": "eyJhbG...", "user": {...}}`} -

{/* Endpoints */} @@ -2709,10 +2808,10 @@ const Index = () => {

3. Exemple complet (curl)

-
{`# Créer une note avec tâches
+                
{`# Créer une note avec tâches (clé API permanente)
 curl -X POST ${window.location.origin}/api/notes/full \\
   -H "Content-Type: application/json" \\
-  -H "Authorization: Bearer VOTRE_TOKEN" \\
+  -H "X-API-Key: ${apiInfoModal.apiKey || 'VOTRE_CLE_API'}" \\
   -d '{
     "title": "Liste de courses",
     "content": "Pour le weekend",
@@ -2727,13 +2826,14 @@ curl -X POST ${window.location.origin}/api/notes/full \\
             
{/* Info importante */} -
-

- ⚠️ Important +

+

+ ✓ Clé API permanente

-
    -
  • • Le token expire après 24 heures
  • -
  • • Utilisez le header Authorization: Bearer TOKEN
  • +
      +
    • • La clé API n'expire jamais (sauf si révoquée)
    • +
    • • Utilisez le header X-API-Key: VOTRE_CLE
    • +
    • • Gardez votre clé secrète - ne la partagez jamais
    • • Les données sont isolées par utilisateur
diff --git a/src/services/AdminService.ts b/src/services/AdminService.ts index 9e18e66..0d3e1a6 100644 --- a/src/services/AdminService.ts +++ b/src/services/AdminService.ts @@ -5,6 +5,7 @@ interface User { id: number; username: string; is_admin: boolean; + has_api_key?: boolean; created_at?: string; } @@ -134,6 +135,70 @@ class AdminService { }; } } + + /** + * Récupérer la clé API d'un utilisateur (génère si n'existe pas) + */ + async getApiKey(userId: number): Promise<{ api_key: string; generated: boolean } | null> { + try { + const response = await fetch(`/api/users/${userId}/api-key`, { + headers: AuthService.getHeaders() + }); + + if (!response.ok) { + throw new Error("Erreur lors de la récupération de la clé API"); + } + + return await response.json(); + } catch (error) { + showError(error instanceof Error ? error.message : "Erreur serveur"); + return null; + } + } + + /** + * Régénérer la clé API d'un utilisateur + */ + async regenerateApiKey(userId: number): Promise<{ api_key: string } | null> { + try { + const response = await fetch(`/api/users/${userId}/api-key/regenerate`, { + method: "POST", + headers: AuthService.getHeaders() + }); + + if (!response.ok) { + throw new Error("Erreur lors de la régénération de la clé API"); + } + + showSuccess("Clé API régénérée avec succès"); + return await response.json(); + } catch (error) { + showError(error instanceof Error ? error.message : "Erreur serveur"); + return null; + } + } + + /** + * Révoquer la clé API d'un utilisateur + */ + async revokeApiKey(userId: number): Promise { + try { + const response = await fetch(`/api/users/${userId}/api-key`, { + method: "DELETE", + headers: AuthService.getHeaders() + }); + + if (!response.ok) { + throw new Error("Erreur lors de la révocation de la clé API"); + } + + showSuccess("Clé API révoquée avec succès"); + return true; + } catch (error) { + showError(error instanceof Error ? error.message : "Erreur serveur"); + return false; + } + } } // Créer une instance unique