a787667dcb
Alle Routen nutzen jetzt das zentrale Validierungsmodul (validate.js): - Maximale Stringlängen (200 Titel, 5000 Text, 100 Kurztexte) - Enum-Validation für Kategorien, Prioritäten, Meal-Types - Datum/Zeit/DateTime-Format-Prüfung - RRULE-Validation (neue rrule()-Funktion) - Farbwert-Prüfung (#RRGGBB) Betroffene Routen: calendar, notes, contacts, budget, shopping, meals. Tasks-Route um RRULE-Validation ergänzt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
153 lines
5.1 KiB
JavaScript
153 lines
5.1 KiB
JavaScript
/**
|
|
* Modul: Pinnwand / Notizen (Notes)
|
|
* Zweck: REST-API-Routen für Notizen (CRUD, Pin-Toggle)
|
|
* Abhängigkeiten: express, server/db.js, server/auth.js
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const express = require('express');
|
|
const router = express.Router();
|
|
const db = require('../db');
|
|
const { str, color, collectErrors, MAX_TEXT, MAX_TITLE } = require('../middleware/validate');
|
|
|
|
/**
|
|
* GET /api/v1/notes
|
|
* Alle Notizen, angepinnte zuerst, dann nach updated_at DESC.
|
|
* Response: { data: Note[] }
|
|
*/
|
|
router.get('/', (req, res) => {
|
|
try {
|
|
const notes = db.get().prepare(`
|
|
SELECT n.*, u.display_name AS creator_name, u.avatar_color AS creator_color
|
|
FROM notes n
|
|
LEFT JOIN users u ON u.id = n.created_by
|
|
ORDER BY n.pinned DESC, n.updated_at DESC
|
|
`).all();
|
|
res.json({ data: notes });
|
|
} catch (err) {
|
|
console.error('[notes/GET /]', err);
|
|
res.status(500).json({ error: 'Interner Fehler', code: 500 });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/v1/notes
|
|
* Neue Notiz anlegen.
|
|
* Body: { content, title?, color?, pinned? }
|
|
* Response: { data: Note }
|
|
*/
|
|
router.post('/', (req, res) => {
|
|
try {
|
|
const { pinned = 0 } = req.body;
|
|
const vContent = str(req.body.content, 'Inhalt', { max: MAX_TEXT });
|
|
const vTitle = str(req.body.title, 'Titel', { max: MAX_TITLE, required: false });
|
|
const vColor = color(req.body.color || '#FFEB3B', 'Farbe');
|
|
const errors = collectErrors([vContent, vTitle, vColor]);
|
|
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
|
|
|
|
const result = db.get().prepare(`
|
|
INSERT INTO notes (content, title, color, pinned, created_by)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
`).run(vContent.value, vTitle.value, vColor.value, pinned ? 1 : 0, req.session.userId);
|
|
|
|
const note = db.get().prepare(`
|
|
SELECT n.*, u.display_name AS creator_name, u.avatar_color AS creator_color
|
|
FROM notes n LEFT JOIN users u ON u.id = n.created_by
|
|
WHERE n.id = ?
|
|
`).get(result.lastInsertRowid);
|
|
|
|
res.status(201).json({ data: note });
|
|
} catch (err) {
|
|
console.error('[notes/POST /]', err);
|
|
res.status(500).json({ error: 'Interner Fehler', code: 500 });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PUT /api/v1/notes/:id
|
|
* Notiz bearbeiten.
|
|
* Body: { content?, title?, color?, pinned? }
|
|
* Response: { data: Note }
|
|
*/
|
|
router.put('/:id', (req, res) => {
|
|
try {
|
|
const id = parseInt(req.params.id, 10);
|
|
const note = db.get().prepare('SELECT * FROM notes WHERE id = ?').get(id);
|
|
if (!note) return res.status(404).json({ error: 'Notiz nicht gefunden', code: 404 });
|
|
|
|
const { pinned } = req.body;
|
|
const checks = [];
|
|
if (req.body.content !== undefined) checks.push(str(req.body.content, 'Inhalt', { max: MAX_TEXT, required: false }));
|
|
if (req.body.title !== undefined) checks.push(str(req.body.title, 'Titel', { max: MAX_TITLE, required: false }));
|
|
if (req.body.color !== undefined) checks.push(color(req.body.color, 'Farbe'));
|
|
const errors = collectErrors(checks);
|
|
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
|
|
|
|
db.get().prepare(`
|
|
UPDATE notes
|
|
SET content = COALESCE(?, content),
|
|
title = ?,
|
|
color = COALESCE(?, color),
|
|
pinned = COALESCE(?, pinned)
|
|
WHERE id = ?
|
|
`).run(
|
|
req.body.content?.trim() ?? null,
|
|
req.body.title !== undefined ? (req.body.title?.trim() || null) : note.title,
|
|
req.body.color ?? null,
|
|
pinned !== undefined ? (pinned ? 1 : 0) : null,
|
|
id
|
|
);
|
|
|
|
const updated = db.get().prepare(`
|
|
SELECT n.*, u.display_name AS creator_name, u.avatar_color AS creator_color
|
|
FROM notes n LEFT JOIN users u ON u.id = n.created_by WHERE n.id = ?
|
|
`).get(id);
|
|
|
|
res.json({ data: updated });
|
|
} catch (err) {
|
|
console.error('[notes/PUT /:id]', err);
|
|
res.status(500).json({ error: 'Interner Fehler', code: 500 });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PATCH /api/v1/notes/:id/pin
|
|
* Pin-Status toggeln.
|
|
* Response: { data: { id, pinned } }
|
|
*/
|
|
router.patch('/:id/pin', (req, res) => {
|
|
try {
|
|
const id = parseInt(req.params.id, 10);
|
|
const note = db.get().prepare('SELECT pinned FROM notes WHERE id = ?').get(id);
|
|
if (!note) return res.status(404).json({ error: 'Notiz nicht gefunden', code: 404 });
|
|
|
|
const newPinned = note.pinned ? 0 : 1;
|
|
db.get().prepare('UPDATE notes SET pinned = ? WHERE id = ?').run(newPinned, id);
|
|
res.json({ data: { id, pinned: newPinned } });
|
|
} catch (err) {
|
|
console.error('[notes/PATCH /:id/pin]', err);
|
|
res.status(500).json({ error: 'Interner Fehler', code: 500 });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* DELETE /api/v1/notes/:id
|
|
* Notiz löschen.
|
|
* Response: 204 No Content
|
|
*/
|
|
router.delete('/:id', (req, res) => {
|
|
try {
|
|
const id = parseInt(req.params.id, 10);
|
|
const result = db.get().prepare('DELETE FROM notes WHERE id = ?').run(id);
|
|
if (result.changes === 0)
|
|
return res.status(404).json({ error: 'Notiz nicht gefunden', code: 404 });
|
|
res.status(204).end();
|
|
} catch (err) {
|
|
console.error('[notes/DELETE /:id]', err);
|
|
res.status(500).json({ error: 'Interner Fehler', code: 500 });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|