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>
116 lines
4.8 KiB
TypeScript
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 });
|
|
}
|
|
}
|