fix: Input-Validation auf allen API-Routen vereinheitlichen (Phase 5, Schritt 27)

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>
This commit is contained in:
ulsklyc
2026-03-26 00:23:57 +01:00
parent f507ef8488
commit a787667dcb
8 changed files with 193 additions and 140 deletions
+60 -1
View File
@@ -10,6 +10,15 @@
const MAX_TITLE = 200;
const MAX_TEXT = 5000;
const MAX_SHORT = 100;
const MAX_RRULE = 300;
// Regex-Muster
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
const TIME_RE = /^\d{2}:\d{2}$/;
const DATETIME_RE = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2})?Z?)?$/;
const COLOR_RE = /^#[0-9A-Fa-f]{6}$/;
const MONTH_RE = /^\d{4}-\d{2}$/;
const RRULE_RE = /^(FREQ=(DAILY|WEEKLY|MONTHLY)(;INTERVAL=\d{1,2})?(;BYDAY=[A-Z,]{2,}(,[A-Z]{2})*)?(;UNTIL=\d{8}(T\d{6}Z)?)?)?$/;
/**
* Bereinigt und validiert einen Pflicht-String.
@@ -103,4 +112,54 @@ function collectErrors(results) {
return results.map((r) => r.error).filter(Boolean);
}
module.exports = { str, oneOf, date, time, num, color, collectErrors, MAX_TITLE, MAX_TEXT, MAX_SHORT };
/**
* Validiert ein Datetime-Format YYYY-MM-DD oder YYYY-MM-DDTHH:MM[:SS][Z].
*/
function datetime(val, field, required = false) {
if (!val) {
if (required) return { value: null, error: `${field} ist erforderlich.` };
return { value: null, error: null };
}
if (!DATETIME_RE.test(String(val)))
return { value: null, error: `${field} muss im Format YYYY-MM-DD oder YYYY-MM-DDTHH:MM sein.` };
return { value: String(val), error: null };
}
/**
* Validiert ein Monatsformat YYYY-MM.
*/
function month(val, field) {
if (!val) return { value: null, error: null };
if (!MONTH_RE.test(String(val)))
return { value: null, error: `${field} muss im Format YYYY-MM sein.` };
return { value: String(val), error: null };
}
/**
* Validiert eine optionale RRULE.
*/
function rrule(val, field) {
if (!val) return { value: null, error: null };
const s = String(val).trim();
if (s.length > MAX_RRULE)
return { value: null, error: `${field} darf maximal ${MAX_RRULE} Zeichen haben.` };
// Grundlegende Struktur: KEY=VALUE;KEY=VALUE
if (!/^FREQ=(DAILY|WEEKLY|MONTHLY)/.test(s))
return { value: null, error: `${field}: Ungültige Wiederholungsregel.` };
return { value: s, error: null };
}
/**
* Validiert eine ganzzahlige ID (positiv).
*/
function id(val, field) {
const n = parseInt(val, 10);
if (!n || n < 1) return { value: null, error: `${field} muss eine positive Zahl sein.` };
return { value: n, error: null };
}
module.exports = {
str, oneOf, date, time, datetime, month, num, color, rrule, id, collectErrors,
MAX_TITLE, MAX_TEXT, MAX_SHORT, MAX_RRULE,
DATE_RE, TIME_RE, DATETIME_RE, COLOR_RE, MONTH_RE,
};
+22 -27
View File
@@ -9,12 +9,12 @@
const express = require('express');
const router = express.Router();
const db = require('../db');
const { str, oneOf, date, num, rrule, collectErrors, MAX_TITLE, MONTH_RE } = require('../middleware/validate');
const VALID_CATEGORIES = [
'Lebensmittel', 'Miete', 'Versicherung', 'Mobilität',
'Freizeit', 'Kleidung', 'Gesundheit', 'Bildung', 'Sonstiges',
];
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
// --------------------------------------------------------
// Statische Routen vor /:id
@@ -31,7 +31,7 @@ router.get('/summary', (req, res) => {
const today = new Date().toISOString().slice(0, 7); // YYYY-MM
const month = req.query.month || today;
if (!/^\d{4}-\d{2}$/.test(month))
if (!MONTH_RE.test(month))
return res.status(400).json({ error: 'month muss YYYY-MM sein', code: 400 });
const from = `${month}-01`;
@@ -83,7 +83,7 @@ router.get('/export', (req, res) => {
const today = new Date().toISOString().slice(0, 7);
const month = req.query.month || today;
if (!/^\d{4}-\d{2}$/.test(month))
if (!MONTH_RE.test(month))
return res.status(400).json({ error: 'month muss YYYY-MM sein', code: 400 });
const from = `${month}-01`;
@@ -141,7 +141,7 @@ router.get('/', (req, res) => {
const today = new Date().toISOString().slice(0, 7);
const month = req.query.month || today;
if (!/^\d{4}-\d{2}$/.test(month))
if (!MONTH_RE.test(month))
return res.status(400).json({ error: 'month muss YYYY-MM sein', code: 400 });
const from = `${month}-01`;
@@ -177,26 +177,20 @@ router.get('/', (req, res) => {
*/
router.post('/', (req, res) => {
try {
const {
title, amount, category = 'Sonstiges',
date, is_recurring = 0, recurrence_rule = null,
} = req.body;
if (!title || !title.trim())
return res.status(400).json({ error: 'Titel ist erforderlich', code: 400 });
if (amount === undefined || amount === null || isNaN(Number(amount)))
return res.status(400).json({ error: 'Betrag (Zahl) ist erforderlich', code: 400 });
if (!date || !DATE_RE.test(date))
return res.status(400).json({ error: 'Gültiges Datum (YYYY-MM-DD) erforderlich', code: 400 });
if (!VALID_CATEGORIES.includes(category))
return res.status(400).json({ error: `Ungültige Kategorie: ${category}`, code: 400 });
const vTitle = str(req.body.title, 'Titel', { max: MAX_TITLE });
const vAmount = num(req.body.amount, 'Betrag', { required: true });
const vCat = oneOf(req.body.category || 'Sonstiges', VALID_CATEGORIES, 'Kategorie');
const vDate = date(req.body.date, 'Datum', true);
const vRrule = rrule(req.body.recurrence_rule, 'Wiederholung');
const errors = collectErrors([vTitle, vAmount, vCat, vDate, vRrule]);
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
const result = db.get().prepare(`
INSERT INTO budget_entries (title, amount, category, date, is_recurring, recurrence_rule, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(
title.trim(), Number(amount), category, date,
is_recurring ? 1 : 0, recurrence_rule || null,
vTitle.value, vAmount.value, vCat.value || 'Sonstiges', vDate.value,
req.body.is_recurring ? 1 : 0, vRrule.value,
req.session.userId
);
@@ -225,14 +219,15 @@ router.put('/:id', (req, res) => {
const entry = db.get().prepare('SELECT * FROM budget_entries WHERE id = ?').get(id);
if (!entry) return res.status(404).json({ error: 'Eintrag nicht gefunden', code: 404 });
const { title, amount, category, date, is_recurring, recurrence_rule } = req.body;
if (amount !== undefined && isNaN(Number(amount)))
return res.status(400).json({ error: 'Betrag muss eine Zahl sein', code: 400 });
if (date !== undefined && !DATE_RE.test(date))
return res.status(400).json({ error: 'Ungültiges Datum', code: 400 });
if (category !== undefined && !VALID_CATEGORIES.includes(category))
return res.status(400).json({ error: `Ungültige Kategorie: ${category}`, code: 400 });
const checks = [];
if (req.body.title !== undefined) checks.push(str(req.body.title, 'Titel', { max: MAX_TITLE, required: false }));
if (req.body.amount !== undefined) checks.push(num(req.body.amount, 'Betrag'));
if (req.body.category !== undefined) checks.push(oneOf(req.body.category, VALID_CATEGORIES, 'Kategorie'));
if (req.body.date !== undefined) checks.push(date(req.body.date, 'Datum'));
if (req.body.recurrence_rule !== undefined) checks.push(rrule(req.body.recurrence_rule, 'Wiederholung'));
const errors = collectErrors(checks);
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
const { title, amount, category, is_recurring, recurrence_rule } = req.body;
db.get().prepare(`
UPDATE budget_entries
+29 -38
View File
@@ -13,11 +13,9 @@ const db = require('../db');
const googleCalendar = require('../services/google-calendar');
const appleCalendar = require('../services/apple-calendar');
const { requireAdmin } = require('../auth');
const { str, color, datetime, rrule, collectErrors, MAX_TITLE, MAX_TEXT, DATE_RE, DATETIME_RE } = require('../middleware/validate');
const VALID_SOURCES = ['local', 'google', 'apple'];
const DATETIME_RE = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2})?Z?)?$/;
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
const COLOR_RE = /^#[0-9A-Fa-f]{6}$/;
// --------------------------------------------------------
// GET /api/v1/calendar
@@ -260,26 +258,17 @@ router.get('/:id', (req, res) => {
// --------------------------------------------------------
router.post('/', (req, res) => {
try {
const {
title,
description = null,
start_datetime,
end_datetime = null,
all_day = 0,
location = null,
color = '#007AFF',
assigned_to = null,
recurrence_rule = null,
} = req.body;
const vTitle = str(req.body.title, 'Titel', { max: MAX_TITLE });
const vDesc = str(req.body.description, 'Beschreibung', { max: MAX_TEXT, required: false });
const vStart = datetime(req.body.start_datetime, 'Startdatum', true);
const vEnd = datetime(req.body.end_datetime, 'Enddatum');
const vColor = color(req.body.color || '#007AFF', 'Farbe');
const vLoc = str(req.body.location, 'Ort', { max: MAX_TITLE, required: false });
const vRrule = rrule(req.body.recurrence_rule, 'Wiederholung');
const errors = collectErrors([vTitle, vDesc, vStart, vEnd, vColor, vLoc, vRrule]);
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
if (!title || !title.trim())
return res.status(400).json({ error: 'Titel ist erforderlich', code: 400 });
if (!start_datetime || !DATETIME_RE.test(start_datetime))
return res.status(400).json({ error: 'Gültiges start_datetime erforderlich', code: 400 });
if (end_datetime && !DATETIME_RE.test(end_datetime))
return res.status(400).json({ error: 'Ungültiges end_datetime', code: 400 });
if (color && !COLOR_RE.test(color))
return res.status(400).json({ error: 'Farbe muss #RRGGBB sein', code: 400 });
const { all_day = 0, assigned_to = null } = req.body;
if (assigned_to) {
const user = db.get().prepare('SELECT id FROM users WHERE id = ?').get(assigned_to);
@@ -292,11 +281,11 @@ router.post('/', (req, res) => {
location, color, assigned_to, created_by, recurrence_rule)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
title.trim(), description || null,
start_datetime, end_datetime || null,
all_day ? 1 : 0, location || null,
color, assigned_to || null,
req.session.userId, recurrence_rule || null
vTitle.value, vDesc.value,
vStart.value, vEnd.value,
all_day ? 1 : 0, vLoc.value,
vColor.value, assigned_to || null,
req.session.userId, vRrule.value
);
const event = db.get().prepare(`
@@ -329,20 +318,22 @@ router.put('/:id', (req, res) => {
const event = db.get().prepare('SELECT * FROM calendar_events WHERE id = ?').get(id);
if (!event) return res.status(404).json({ error: 'Termin nicht gefunden', code: 404 });
const checks = [];
if (req.body.title !== undefined) checks.push(str(req.body.title, 'Titel', { max: MAX_TITLE, required: false }));
if (req.body.description !== undefined) checks.push(str(req.body.description, 'Beschreibung', { max: MAX_TEXT, required: false }));
if (req.body.start_datetime !== undefined) checks.push(datetime(req.body.start_datetime, 'Startdatum'));
if (req.body.end_datetime !== undefined) checks.push(datetime(req.body.end_datetime, 'Enddatum'));
if (req.body.color !== undefined) checks.push(color(req.body.color, 'Farbe'));
if (req.body.location !== undefined) checks.push(str(req.body.location, 'Ort', { max: MAX_TITLE, required: false }));
if (req.body.recurrence_rule !== undefined) checks.push(rrule(req.body.recurrence_rule, 'Wiederholung'));
const errors = collectErrors(checks);
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
const {
title, description, start_datetime, end_datetime,
all_day, location, color, assigned_to, recurrence_rule,
all_day, location, color: colorVal, assigned_to, recurrence_rule,
} = req.body;
if (title !== undefined && !title.trim())
return res.status(400).json({ error: 'Titel darf nicht leer sein', code: 400 });
if (start_datetime !== undefined && !DATETIME_RE.test(start_datetime))
return res.status(400).json({ error: 'Ungültiges start_datetime', code: 400 });
if (end_datetime !== undefined && end_datetime && !DATETIME_RE.test(end_datetime))
return res.status(400).json({ error: 'Ungültiges end_datetime', code: 400 });
if (color !== undefined && !COLOR_RE.test(color))
return res.status(400).json({ error: 'Farbe muss #RRGGBB sein', code: 400 });
db.get().prepare(`
UPDATE calendar_events
SET title = COALESCE(?, title),
@@ -362,7 +353,7 @@ router.put('/:id', (req, res) => {
end_datetime !== undefined ? (end_datetime || null) : event.end_datetime,
all_day !== undefined ? (all_day ? 1 : 0) : null,
location !== undefined ? (location || null) : event.location,
color ?? null,
colorVal ?? null,
assigned_to !== undefined ? (assigned_to || null) : event.assigned_to,
recurrence_rule !== undefined ? (recurrence_rule || null) : event.recurrence_rule,
id
+26 -21
View File
@@ -9,6 +9,7 @@
const express = require('express');
const router = express.Router();
const db = require('../db');
const { str, oneOf, collectErrors, MAX_TITLE, MAX_TEXT, MAX_SHORT } = require('../middleware/validate');
const VALID_CATEGORIES = ['Arzt', 'Schule/Kita', 'Behörde', 'Versicherung',
'Handwerker', 'Notfall', 'Sonstiges'];
@@ -55,21 +56,20 @@ router.get('/', (req, res) => {
*/
router.post('/', (req, res) => {
try {
const {
name, category = 'Sonstiges',
phone = null, email = null, address = null, notes = null,
} = req.body;
if (!name || !name.trim())
return res.status(400).json({ error: 'Name ist erforderlich', code: 400 });
if (!VALID_CATEGORIES.includes(category))
return res.status(400).json({ error: `Ungültige Kategorie: ${category}`, code: 400 });
const vName = str(req.body.name, 'Name', { max: MAX_TITLE });
const vCat = oneOf(req.body.category || 'Sonstiges', VALID_CATEGORIES, 'Kategorie');
const vPhone = str(req.body.phone, 'Telefon', { max: MAX_SHORT, required: false });
const vEmail = str(req.body.email, 'E-Mail', { max: MAX_TITLE, required: false });
const vAddress = str(req.body.address, 'Adresse', { max: MAX_TEXT, required: false });
const vNotes = str(req.body.notes, 'Notizen', { max: MAX_TEXT, required: false });
const errors = collectErrors([vName, vCat, vPhone, vEmail, vAddress, vNotes]);
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
const result = db.get().prepare(`
INSERT INTO contacts (name, category, phone, email, address, notes)
VALUES (?, ?, ?, ?, ?, ?)
`).run(name.trim(), category, phone || null, email || null,
address || null, notes || null);
`).run(vName.value, vCat.value || 'Sonstiges', vPhone.value, vEmail.value,
vAddress.value, vNotes.value);
const contact = db.get().prepare('SELECT * FROM contacts WHERE id = ?').get(result.lastInsertRowid);
res.status(201).json({ data: contact });
@@ -91,10 +91,15 @@ router.put('/:id', (req, res) => {
const contact = db.get().prepare('SELECT * FROM contacts WHERE id = ?').get(id);
if (!contact) return res.status(404).json({ error: 'Kontakt nicht gefunden', code: 404 });
const { name, category, phone, email, address, notes } = req.body;
if (category !== undefined && !VALID_CATEGORIES.includes(category))
return res.status(400).json({ error: `Ungültige Kategorie: ${category}`, code: 400 });
const checks = [];
if (req.body.name !== undefined) checks.push(str(req.body.name, 'Name', { max: MAX_TITLE, required: false }));
if (req.body.category !== undefined) checks.push(oneOf(req.body.category, VALID_CATEGORIES, 'Kategorie'));
if (req.body.phone !== undefined) checks.push(str(req.body.phone, 'Telefon', { max: MAX_SHORT, required: false }));
if (req.body.email !== undefined) checks.push(str(req.body.email, 'E-Mail', { max: MAX_TITLE, required: false }));
if (req.body.address !== undefined) checks.push(str(req.body.address, 'Adresse', { max: MAX_TEXT, required: false }));
if (req.body.notes !== undefined) checks.push(str(req.body.notes, 'Notizen', { max: MAX_TEXT, required: false }));
const errors = collectErrors(checks);
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
db.get().prepare(`
UPDATE contacts
@@ -106,12 +111,12 @@ router.put('/:id', (req, res) => {
notes = ?
WHERE id = ?
`).run(
name?.trim() ?? null,
category ?? null,
phone !== undefined ? (phone || null) : contact.phone,
email !== undefined ? (email || null) : contact.email,
address !== undefined ? (address || null) : contact.address,
notes !== undefined ? (notes || null) : contact.notes,
req.body.name?.trim() ?? null,
req.body.category ?? null,
req.body.phone !== undefined ? (req.body.phone?.trim() || null) : contact.phone,
req.body.email !== undefined ? (req.body.email?.trim() || null) : contact.email,
req.body.address !== undefined ? (req.body.address?.trim() || null) : contact.address,
req.body.notes !== undefined ? (req.body.notes?.trim() || null) : contact.notes,
id
);
+24 -23
View File
@@ -9,9 +9,9 @@
const express = require('express');
const router = express.Router();
const db = require('../db');
const { str, oneOf, date, collectErrors, MAX_TITLE, MAX_TEXT, MAX_SHORT } = require('../middleware/validate');
const VALID_MEAL_TYPES = ['breakfast', 'lunch', 'dinner', 'snack'];
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
// --------------------------------------------------------
// Hilfsfunktionen
@@ -148,20 +148,20 @@ router.get('/', (req, res) => {
*/
router.post('/', (req, res) => {
try {
const { date, meal_type, title, notes = null, ingredients = [] } = req.body;
if (!date || !DATE_RE.test(date))
return res.status(400).json({ error: 'Gültiges Datum (YYYY-MM-DD) erforderlich', code: 400 });
if (!meal_type || !VALID_MEAL_TYPES.includes(meal_type))
return res.status(400).json({ error: `meal_type muss einer von: ${VALID_MEAL_TYPES.join(', ')} sein`, code: 400 });
if (!title || !title.trim())
return res.status(400).json({ error: 'Titel ist erforderlich', code: 400 });
const { ingredients = [] } = req.body;
const vDate = date(req.body.date, 'Datum', true);
const vType = oneOf(req.body.meal_type, VALID_MEAL_TYPES, 'Mahlzeit-Typ');
const vTitle = str(req.body.title, 'Titel', { max: MAX_TITLE });
const vNotes = str(req.body.notes, 'Notizen', { max: MAX_TEXT, required: false });
const errors = collectErrors([vDate, vType, vTitle, vNotes]);
if (!req.body.meal_type) errors.push('Mahlzeit-Typ ist erforderlich.');
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
const meal = db.transaction(() => {
const result = db.get().prepare(`
INSERT INTO meals (date, meal_type, title, notes, created_by)
VALUES (?, ?, ?, ?, ?)
`).run(date, meal_type, title.trim(), notes || null, req.session.userId);
`).run(vDate.value, vType.value, vTitle.value, vNotes.value, req.session.userId);
const mealId = result.lastInsertRowid;
@@ -170,9 +170,9 @@ router.post('/', (req, res) => {
`);
for (const ing of ingredients) {
if (ing.name && ing.name.trim()) {
insertIng.run(mealId, ing.name.trim(), ing.quantity?.trim() || null);
}
const name = String(ing.name || '').trim().slice(0, MAX_TITLE);
const qty = String(ing.quantity || '').trim().slice(0, MAX_SHORT) || null;
if (name) insertIng.run(mealId, name, qty);
}
return db.get().prepare(`
@@ -207,12 +207,13 @@ router.put('/:id', (req, res) => {
const meal = db.get().prepare('SELECT * FROM meals WHERE id = ?').get(id);
if (!meal) return res.status(404).json({ error: 'Mahlzeit nicht gefunden', code: 404 });
const { date, meal_type, title, notes } = req.body;
if (date !== undefined && !DATE_RE.test(date))
return res.status(400).json({ error: 'Ungültiges Datum', code: 400 });
if (meal_type !== undefined && !VALID_MEAL_TYPES.includes(meal_type))
return res.status(400).json({ error: 'Ungültiger meal_type', code: 400 });
const checks = [];
if (req.body.date !== undefined) checks.push(date(req.body.date, 'Datum'));
if (req.body.meal_type !== undefined) checks.push(oneOf(req.body.meal_type, VALID_MEAL_TYPES, 'Mahlzeit-Typ'));
if (req.body.title !== undefined) checks.push(str(req.body.title, 'Titel', { max: MAX_TITLE, required: false }));
if (req.body.notes !== undefined) checks.push(str(req.body.notes, 'Notizen', { max: MAX_TEXT, required: false }));
const errors = collectErrors(checks);
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
db.get().prepare(`
UPDATE meals
@@ -222,10 +223,10 @@ router.put('/:id', (req, res) => {
notes = ?
WHERE id = ?
`).run(
date ?? null,
meal_type ?? null,
title?.trim() ?? null,
notes !== undefined ? (notes || null) : meal.notes,
req.body.date ?? null,
req.body.meal_type ?? null,
req.body.title?.trim() ?? null,
req.body.notes !== undefined ? (req.body.notes || null) : meal.notes,
id
);
+18 -16
View File
@@ -9,8 +9,7 @@
const express = require('express');
const router = express.Router();
const db = require('../db');
const COLOR_RE = /^#[0-9A-Fa-f]{6}$/;
const { str, color, collectErrors, MAX_TEXT, MAX_TITLE } = require('../middleware/validate');
/**
* GET /api/v1/notes
@@ -40,17 +39,17 @@ router.get('/', (req, res) => {
*/
router.post('/', (req, res) => {
try {
const { content, title = null, color = '#FFEB3B', pinned = 0 } = req.body;
if (!content || !content.trim())
return res.status(400).json({ error: 'Inhalt ist erforderlich', code: 400 });
if (!COLOR_RE.test(color))
return res.status(400).json({ error: 'Farbe muss #RRGGBB sein', code: 400 });
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(content.trim(), title?.trim() || null, color, pinned ? 1 : 0, req.session.userId);
`).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
@@ -77,10 +76,13 @@ router.put('/:id', (req, res) => {
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 { content, title, color, pinned } = req.body;
if (color !== undefined && !COLOR_RE.test(color))
return res.status(400).json({ error: 'Farbe muss #RRGGBB sein', code: 400 });
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
@@ -90,9 +92,9 @@ router.put('/:id', (req, res) => {
pinned = COALESCE(?, pinned)
WHERE id = ?
`).run(
content?.trim() ?? null,
title !== undefined ? (title?.trim() || null) : note.title,
color ?? null,
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
);
+13 -14
View File
@@ -12,6 +12,7 @@
const express = require('express');
const router = express.Router();
const db = require('../db');
const { str, oneOf, collectErrors, MAX_TITLE, MAX_SHORT } = require('../middleware/validate');
// --------------------------------------------------------
// Konstanten
@@ -137,12 +138,12 @@ router.get('/', (req, res) => {
// --------------------------------------------------------
router.post('/', (req, res) => {
try {
const name = req.body.name?.trim();
if (!name) return res.status(400).json({ error: 'name ist erforderlich.', code: 400 });
const vName = str(req.body.name, 'Name', { max: MAX_TITLE });
if (vName.error) return res.status(400).json({ error: vName.error, code: 400 });
const result = db.get()
.prepare('INSERT INTO shopping_lists (name, created_by) VALUES (?, ?)')
.run(name, req.session.userId);
.run(vName.value, req.session.userId);
const list = db.get()
.prepare('SELECT * FROM shopping_lists WHERE id = ?')
@@ -162,12 +163,12 @@ router.post('/', (req, res) => {
// --------------------------------------------------------
router.put('/:listId', (req, res) => {
try {
const name = req.body.name?.trim();
if (!name) return res.status(400).json({ error: 'name ist erforderlich.', code: 400 });
const vName = str(req.body.name, 'Name', { max: MAX_TITLE });
if (vName.error) return res.status(400).json({ error: vName.error, code: 400 });
const result = db.get()
.prepare('UPDATE shopping_lists SET name = ? WHERE id = ?')
.run(name, req.params.listId);
.run(vName.value, req.params.listId);
if (result.changes === 0)
return res.status(404).json({ error: 'Liste nicht gefunden.', code: 404 });
@@ -244,18 +245,16 @@ router.post('/:listId/items', (req, res) => {
.get(req.params.listId);
if (!list) return res.status(404).json({ error: 'Liste nicht gefunden.', code: 404 });
const name = req.body.name?.trim();
const quantity = req.body.quantity?.trim() || null;
const category = req.body.category || 'Sonstiges';
if (!name) return res.status(400).json({ error: 'name ist erforderlich.', code: 400 });
if (!ITEM_CATEGORIES.includes(category))
return res.status(400).json({ error: 'Ungültige Kategorie.', code: 400 });
const vName = str(req.body.name, 'Name', { max: MAX_TITLE });
const vQty = str(req.body.quantity, 'Menge', { max: MAX_SHORT, required: false });
const vCat = oneOf(req.body.category || 'Sonstiges', ITEM_CATEGORIES, 'Kategorie');
const errors = collectErrors([vName, vQty, vCat]);
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
const result = db.get().prepare(`
INSERT INTO shopping_items (list_id, name, quantity, category)
VALUES (?, ?, ?, ?)
`).run(req.params.listId, name, quantity, category);
`).run(req.params.listId, vName.value, vQty.value, vCat.value || 'Sonstiges');
const item = db.get()
.prepare('SELECT * FROM shopping_items WHERE id = ?')
+1
View File
@@ -58,6 +58,7 @@ function validateTaskInput(body, isCreate = true) {
v.oneOf(body.category, VALID_CATEGORIES, 'category'),
v.date(body.due_date, 'due_date'),
v.time(body.due_time, 'due_time'),
v.rrule(body.recurrence_rule, 'recurrence_rule'),
]);
}