diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b615fe..9a96dba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,27 @@ All notable changes to this project are documented in this file. +## [1.123.2] - 2026-09-12 + +Erste drei Punkte aus dem TODO-Block L (Sicherheitsdurchgang vom 12.09.) — sichere, in +sich abgeschlossene Änderungen ohne Datenmodell-Migration. + +### Security +- **Kein automatischer `db push --accept-data-loss`-Fallback mehr in + `scripts/update-app.sh`.** Eine fehlgeschlagene `prisma migrate deploy` bricht das + Deployment jetzt sauber ab statt unbeaufsichtigt das Schema zu überschreiben — genau + dieser Fallback ist am 11.09. beim Boot-Lauf tatsächlich ausgelöst worden. +- **14 API-Routen gaben rohe Fehlermeldungen (`error.message`) direkt an den Client + zurück** (Kalender-Connect/-Sync, Task-Import/-Sync, Someday-Listen, Datenimport). Neuer + Helper `src/lib/api-error.ts` (`safeErrorMessage`): generische Meldung in Produktion, + echte Meldung nur noch lokal beim Entwickeln. Volles Logging bleibt unverändert in + `console.error`. +- SMTP-Zertifikatsprüfung wieder aktiv: `tls: { rejectUnauthorized: false }` in + `src/lib/email.ts` entfernt — stand unbegründet seit dem allerersten Commit der Datei und + öffnete Tür für Man-in-the-Middle auf Passwort-Reset-/Verifizierungs-Mails. + +Verifiziert: Typecheck, Lint, 79 Unit-Tests, Produktions-Build alle grün. + ## [1.123.1] - 2026-09-12 ### Fixed diff --git a/package.json b/package.json index 2db0826..8b1f8c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.123.1", + "version": "1.123.2", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "main": "index.js", "scripts": { diff --git a/scripts/update-app.sh b/scripts/update-app.sh index 589ae43..72ecaa6 100755 --- a/scripts/update-app.sh +++ b/scripts/update-app.sh @@ -104,11 +104,15 @@ main() { npm ci fi + # Kein automatischer db-push-Fallback mehr: "--accept-data-loss" darf niemand + # unbeaufsichtigt aus einem Deploy-Skript heraus zustimmen. Das ist am 11.09. beim + # Boot-Lauf tatsaechlich ausgeloest worden ("migrate deploy failed, falling back to + # db push..."), ohne dass irgendjemand das gesehen oder bestaetigt haette. Ein + # fehlgeschlagenes Migrate bricht das Update jetzt sauber ab -- die laufende App bleibt + # unveraendert, exakt wie bei einem fehlgeschlagenen Build. Migrationsfehler muessen von + # Hand untersucht werden, nicht automatisch ueberschrieben. log "--- Updating database schema (safe migrate) ---" - npx prisma migrate deploy || { - log "migrate deploy failed, falling back to db push..." - npx prisma db push --accept-data-loss - } + npx prisma migrate deploy log "--- Regenerating Prisma client ---" npx prisma generate diff --git a/src/app/api/calendar/apple/connect/route.ts b/src/app/api/calendar/apple/connect/route.ts index 0486d89..9f40328 100644 --- a/src/app/api/calendar/apple/connect/route.ts +++ b/src/app/api/calendar/apple/connect/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from 'next/server'; +import { safeErrorMessage } from '@/lib/api-error'; import { validateCredentials } from '@/lib/apple-calendar'; import { CalendarConnection } from '@/lib/calendar-sync'; import { getServerSession } from 'next-auth'; @@ -82,7 +83,7 @@ export async function POST(req: Request) { } catch (error: any) { console.error('Error connecting Apple Calendar:', error); return NextResponse.json( - { error: error.message || 'Failed to connect Apple Calendar' }, + { error: safeErrorMessage(error, 'Failed to connect Apple Calendar') }, { status: 500 } ); } diff --git a/src/app/api/calendar/apple/sync/route.ts b/src/app/api/calendar/apple/sync/route.ts index da39833..e343336 100644 --- a/src/app/api/calendar/apple/sync/route.ts +++ b/src/app/api/calendar/apple/sync/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { getUpcomingEvents } from '@/lib/apple-calendar'; import { authOptions } from "@/lib/auth"; +import { safeErrorMessage } from '@/lib/api-error'; export async function POST(req: Request) { try { @@ -36,7 +37,7 @@ export async function POST(req: Request) { } catch (error: any) { console.error('Error syncing Apple Calendar:', error); return NextResponse.json( - { error: error.message || 'Failed to sync Apple Calendar' }, + { error: safeErrorMessage(error, 'Failed to sync Apple Calendar') }, { status: 500 } ); } diff --git a/src/app/api/calendar/events/route.ts b/src/app/api/calendar/events/route.ts index 5601822..8272697 100644 --- a/src/app/api/calendar/events/route.ts +++ b/src/app/api/calendar/events/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; +import { safeErrorMessage } from '@/lib/api-error'; import { getServerSession } from 'next-auth'; import { authOptions } from "@/lib/auth"; import { prisma } from '@/lib/prisma'; @@ -98,7 +99,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ event }); } catch (error: any) { console.error('Error creating event:', error); - return NextResponse.json({ error: error.message || 'Failed to create event' }, { status: 500 }); + return NextResponse.json({ error: safeErrorMessage(error, 'Failed to create event') }, { status: 500 }); } } @@ -195,7 +196,7 @@ export async function PATCH(request: NextRequest) { return NextResponse.json({ event }); } catch (error: any) { console.error('Error updating event:', error); - return NextResponse.json({ error: error.message || 'Failed to update event' }, { status: 500 }); + return NextResponse.json({ error: safeErrorMessage(error, 'Failed to update event') }, { status: 500 }); } } @@ -241,6 +242,6 @@ export async function DELETE(request: NextRequest) { return NextResponse.json({ success: true }); } catch (error: any) { console.error('Error deleting event:', error); - return NextResponse.json({ error: error.message || 'Failed to delete event' }, { status: 500 }); + return NextResponse.json({ error: safeErrorMessage(error, 'Failed to delete event') }, { status: 500 }); } } diff --git a/src/app/api/calendar/sync/route.ts b/src/app/api/calendar/sync/route.ts index 9661b7e..a722bec 100644 --- a/src/app/api/calendar/sync/route.ts +++ b/src/app/api/calendar/sync/route.ts @@ -5,6 +5,7 @@ import { authOptions } from "@/lib/auth"; import { prisma } from '@/lib/prisma'; import { readCachedEvents, isCacheStale, refreshConnectionCache, RefreshableConnection } from '@/lib/calendar-cache'; import { runSyncRules } from '@/lib/calendar-cross-sync'; +import { safeErrorMessage } from '@/lib/api-error'; export async function POST(request: NextRequest) { try { @@ -108,7 +109,7 @@ export async function POST(request: NextRequest) { console.error('[CALENDAR SYNC] Sync request failed:', error); return NextResponse.json({ error: 'Sync failed', - details: error instanceof Error ? error.message : 'Unknown error' + details: safeErrorMessage(error, 'Unknown error') }, { status: 500 }); } } diff --git a/src/app/api/calendar/synology/connect/route.ts b/src/app/api/calendar/synology/connect/route.ts index 986a565..7bb139d 100644 --- a/src/app/api/calendar/synology/connect/route.ts +++ b/src/app/api/calendar/synology/connect/route.ts @@ -3,6 +3,7 @@ import { validateCredentials } from '@/lib/synology-calendar'; import { getServerSession } from 'next-auth'; import { authOptions } from "@/lib/auth"; import { prisma } from '@/lib/prisma'; +import { safeErrorMessage } from '@/lib/api-error'; export async function POST(req: Request) { try { @@ -80,7 +81,7 @@ export async function POST(req: Request) { } catch (error: any) { console.error('Error connecting Synology Calendar:', error); return NextResponse.json( - { error: error.message || 'Failed to connect Synology Calendar' }, + { error: safeErrorMessage(error, 'Failed to connect Synology Calendar') }, { status: 500 } ); } diff --git a/src/app/api/someday-lists/external/route.ts b/src/app/api/someday-lists/external/route.ts index 469e3de..2a094aa 100644 --- a/src/app/api/someday-lists/external/route.ts +++ b/src/app/api/someday-lists/external/route.ts @@ -4,6 +4,7 @@ import { getServerSession } from 'next-auth'; import { authOptions } from "@/lib/auth"; import { prisma } from '@/lib/prisma'; import { createGoogleClient, createGoogleTaskList } from '@/lib/google-tasks'; +import { safeErrorMessage } from '@/lib/api-error'; import { createMsTodoList } from '@/lib/microsoft-todo'; import { getOutlookAccessToken } from '@/lib/outlook-token'; import { createAppleReminderList } from '@/lib/apple-reminders'; @@ -104,7 +105,7 @@ export async function POST(req: NextRequest) { } catch (error: any) { console.error('Error creating external someday list:', error); return NextResponse.json( - { error: error.message || 'Failed to create list' }, + { error: safeErrorMessage(error, 'Failed to create list') }, { status: 500 } ); } diff --git a/src/app/api/tasks/import/route.ts b/src/app/api/tasks/import/route.ts index e389ed8..44181f9 100644 --- a/src/app/api/tasks/import/route.ts +++ b/src/app/api/tasks/import/route.ts @@ -4,6 +4,7 @@ import { getServerSession } from 'next-auth'; import { authOptions } from "@/lib/auth"; import { prisma } from '@/lib/prisma'; import { createGoogleClient, fetchGoogleTasks, fetchGoogleTaskLists } from '@/lib/google-tasks'; +import { safeErrorMessage } from '@/lib/api-error'; import { fetchMsTodoLists, fetchMsTodoTasks, isMsTodoTaskCompleted, fetchMsChecklistItems } from '@/lib/microsoft-todo'; import { getOutlookAccessToken } from '@/lib/outlook-token'; @@ -343,7 +344,7 @@ export async function POST(req: NextRequest) { } catch (error: unknown) { console.error('Import error:', error); - const message = error instanceof Error ? error.message : 'Import failed'; + const message = safeErrorMessage(error, 'Import failed'); return NextResponse.json({ error: message }, { status: 500 }); } } diff --git a/src/app/api/tasks/lists/route.ts b/src/app/api/tasks/lists/route.ts index 43eb457..c5c298b 100644 --- a/src/app/api/tasks/lists/route.ts +++ b/src/app/api/tasks/lists/route.ts @@ -4,6 +4,7 @@ import { authOptions } from "@/lib/auth"; import { prisma } from '@/lib/prisma'; import { createGoogleClient, fetchGoogleTaskLists } from '@/lib/google-tasks'; import { fetchMsTodoLists } from '@/lib/microsoft-todo'; +import { safeErrorMessage } from '@/lib/api-error'; import { getOutlookAccessToken } from '@/lib/outlook-token'; export const dynamic = 'force-dynamic'; @@ -83,7 +84,7 @@ export async function GET(req: NextRequest) { } catch (error: unknown) { console.error('Fetch lists error:', error); - const message = error instanceof Error ? error.message : 'Failed to fetch lists'; + const message = safeErrorMessage(error, 'Failed to fetch lists'); return NextResponse.json({ error: message }, { status: 500 }); } } diff --git a/src/app/api/tasks/sync/route.ts b/src/app/api/tasks/sync/route.ts index 3a3bfac..3236d68 100644 --- a/src/app/api/tasks/sync/route.ts +++ b/src/app/api/tasks/sync/route.ts @@ -4,6 +4,7 @@ import { authOptions } from "@/lib/auth"; import { prisma } from '@/lib/prisma'; import { createGoogleClient, createGoogleTask, updateGoogleTaskStatus, updateGoogleTask, deleteGoogleTask, fetchGoogleTasksForSync, GoogleTask } from '@/lib/google-tasks'; import { fetchMsTodoTasksForSync, fetchMsTodoTasksDelta, updateMsTodoTask, deleteMsTodoTask, createMsTodoTask, isMsTodoTaskCompleted } from '@/lib/microsoft-todo'; +import { safeErrorMessage } from '@/lib/api-error'; import { getOutlookAccessToken } from '@/lib/outlook-token'; // GET - Pull changes from Google Tasks into local DB @@ -567,7 +568,7 @@ export async function GET(req: NextRequest) { } catch (error: unknown) { console.error('Pull sync error:', error); - const message = error instanceof Error ? error.message : 'Pull sync failed'; + const message = safeErrorMessage(error, 'Pull sync failed'); return NextResponse.json({ error: message }, { status: 500 }); } } @@ -706,7 +707,7 @@ export async function PATCH(req: NextRequest) { } catch (error: unknown) { console.error('Sync error:', error); - const message = error instanceof Error ? error.message : 'Sync failed'; + const message = safeErrorMessage(error, 'Sync failed'); return NextResponse.json({ error: message }, { status: 500 }); } } @@ -839,7 +840,7 @@ export async function POST(req: NextRequest) { } catch (error: unknown) { console.error('Task creation sync error:', error); - const message = error instanceof Error ? error.message : 'Task creation sync failed'; + const message = safeErrorMessage(error, 'Task creation sync failed'); return NextResponse.json({ error: message }, { status: 500 }); } } diff --git a/src/app/api/user/import-data/route.ts b/src/app/api/user/import-data/route.ts index 4d2a572..440c881 100644 --- a/src/app/api/user/import-data/route.ts +++ b/src/app/api/user/import-data/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from "@/lib/auth"; import { prisma } from '@/lib/prisma'; +import { safeErrorMessage } from '@/lib/api-error'; interface ImportTask { id?: string; @@ -262,7 +263,7 @@ export async function POST(request: Request) { if (error instanceof SyntaxError) { message = 'Invalid JSON file'; } else if (error instanceof Error) { - message = `Import failed: ${error.message}`; + message = safeErrorMessage(error, 'Import failed'); } return NextResponse.json({ error: message }, { status: 500 }); } diff --git a/src/lib/api-error.ts b/src/lib/api-error.ts new file mode 100644 index 0000000..6573807 --- /dev/null +++ b/src/lib/api-error.ts @@ -0,0 +1,16 @@ +/** + * Sichere Fehlermeldung fuer API-Antworten. + * + * Mehrere Routen haben bislang `error.message` direkt in die JSON-Antwort geschrieben -- + * das kann Datenbankfehler, Dateipfade oder Provider-interne Details an den Client (und + * damit an jeden, der die API abfragt) durchreichen. In der Produktion gibt es deshalb nur + * noch die generische Fallback-Meldung; lokal beim Entwickeln weiterhin die echte Meldung, + * damit Fehlersuche nicht erschwert wird. Das vollstaendige Loggen des Fehlers bleibt + * Aufgabe der aufrufenden Stelle (meist schon vorhandenes `console.error` direkt davor). + */ +export function safeErrorMessage(error: unknown, fallback: string): string { + if (process.env.NODE_ENV !== 'production' && error instanceof Error && error.message) { + return error.message; + } + return fallback; +} diff --git a/src/lib/email.ts b/src/lib/email.ts index 071262f..861e9b5 100644 --- a/src/lib/email.ts +++ b/src/lib/email.ts @@ -20,7 +20,11 @@ function getTransporter() { port, secure, auth: { user, pass }, - tls: { rejectUnauthorized: false }, + // Kein rejectUnauthorized: false mehr -- das schaltete die Zertifikatspruefung fuer + // die SMTP-Verbindung komplett ab (Tuer fuer MITM auf Passwort-Reset-/Verifizierungs- + // Mails) und war seit dem allerersten Commit unbegruendet gesetzt. Der produktiv + // konfigurierte Host (Kasserver) hat ein gueltiges Zertifikat; keine Notwendigkeit, + // die Pruefung abzuschalten. }); } return _transporter;