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/notes.routes.js b/routes/notes.routes.js index e7a40d0..f24a084 100644 --- a/routes/notes.routes.js +++ b/routes/notes.routes.js @@ -220,6 +220,385 @@ router.post('/', } ); +/** + * POST /api/notes/full + * Créer une note complète avec tâches et tags en une seule requête + */ +router.post('/full', + [ + body('title').trim().notEmpty().withMessage('Le titre est requis'), + body('content').optional(), + body('todos').optional().isArray(), + body('tags').optional().isArray() + ], + async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ error: 'Données invalides', details: errors.array() }); + } + + const { title, content, todos = [], tags = [] } = req.body; + + logger.info(`[CREATE NOTE FULL] Début création - user_id: ${req.user.id}, title: "${title}", todos: ${todos.length}, tags: ${tags.length}`); + + // 1. Créer la note + const noteResult = await runQuery(` + INSERT INTO notes (user_id, title, content) + VALUES ($1, $2, $3) + RETURNING * + `, [req.user.id, title, content || '']); + + const noteId = noteResult.id; + const createdTodos = []; + const createdTags = []; + + // 2. Créer les todos (avec support des subtasks) + const createTodosRecursive = async (todoList, parentId = null, level = 0) => { + let position = 0; + for (const todo of todoList) { + position++; + const todoResult = await runQuery(` + INSERT INTO note_todos (note_id, text, completed, priority, position, parent_id, level) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING * + `, [ + noteId, + todo.text, + todo.completed || false, + todo.priority || false, + position, + parentId, + level + ]); + + const createdTodo = { + id: todoResult.id, + text: todoResult.text, + completed: todoResult.completed, + priority: todoResult.priority, + position: todoResult.position, + parent_id: todoResult.parent_id, + level: todoResult.level + }; + + // Traiter les subtasks si présents + if (todo.subtasks && Array.isArray(todo.subtasks) && todo.subtasks.length > 0) { + createdTodo.subtasks = []; + const subtasks = await createTodosRecursive(todo.subtasks, todoResult.id, level + 1); + createdTodo.subtasks = subtasks; + } + + createdTodos.push(createdTodo); + } + return createdTodos.filter(t => t.parent_id === parentId); + }; + + await createTodosRecursive(todos); + + // 3. Créer les tags + for (const tag of tags) { + const tagName = typeof tag === 'string' ? tag.trim().toLowerCase() : tag.name?.trim().toLowerCase(); + if (tagName) { + try { + const tagResult = await runQuery(` + INSERT INTO note_tags (note_id, tag) + VALUES ($1, $2) + RETURNING * + `, [noteId, tagName]); + createdTags.push({ id: tagResult.id, name: tagResult.tag }); + } catch (err) { + // Ignorer les doublons + if (!err.message?.includes('UNIQUE')) { + throw err; + } + } + } + } + + logger.info(`[CREATE NOTE FULL] Note créée avec succès - ID: ${noteId}, todos: ${createdTodos.length}, tags: ${createdTags.length}`); + + res.status(201).json({ + id: noteId, + title, + content: content || '', + image_filename: null, + archived: false, + priority: 0, + todos: createdTodos, + tags: createdTags, + images: [], + files: [], + created_at: noteResult.created_at, + updated_at: noteResult.updated_at + }); + } catch (error) { + logger.error('Erreur lors de la création de la note complète:', error); + res.status(500).json({ error: 'Erreur serveur' }); + } + } +); + +/** + * PUT /api/notes/:id/full + * Mise à jour complète d'une note avec ses tâches et tags + */ +router.put('/:id/full', + [ + body('title').optional().trim().notEmpty(), + body('content').optional(), + body('todos').optional().isArray(), + body('tags').optional().isArray(), + body('replace_todos').optional().isBoolean(), + body('replace_tags').optional().isBoolean() + ], + async (req, res) => { + try { + const { title, content, todos, tags, replace_todos = false, replace_tags = false } = req.body; + const noteId = req.params.id; + + // Vérifier que la note appartient à l'utilisateur + const note = await getOne('SELECT id FROM notes WHERE id = $1 AND user_id = $2', [noteId, req.user.id]); + if (!note) { + return res.status(404).json({ error: 'Note non trouvée' }); + } + + logger.info(`[UPDATE NOTE FULL] Mise à jour - note_id: ${noteId}, replace_todos: ${replace_todos}, replace_tags: ${replace_tags}`); + + // 1. Mettre à jour la note + const updates = []; + const params = []; + let paramCount = 0; + + if (title !== undefined) { + paramCount++; + updates.push(`title = $${paramCount}`); + params.push(title); + } + if (content !== undefined) { + paramCount++; + updates.push(`content = $${paramCount}`); + params.push(content); + } + + if (updates.length > 0) { + updates.push('updated_at = CURRENT_TIMESTAMP'); + paramCount++; + params.push(noteId); + await runQuery(`UPDATE notes SET ${updates.join(', ')} WHERE id = $${paramCount}`, params); + } + + const createdTodos = []; + const createdTags = []; + + // 2. Gérer les todos + if (todos !== undefined) { + if (replace_todos) { + // Supprimer tous les todos existants + await runQuery('DELETE FROM note_todos WHERE note_id = $1', [noteId]); + } + + // Créer les nouveaux todos + const createTodosRecursive = async (todoList, parentId = null, level = 0) => { + let position = 0; + for (const todo of todoList) { + position++; + const todoResult = await runQuery(` + INSERT INTO note_todos (note_id, text, completed, priority, position, parent_id, level) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING * + `, [ + noteId, + todo.text, + todo.completed || false, + todo.priority || false, + position, + parentId, + level + ]); + + const createdTodo = { + id: todoResult.id, + text: todoResult.text, + completed: todoResult.completed, + priority: todoResult.priority, + position: todoResult.position, + parent_id: todoResult.parent_id, + level: todoResult.level + }; + + if (todo.subtasks && Array.isArray(todo.subtasks) && todo.subtasks.length > 0) { + createdTodo.subtasks = []; + await createTodosRecursive(todo.subtasks, todoResult.id, level + 1); + } + + createdTodos.push(createdTodo); + } + }; + + await createTodosRecursive(todos); + } + + // 3. Gérer les tags + if (tags !== undefined) { + if (replace_tags) { + // Supprimer tous les tags existants + await runQuery('DELETE FROM note_tags WHERE note_id = $1', [noteId]); + } + + // Créer les nouveaux tags + for (const tag of tags) { + const tagName = typeof tag === 'string' ? tag.trim().toLowerCase() : tag.name?.trim().toLowerCase(); + if (tagName) { + try { + const tagResult = await runQuery(` + INSERT INTO note_tags (note_id, tag) + VALUES ($1, $2) + RETURNING * + `, [noteId, tagName]); + createdTags.push({ id: tagResult.id, name: tagResult.tag }); + } catch (err) { + if (!err.message?.includes('UNIQUE')) { + throw err; + } + } + } + } + } + + // Mettre à jour updated_at + await runQuery('UPDATE notes SET updated_at = CURRENT_TIMESTAMP WHERE id = $1', [noteId]); + + // Récupérer la note mise à jour + const updatedNote = await getOne(` + SELECT id, title, content, image_filename, archived, priority, created_at, updated_at + FROM notes WHERE id = $1 + `, [noteId]); + + // Récupérer tous les todos de la note + const allTodos = await getAll(` + SELECT id, text, completed, priority, position, parent_id, level + FROM note_todos WHERE note_id = $1 + ORDER BY position ASC, id ASC + `, [noteId]); + + // Récupérer tous les tags de la note + const allTags = await getAll(` + SELECT id, tag as name FROM note_tags WHERE note_id = $1 + ORDER BY tag ASC + `, [noteId]); + + logger.info(`[UPDATE NOTE FULL] Mise à jour réussie - note_id: ${noteId}`); + + res.json({ + message: 'Note mise à jour avec succès', + note: { + ...updatedNote, + todos: allTodos || [], + tags: allTags || [] + } + }); + } catch (error) { + logger.error('Erreur lors de la mise à jour complète de la note:', error); + res.status(500).json({ error: 'Erreur serveur' }); + } + } +); + +/** + * POST /api/notes/:id/todos/batch + * Ajouter plusieurs tâches à une note en une seule requête + */ +router.post('/:id/todos/batch', + [ + body('todos').isArray({ min: 1 }).withMessage('Au moins une tâche est requise'), + body('todos.*.text').trim().notEmpty().withMessage('Le texte de chaque tâche est requis') + ], + async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ error: 'Données invalides', details: errors.array() }); + } + + const noteId = req.params.id; + const { todos } = req.body; + + // Vérifier que la note appartient à l'utilisateur + const note = await getOne('SELECT id FROM notes WHERE id = $1 AND user_id = $2', [noteId, req.user.id]); + if (!note) { + return res.status(404).json({ error: 'Note non trouvée' }); + } + + logger.info(`[CREATE TODOS BATCH] Ajout de ${todos.length} tâches à la note ${noteId}`); + + const createdTodos = []; + + // Créer les todos récursivement + const createTodosRecursive = async (todoList, parentId = null, level = 0) => { + // Récupérer la position max actuelle + const maxPosResult = await getOne(` + SELECT COALESCE(MAX(position), 0) as max_pos + FROM note_todos + WHERE note_id = $1 AND COALESCE(parent_id, 0) = COALESCE($2, 0) + `, [noteId, parentId]); + let position = maxPosResult?.max_pos || 0; + + for (const todo of todoList) { + position++; + const todoResult = await runQuery(` + INSERT INTO note_todos (note_id, text, completed, priority, position, parent_id, level) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING * + `, [ + noteId, + todo.text, + todo.completed || false, + todo.priority || false, + position, + parentId, + level + ]); + + const createdTodo = { + id: todoResult.id, + text: todoResult.text, + completed: todoResult.completed, + priority: todoResult.priority, + position: todoResult.position, + parent_id: todoResult.parent_id, + level: todoResult.level + }; + + // Traiter les subtasks si présents + if (todo.subtasks && Array.isArray(todo.subtasks) && todo.subtasks.length > 0) { + createdTodo.subtasks = await createTodosRecursive(todo.subtasks, todoResult.id, level + 1); + } + + createdTodos.push(createdTodo); + } + + return createdTodos.filter(t => t.parent_id === parentId); + }; + + await createTodosRecursive(todos); + + // Mettre à jour updated_at de la note + await runQuery('UPDATE notes SET updated_at = CURRENT_TIMESTAMP WHERE id = $1', [noteId]); + + logger.info(`[CREATE TODOS BATCH] ${createdTodos.length} tâches créées pour la note ${noteId}`); + + res.status(201).json({ + message: `${createdTodos.length} tâche(s) ajoutée(s) avec succès`, + todos: createdTodos + }); + } catch (error) { + logger.error('Erreur lors de l\'ajout des tâches en lot:', error); + res.status(500).json({ error: 'Erreur serveur' }); + } + } +); + /** * PUT /api/notes/:id * Modifier une note 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 a416f7f..4152632 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -194,6 +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; apiKey?: string; loading?: boolean }>({ open: false }); // Pomodoro timer states const [pomodoroRunning, setPomodoroRunning] = useState(false); @@ -2258,6 +2259,22 @@ 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": "MOT_DE_PASSE"}'`}
+
+
+ + {/* Endpoints */} +
+

2. Endpoints disponibles

+
+ + +
+ POST + /api/notes/full +
+

+ Créer une note complète avec titre, contenu, tâches et tags +

+
+
{`{
+  "title": "Ma note",
+  "content": "Contenu de la note",
+  "todos": [
+    { "text": "Tâche 1", "priority": true },
+    { "text": "Tâche 2", "subtasks": [{ "text": "Sous-tâche" }] }
+  ],
+  "tags": ["important", "travail"]
+}`}
+
+
+
+ + + +
+ PUT + /api/notes/:id/full +
+

+ Mettre à jour une note avec option de remplacement des tâches/tags +

+
+
{`{
+  "title": "Nouveau titre",
+  "todos": [{ "text": "Nouvelle tâche" }],
+  "replace_todos": true,
+  "replace_tags": false
+}`}
+
+
+
+ + + +
+ POST + /api/notes/:id/todos/batch +
+

+ Ajouter plusieurs tâches en lot à une note existante +

+
+
{`{
+  "todos": [
+    { "text": "Tâche 1" },
+    { "text": "Tâche 2", "priority": true }
+  ]
+}`}
+
+
+
+
+
+ + {/* Exemple complet */} +
+

3. Exemple complet (curl)

+
+
{`# 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 "X-API-Key: ${apiInfoModal.apiKey || 'VOTRE_CLE_API'}" \\
+  -d '{
+    "title": "Liste de courses",
+    "content": "Pour le weekend",
+    "todos": [
+      { "text": "Pain" },
+      { "text": "Lait" },
+      { "text": "Fruits" }
+    ],
+    "tags": ["courses"]
+  }'`}
+
+
+ + {/* Info importante */} +
+

+ ✓ Clé API permanente +

+ +
+ + {/* URL de base */} +
+

URL de base de l'API

+ {window.location.origin}/api +
+ + + + { + 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 diff --git a/src/services/NotesService.ts b/src/services/NotesService.ts index dab034a..df8830e 100644 --- a/src/services/NotesService.ts +++ b/src/services/NotesService.ts @@ -323,6 +323,130 @@ class NotesService { return false; } } + + /** + * Créer une note complète avec titre, contenu, tâches et tags en une seule requête + */ + async createNoteFull(data: { + title: string; + content?: string; + todos?: Array<{ + text: string; + completed?: boolean; + priority?: boolean; + subtasks?: Array<{ text: string; completed?: boolean; priority?: boolean }>; + }>; + tags?: string[]; + }): Promise { + try { + const response = await fetch("/api/notes/full", { + method: "POST", + headers: AuthService.getHeaders(), + body: JSON.stringify(data) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Erreur lors de la création de la note"); + } + + showSuccess("Note créée avec succès"); + const note = await response.json(); + return { + ...note, + todos: note.todos || [], + images: note.images || [], + tags: note.tags || [] + }; + } catch (error) { + showError(error instanceof Error ? error.message : "Erreur serveur"); + return null; + } + } + + /** + * Mettre à jour une note complète avec titre, contenu, tâches et tags + * @param replace_todos - Si true, remplace toutes les tâches existantes + * @param replace_tags - Si true, remplace tous les tags existants + */ + async updateNoteFull( + noteId: number, + data: { + title?: string; + content?: string; + todos?: Array<{ + text: string; + completed?: boolean; + priority?: boolean; + subtasks?: Array<{ text: string; completed?: boolean; priority?: boolean }>; + }>; + tags?: string[]; + replace_todos?: boolean; + replace_tags?: boolean; + } + ): Promise { + try { + const response = await fetch(`/api/notes/${noteId}/full`, { + method: "PUT", + headers: AuthService.getHeaders(), + body: JSON.stringify(data) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Erreur lors de la mise à jour de la note"); + } + + const result = await response.json(); + showSuccess("Note mise à jour avec succès"); + + if (result.note) { + return { + ...result.note, + todos: result.note.todos || [], + images: result.note.images || [], + tags: result.note.tags || [] + }; + } + return null; + } catch (error) { + showError(error instanceof Error ? error.message : "Erreur serveur"); + return null; + } + } + + /** + * Ajouter plusieurs tâches à une note en une seule requête + */ + async addTodosBatch( + noteId: number, + todos: Array<{ + text: string; + completed?: boolean; + priority?: boolean; + subtasks?: Array<{ text: string; completed?: boolean; priority?: boolean }>; + }> + ): Promise { + try { + const response = await fetch(`/api/notes/${noteId}/todos/batch`, { + method: "POST", + headers: AuthService.getHeaders(), + body: JSON.stringify({ todos }) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Erreur lors de l'ajout des tâches"); + } + + const result = await response.json(); + showSuccess(`${result.todos?.length || 0} tâche(s) ajoutée(s)`); + return result.todos || []; + } catch (error) { + showError(error instanceof Error ? error.message : "Erreur serveur"); + return null; + } + } } export default new NotesService(); \ No newline at end of file