import NextAuth from "next-auth"; import Credentials from "next-auth/providers/credentials"; import { compare } from "bcryptjs"; import { z } from "zod"; import { prisma } from "@/lib/prisma"; // ===================================================================== // NextAuth.js (Auth.js v5) · Credentials-Provider mit JWT-Strategie // --------------------------------------------------------------------- // Mandantenfähigkeit: `id`, `role`, `kitaId`, `familyId` werden über die JWT-/Session- // Callbacks aus der DB in jede Session durchgeschleift, damit jede // Server Action / API-Route den Tenant-Filter setzen kann. // ===================================================================== const credentialsSchema = z.object({ email: z.string().email().toLowerCase().trim(), password: z.string().min(1), }); // Brute-Force-Schutz auf Account-Ebene (planung.md §5.5). // Globales Rate-Limiting ergänzen wir später per Middleware. const MAX_FAILED_ATTEMPTS = 10; const LOCKOUT_MINUTES = 15; export const { handlers, auth, signIn, signOut } = NextAuth({ session: { strategy: "jwt" }, pages: { signIn: "/login", }, providers: [ Credentials({ name: "Credentials", credentials: { email: { label: "E-Mail", type: "email" }, password: { label: "Passwort", type: "password" }, }, async authorize(rawCredentials) { const parsed = credentialsSchema.safeParse(rawCredentials); if (!parsed.success) return null; const { email, password } = parsed.data; const user = await prisma.user.findUnique({ where: { email } }); if (!user) return null; // Account temporär gesperrt → Login pauschal ablehnen. if (user.lockedUntil && user.lockedUntil > new Date()) { return null; } const passwordOk = await compare(password, user.passwordHash); if (!passwordOk) { const nextAttempts = user.failedLoginAttempts + 1; await prisma.user.update({ where: { id: user.id }, data: { failedLoginAttempts: nextAttempts, lockedUntil: nextAttempts >= MAX_FAILED_ATTEMPTS ? new Date(Date.now() + LOCKOUT_MINUTES * 60_000) : user.lockedUntil, }, }); return null; } // Erfolgreicher Login → Counter zurücksetzen, lastLogin aktualisieren. await prisma.user.update({ where: { id: user.id }, data: { failedLoginAttempts: 0, lockedUntil: null, lastLoginAt: new Date(), }, }); return { id: user.id, email: user.email, name: `${user.firstName} ${user.lastName}`.trim(), role: user.role, kitaId: user.kitaId, familyId: user.familyId, }; }, }), ], callbacks: { /** * Wird beim Login (mit `user`) und bei jedem Token-Refresh (ohne `user`) * aufgerufen. Beim Login schreiben wir die mandantenrelevanten Felder * einmalig in den Token; bei jedem weiteren Aufruf re-validieren wir * `role` und `kitaId` gegen die DB, damit z.B. ein gerade entzogener * Admin-Rang sofort greift (kein 30-Tage-Token mit veralteter Rolle). */ async jwt({ token, user }) { if (user) { token.id = user.id; token.role = user.role; token.kitaId = user.kitaId; token.familyId = user.familyId; return token; } const tokenUserId = token.id ?? token.sub; if (tokenUserId) { const fresh = await prisma.user.findUnique({ where: { id: tokenUserId }, select: { role: true, kitaId: true, familyId: true }, }); if (!fresh) { // User wurde gelöscht → Token entwerten. // (Auth.js erkennt den fehlenden `sub`/`id` und meldet ab.) delete (token as Partial).id; delete (token as Partial).sub; return token; } token.id = tokenUserId; token.role = fresh.role; token.kitaId = fresh.kitaId; token.familyId = fresh.familyId; } return token; }, /** * Wird bei jedem `auth()` / `useSession()`-Aufruf ausgeführt. * Hier projizieren wir die JWT-Felder in das Session-Objekt, * damit Server Components / Client Components sie typsicher lesen können. */ async session({ session, token }) { if (token && session.user) { session.user.id = token.id ?? token.sub; session.user.role = token.role; session.user.kitaId = token.kitaId; session.user.familyId = token.familyId; } return session; }, }, });