added chaching and serialisation and indexing

This commit is contained in:
Rajpreet0 2026-04-28 19:28:40 +02:00
parent e05984c1c3
commit 06d1290921
6 changed files with 177 additions and 252 deletions

View file

@ -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 {

View file

@ -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<AuthUserData | null> => {
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<AuthUserData | null> => {
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<string, unknown> | 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<EmployeeData["permissions"]["employees"]>;
courses?: Partial<EmployeeData["permissions"]["courses"]>;
rooms?: Partial<EmployeeData["permissions"]["rooms"]>;
} | 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<string, unknown> | 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<EmployeeData["permissions"]["employees"]>;
courses?: Partial<EmployeeData["permissions"]["courses"]>;
rooms?: Partial<EmployeeData["permissions"]["rooms"]>;
} | 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() {

View file

@ -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)$).*)",
],
};

View file

@ -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 },

View file

@ -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({

View file

@ -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({