Replacing entire backend messages (especially logs) with English instead of Germany
This commit is contained in:
+42
-42
@@ -136,7 +136,7 @@ function requireAuth(req, res, next) {
|
|||||||
if (req.session && req.session.userId) {
|
if (req.session && req.session.userId) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
res.status(401).json({ error: 'Nicht authentifiziert.', code: 401 });
|
res.status(401).json({ error: 'Not authenticated.', code: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -146,7 +146,7 @@ function requireAdmin(req, res, next) {
|
|||||||
if (req.session && req.session.role === 'admin') {
|
if (req.session && req.session.role === 'admin') {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
res.status(403).json({ error: 'Keine Berechtigung.', code: 403 });
|
res.status(403).json({ error: 'Permission denied.', code: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -165,11 +165,11 @@ router.post('/login', loginLimiter, async (req, res) => {
|
|||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
|
|
||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
return res.status(400).json({ error: 'Benutzername und Passwort erforderlich.', code: 400 });
|
return res.status(400).json({ error: 'Username and password are required.', code: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (username.length > 64 || password.length > 1024) {
|
if (username.length > 64 || password.length > 1024) {
|
||||||
return res.status(400).json({ error: 'Eingabe zu lang.', code: 400 });
|
return res.status(400).json({ error: 'Input is too long.', code: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = db.get().prepare('SELECT * FROM users WHERE username = ?').get(username);
|
const user = db.get().prepare('SELECT * FROM users WHERE username = ?').get(username);
|
||||||
@@ -177,18 +177,18 @@ router.post('/login', loginLimiter, async (req, res) => {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
// Timing-Attack-Schutz: trotzdem bcrypt ausführen
|
// Timing-Attack-Schutz: trotzdem bcrypt ausführen
|
||||||
await bcrypt.compare(password, '$2b$12$invalidhashfortimingprotection000000000000000000000');
|
await bcrypt.compare(password, '$2b$12$invalidhashfortimingprotection000000000000000000000');
|
||||||
return res.status(401).json({ error: 'Ungültige Anmeldedaten.', code: 401 });
|
return res.status(401).json({ error: 'Invalid credentials.', code: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const valid = await bcrypt.compare(password, user.password_hash);
|
const valid = await bcrypt.compare(password, user.password_hash);
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
return res.status(401).json({ error: 'Ungültige Anmeldedaten.', code: 401 });
|
return res.status(401).json({ error: 'Invalid credentials.', code: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
req.session.regenerate((err) => {
|
req.session.regenerate((err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.error('Session-Regenerierung fehlgeschlagen:', err);
|
log.error('Session regeneration failed:', err);
|
||||||
return res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
return res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
req.session.userId = user.id;
|
req.session.userId = user.id;
|
||||||
@@ -215,8 +215,8 @@ router.post('/login', loginLimiter, async (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('Login-Fehler:', err);
|
log.error('Login error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -227,8 +227,8 @@ router.post('/login', loginLimiter, async (req, res) => {
|
|||||||
router.post('/logout', requireAuth, csrfMiddleware, (req, res) => {
|
router.post('/logout', requireAuth, csrfMiddleware, (req, res) => {
|
||||||
req.session.destroy((err) => {
|
req.session.destroy((err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.error('Logout-Fehler:', err);
|
log.error('Logout error:', err);
|
||||||
return res.status(500).json({ error: 'Logout fehlgeschlagen.', code: 500 });
|
return res.status(500).json({ error: 'Logout failed.', code: 500 });
|
||||||
}
|
}
|
||||||
res.clearCookie('oikos.sid');
|
res.clearCookie('oikos.sid');
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
@@ -246,7 +246,7 @@ router.post('/setup', loginLimiter, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { count } = db.get().prepare('SELECT COUNT(*) as count FROM users').get();
|
const { count } = db.get().prepare('SELECT COUNT(*) as count FROM users').get();
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
return res.status(403).json({ error: 'Setup bereits abgeschlossen.', code: 403 });
|
return res.status(403).json({ error: 'Setup has already been completed.', code: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const username = (req.body.username || '').trim();
|
const username = (req.body.username || '').trim();
|
||||||
@@ -254,16 +254,16 @@ router.post('/setup', loginLimiter, async (req, res) => {
|
|||||||
const { password } = req.body;
|
const { password } = req.body;
|
||||||
|
|
||||||
if (!username || !display_name || !password) {
|
if (!username || !display_name || !password) {
|
||||||
return res.status(400).json({ error: 'Benutzername, Anzeigename und Passwort erforderlich.', code: 400 });
|
return res.status(400).json({ error: 'Username, display name, and password are required.', code: 400 });
|
||||||
}
|
}
|
||||||
if (!/^[a-zA-Z0-9._-]{3,64}$/.test(username)) {
|
if (!/^[a-zA-Z0-9._-]{3,64}$/.test(username)) {
|
||||||
return res.status(400).json({ error: 'Benutzername muss 3-64 Zeichen lang sein und darf nur Buchstaben, Zahlen, Punkte, Bindestriche und Unterstriche enthalten.', code: 400 });
|
return res.status(400).json({ error: 'Username must be 3-64 characters long and may only contain letters, numbers, dots, hyphens, and underscores.', code: 400 });
|
||||||
}
|
}
|
||||||
if (display_name.length > 128) {
|
if (display_name.length > 128) {
|
||||||
return res.status(400).json({ error: 'Anzeigename darf maximal 128 Zeichen lang sein.', code: 400 });
|
return res.status(400).json({ error: 'Display name may be at most 128 characters long.', code: 400 });
|
||||||
}
|
}
|
||||||
if (password.length < 8) {
|
if (password.length < 8) {
|
||||||
return res.status(400).json({ error: 'Passwort muss mindestens 8 Zeichen haben.', code: 400 });
|
return res.status(400).json({ error: 'Password must be at least 8 characters long.', code: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const avatarColor = avatarColors[Math.floor(Math.random() * avatarColors.length)];
|
const avatarColor = avatarColors[Math.floor(Math.random() * avatarColors.length)];
|
||||||
@@ -278,10 +278,10 @@ router.post('/setup', loginLimiter, async (req, res) => {
|
|||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.message?.includes('UNIQUE constraint')) {
|
if (err.message?.includes('UNIQUE constraint')) {
|
||||||
return res.status(409).json({ error: 'Benutzername bereits vergeben.', code: 409 });
|
return res.status(409).json({ error: 'Username is already taken.', code: 409 });
|
||||||
}
|
}
|
||||||
log.error('Setup error:', err);
|
log.error('Setup error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -297,7 +297,7 @@ router.get('/me', requireAuth, (req, res) => {
|
|||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
req.session.destroy(() => {});
|
req.session.destroy(() => {});
|
||||||
return res.status(401).json({ error: 'Benutzer nicht gefunden.', code: 401 });
|
return res.status(401).json({ error: 'User not found.', code: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// CSRF-Token erneuern falls vorhanden (wichtig fuer iOS-PWA-Resume:
|
// CSRF-Token erneuern falls vorhanden (wichtig fuer iOS-PWA-Resume:
|
||||||
@@ -315,8 +315,8 @@ router.get('/me', requireAuth, (req, res) => {
|
|||||||
|
|
||||||
res.json({ user, csrfToken: req.session.csrfToken });
|
res.json({ user, csrfToken: req.session.csrfToken });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('/me Fehler:', err);
|
log.error('/me error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -332,8 +332,8 @@ router.get('/users', requireAuth, requireAdmin, (req, res) => {
|
|||||||
.all();
|
.all();
|
||||||
res.json({ data: users });
|
res.json({ data: users });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('Users-Fehler:', err);
|
log.error('Users error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -348,23 +348,23 @@ router.post('/users', requireAuth, requireAdmin, csrfMiddleware, async (req, res
|
|||||||
const { username, display_name, password, avatar_color = '#007AFF', role = 'member' } = req.body;
|
const { username, display_name, password, avatar_color = '#007AFF', role = 'member' } = req.body;
|
||||||
|
|
||||||
if (!username || !display_name || !password) {
|
if (!username || !display_name || !password) {
|
||||||
return res.status(400).json({ error: 'Benutzername, Anzeigename und Passwort erforderlich.', code: 400 });
|
return res.status(400).json({ error: 'Username, display name, and password are required.', code: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (password.length < 8) {
|
if (password.length < 8) {
|
||||||
return res.status(400).json({ error: 'Passwort muss mindestens 8 Zeichen haben.', code: 400 });
|
return res.status(400).json({ error: 'Password must be at least 8 characters long.', code: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!/^[a-zA-Z0-9._-]{3,64}$/.test(username)) {
|
if (!/^[a-zA-Z0-9._-]{3,64}$/.test(username)) {
|
||||||
return res.status(400).json({ error: 'Benutzername muss 3-64 Zeichen lang sein und darf nur Buchstaben, Zahlen, Punkte, Bindestriche und Unterstriche enthalten.', code: 400 });
|
return res.status(400).json({ error: 'Username must be 3-64 characters long and may only contain letters, numbers, dots, hyphens, and underscores.', code: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (display_name.length > 128) {
|
if (display_name.length > 128) {
|
||||||
return res.status(400).json({ error: 'Anzeigename darf maximal 128 Zeichen lang sein.', code: 400 });
|
return res.status(400).json({ error: 'Display name may be at most 128 characters long.', code: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!['admin', 'member'].includes(role)) {
|
if (!['admin', 'member'].includes(role)) {
|
||||||
return res.status(400).json({ error: 'Ungültige Rolle.', code: 400 });
|
return res.status(400).json({ error: 'Invalid role.', code: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const hash = await bcrypt.hash(password, 12);
|
const hash = await bcrypt.hash(password, 12);
|
||||||
@@ -381,10 +381,10 @@ router.post('/users', requireAuth, requireAdmin, csrfMiddleware, async (req, res
|
|||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.message && err.message.includes('UNIQUE constraint')) {
|
if (err.message && err.message.includes('UNIQUE constraint')) {
|
||||||
return res.status(409).json({ error: 'Benutzername bereits vergeben.', code: 409 });
|
return res.status(409).json({ error: 'Username is already taken.', code: 409 });
|
||||||
}
|
}
|
||||||
log.error('User-Erstellen-Fehler:', err);
|
log.error('User creation error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -399,17 +399,17 @@ router.patch('/me/password', requireAuth, csrfMiddleware, async (req, res) => {
|
|||||||
const { current_password, new_password } = req.body;
|
const { current_password, new_password } = req.body;
|
||||||
|
|
||||||
if (!current_password || !new_password) {
|
if (!current_password || !new_password) {
|
||||||
return res.status(400).json({ error: 'Aktuelles und neues Passwort erforderlich.', code: 400 });
|
return res.status(400).json({ error: 'Current and new password are required.', code: 400 });
|
||||||
}
|
}
|
||||||
if (new_password.length < 8) {
|
if (new_password.length < 8) {
|
||||||
return res.status(400).json({ error: 'Neues Passwort muss mindestens 8 Zeichen haben.', code: 400 });
|
return res.status(400).json({ error: 'New password must be at least 8 characters long.', code: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = db.get().prepare('SELECT password_hash FROM users WHERE id = ?').get(req.session.userId);
|
const user = db.get().prepare('SELECT password_hash FROM users WHERE id = ?').get(req.session.userId);
|
||||||
if (!user) return res.status(404).json({ error: 'Benutzer nicht gefunden.', code: 404 });
|
if (!user) return res.status(404).json({ error: 'User not found.', code: 404 });
|
||||||
|
|
||||||
const valid = await bcrypt.compare(current_password, user.password_hash);
|
const valid = await bcrypt.compare(current_password, user.password_hash);
|
||||||
if (!valid) return res.status(401).json({ error: 'Aktuelles Passwort falsch.', code: 401 });
|
if (!valid) return res.status(401).json({ error: 'Current password is incorrect.', code: 401 });
|
||||||
|
|
||||||
const hash = await bcrypt.hash(new_password, 12);
|
const hash = await bcrypt.hash(new_password, 12);
|
||||||
db.get().prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(hash, req.session.userId);
|
db.get().prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(hash, req.session.userId);
|
||||||
@@ -429,8 +429,8 @@ router.patch('/me/password', requireAuth, csrfMiddleware, async (req, res) => {
|
|||||||
|
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('Passwort-Aendern-Fehler:', err);
|
log.error('Password change error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -444,13 +444,13 @@ router.delete('/users/:id', requireAuth, requireAdmin, csrfMiddleware, (req, res
|
|||||||
const userId = parseInt(req.params.id, 10);
|
const userId = parseInt(req.params.id, 10);
|
||||||
|
|
||||||
if (userId === req.session.userId) {
|
if (userId === req.session.userId) {
|
||||||
return res.status(400).json({ error: 'Eigenes Konto kann nicht gelöscht werden.', code: 400 });
|
return res.status(400).json({ error: 'You cannot delete your own account.', code: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = db.get().prepare('DELETE FROM users WHERE id = ?').run(userId);
|
const result = db.get().prepare('DELETE FROM users WHERE id = ?').run(userId);
|
||||||
|
|
||||||
if (result.changes === 0) {
|
if (result.changes === 0) {
|
||||||
return res.status(404).json({ error: 'Benutzer nicht gefunden.', code: 404 });
|
return res.status(404).json({ error: 'User not found.', code: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alle aktiven Sessions des geloeschten Users invalidieren
|
// Alle aktiven Sessions des geloeschten Users invalidieren
|
||||||
@@ -466,8 +466,8 @@ router.delete('/users/:id', requireAuth, requireAdmin, csrfMiddleware, (req, res
|
|||||||
|
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('User-Loeschen-Fehler:', err);
|
log.error('User deletion error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+19
-19
@@ -41,7 +41,7 @@ function init() {
|
|||||||
try {
|
try {
|
||||||
db.prepare('SELECT count(*) FROM sqlite_master').get();
|
db.prepare('SELECT count(*) FROM sqlite_master').get();
|
||||||
} catch {
|
} catch {
|
||||||
throw new Error('[DB] Falscher Verschlüsselungsschlüssel oder keine SQLCipher-Unterstützung.');
|
throw new Error('[DB] Wrong encryption key or SQLCipher support is unavailable.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ function init() {
|
|||||||
|
|
||||||
migrate();
|
migrate();
|
||||||
|
|
||||||
log.info(`Verbunden: ${DB_PATH} | Schema v${currentVersion()}`);
|
log.info(`Connected: ${DB_PATH} | Schema v${currentVersion()}`);
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ function init() {
|
|||||||
const MIGRATIONS = [
|
const MIGRATIONS = [
|
||||||
{
|
{
|
||||||
version: 1,
|
version: 1,
|
||||||
description: 'Initiales Schema',
|
description: 'Initial schema',
|
||||||
up: `
|
up: `
|
||||||
-- Benutzer
|
-- Benutzer
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
@@ -269,7 +269,7 @@ const MIGRATIONS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
version: 2,
|
version: 2,
|
||||||
description: 'Sync-Konfigurationstabelle für Google/Apple Calendar',
|
description: 'Sync configuration table for Google/Apple Calendar',
|
||||||
up: `
|
up: `
|
||||||
CREATE TABLE IF NOT EXISTS sync_config (
|
CREATE TABLE IF NOT EXISTS sync_config (
|
||||||
key TEXT PRIMARY KEY,
|
key TEXT PRIMARY KEY,
|
||||||
@@ -282,7 +282,7 @@ const MIGRATIONS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
version: 3,
|
version: 3,
|
||||||
description: 'Wiederkehrende Budget-Einträge: parent-Referenz und Skip-Tabelle',
|
description: 'Recurring budget entries: parent reference and skip table',
|
||||||
up: `
|
up: `
|
||||||
ALTER TABLE budget_entries ADD COLUMN recurrence_parent_id INTEGER
|
ALTER TABLE budget_entries ADD COLUMN recurrence_parent_id INTEGER
|
||||||
REFERENCES budget_entries(id) ON DELETE SET NULL;
|
REFERENCES budget_entries(id) ON DELETE SET NULL;
|
||||||
@@ -298,7 +298,7 @@ const MIGRATIONS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
version: 4,
|
version: 4,
|
||||||
description: 'Priorität "none" erlauben und als Default setzen',
|
description: 'Allow "none" priority and set it as default',
|
||||||
up: `
|
up: `
|
||||||
-- SQLite erlaubt kein ALTER CHECK, daher Tabelle neu erstellen
|
-- SQLite erlaubt kein ALTER CHECK, daher Tabelle neu erstellen
|
||||||
CREATE TABLE tasks_new (
|
CREATE TABLE tasks_new (
|
||||||
@@ -333,7 +333,7 @@ const MIGRATIONS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
version: 5,
|
version: 5,
|
||||||
description: 'Einkaufskategorien als eigene Tabelle (anpassbar, sortierbar)',
|
description: 'Shopping categories as a separate table (customizable, sortable)',
|
||||||
up: `
|
up: `
|
||||||
CREATE TABLE IF NOT EXISTS shopping_categories (
|
CREATE TABLE IF NOT EXISTS shopping_categories (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -357,21 +357,21 @@ const MIGRATIONS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
version: 6,
|
version: 6,
|
||||||
description: 'Rezept-URL für Mahlzeiten',
|
description: 'Recipe URL for meals',
|
||||||
up: `
|
up: `
|
||||||
ALTER TABLE meals ADD COLUMN recipe_url TEXT;
|
ALTER TABLE meals ADD COLUMN recipe_url TEXT;
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
version: 7,
|
version: 7,
|
||||||
description: 'Kategorie pro Zutat für Einkaufslisten-Transfer',
|
description: 'Category per ingredient for shopping list transfer',
|
||||||
up: `
|
up: `
|
||||||
ALTER TABLE meal_ingredients ADD COLUMN category TEXT NOT NULL DEFAULT 'Sonstiges';
|
ALTER TABLE meal_ingredients ADD COLUMN category TEXT NOT NULL DEFAULT 'Sonstiges';
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
version: 8,
|
version: 8,
|
||||||
description: 'Erinnerungen (Reminders) für Aufgaben und Kalender-Events',
|
description: 'Reminders for tasks and calendar events',
|
||||||
up: `
|
up: `
|
||||||
CREATE TABLE IF NOT EXISTS reminders (
|
CREATE TABLE IF NOT EXISTS reminders (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -390,7 +390,7 @@ const MIGRATIONS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
version: 9,
|
version: 9,
|
||||||
description: 'Task-Kategorien auf englische Schlüssel migrieren',
|
description: 'Migrate task categories to English keys',
|
||||||
up: `
|
up: `
|
||||||
UPDATE tasks SET category = CASE category
|
UPDATE tasks SET category = CASE category
|
||||||
WHEN 'Haushalt' THEN 'household'
|
WHEN 'Haushalt' THEN 'household'
|
||||||
@@ -407,7 +407,7 @@ const MIGRATIONS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
version: 10,
|
version: 10,
|
||||||
description: 'ICS-Abonnements Tabelle',
|
description: 'ICS subscriptions table',
|
||||||
up: `
|
up: `
|
||||||
CREATE TABLE IF NOT EXISTS ics_subscriptions (
|
CREATE TABLE IF NOT EXISTS ics_subscriptions (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -477,7 +477,7 @@ const MIGRATIONS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
version: 12,
|
version: 12,
|
||||||
description: 'calendar_events: partiellen Unique-Index durch vollständigen ersetzen (ON CONFLICT support)',
|
description: 'calendar_events: replace partial unique index with full index (ON CONFLICT support)',
|
||||||
up: `
|
up: `
|
||||||
DROP INDEX IF EXISTS idx_calendar_sub_extid;
|
DROP INDEX IF EXISTS idx_calendar_sub_extid;
|
||||||
CREATE UNIQUE INDEX idx_calendar_sub_extid
|
CREATE UNIQUE INDEX idx_calendar_sub_extid
|
||||||
@@ -486,7 +486,7 @@ const MIGRATIONS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
version: 13,
|
version: 13,
|
||||||
description: 'Rezepte-Tabelle und Mahlzeiten-Verknuepfung',
|
description: 'Recipes table and meal association',
|
||||||
up: `
|
up: `
|
||||||
CREATE TABLE IF NOT EXISTS recipes (
|
CREATE TABLE IF NOT EXISTS recipes (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -525,7 +525,7 @@ const MIGRATIONS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
version: 14,
|
version: 14,
|
||||||
description: 'Externe Kalender-Metadaten (Name, Farbe) und Verknüpfung mit Events',
|
description: 'External calendar metadata (name, color) and event association',
|
||||||
up: `
|
up: `
|
||||||
CREATE TABLE IF NOT EXISTS external_calendars (
|
CREATE TABLE IF NOT EXISTS external_calendars (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -546,7 +546,7 @@ const MIGRATIONS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
version: 15,
|
version: 15,
|
||||||
description: 'Budget-Ausgabenkategorien als stabile Schlüssel mit Unterkategorien',
|
description: 'Budget expense categories as stable keys with subcategories',
|
||||||
up: `
|
up: `
|
||||||
ALTER TABLE budget_entries ADD COLUMN subcategory TEXT NOT NULL DEFAULT '';
|
ALTER TABLE budget_entries ADD COLUMN subcategory TEXT NOT NULL DEFAULT '';
|
||||||
|
|
||||||
@@ -586,7 +586,7 @@ const MIGRATIONS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
version: 16,
|
version: 16,
|
||||||
description: 'Budget-Kategorien und Unterkategorien in eigene Tabellen auslagern',
|
description: 'Move budget categories and subcategories to separate tables',
|
||||||
up: `
|
up: `
|
||||||
CREATE TABLE IF NOT EXISTS budget_categories (
|
CREATE TABLE IF NOT EXISTS budget_categories (
|
||||||
key TEXT PRIMARY KEY,
|
key TEXT PRIMARY KEY,
|
||||||
@@ -698,7 +698,7 @@ function migrate() {
|
|||||||
db.exec(migration.up);
|
db.exec(migration.up);
|
||||||
db.prepare('INSERT INTO schema_migrations (version, description) VALUES (?, ?)')
|
db.prepare('INSERT INTO schema_migrations (version, description) VALUES (?, ?)')
|
||||||
.run(migration.version, migration.description);
|
.run(migration.version, migration.description);
|
||||||
log.info(`Migration ${migration.version} angewendet: ${migration.description}`);
|
log.info(`Migration ${migration.version} applied: ${migration.description}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const migration of pending) {
|
for (const migration of pending) {
|
||||||
@@ -729,7 +729,7 @@ function currentVersion() {
|
|||||||
* @returns {import('better-sqlite3').Database}
|
* @returns {import('better-sqlite3').Database}
|
||||||
*/
|
*/
|
||||||
function get() {
|
function get() {
|
||||||
if (!db) throw new Error('[DB] Nicht initialisiert - init() zuerst aufrufen.');
|
if (!db) throw new Error('[DB] Not initialized - call init() first.');
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-13
@@ -90,10 +90,10 @@ app.use(express.urlencoded({ extended: true, limit: '1mb' }));
|
|||||||
// JSON-Parse-Fehler abfangen (gibt sonst HTML zurück)
|
// JSON-Parse-Fehler abfangen (gibt sonst HTML zurück)
|
||||||
app.use((err, req, res, next) => {
|
app.use((err, req, res, next) => {
|
||||||
if (err.type === 'entity.parse.failed') {
|
if (err.type === 'entity.parse.failed') {
|
||||||
return res.status(400).json({ error: 'Ungültiges JSON im Request-Body.', code: 400 });
|
return res.status(400).json({ error: 'Invalid JSON in request body.', code: 400 });
|
||||||
}
|
}
|
||||||
if (err.type === 'entity.too.large') {
|
if (err.type === 'entity.too.large') {
|
||||||
return res.status(413).json({ error: 'Request-Body zu groß (max. 1 MB).', code: 413 });
|
return res.status(413).json({ error: 'Request body too large (max. 1 MB).', code: 413 });
|
||||||
}
|
}
|
||||||
next(err);
|
next(err);
|
||||||
});
|
});
|
||||||
@@ -151,7 +151,7 @@ const apiLimiter = rateLimit({
|
|||||||
max: 300, // 300 Requests/Minute pro IP (großzügig für Familien-App)
|
max: 300, // 300 Requests/Minute pro IP (großzügig für Familien-App)
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false,
|
legacyHeaders: false,
|
||||||
message: { error: 'Zu viele Anfragen. Bitte warte kurz.', code: 429 },
|
message: { error: 'Too many requests. Please wait a moment.', code: 429 },
|
||||||
skip: (req) => req.path === '/health', // Health-Check ausgenommen
|
skip: (req) => req.path === '/health', // Health-Check ausgenommen
|
||||||
});
|
});
|
||||||
app.use('/api/', apiLimiter);
|
app.use('/api/', apiLimiter);
|
||||||
@@ -198,7 +198,7 @@ const spaLimiter = rateLimit({
|
|||||||
max: 200,
|
max: 200,
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false,
|
legacyHeaders: false,
|
||||||
message: { error: 'Zu viele Anfragen. Bitte warte kurz.', code: 429 },
|
message: { error: 'Too many requests. Please wait a moment.', code: 429 },
|
||||||
});
|
});
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -206,7 +206,7 @@ const spaLimiter = rateLimit({
|
|||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
app.get('/{*path}', spaLimiter, (req, res) => {
|
app.get('/{*path}', spaLimiter, (req, res) => {
|
||||||
if (req.path.startsWith('/api/')) {
|
if (req.path.startsWith('/api/')) {
|
||||||
return res.status(404).json({ error: 'Nicht gefunden.', code: 404 });
|
return res.status(404).json({ error: 'Not found.', code: 404 });
|
||||||
}
|
}
|
||||||
res.sendFile(path.join(import.meta.dirname, '..', 'public', 'index.html'));
|
res.sendFile(path.join(import.meta.dirname, '..', 'public', 'index.html'));
|
||||||
});
|
});
|
||||||
@@ -215,8 +215,8 @@ app.get('/{*path}', spaLimiter, (req, res) => {
|
|||||||
// Globaler Error-Handler
|
// Globaler Error-Handler
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
app.use((err, req, res, _next) => {
|
app.use((err, req, res, _next) => {
|
||||||
log.error('Unbehandelter Fehler:', err);
|
log.error('Unhandled error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
});
|
});
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -228,30 +228,30 @@ const SYNC_INTERVAL_MS = (parseInt(process.env.SYNC_INTERVAL_MINUTES, 10) || 15)
|
|||||||
async function runSync() {
|
async function runSync() {
|
||||||
const { connected: googleConnected } = googleCalendar.getStatus();
|
const { connected: googleConnected } = googleCalendar.getStatus();
|
||||||
if (googleConnected) {
|
if (googleConnected) {
|
||||||
googleCalendar.sync().catch((e) => logSync.error('Google Fehler:', e.message));
|
googleCalendar.sync().catch((e) => logSync.error('Google error:', e.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
const { configured: appleConfigured } = appleCalendar.getStatus();
|
const { configured: appleConfigured } = appleCalendar.getStatus();
|
||||||
if (appleConfigured) {
|
if (appleConfigured) {
|
||||||
appleCalendar.sync().catch((e) => logSync.error('Apple Fehler:', e.message));
|
appleCalendar.sync().catch((e) => logSync.error('Apple error:', e.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ICS: kein Guard nötig — sync() fragt die DB ab und kehrt sofort zurück wenn keine Abonnements existieren
|
// ICS: kein Guard nötig — sync() fragt die DB ab und kehrt sofort zurück wenn keine Abonnements existieren
|
||||||
icsSubscription.sync().catch((e) => logSync.error('ICS Fehler:', e.message));
|
icsSubscription.sync().catch((e) => logSync.error('ICS error:', e.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// Server starten
|
// Server starten
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
logOikos.info(`Server laeuft auf Port ${PORT}`);
|
logOikos.info(`Server running on port ${PORT}`);
|
||||||
logOikos.info(`Umgebung: ${process.env.NODE_ENV || 'development'}`);
|
logOikos.info(`Environment: ${process.env.NODE_ENV || 'development'}`);
|
||||||
|
|
||||||
// Erster Sync nach 10 Sekunden (warten bis DB vollständig initialisiert)
|
// Erster Sync nach 10 Sekunden (warten bis DB vollständig initialisiert)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
runSync();
|
runSync();
|
||||||
setInterval(runSync, SYNC_INTERVAL_MS);
|
setInterval(runSync, SYNC_INTERVAL_MS);
|
||||||
logSync.info(`Auto-Sync alle ${SYNC_INTERVAL_MS / 60_000} Minuten aktiv.`);
|
logSync.info(`Auto-sync active every ${SYNC_INTERVAL_MS / 60_000} minutes.`);
|
||||||
}, 10_000);
|
}, 10_000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ function csrfMiddleware(req, res, next) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!tokenValid) {
|
if (!tokenValid) {
|
||||||
return res.status(403).json({ error: 'Ungültiges CSRF-Token.', code: 403 });
|
return res.status(403).json({ error: 'Invalid CSRF token.', code: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
next();
|
next();
|
||||||
|
|||||||
@@ -29,12 +29,12 @@ const RRULE_RE = /^(FREQ=(DAILY|WEEKLY|MONTHLY)(;INTERVAL=\d{1,2})?(;BYDAY=[A
|
|||||||
*/
|
*/
|
||||||
function str(val, field, { max = MAX_TITLE, required = true } = {}) {
|
function str(val, field, { max = MAX_TITLE, required = true } = {}) {
|
||||||
if (val === undefined || val === null || val === '') {
|
if (val === undefined || val === null || val === '') {
|
||||||
if (required) return { value: null, error: `${field} ist erforderlich.` };
|
if (required) return { value: null, error: `${field} is required.` };
|
||||||
return { value: null, error: null };
|
return { value: null, error: null };
|
||||||
}
|
}
|
||||||
const s = String(val).trim();
|
const s = String(val).trim();
|
||||||
if (required && !s) return { value: null, error: `${field} darf nicht leer sein.` };
|
if (required && !s) return { value: null, error: `${field} must not be empty.` };
|
||||||
if (s.length > max) return { value: null, error: `${field} darf maximal ${max} Zeichen haben.` };
|
if (s.length > max) return { value: null, error: `${field} may be at most ${max} characters long.` };
|
||||||
return { value: s || null, error: null };
|
return { value: s || null, error: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ function str(val, field, { max = MAX_TITLE, required = true } = {}) {
|
|||||||
function oneOf(val, allowed, field) {
|
function oneOf(val, allowed, field) {
|
||||||
if (val === undefined || val === null || val === '') return { value: null, error: null };
|
if (val === undefined || val === null || val === '') return { value: null, error: null };
|
||||||
if (!allowed.includes(val))
|
if (!allowed.includes(val))
|
||||||
return { value: null, error: `${field} muss eines von: ${allowed.join(', ')} sein.` };
|
return { value: null, error: `${field} must be one of: ${allowed.join(', ')}.` };
|
||||||
return { value: val, error: null };
|
return { value: val, error: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,11 +60,11 @@ function oneOf(val, allowed, field) {
|
|||||||
*/
|
*/
|
||||||
function date(val, field, required = false) {
|
function date(val, field, required = false) {
|
||||||
if (!val) {
|
if (!val) {
|
||||||
if (required) return { value: null, error: `${field} ist erforderlich.` };
|
if (required) return { value: null, error: `${field} is required.` };
|
||||||
return { value: null, error: null };
|
return { value: null, error: null };
|
||||||
}
|
}
|
||||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(String(val)))
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(String(val)))
|
||||||
return { value: null, error: `${field} muss im Format YYYY-MM-DD sein.` };
|
return { value: null, error: `${field} must be in YYYY-MM-DD format.` };
|
||||||
return { value: String(val), error: null };
|
return { value: String(val), error: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ function date(val, field, required = false) {
|
|||||||
function time(val, field) {
|
function time(val, field) {
|
||||||
if (!val) return { value: null, error: null };
|
if (!val) return { value: null, error: null };
|
||||||
if (!/^\d{2}:\d{2}$/.test(String(val)))
|
if (!/^\d{2}:\d{2}$/.test(String(val)))
|
||||||
return { value: null, error: `${field} muss im Format HH:MM sein.` };
|
return { value: null, error: `${field} must be in HH:MM format.` };
|
||||||
return { value: String(val), error: null };
|
return { value: String(val), error: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,11 +83,11 @@ function time(val, field) {
|
|||||||
*/
|
*/
|
||||||
function num(val, field, { required = false } = {}) {
|
function num(val, field, { required = false } = {}) {
|
||||||
if (val === undefined || val === null || val === '') {
|
if (val === undefined || val === null || val === '') {
|
||||||
if (required) return { value: null, error: `${field} ist erforderlich.` };
|
if (required) return { value: null, error: `${field} is required.` };
|
||||||
return { value: null, error: null };
|
return { value: null, error: null };
|
||||||
}
|
}
|
||||||
const n = Number(val);
|
const n = Number(val);
|
||||||
if (!isFinite(n)) return { value: null, error: `${field} muss eine gültige Zahl sein.` };
|
if (!isFinite(n)) return { value: null, error: `${field} must be a valid number.` };
|
||||||
return { value: n, error: null };
|
return { value: n, error: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ function num(val, field, { required = false } = {}) {
|
|||||||
function color(val, field) {
|
function color(val, field) {
|
||||||
if (!val) return { value: null, error: null };
|
if (!val) return { value: null, error: null };
|
||||||
if (!/^#[0-9A-Fa-f]{6}$/.test(String(val)))
|
if (!/^#[0-9A-Fa-f]{6}$/.test(String(val)))
|
||||||
return { value: null, error: `${field} muss ein gültiger HEX-Farbwert sein (#RRGGBB).` };
|
return { value: null, error: `${field} must be a valid HEX color (#RRGGBB).` };
|
||||||
return { value: String(val), error: null };
|
return { value: String(val), error: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,11 +115,11 @@ function collectErrors(results) {
|
|||||||
*/
|
*/
|
||||||
function datetime(val, field, required = false) {
|
function datetime(val, field, required = false) {
|
||||||
if (!val) {
|
if (!val) {
|
||||||
if (required) return { value: null, error: `${field} ist erforderlich.` };
|
if (required) return { value: null, error: `${field} is required.` };
|
||||||
return { value: null, error: null };
|
return { value: null, error: null };
|
||||||
}
|
}
|
||||||
if (!DATETIME_RE.test(String(val)))
|
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: null, error: `${field} must be in YYYY-MM-DD or YYYY-MM-DDTHH:MM format.` };
|
||||||
return { value: String(val), error: null };
|
return { value: String(val), error: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ function datetime(val, field, required = false) {
|
|||||||
function month(val, field) {
|
function month(val, field) {
|
||||||
if (!val) return { value: null, error: null };
|
if (!val) return { value: null, error: null };
|
||||||
if (!MONTH_RE.test(String(val)))
|
if (!MONTH_RE.test(String(val)))
|
||||||
return { value: null, error: `${field} muss im Format YYYY-MM sein.` };
|
return { value: null, error: `${field} must be in YYYY-MM format.` };
|
||||||
return { value: String(val), error: null };
|
return { value: String(val), error: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,10 +140,10 @@ function rrule(val, field) {
|
|||||||
if (!val) return { value: null, error: null };
|
if (!val) return { value: null, error: null };
|
||||||
const s = String(val).trim();
|
const s = String(val).trim();
|
||||||
if (s.length > MAX_RRULE)
|
if (s.length > MAX_RRULE)
|
||||||
return { value: null, error: `${field} darf maximal ${MAX_RRULE} Zeichen haben.` };
|
return { value: null, error: `${field} may be at most ${MAX_RRULE} characters long.` };
|
||||||
// Grundlegende Struktur: KEY=VALUE;KEY=VALUE
|
// Grundlegende Struktur: KEY=VALUE;KEY=VALUE
|
||||||
if (!/^FREQ=(DAILY|WEEKLY|MONTHLY)/.test(s))
|
if (!/^FREQ=(DAILY|WEEKLY|MONTHLY)/.test(s))
|
||||||
return { value: null, error: `${field}: Ungültige Wiederholungsregel.` };
|
return { value: null, error: `${field}: invalid recurrence rule.` };
|
||||||
return { value: s, error: null };
|
return { value: s, error: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ function rrule(val, field) {
|
|||||||
*/
|
*/
|
||||||
function id(val, field) {
|
function id(val, field) {
|
||||||
const n = parseInt(val, 10);
|
const n = parseInt(val, 10);
|
||||||
if (!n || n < 1) return { value: null, error: `${field} muss eine positive Zahl sein.` };
|
if (!n || n < 1) return { value: null, error: `${field} must be a positive number.` };
|
||||||
return { value: n, error: null };
|
return { value: n, error: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+19
-19
@@ -191,7 +191,7 @@ router.get('/summary', (req, res) => {
|
|||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('', err);
|
log.error('', err);
|
||||||
res.status(500).json({ error: 'Interner Fehler', code: 500 });
|
res.status(500).json({ error: 'Internal error', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -219,7 +219,7 @@ router.get('/export', (req, res) => {
|
|||||||
ORDER BY b.date ASC
|
ORDER BY b.date ASC
|
||||||
`).all(from, to);
|
`).all(from, to);
|
||||||
|
|
||||||
const header = 'Datum,Titel,Betrag,Kategorie,Unterkategorie,Wiederkehrend,Erstellt von\n';
|
const header = 'Date,Title,Amount,Category,Subcategory,Recurring,Created by\n';
|
||||||
const csvSafe = (val) => {
|
const csvSafe = (val) => {
|
||||||
let s = String(val || '').replace(/"/g, '""');
|
let s = String(val || '').replace(/"/g, '""');
|
||||||
if (/^[=+\-@\t\r]/.test(s)) s = "'" + s;
|
if (/^[=+\-@\t\r]/.test(s)) s = "'" + s;
|
||||||
@@ -232,7 +232,7 @@ router.get('/export', (req, res) => {
|
|||||||
e.amount.toFixed(2).replace('.', ','),
|
e.amount.toFixed(2).replace('.', ','),
|
||||||
e.category,
|
e.category,
|
||||||
e.subcategory || '',
|
e.subcategory || '',
|
||||||
e.is_recurring ? 'Ja' : 'Nein',
|
e.is_recurring ? 'Yes' : 'No',
|
||||||
csvSafe(e.creator_name),
|
csvSafe(e.creator_name),
|
||||||
].join(',')
|
].join(',')
|
||||||
).join('\n');
|
).join('\n');
|
||||||
@@ -242,7 +242,7 @@ router.get('/export', (req, res) => {
|
|||||||
res.send('\uFEFF' + header + rows); // BOM für Excel
|
res.send('\uFEFF' + header + rows); // BOM für Excel
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('', err);
|
log.error('', err);
|
||||||
res.status(500).json({ error: 'Interner Fehler', code: 500 });
|
res.status(500).json({ error: 'Internal error', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -265,7 +265,7 @@ router.post('/categories', (req, res) => {
|
|||||||
const conflict = db.get().prepare(`
|
const conflict = db.get().prepare(`
|
||||||
SELECT key FROM budget_categories WHERE type = ? AND name = ? COLLATE NOCASE
|
SELECT key FROM budget_categories WHERE type = ? AND name = ? COLLATE NOCASE
|
||||||
`).get(vType.value, vName.value);
|
`).get(vType.value, vName.value);
|
||||||
if (conflict) return res.status(409).json({ error: 'Kategorie existiert bereits.', code: 409 });
|
if (conflict) return res.status(409).json({ error: 'Category already exists.', code: 409 });
|
||||||
|
|
||||||
const maxOrder = db.get().prepare(`
|
const maxOrder = db.get().prepare(`
|
||||||
SELECT COALESCE(MAX(sort_order), -1) AS m FROM budget_categories WHERE type = ?
|
SELECT COALESCE(MAX(sort_order), -1) AS m FROM budget_categories WHERE type = ?
|
||||||
@@ -279,8 +279,8 @@ router.post('/categories', (req, res) => {
|
|||||||
const cat = db.get().prepare('SELECT key, name, type, sort_order FROM budget_categories WHERE key = ?').get(key);
|
const cat = db.get().prepare('SELECT key, name, type, sort_order FROM budget_categories WHERE key = ?').get(key);
|
||||||
res.status(201).json({ data: cat });
|
res.status(201).json({ data: cat });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('POST /categories Fehler:', err);
|
log.error('POST /categories error:', err);
|
||||||
res.status(500).json({ error: 'Interner Fehler', code: 500 });
|
res.status(500).json({ error: 'Internal error', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -289,7 +289,7 @@ router.post('/categories/:categoryKey/subcategories', (req, res) => {
|
|||||||
const cat = db.get().prepare(`
|
const cat = db.get().prepare(`
|
||||||
SELECT * FROM budget_categories WHERE key = ? AND type = 'expense'
|
SELECT * FROM budget_categories WHERE key = ? AND type = 'expense'
|
||||||
`).get(req.params.categoryKey);
|
`).get(req.params.categoryKey);
|
||||||
if (!cat) return res.status(404).json({ error: 'Kategorie nicht gefunden.', code: 404 });
|
if (!cat) return res.status(404).json({ error: 'Category not found.', code: 404 });
|
||||||
|
|
||||||
const vName = str(req.body.name, 'Name', { max: MAX_SHORT });
|
const vName = str(req.body.name, 'Name', { max: MAX_SHORT });
|
||||||
if (vName.error) return res.status(400).json({ error: vName.error, code: 400 });
|
if (vName.error) return res.status(400).json({ error: vName.error, code: 400 });
|
||||||
@@ -297,7 +297,7 @@ router.post('/categories/:categoryKey/subcategories', (req, res) => {
|
|||||||
const conflict = db.get().prepare(`
|
const conflict = db.get().prepare(`
|
||||||
SELECT key FROM budget_subcategories WHERE category_key = ? AND name = ? COLLATE NOCASE
|
SELECT key FROM budget_subcategories WHERE category_key = ? AND name = ? COLLATE NOCASE
|
||||||
`).get(cat.key, vName.value);
|
`).get(cat.key, vName.value);
|
||||||
if (conflict) return res.status(409).json({ error: 'Unterkategorie existiert bereits.', code: 409 });
|
if (conflict) return res.status(409).json({ error: 'Subcategory already exists.', code: 409 });
|
||||||
|
|
||||||
const maxOrder = db.get().prepare(`
|
const maxOrder = db.get().prepare(`
|
||||||
SELECT COALESCE(MAX(sort_order), -1) AS m FROM budget_subcategories WHERE category_key = ?
|
SELECT COALESCE(MAX(sort_order), -1) AS m FROM budget_subcategories WHERE category_key = ?
|
||||||
@@ -313,8 +313,8 @@ router.post('/categories/:categoryKey/subcategories', (req, res) => {
|
|||||||
`).get(key);
|
`).get(key);
|
||||||
res.status(201).json({ data: sub });
|
res.status(201).json({ data: sub });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('POST /categories/:categoryKey/subcategories Fehler:', err);
|
log.error('POST /categories/:categoryKey/subcategories error:', err);
|
||||||
res.status(500).json({ error: 'Interner Fehler', code: 500 });
|
res.status(500).json({ error: 'Internal error', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -359,7 +359,7 @@ router.get('/', (req, res) => {
|
|||||||
res.json({ data: entries });
|
res.json({ data: entries });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('', err);
|
log.error('', err);
|
||||||
res.status(500).json({ error: 'Interner Fehler', code: 500 });
|
res.status(500).json({ error: 'Internal error', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -381,7 +381,7 @@ router.post('/', (req, res) => {
|
|||||||
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
|
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
|
||||||
const subcategory = validateSubcategory(vCat.value, req.body.subcategory);
|
const subcategory = validateSubcategory(vCat.value, req.body.subcategory);
|
||||||
if (subcategory === null) {
|
if (subcategory === null) {
|
||||||
return res.status(400).json({ error: 'Ungültige Unterkategorie.', code: 400 });
|
return res.status(400).json({ error: 'Invalid subcategory.', code: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = db.get().prepare(`
|
const result = db.get().prepare(`
|
||||||
@@ -402,7 +402,7 @@ router.post('/', (req, res) => {
|
|||||||
res.status(201).json({ data: entry });
|
res.status(201).json({ data: entry });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('', err);
|
log.error('', err);
|
||||||
res.status(500).json({ error: 'Interner Fehler', code: 500 });
|
res.status(500).json({ error: 'Internal error', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -416,7 +416,7 @@ router.put('/:id', (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const id = parseInt(req.params.id, 10);
|
const id = parseInt(req.params.id, 10);
|
||||||
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: 'Entry not found', code: 404 });
|
||||||
|
|
||||||
const checks = [];
|
const checks = [];
|
||||||
if (req.body.title !== undefined) checks.push(str(req.body.title, 'Titel', { max: MAX_TITLE, required: false }));
|
if (req.body.title !== undefined) checks.push(str(req.body.title, 'Titel', { max: MAX_TITLE, required: false }));
|
||||||
@@ -432,7 +432,7 @@ router.put('/:id', (req, res) => {
|
|||||||
? validateSubcategory(nextCategory, requestedSubcategory ?? entry.subcategory)
|
? validateSubcategory(nextCategory, requestedSubcategory ?? entry.subcategory)
|
||||||
: undefined;
|
: undefined;
|
||||||
if (subcategory === null) {
|
if (subcategory === null) {
|
||||||
return res.status(400).json({ error: 'Ungültige Unterkategorie.', code: 400 });
|
return res.status(400).json({ error: 'Invalid subcategory.', code: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
db.get().prepare(`
|
db.get().prepare(`
|
||||||
@@ -464,7 +464,7 @@ router.put('/:id', (req, res) => {
|
|||||||
res.json({ data: updated });
|
res.json({ data: updated });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('', err);
|
log.error('', err);
|
||||||
res.status(500).json({ error: 'Interner Fehler', code: 500 });
|
res.status(500).json({ error: 'Internal error', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -477,7 +477,7 @@ router.delete('/:id', (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const id = parseInt(req.params.id, 10);
|
const id = parseInt(req.params.id, 10);
|
||||||
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: 'Entry not found', code: 404 });
|
||||||
|
|
||||||
db.get().prepare('DELETE FROM budget_entries WHERE id = ?').run(id);
|
db.get().prepare('DELETE FROM budget_entries WHERE id = ?').run(id);
|
||||||
|
|
||||||
@@ -492,7 +492,7 @@ router.delete('/:id', (req, res) => {
|
|||||||
res.status(204).end();
|
res.status(204).end();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('', err);
|
log.error('', err);
|
||||||
res.status(500).json({ error: 'Interner Fehler', code: 500 });
|
res.status(500).json({ error: 'Internal error', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ router.get('/google/callback', async (req, res) => {
|
|||||||
await googleCalendar.handleCallback(code);
|
await googleCalendar.handleCallback(code);
|
||||||
|
|
||||||
// Initialen Sync im Hintergrund starten (kein await - Redirect soll sofort erfolgen)
|
// Initialen Sync im Hintergrund starten (kein await - Redirect soll sofort erfolgen)
|
||||||
googleCalendar.sync().catch((e) => log.error('Initialer Sync fehlgeschlagen:', e.message));
|
googleCalendar.sync().catch((e) => log.error('Initial sync failed:', e.message));
|
||||||
|
|
||||||
res.redirect('/settings?sync_ok=google');
|
res.redirect('/settings?sync_ok=google');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ router.get('/', (req, res) => {
|
|||||||
LIMIT 5
|
LIMIT 5
|
||||||
`).all(now.toISOString());
|
`).all(now.toISOString());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('upcomingEvents-Fehler:', err.message);
|
log.error('upcomingEvents error:', err.message);
|
||||||
result.upcomingEvents = [];
|
result.upcomingEvents = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ router.get('/', (req, res) => {
|
|||||||
|
|
||||||
result.urgentTasks = allOpen.slice(0, 5);
|
result.urgentTasks = allOpen.slice(0, 5);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('urgentTasks-Fehler:', err.message);
|
log.error('urgentTasks error:', err.message);
|
||||||
result.urgentTasks = [];
|
result.urgentTasks = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ router.get('/', (req, res) => {
|
|||||||
END
|
END
|
||||||
`).all(todayStr, ...visibleTypes);
|
`).all(todayStr, ...visibleTypes);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('todayMeals-Fehler:', err.message);
|
log.error('todayMeals error:', err.message);
|
||||||
result.todayMeals = [];
|
result.todayMeals = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ router.get('/', (req, res) => {
|
|||||||
LIMIT 3
|
LIMIT 3
|
||||||
`).all();
|
`).all();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('pinnedNotes-Fehler:', err.message);
|
log.error('pinnedNotes error:', err.message);
|
||||||
result.pinnedNotes = [];
|
result.pinnedNotes = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@ router.get('/', (req, res) => {
|
|||||||
}
|
}
|
||||||
result.shoppingLists = lists;
|
result.shoppingLists = lists;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('shoppingLists-Fehler:', err.message);
|
log.error('shoppingLists error:', err.message);
|
||||||
result.shoppingLists = [];
|
result.shoppingLists = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,8 +172,8 @@ router.get('/', (req, res) => {
|
|||||||
|
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('Kritischer Fehler:', err.message);
|
log.error('Critical error:', err.message);
|
||||||
res.status(500).json({ error: 'Dashboard konnte nicht geladen werden.', code: 500 });
|
res.status(500).json({ error: 'Dashboard could not be loaded.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+14
-14
@@ -59,8 +59,8 @@ router.get('/', (_req, res) => {
|
|||||||
|
|
||||||
res.json({ data: recipes.map((r) => ({ ...r, ingredients: ingredientMap[r.id] || [] })) });
|
res.json({ data: recipes.map((r) => ({ ...r, ingredients: ingredientMap[r.id] || [] })) });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('GET / Fehler:', err);
|
log.error('GET / error:', err);
|
||||||
res.status(500).json({ error: 'Interner Fehler', code: 500 });
|
res.status(500).json({ error: 'Internal error', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -100,8 +100,8 @@ router.post('/', (req, res) => {
|
|||||||
const created = loadRecipeWithIngredients(recipeId);
|
const created = loadRecipeWithIngredients(recipeId);
|
||||||
res.status(201).json({ data: created });
|
res.status(201).json({ data: created });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('POST / Fehler:', err);
|
log.error('POST / error:', err);
|
||||||
res.status(500).json({ error: 'Interner Fehler', code: 500 });
|
res.status(500).json({ error: 'Internal error', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -111,8 +111,8 @@ router.put('/:id', (req, res) => {
|
|||||||
if (!id) return res.status(400).json({ error: 'Ungueltige Rezept-ID', code: 400 });
|
if (!id) return res.status(400).json({ error: 'Ungueltige Rezept-ID', code: 400 });
|
||||||
|
|
||||||
const existing = db.get().prepare('SELECT id, created_by FROM recipes WHERE id = ?').get(id);
|
const existing = db.get().prepare('SELECT id, created_by FROM recipes WHERE id = ?').get(id);
|
||||||
if (!existing) return res.status(404).json({ error: 'Rezept nicht gefunden', code: 404 });
|
if (!existing) return res.status(404).json({ error: 'Recipe not found', code: 404 });
|
||||||
if (existing.created_by !== req.session.userId) return res.status(403).json({ error: 'Nicht autorisiert.', code: 403 });
|
if (existing.created_by !== req.session.userId) return res.status(403).json({ error: 'Not authorized.', code: 403 });
|
||||||
|
|
||||||
const { ingredients = [] } = req.body;
|
const { ingredients = [] } = req.body;
|
||||||
|
|
||||||
@@ -147,27 +147,27 @@ router.put('/:id', (req, res) => {
|
|||||||
const updated = loadRecipeWithIngredients(id);
|
const updated = loadRecipeWithIngredients(id);
|
||||||
res.json({ data: updated });
|
res.json({ data: updated });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('PUT /:id Fehler:', err);
|
log.error('PUT /:id error:', err);
|
||||||
res.status(500).json({ error: 'Interner Fehler', code: 500 });
|
res.status(500).json({ error: 'Internal error', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.delete('/:id', (req, res) => {
|
router.delete('/:id', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const id = parseInt(req.params.id, 10);
|
const id = parseInt(req.params.id, 10);
|
||||||
if (!id) return res.status(400).json({ error: 'Ungültige Rezept-ID.', code: 400 });
|
if (!id) return res.status(400).json({ error: 'Invalid recipe ID.', code: 400 });
|
||||||
|
|
||||||
const existing = db.get().prepare('SELECT id, created_by FROM recipes WHERE id = ?').get(id);
|
const existing = db.get().prepare('SELECT id, created_by FROM recipes WHERE id = ?').get(id);
|
||||||
if (!existing) return res.status(404).json({ error: 'Rezept nicht gefunden.', code: 404 });
|
if (!existing) return res.status(404).json({ error: 'Recipe not found.', code: 404 });
|
||||||
if (existing.created_by !== req.session.userId) return res.status(403).json({ error: 'Nicht autorisiert.', code: 403 });
|
if (existing.created_by !== req.session.userId) return res.status(403).json({ error: 'Not authorized.', code: 403 });
|
||||||
|
|
||||||
const result = db.get().prepare('DELETE FROM recipes WHERE id = ?').run(id);
|
const result = db.get().prepare('DELETE FROM recipes WHERE id = ?').run(id);
|
||||||
if (result.changes === 0) return res.status(404).json({ error: 'Rezept nicht gefunden', code: 404 });
|
if (result.changes === 0) return res.status(404).json({ error: 'Recipe not found', code: 404 });
|
||||||
|
|
||||||
res.status(204).end();
|
res.status(204).end();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('DELETE /:id Fehler:', err);
|
log.error('DELETE /:id error:', err);
|
||||||
res.status(500).json({ error: 'Interner Fehler', code: 500 });
|
res.status(500).json({ error: 'Internal error', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+12
-12
@@ -41,8 +41,8 @@ router.get('/pending', (req, res) => {
|
|||||||
|
|
||||||
res.json({ data: rows });
|
res.json({ data: rows });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('Fehler beim Laden fälliger Erinnerungen:', err.message);
|
log.error('Error loading due reminders:', err.message);
|
||||||
res.status(500).json({ error: 'Interner Fehler.', code: 500 });
|
res.status(500).json({ error: 'Internal error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -69,8 +69,8 @@ router.get('/', (req, res) => {
|
|||||||
|
|
||||||
res.json({ data: row || null });
|
res.json({ data: row || null });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('Fehler beim Laden der Erinnerung:', err.message);
|
log.error('Error loading reminder:', err.message);
|
||||||
res.status(500).json({ error: 'Interner Fehler.', code: 500 });
|
res.status(500).json({ error: 'Internal error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -115,8 +115,8 @@ router.post('/', (req, res) => {
|
|||||||
const row = db.get().prepare('SELECT * FROM reminders WHERE id = ?').get(result.lastInsertRowid);
|
const row = db.get().prepare('SELECT * FROM reminders WHERE id = ?').get(result.lastInsertRowid);
|
||||||
res.status(201).json({ data: row });
|
res.status(201).json({ data: row });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('Fehler beim Erstellen der Erinnerung:', err.message);
|
log.error('Error creating reminder:', err.message);
|
||||||
res.status(500).json({ error: 'Interner Fehler.', code: 500 });
|
res.status(500).json({ error: 'Internal error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -145,8 +145,8 @@ router.patch('/:id/dismiss', (req, res) => {
|
|||||||
db.get().prepare('UPDATE reminders SET dismissed = 1 WHERE id = ?').run(reminderId);
|
db.get().prepare('UPDATE reminders SET dismissed = 1 WHERE id = ?').run(reminderId);
|
||||||
res.json({ data: { id: reminderId } });
|
res.json({ data: { id: reminderId } });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('Fehler beim Verwerfen der Erinnerung:', err.message);
|
log.error('Error dismissing reminder:', err.message);
|
||||||
res.status(500).json({ error: 'Interner Fehler.', code: 500 });
|
res.status(500).json({ error: 'Internal error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -175,8 +175,8 @@ router.delete('/:id', (req, res) => {
|
|||||||
db.get().prepare('DELETE FROM reminders WHERE id = ?').run(reminderId);
|
db.get().prepare('DELETE FROM reminders WHERE id = ?').run(reminderId);
|
||||||
res.status(204).end();
|
res.status(204).end();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('Fehler beim Löschen der Erinnerung:', err.message);
|
log.error('Error deleting reminder:', err.message);
|
||||||
res.status(500).json({ error: 'Interner Fehler.', code: 500 });
|
res.status(500).json({ error: 'Internal error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -202,8 +202,8 @@ router.delete('/', (req, res) => {
|
|||||||
|
|
||||||
res.status(204).end();
|
res.status(204).end();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('Fehler beim Löschen der Erinnerungen:', err.message);
|
log.error('Error deleting reminders:', err.message);
|
||||||
res.status(500).json({ error: 'Interner Fehler.', code: 500 });
|
res.status(500).json({ error: 'Internal error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+42
-42
@@ -39,8 +39,8 @@ router.get('/categories', (_req, res) => {
|
|||||||
try {
|
try {
|
||||||
res.json({ data: loadCategories() });
|
res.json({ data: loadCategories() });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('GET /categories Fehler:', err);
|
log.error('GET /categories error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ router.post('/categories', (req, res) => {
|
|||||||
const existing = db.get()
|
const existing = db.get()
|
||||||
.prepare('SELECT id FROM shopping_categories WHERE name = ? COLLATE NOCASE')
|
.prepare('SELECT id FROM shopping_categories WHERE name = ? COLLATE NOCASE')
|
||||||
.get(vName.value);
|
.get(vName.value);
|
||||||
if (existing) return res.status(409).json({ error: 'Kategorie existiert bereits.', code: 409 });
|
if (existing) return res.status(409).json({ error: 'Category already exists.', code: 409 });
|
||||||
|
|
||||||
const maxOrder = db.get()
|
const maxOrder = db.get()
|
||||||
.prepare('SELECT COALESCE(MAX(sort_order), -1) AS m FROM shopping_categories')
|
.prepare('SELECT COALESCE(MAX(sort_order), -1) AS m FROM shopping_categories')
|
||||||
@@ -73,8 +73,8 @@ router.post('/categories', (req, res) => {
|
|||||||
.get(result.lastInsertRowid);
|
.get(result.lastInsertRowid);
|
||||||
res.status(201).json({ data: cat });
|
res.status(201).json({ data: cat });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('POST /categories Fehler:', err);
|
log.error('POST /categories error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ router.put('/categories/:catId', (req, res) => {
|
|||||||
const cat = db.get()
|
const cat = db.get()
|
||||||
.prepare('SELECT * FROM shopping_categories WHERE id = ?')
|
.prepare('SELECT * FROM shopping_categories WHERE id = ?')
|
||||||
.get(req.params.catId);
|
.get(req.params.catId);
|
||||||
if (!cat) return res.status(404).json({ error: 'Kategorie nicht gefunden.', code: 404 });
|
if (!cat) return res.status(404).json({ error: 'Category not found.', code: 404 });
|
||||||
|
|
||||||
const vName = str(req.body.name, 'Name', { max: MAX_SHORT });
|
const vName = str(req.body.name, 'Name', { max: MAX_SHORT });
|
||||||
if (vName.error) return res.status(400).json({ error: vName.error, code: 400 });
|
if (vName.error) return res.status(400).json({ error: vName.error, code: 400 });
|
||||||
@@ -97,7 +97,7 @@ router.put('/categories/:catId', (req, res) => {
|
|||||||
const conflict = db.get()
|
const conflict = db.get()
|
||||||
.prepare('SELECT id FROM shopping_categories WHERE name = ? COLLATE NOCASE AND id != ?')
|
.prepare('SELECT id FROM shopping_categories WHERE name = ? COLLATE NOCASE AND id != ?')
|
||||||
.get(vName.value, cat.id);
|
.get(vName.value, cat.id);
|
||||||
if (conflict) return res.status(409).json({ error: 'Kategorie existiert bereits.', code: 409 });
|
if (conflict) return res.status(409).json({ error: 'Category already exists.', code: 409 });
|
||||||
|
|
||||||
// Artikel, die die alte Kategorie nutzen, mitumbenennen
|
// Artikel, die die alte Kategorie nutzen, mitumbenennen
|
||||||
db.get().transaction(() => {
|
db.get().transaction(() => {
|
||||||
@@ -114,8 +114,8 @@ router.put('/categories/:catId', (req, res) => {
|
|||||||
.get(cat.id);
|
.get(cat.id);
|
||||||
res.json({ data: updated });
|
res.json({ data: updated });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('PUT /categories/:catId Fehler:', err);
|
log.error('PUT /categories/:catId error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -130,12 +130,12 @@ router.delete('/categories/:catId', (req, res) => {
|
|||||||
const cat = db.get()
|
const cat = db.get()
|
||||||
.prepare('SELECT * FROM shopping_categories WHERE id = ?')
|
.prepare('SELECT * FROM shopping_categories WHERE id = ?')
|
||||||
.get(req.params.catId);
|
.get(req.params.catId);
|
||||||
if (!cat) return res.status(404).json({ error: 'Kategorie nicht gefunden.', code: 404 });
|
if (!cat) return res.status(404).json({ error: 'Category not found.', code: 404 });
|
||||||
|
|
||||||
const total = db.get()
|
const total = db.get()
|
||||||
.prepare('SELECT COUNT(*) AS c FROM shopping_categories')
|
.prepare('SELECT COUNT(*) AS c FROM shopping_categories')
|
||||||
.get().c;
|
.get().c;
|
||||||
if (total <= 1) return res.status(400).json({ error: 'Letzte Kategorie kann nicht gelöscht werden.', code: 400 });
|
if (total <= 1) return res.status(400).json({ error: 'The last category cannot be deleted.', code: 400 });
|
||||||
|
|
||||||
// Fallback-Kategorie: erste andere Kategorie nach sort_order
|
// Fallback-Kategorie: erste andere Kategorie nach sort_order
|
||||||
const fallback = db.get()
|
const fallback = db.get()
|
||||||
@@ -153,8 +153,8 @@ router.delete('/categories/:catId', (req, res) => {
|
|||||||
|
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('DELETE /categories/:catId Fehler:', err);
|
log.error('DELETE /categories/:catId error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -177,8 +177,8 @@ router.patch('/categories/reorder', (req, res) => {
|
|||||||
|
|
||||||
res.json({ data: loadCategories() });
|
res.json({ data: loadCategories() });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('PATCH /categories/reorder Fehler:', err);
|
log.error('PATCH /categories/reorder error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -201,8 +201,8 @@ router.get('/suggestions', (req, res) => {
|
|||||||
|
|
||||||
res.json({ data: rows.map((r) => r.name) });
|
res.json({ data: rows.map((r) => r.name) });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('suggestions Fehler:', err);
|
log.error('suggestions error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -217,7 +217,7 @@ router.patch('/items/:itemId', (req, res) => {
|
|||||||
const item = db.get()
|
const item = db.get()
|
||||||
.prepare('SELECT * FROM shopping_items WHERE id = ?')
|
.prepare('SELECT * FROM shopping_items WHERE id = ?')
|
||||||
.get(req.params.itemId);
|
.get(req.params.itemId);
|
||||||
if (!item) return res.status(404).json({ error: 'Artikel nicht gefunden.', code: 404 });
|
if (!item) return res.status(404).json({ error: 'Item not found.', code: 404 });
|
||||||
|
|
||||||
const {
|
const {
|
||||||
is_checked = item.is_checked,
|
is_checked = item.is_checked,
|
||||||
@@ -230,7 +230,7 @@ router.patch('/items/:itemId', (req, res) => {
|
|||||||
|
|
||||||
const validNames = validCategoryNames();
|
const validNames = validCategoryNames();
|
||||||
if (category && !validNames.includes(category))
|
if (category && !validNames.includes(category))
|
||||||
return res.status(400).json({ error: 'Ungültige Kategorie.', code: 400 });
|
return res.status(400).json({ error: 'Invalid category.', code: 400 });
|
||||||
|
|
||||||
db.get().prepare(`
|
db.get().prepare(`
|
||||||
UPDATE shopping_items
|
UPDATE shopping_items
|
||||||
@@ -243,8 +243,8 @@ router.patch('/items/:itemId', (req, res) => {
|
|||||||
.get(req.params.itemId);
|
.get(req.params.itemId);
|
||||||
res.json({ data: updated });
|
res.json({ data: updated });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('PATCH items/:id Fehler:', err);
|
log.error('PATCH items/:id error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -259,11 +259,11 @@ router.delete('/items/:itemId', (req, res) => {
|
|||||||
.prepare('DELETE FROM shopping_items WHERE id = ?')
|
.prepare('DELETE FROM shopping_items WHERE id = ?')
|
||||||
.run(req.params.itemId);
|
.run(req.params.itemId);
|
||||||
if (result.changes === 0)
|
if (result.changes === 0)
|
||||||
return res.status(404).json({ error: 'Artikel nicht gefunden.', code: 404 });
|
return res.status(404).json({ error: 'Item not found.', code: 404 });
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('DELETE items/:id Fehler:', err);
|
log.error('DELETE items/:id error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -286,8 +286,8 @@ router.get('/', (req, res) => {
|
|||||||
`).all();
|
`).all();
|
||||||
res.json({ data: lists });
|
res.json({ data: lists });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('GET / Fehler:', err);
|
log.error('GET / error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -311,8 +311,8 @@ router.post('/', (req, res) => {
|
|||||||
.get(result.lastInsertRowid);
|
.get(result.lastInsertRowid);
|
||||||
res.status(201).json({ data: list });
|
res.status(201).json({ data: list });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('POST / Fehler:', err);
|
log.error('POST / error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -331,15 +331,15 @@ router.put('/:listId', (req, res) => {
|
|||||||
.prepare('UPDATE shopping_lists SET name = ? WHERE id = ?')
|
.prepare('UPDATE shopping_lists SET name = ? WHERE id = ?')
|
||||||
.run(vName.value, 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: 'List not found.', code: 404 });
|
||||||
|
|
||||||
const list = db.get()
|
const list = db.get()
|
||||||
.prepare('SELECT * FROM shopping_lists WHERE id = ?')
|
.prepare('SELECT * FROM shopping_lists WHERE id = ?')
|
||||||
.get(req.params.listId);
|
.get(req.params.listId);
|
||||||
res.json({ data: list });
|
res.json({ data: list });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('PUT /:listId Fehler:', err);
|
log.error('PUT /:listId error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -354,11 +354,11 @@ router.delete('/:listId', (req, res) => {
|
|||||||
.prepare('DELETE FROM shopping_lists WHERE id = ?')
|
.prepare('DELETE FROM shopping_lists WHERE id = ?')
|
||||||
.run(req.params.listId);
|
.run(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: 'List not found.', code: 404 });
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('DELETE /:listId Fehler:', err);
|
log.error('DELETE /:listId error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -373,7 +373,7 @@ router.get('/:listId/items', (req, res) => {
|
|||||||
const list = db.get()
|
const list = db.get()
|
||||||
.prepare('SELECT * FROM shopping_lists WHERE id = ?')
|
.prepare('SELECT * FROM shopping_lists WHERE id = ?')
|
||||||
.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: 'List not found.', code: 404 });
|
||||||
|
|
||||||
const categories = loadCategories();
|
const categories = loadCategories();
|
||||||
const categoryOrder = categories.map((c, i) => `WHEN '${c.name.replace(/'/g, "''")}' THEN ${i}`).join(' ');
|
const categoryOrder = categories.map((c, i) => `WHEN '${c.name.replace(/'/g, "''")}' THEN ${i}`).join(' ');
|
||||||
@@ -389,8 +389,8 @@ router.get('/:listId/items', (req, res) => {
|
|||||||
|
|
||||||
res.json({ data: items, list, categories });
|
res.json({ data: items, list, categories });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('GET /:listId/items Fehler:', err);
|
log.error('GET /:listId/items error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -405,7 +405,7 @@ router.post('/:listId/items', (req, res) => {
|
|||||||
const list = db.get()
|
const list = db.get()
|
||||||
.prepare('SELECT id FROM shopping_lists WHERE id = ?')
|
.prepare('SELECT id FROM shopping_lists WHERE id = ?')
|
||||||
.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: 'List not found.', code: 404 });
|
||||||
|
|
||||||
const validNames = validCategoryNames();
|
const validNames = validCategoryNames();
|
||||||
const defaultCat = validNames[0] ?? 'Sonstiges';
|
const defaultCat = validNames[0] ?? 'Sonstiges';
|
||||||
@@ -427,8 +427,8 @@ router.post('/:listId/items', (req, res) => {
|
|||||||
.get(result.lastInsertRowid);
|
.get(result.lastInsertRowid);
|
||||||
res.status(201).json({ data: item });
|
res.status(201).json({ data: item });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('POST /:listId/items Fehler:', err);
|
log.error('POST /:listId/items error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -444,8 +444,8 @@ router.delete('/:listId/items/checked', (req, res) => {
|
|||||||
`).run(req.params.listId);
|
`).run(req.params.listId);
|
||||||
res.json({ deleted: result.changes });
|
res.json({ deleted: result.changes });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('DELETE /:listId/items/checked Fehler:', err);
|
log.error('DELETE /:listId/items/checked error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+20
-20
@@ -103,8 +103,8 @@ router.get('/', (req, res) => {
|
|||||||
|
|
||||||
res.json({ data: db.get().prepare(sql).all(...params) });
|
res.json({ data: db.get().prepare(sql).all(...params) });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('GET / Fehler:', err);
|
log.error('GET / error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -122,13 +122,13 @@ router.get('/:id', (req, res) => {
|
|||||||
WHERE t.id = ? AND t.parent_task_id IS NULL
|
WHERE t.id = ? AND t.parent_task_id IS NULL
|
||||||
`).get(req.params.id);
|
`).get(req.params.id);
|
||||||
|
|
||||||
if (!task) return res.status(404).json({ error: 'Aufgabe nicht gefunden.', code: 404 });
|
if (!task) return res.status(404).json({ error: 'Task not found.', code: 404 });
|
||||||
|
|
||||||
task.subtasks = loadSubtasks(task.id);
|
task.subtasks = loadSubtasks(task.id);
|
||||||
res.json({ data: task });
|
res.json({ data: task });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('GET /:id Fehler:', err);
|
log.error('GET /:id error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@ router.post('/', (req, res) => {
|
|||||||
if (parent_task_id) {
|
if (parent_task_id) {
|
||||||
const parent = db.get().prepare('SELECT parent_task_id FROM tasks WHERE id = ?')
|
const parent = db.get().prepare('SELECT parent_task_id FROM tasks WHERE id = ?')
|
||||||
.get(parent_task_id);
|
.get(parent_task_id);
|
||||||
if (!parent) return res.status(404).json({ error: 'Übergeordnete Aufgabe nicht gefunden.', code: 404 });
|
if (!parent) return res.status(404).json({ error: 'Parent task not found.', code: 404 });
|
||||||
if (parent.parent_task_id)
|
if (parent.parent_task_id)
|
||||||
return res.status(400).json({ error: 'Maximal 2 Verschachtelungsebenen erlaubt.', code: 400 });
|
return res.status(400).json({ error: 'Maximal 2 Verschachtelungsebenen erlaubt.', code: 400 });
|
||||||
}
|
}
|
||||||
@@ -185,8 +185,8 @@ router.post('/', (req, res) => {
|
|||||||
|
|
||||||
res.status(201).json({ data: task });
|
res.status(201).json({ data: task });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('POST / Fehler:', err);
|
log.error('POST / error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -200,7 +200,7 @@ router.post('/', (req, res) => {
|
|||||||
router.put('/:id', (req, res) => {
|
router.put('/:id', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const task = db.get().prepare('SELECT * FROM tasks WHERE id = ?').get(req.params.id);
|
const task = db.get().prepare('SELECT * FROM tasks WHERE id = ?').get(req.params.id);
|
||||||
if (!task) return res.status(404).json({ error: 'Aufgabe nicht gefunden.', code: 404 });
|
if (!task) return res.status(404).json({ error: 'Task not found.', code: 404 });
|
||||||
|
|
||||||
const errors = validateTaskInput(req.body, false);
|
const errors = validateTaskInput(req.body, false);
|
||||||
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
|
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
|
||||||
@@ -237,8 +237,8 @@ router.put('/:id', (req, res) => {
|
|||||||
|
|
||||||
res.json({ data: updated });
|
res.json({ data: updated });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('PUT /:id Fehler:', err);
|
log.error('PUT /:id error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -252,13 +252,13 @@ router.patch('/:id/status', (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { status } = req.body;
|
const { status } = req.body;
|
||||||
if (!VALID_STATUSES.includes(status))
|
if (!VALID_STATUSES.includes(status))
|
||||||
return res.status(400).json({ error: `Ungültiger Status. Erlaubt: ${VALID_STATUSES.join(', ')}`, code: 400 });
|
return res.status(400).json({ error: `Invalid status. Allowed: ${VALID_STATUSES.join(', ')}`, code: 400 });
|
||||||
|
|
||||||
const result = db.get().prepare('UPDATE tasks SET status = ? WHERE id = ?')
|
const result = db.get().prepare('UPDATE tasks SET status = ? WHERE id = ?')
|
||||||
.run(status, req.params.id);
|
.run(status, req.params.id);
|
||||||
|
|
||||||
if (result.changes === 0)
|
if (result.changes === 0)
|
||||||
return res.status(404).json({ error: 'Aufgabe nicht gefunden.', code: 404 });
|
return res.status(404).json({ error: 'Task not found.', code: 404 });
|
||||||
|
|
||||||
// Wiederkehrende Aufgabe: nächste Instanz erstellen wenn erledigt
|
// Wiederkehrende Aufgabe: nächste Instanz erstellen wenn erledigt
|
||||||
if (status === 'done') {
|
if (status === 'done') {
|
||||||
@@ -281,8 +281,8 @@ router.patch('/:id/status', (req, res) => {
|
|||||||
|
|
||||||
res.json({ data: { id: Number(req.params.id), status } });
|
res.json({ data: { id: Number(req.params.id), status } });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('PATCH /:id/status Fehler:', err);
|
log.error('PATCH /:id/status error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -295,11 +295,11 @@ router.delete('/:id', (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const result = db.get().prepare('DELETE FROM tasks WHERE id = ?').run(req.params.id);
|
const result = db.get().prepare('DELETE FROM tasks WHERE id = ?').run(req.params.id);
|
||||||
if (result.changes === 0)
|
if (result.changes === 0)
|
||||||
return res.status(404).json({ error: 'Aufgabe nicht gefunden.', code: 404 });
|
return res.status(404).json({ error: 'Task not found.', code: 404 });
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('DELETE /:id Fehler:', err);
|
log.error('DELETE /:id error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -315,8 +315,8 @@ router.get('/meta/options', (req, res) => {
|
|||||||
).all();
|
).all();
|
||||||
res.json({ users, priorities: VALID_PRIORITIES, statuses: VALID_STATUSES, categories: VALID_CATEGORIES });
|
res.json({ users, priorities: VALID_PRIORITIES, statuses: VALID_STATUSES, categories: VALID_CATEGORIES });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('GET /meta/options Fehler:', err);
|
log.error('GET /meta/options error:', err);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
|
res.status(500).json({ error: 'Internal server error.', code: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ router.get('/', async (req, res) => {
|
|||||||
const currentUrl = `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&appid=${apiKey}&units=${units}&lang=${lang}`;
|
const currentUrl = `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&appid=${apiKey}&units=${units}&lang=${lang}`;
|
||||||
const currentRes = await fetch(currentUrl, { signal: AbortSignal.timeout(8000) });
|
const currentRes = await fetch(currentUrl, { signal: AbortSignal.timeout(8000) });
|
||||||
if (!currentRes.ok) {
|
if (!currentRes.ok) {
|
||||||
log.warn(`API Fehler: ${currentRes.status}`);
|
log.warn(`API error: ${currentRes.status}`);
|
||||||
return res.json({ data: null });
|
return res.json({ data: null });
|
||||||
}
|
}
|
||||||
const currentJson = await currentRes.json();
|
const currentJson = await currentRes.json();
|
||||||
@@ -116,7 +116,7 @@ router.get('/', async (req, res) => {
|
|||||||
cache = { data, ts: Date.now() };
|
cache = { data, ts: Date.now() };
|
||||||
res.json({ data });
|
res.json({ data });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.warn('Fehler:', err.message);
|
log.warn('Error:', err.message);
|
||||||
res.json({ data: null }); // Fallback: Widget ausblenden, kein Error-Screen
|
res.json({ data: null }); // Fallback: Widget ausblenden, kein Error-Screen
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -145,8 +145,8 @@ router.get('/icon/:code', async (req, res) => {
|
|||||||
res.setHeader('Cache-Control', 'public, max-age=86400'); // 24 Stunden
|
res.setHeader('Cache-Control', 'public, max-age=86400'); // 24 Stunden
|
||||||
upstream.body.pipe(res);
|
upstream.body.pipe(res);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.warn('Icon-Proxy Fehler:', err.message);
|
log.warn('Icon proxy error:', err.message);
|
||||||
res.status(502).json({ error: 'Icon-Proxy fehlgeschlagen.', code: 502 });
|
res.status(502).json({ error: 'Icon proxy failed.', code: 502 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ function getCredentials() {
|
|||||||
function saveCredentials(url, username, password) {
|
function saveCredentials(url, username, password) {
|
||||||
// Warnung wenn DB-Verschluesselung nicht aktiv - Credentials liegen dann im Klartext
|
// Warnung wenn DB-Verschluesselung nicht aktiv - Credentials liegen dann im Klartext
|
||||||
if (!process.env.DB_ENCRYPTION_KEY) {
|
if (!process.env.DB_ENCRYPTION_KEY) {
|
||||||
log.warn('WARNUNG: DB_ENCRYPTION_KEY nicht gesetzt - CalDAV-Credentials werden unverschluesselt gespeichert.');
|
log.warn('WARNING: DB_ENCRYPTION_KEY is not set - CalDAV credentials will be stored unencrypted.');
|
||||||
}
|
}
|
||||||
cfgSet('apple_caldav_url', url);
|
cfgSet('apple_caldav_url', url);
|
||||||
cfgSet('apple_username', username);
|
cfgSet('apple_username', username);
|
||||||
@@ -89,7 +89,7 @@ function saveCredentials(url, username, password) {
|
|||||||
|
|
||||||
function clearCredentials() {
|
function clearCredentials() {
|
||||||
['apple_caldav_url', 'apple_username', 'apple_app_password', 'apple_last_sync'].forEach(cfgDel);
|
['apple_caldav_url', 'apple_username', 'apple_app_password', 'apple_last_sync'].forEach(cfgDel);
|
||||||
log.info('Verbindung getrennt.');
|
log.info('Disconnected.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -110,7 +110,7 @@ function getStatus() {
|
|||||||
*/
|
*/
|
||||||
async function testConnection() {
|
async function testConnection() {
|
||||||
const creds = getCredentials();
|
const creds = getCredentials();
|
||||||
if (!creds) throw new Error('[Apple] Keine Credentials konfiguriert.');
|
if (!creds) throw new Error('[Apple] No credentials configured.');
|
||||||
|
|
||||||
const { createDAVClient } = await import('tsdav');
|
const { createDAVClient } = await import('tsdav');
|
||||||
const client = await createDAVClient({
|
const client = await createDAVClient({
|
||||||
@@ -121,7 +121,7 @@ async function testConnection() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const calendars = await client.fetchCalendars();
|
const calendars = await client.fetchCalendars();
|
||||||
if (!calendars.length) throw new Error('[Apple] Verbunden, aber keine Kalender gefunden.');
|
if (!calendars.length) throw new Error('[Apple] Connected, but no calendars found.');
|
||||||
return { ok: true, calendarCount: calendars.length };
|
return { ok: true, calendarCount: calendars.length };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,7 +196,7 @@ function unescapeICS(str) {
|
|||||||
async function sync() {
|
async function sync() {
|
||||||
const creds = getCredentials();
|
const creds = getCredentials();
|
||||||
if (!creds) {
|
if (!creds) {
|
||||||
throw new Error('[Apple] Keine Credentials konfiguriert (weder in DB noch in .env).');
|
throw new Error('[Apple] No credentials configured (neither in DB nor in .env).');
|
||||||
}
|
}
|
||||||
|
|
||||||
// tsdav ist eine optionale Abhängigkeit - dynamischer Import für graceful degradation
|
// tsdav ist eine optionale Abhängigkeit - dynamischer Import für graceful degradation
|
||||||
@@ -211,14 +211,14 @@ async function sync() {
|
|||||||
|
|
||||||
const calendars = await client.fetchCalendars();
|
const calendars = await client.fetchCalendars();
|
||||||
if (!calendars.length) {
|
if (!calendars.length) {
|
||||||
log.warn('Keine Kalender gefunden.');
|
log.warn('No calendars found.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// created_by: ersten existierenden User verwenden (nicht hardcoded ID 1)
|
// created_by: ersten existierenden User verwenden (nicht hardcoded ID 1)
|
||||||
const owner = db.get().prepare('SELECT id FROM users ORDER BY id ASC LIMIT 1').get();
|
const owner = db.get().prepare('SELECT id FROM users ORDER BY id ASC LIMIT 1').get();
|
||||||
if (!owner) {
|
if (!owner) {
|
||||||
log.warn('Kein User in der Datenbank - Sync übersprungen.');
|
log.warn('No user in database - sync skipped.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const createdBy = owner.id;
|
const createdBy = owner.id;
|
||||||
@@ -233,7 +233,7 @@ async function sync() {
|
|||||||
try {
|
try {
|
||||||
calObjects = await client.fetchCalendarObjects({ calendar: cal });
|
calObjects = await client.fetchCalendarObjects({ calendar: cal });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.warn(`Kalender "${cal.displayName || '(unbenannt)'}" nicht abrufbar: ${err.message}`);
|
log.warn(`Calendar "${cal.displayName || '(unnamed)'}" is not accessible: ${err.message}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,7 +277,7 @@ async function sync() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error(`Upsert-Fehler für UID ${ev.uid}:`, err.message);
|
log.error(`Upsert error for UID ${ev.uid}:`, err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -308,12 +308,12 @@ async function sync() {
|
|||||||
UPDATE calendar_events SET external_calendar_id = ?, external_source = 'apple' WHERE id = ?
|
UPDATE calendar_events SET external_calendar_id = ?, external_source = 'apple' WHERE id = ?
|
||||||
`).run(uid, event.id);
|
`).run(uid, event.id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error(`Outbound-Fehler für Event ${event.id}:`, err.message);
|
log.error(`Outbound error for event ${event.id}:`, err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cfgSet('apple_last_sync', new Date().toISOString());
|
cfgSet('apple_last_sync', new Date().toISOString());
|
||||||
log.info(`Sync abgeschlossen - ${totalObjects} Objekte aus ${syncCalendars.length} Kalendern inbound, ${localEvents.length} lokal → iCloud.`);
|
log.info(`Sync completed - ${totalObjects} objects from ${syncCalendars.length} calendars inbound, ${localEvents.length} local → iCloud.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { sync, getStatus, saveCredentials, clearCredentials, testConnection };
|
export { sync, getStatus, saveCredentials, clearCredentials, testConnection };
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ function createClient() {
|
|||||||
const redirectUri = process.env.GOOGLE_REDIRECT_URI;
|
const redirectUri = process.env.GOOGLE_REDIRECT_URI;
|
||||||
|
|
||||||
if (!clientId || !clientSecret || !redirectUri) {
|
if (!clientId || !clientSecret || !redirectUri) {
|
||||||
throw new Error('[Google] GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET und GOOGLE_REDIRECT_URI müssen gesetzt sein.');
|
throw new Error('[Google] GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and GOOGLE_REDIRECT_URI must be set.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return new google.auth.OAuth2(clientId, clientSecret, redirectUri);
|
return new google.auth.OAuth2(clientId, clientSecret, redirectUri);
|
||||||
@@ -79,7 +79,7 @@ function loadAuthorizedClient() {
|
|||||||
const refreshToken = cfgGet('google_refresh_token');
|
const refreshToken = cfgGet('google_refresh_token');
|
||||||
|
|
||||||
if (!accessToken || !refreshToken) {
|
if (!accessToken || !refreshToken) {
|
||||||
throw new Error('[Google] Nicht konfiguriert - zuerst OAuth durchführen.');
|
throw new Error('[Google] Not configured - complete OAuth first.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = createClient();
|
const client = createClient();
|
||||||
@@ -133,14 +133,14 @@ async function handleCallback(code) {
|
|||||||
const { tokens } = await client.getToken(code);
|
const { tokens } = await client.getToken(code);
|
||||||
|
|
||||||
if (!tokens.refresh_token) {
|
if (!tokens.refresh_token) {
|
||||||
throw new Error('[Google] Kein Refresh Token erhalten. Bitte Zugriff in Google-Konto widerrufen und erneut verbinden.');
|
throw new Error('[Google] No refresh token received. Revoke access in your Google account and connect again.');
|
||||||
}
|
}
|
||||||
|
|
||||||
cfgSet('google_access_token', tokens.access_token);
|
cfgSet('google_access_token', tokens.access_token);
|
||||||
cfgSet('google_refresh_token', tokens.refresh_token);
|
cfgSet('google_refresh_token', tokens.refresh_token);
|
||||||
if (tokens.expiry_date) cfgSet('google_token_expiry', String(tokens.expiry_date));
|
if (tokens.expiry_date) cfgSet('google_token_expiry', String(tokens.expiry_date));
|
||||||
|
|
||||||
log.info('OAuth erfolgreich - Tokens gespeichert.');
|
log.info('OAuth successful - tokens saved.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -160,7 +160,7 @@ function getStatus() {
|
|||||||
function disconnect() {
|
function disconnect() {
|
||||||
['google_access_token', 'google_refresh_token', 'google_token_expiry',
|
['google_access_token', 'google_refresh_token', 'google_token_expiry',
|
||||||
'google_sync_token', 'google_last_sync'].forEach(cfgDel);
|
'google_sync_token', 'google_last_sync'].forEach(cfgDel);
|
||||||
log.info('Verbindung getrennt.');
|
log.info('Disconnected.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -181,7 +181,7 @@ async function sync() {
|
|||||||
const calName = meta.data.summary || 'Google Calendar';
|
const calName = meta.data.summary || 'Google Calendar';
|
||||||
calRefId = upsertExternalCalendar('google', 'primary', calName, calColor);
|
calRefId = upsertExternalCalendar('google', 'primary', calName, calColor);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.warn('Kalender-Metadaten nicht abrufbar:', err.message);
|
log.warn('Calendar metadata is not accessible:', err.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -214,7 +214,7 @@ async function sync() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.code === 410) {
|
if (err.code === 410) {
|
||||||
// syncToken abgelaufen → vollständiger Resync
|
// syncToken abgelaufen → vollständiger Resync
|
||||||
log.warn('syncToken ungültig - vollständiger Resync.');
|
log.warn('syncToken invalid - full resync.');
|
||||||
cfgDel('google_sync_token');
|
cfgDel('google_sync_token');
|
||||||
syncToken = null;
|
syncToken = null;
|
||||||
continue;
|
continue;
|
||||||
@@ -250,12 +250,12 @@ async function sync() {
|
|||||||
UPDATE calendar_events SET external_calendar_id = ?, external_source = 'google' WHERE id = ?
|
UPDATE calendar_events SET external_calendar_id = ?, external_source = 'google' WHERE id = ?
|
||||||
`).run(created.data.id, event.id);
|
`).run(created.data.id, event.id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error(`Outbound-Fehler für Event ${event.id}:`, err.message);
|
log.error(`Outbound error for event ${event.id}:`, err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cfgSet('google_last_sync', new Date().toISOString());
|
cfgSet('google_last_sync', new Date().toISOString());
|
||||||
log.info(`Sync abgeschlossen - ${localEvents.length} lokal → Google, Inbound via syncToken.`);
|
log.info(`Sync completed - ${localEvents.length} local → Google, inbound via syncToken.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -306,7 +306,7 @@ function upsertGoogleEvents(items, calRefId = null, calColor = GOOGLE_COLOR) {
|
|||||||
try {
|
try {
|
||||||
insertOrUpdate(item);
|
insertOrUpdate(item);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error(`Upsert-Fehler für Event ${item.id}:`, err.message);
|
log.error(`Upsert error for event ${item.id}:`, err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const syncingNow = new Set();
|
|||||||
|
|
||||||
function normalizeUrl(raw) {
|
function normalizeUrl(raw) {
|
||||||
const url = new URL(raw.replace(/^webcal:\/\//i, 'https://'));
|
const url = new URL(raw.replace(/^webcal:\/\//i, 'https://'));
|
||||||
if (url.protocol !== 'https:') throw new Error('Nur https:// und webcal:// URLs sind erlaubt.');
|
if (url.protocol !== 'https:') throw new Error('Only https:// and webcal:// URLs are allowed.');
|
||||||
return url.href;
|
return url.href;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ async function checkSSRF(urlStr) {
|
|||||||
const v6 = await dns.resolve6(hostname).catch(() => []);
|
const v6 = await dns.resolve6(hostname).catch(() => []);
|
||||||
for (const addr of [...v4, ...v6]) {
|
for (const addr of [...v4, ...v6]) {
|
||||||
if (PRIVATE_RANGES.some((re) => re.test(addr))) {
|
if (PRIVATE_RANGES.some((re) => re.test(addr))) {
|
||||||
throw new Error(`URL löst auf eine private IP-Adresse auf: ${addr}`);
|
throw new Error(`URL resolves to a private IP address: ${addr}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,12 +61,12 @@ async function fetchAndParse(urlRaw, etag, lastModified) {
|
|||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
|
||||||
const cl = parseInt(res.headers.get('content-length') || '0', 10);
|
const cl = parseInt(res.headers.get('content-length') || '0', 10);
|
||||||
if (cl > MAX_RESPONSE_BYTES) throw new Error('ICS-Datei überschreitet 10 MB Limit.');
|
if (cl > MAX_RESPONSE_BYTES) throw new Error('ICS file exceeds the 10 MB limit.');
|
||||||
|
|
||||||
let body = '', received = 0;
|
let body = '', received = 0;
|
||||||
for await (const chunk of res.body) {
|
for await (const chunk of res.body) {
|
||||||
received += chunk.length;
|
received += chunk.length;
|
||||||
if (received > MAX_RESPONSE_BYTES) throw new Error('ICS-Datei überschreitet 10 MB Limit.');
|
if (received > MAX_RESPONSE_BYTES) throw new Error('ICS file exceeds the 10 MB limit.');
|
||||||
body += chunk.toString();
|
body += chunk.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ function syncWindow() {
|
|||||||
|
|
||||||
async function syncOne(sub) {
|
async function syncOne(sub) {
|
||||||
if (syncingNow.has(sub.id)) {
|
if (syncingNow.has(sub.id)) {
|
||||||
log.info(`Abonnement ${sub.id} wird bereits synchronisiert - übersprungen.`);
|
log.info(`Subscription ${sub.id} is already syncing - skipped.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
syncingNow.add(sub.id);
|
syncingNow.add(sub.id);
|
||||||
@@ -95,7 +95,7 @@ async function syncOne(sub) {
|
|||||||
let result;
|
let result;
|
||||||
try { result = await fetchAndParse(sub.url, sub.etag, sub.last_modified); }
|
try { result = await fetchAndParse(sub.url, sub.etag, sub.last_modified); }
|
||||||
catch (err) {
|
catch (err) {
|
||||||
log.warn(`Abonnement ${sub.id} (${sub.name}): Fetch fehlgeschlagen - ${err.message}`);
|
log.warn(`Subscription ${sub.id} (${sub.name}): fetch failed - ${err.message}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ async function syncOne(sub) {
|
|||||||
const { windowStart, windowEnd } = syncWindow();
|
const { windowStart, windowEnd } = syncWindow();
|
||||||
const owner = db.get().prepare('SELECT id FROM users ORDER BY id ASC LIMIT 1').get();
|
const owner = db.get().prepare('SELECT id FROM users ORDER BY id ASC LIMIT 1').get();
|
||||||
const createdBy = sub.created_by ?? owner?.id;
|
const createdBy = sub.created_by ?? owner?.id;
|
||||||
if (!createdBy) { log.warn('Kein User gefunden.'); return; }
|
if (!createdBy) { log.warn('No user found.'); return; }
|
||||||
|
|
||||||
const flatEvents = [];
|
const flatEvents = [];
|
||||||
for (const ev of events) {
|
for (const ev of events) {
|
||||||
@@ -157,7 +157,7 @@ async function syncOne(sub) {
|
|||||||
.run(new Date().toISOString(), newEtag, newLastModified, sub.id);
|
.run(new Date().toISOString(), newEtag, newLastModified, sub.id);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
log.info(`Abonnement ${sub.id} (${sub.name}): ${flatEvents.length} Events synchronisiert.`);
|
log.info(`Subscription ${sub.id} (${sub.name}): ${flatEvents.length} events synced.`);
|
||||||
} finally { syncingNow.delete(sub.id); }
|
} finally { syncingNow.delete(sub.id); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ async function sync(subscriptionId) {
|
|||||||
: db.get().prepare('SELECT * FROM ics_subscriptions').all();
|
: db.get().prepare('SELECT * FROM ics_subscriptions').all();
|
||||||
for (const sub of subs) {
|
for (const sub of subs) {
|
||||||
try { await syncOne(sub); }
|
try { await syncOne(sub); }
|
||||||
catch (err) { log.error(`Sync Abonnement ${sub.id} fehlgeschlagen: ${err.message}`); }
|
catch (err) { log.error(`Subscription ${sub.id} sync failed: ${err.message}`); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@ async function create(userId, { name, url, color, shared }) {
|
|||||||
function update(userId, subId, fields, isAdmin) {
|
function update(userId, subId, fields, isAdmin) {
|
||||||
const sub = db.get().prepare('SELECT * FROM ics_subscriptions WHERE id = ?').get(subId);
|
const sub = db.get().prepare('SELECT * FROM ics_subscriptions WHERE id = ?').get(subId);
|
||||||
if (!sub) return null;
|
if (!sub) return null;
|
||||||
if (!isAdmin && sub.created_by !== userId) throw new Error('Nicht autorisiert.');
|
if (!isAdmin && sub.created_by !== userId) throw new Error('Not authorized.');
|
||||||
const name = fields.name !== undefined ? fields.name : sub.name;
|
const name = fields.name !== undefined ? fields.name : sub.name;
|
||||||
const color = fields.color !== undefined ? fields.color : sub.color;
|
const color = fields.color !== undefined ? fields.color : sub.color;
|
||||||
const shared = fields.shared !== undefined ? (fields.shared ? 1 : 0) : sub.shared;
|
const shared = fields.shared !== undefined ? (fields.shared ? 1 : 0) : sub.shared;
|
||||||
@@ -204,7 +204,7 @@ function update(userId, subId, fields, isAdmin) {
|
|||||||
function remove(userId, subId, isAdmin) {
|
function remove(userId, subId, isAdmin) {
|
||||||
const sub = db.get().prepare('SELECT * FROM ics_subscriptions WHERE id = ?').get(subId);
|
const sub = db.get().prepare('SELECT * FROM ics_subscriptions WHERE id = ?').get(subId);
|
||||||
if (!sub) return false;
|
if (!sub) return false;
|
||||||
if (!isAdmin && sub.created_by !== userId) throw new Error('Nicht autorisiert.');
|
if (!isAdmin && sub.created_by !== userId) throw new Error('Not authorized.');
|
||||||
db.get().prepare('DELETE FROM ics_subscriptions WHERE id = ?').run(subId);
|
db.get().prepare('DELETE FROM ics_subscriptions WHERE id = ?').run(subId);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,45 +69,45 @@ async function main() {
|
|||||||
.get();
|
.get();
|
||||||
|
|
||||||
if (existingAdmin) {
|
if (existingAdmin) {
|
||||||
console.log('ℹ Es existiert bereits ein Admin-Account.\n');
|
console.log('ℹ An admin account already exists.\n');
|
||||||
const proceed = await prompt('Trotzdem einen weiteren Admin anlegen? (j/N): ');
|
const proceed = await prompt('Create another admin anyway? (y/N): ');
|
||||||
if (proceed.toLowerCase() !== 'j') {
|
if (proceed.toLowerCase() !== 'y') {
|
||||||
console.log('Setup abgebrochen.');
|
console.log('Setup cancelled.');
|
||||||
rl.close();
|
rl.close();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Admin-Account anlegen:\n');
|
console.log('Create admin account:\n');
|
||||||
|
|
||||||
const username = (await prompt('Benutzername: ')).trim();
|
const username = (await prompt('Username: ')).trim();
|
||||||
if (!username || username.length < 3) {
|
if (!username || username.length < 3) {
|
||||||
console.error('Fehler: Benutzername muss mindestens 3 Zeichen lang sein.');
|
console.error('Error: username must be at least 3 characters long.');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayName = (await prompt('Anzeigename (z.B. "Max Mustermann"): ')).trim();
|
const displayName = (await prompt('Display name (e.g. "Max Mustermann"): ')).trim();
|
||||||
if (!displayName) {
|
if (!displayName) {
|
||||||
console.error('Fehler: Anzeigename darf nicht leer sein.');
|
console.error('Error: display name must not be empty.');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const password = await promptPassword('Passwort: ');
|
const password = await promptPassword('Password: ');
|
||||||
if (password.length < 8) {
|
if (password.length < 8) {
|
||||||
console.error('Fehler: Passwort muss mindestens 8 Zeichen lang sein.');
|
console.error('Error: password must be at least 8 characters long.');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const passwordConfirm = await promptPassword('Passwort bestätigen: ');
|
const passwordConfirm = await promptPassword('Confirm password: ');
|
||||||
if (password !== passwordConfirm) {
|
if (password !== passwordConfirm) {
|
||||||
console.error('Fehler: Passwörter stimmen nicht überein.');
|
console.error('Error: passwords do not match.');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const avatarColors = ['#007AFF', '#34C759', '#FF9500', '#FF3B30', '#AF52DE', '#FF2D55'];
|
const avatarColors = ['#007AFF', '#34C759', '#FF9500', '#FF3B30', '#AF52DE', '#FF2D55'];
|
||||||
const avatarColor = avatarColors[Math.floor(Math.random() * avatarColors.length)];
|
const avatarColor = avatarColors[Math.floor(Math.random() * avatarColors.length)];
|
||||||
|
|
||||||
console.log('\nAccount wird erstellt …');
|
console.log('\nCreating account …');
|
||||||
|
|
||||||
const hash = await bcrypt.hash(password, 12);
|
const hash = await bcrypt.hash(password, 12);
|
||||||
|
|
||||||
@@ -122,23 +122,23 @@ async function main() {
|
|||||||
const port = process.env.PORT || 3000;
|
const port = process.env.PORT || 3000;
|
||||||
const host = getLocalIP();
|
const host = getLocalIP();
|
||||||
|
|
||||||
console.log(`\n✅ Admin-Account erfolgreich erstellt!`);
|
console.log(`\n✅ Admin account created successfully!`);
|
||||||
console.log(`${'─'.repeat(40)}`);
|
console.log(`${'─'.repeat(40)}`);
|
||||||
console.log(` Benutzername: ${username}`);
|
console.log(` Username: ${username}`);
|
||||||
console.log(` Anzeigename: ${displayName}`);
|
console.log(` Display name: ${displayName}`);
|
||||||
console.log(` Rolle: Admin`);
|
console.log(` Role: Admin`);
|
||||||
console.log(`${'─'.repeat(40)}`);
|
console.log(`${'─'.repeat(40)}`);
|
||||||
console.log(`\n🌐 Oikos ist erreichbar unter:\n`);
|
console.log(`\n🌐 Oikos is available at:\n`);
|
||||||
console.log(` Lokal: http://localhost:${port}`);
|
console.log(` Local: http://localhost:${port}`);
|
||||||
if (host) {
|
if (host) {
|
||||||
console.log(` Netzwerk: http://${host}:${port}`);
|
console.log(` Network: http://${host}:${port}`);
|
||||||
}
|
}
|
||||||
console.log(`\n Melde dich mit deinem neuen Account an. Viel Spaß!\n`);
|
console.log(`\n Sign in with your new account.\n`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.message?.includes('UNIQUE constraint')) {
|
if (err.message?.includes('UNIQUE constraint')) {
|
||||||
console.error(`\nFehler: Benutzername "${username}" ist bereits vergeben.`);
|
console.error(`\nError: username "${username}" is already taken.`);
|
||||||
} else {
|
} else {
|
||||||
console.error('\nFehler beim Erstellen:', err.message);
|
console.error('\nCreation error:', err.message);
|
||||||
}
|
}
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -148,6 +148,6 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
main().catch((err) => {
|
main().catch((err) => {
|
||||||
console.error('Unerwarteter Fehler:', err.message);
|
console.error('Unexpected error:', err.message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|||||||
+4
-4
@@ -43,7 +43,7 @@ function setup() {
|
|||||||
|
|
||||||
test('auth.login: 401 feuert kein auth:expired', async () => {
|
test('auth.login: 401 feuert kein auth:expired', async () => {
|
||||||
setup();
|
setup();
|
||||||
_mockFetch = () => mockResponse(401, { error: 'Ungültige Anmeldedaten.', code: 401 });
|
_mockFetch = () => mockResponse(401, { error: 'Invalid credentials.', code: 401 });
|
||||||
|
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
() => auth.login('user', 'wrong'),
|
() => auth.login('user', 'wrong'),
|
||||||
@@ -60,7 +60,7 @@ test('auth.login: 401 feuert kein auth:expired', async () => {
|
|||||||
|
|
||||||
test('auth.login: 401 wirft ApiError mit status 401', async () => {
|
test('auth.login: 401 wirft ApiError mit status 401', async () => {
|
||||||
setup();
|
setup();
|
||||||
_mockFetch = () => mockResponse(401, { error: 'Ungültige Anmeldedaten.', code: 401 });
|
_mockFetch = () => mockResponse(401, { error: 'Invalid credentials.', code: 401 });
|
||||||
|
|
||||||
let thrownErr;
|
let thrownErr;
|
||||||
try {
|
try {
|
||||||
@@ -77,7 +77,7 @@ test('auth.login: 401 wirft ApiError mit status 401', async () => {
|
|||||||
|
|
||||||
test('api.get: 401 auf geschütztem Endpunkt feuert auth:expired', async () => {
|
test('api.get: 401 auf geschütztem Endpunkt feuert auth:expired', async () => {
|
||||||
setup();
|
setup();
|
||||||
_mockFetch = () => mockResponse(401, { error: 'Nicht authentifiziert.', code: 401 });
|
_mockFetch = () => mockResponse(401, { error: 'Not authenticated.', code: 401 });
|
||||||
|
|
||||||
await assert.rejects(() => api.get('/tasks'));
|
await assert.rejects(() => api.get('/tasks'));
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ test('api.get: 401 auf geschütztem Endpunkt feuert auth:expired', async () => {
|
|||||||
|
|
||||||
test('api.post: 401 auf Logout-Endpunkt feuert auth:expired', async () => {
|
test('api.post: 401 auf Logout-Endpunkt feuert auth:expired', async () => {
|
||||||
setup();
|
setup();
|
||||||
_mockFetch = () => mockResponse(401, { error: 'Nicht authentifiziert.', code: 401 });
|
_mockFetch = () => mockResponse(401, { error: 'Not authenticated.', code: 401 });
|
||||||
|
|
||||||
await assert.rejects(() => api.post('/auth/logout', {}));
|
await assert.rejects(() => api.post('/auth/logout', {}));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user