Compare commits

..

2 Commits

Author SHA1 Message Date
f53ae184ea fix: Kalender-Verbindungen ueber user-include liefern verschluesseltes Token
Some checks failed
Playwright Tests / test (push) Has been cancelled
Test and SonarQube / quality (push) Has been cancelled
Die accessToken/refreshToken-Entschluesselung (Prisma-Extension in lib/prisma.ts)
faengt nur direkte calendarConnection-Queries ab, keine ueber user.findUnique({include})
geladenen Relationen. Outlook-Tokens werden bei jedem Refresh neu geschrieben (also
verschluesselt) und wurden dadurch als Chiffretext an Microsoft Graph geschickt -> 401
"JWT is not well formed". Apple/Synology blieben unbemerkt, weil ihre statischen
App-Passwoerter nie neu geschrieben und damit nie verschluesselt wurden.

Betroffen: calendar/events, calendar/sync, calendar/background-sync,
calendar/connections. Alle vier laden CalendarConnection jetzt direkt statt ueber
user-include.

v1.129.3

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 11:48:36 +02:00
e579fc0be8 fix: Kalender-Abfragefenster skaliert mit der eingestellten Tage-Ansicht
Some checks are pending
Playwright Tests / test (push) Waiting to run
Test and SonarQube / quality (push) Waiting to run
fetchCalendarEvents holte serverseitig immer nur currentWeekStart -7/+14 Tage, egal
wie breit die Ansicht (bis zu 31 Tage) tatsächlich war. Spalten jenseits von Tag 14
blieben leer, der "all day"-Zaehler zaehlte nur die geladenen Termine. timeMax
skaliert jetzt mit viewDays (max(14, viewDays + 7) Tage vorwaerts), 7-Tage-Standard
unveraendert.

v1.129.2

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 23:27:05 +02:00
7 changed files with 86 additions and 21 deletions

View File

@ -2,6 +2,49 @@
All notable changes to this project are documented in this file.
## [1.129.3] - 2026-09-15
### Fixed
- **"Failed to update event" bei Outlook-Terminen** ("IDX14100: JWT is not well formed"
in den Server-Logs). Die Verschlüsselung von `accessToken`/`refreshToken` in
`CalendarConnection` (siehe [1.x] Security-Härtung) läuft als Prisma-Client-Extension
ausschließlich auf direkten `calendarConnection`-Queries — Next.js/Prisma-Extensions
fangen keine verschachtelten `include`-Relationen ab, die über ein anderes Modell
geladen werden. Vier Routen luden Verbindungen aber genau so:
`prisma.user.findUnique({ include: { calendarConnections: true } })`. Für Apple/Synology
(statisches App-Passwort, nie neu geschrieben) blieb das unbemerkt, weil deren Zeilen nie
verschlüsselt wurden. Outlook-Tokens werden dagegen bei jedem Refresh neu geschrieben und
damit verschlüsselt — ab dann schickte die App den Chiffretext statt des echten Tokens an
Microsoft Graph, die Anfrage lief mit 401 auf.
- Betroffen und korrigiert: `api/calendar/events/route.ts` (Termin erstellen/ändern/
löschen — der hier gemeldete Fehler), `api/calendar/sync/route.ts` (normale Anzeige-
Synchronisation), `api/calendar/background-sync/route.ts` (Hintergrund-Sync),
`api/calendar/connections/route.ts` (Verbindungsliste, inkl. Apple/Synology-
Farbaktualisierung im Hintergrund). Alle vier laden `CalendarConnection` jetzt per
direktem `calendarConnection.findMany`/`findFirst` statt über `user`-`include`.
Verifiziert nicht nur mit Typecheck/Build: Wegwerf-Postgres (Docker) aufgesetzt, eine
`CalendarConnection` mit echtem Prisma-Client (inkl. Verschlüsselungs-Extension)
angelegt — Rohwert in der DB trägt das `enc1:`-Präfix. Der alte `include`-Pfad lieferte
exakt den Chiffretext zurück (Reproduktion des Produktionsfehlers), der neue direkte
Query-Pfad das korrekte Klartext-Token.
## [1.129.2] - 2026-09-14
### Fixed
- **Kalendertermine und der "All day"-Zähler fehlten in weiten Ansichten** (Tage-Auswahl
> 14, z.B. 21 oder 31 Tage): `fetchCalendarEvents` fragte serverseitig immer nur ein
festes Fenster von `currentWeekStart - 7` bis `currentWeekStart + 14` Tage ab, egal wie
breit die Ansicht tatsächlich eingestellt war. Spalten jenseits von Tag 14 blieben
dadurch leer, und der Zähler neben "all day" zählte nur die tatsächlich geladenen
Termine mit — beides sah nach fehlenden Daten aus, obwohl die Termine existierten.
`timeMax` skaliert jetzt mit `viewDays` (`currentWeekStart + max(14, viewDays + 7)`
Tage), der Standard-7-Tage-Fall bleibt unverändert (weiterhin 14 Tage Vorlauf).
Verifiziert: Typecheck und Produktions-Build grün (Scratchpad-Klon), 118 Unit-Tests
grün, Datumsrechnung für viewDays 7/14/21/31 per Skript gegengeprüft — das neue Fenster
deckt in jedem Fall den letzten sichtbaren Tag plus Puffer ab.
## [1.129.1] - 2026-09-14
### Fixed

View File

@ -1,6 +1,6 @@
{
"name": "my-weekly-todo-list",
"version": "1.129.1",
"version": "1.129.3",
"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": {

View File

@ -16,10 +16,17 @@ export async function POST(request: NextRequest) {
const user = await prisma.user.findUnique({
where: { email: session.user.email },
include: { calendarConnections: true },
select: { id: true },
});
if (!user || user.calendarConnections.length === 0) {
if (!user) {
return NextResponse.json({ queued: 0 });
}
// Queried directly (not via user's `include`) so the accessToken/refreshToken
// decryption hook in lib/prisma.ts runs — see comment in calendar/sync/route.ts.
const connections = await prisma.calendarConnection.findMany({ where: { userId: user.id } });
if (connections.length === 0) {
return NextResponse.json({ queued: 0 });
}
@ -28,7 +35,7 @@ export async function POST(request: NextRequest) {
const tMax = timeMax ? new Date(timeMax) : new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000);
const staleChecks = await Promise.all(
user.calendarConnections.map(async conn => ({
connections.map(async conn => ({
conn,
stale: forceRefresh || await isCacheStale(conn.id, tMin),
}))

View File

@ -21,7 +21,7 @@ export async function GET(request: NextRequest) {
// Find user by email
const user = await prisma.user.findUnique({
where: { email: session.user.email },
include: { calendarConnections: true }
select: { id: true }
});
if (!user) {
@ -31,9 +31,13 @@ export async function GET(request: NextRequest) {
);
}
// Queried directly (not via user's `include`) so the accessToken/refreshToken
// decryption hook in lib/prisma.ts runs — see comment in calendar/sync/route.ts.
const rawConnections = await prisma.calendarConnection.findMany({ where: { userId: user.id } });
// Return calendar connections (without sensitive tokens)
// Prune deleted calendars from providers that support live listing
const connections = user.calendarConnections.map((conn) => {
const connections = rawConnections.map((conn) => {
let calendars = conn.calendars as any[] | null;
// For Apple connections, filter out VTODO/Reminders collections

View File

@ -10,12 +10,16 @@ import { upsertCachedEvent, deleteCachedEvent } from '@/lib/calendar-cache';
async function findConnectionForCalendar(email: string, calendarId: string) {
const user = await prisma.user.findUnique({
where: { email },
include: { calendarConnections: true }
select: { id: true }
});
if (!user) return null;
for (const conn of user.calendarConnections) {
// Queried directly (not via user's `include`) so the accessToken/refreshToken
// decryption hook in lib/prisma.ts runs — see comment in calendar/sync/route.ts.
const connections = await prisma.calendarConnection.findMany({ where: { userId: user.id } });
for (const conn of connections) {
if (conn.calendars && Array.isArray(conn.calendars)) {
const calendars = conn.calendars as any[];
if (calendars.some(c => c.id === calendarId)) {

View File

@ -23,14 +23,18 @@ export async function POST(request: NextRequest) {
// 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 },
select: { id: true },
});
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
let connections = user.calendarConnections;
// calendarConnection must be queried directly (not via user's `include`) so the
// accessToken/refreshToken decryption hook in lib/prisma.ts actually runs — Prisma
// client extensions only intercept top-level queries on the model, not nested
// relation loads reached through another model's `include`.
let connections = await prisma.calendarConnection.findMany({ where: { userId: user.id } });
if (connectionId) {
connections = connections.filter(c => c.id === connectionId);
}

View File

@ -1397,16 +1397,22 @@ export default function WeeklyView() {
startSync();
setIsFetchingCalendar(true);
try {
// timeMax must cover the whole configured view (up to MAX_VIEW_DAYS=31), not just
// the default 7-day week — otherwise columns past day 14 silently show no events
// and the all-day count badge undercounts (it only tallies fetched events).
const forwardDays = Math.max(14, viewDays + 7);
const timeMin = new Date(
currentWeekStart.getTime() - 7 * 24 * 60 * 60 * 1000,
).toISOString();
const timeMax = new Date(
currentWeekStart.getTime() + forwardDays * 24 * 60 * 60 * 1000,
).toISOString();
const response = await fetch("/api/calendar/sync", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
timeMin: new Date(
currentWeekStart.getTime() - 7 * 24 * 60 * 60 * 1000,
).toISOString(),
timeMax: new Date(
currentWeekStart.getTime() + 14 * 24 * 60 * 60 * 1000,
).toISOString(),
timeMin,
timeMax,
forceRefresh,
...(connectionId ? { connectionId } : {}),
}),
@ -1425,10 +1431,7 @@ export default function WeeklyView() {
fetch("/api/calendar/sync", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
timeMin: new Date(currentWeekStart.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
timeMax: new Date(currentWeekStart.getTime() + 14 * 24 * 60 * 60 * 1000).toISOString(),
}),
body: JSON.stringify({ timeMin, timeMax }),
}).then(r => r.json()).then(d => {
if (d.events) setRawCalendarEvents(d.events);
}).catch(() => {});
@ -1447,7 +1450,7 @@ export default function WeeklyView() {
setIsFetchingCalendar(false);
endSync();
}
}, [currentWeekStart, startSync, endSync]);
}, [currentWeekStart, viewDays, startSync, endSync]);
// Weather fetch
const fetchWeather = useCallback(async () => {