mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-11 17:29:37 +02:00
Merge pull request #160 from R0m1k3/claude/notes-api-with-tasks-qAROu
Claude/notes api with tasks q ar ou
This commit is contained in:
7 files changed
+958
-7
No files matched your search
@@ -97,10 +97,21 @@ async function initDatabase() {
|
|||||||
username VARCHAR(255) UNIQUE NOT NULL,
|
username VARCHAR(255) UNIQUE NOT NULL,
|
||||||
password_hash VARCHAR(255) NOT NULL,
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
is_admin BOOLEAN DEFAULT FALSE,
|
is_admin BOOLEAN DEFAULT FALSE,
|
||||||
|
api_key VARCHAR(64) UNIQUE,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
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
|
// Table notes
|
||||||
await client.query(`
|
await client.query(`
|
||||||
CREATE TABLE IF NOT EXISTS notes (
|
CREATE TABLE IF NOT EXISTS notes (
|
||||||
|
|||||||
+34
-4
@@ -1,6 +1,7 @@
|
|||||||
// Middleware d'authentification JWT
|
// Middleware d'authentification JWT et API Key
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const logger = require('../config/logger');
|
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';
|
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 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"
|
const token = authHeader && authHeader.split(' ')[1]; // Format: "Bearer TOKEN"
|
||||||
|
|
||||||
if (!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) => {
|
jwt.verify(token, JWT_SECRET, (err, user) => {
|
||||||
@@ -27,6 +56,7 @@ function authenticateToken(req, res, next) {
|
|||||||
|
|
||||||
// Ajouter les informations utilisateur à la requête
|
// Ajouter les informations utilisateur à la requête
|
||||||
req.user = user;
|
req.user = user;
|
||||||
|
req.authType = 'jwt';
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
* PUT /api/notes/:id
|
||||||
* Modifier une note
|
* Modifier une note
|
||||||
|
|||||||
+95
-3
@@ -2,32 +2,124 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const bcrypt = require('bcrypt');
|
const bcrypt = require('bcrypt');
|
||||||
|
const crypto = require('crypto');
|
||||||
const { body, validationResult } = require('express-validator');
|
const { body, validationResult } = require('express-validator');
|
||||||
|
|
||||||
const { getAll, getOne, runQuery } = require('../config/database');
|
const { getAll, getOne, runQuery } = require('../config/database');
|
||||||
const { authenticateToken, requireAdmin } = require('../middleware/auth');
|
const { authenticateToken, requireAdmin } = require('../middleware/auth');
|
||||||
const logger = require('../config/logger');
|
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
|
// Toutes les routes nécessitent authentification et droits admin
|
||||||
router.use(authenticateToken);
|
router.use(authenticateToken);
|
||||||
router.use(requireAdmin);
|
router.use(requireAdmin);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/users
|
* GET /api/users
|
||||||
* Liste tous les utilisateurs
|
* Liste tous les utilisateurs (avec indication si clé API existe)
|
||||||
*/
|
*/
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const users = await getAll(
|
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) {
|
} catch (error) {
|
||||||
logger.error('Erreur lors de la récupération des utilisateurs:', error);
|
logger.error('Erreur lors de la récupération des utilisateurs:', error);
|
||||||
res.status(500).json({ error: 'Erreur serveur' });
|
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
|
* POST /api/users
|
||||||
* Créer un nouvel utilisateur
|
* Créer un nouvel utilisateur
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ const Index = () => {
|
|||||||
const [showKeyboardHelp, setShowKeyboardHelp] = useState(false);
|
const [showKeyboardHelp, setShowKeyboardHelp] = useState(false);
|
||||||
const [showStatsDashboard, setShowStatsDashboard] = useState(false);
|
const [showStatsDashboard, setShowStatsDashboard] = useState(false);
|
||||||
const [showPomodoroModal, setShowPomodoroModal] = useState(false);
|
const [showPomodoroModal, setShowPomodoroModal] = useState(false);
|
||||||
|
const [apiInfoModal, setApiInfoModal] = useState<{ open: boolean; user?: UserType; apiKey?: string; loading?: boolean }>({ open: false });
|
||||||
|
|
||||||
// Pomodoro timer states
|
// Pomodoro timer states
|
||||||
const [pomodoroRunning, setPomodoroRunning] = useState(false);
|
const [pomodoroRunning, setPomodoroRunning] = useState(false);
|
||||||
@@ -2258,6 +2259,22 @@ const Index = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={async () => {
|
||||||
|
setApiInfoModal({ open: true, user: u, loading: true });
|
||||||
|
const result = await AdminService.getApiKey(u.id);
|
||||||
|
if (result) {
|
||||||
|
setApiInfoModal({ open: true, user: u, apiKey: result.api_key, loading: false });
|
||||||
|
} else {
|
||||||
|
setApiInfoModal({ open: true, user: u, loading: false });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Zap className="h-4 w-4 mr-2" />
|
||||||
|
API
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -2597,6 +2614,239 @@ const Index = () => {
|
|||||||
type="password"
|
type="password"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* API Info Modal */}
|
||||||
|
<Dialog open={apiInfoModal.open} onOpenChange={(open) => setApiInfoModal({ open })}>
|
||||||
|
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Zap className="h-5 w-5" />
|
||||||
|
Informations API - {apiInfoModal.user?.username}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Clé API permanente et exemples d'utilisation
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-6 mt-4">
|
||||||
|
{/* Clé API permanente */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="font-semibold text-lg flex items-center gap-2">
|
||||||
|
<Key className="h-5 w-5" />
|
||||||
|
Clé API (permanente)
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{apiInfoModal.loading ? (
|
||||||
|
<div className="flex items-center gap-2 text-muted-foreground">
|
||||||
|
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||||
|
Chargement...
|
||||||
|
</div>
|
||||||
|
) : apiInfoModal.apiKey ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="p-3 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
||||||
|
<p className="text-xs text-green-700 dark:text-green-300 mb-2 font-medium">
|
||||||
|
Clé API active - Ne la partagez jamais !
|
||||||
|
</p>
|
||||||
|
<code className="block p-2 bg-white dark:bg-gray-800 border rounded text-sm font-mono break-all select-all">
|
||||||
|
{apiInfoModal.apiKey}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(apiInfoModal.apiKey || '');
|
||||||
|
showSuccess("Clé API copiée !");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Copier
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={async () => {
|
||||||
|
if (apiInfoModal.user) {
|
||||||
|
const result = await AdminService.regenerateApiKey(apiInfoModal.user.id);
|
||||||
|
if (result) {
|
||||||
|
setApiInfoModal({ ...apiInfoModal, apiKey: result.api_key });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-4 w-4 mr-2" />
|
||||||
|
Régénérer
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={async () => {
|
||||||
|
if (apiInfoModal.user) {
|
||||||
|
const success = await AdminService.revokeApiKey(apiInfoModal.user.id);
|
||||||
|
if (success) {
|
||||||
|
setApiInfoModal({ ...apiInfoModal, apiKey: undefined });
|
||||||
|
loadUsers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Révoquer
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="p-3 bg-muted rounded-lg">
|
||||||
|
<p className="text-sm text-muted-foreground mb-2">
|
||||||
|
Aucune clé API. Cliquez pour en générer une.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={async () => {
|
||||||
|
if (apiInfoModal.user) {
|
||||||
|
setApiInfoModal({ ...apiInfoModal, loading: true });
|
||||||
|
const result = await AdminService.getApiKey(apiInfoModal.user.id);
|
||||||
|
if (result) {
|
||||||
|
setApiInfoModal({ ...apiInfoModal, apiKey: result.api_key, loading: false });
|
||||||
|
loadUsers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Key className="h-4 w-4 mr-2" />
|
||||||
|
Générer une clé API
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="bg-muted p-3 rounded-lg font-mono text-sm overflow-x-auto">
|
||||||
|
<p className="text-xs text-muted-foreground mb-2 font-sans">Utilisation avec curl :</p>
|
||||||
|
<pre>{`curl ${window.location.origin}/api/notes \\
|
||||||
|
-H "X-API-Key: ${apiInfoModal.apiKey || 'VOTRE_CLE_API'}"`}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Alternative JWT */}
|
||||||
|
<div className="space-y-2 border-t pt-4">
|
||||||
|
<h3 className="font-semibold text-sm text-muted-foreground">Alternative : Token JWT (expire après 24h)</h3>
|
||||||
|
<div className="bg-muted p-3 rounded-lg font-mono text-xs overflow-x-auto">
|
||||||
|
<pre>{`curl -X POST ${window.location.origin}/api/auth/login \\
|
||||||
|
-H "Content-Type: application/json" \\
|
||||||
|
-d '{"username": "${apiInfoModal.user?.username}", "password": "MOT_DE_PASSE"}'`}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Endpoints */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="font-semibold text-lg">2. Endpoints disponibles</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Badge className="bg-green-600">POST</Badge>
|
||||||
|
<code className="text-sm">/api/notes/full</code>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground mb-2">
|
||||||
|
Créer une note complète avec titre, contenu, tâches et tags
|
||||||
|
</p>
|
||||||
|
<div className="bg-muted p-2 rounded text-xs font-mono overflow-x-auto">
|
||||||
|
<pre>{`{
|
||||||
|
"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"]
|
||||||
|
}`}</pre>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Badge className="bg-blue-600">PUT</Badge>
|
||||||
|
<code className="text-sm">/api/notes/:id/full</code>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground mb-2">
|
||||||
|
Mettre à jour une note avec option de remplacement des tâches/tags
|
||||||
|
</p>
|
||||||
|
<div className="bg-muted p-2 rounded text-xs font-mono overflow-x-auto">
|
||||||
|
<pre>{`{
|
||||||
|
"title": "Nouveau titre",
|
||||||
|
"todos": [{ "text": "Nouvelle tâche" }],
|
||||||
|
"replace_todos": true,
|
||||||
|
"replace_tags": false
|
||||||
|
}`}</pre>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Badge className="bg-green-600">POST</Badge>
|
||||||
|
<code className="text-sm">/api/notes/:id/todos/batch</code>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground mb-2">
|
||||||
|
Ajouter plusieurs tâches en lot à une note existante
|
||||||
|
</p>
|
||||||
|
<div className="bg-muted p-2 rounded text-xs font-mono overflow-x-auto">
|
||||||
|
<pre>{`{
|
||||||
|
"todos": [
|
||||||
|
{ "text": "Tâche 1" },
|
||||||
|
{ "text": "Tâche 2", "priority": true }
|
||||||
|
]
|
||||||
|
}`}</pre>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Exemple complet */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="font-semibold text-lg">3. Exemple complet (curl)</h3>
|
||||||
|
<div className="bg-muted p-3 rounded-lg font-mono text-sm overflow-x-auto">
|
||||||
|
<pre>{`# 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"]
|
||||||
|
}'`}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info importante */}
|
||||||
|
<div className="p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
||||||
|
<p className="text-sm font-semibold text-green-900 dark:text-green-200 mb-1">
|
||||||
|
✓ Clé API permanente
|
||||||
|
</p>
|
||||||
|
<ul className="text-xs text-green-800 dark:text-green-300 space-y-1">
|
||||||
|
<li>• La clé API <strong>n'expire jamais</strong> (sauf si révoquée)</li>
|
||||||
|
<li>• Utilisez le header <code className="bg-green-100 dark:bg-green-800 px-1 rounded">X-API-Key: VOTRE_CLE</code></li>
|
||||||
|
<li>• Gardez votre clé secrète - ne la partagez jamais</li>
|
||||||
|
<li>• Les données sont isolées par utilisateur</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* URL de base */}
|
||||||
|
<div className="p-3 bg-muted rounded-lg">
|
||||||
|
<p className="text-sm font-medium mb-1">URL de base de l'API</p>
|
||||||
|
<code className="text-sm text-primary">{window.location.origin}/api</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<InputModal
|
<InputModal
|
||||||
open={addNoteTodoModal}
|
open={addNoteTodoModal}
|
||||||
onOpenChange={setAddNoteTodoModal}
|
onOpenChange={setAddNoteTodoModal}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ interface User {
|
|||||||
id: number;
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
is_admin: boolean;
|
is_admin: boolean;
|
||||||
|
has_api_key?: boolean;
|
||||||
created_at?: string;
|
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<boolean> {
|
||||||
|
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
|
// Créer une instance unique
|
||||||
|
|||||||
@@ -323,6 +323,130 @@ class NotesService {
|
|||||||
return false;
|
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<Note | null> {
|
||||||
|
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<Note | null> {
|
||||||
|
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<Todo[] | null> {
|
||||||
|
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();
|
export default new NotesService();
|
||||||
Reference in new issue
Block a user