fix: resolve stale session ID in someday-lists and calendar/events routes

All handlers in someday-lists/route.ts and calendar/events/route.ts were
using (session.user as any).id which can be stale after DB restore/migration.
Fixed to look up by session.user.email instead — matching the pattern already
established in tasks/route.ts and calendar/sync/route.ts.

This was causing other users to see empty someday lists while their tasks
(fetched via the already-fixed tasks route) still had somedayListId references,
triggering false RESCUE recoveries on every load.

Also fixed outlook/callback/route.ts fallback which could still use stale ID.

v1.81.19
This commit is contained in:
mARTin 2026-04-03 11:04:32 +02:00
parent 4cab83e545
commit 0371532e7d
4 changed files with 56 additions and 40 deletions

View File

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

@ -48,13 +48,17 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
} }
const userId = (session.user as any).id; const connection = await findConnectionForCalendar(session.user.email, calendarId);
const connection = await findConnectionForCalendar(userId, calendarId);
if (!connection) { if (!connection) {
return NextResponse.json({ error: 'Calendar connection not found' }, { status: 404 }); return NextResponse.json({ error: 'Calendar connection not found' }, { status: 404 });
} }
// Resolve real DB user ID for cache operations
const dbUser = await prisma.user.findUnique({ where: { email: session.user.email }, select: { id: true } });
const userId = dbUser?.id;
if (!userId) return NextResponse.json({ error: 'User not found' }, { status: 404 });
const event = await createCalendarEvent(connection, calendarId, { const event = await createCalendarEvent(connection, calendarId, {
title, title,
description, description,
@ -114,13 +118,16 @@ export async function PATCH(request: NextRequest) {
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
} }
const userId = (session.user as any).id; const connection = await findConnectionForCalendar(session.user.email, calendarId);
const connection = await findConnectionForCalendar(userId, calendarId);
if (!connection) { if (!connection) {
return NextResponse.json({ error: 'Calendar connection not found' }, { status: 404 }); return NextResponse.json({ error: 'Calendar connection not found' }, { status: 404 });
} }
const dbUser2 = await prisma.user.findUnique({ where: { email: session.user.email }, select: { id: true } });
const userId = dbUser2?.id;
if (!userId) return NextResponse.json({ error: 'User not found' }, { status: 404 });
// For "all events" edit mode on recurring events, use the series master ID // For "all events" edit mode on recurring events, use the series master ID
let targetEventId = eventId; let targetEventId = eventId;
if (editMode === 'all' && recurringEventId) { if (editMode === 'all' && recurringEventId) {
@ -191,13 +198,16 @@ export async function DELETE(request: NextRequest) {
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
} }
const userId = (session.user as any).id; const connection = await findConnectionForCalendar(session.user.email, calendarId);
const connection = await findConnectionForCalendar(userId, calendarId);
if (!connection) { if (!connection) {
return NextResponse.json({ error: 'Calendar connection not found' }, { status: 404 }); return NextResponse.json({ error: 'Calendar connection not found' }, { status: 404 });
} }
const dbUser3 = await prisma.user.findUnique({ where: { email: session.user.email }, select: { id: true } });
const userId = dbUser3?.id;
if (!userId) return NextResponse.json({ error: 'User not found' }, { status: 404 });
await deleteCalendarEvent(connection, calendarId, eventId, deleteMode); await deleteCalendarEvent(connection, calendarId, eventId, deleteMode);
// Remove from cache - await to ensure consistency // Remove from cache - await to ensure consistency

View File

@ -38,10 +38,8 @@ export async function GET(request: NextRequest) {
// Fetch user's calendars to store initial list // Fetch user's calendars to store initial list
const calendars = await getUserCalendars(accessToken); const calendars = await getUserCalendars(accessToken);
const userId = (session.user as any).id; // Always look up by email — session ID can be stale after DB restore/migration
const user = userId const user = await prisma.user.findUnique({ where: { email: session.user.email } });
? await prisma.user.findUnique({ where: { id: userId } })
: await prisma.user.findUnique({ where: { email: session.user.email } });
if (!user) { if (!user) {
return NextResponse.redirect(new URL('/auth/login', baseUrl)); return NextResponse.redirect(new URL('/auth/login', baseUrl));

View File

@ -6,18 +6,29 @@ import { notifyUser } from '@/lib/sse';
const prisma = new PrismaClient(); const prisma = new PrismaClient();
async function resolveUserId(email: string): Promise<string | null> {
const user = await prisma.user.findUnique({
where: { email },
select: { id: true }
});
return user?.id ?? null;
}
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
if (!session?.user) { if (!session?.user?.email) {
return NextResponse.json( return NextResponse.json(
{ error: 'Unauthorized' }, { error: 'Unauthorized' },
{ status: 401 } { status: 401 }
); );
} }
const userId = (session.user as any).id; const userId = await resolveUserId(session.user.email);
if (!userId) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
const lists = await prisma.somedayList.findMany({ const lists = await prisma.somedayList.findMany({
where: { userId }, where: { userId },
@ -44,14 +55,18 @@ export async function POST(request: NextRequest) {
try { try {
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
if (!session?.user) { if (!session?.user?.email) {
return NextResponse.json( return NextResponse.json(
{ error: 'Unauthorized' }, { error: 'Unauthorized' },
{ status: 401 } { status: 401 }
); );
} }
const userId = (session.user as any).id; const userId = await resolveUserId(session.user.email);
if (!userId) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
const { title } = await request.json(); const { title } = await request.json();
if (!title) { if (!title) {
@ -92,13 +107,18 @@ export async function DELETE(request: NextRequest) {
try { try {
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
if (!session?.user) { if (!session?.user?.email) {
return NextResponse.json( return NextResponse.json(
{ error: 'Unauthorized' }, { error: 'Unauthorized' },
{ status: 401 } { status: 401 }
); );
} }
const userId = await resolveUserId(session.user.email);
if (!userId) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
const id = searchParams.get('id'); const id = searchParams.get('id');
@ -114,27 +134,13 @@ export async function DELETE(request: NextRequest) {
where: { id } where: { id }
}); });
if (!list || list.userId !== (session.user as any).id) { if (!list || list.userId !== userId) {
return NextResponse.json( return NextResponse.json(
{ error: 'List not found or unauthorized' }, { error: 'List not found or unauthorized' },
{ status: 404 } { status: 404 }
); );
} }
// Delete list (tasks cascade delete is not set in schema for tasks->list, check schema)
// In schema: tasks defined as `tasks Task[]`.
// We updated schema: `user User ... onDelete: Cascade`. `tasks` are separate.
// We need to verify if deleting list deletes tasks or unlinks them.
// Schema: `somedayList SomedayList? @relation...`
// If we want cascade delete tasks in the list, we should check relations.
// Prisma default is usually not cascade for optional relations unless specified.
// Let's assume we want to keep tasks or delete them? Usually delete list = delete tasks in it.
// Let's explicitly delete tasks first or rely on schema if configured.
// Schema update I did: `tasks Task[]`. `Task` has `somedayListId`.
// I didn't add `onDelete: Cascade` to the `somedayList` relation in `Task`.
// So I should clean up tasks manually or update schema.
// For now, let's delete tasks in the list.
// Soft-delete tasks in this list (they can be recovered from trash) // Soft-delete tasks in this list (they can be recovered from trash)
await prisma.task.updateMany({ await prisma.task.updateMany({
where: { somedayListId: id }, where: { somedayListId: id },
@ -145,7 +151,7 @@ export async function DELETE(request: NextRequest) {
where: { id } where: { id }
}); });
notifyUser((session.user as any).id, "list-changed", { action: "deleted" }); notifyUser(userId, "list-changed", { action: "deleted" });
return NextResponse.json({ success: true }); return NextResponse.json({ success: true });
} catch (error) { } catch (error) {
console.error('Error deleting someday list:', error); console.error('Error deleting someday list:', error);
@ -160,32 +166,34 @@ export async function PATCH(request: NextRequest) {
try { try {
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
if (!session?.user) { if (!session?.user?.email) {
return NextResponse.json( return NextResponse.json(
{ error: 'Unauthorized' }, { error: 'Unauthorized' },
{ status: 401 } { status: 401 }
); );
} }
const userId = await resolveUserId(session.user.email);
if (!userId) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
const body = await request.json(); const body = await request.json();
// Handle Reordering (Array of { id, order }) // Handle Reordering (Array of { id, order })
if (Array.isArray(body)) { if (Array.isArray(body)) {
const updates = body.map(async (item: { id: string; order: number }) => { const updates = body.map(async (item: { id: string; order: number }) => {
// Verify ownership for each or just assume if one matches?
// Better to be safe, but for performance in batch, we might trust ID if valid.
// Let's verify ownership implicitly by where clause.
return prisma.somedayList.updateMany({ return prisma.somedayList.updateMany({
where: { where: {
id: item.id, id: item.id,
userId: (session.user as any).id userId
}, },
data: { order: item.order } data: { order: item.order }
}); });
}); });
await Promise.all(updates); await Promise.all(updates);
notifyUser((session.user as any).id, "list-changed", { action: "reordered" }); notifyUser(userId, "list-changed", { action: "reordered" });
return NextResponse.json({ success: true }); return NextResponse.json({ success: true });
} }
@ -204,7 +212,7 @@ export async function PATCH(request: NextRequest) {
where: { id } where: { id }
}); });
if (!existingList || existingList.userId !== (session.user as any).id) { if (!existingList || existingList.userId !== userId) {
return NextResponse.json( return NextResponse.json(
{ error: 'List not found or unauthorized' }, { error: 'List not found or unauthorized' },
{ status: 404 } { status: 404 }
@ -220,7 +228,7 @@ export async function PATCH(request: NextRequest) {
data data
}); });
notifyUser((session.user as any).id, "list-changed", { action: "updated" }); notifyUser(userId, "list-changed", { action: "updated" });
return NextResponse.json({ list }); return NextResponse.json({ list });
} catch (error) { } catch (error) {
console.error('Error updating someday list:', error); console.error('Error updating someday list:', error);