mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-11 17:29:37 +02:00
Fixed issue where notes and todos were not displaying due to boolean type mismatch between SQLite (uses 0/1) and PostgreSQL (uses TRUE/FALSE). Changes: - routes/notes.routes.js: Fixed archived, completed, and priority fields - routes/todos.routes.js: Fixed completed, priority, and in_progress fields - routes/calendar.routes.js: Fixed all_day field - routes/rss.routes.js: Fixed enabled field - routes/rss.routes-v2.js: Fixed enabled field - routes/users.routes.js: Fixed is_admin field All boolean parameters now use native boolean values instead of converting to 0/1, ensuring compatibility with PostgreSQL BOOLEAN columns. This resolves the issue where notes were not displaying even though they existed in the database.
987 lines
32 KiB
JavaScript
987 lines
32 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?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, req.params.id]); // BOOLEAN instead of 0/1
|
|
|
|
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;
|