mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-11 17:29:37 +02:00
1011 lines
33 KiB
JavaScript
1011 lines
33 KiB
JavaScript
// Routes de gestion des notes
|
|
const express = require('express');
|
|
const router = express.Router();
|
|
const multer = require('multer');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const { body, validationResult } = require('express-validator');
|
|
|
|
const { getAll, getOne, runQuery } = require('../config/database');
|
|
const { authenticateToken } = require('../middleware/auth');
|
|
const logger = require('../config/logger');
|
|
|
|
// Configuration de multer pour l'upload d'images
|
|
const storage = multer.diskStorage({
|
|
destination: (req, file, cb) => {
|
|
const uploadsDir = path.join(__dirname, '../public/uploads');
|
|
cb(null, uploadsDir);
|
|
},
|
|
filename: (req, file, cb) => {
|
|
const uniqueName = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}${path.extname(file.originalname)}`;
|
|
cb(null, uniqueName);
|
|
}
|
|
});
|
|
|
|
const upload = multer({
|
|
storage,
|
|
limits: { fileSize: parseInt(process.env.MAX_FILE_SIZE) || 5 * 1024 * 1024 }, // 5MB
|
|
fileFilter: (req, file, cb) => {
|
|
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
|
|
if (allowedTypes.includes(file.mimetype)) {
|
|
cb(null, true);
|
|
} else {
|
|
cb(new Error('Type de fichier non autorisé. Utilisez JPEG, PNG, WebP ou GIF.'));
|
|
}
|
|
}
|
|
});
|
|
|
|
// Toutes les routes nécessitent authentification
|
|
router.use(authenticateToken);
|
|
|
|
/**
|
|
* GET /api/notes/counts
|
|
* Récupérer le nombre de notes actives et archivées
|
|
*/
|
|
router.get('/counts', async (req, res) => {
|
|
try {
|
|
const activeCount = await getAll(`
|
|
SELECT COUNT(*) as count FROM notes WHERE user_id = $1 AND archived = false
|
|
`, [req.user.id]);
|
|
|
|
const archivedCount = await getAll(`
|
|
SELECT COUNT(*) as count FROM notes WHERE user_id = $1 AND archived = true
|
|
`, [req.user.id]);
|
|
|
|
res.json({
|
|
active: parseInt(activeCount[0]?.count || 0),
|
|
archived: parseInt(archivedCount[0]?.count || 0)
|
|
});
|
|
} catch (error) {
|
|
logger.error('Erreur lors de la récupération des compteurs:', error);
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/notes?archived=true/false
|
|
* Liste toutes les notes de l'utilisateur
|
|
*/
|
|
router.get('/', async (req, res) => {
|
|
try {
|
|
const showArchived = req.query.archived === 'true';
|
|
const archivedFilter = showArchived; // BOOLEAN instead of 0/1
|
|
|
|
const notes = await getAll(`
|
|
SELECT
|
|
n.id, n.title, n.content, n.image_filename, n.archived, n.priority,
|
|
n.created_at, n.updated_at,
|
|
(SELECT COUNT(*) FROM note_todos WHERE note_id = n.id) as todos_count,
|
|
(SELECT COUNT(*) FROM note_todos WHERE note_id = n.id AND completed = TRUE) as todos_completed
|
|
FROM notes n
|
|
WHERE n.user_id = $1 AND n.archived = $2
|
|
ORDER BY n.priority DESC, n.updated_at DESC
|
|
`, [req.user.id, archivedFilter]);
|
|
|
|
// Charger les todos, images, fichiers et tags pour chaque note
|
|
for (const note of notes) {
|
|
const todos = await getAll(`
|
|
SELECT id, text, completed, position, parent_id, level
|
|
FROM note_todos
|
|
WHERE note_id = $1
|
|
ORDER BY position ASC, id ASC
|
|
`, [note.id]);
|
|
note.todos = todos || [];
|
|
|
|
const images = await getAll(`
|
|
SELECT id, filename, original_name, created_at
|
|
FROM note_images
|
|
WHERE note_id = $1
|
|
ORDER BY created_at DESC
|
|
`, [note.id]);
|
|
note.images = images || [];
|
|
|
|
const files = await getAll(`
|
|
SELECT id, filename, original_name, file_size, mime_type, created_at
|
|
FROM note_files
|
|
WHERE note_id = $1
|
|
ORDER BY created_at DESC
|
|
`, [note.id]);
|
|
note.files = files || [];
|
|
note.files_count = files?.length || 0;
|
|
|
|
const tags = await getAll(`
|
|
SELECT id, tag as name
|
|
FROM note_tags
|
|
WHERE note_id = $1
|
|
ORDER BY tag ASC
|
|
`, [note.id]);
|
|
note.tags = tags || [];
|
|
}
|
|
|
|
res.json(notes || []);
|
|
} catch (error) {
|
|
logger.error('Erreur lors de la récupération des notes:', error);
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/notes/:id
|
|
* Récupérer une note avec ses todos
|
|
*/
|
|
router.get('/:id', async (req, res) => {
|
|
try {
|
|
const note = await getOne(`
|
|
SELECT id, title, content, image_filename, created_at, updated_at
|
|
FROM notes
|
|
WHERE id = $1 AND user_id = $2
|
|
`, [req.params.id, req.user.id]);
|
|
|
|
if (!note) {
|
|
return res.status(404).json({ error: 'Note non trouvée' });
|
|
}
|
|
|
|
// Récupérer les todos de la note
|
|
const todos = await getAll(`
|
|
SELECT id, text, completed, position
|
|
FROM note_todos
|
|
WHERE note_id = $1
|
|
ORDER BY position, id
|
|
`, [req.params.id]);
|
|
|
|
note.todos = todos;
|
|
|
|
// Récupérer les images de la note
|
|
const images = await getAll(`
|
|
SELECT id, filename, original_name, created_at
|
|
FROM note_images
|
|
WHERE note_id = $1
|
|
ORDER BY created_at DESC
|
|
`, [req.params.id]);
|
|
|
|
note.images = images || [];
|
|
|
|
// Récupérer les fichiers de la note
|
|
const files = await getAll(`
|
|
SELECT id, filename, original_name, file_size, mime_type, created_at
|
|
FROM note_files
|
|
WHERE note_id = $1
|
|
ORDER BY created_at DESC
|
|
`, [req.params.id]);
|
|
|
|
note.files = files || [];
|
|
|
|
res.json(note);
|
|
} catch (error) {
|
|
logger.error('Erreur lors de la récupération de la note:', error);
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/notes
|
|
* Créer une nouvelle note
|
|
*/
|
|
router.post('/',
|
|
[
|
|
body('title').trim().notEmpty().withMessage('Le titre est requis'),
|
|
body('content').optional()
|
|
],
|
|
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 } = req.body;
|
|
|
|
logger.info(`[CREATE NOTE] Début création - user_id: ${req.user.id}, title: "${title}"`);
|
|
|
|
const result = await runQuery(`
|
|
INSERT INTO notes (user_id, title, content)
|
|
VALUES ($1, $2, $3)
|
|
RETURNING *
|
|
`, [req.user.id, title, content || '']);
|
|
|
|
logger.info(`[CREATE NOTE] Note créée avec succès - ID: ${result.id}, title: "${title}", user: ${req.user.username}`);
|
|
|
|
res.status(201).json({
|
|
id: result.id,
|
|
title,
|
|
content: content || '',
|
|
image_filename: null,
|
|
todos: []
|
|
});
|
|
} catch (error) {
|
|
logger.error('Erreur lors de la création de la note:', error);
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
}
|
|
);
|
|
|
|
/**
|
|
* PUT /api/notes/:id
|
|
* Modifier une note
|
|
*/
|
|
router.put('/:id',
|
|
[
|
|
body('title').optional().trim().notEmpty(),
|
|
body('content').optional()
|
|
],
|
|
async (req, res) => {
|
|
try {
|
|
const { title, content } = 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', [req.params.id, req.user.id]);
|
|
if (!note) {
|
|
return res.status(404).json({ error: 'Note non trouvée' });
|
|
}
|
|
|
|
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(req.params.id);
|
|
|
|
await runQuery(`UPDATE notes SET ${updates.join(', ')} WHERE id = $${paramCount}`, params);
|
|
}
|
|
|
|
// Récupérer la note mise à jour avec sa nouvelle updated_at
|
|
const updatedNote = await getOne(`
|
|
SELECT id, title, content, image_filename, archived, priority, created_at, updated_at
|
|
FROM notes
|
|
WHERE id = $1
|
|
`, [req.params.id]);
|
|
|
|
logger.info(`[UPDATE NOTE] Note mise à jour - ID: ${req.params.id}, updated_at: ${updatedNote.updated_at}`);
|
|
|
|
res.json({
|
|
message: 'Note modifiée avec succès',
|
|
note: updatedNote
|
|
});
|
|
} catch (error) {
|
|
logger.error('Erreur lors de la modification de la note:', error);
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
}
|
|
);
|
|
|
|
/**
|
|
* PUT /api/notes/:id/archive
|
|
* Archiver ou désarchiver une note
|
|
*/
|
|
router.put('/:id/archive', async (req, res) => {
|
|
try {
|
|
const { archived } = 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', [req.params.id, req.user.id]);
|
|
if (!note) {
|
|
return res.status(404).json({ error: 'Note non trouvée' });
|
|
}
|
|
|
|
await runQuery(
|
|
'UPDATE notes SET archived = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2',
|
|
[archived, req.params.id] // BOOLEAN instead of 0/1
|
|
);
|
|
|
|
logger.info(`Note ${archived ? 'archivée' : 'désarchivée'}: ${req.params.id}`);
|
|
res.json({ message: `Note ${archived ? 'archivée' : 'désarchivée'} avec succès`, archived });
|
|
} catch (error) {
|
|
logger.error('Erreur lors de l\'archivage de la note:', error);
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* DELETE /api/notes/:id
|
|
* Supprimer une note
|
|
*/
|
|
router.delete('/:id', async (req, res) => {
|
|
try {
|
|
// Vérifier que la note appartient à l'utilisateur
|
|
const note = await getOne('SELECT id, image_filename FROM notes WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id]);
|
|
if (!note) {
|
|
return res.status(404).json({ error: 'Note non trouvée' });
|
|
}
|
|
|
|
// Supprimer l'image si elle existe
|
|
if (note.image_filename) {
|
|
const imagePath = path.join(__dirname, '../public/uploads', note.image_filename);
|
|
if (fs.existsSync(imagePath)) {
|
|
fs.unlinkSync(imagePath);
|
|
}
|
|
}
|
|
|
|
await runQuery('DELETE FROM notes WHERE id = $1', [req.params.id]);
|
|
|
|
logger.info(`Note supprimée (ID: ${req.params.id}) par ${req.user.username}`);
|
|
|
|
res.json({ message: 'Note supprimée avec succès' });
|
|
} catch (error) {
|
|
logger.error('Erreur lors de la suppression de la note:', error);
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/notes/:id/image
|
|
* Ajouter une image à une note
|
|
*/
|
|
router.post('/:id/image', upload.single('image'), async (req, res) => {
|
|
try {
|
|
if (!req.file) {
|
|
return res.status(400).json({ error: 'Aucune image fournie' });
|
|
}
|
|
|
|
// Vérifier que la note appartient à l'utilisateur
|
|
const note = await getOne('SELECT id, image_filename FROM notes WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id]);
|
|
if (!note) {
|
|
// Supprimer le fichier uploadé
|
|
fs.unlinkSync(req.file.path);
|
|
return res.status(404).json({ error: 'Note non trouvée' });
|
|
}
|
|
|
|
// Supprimer l'ancienne image si elle existe
|
|
if (note.image_filename) {
|
|
const oldImagePath = path.join(__dirname, '../public/uploads', note.image_filename);
|
|
if (fs.existsSync(oldImagePath)) {
|
|
fs.unlinkSync(oldImagePath);
|
|
}
|
|
}
|
|
|
|
// Mettre à jour la note avec le nouveau fichier
|
|
await runQuery('UPDATE notes SET image_filename = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2', [req.file.filename, req.params.id]);
|
|
|
|
res.json({
|
|
message: 'Image ajoutée avec succès',
|
|
filename: req.file.filename,
|
|
url: `/uploads/${req.file.filename}`
|
|
});
|
|
} catch (error) {
|
|
logger.error('Erreur lors de l\'ajout de l\'image:', error);
|
|
if (req.file) {
|
|
fs.unlinkSync(req.file.path);
|
|
}
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* DELETE /api/notes/:id/image
|
|
* Supprimer l'image d'une note
|
|
*/
|
|
router.delete('/:id/image', async (req, res) => {
|
|
try {
|
|
const note = await getOne('SELECT id, image_filename FROM notes WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id]);
|
|
if (!note) {
|
|
return res.status(404).json({ error: 'Note non trouvée' });
|
|
}
|
|
|
|
if (!note.image_filename) {
|
|
return res.status(400).json({ error: 'Aucune image à supprimer' });
|
|
}
|
|
|
|
// Supprimer le fichier
|
|
const imagePath = path.join(__dirname, '../public/uploads', note.image_filename);
|
|
if (fs.existsSync(imagePath)) {
|
|
fs.unlinkSync(imagePath);
|
|
}
|
|
|
|
await runQuery('UPDATE notes SET image_filename = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = $1', [req.params.id]);
|
|
|
|
res.json({ message: 'Image supprimée avec succès' });
|
|
} catch (error) {
|
|
logger.error('Erreur lors de la suppression de l\'image:', error);
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/notes/:id/todos
|
|
* Ajouter un todo à une note (supports subtasks via parent_id)
|
|
*/
|
|
router.post('/:id/todos',
|
|
[
|
|
body('text').trim().notEmpty().withMessage('Le texte est requis'),
|
|
body('parent_id').optional().isInt()
|
|
],
|
|
async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ error: 'Données invalides', details: errors.array() });
|
|
}
|
|
|
|
// Vérifier que la note appartient à l'utilisateur
|
|
const note = await getOne('SELECT id FROM notes WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id]);
|
|
if (!note) {
|
|
return res.status(404).json({ error: 'Note non trouvée' });
|
|
}
|
|
|
|
const { text, parent_id } = req.body;
|
|
let level = 0;
|
|
|
|
// Si c'est un subtask, vérifier que le parent existe et calculer le niveau
|
|
if (parent_id) {
|
|
const parent = await getOne('SELECT id, level FROM note_todos WHERE id = $1 AND note_id = $2', [parent_id, req.params.id]);
|
|
if (!parent) {
|
|
return res.status(404).json({ error: 'Todo parent non trouvé' });
|
|
}
|
|
level = (parent.level || 0) + 1;
|
|
}
|
|
|
|
logger.info(`[CREATE TODO] Ajout todo à note ${req.params.id}: "${text}"${parent_id ? ` (subtask of ${parent_id})` : ''}`);
|
|
|
|
const result = await runQuery(`
|
|
INSERT INTO note_todos (note_id, text, parent_id, level, position)
|
|
VALUES ($1, $2, $3, $4, (SELECT COALESCE(MAX(position), 0) + 1 FROM note_todos WHERE note_id = $5 AND COALESCE(parent_id, 0) = COALESCE($6, 0)))
|
|
RETURNING *
|
|
`, [req.params.id, text, parent_id || null, level, req.params.id, parent_id || null]);
|
|
|
|
logger.info(`[CREATE TODO] Todo créé - ID: ${result.id}, note_id: ${req.params.id}, level: ${level}`);
|
|
|
|
await runQuery('UPDATE notes SET updated_at = CURRENT_TIMESTAMP WHERE id = $1', [req.params.id]);
|
|
|
|
res.status(201).json({
|
|
id: result.id,
|
|
text,
|
|
completed: false,
|
|
position: result.position || 0,
|
|
parent_id: parent_id || null,
|
|
level: level
|
|
});
|
|
} catch (error) {
|
|
logger.error('Erreur lors de l\'ajout du todo:', error);
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
}
|
|
);
|
|
|
|
/**
|
|
* PUT /api/notes/todos/:todoId
|
|
* Modifier un todo dans une note
|
|
*/
|
|
router.put('/todos/:todoId',
|
|
[
|
|
body('text').optional().trim().notEmpty(),
|
|
body('completed').optional().isBoolean(),
|
|
body('position').optional().isInt()
|
|
],
|
|
async (req, res) => {
|
|
try {
|
|
const { text, completed, position } = req.body;
|
|
|
|
// Vérifier que le todo appartient à une note de l'utilisateur
|
|
const todo = await getOne(`
|
|
SELECT nt.id, nt.note_id
|
|
FROM note_todos nt
|
|
JOIN notes n ON nt.note_id = n.id
|
|
WHERE nt.id = $1 AND n.user_id = $2
|
|
`, [req.params.todoId, req.user.id]);
|
|
|
|
if (!todo) {
|
|
return res.status(404).json({ error: 'Todo non trouvé' });
|
|
}
|
|
|
|
const updates = [];
|
|
const params = [];
|
|
let paramCount = 0;
|
|
|
|
if (text !== undefined) {
|
|
paramCount++;
|
|
updates.push(`text = $${paramCount}`);
|
|
params.push(text);
|
|
}
|
|
if (completed !== undefined) {
|
|
paramCount++;
|
|
updates.push(`completed = $${paramCount}`);
|
|
params.push(completed); // BOOLEAN instead of 0/1
|
|
}
|
|
if (position !== undefined) {
|
|
paramCount++;
|
|
updates.push(`position = $${paramCount}`);
|
|
params.push(position);
|
|
}
|
|
|
|
if (updates.length > 0) {
|
|
paramCount++;
|
|
params.push(req.params.todoId);
|
|
await runQuery(`UPDATE note_todos SET ${updates.join(', ')} WHERE id = $${paramCount}`, params);
|
|
await runQuery('UPDATE notes SET updated_at = CURRENT_TIMESTAMP WHERE id = $1', [todo.note_id]);
|
|
}
|
|
|
|
res.json({ message: 'Todo modifié avec succès' });
|
|
} catch (error) {
|
|
logger.error('Erreur lors de la modification du todo:', error);
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
}
|
|
);
|
|
|
|
/**
|
|
* DELETE /api/notes/todos/:todoId
|
|
* Supprimer un todo d'une note
|
|
*/
|
|
router.delete('/todos/:todoId', async (req, res) => {
|
|
try {
|
|
// Vérifier que le todo appartient à une note de l'utilisateur
|
|
const todo = await getOne(`
|
|
SELECT nt.id, nt.note_id
|
|
FROM note_todos nt
|
|
JOIN notes n ON nt.note_id = n.id
|
|
WHERE nt.id = $1 AND n.user_id = $2
|
|
`, [req.params.todoId, req.user.id]);
|
|
|
|
if (!todo) {
|
|
return res.status(404).json({ error: 'Todo non trouvé' });
|
|
}
|
|
|
|
await runQuery('DELETE FROM note_todos WHERE id = $1', [req.params.todoId]);
|
|
await runQuery('UPDATE notes SET updated_at = CURRENT_TIMESTAMP WHERE id = $1', [todo.note_id]);
|
|
|
|
res.json({ message: 'Todo supprimé avec succès' });
|
|
} catch (error) {
|
|
logger.error('Erreur lors de la suppression du todo:', error);
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/search
|
|
* Rechercher des notes
|
|
*/
|
|
const searchNotes = async (req, res) => {
|
|
try {
|
|
const query = req.query.q;
|
|
if (!query || query.trim().length === 0) {
|
|
return res.json([]);
|
|
}
|
|
|
|
const searchTerm = `%${query}%`;
|
|
|
|
const notes = await getAll(`
|
|
SELECT id, title, content, image_filename, created_at, updated_at
|
|
FROM notes
|
|
WHERE user_id = $1 AND (title LIKE $2 OR content LIKE $3)
|
|
ORDER BY updated_at DESC
|
|
LIMIT 50
|
|
`, [req.user.id, searchTerm, searchTerm]);
|
|
|
|
res.json(notes);
|
|
} catch (error) {
|
|
logger.error('Erreur lors de la recherche:', error);
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
};
|
|
|
|
// ==================== IMAGE ATTACHMENTS ====================
|
|
|
|
// Configuration multer pour les images multiples
|
|
const imageStorage = multer.diskStorage({
|
|
destination: (req, file, cb) => {
|
|
const imagesDir = path.join(__dirname, '../public/uploads/images');
|
|
// Créer le dossier s'il n'existe pas
|
|
if (!fs.existsSync(imagesDir)) {
|
|
fs.mkdirSync(imagesDir, { recursive: true });
|
|
}
|
|
cb(null, imagesDir);
|
|
},
|
|
filename: (req, file, cb) => {
|
|
const uniqueName = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}${path.extname(file.originalname)}`;
|
|
cb(null, uniqueName);
|
|
}
|
|
});
|
|
|
|
const imageUpload = multer({
|
|
storage: imageStorage,
|
|
limits: { fileSize: parseInt(process.env.MAX_FILE_SIZE) || 5 * 1024 * 1024 }, // 5MB
|
|
fileFilter: (req, file, cb) => {
|
|
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
|
|
if (allowedTypes.includes(file.mimetype)) {
|
|
cb(null, true);
|
|
} else {
|
|
cb(new Error('Type de fichier non autorisé. Utilisez JPEG, PNG, WebP ou GIF.'));
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/notes/:id/images
|
|
* Ajouter une image à une note
|
|
*/
|
|
router.post('/:id/images', imageUpload.single('image'), async (req, res) => {
|
|
try {
|
|
if (!req.file) {
|
|
return res.status(400).json({ error: 'Aucune image fournie' });
|
|
}
|
|
|
|
// Vérifier que la note appartient à l'utilisateur
|
|
const note = await getOne('SELECT id FROM notes WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id]);
|
|
if (!note) {
|
|
// Supprimer le fichier uploadé
|
|
fs.unlinkSync(req.file.path);
|
|
return res.status(404).json({ error: 'Note non trouvée' });
|
|
}
|
|
|
|
// Enregistrer l'image dans la base de données
|
|
logger.info(`[CREATE IMAGE] Ajout image à note ${req.params.id}: ${req.file.originalname}`);
|
|
|
|
const result = await runQuery(`
|
|
INSERT INTO note_images (note_id, filename, original_name)
|
|
VALUES ($1, $2, $3)
|
|
RETURNING *
|
|
`, [req.params.id, req.file.filename, req.file.originalname]);
|
|
|
|
// Mettre à jour la date de modification de la note
|
|
await runQuery('UPDATE notes SET updated_at = CURRENT_TIMESTAMP WHERE id = $1', [req.params.id]);
|
|
|
|
logger.info(`[CREATE IMAGE] Image créée - ID: ${result.id}, filename: ${req.file.filename}, note_id: ${req.params.id}`);
|
|
|
|
res.json({
|
|
message: 'Image ajoutée avec succès',
|
|
image: {
|
|
id: result.id,
|
|
filename: req.file.filename,
|
|
original_name: req.file.originalname
|
|
}
|
|
});
|
|
} catch (error) {
|
|
logger.error('Erreur lors de l\'ajout de l\'image:', error);
|
|
if (req.file && fs.existsSync(req.file.path)) {
|
|
fs.unlinkSync(req.file.path);
|
|
}
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/notes/:id/images
|
|
* Liste les images d'une note
|
|
*/
|
|
router.get('/:id/images', async (req, res) => {
|
|
try {
|
|
// Vérifier que la note appartient à l'utilisateur
|
|
const note = await getOne('SELECT id FROM notes WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id]);
|
|
if (!note) {
|
|
return res.status(404).json({ error: 'Note non trouvée' });
|
|
}
|
|
|
|
const images = await getAll(`
|
|
SELECT id, filename, original_name, created_at
|
|
FROM note_images
|
|
WHERE note_id = $1
|
|
ORDER BY created_at DESC
|
|
`, [req.params.id]);
|
|
|
|
res.json(images);
|
|
} catch (error) {
|
|
logger.error('Erreur lors de la récupération des images:', error);
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* DELETE /api/notes/:noteId/images/:imageId
|
|
* Supprimer une image d'une note
|
|
*/
|
|
router.delete('/:noteId/images/:imageId', async (req, res) => {
|
|
try {
|
|
// Vérifier que l'image appartient à une note de l'utilisateur
|
|
const image = await getOne(`
|
|
SELECT ni.id, ni.filename, ni.note_id
|
|
FROM note_images ni
|
|
JOIN notes n ON ni.note_id = n.id
|
|
WHERE ni.id = $1 AND ni.note_id = $2 AND n.user_id = $3
|
|
`, [req.params.imageId, req.params.noteId, req.user.id]);
|
|
|
|
if (!image) {
|
|
return res.status(404).json({ error: 'Image non trouvée' });
|
|
}
|
|
|
|
// Supprimer le fichier physique
|
|
const imagePath = path.join(__dirname, '../public/uploads/images', image.filename);
|
|
if (fs.existsSync(imagePath)) {
|
|
fs.unlinkSync(imagePath);
|
|
}
|
|
|
|
// Supprimer l'entrée de la base de données
|
|
await runQuery('DELETE FROM note_images WHERE id = $1', [req.params.imageId]);
|
|
|
|
// Mettre à jour la date de modification de la note
|
|
await runQuery('UPDATE notes SET updated_at = CURRENT_TIMESTAMP WHERE id = $1', [req.params.noteId]);
|
|
|
|
logger.info(`Image ${image.filename} supprimée de la note ${req.params.noteId}`);
|
|
|
|
res.json({ message: 'Image supprimée avec succès' });
|
|
} catch (error) {
|
|
logger.error('Erreur lors de la suppression de l\'image:', error);
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
});
|
|
|
|
// ==================== FILE ATTACHMENTS ====================
|
|
|
|
// Configuration multer pour tous types de fichiers
|
|
const fileStorage = multer.diskStorage({
|
|
destination: (req, file, cb) => {
|
|
const filesDir = path.join(__dirname, '../public/uploads/files');
|
|
// Créer le dossier s'il n'existe pas
|
|
if (!fs.existsSync(filesDir)) {
|
|
fs.mkdirSync(filesDir, { recursive: true });
|
|
}
|
|
cb(null, filesDir);
|
|
},
|
|
filename: (req, file, cb) => {
|
|
const uniqueName = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}-${file.originalname}`;
|
|
cb(null, uniqueName);
|
|
}
|
|
});
|
|
|
|
const fileUpload = multer({
|
|
storage: fileStorage,
|
|
limits: { fileSize: parseInt(process.env.MAX_FILE_SIZE) || 10 * 1024 * 1024 } // 10MB
|
|
});
|
|
|
|
/**
|
|
* POST /api/notes/:id/files
|
|
* Ajouter un fichier à une note
|
|
*/
|
|
router.post('/:id/files', fileUpload.single('file'), async (req, res) => {
|
|
try {
|
|
if (!req.file) {
|
|
return res.status(400).json({ error: 'Aucun fichier fourni' });
|
|
}
|
|
|
|
// Vérifier que la note appartient à l'utilisateur
|
|
const note = await getOne('SELECT id FROM notes WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id]);
|
|
if (!note) {
|
|
// Supprimer le fichier uploadé
|
|
fs.unlinkSync(req.file.path);
|
|
return res.status(404).json({ error: 'Note non trouvée' });
|
|
}
|
|
|
|
// Enregistrer le fichier dans la base de données
|
|
logger.info(`[CREATE FILE] Ajout fichier à note ${req.params.id}: ${req.file.originalname}`);
|
|
|
|
const result = await runQuery(`
|
|
INSERT INTO note_files (note_id, filename, original_name, file_size, mime_type)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
RETURNING *
|
|
`, [req.params.id, req.file.filename, req.file.originalname, req.file.size, req.file.mimetype]);
|
|
|
|
// Mettre à jour la date de modification de la note
|
|
await runQuery('UPDATE notes SET updated_at = CURRENT_TIMESTAMP WHERE id = $1', [req.params.id]);
|
|
|
|
logger.info(`[CREATE FILE] Fichier créé - ID: ${result.id}, filename: ${req.file.filename}, note_id: ${req.params.id}`);
|
|
|
|
res.json({
|
|
message: 'Fichier ajouté avec succès',
|
|
file: {
|
|
id: result.id,
|
|
filename: req.file.filename,
|
|
original_name: req.file.originalname,
|
|
file_size: req.file.size,
|
|
mime_type: req.file.mimetype
|
|
}
|
|
});
|
|
} catch (error) {
|
|
logger.error('Erreur lors de l\'ajout du fichier:', error);
|
|
if (req.file && fs.existsSync(req.file.path)) {
|
|
fs.unlinkSync(req.file.path);
|
|
}
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/notes/:id/files
|
|
* Liste les fichiers d'une note
|
|
*/
|
|
router.get('/:id/files', async (req, res) => {
|
|
try {
|
|
// Vérifier que la note appartient à l'utilisateur
|
|
const note = await getOne('SELECT id FROM notes WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id]);
|
|
if (!note) {
|
|
return res.status(404).json({ error: 'Note non trouvée' });
|
|
}
|
|
|
|
const files = await getAll(`
|
|
SELECT id, filename, original_name, file_size, mime_type, created_at
|
|
FROM note_files
|
|
WHERE note_id = $1
|
|
ORDER BY created_at DESC
|
|
`, [req.params.id]);
|
|
|
|
res.json(files);
|
|
} catch (error) {
|
|
logger.error('Erreur lors de la récupération des fichiers:', error);
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/notes/files/:fileId/download
|
|
* Télécharger un fichier
|
|
*/
|
|
router.get('/files/:fileId/download', async (req, res) => {
|
|
try {
|
|
// Vérifier que le fichier appartient à une note de l'utilisateur
|
|
const file = await getOne(`
|
|
SELECT nf.id, nf.filename, nf.original_name, nf.mime_type
|
|
FROM note_files nf
|
|
JOIN notes n ON nf.note_id = n.id
|
|
WHERE nf.id = $1 AND n.user_id = $2
|
|
`, [req.params.fileId, req.user.id]);
|
|
|
|
if (!file) {
|
|
return res.status(404).json({ error: 'Fichier non trouvé' });
|
|
}
|
|
|
|
const filePath = path.join(__dirname, '../public/uploads/files', file.filename);
|
|
if (!fs.existsSync(filePath)) {
|
|
return res.status(404).json({ error: 'Fichier physique non trouvé' });
|
|
}
|
|
|
|
res.download(filePath, file.original_name);
|
|
} catch (error) {
|
|
logger.error('Erreur lors du téléchargement du fichier:', error);
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* DELETE /api/notes/files/:fileId
|
|
* Supprimer un fichier
|
|
*/
|
|
router.delete('/files/:fileId', async (req, res) => {
|
|
try {
|
|
// Vérifier que le fichier appartient à une note de l'utilisateur
|
|
const file = await getOne(`
|
|
SELECT nf.id, nf.note_id, nf.filename
|
|
FROM note_files nf
|
|
JOIN notes n ON nf.note_id = n.id
|
|
WHERE nf.id = $1 AND n.user_id = $2
|
|
`, [req.params.fileId, req.user.id]);
|
|
|
|
if (!file) {
|
|
return res.status(404).json({ error: 'Fichier non trouvé' });
|
|
}
|
|
|
|
// Supprimer le fichier physique
|
|
const filePath = path.join(__dirname, '../public/uploads/files', file.filename);
|
|
if (fs.existsSync(filePath)) {
|
|
fs.unlinkSync(filePath);
|
|
}
|
|
|
|
// Supprimer de la base de données
|
|
await runQuery('DELETE FROM note_files WHERE id = $1', [req.params.fileId]);
|
|
|
|
// Mettre à jour la date de modification de la note
|
|
await runQuery('UPDATE notes SET updated_at = CURRENT_TIMESTAMP WHERE id = $1', [file.note_id]);
|
|
|
|
logger.info(`Fichier supprimé: ${file.filename}`);
|
|
|
|
res.json({ message: 'Fichier supprimé avec succès' });
|
|
} catch (error) {
|
|
logger.error('Erreur lors de la suppression du fichier:', error);
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
});
|
|
|
|
// ==================== TAGS ====================
|
|
|
|
/**
|
|
* GET /api/notes/:id/tags
|
|
* Récupérer les tags d'une note
|
|
*/
|
|
router.get('/:id/tags', async (req, res) => {
|
|
try {
|
|
const note = await getOne('SELECT id FROM notes WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id]);
|
|
if (!note) {
|
|
return res.status(404).json({ error: 'Note non trouvée' });
|
|
}
|
|
|
|
const tags = await getAll('SELECT id, tag FROM note_tags WHERE note_id = $1 ORDER BY tag ASC', [req.params.id]);
|
|
res.json(tags || []);
|
|
} catch (error) {
|
|
logger.error('Erreur lors de la récupération des tags:', error);
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/notes/:id/tags
|
|
* Ajouter un tag à une note
|
|
*/
|
|
router.post('/:id/tags', async (req, res) => {
|
|
try {
|
|
const { tag } = req.body;
|
|
if (!tag || !tag.trim()) {
|
|
return res.status(400).json({ error: 'Tag invalide' });
|
|
}
|
|
|
|
const note = await getOne('SELECT id FROM notes WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id]);
|
|
if (!note) {
|
|
return res.status(404).json({ error: 'Note non trouvée' });
|
|
}
|
|
|
|
logger.info(`[CREATE TAG] Ajout tag à note ${req.params.id}: "${tag.trim().toLowerCase()}"`);
|
|
|
|
const result = await runQuery('INSERT INTO note_tags (note_id, tag) VALUES ($1, $2) RETURNING *', [req.params.id, tag.trim().toLowerCase()]);
|
|
|
|
logger.info(`[CREATE TAG] Tag créé - ID: ${result.id}, tag: "${result.tag}", note_id: ${req.params.id}`);
|
|
|
|
res.json({ id: result.id, tag: tag.trim().toLowerCase() });
|
|
} catch (error) {
|
|
if (error.message && error.message.includes('UNIQUE constraint')) {
|
|
return res.status(409).json({ error: 'Ce tag existe déjà' });
|
|
}
|
|
logger.error('Erreur lors de l\'ajout du tag:', error);
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* DELETE /api/notes/:noteId/tags/:tagId
|
|
* Supprimer un tag d'une note
|
|
*/
|
|
router.delete('/:noteId/tags/:tagId', async (req, res) => {
|
|
try {
|
|
const tag = await getOne(`
|
|
SELECT nt.id FROM note_tags nt
|
|
JOIN notes n ON nt.note_id = n.id
|
|
WHERE nt.id = $1 AND nt.note_id = $2 AND n.user_id = $3
|
|
`, [req.params.tagId, req.params.noteId, req.user.id]);
|
|
|
|
if (!tag) {
|
|
return res.status(404).json({ error: 'Tag non trouvé' });
|
|
}
|
|
|
|
await runQuery('DELETE FROM note_tags WHERE id = $1', [req.params.tagId]);
|
|
res.json({ message: 'Tag supprimé' });
|
|
} catch (error) {
|
|
logger.error('Erreur lors de la suppression du tag:', error);
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PATCH /api/notes/:id/priority
|
|
* Modifier la priorité d'une note
|
|
*/
|
|
router.patch('/:id/priority', async (req, res) => {
|
|
try {
|
|
const { priority } = 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', [req.params.id, req.user.id]);
|
|
if (!note) {
|
|
return res.status(404).json({ error: 'Note non trouvée' });
|
|
}
|
|
|
|
await runQuery('UPDATE notes SET priority = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2', [priority ? 1 : 0, req.params.id]); // notes.priority is INTEGER not BOOLEAN
|
|
|
|
logger.info(`Priorité de la note ${req.params.id} modifiée: ${priority}`);
|
|
res.json({ message: 'Priorité modifiée avec succès', priority });
|
|
} catch (error) {
|
|
logger.error('Erreur lors de la modification de la priorité:', error);
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|
|
module.exports.searchNotes = searchNotes;
|