Compare commits
No commits in common. "main" and "v1.129.1" have entirely different histories.
43
CHANGELOG.md
43
CHANGELOG.md
@ -2,49 +2,6 @@
|
|||||||
|
|
||||||
All notable changes to this project are documented in this file.
|
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
|
## [1.129.1] - 2026-09-14
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "my-weekly-todo-list",
|
"name": "my-weekly-todo-list",
|
||||||
"version": "1.129.3",
|
"version": "1.129.1",
|
||||||
"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": {
|
||||||
|
|||||||
@ -16,17 +16,10 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { email: session.user.email },
|
where: { email: session.user.email },
|
||||||
select: { id: true },
|
include: { calendarConnections: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user || user.calendarConnections.length === 0) {
|
||||||
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 });
|
return NextResponse.json({ queued: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,7 +28,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const tMax = timeMax ? new Date(timeMax) : new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000);
|
const tMax = timeMax ? new Date(timeMax) : new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
const staleChecks = await Promise.all(
|
const staleChecks = await Promise.all(
|
||||||
connections.map(async conn => ({
|
user.calendarConnections.map(async conn => ({
|
||||||
conn,
|
conn,
|
||||||
stale: forceRefresh || await isCacheStale(conn.id, tMin),
|
stale: forceRefresh || await isCacheStale(conn.id, tMin),
|
||||||
}))
|
}))
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export async function GET(request: NextRequest) {
|
|||||||
// Find user by email
|
// Find user by email
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { email: session.user.email },
|
where: { email: session.user.email },
|
||||||
select: { id: true }
|
include: { calendarConnections: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@ -31,13 +31,9 @@ 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)
|
// Return calendar connections (without sensitive tokens)
|
||||||
// Prune deleted calendars from providers that support live listing
|
// Prune deleted calendars from providers that support live listing
|
||||||
const connections = rawConnections.map((conn) => {
|
const connections = user.calendarConnections.map((conn) => {
|
||||||
let calendars = conn.calendars as any[] | null;
|
let calendars = conn.calendars as any[] | null;
|
||||||
|
|
||||||
// For Apple connections, filter out VTODO/Reminders collections
|
// For Apple connections, filter out VTODO/Reminders collections
|
||||||
|
|||||||
@ -10,16 +10,12 @@ import { upsertCachedEvent, deleteCachedEvent } from '@/lib/calendar-cache';
|
|||||||
async function findConnectionForCalendar(email: string, calendarId: string) {
|
async function findConnectionForCalendar(email: string, calendarId: string) {
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { email },
|
where: { email },
|
||||||
select: { id: true }
|
include: { calendarConnections: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
|
|
||||||
// Queried directly (not via user's `include`) so the accessToken/refreshToken
|
for (const conn of user.calendarConnections) {
|
||||||
// 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)) {
|
if (conn.calendars && Array.isArray(conn.calendars)) {
|
||||||
const calendars = conn.calendars as any[];
|
const calendars = conn.calendars as any[];
|
||||||
if (calendars.some(c => c.id === calendarId)) {
|
if (calendars.some(c => c.id === calendarId)) {
|
||||||
|
|||||||
@ -23,18 +23,14 @@ export async function POST(request: NextRequest) {
|
|||||||
// Look up by email — session ID can be stale after DB restore/migration
|
// Look up by email — session ID can be stale after DB restore/migration
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { email: session.user.email },
|
where: { email: session.user.email },
|
||||||
select: { id: true },
|
include: { calendarConnections: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// calendarConnection must be queried directly (not via user's `include`) so the
|
let connections = user.calendarConnections;
|
||||||
// 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) {
|
if (connectionId) {
|
||||||
connections = connections.filter(c => c.id === connectionId);
|
connections = connections.filter(c => c.id === connectionId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1397,22 +1397,16 @@ export default function WeeklyView() {
|
|||||||
startSync();
|
startSync();
|
||||||
setIsFetchingCalendar(true);
|
setIsFetchingCalendar(true);
|
||||||
try {
|
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", {
|
const response = await fetch("/api/calendar/sync", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
timeMin,
|
timeMin: new Date(
|
||||||
timeMax,
|
currentWeekStart.getTime() - 7 * 24 * 60 * 60 * 1000,
|
||||||
|
).toISOString(),
|
||||||
|
timeMax: new Date(
|
||||||
|
currentWeekStart.getTime() + 14 * 24 * 60 * 60 * 1000,
|
||||||
|
).toISOString(),
|
||||||
forceRefresh,
|
forceRefresh,
|
||||||
...(connectionId ? { connectionId } : {}),
|
...(connectionId ? { connectionId } : {}),
|
||||||
}),
|
}),
|
||||||
@ -1431,7 +1425,10 @@ export default function WeeklyView() {
|
|||||||
fetch("/api/calendar/sync", {
|
fetch("/api/calendar/sync", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ timeMin, timeMax }),
|
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(),
|
||||||
|
}),
|
||||||
}).then(r => r.json()).then(d => {
|
}).then(r => r.json()).then(d => {
|
||||||
if (d.events) setRawCalendarEvents(d.events);
|
if (d.events) setRawCalendarEvents(d.events);
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
@ -1450,7 +1447,7 @@ export default function WeeklyView() {
|
|||||||
setIsFetchingCalendar(false);
|
setIsFetchingCalendar(false);
|
||||||
endSync();
|
endSync();
|
||||||
}
|
}
|
||||||
}, [currentWeekStart, viewDays, startSync, endSync]);
|
}, [currentWeekStart, startSync, endSync]);
|
||||||
|
|
||||||
// Weather fetch
|
// Weather fetch
|
||||||
const fetchWeather = useCallback(async () => {
|
const fetchWeather = useCallback(async () => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user