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:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 = ?')
|
||||
|
||||
@@ -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'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user