import "server-only"; import { cookies, headers } from "next/headers"; import { redirect } from "next/navigation"; import bcrypt from "bcryptjs"; import { randomBytes } from "crypto"; import { prisma } from "./db"; import type { Role } from "@prisma/client"; const COOKIE = "baudoc_session"; const SESSION_DAYS = 12; export type SessionUser = { id: string; email: string; name: string; isAdmin: boolean; companyId: string | null; companyName: string | null; companyType: string | null; }; export async function hashPassword(pw: string) { return bcrypt.hash(pw, 11); } export async function verifyPassword(pw: string, hash: string) { return bcrypt.compare(pw, hash); } export async function login(email: string, password: string) { const user = await prisma.user.findUnique({ where: { email: email.trim().toLowerCase() }, include: { company: true }, }); if (!user || !user.active) return { ok: false as const, error: "Anmeldung fehlgeschlagen." }; if (!(await verifyPassword(password, user.passwordHash))) return { ok: false as const, error: "Anmeldung fehlgeschlagen." }; const token = randomBytes(32).toString("hex"); const expiresAt = new Date(Date.now() + SESSION_DAYS * 864e5); const h = await headers(); await prisma.session.create({ data: { userId: user.id, token, expiresAt, userAgent: h.get("user-agent")?.slice(0, 250) ?? null, ip: h.get("x-forwarded-for")?.split(",")[0]?.trim() ?? null, }, }); await prisma.user.update({ where: { id: user.id }, data: { lastLoginAt: new Date() } }); await prisma.auditLog.create({ data: { userId: user.id, aktion: "LOGIN", entitaet: "User", entitaetId: user.id }, }); const store = await cookies(); store.set(COOKIE, token, { httpOnly: true, sameSite: "lax", secure: process.env.NODE_ENV === "production" && (process.env.APP_URL ?? "").startsWith("https"), path: "/", expires: expiresAt, }); return { ok: true as const }; } export async function logout() { const store = await cookies(); const token = store.get(COOKIE)?.value; if (token) await prisma.session.deleteMany({ where: { token } }); store.delete(COOKIE); } export async function getSessionUser(): Promise { const store = await cookies(); const token = store.get(COOKIE)?.value; if (!token) return null; const session = await prisma.session.findUnique({ where: { token }, include: { user: { include: { company: true } } }, }); if (!session || session.expiresAt < new Date() || !session.user.active) return null; return { id: session.user.id, email: session.user.email, name: session.user.name, isAdmin: session.user.isAdmin, companyId: session.user.companyId, companyName: session.user.company?.name ?? null, companyType: session.user.company?.type ?? null, }; } export async function requireUser(): Promise { const user = await getSessionUser(); if (!user) redirect("/anmelden"); return user; } /** Rollen des Nutzers im Projekt (Admins gelten überall als ADMIN). */ export async function rolesInProject(userId: string, projectId: string): Promise { const user = await prisma.user.findUnique({ where: { id: userId }, select: { isAdmin: true } }); const memberships = await prisma.projectMember.findMany({ where: { userId, projectId }, select: { role: true }, }); const roles = memberships.map((m) => m.role); if (user?.isAdmin && !roles.includes("ADMIN")) roles.push("ADMIN"); return roles; } export async function requireProjectAccess(projectId: string) { const user = await requireUser(); const roles = await rolesInProject(user.id, projectId); if (roles.length === 0) redirect("/projekte?fehler=kein-zugriff"); return { user, roles }; } /** * Zugriff prüfen und zugleich das Projekt samt seiner Einstellungen laden. * Seiten sollten diese Variante verwenden, damit die projektbezogenen * Sichtbarkeitsschalter nirgends vergessen werden. */ export async function projektKontext(projectId: string) { const user = await requireUser(); const roles = await rolesInProject(user.id, projectId); if (roles.length === 0) redirect("/projekte?fehler=kein-zugriff"); const projekt = await prisma.project.findUnique({ where: { id: projectId } }); if (!projekt) redirect("/projekte?fehler=nicht-gefunden"); const { optionenAus } = await import("./projekte"); const { isExternalOnly } = await import("./rbac"); return { user, roles, projekt, optionen: optionenAus(projekt), /** true = darf ausschließlich Daten der eigenen Firma sehen */ nurEigeneFirma: isExternalOnly(roles), }; }