feat: Implement core application dashboard, add todo and task management API, and include database testing utilities.

This commit is contained in:
Michael committed 2026-03-25 09:09:53 +01:00
1 parent b1f58a2f13
commit ef6a8c7b4a
5 files changed
+84 -24

No files matched your search

+19 -6
View File
@@ -29,10 +29,16 @@ Application web moderne de gestion de notes et de tâches, Dockerisée, avec aut
- Attribution des droits administrateur - Attribution des droits administrateur
- Interface dédiée pour les admins - Interface dédiée pour les admins
## Architecture
- **Frontend**: React 18, Vite, Tailwind CSS, Shadcn UI
- **Backend**: Node.js, Express
- **Base de Données**: PostgreSQL 16 (recommandé)
## 🛠️ Stack Technique ## 🛠️ Stack Technique
- **Backend** : Node.js 20 + Express - **Backend** : Node.js 20 + Express
- **Base de données** : SQLite3 - **Base de données** : PostgreSQL 16
- **Authentification** : JWT + bcrypt - **Authentification** : JWT + bcrypt
- **Frontend** : React 19 + TypeScript + Vite - **Frontend** : React 19 + TypeScript + Vite
- **UI Components** : shadcn/ui + Tailwind CSS - **UI Components** : shadcn/ui + Tailwind CSS
@@ -224,12 +230,19 @@ PUT /api/notes/todos/:todoId
DELETE /api/notes/todos/:todoId DELETE /api/notes/todos/:todoId
``` ```
### Todos Globaux ### Tâches (Todos Globaux)
``` ```
GET /api/todos GET /api/todos # Liste toutes les tâches
POST /api/todos POST /api/todos # Créer une tâche (text, priority, due_date, parent_id)
PUT /api/todos/:id PUT /api/todos/:id # Modifier une tâche
DELETE /api/todos/:id PATCH /api/todos/:id/toggle # Basculer l'état complété
PATCH /api/todos/:id/priority # Basculer la priorité
PATCH /api/todos/:id/in-progress # Basculer l'état "en cours"
DELETE /api/todos/:id # Supprimer une tâche
# Alias sémantique (recommandé pour les intégrations externes)
POST /api/tasks # Identique à POST /api/todos
GET /api/tasks # Identique à GET /api/todos
``` ```
## 🔒 Sécurité ## 🔒 Sécurité
+21 -11
View File
@@ -19,10 +19,10 @@ router.get('/', async (req, res) => {
// Essayer d'abord avec le champ priority (nouvelle version) // Essayer d'abord avec le champ priority (nouvelle version)
try { try {
const todos = await getAll(` const todos = await getAll(`
SELECT id, text, completed, priority, in_progress, parent_id, level, created_at SELECT id, text, completed, priority, in_progress, parent_id, level, due_date, created_at
FROM global_todos FROM global_todos
WHERE user_id = $1 WHERE user_id = $1
ORDER BY priority DESC, created_at DESC ORDER BY priority DESC, due_date ASC NULLS LAST, created_at DESC
`, [req.user.id]); `, [req.user.id]);
res.json(todos); res.json(todos);
@@ -54,7 +54,10 @@ router.get('/', async (req, res) => {
router.post('/', router.post('/',
[ [
body('text').trim().notEmpty().withMessage('Le texte est requis'), body('text').trim().notEmpty().withMessage('Le texte est requis'),
body('parent_id').optional().isInt() body('parent_id').optional().isInt(),
body('priority').optional().isBoolean(),
body('in_progress').optional().isBoolean(),
body('due_date').optional().isISO8601()
], ],
async (req, res) => { async (req, res) => {
try { try {
@@ -63,7 +66,7 @@ router.post('/',
return res.status(400).json({ error: 'Données invalides', details: errors.array() }); return res.status(400).json({ error: 'Données invalides', details: errors.array() });
} }
const { text, parent_id } = req.body; const { text, parent_id, priority = false, in_progress = false, due_date = null } = req.body;
let level = 0; let level = 0;
// Si c'est un subtask, vérifier que le parent existe et calculer le niveau // Si c'est un subtask, vérifier que le parent existe et calculer le niveau
@@ -78,10 +81,10 @@ router.post('/',
logger.info(`[CREATE GLOBAL TODO] Début création - user_id: ${req.user.id}, text: "${text}"${parent_id ? ` (subtask of ${parent_id})` : ''}`); logger.info(`[CREATE GLOBAL TODO] Début création - user_id: ${req.user.id}, text: "${text}"${parent_id ? ` (subtask of ${parent_id})` : ''}`);
const result = await runQuery(` const result = await runQuery(`
INSERT INTO global_todos (user_id, text, parent_id, level) INSERT INTO global_todos (user_id, text, parent_id, level, priority, in_progress, due_date)
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING * RETURNING *
`, [req.user.id, text, parent_id || null, level]); `, [req.user.id, text, parent_id || null, level, priority, in_progress ? 1 : 0, due_date]);
logger.info(`[CREATE GLOBAL TODO] Todo global créé avec succès - ID: ${result.id}, user: ${req.user.username}, level: ${level}`); logger.info(`[CREATE GLOBAL TODO] Todo global créé avec succès - ID: ${result.id}, user: ${req.user.username}, level: ${level}`);
@@ -89,10 +92,11 @@ router.post('/',
id: result.id, id: result.id,
text, text,
completed: false, completed: false,
priority: false, priority: priority,
in_progress: false, in_progress: !!in_progress,
parent_id: parent_id || null, parent_id: parent_id || null,
level: level, level: level,
due_date: due_date,
created_at: new Date().toISOString() created_at: new Date().toISOString()
}); });
} catch (error) { } catch (error) {
@@ -111,7 +115,8 @@ router.put('/:id',
body('text').optional().trim().notEmpty(), body('text').optional().trim().notEmpty(),
body('completed').optional().isBoolean(), body('completed').optional().isBoolean(),
body('priority').optional().isBoolean(), body('priority').optional().isBoolean(),
body('in_progress').optional().isBoolean() body('in_progress').optional().isBoolean(),
body('due_date').optional().isISO8601().nullable()
], ],
async (req, res) => { async (req, res) => {
try { try {
@@ -120,7 +125,7 @@ router.put('/:id',
return res.status(400).json({ error: 'Données invalides', details: errors.array() }); return res.status(400).json({ error: 'Données invalides', details: errors.array() });
} }
const { text, completed, priority, in_progress } = req.body; const { text, completed, priority, in_progress, due_date } = req.body;
// Vérifier que le todo appartient à l'utilisateur // Vérifier que le todo appartient à l'utilisateur
const todo = await getOne('SELECT id FROM global_todos WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id]); const todo = await getOne('SELECT id FROM global_todos WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id]);
@@ -152,6 +157,11 @@ router.put('/:id',
updates.push(`in_progress = $${paramCount}`); updates.push(`in_progress = $${paramCount}`);
params.push(in_progress ? 1 : 0); // global_todos.in_progress is INTEGER not BOOLEAN params.push(in_progress ? 1 : 0); // global_todos.in_progress is INTEGER not BOOLEAN
} }
if (due_date !== undefined) {
paramCount++;
updates.push(`due_date = $${paramCount}`);
params.push(due_date);
}
if (updates.length > 0) { if (updates.length > 0) {
paramCount++; paramCount++;
+1
View File
@@ -94,6 +94,7 @@ app.use('/api/auth', require('./routes/auth.routes'));
app.use('/api/users', require('./routes/users.routes')); app.use('/api/users', require('./routes/users.routes'));
app.use('/api/notes', require('./routes/notes.routes')); app.use('/api/notes', require('./routes/notes.routes'));
app.use('/api/todos', require('./routes/todos.routes')); app.use('/api/todos', require('./routes/todos.routes'));
app.use('/api/tasks', require('./routes/todos.routes'));
app.use('/api/recurring-todos', require('./routes/recurring-todos.routes')); app.use('/api/recurring-todos', require('./routes/recurring-todos.routes'));
app.use('/api/settings', require('./routes/settings.routes')); app.use('/api/settings', require('./routes/settings.routes'));
app.use('/api/rss', require('./routes/rss.routes')); app.use('/api/rss', require('./routes/rss.routes'));
+33 -7
View File
@@ -2809,6 +2809,27 @@ const Index = () => {
{ "text": "Tâche 1" }, { "text": "Tâche 1" },
{ "text": "Tâche 2", "priority": true } { "text": "Tâche 2", "priority": true }
] ]
}`}</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/tasks</code>
<span className="text-xs text-muted-foreground ml-2">(Alias de /api/todos)</span>
</div>
<p className="text-sm text-muted-foreground mb-2">
Créer une tâche générale avec priorité, progression et date d'échéance
</p>
<div className="bg-muted p-2 rounded text-xs font-mono overflow-x-auto">
<pre>{`{
"text": "Ma tâche importante",
"priority": true,
"in_progress": false,
"due_date": "2024-12-31T23:59:59Z"
}`}</pre> }`}</pre>
</div> </div>
</CardContent> </CardContent>
@@ -2820,19 +2841,24 @@ const Index = () => {
<div className="space-y-2"> <div className="space-y-2">
<h3 className="font-semibold text-lg">3. Exemple complet (curl)</h3> <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"> <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) <p className="text-xs text-muted-foreground mb-2 font-sans"># Créer une note avec tâches :</p>
curl -X POST ${window.location.origin}/api/notes/full \\ <pre className="mb-4">{`curl -X POST ${window.location.origin}/api/notes/full \\
-H "Content-Type: application/json" \\ -H "Content-Type: application/json" \\
-H "X-API-Key: ${apiInfoModal.apiKey || 'VOTRE_CLE_API'}" \\ -H "X-API-Key: ${apiInfoModal.apiKey || 'VOTRE_CLE_API'}" \\
-d '{ -d '{
"title": "Liste de courses", "title": "Liste de courses",
"content": "Pour le weekend", "content": "Pour le weekend",
"todos": [ "todos": [{ "text": "Pain" }, { "text": "Lait" }],
{ "text": "Pain" },
{ "text": "Lait" },
{ "text": "Fruits" }
],
"tags": ["courses"] "tags": ["courses"]
}'`}</pre>
<p className="text-xs text-muted-foreground mb-2 font-sans"># Créer une tâche générale prioritaire :</p>
<pre>{`curl -X POST ${window.location.origin}/api/tasks \\
-H "Content-Type: application/json" \\
-H "X-API-Key: ${apiInfoModal.apiKey || 'VOTRE_CLE_API'}" \\
-d '{
"text": "Finaliser le rapport",
"priority": true,
"due_date": "2024-10-25T17:00:00Z"
}'`}</pre> }'`}</pre>
</div> </div>
</div> </div>
+10
View File
@@ -0,0 +1,10 @@
const { pool } = require('./config/database');
pool.query('SELECT 1', (err, res) => {
if (err) {
console.error('DB Connection Error:', err);
process.exit(1);
} else {
console.log('DB Connection Success');
process.exit(0);
}
});