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_TITLE = 200;
const MAX_TEXT = 5000; const MAX_TEXT = 5000;
const MAX_SHORT = 100; 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. * Bereinigt und validiert einen Pflicht-String.
@@ -103,4 +112,54 @@ function collectErrors(results) {
return results.map((r) => r.error).filter(Boolean); 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 express = require('express');
const router = express.Router(); const router = express.Router();
const db = require('../db'); const db = require('../db');
const { str, oneOf, date, num, rrule, collectErrors, MAX_TITLE, MONTH_RE } = require('../middleware/validate');
const VALID_CATEGORIES = [ const VALID_CATEGORIES = [
'Lebensmittel', 'Miete', 'Versicherung', 'Mobilität', 'Lebensmittel', 'Miete', 'Versicherung', 'Mobilität',
'Freizeit', 'Kleidung', 'Gesundheit', 'Bildung', 'Sonstiges', 'Freizeit', 'Kleidung', 'Gesundheit', 'Bildung', 'Sonstiges',
]; ];
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
// -------------------------------------------------------- // --------------------------------------------------------
// Statische Routen vor /:id // Statische Routen vor /:id
@@ -31,7 +31,7 @@ router.get('/summary', (req, res) => {
const today = new Date().toISOString().slice(0, 7); // YYYY-MM const today = new Date().toISOString().slice(0, 7); // YYYY-MM
const month = req.query.month || today; 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 }); return res.status(400).json({ error: 'month muss YYYY-MM sein', code: 400 });
const from = `${month}-01`; const from = `${month}-01`;
@@ -83,7 +83,7 @@ router.get('/export', (req, res) => {
const today = new Date().toISOString().slice(0, 7); const today = new Date().toISOString().slice(0, 7);
const month = req.query.month || today; 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 }); return res.status(400).json({ error: 'month muss YYYY-MM sein', code: 400 });
const from = `${month}-01`; const from = `${month}-01`;
@@ -141,7 +141,7 @@ router.get('/', (req, res) => {
const today = new Date().toISOString().slice(0, 7); const today = new Date().toISOString().slice(0, 7);
const month = req.query.month || today; 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 }); return res.status(400).json({ error: 'month muss YYYY-MM sein', code: 400 });
const from = `${month}-01`; const from = `${month}-01`;
@@ -177,26 +177,20 @@ router.get('/', (req, res) => {
*/ */
router.post('/', (req, res) => { router.post('/', (req, res) => {
try { try {
const { const vTitle = str(req.body.title, 'Titel', { max: MAX_TITLE });
title, amount, category = 'Sonstiges', const vAmount = num(req.body.amount, 'Betrag', { required: true });
date, is_recurring = 0, recurrence_rule = null, const vCat = oneOf(req.body.category || 'Sonstiges', VALID_CATEGORIES, 'Kategorie');
} = req.body; const vDate = date(req.body.date, 'Datum', true);
const vRrule = rrule(req.body.recurrence_rule, 'Wiederholung');
if (!title || !title.trim()) const errors = collectErrors([vTitle, vAmount, vCat, vDate, vRrule]);
return res.status(400).json({ error: 'Titel ist erforderlich', code: 400 }); if (errors.length) return res.status(400).json({ error: errors.join(' '), 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 result = db.get().prepare(` const result = db.get().prepare(`
INSERT INTO budget_entries (title, amount, category, date, is_recurring, recurrence_rule, created_by) INSERT INTO budget_entries (title, amount, category, date, is_recurring, recurrence_rule, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
`).run( `).run(
title.trim(), Number(amount), category, date, vTitle.value, vAmount.value, vCat.value || 'Sonstiges', vDate.value,
is_recurring ? 1 : 0, recurrence_rule || null, req.body.is_recurring ? 1 : 0, vRrule.value,
req.session.userId 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); 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 }); if (!entry) return res.status(404).json({ error: 'Eintrag nicht gefunden', code: 404 });
const { title, amount, category, date, is_recurring, recurrence_rule } = req.body; const checks = [];
if (req.body.title !== undefined) checks.push(str(req.body.title, 'Titel', { max: MAX_TITLE, required: false }));
if (amount !== undefined && isNaN(Number(amount))) if (req.body.amount !== undefined) checks.push(num(req.body.amount, 'Betrag'));
return res.status(400).json({ error: 'Betrag muss eine Zahl sein', code: 400 }); if (req.body.category !== undefined) checks.push(oneOf(req.body.category, VALID_CATEGORIES, 'Kategorie'));
if (date !== undefined && !DATE_RE.test(date)) if (req.body.date !== undefined) checks.push(date(req.body.date, 'Datum'));
return res.status(400).json({ error: 'Ungültiges Datum', code: 400 }); if (req.body.recurrence_rule !== undefined) checks.push(rrule(req.body.recurrence_rule, 'Wiederholung'));
if (category !== undefined && !VALID_CATEGORIES.includes(category)) const errors = collectErrors(checks);
return res.status(400).json({ error: `Ungültige Kategorie: ${category}`, code: 400 }); 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(` db.get().prepare(`
UPDATE budget_entries UPDATE budget_entries
+29 -38
View File
@@ -13,11 +13,9 @@ const db = require('../db');
const googleCalendar = require('../services/google-calendar'); const googleCalendar = require('../services/google-calendar');
const appleCalendar = require('../services/apple-calendar'); const appleCalendar = require('../services/apple-calendar');
const { requireAdmin } = require('../auth'); 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 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 // GET /api/v1/calendar
@@ -260,26 +258,17 @@ router.get('/:id', (req, res) => {
// -------------------------------------------------------- // --------------------------------------------------------
router.post('/', (req, res) => { router.post('/', (req, res) => {
try { try {
const { const vTitle = str(req.body.title, 'Titel', { max: MAX_TITLE });
title, const vDesc = str(req.body.description, 'Beschreibung', { max: MAX_TEXT, required: false });
description = null, const vStart = datetime(req.body.start_datetime, 'Startdatum', true);
start_datetime, const vEnd = datetime(req.body.end_datetime, 'Enddatum');
end_datetime = null, const vColor = color(req.body.color || '#007AFF', 'Farbe');
all_day = 0, const vLoc = str(req.body.location, 'Ort', { max: MAX_TITLE, required: false });
location = null, const vRrule = rrule(req.body.recurrence_rule, 'Wiederholung');
color = '#007AFF', const errors = collectErrors([vTitle, vDesc, vStart, vEnd, vColor, vLoc, vRrule]);
assigned_to = null, if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
recurrence_rule = null,
} = req.body;
if (!title || !title.trim()) const { all_day = 0, assigned_to = null } = req.body;
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 });
if (assigned_to) { if (assigned_to) {
const user = db.get().prepare('SELECT id FROM users WHERE id = ?').get(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) location, color, assigned_to, created_by, recurrence_rule)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run( `).run(
title.trim(), description || null, vTitle.value, vDesc.value,
start_datetime, end_datetime || null, vStart.value, vEnd.value,
all_day ? 1 : 0, location || null, all_day ? 1 : 0, vLoc.value,
color, assigned_to || null, vColor.value, assigned_to || null,
req.session.userId, recurrence_rule || null req.session.userId, vRrule.value
); );
const event = db.get().prepare(` 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); 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 }); 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 { const {
title, description, start_datetime, end_datetime, 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; } = 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(` db.get().prepare(`
UPDATE calendar_events UPDATE calendar_events
SET title = COALESCE(?, title), SET title = COALESCE(?, title),
@@ -362,7 +353,7 @@ router.put('/:id', (req, res) => {
end_datetime !== undefined ? (end_datetime || null) : event.end_datetime, end_datetime !== undefined ? (end_datetime || null) : event.end_datetime,
all_day !== undefined ? (all_day ? 1 : 0) : null, all_day !== undefined ? (all_day ? 1 : 0) : null,
location !== undefined ? (location || null) : event.location, location !== undefined ? (location || null) : event.location,
color ?? null, colorVal ?? null,
assigned_to !== undefined ? (assigned_to || null) : event.assigned_to, assigned_to !== undefined ? (assigned_to || null) : event.assigned_to,
recurrence_rule !== undefined ? (recurrence_rule || null) : event.recurrence_rule, recurrence_rule !== undefined ? (recurrence_rule || null) : event.recurrence_rule,
id id
+26 -21
View File
@@ -9,6 +9,7 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const db = require('../db'); 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', const VALID_CATEGORIES = ['Arzt', 'Schule/Kita', 'Behörde', 'Versicherung',
'Handwerker', 'Notfall', 'Sonstiges']; 'Handwerker', 'Notfall', 'Sonstiges'];
@@ -55,21 +56,20 @@ router.get('/', (req, res) => {
*/ */
router.post('/', (req, res) => { router.post('/', (req, res) => {
try { try {
const { const vName = str(req.body.name, 'Name', { max: MAX_TITLE });
name, category = 'Sonstiges', const vCat = oneOf(req.body.category || 'Sonstiges', VALID_CATEGORIES, 'Kategorie');
phone = null, email = null, address = null, notes = null, const vPhone = str(req.body.phone, 'Telefon', { max: MAX_SHORT, required: false });
} = req.body; 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 });
if (!name || !name.trim()) const vNotes = str(req.body.notes, 'Notizen', { max: MAX_TEXT, required: false });
return res.status(400).json({ error: 'Name ist erforderlich', code: 400 }); const errors = collectErrors([vName, vCat, vPhone, vEmail, vAddress, vNotes]);
if (!VALID_CATEGORIES.includes(category)) if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
return res.status(400).json({ error: `Ungültige Kategorie: ${category}`, code: 400 });
const result = db.get().prepare(` const result = db.get().prepare(`
INSERT INTO contacts (name, category, phone, email, address, notes) INSERT INTO contacts (name, category, phone, email, address, notes)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?)
`).run(name.trim(), category, phone || null, email || null, `).run(vName.value, vCat.value || 'Sonstiges', vPhone.value, vEmail.value,
address || null, notes || null); vAddress.value, vNotes.value);
const contact = db.get().prepare('SELECT * FROM contacts WHERE id = ?').get(result.lastInsertRowid); const contact = db.get().prepare('SELECT * FROM contacts WHERE id = ?').get(result.lastInsertRowid);
res.status(201).json({ data: contact }); 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); 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 }); if (!contact) return res.status(404).json({ error: 'Kontakt nicht gefunden', code: 404 });
const { name, category, phone, email, address, notes } = req.body; const checks = [];
if (req.body.name !== undefined) checks.push(str(req.body.name, 'Name', { max: MAX_TITLE, required: false }));
if (category !== undefined && !VALID_CATEGORIES.includes(category)) if (req.body.category !== undefined) checks.push(oneOf(req.body.category, VALID_CATEGORIES, 'Kategorie'));
return res.status(400).json({ error: `Ungültige Kategorie: ${category}`, code: 400 }); 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(` db.get().prepare(`
UPDATE contacts UPDATE contacts
@@ -106,12 +111,12 @@ router.put('/:id', (req, res) => {
notes = ? notes = ?
WHERE id = ? WHERE id = ?
`).run( `).run(
name?.trim() ?? null, req.body.name?.trim() ?? null,
category ?? null, req.body.category ?? null,
phone !== undefined ? (phone || null) : contact.phone, req.body.phone !== undefined ? (req.body.phone?.trim() || null) : contact.phone,
email !== undefined ? (email || null) : contact.email, req.body.email !== undefined ? (req.body.email?.trim() || null) : contact.email,
address !== undefined ? (address || null) : contact.address, req.body.address !== undefined ? (req.body.address?.trim() || null) : contact.address,
notes !== undefined ? (notes || null) : contact.notes, req.body.notes !== undefined ? (req.body.notes?.trim() || null) : contact.notes,
id id
); );
+24 -23
View File
@@ -9,9 +9,9 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const db = require('../db'); 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 VALID_MEAL_TYPES = ['breakfast', 'lunch', 'dinner', 'snack'];
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
// -------------------------------------------------------- // --------------------------------------------------------
// Hilfsfunktionen // Hilfsfunktionen
@@ -148,20 +148,20 @@ router.get('/', (req, res) => {
*/ */
router.post('/', (req, res) => { router.post('/', (req, res) => {
try { try {
const { date, meal_type, title, notes = null, ingredients = [] } = req.body; const { ingredients = [] } = req.body;
const vDate = date(req.body.date, 'Datum', true);
if (!date || !DATE_RE.test(date)) const vType = oneOf(req.body.meal_type, VALID_MEAL_TYPES, 'Mahlzeit-Typ');
return res.status(400).json({ error: 'Gültiges Datum (YYYY-MM-DD) erforderlich', code: 400 }); const vTitle = str(req.body.title, 'Titel', { max: MAX_TITLE });
if (!meal_type || !VALID_MEAL_TYPES.includes(meal_type)) const vNotes = str(req.body.notes, 'Notizen', { max: MAX_TEXT, required: false });
return res.status(400).json({ error: `meal_type muss einer von: ${VALID_MEAL_TYPES.join(', ')} sein`, code: 400 }); const errors = collectErrors([vDate, vType, vTitle, vNotes]);
if (!title || !title.trim()) if (!req.body.meal_type) errors.push('Mahlzeit-Typ ist erforderlich.');
return res.status(400).json({ error: 'Titel ist erforderlich', code: 400 }); if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
const meal = db.transaction(() => { const meal = db.transaction(() => {
const result = db.get().prepare(` const result = db.get().prepare(`
INSERT INTO meals (date, meal_type, title, notes, created_by) INSERT INTO meals (date, meal_type, title, notes, created_by)
VALUES (?, ?, ?, ?, ?) 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; const mealId = result.lastInsertRowid;
@@ -170,9 +170,9 @@ router.post('/', (req, res) => {
`); `);
for (const ing of ingredients) { for (const ing of ingredients) {
if (ing.name && ing.name.trim()) { const name = String(ing.name || '').trim().slice(0, MAX_TITLE);
insertIng.run(mealId, ing.name.trim(), ing.quantity?.trim() || null); const qty = String(ing.quantity || '').trim().slice(0, MAX_SHORT) || null;
} if (name) insertIng.run(mealId, name, qty);
} }
return db.get().prepare(` 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); 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 }); if (!meal) return res.status(404).json({ error: 'Mahlzeit nicht gefunden', code: 404 });
const { date, meal_type, title, notes } = req.body; const checks = [];
if (req.body.date !== undefined) checks.push(date(req.body.date, 'Datum'));
if (date !== undefined && !DATE_RE.test(date)) if (req.body.meal_type !== undefined) checks.push(oneOf(req.body.meal_type, VALID_MEAL_TYPES, 'Mahlzeit-Typ'));
return res.status(400).json({ error: 'Ungültiges Datum', code: 400 }); if (req.body.title !== undefined) checks.push(str(req.body.title, 'Titel', { max: MAX_TITLE, required: false }));
if (meal_type !== undefined && !VALID_MEAL_TYPES.includes(meal_type)) if (req.body.notes !== undefined) checks.push(str(req.body.notes, 'Notizen', { max: MAX_TEXT, required: false }));
return res.status(400).json({ error: 'Ungültiger meal_type', code: 400 }); const errors = collectErrors(checks);
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
db.get().prepare(` db.get().prepare(`
UPDATE meals UPDATE meals
@@ -222,10 +223,10 @@ router.put('/:id', (req, res) => {
notes = ? notes = ?
WHERE id = ? WHERE id = ?
`).run( `).run(
date ?? null, req.body.date ?? null,
meal_type ?? null, req.body.meal_type ?? null,
title?.trim() ?? null, req.body.title?.trim() ?? null,
notes !== undefined ? (notes || null) : meal.notes, req.body.notes !== undefined ? (req.body.notes || null) : meal.notes,
id id
); );
+18 -16
View File
@@ -9,8 +9,7 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const db = require('../db'); const db = require('../db');
const { str, color, collectErrors, MAX_TEXT, MAX_TITLE } = require('../middleware/validate');
const COLOR_RE = /^#[0-9A-Fa-f]{6}$/;
/** /**
* GET /api/v1/notes * GET /api/v1/notes
@@ -40,17 +39,17 @@ router.get('/', (req, res) => {
*/ */
router.post('/', (req, res) => { router.post('/', (req, res) => {
try { try {
const { content, title = null, color = '#FFEB3B', pinned = 0 } = req.body; const { pinned = 0 } = req.body;
const vContent = str(req.body.content, 'Inhalt', { max: MAX_TEXT });
if (!content || !content.trim()) const vTitle = str(req.body.title, 'Titel', { max: MAX_TITLE, required: false });
return res.status(400).json({ error: 'Inhalt ist erforderlich', code: 400 }); const vColor = color(req.body.color || '#FFEB3B', 'Farbe');
if (!COLOR_RE.test(color)) const errors = collectErrors([vContent, vTitle, vColor]);
return res.status(400).json({ error: 'Farbe muss #RRGGBB sein', code: 400 }); if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
const result = db.get().prepare(` const result = db.get().prepare(`
INSERT INTO notes (content, title, color, pinned, created_by) INSERT INTO notes (content, title, color, pinned, created_by)
VALUES (?, ?, ?, ?, ?) 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(` const note = db.get().prepare(`
SELECT n.*, u.display_name AS creator_name, u.avatar_color AS creator_color 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); 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 }); if (!note) return res.status(404).json({ error: 'Notiz nicht gefunden', code: 404 });
const { content, title, color, pinned } = req.body; const { pinned } = req.body;
const checks = [];
if (color !== undefined && !COLOR_RE.test(color)) if (req.body.content !== undefined) checks.push(str(req.body.content, 'Inhalt', { max: MAX_TEXT, required: false }));
return res.status(400).json({ error: 'Farbe muss #RRGGBB sein', code: 400 }); 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(` db.get().prepare(`
UPDATE notes UPDATE notes
@@ -90,9 +92,9 @@ router.put('/:id', (req, res) => {
pinned = COALESCE(?, pinned) pinned = COALESCE(?, pinned)
WHERE id = ? WHERE id = ?
`).run( `).run(
content?.trim() ?? null, req.body.content?.trim() ?? null,
title !== undefined ? (title?.trim() || null) : note.title, req.body.title !== undefined ? (req.body.title?.trim() || null) : note.title,
color ?? null, req.body.color ?? null,
pinned !== undefined ? (pinned ? 1 : 0) : null, pinned !== undefined ? (pinned ? 1 : 0) : null,
id id
); );
+13 -14
View File
@@ -12,6 +12,7 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const db = require('../db'); const db = require('../db');
const { str, oneOf, collectErrors, MAX_TITLE, MAX_SHORT } = require('../middleware/validate');
// -------------------------------------------------------- // --------------------------------------------------------
// Konstanten // Konstanten
@@ -137,12 +138,12 @@ router.get('/', (req, res) => {
// -------------------------------------------------------- // --------------------------------------------------------
router.post('/', (req, res) => { router.post('/', (req, res) => {
try { try {
const name = req.body.name?.trim(); const vName = str(req.body.name, 'Name', { max: MAX_TITLE });
if (!name) return res.status(400).json({ error: 'name ist erforderlich.', code: 400 }); if (vName.error) return res.status(400).json({ error: vName.error, code: 400 });
const result = db.get() const result = db.get()
.prepare('INSERT INTO shopping_lists (name, created_by) VALUES (?, ?)') .prepare('INSERT INTO shopping_lists (name, created_by) VALUES (?, ?)')
.run(name, req.session.userId); .run(vName.value, req.session.userId);
const list = db.get() const list = db.get()
.prepare('SELECT * FROM shopping_lists WHERE id = ?') .prepare('SELECT * FROM shopping_lists WHERE id = ?')
@@ -162,12 +163,12 @@ router.post('/', (req, res) => {
// -------------------------------------------------------- // --------------------------------------------------------
router.put('/:listId', (req, res) => { router.put('/:listId', (req, res) => {
try { try {
const name = req.body.name?.trim(); const vName = str(req.body.name, 'Name', { max: MAX_TITLE });
if (!name) return res.status(400).json({ error: 'name ist erforderlich.', code: 400 }); if (vName.error) return res.status(400).json({ error: vName.error, code: 400 });
const result = db.get() const result = db.get()
.prepare('UPDATE shopping_lists SET name = ? WHERE id = ?') .prepare('UPDATE shopping_lists SET name = ? WHERE id = ?')
.run(name, req.params.listId); .run(vName.value, req.params.listId);
if (result.changes === 0) if (result.changes === 0)
return res.status(404).json({ error: 'Liste nicht gefunden.', code: 404 }); 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); .get(req.params.listId);
if (!list) return res.status(404).json({ error: 'Liste nicht gefunden.', code: 404 }); if (!list) return res.status(404).json({ error: 'Liste nicht gefunden.', code: 404 });
const name = req.body.name?.trim(); const vName = str(req.body.name, 'Name', { max: MAX_TITLE });
const quantity = req.body.quantity?.trim() || null; const vQty = str(req.body.quantity, 'Menge', { max: MAX_SHORT, required: false });
const category = req.body.category || 'Sonstiges'; const vCat = oneOf(req.body.category || 'Sonstiges', ITEM_CATEGORIES, 'Kategorie');
const errors = collectErrors([vName, vQty, vCat]);
if (!name) return res.status(400).json({ error: 'name ist erforderlich.', code: 400 }); if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
if (!ITEM_CATEGORIES.includes(category))
return res.status(400).json({ error: 'Ungültige Kategorie.', code: 400 });
const result = db.get().prepare(` const result = db.get().prepare(`
INSERT INTO shopping_items (list_id, name, quantity, category) INSERT INTO shopping_items (list_id, name, quantity, category)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?)
`).run(req.params.listId, name, quantity, category); `).run(req.params.listId, vName.value, vQty.value, vCat.value || 'Sonstiges');
const item = db.get() const item = db.get()
.prepare('SELECT * FROM shopping_items WHERE id = ?') .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.oneOf(body.category, VALID_CATEGORIES, 'category'),
v.date(body.due_date, 'due_date'), v.date(body.due_date, 'due_date'),
v.time(body.due_time, 'due_time'), v.time(body.due_time, 'due_time'),
v.rrule(body.recurrence_rule, 'recurrence_rule'),
]); ]);
} }