fix: guard session user id and batch availability reads
This commit is contained in:
@@ -35,7 +35,7 @@ export async function GET(request: NextRequest) {
|
||||
});
|
||||
|
||||
const result = await getAvailability(
|
||||
Number(auth.session.user.id),
|
||||
auth.userId,
|
||||
input.attendeeIds,
|
||||
input.rangeStart,
|
||||
input.rangeEnd,
|
||||
|
||||
@@ -8,5 +8,5 @@ export const dynamic = "force-dynamic";
|
||||
export async function GET(request: NextRequest) {
|
||||
const auth = await requireSchedulerSession(request);
|
||||
if ("response" in auth) return auth.response;
|
||||
return NextResponse.json(await getConnections(Number(auth.session.user.id)));
|
||||
return NextResponse.json(await getConnections(auth.userId));
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export const dynamic = "force-dynamic";
|
||||
export async function GET(request: NextRequest) {
|
||||
const auth = await requireSchedulerSession(request);
|
||||
if ("response" in auth) return auth.response;
|
||||
return NextResponse.json(await listMeetings(Number(auth.session.user.id)));
|
||||
return NextResponse.json(await listMeetings(auth.userId));
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
@@ -24,7 +24,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await createMeeting(Number(auth.session.user.id), body);
|
||||
const result = await createMeeting(auth.userId, body);
|
||||
if (result.status === "not_implemented") {
|
||||
return NextResponse.json(result, { status: 501 });
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export const dynamic = "force-dynamic";
|
||||
export async function GET(request: NextRequest) {
|
||||
const auth = await requireSchedulerSession(request);
|
||||
if ("response" in auth) return auth.response;
|
||||
return NextResponse.json(await getSchedule(Number(auth.session.user.id)));
|
||||
return NextResponse.json(await getSchedule(auth.userId));
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest) {
|
||||
@@ -24,7 +24,7 @@ export async function PUT(request: NextRequest) {
|
||||
}
|
||||
|
||||
try {
|
||||
const schedule = await updateSchedule(Number(auth.session.user.id), body);
|
||||
const schedule = await updateSchedule(auth.userId, body);
|
||||
return NextResponse.json(schedule);
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
|
||||
@@ -8,5 +8,5 @@ export const dynamic = "force-dynamic";
|
||||
export async function GET(request: NextRequest) {
|
||||
const auth = await requireSchedulerSession(request);
|
||||
if ("response" in auth) return auth.response;
|
||||
return NextResponse.json(await getTeamContext(Number(auth.session.user.id)));
|
||||
return NextResponse.json(await getTeamContext(auth.userId));
|
||||
}
|
||||
|
||||
@@ -9,8 +9,11 @@ export async function getSchedulerSession(request: NextRequest) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns either `{ session }` for an authenticated viewer, or `{ response }`
|
||||
* carrying a 401 the route handler should return directly.
|
||||
* Returns either `{ session, userId }` for an authenticated viewer, or `{ response }`
|
||||
* carrying a 401 the route handler should return directly. The `userId` is the
|
||||
* session user id coerced and validated to a finite positive integer, so callers
|
||||
* never re-coerce a possibly-undefined value into a `NaN` that would silently flow
|
||||
* into a Prisma query.
|
||||
*/
|
||||
export async function requireSchedulerSession(request: NextRequest) {
|
||||
const session = await getSchedulerSession(request);
|
||||
@@ -19,5 +22,13 @@ export async function requireSchedulerSession(request: NextRequest) {
|
||||
response: NextResponse.json({ error: "AUTHENTIK_LOGIN_REQUIRED" }, { status: 401 }),
|
||||
};
|
||||
}
|
||||
return { session };
|
||||
|
||||
const userId = Number(session.user.id);
|
||||
if (!Number.isInteger(userId) || userId <= 0) {
|
||||
return {
|
||||
response: NextResponse.json({ error: "INVALID_SESSION_USER_ID" }, { status: 401 }),
|
||||
};
|
||||
}
|
||||
|
||||
return { session, userId };
|
||||
}
|
||||
|
||||
@@ -5,6 +5,11 @@ import type { NextRequest } from "next/server";
|
||||
* Adapts an App Router `NextRequest` into the `NextApiRequest`-shaped object that
|
||||
* Cal's `getServerSession` (via `next-auth/jwt` `getToken`) expects: a plain cookies
|
||||
* map, a plain headers map, plus `method` and `url`.
|
||||
*
|
||||
* Contract: `getToken`/`getServerSession` read ONLY `cookies`, `headers`, `method`,
|
||||
* and `url` from the request. The `as unknown as NextApiRequest` cast is sound only
|
||||
* while that holds — if a future Cal/next-auth version reads other `NextApiRequest`
|
||||
* fields (e.g. `body`, `query`), this adapter must be extended to provide them.
|
||||
*/
|
||||
export function buildLegacyRequest(request: NextRequest): NextApiRequest {
|
||||
const cookies = Object.fromEntries(
|
||||
|
||||
@@ -155,6 +155,43 @@ async function loadSchedule(userId: number): Promise<SchedulerSchedule> {
|
||||
return buildSchedule(userId, schedule, user.timeZone ?? DEFAULT_TIME_ZONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Batched form of `loadSchedule` for the availability hot path: two queries total
|
||||
* (users + schedules) regardless of attendee count, instead of ~2N. Output order and
|
||||
* shape mirror `attendeeIds.map(loadSchedule)`.
|
||||
*/
|
||||
async function loadSchedules(userIds: number[]): Promise<SchedulerSchedule[]> {
|
||||
if (userIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const [users, schedules] = await Promise.all([
|
||||
prisma.user.findMany({ where: { id: { in: userIds } }, select: USER_SELECT }),
|
||||
prisma.schedule.findMany({
|
||||
where: { userId: { in: userIds } },
|
||||
orderBy: { id: "asc" },
|
||||
select: { id: true, userId: true, timeZone: true, ...SCHEDULE_INCLUDE },
|
||||
}),
|
||||
]);
|
||||
|
||||
const usersById = new Map(users.map((user) => [user.id, user]));
|
||||
// Keep the first schedule per user, mirroring loadSchedule's findFirst(orderBy id asc).
|
||||
const scheduleByUser = new Map<number, (typeof schedules)[number]>();
|
||||
for (const schedule of schedules) {
|
||||
if (!scheduleByUser.has(schedule.userId)) {
|
||||
scheduleByUser.set(schedule.userId, schedule);
|
||||
}
|
||||
}
|
||||
|
||||
return userIds.map((userId) => {
|
||||
const user = usersById.get(userId);
|
||||
if (!user) {
|
||||
throw new Error(`Scheduler user not found: ${userId}`);
|
||||
}
|
||||
return buildSchedule(userId, scheduleByUser.get(userId) ?? null, user.timeZone ?? DEFAULT_TIME_ZONE);
|
||||
});
|
||||
}
|
||||
|
||||
export async function getSchedule(userId: number): Promise<SchedulerSchedule> {
|
||||
if (isDemoMode()) {
|
||||
return demoSchedules.find((schedule) => schedule.userId === userId) ?? demoSchedules[0];
|
||||
@@ -235,7 +272,7 @@ export async function updateSchedule(userId: number, body: unknown): Promise<Sch
|
||||
];
|
||||
|
||||
// Replace the schedule's availability atomically; immutable from the caller's view.
|
||||
const scheduleId = await prisma.$transaction(async (tx) => {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const schedule = existing
|
||||
? await tx.schedule.update({
|
||||
where: { id: existing.id },
|
||||
@@ -253,10 +290,9 @@ export async function updateSchedule(userId: number, body: unknown): Promise<Sch
|
||||
data: availabilityRows.map((row) => ({ ...row, scheduleId: schedule.id })),
|
||||
});
|
||||
}
|
||||
return schedule.id;
|
||||
});
|
||||
|
||||
void scheduleId;
|
||||
// Read back the canonical persisted state so callers see exactly what was stored.
|
||||
return loadSchedule(userId);
|
||||
}
|
||||
|
||||
@@ -360,8 +396,10 @@ export async function getAvailability(
|
||||
return { busy: filterBusyBlocksForViewer(relevant, viewerId), mutualSlots };
|
||||
}
|
||||
|
||||
const busyBlocks = await bookingsToBusyBlocks(attendeeIds, rangeStart, rangeEnd);
|
||||
const schedules = await Promise.all(attendeeIds.map((id) => loadSchedule(id)));
|
||||
const [busyBlocks, schedules] = await Promise.all([
|
||||
bookingsToBusyBlocks(attendeeIds, rangeStart, rangeEnd),
|
||||
loadSchedules(attendeeIds),
|
||||
]);
|
||||
|
||||
const mutualSlots = computeMutualSlots({
|
||||
schedules,
|
||||
|
||||
Reference in New Issue
Block a user