From a787667dcb31eb9415fd04fe3fab00bd13579f05 Mon Sep 17 00:00:00 2001 From: ulsklyc Date: Thu, 26 Mar 2026 00:23:57 +0100 Subject: [PATCH] fix: Input-Validation auf allen API-Routen vereinheitlichen (Phase 5, Schritt 27) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- server/middleware/validate.js | 61 ++++++++++++++++++++++++++++++- server/routes/budget.js | 49 ++++++++++++------------- server/routes/calendar.js | 67 +++++++++++++++-------------------- server/routes/contacts.js | 47 +++++++++++++----------- server/routes/meals.js | 47 ++++++++++++------------ server/routes/notes.js | 34 +++++++++--------- server/routes/shopping.js | 27 +++++++------- server/routes/tasks.js | 1 + 8 files changed, 193 insertions(+), 140 deletions(-) diff --git a/server/middleware/validate.js b/server/middleware/validate.js index 0ea37e0..838f3f1 100644 --- a/server/middleware/validate.js +++ b/server/middleware/validate.js @@ -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, +}; diff --git a/server/routes/budget.js b/server/routes/budget.js index aa1a5e9..c1a7d02 100644 --- a/server/routes/budget.js +++ b/server/routes/budget.js @@ -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 diff --git a/server/routes/calendar.js b/server/routes/calendar.js index 82f2a60..347561e 100644 --- a/server/routes/calendar.js +++ b/server/routes/calendar.js @@ -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 diff --git a/server/routes/contacts.js b/server/routes/contacts.js index 5f823dc..ff45016 100644 --- a/server/routes/contacts.js +++ b/server/routes/contacts.js @@ -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 ); diff --git a/server/routes/meals.js b/server/routes/meals.js index 822b99f..8c2a8c4 100644 --- a/server/routes/meals.js +++ b/server/routes/meals.js @@ -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 ); diff --git a/server/routes/notes.js b/server/routes/notes.js index 78e8fa9..5e95f03 100644 --- a/server/routes/notes.js +++ b/server/routes/notes.js @@ -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 ); diff --git a/server/routes/shopping.js b/server/routes/shopping.js index 078e88f..3b2c671 100644 --- a/server/routes/shopping.js +++ b/server/routes/shopping.js @@ -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 = ?') diff --git a/server/routes/tasks.js b/server/routes/tasks.js index c392edb..412f318 100644 --- a/server/routes/tasks.js +++ b/server/routes/tasks.js @@ -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'), ]); }