My-Weekly-ToDo-List/src/app/api/calendar/sync/route.ts
mARTin-B78 8f017cb879
Some checks are pending
Playwright Tests / test (push) Waiting to run
Test and SonarQube / quality (push) Waiting to run
fix: Deploy-Skript darf Daten nicht mehr automatisch loeschen, Fehlermeldungen haerten
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>
2026-09-12 15:36:00 +02:00

116 lines
4.8 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
export const dynamic = 'force-dynamic';
import { getServerSession } from 'next-auth';
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 {
const session = await getServerSession(authOptions);
if (!session?.user?.email) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const { timeMin, timeMax, connectionId, forceRefresh } = body;
const timeMinDate = new Date(timeMin ?? Date.now());
const timeMaxDate = new Date(timeMax ?? Date.now() + 7 * 24 * 60 * 60 * 1000);
// Look up by email — session ID can be stale after DB restore/migration
const user = await prisma.user.findUnique({
where: { email: session.user.email },
include: { calendarConnections: true },
});
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
let connections = user.calendarConnections;
if (connectionId) {
connections = connections.filter(c => c.id === connectionId);
}
if (connections.length === 0) {
return NextResponse.json({ success: true, events: [], fromCache: true });
}
// FAST PATH: read from DB cache
const cachedEvents = await readCachedEvents(user.id, timeMinDate, timeMaxDate);
// Check which connections are stale
const staleChecks = await Promise.all(
connections.map(async conn => ({
conn,
stale: forceRefresh || await isCacheStale(conn.id, timeMinDate),
}))
);
const staleConnections = staleChecks.filter(c => c.stale).map(c => c.conn);
// Refresh stale connections
if (staleConnections.length > 0) {
const doRefresh = () => Promise.allSettled(
staleConnections.map(conn => {
const rc: RefreshableConnection = {
dbId: conn.id,
userId: user.id,
id: conn.id,
provider: conn.provider as 'google' | 'apple' | 'outlook' | 'synology',
accessToken: conn.accessToken,
refreshToken: conn.refreshToken ?? undefined,
expiresAt: conn.expiresAt ?? undefined,
calendars: conn.calendars as any,
};
return refreshConnectionCache(rc, timeMinDate, timeMaxDate);
})
);
if (forceRefresh) {
// BLOCKING: wait for fresh data when user explicitly requests refresh
await doRefresh();
// Run cross-sync rules after cache refresh (fire-and-forget, don't block response)
runSyncRules(user.id).catch(e => console.error('[cross-sync] Error running sync rules:', e));
const freshEvents = await readCachedEvents(user.id, timeMinDate, timeMaxDate);
return NextResponse.json({
success: true,
events: freshEvents,
count: freshEvents.length,
fromCache: false,
staleConnectionCount: 0,
});
} else {
// BACKGROUND: try to refresh within a short window; fall back to stale cache
const refreshWork = doRefresh().catch(e => console.error('[CACHE] Background refresh error:', e));
const timeout = new Promise<void>(resolve => setTimeout(resolve, 800));
await Promise.race([refreshWork, timeout]);
const maybeRefreshed = await readCachedEvents(user.id, timeMinDate, timeMaxDate);
return NextResponse.json({
success: true,
events: maybeRefreshed,
count: maybeRefreshed.length,
fromCache: true,
staleConnectionCount: staleConnections.length,
});
}
}
return NextResponse.json({
success: true,
events: cachedEvents,
count: cachedEvents.length,
fromCache: true,
staleConnectionCount: 0,
});
} catch (error) {
console.error('[CALENDAR SYNC] Sync request failed:', error);
return NextResponse.json({
error: 'Sync failed',
details: safeErrorMessage(error, 'Unknown error')
}, { status: 500 });
}
}