fix: Deploy-Skript darf Daten nicht mehr automatisch loeschen, Fehlermeldungen haerten
Some checks are pending
Playwright Tests / test (push) Waiting to run
Test and SonarQube / quality (push) Waiting to run

Erste drei Punkte aus dem TODO-Block L (Sicherheitsdurchgang vom 12.09.):

- scripts/update-app.sh: kein automatischer "prisma db push --accept-data-loss"-
  Fallback mehr, wenn "migrate deploy" fehlschlaegt. Das lief am 11.09. beim
  Boot-Lauf tatsaechlich unbeaufsichtigt durch. Ein fehlgeschlagenes Migrate
  bricht das Deployment jetzt sauber ab, exakt wie ein fehlgeschlagener Build.
- 14 API-Routen gaben error.message direkt an den Client zurueck (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. Logging unveraendert in console.error.
- src/lib/email.ts: tls.rejectUnauthorized: false entfernt -- stand seit dem
  allerersten Commit unbegruendet drin, produktiver SMTP-Host (Kasserver) hat
  ein gueltiges Zertifikat.

Verifiziert: Typecheck, Lint, 79 Unit-Tests, Produktions-Build alle gruen.

v1.123.2

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-09-12 15:36:00 +02:00
parent e62294a204
commit 8f017cb879
15 changed files with 75 additions and 20 deletions

View File

@ -2,6 +2,27 @@
All notable changes to this project are documented in this file. 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 ## [1.123.1] - 2026-09-12
### Fixed ### Fixed

View File

@ -1,6 +1,6 @@
{ {
"name": "my-weekly-todo-list", "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", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -104,11 +104,15 @@ main() {
npm ci npm ci
fi 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) ---" log "--- Updating database schema (safe migrate) ---"
npx prisma migrate deploy || { npx prisma migrate deploy
log "migrate deploy failed, falling back to db push..."
npx prisma db push --accept-data-loss
}
log "--- Regenerating Prisma client ---" log "--- Regenerating Prisma client ---"
npx prisma generate npx prisma generate

View File

@ -1,4 +1,5 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { safeErrorMessage } from '@/lib/api-error';
import { validateCredentials } from '@/lib/apple-calendar'; import { validateCredentials } from '@/lib/apple-calendar';
import { CalendarConnection } from '@/lib/calendar-sync'; import { CalendarConnection } from '@/lib/calendar-sync';
import { getServerSession } from 'next-auth'; import { getServerSession } from 'next-auth';
@ -82,7 +83,7 @@ export async function POST(req: Request) {
} catch (error: any) { } catch (error: any) {
console.error('Error connecting Apple Calendar:', error); console.error('Error connecting Apple Calendar:', error);
return NextResponse.json( return NextResponse.json(
{ error: error.message || 'Failed to connect Apple Calendar' }, { error: safeErrorMessage(error, 'Failed to connect Apple Calendar') },
{ status: 500 } { status: 500 }
); );
} }

View File

@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth'; import { getServerSession } from 'next-auth';
import { getUpcomingEvents } from '@/lib/apple-calendar'; import { getUpcomingEvents } from '@/lib/apple-calendar';
import { authOptions } from "@/lib/auth"; import { authOptions } from "@/lib/auth";
import { safeErrorMessage } from '@/lib/api-error';
export async function POST(req: Request) { export async function POST(req: Request) {
try { try {
@ -36,7 +37,7 @@ export async function POST(req: Request) {
} catch (error: any) { } catch (error: any) {
console.error('Error syncing Apple Calendar:', error); console.error('Error syncing Apple Calendar:', error);
return NextResponse.json( return NextResponse.json(
{ error: error.message || 'Failed to sync Apple Calendar' }, { error: safeErrorMessage(error, 'Failed to sync Apple Calendar') },
{ status: 500 } { status: 500 }
); );
} }

View File

@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { safeErrorMessage } from '@/lib/api-error';
import { getServerSession } from 'next-auth'; import { getServerSession } from 'next-auth';
import { authOptions } from "@/lib/auth"; import { authOptions } from "@/lib/auth";
import { prisma } from '@/lib/prisma'; import { prisma } from '@/lib/prisma';
@ -98,7 +99,7 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ event }); return NextResponse.json({ event });
} catch (error: any) { } catch (error: any) {
console.error('Error creating event:', error); 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 }); return NextResponse.json({ event });
} catch (error: any) { } catch (error: any) {
console.error('Error updating event:', error); 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 }); return NextResponse.json({ success: true });
} catch (error: any) { } catch (error: any) {
console.error('Error deleting event:', error); 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 });
} }
} }

View File

@ -5,6 +5,7 @@ import { authOptions } from "@/lib/auth";
import { prisma } from '@/lib/prisma'; import { prisma } from '@/lib/prisma';
import { readCachedEvents, isCacheStale, refreshConnectionCache, RefreshableConnection } from '@/lib/calendar-cache'; import { readCachedEvents, isCacheStale, refreshConnectionCache, RefreshableConnection } from '@/lib/calendar-cache';
import { runSyncRules } from '@/lib/calendar-cross-sync'; import { runSyncRules } from '@/lib/calendar-cross-sync';
import { safeErrorMessage } from '@/lib/api-error';
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
@ -108,7 +109,7 @@ export async function POST(request: NextRequest) {
console.error('[CALENDAR SYNC] Sync request failed:', error); console.error('[CALENDAR SYNC] Sync request failed:', error);
return NextResponse.json({ return NextResponse.json({
error: 'Sync failed', error: 'Sync failed',
details: error instanceof Error ? error.message : 'Unknown error' details: safeErrorMessage(error, 'Unknown error')
}, { status: 500 }); }, { status: 500 });
} }
} }

View File

@ -3,6 +3,7 @@ import { validateCredentials } from '@/lib/synology-calendar';
import { getServerSession } from 'next-auth'; import { getServerSession } from 'next-auth';
import { authOptions } from "@/lib/auth"; import { authOptions } from "@/lib/auth";
import { prisma } from '@/lib/prisma'; import { prisma } from '@/lib/prisma';
import { safeErrorMessage } from '@/lib/api-error';
export async function POST(req: Request) { export async function POST(req: Request) {
try { try {
@ -80,7 +81,7 @@ export async function POST(req: Request) {
} catch (error: any) { } catch (error: any) {
console.error('Error connecting Synology Calendar:', error); console.error('Error connecting Synology Calendar:', error);
return NextResponse.json( return NextResponse.json(
{ error: error.message || 'Failed to connect Synology Calendar' }, { error: safeErrorMessage(error, 'Failed to connect Synology Calendar') },
{ status: 500 } { status: 500 }
); );
} }

View File

@ -4,6 +4,7 @@ import { getServerSession } from 'next-auth';
import { authOptions } from "@/lib/auth"; import { authOptions } from "@/lib/auth";
import { prisma } from '@/lib/prisma'; import { prisma } from '@/lib/prisma';
import { createGoogleClient, createGoogleTaskList } from '@/lib/google-tasks'; import { createGoogleClient, createGoogleTaskList } from '@/lib/google-tasks';
import { safeErrorMessage } from '@/lib/api-error';
import { createMsTodoList } from '@/lib/microsoft-todo'; import { createMsTodoList } from '@/lib/microsoft-todo';
import { getOutlookAccessToken } from '@/lib/outlook-token'; import { getOutlookAccessToken } from '@/lib/outlook-token';
import { createAppleReminderList } from '@/lib/apple-reminders'; import { createAppleReminderList } from '@/lib/apple-reminders';
@ -104,7 +105,7 @@ export async function POST(req: NextRequest) {
} catch (error: any) { } catch (error: any) {
console.error('Error creating external someday list:', error); console.error('Error creating external someday list:', error);
return NextResponse.json( return NextResponse.json(
{ error: error.message || 'Failed to create list' }, { error: safeErrorMessage(error, 'Failed to create list') },
{ status: 500 } { status: 500 }
); );
} }

View File

@ -4,6 +4,7 @@ import { getServerSession } from 'next-auth';
import { authOptions } from "@/lib/auth"; import { authOptions } from "@/lib/auth";
import { prisma } from '@/lib/prisma'; import { prisma } from '@/lib/prisma';
import { createGoogleClient, fetchGoogleTasks, fetchGoogleTaskLists } from '@/lib/google-tasks'; import { createGoogleClient, fetchGoogleTasks, fetchGoogleTaskLists } from '@/lib/google-tasks';
import { safeErrorMessage } from '@/lib/api-error';
import { fetchMsTodoLists, fetchMsTodoTasks, isMsTodoTaskCompleted, fetchMsChecklistItems } from '@/lib/microsoft-todo'; import { fetchMsTodoLists, fetchMsTodoTasks, isMsTodoTaskCompleted, fetchMsChecklistItems } from '@/lib/microsoft-todo';
import { getOutlookAccessToken } from '@/lib/outlook-token'; import { getOutlookAccessToken } from '@/lib/outlook-token';
@ -343,7 +344,7 @@ export async function POST(req: NextRequest) {
} catch (error: unknown) { } catch (error: unknown) {
console.error('Import error:', error); 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 }); return NextResponse.json({ error: message }, { status: 500 });
} }
} }

View File

@ -4,6 +4,7 @@ import { authOptions } from "@/lib/auth";
import { prisma } from '@/lib/prisma'; import { prisma } from '@/lib/prisma';
import { createGoogleClient, fetchGoogleTaskLists } from '@/lib/google-tasks'; import { createGoogleClient, fetchGoogleTaskLists } from '@/lib/google-tasks';
import { fetchMsTodoLists } from '@/lib/microsoft-todo'; import { fetchMsTodoLists } from '@/lib/microsoft-todo';
import { safeErrorMessage } from '@/lib/api-error';
import { getOutlookAccessToken } from '@/lib/outlook-token'; import { getOutlookAccessToken } from '@/lib/outlook-token';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@ -83,7 +84,7 @@ export async function GET(req: NextRequest) {
} catch (error: unknown) { } catch (error: unknown) {
console.error('Fetch lists error:', error); 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 }); return NextResponse.json({ error: message }, { status: 500 });
} }
} }

View File

@ -4,6 +4,7 @@ import { authOptions } from "@/lib/auth";
import { prisma } from '@/lib/prisma'; import { prisma } from '@/lib/prisma';
import { createGoogleClient, createGoogleTask, updateGoogleTaskStatus, updateGoogleTask, deleteGoogleTask, fetchGoogleTasksForSync, GoogleTask } from '@/lib/google-tasks'; import { createGoogleClient, createGoogleTask, updateGoogleTaskStatus, updateGoogleTask, deleteGoogleTask, fetchGoogleTasksForSync, GoogleTask } from '@/lib/google-tasks';
import { fetchMsTodoTasksForSync, fetchMsTodoTasksDelta, updateMsTodoTask, deleteMsTodoTask, createMsTodoTask, isMsTodoTaskCompleted } from '@/lib/microsoft-todo'; import { fetchMsTodoTasksForSync, fetchMsTodoTasksDelta, updateMsTodoTask, deleteMsTodoTask, createMsTodoTask, isMsTodoTaskCompleted } from '@/lib/microsoft-todo';
import { safeErrorMessage } from '@/lib/api-error';
import { getOutlookAccessToken } from '@/lib/outlook-token'; import { getOutlookAccessToken } from '@/lib/outlook-token';
// GET - Pull changes from Google Tasks into local DB // GET - Pull changes from Google Tasks into local DB
@ -567,7 +568,7 @@ export async function GET(req: NextRequest) {
} catch (error: unknown) { } catch (error: unknown) {
console.error('Pull sync error:', error); 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 }); return NextResponse.json({ error: message }, { status: 500 });
} }
} }
@ -706,7 +707,7 @@ export async function PATCH(req: NextRequest) {
} catch (error: unknown) { } catch (error: unknown) {
console.error('Sync error:', error); 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 }); return NextResponse.json({ error: message }, { status: 500 });
} }
} }
@ -839,7 +840,7 @@ export async function POST(req: NextRequest) {
} catch (error: unknown) { } catch (error: unknown) {
console.error('Task creation sync error:', error); 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 }); return NextResponse.json({ error: message }, { status: 500 });
} }
} }

View File

@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth'; import { getServerSession } from 'next-auth';
import { authOptions } from "@/lib/auth"; import { authOptions } from "@/lib/auth";
import { prisma } from '@/lib/prisma'; import { prisma } from '@/lib/prisma';
import { safeErrorMessage } from '@/lib/api-error';
interface ImportTask { interface ImportTask {
id?: string; id?: string;
@ -262,7 +263,7 @@ export async function POST(request: Request) {
if (error instanceof SyntaxError) { if (error instanceof SyntaxError) {
message = 'Invalid JSON file'; message = 'Invalid JSON file';
} else if (error instanceof Error) { } else if (error instanceof Error) {
message = `Import failed: ${error.message}`; message = safeErrorMessage(error, 'Import failed');
} }
return NextResponse.json({ error: message }, { status: 500 }); return NextResponse.json({ error: message }, { status: 500 });
} }

16
src/lib/api-error.ts Normal file
View File

@ -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;
}

View File

@ -20,7 +20,11 @@ function getTransporter() {
port, port,
secure, secure,
auth: { user, pass }, 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; return _transporter;