mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-11 17:29:37 +02:00
feat: Implement core application dashboard, add todo and task management API, and include database testing utilities.
This commit is contained in:
1 parent
b1f58a2f13
commit
ef6a8c7b4a
5 files changed
+84
-24
No files matched your search
@@ -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
@@ -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++;
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in new issue
Block a user