From 06d12909210b5e30e768944fe45fbc5f7648d988 Mon Sep 17 00:00:00 2001 From: Rajpreet0 Date: Tue, 28 Apr 2026 19:28:40 +0200 Subject: [PATCH] added chaching and serialisation and indexing --- prisma/schema.prisma | 10 + src/lib/auth/getUser.ts | 305 ++++++++---------- src/middleware.ts | 4 +- src/modules/courses/actions/course-actions.ts | 36 +-- .../employee/actions/employee-actions.ts | 44 +-- src/modules/rooms/actions/room-actions.ts | 30 +- 6 files changed, 177 insertions(+), 252 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1c4310d..b3de529 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -60,6 +60,8 @@ model Employee { trainings UserTraining[] createdAt DateTime? @default(now()) updatedAt DateTime? @updatedAt + + @@index([createdBy]) } model UserTraining { @@ -98,6 +100,8 @@ model Business { manager Manager @relation(fields: [managerId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + + @@index([managerId]) } model Course { @@ -121,6 +125,10 @@ model Course { bookings CourseBooking[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + + @@index([createdBy]) + @@index([date]) + @@index([status]) } model Room { @@ -129,6 +137,8 @@ model Room { createdBy String // Manager ID who created the room createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + + @@index([createdBy]) } model CourseBooking { diff --git a/src/lib/auth/getUser.ts b/src/lib/auth/getUser.ts index 452840a..06a7bb5 100644 --- a/src/lib/auth/getUser.ts +++ b/src/lib/auth/getUser.ts @@ -1,6 +1,7 @@ import { createClient } from "@/src/lib/supabase/server"; import { prisma } from "@/src/lib/prisma"; import { cache } from "react"; +import { unstable_cache } from "next/cache"; // Define the user data types export type UserData = { @@ -43,6 +44,7 @@ export type EmployeeData = { qualification: string | null; pbSrc: string | null; createdAt: Date | null; + createdBy: string | null; role: "employee"; permissions: { employees: { view: boolean; create: boolean; edit: boolean; delete: boolean }; @@ -53,192 +55,159 @@ export type EmployeeData = { export type AuthUserData = UserData | ManagerData | EmployeeData; -// Note: We don't use in-memory caching because this app runs on Vercel (serverless) -// Serverless functions are stateless - in-memory cache would be: -// 1. Inconsistent across Lambda instances -// 2. Lost on cold starts -// 3. Not shared between requests -// Instead, we rely on React's cache() for per-request deduplication only. - -let userDataCallCount = 0; -let supabaseCallCount = 0; - // Get authenticated user from Supabase // Uses React's cache() for per-request deduplication -// No cross-request caching due to serverless architecture export const getUser = cache(async () => { - supabaseCallCount++; - console.log(`🔐 [AUTH] getUser called (count: ${supabaseCallCount})`); - const supabase = await createClient(); - // Validate with Auth server (secure - always authenticates against Supabase) const { data: { user }, } = await supabase.auth.getUser(); - if (user) { - console.log("🔐 [AUTH] User authenticated:", user.email); - } else { - console.log("🔐 [AUTH] No authenticated user"); - } - return user; }); -// Get user data from database -// Uses React's cache() for per-request deduplication -// No cross-request caching due to serverless architecture +// Fetch user data from all three tables in parallel, return the first match. +// Wrapped in unstable_cache (Next.js Data Cache, shared across serverless instances) +// keyed by email with a 60s TTL. React.cache() deduplicates within a single request. export const getUserData = cache(async (): Promise => { - userDataCallCount++; - console.log(`📊 [DB] getUserData called (count: ${userDataCallCount})`); - const user = await getUser(); - if (!user) return null; + if (!user || !user.email) return null; - console.log("🗄️ [DB] Fetching user data from database for:", user.email); + return unstable_cache( + async (email: string, id: string): Promise => { + const [userData, managerData, employeeData] = await Promise.all([ + prisma.user.findUnique({ + where: { id }, + select: { + id: true, + name: true, + email: true, + birthday: true, + plz: true, + city: true, + street: true, + houseNumber: true, + gender: true, + pbSrc: true, + createdAt: true, + }, + }), + prisma.manager.findUnique({ + where: { id }, + select: { + id: true, + firstName: true, + lastName: true, + email: true, + tel: true, + pbSrc: true, + createdAt: true, + businesses: { + select: { + id: true, + name: true, + address: true, + email: true, + }, + }, + }, + }), + prisma.employee.findUnique({ + where: { email }, + select: { + id: true, + firstName: true, + lastName: true, + email: true, + qualification: true, + pbSrc: true, + permissions: true, + createdAt: true, + createdBy: true, + }, + }), + ]); - // Try to find in User table first - const userData = await prisma.user.findUnique({ - where: { id: user.id }, - select: { - id: true, - name: true, - email: true, - birthday: true, - plz: true, - city: true, - street: true, - houseNumber: true, - gender: true, - pbSrc: true, - createdAt: true, + if (userData) { + const userWithRole: UserData = { ...userData, role: "user" }; + return userWithRole; + } + + if (managerData) { + const managerWithRole: ManagerData = { ...managerData, role: "manager" }; + return managerWithRole; + } + + if (employeeData) { + const raw = employeeData.permissions as Record | null; + const empty = () => ({ view: false, create: false, edit: false, delete: false }); + + let permissions: EmployeeData["permissions"]; + + if (raw && ("canCreateCourses" in raw || "canCreateEmployees" in raw)) { + // Backward-Kompatibilität: altes Format migrieren + const old = raw as { canCreateCourses?: boolean; canCreateEmployees?: boolean }; + permissions = { + employees: { + view: old.canCreateEmployees ?? false, + create: old.canCreateEmployees ?? false, + edit: false, + delete: false, + }, + courses: { + view: old.canCreateCourses ?? false, + create: old.canCreateCourses ?? false, + edit: false, + delete: false, + }, + rooms: empty(), + }; + } else { + const p = raw as { + employees?: Partial; + courses?: Partial; + rooms?: Partial; + } | null; + permissions = { + employees: { + view: p?.employees?.view ?? false, + create: p?.employees?.create ?? false, + edit: p?.employees?.edit ?? false, + delete: p?.employees?.delete ?? false, + }, + courses: { + view: p?.courses?.view ?? false, + create: p?.courses?.create ?? false, + edit: p?.courses?.edit ?? false, + delete: p?.courses?.delete ?? false, + }, + rooms: { + view: p?.rooms?.view ?? false, + create: p?.rooms?.create ?? false, + edit: p?.rooms?.edit ?? false, + delete: p?.rooms?.delete ?? false, + }, + }; + } + + const employeeWithRole: EmployeeData = { + ...employeeData, + role: "employee", + permissions, + createdBy: employeeData.createdBy, + }; + return employeeWithRole; + } + + return null; }, - }); - - if (userData) { - console.log("✅ [DB] User data fetched:", userData.name); - const userWithRole: UserData = { ...userData, role: "user" }; - return userWithRole; - } - - // If not found in User table, try Manager table - const managerData = await prisma.manager.findUnique({ - where: { id: user.id }, - select: { - id: true, - firstName: true, - lastName: true, - email: true, - tel: true, - pbSrc: true, - createdAt: true, - businesses: { - select: { - id: true, - name: true, - address: true, - email: true, - }, - }, - }, - }); - - if (managerData) { - console.log( - "✅ [DB] Manager data fetched:", - `${managerData.firstName} ${managerData.lastName}` - ); - const managerWithRole: ManagerData = { ...managerData, role: "manager" }; - return managerWithRole; - } - - // Try Employee table — match by email since Supabase ID is not stored in Employee - if (!user.email) { - console.log("⚠️ [DB] User has no email, cannot lookup employee"); - return null; - } - - const employeeData = await prisma.employee.findUnique({ - where: { email: user.email }, - select: { - id: true, - firstName: true, - lastName: true, - email: true, - qualification: true, - pbSrc: true, - permissions: true, - createdAt: true, - }, - }); - - if (employeeData) { - console.log( - "✅ [DB] Employee data fetched:", - `${employeeData.firstName} ${employeeData.lastName}` - ); - const raw = employeeData.permissions as Record | null; - const empty = () => ({ view: false, create: false, edit: false, delete: false }); - - let permissions: EmployeeData["permissions"]; - - if (raw && ("canCreateCourses" in raw || "canCreateEmployees" in raw)) { - // Backward-Kompatibilität: altes Format migrieren - const old = raw as { canCreateCourses?: boolean; canCreateEmployees?: boolean }; - permissions = { - employees: { - view: old.canCreateEmployees ?? false, - create: old.canCreateEmployees ?? false, - edit: false, - delete: false, - }, - courses: { - view: old.canCreateCourses ?? false, - create: old.canCreateCourses ?? false, - edit: false, - delete: false, - }, - rooms: empty(), - }; - } else { - const p = raw as { - employees?: Partial; - courses?: Partial; - rooms?: Partial; - } | null; - permissions = { - employees: { - view: p?.employees?.view ?? false, - create: p?.employees?.create ?? false, - edit: p?.employees?.edit ?? false, - delete: p?.employees?.delete ?? false, - }, - courses: { - view: p?.courses?.view ?? false, - create: p?.courses?.create ?? false, - edit: p?.courses?.edit ?? false, - delete: p?.courses?.delete ?? false, - }, - rooms: { - view: p?.rooms?.view ?? false, - create: p?.rooms?.create ?? false, - edit: p?.rooms?.edit ?? false, - delete: p?.rooms?.delete ?? false, - }, - }; + ["user-data", user.email], + { + tags: [`user-data-${user.email}`], + revalidate: 60, } - - const employeeWithRole: EmployeeData = { - ...employeeData, - role: "employee", - permissions, - }; - return employeeWithRole; - } - - console.log("⚠️ [DB] No user, manager or employee found for:", user.email); - return null; + )(user.email, user.id); }); export async function requireAuth() { diff --git a/src/middleware.ts b/src/middleware.ts index 155f9a0..9022ec0 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -60,5 +60,7 @@ export async function middleware(request: NextRequest) { } export const config = { - matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)"], + matcher: [ + "/((?!_next/static|_next/data|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)", + ], }; diff --git a/src/modules/courses/actions/course-actions.ts b/src/modules/courses/actions/course-actions.ts index 70e3fba..0669f0c 100644 --- a/src/modules/courses/actions/course-actions.ts +++ b/src/modules/courses/actions/course-actions.ts @@ -62,11 +62,7 @@ export async function createCourse(data: CourseFormData, status: "draft" | "publ const validatedData = validation.data; // Schritt 5: Manager-ID ermitteln (Employee erbt createdBy vom eigenen Datensatz) - let creatorId = userData.id; - if (isEmployee(userData)) { - const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } }); - creatorId = selfRecord?.createdBy ?? userData.id; - } + const creatorId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; // Schritt 6: Kurs in der Datenbank erstellen const course = await prisma.course.create({ @@ -175,11 +171,7 @@ export async function getMyCourses(options?: { dateFrom?: Date; dateTo?: Date }) } // Step 3: Manager-ID ermitteln (Employee erbt createdBy vom eigenen Datensatz) - let managerId = userData.id; - if (isEmployee(userData)) { - const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } }); - managerId = selfRecord?.createdBy ?? userData.id; - } + const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; // Step 4: Build the where clause with optional date filtering const whereClause: Prisma.CourseWhereInput = { @@ -254,11 +246,7 @@ export async function getCourseById(courseId: string) { } // Schritt 3: Effektive Manager-ID ermitteln - let managerId = userData.id; - if (isEmployee(userData)) { - const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } }); - managerId = selfRecord?.createdBy ?? userData.id; - } + const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; // Schritt 4: Kurs aus der Datenbank abrufen const course = await prisma.course.findUnique({ @@ -334,11 +322,7 @@ export async function updateCourse( } // Schritt 4: Effektive Manager-ID ermitteln - let managerId = userData.id; - if (isEmployee(userData)) { - const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } }); - managerId = selfRecord?.createdBy ?? userData.id; - } + const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; // Schritt 5: Kurs aus der Datenbank abrufen const existingCourse = await prisma.course.findUnique({ @@ -445,11 +429,7 @@ export async function deleteCourse(courseId: string) { } // Schritt 3: Effektive Manager-ID ermitteln - let managerId = userData.id; - if (isEmployee(userData)) { - const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } }); - managerId = selfRecord?.createdBy ?? userData.id; - } + const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; // Schritt 4: Kurs aus der Datenbank abrufen const course = await prisma.course.findUnique({ @@ -507,11 +487,7 @@ export async function getMySportTypes() { return { success: false, error: "Unauthorized", sports: [] }; } - let managerId = userData.id; - if (isEmployee(userData)) { - const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } }); - managerId = selfRecord?.createdBy ?? userData.id; - } + const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; const courses = await prisma.course.findMany({ where: { createdBy: managerId }, diff --git a/src/modules/employee/actions/employee-actions.ts b/src/modules/employee/actions/employee-actions.ts index bb91ae8..6a6a7c6 100644 --- a/src/modules/employee/actions/employee-actions.ts +++ b/src/modules/employee/actions/employee-actions.ts @@ -12,7 +12,12 @@ import { prisma } from "@/src/lib/prisma"; import { getUserData, isManager, isEmployee } from "@/src/lib/auth/getUser"; import { createAdminClient } from "@/src/lib/supabase/admin"; import { employeeSchema, type EmployeeFormData } from "../schemas/employee-schema"; -import { revalidatePath } from "next/cache"; +import { revalidatePath, revalidateTag } from "next/cache"; + +function invalidateUserDataCache(email: string) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (revalidateTag as any)(`user-data-${email}`); +} import { randomBytes } from "crypto"; import { sendMail } from "@/src/lib/mail/nodemailer"; import { generateOnboardingEmail } from "@/src/lib/mail/templates/employee-onboarding"; @@ -124,11 +129,7 @@ export async function createEmployee(data: EmployeeFormData, status: "draft" | " } // Schritt 7: Manager-ID ermitteln (Employee erbt createdBy vom eigenen Datensatz) - let managerId = userData.id; - if (isEmployee(userData)) { - const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } }); - managerId = selfRecord?.createdBy ?? userData.id; - } + const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; // Schritt 8: Berechtigungen auf Caller-Ceiling begrenzen (Privilege-Escalation verhindern) const finalPermissions = isEmployee(userData) @@ -215,11 +216,7 @@ export async function getMyTrainers() { }; } - let managerId = userData.id; - if (isEmployee(userData)) { - const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } }); - managerId = selfRecord?.createdBy ?? userData.id; - } + const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; const employees = await prisma.employee.findMany({ where: { @@ -286,8 +283,7 @@ export async function getMyEmployees() { whereClause = { createdBy: userData.id }; } else { // Employee: eigene Manager-ID ermitteln, dann alle unter diesem Manager anzeigen (außer sich selbst) - const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } }); - const managerId = selfRecord?.createdBy; + const managerId = userData.createdBy; whereClause = { createdBy: managerId, NOT: { email: userData.email } }; } const employees = await prisma.employee.findMany({ @@ -439,11 +435,7 @@ export async function updateEmployee( } // Schritt 6: Überprüfen, ob der Benutzer der Besitzer des Mitarbeiters ist - let ownerManagerId = userData.id; - if (isEmployee(userData)) { - const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } }); - ownerManagerId = selfRecord?.createdBy ?? userData.id; - } + const ownerManagerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; if (existingEmployee.createdBy !== ownerManagerId) { return { success: false, @@ -525,6 +517,7 @@ export async function updateEmployee( // Schritt 11: Next.js Cache invalidieren revalidatePath("/employee"); revalidatePath(`/employee/edit/${employeeId}`); + invalidateUserDataCache(validatedData.email); return { success: true, @@ -571,11 +564,7 @@ export async function getMyEmployeeRoles() { } // Schritt 3: Effektive Manager-ID ermitteln - let managerId = userData.id; - if (isEmployee(userData)) { - const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } }); - managerId = selfRecord?.createdBy ?? userData.id; - } + const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; // Schritt 4: Alle Mitarbeiter des Managers abrufen (nur roles Feld) const employees = await prisma.employee.findMany({ @@ -660,11 +649,7 @@ export async function deleteEmployee(employeeId: string) { } // Schritt 5: Überprüfen, ob der Benutzer der Besitzer des Mitarbeiters ist - let ownerManagerId = userData.id; - if (isEmployee(userData)) { - const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } }); - ownerManagerId = selfRecord?.createdBy ?? userData.id; - } + const ownerManagerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; if (employee.createdBy !== ownerManagerId) { return { success: false, @@ -694,6 +679,7 @@ export async function deleteEmployee(employeeId: string) { // Schritt 8: Next.js Cache invalidieren revalidatePath("/employee"); + invalidateUserDataCache(employee.email); return { success: true, @@ -840,6 +826,8 @@ export async function completeEmployeeOnboarding( }; } + invalidateUserDataCache(employee.email); + // Manager-Bestätigungs-E-Mail senden if (employee.createdBy) { const manager = await prisma.manager.findUnique({ diff --git a/src/modules/rooms/actions/room-actions.ts b/src/modules/rooms/actions/room-actions.ts index 3cd1725..682772c 100644 --- a/src/modules/rooms/actions/room-actions.ts +++ b/src/modules/rooms/actions/room-actions.ts @@ -59,11 +59,7 @@ export async function createRoom(data: RoomFormData) { const validatedData = validation.data; // Schritt 5: Manager-ID ermitteln (Employee erbt createdBy vom eigenen Datensatz) - let creatorId = userData.id; - if (isEmployee(userData)) { - const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } }); - creatorId = selfRecord?.createdBy ?? userData.id; - } + const creatorId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; // Schritt 6: Raum in der Datenbank erstellen const room = await prisma.room.create({ @@ -147,11 +143,7 @@ export async function getMyRooms() { } // Schritt 3: Manager-ID ermitteln (Employee erbt createdBy vom eigenen Datensatz) - let managerId = userData.id; - if (isEmployee(userData)) { - const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } }); - managerId = selfRecord?.createdBy ?? userData.id; - } + const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; // Schritt 4: Räume abrufen, die von diesem Manager erstellt wurden const rooms = await prisma.room.findMany({ @@ -203,11 +195,7 @@ export async function getRoomById(roomId: string) { } // Schritt 3: Effektive Manager-ID ermitteln - let managerId = userData.id; - if (isEmployee(userData)) { - const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } }); - managerId = selfRecord?.createdBy ?? userData.id; - } + const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; // Schritt 4: Raum aus der Datenbank abrufen const room = await prisma.room.findUnique({ @@ -279,11 +267,7 @@ export async function updateRoom(roomId: string, data: RoomFormData) { } // Schritt 4: Effektive Manager-ID ermitteln - let managerId = userData.id; - if (isEmployee(userData)) { - const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } }); - managerId = selfRecord?.createdBy ?? userData.id; - } + const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; // Schritt 5: Raum aus der Datenbank abrufen const existingRoom = await prisma.room.findUnique({ @@ -372,11 +356,7 @@ export async function deleteRoom(roomId: string) { } // Schritt 3: Effektive Manager-ID ermitteln - let managerId = userData.id; - if (isEmployee(userData)) { - const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } }); - managerId = selfRecord?.createdBy ?? userData.id; - } + const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; // Schritt 4: Raum aus der Datenbank abrufen const room = await prisma.room.findUnique({