import type { NextAuthOptions } from "next-auth"; import CredentialsProvider from "next-auth/providers/credentials"; import { prisma } from "@/lib/prisma"; import { compare } from "bcryptjs"; import { z } from "zod"; import { consumeRateLimit, getClientIpFromHeaders } from "@/lib/rate-limit"; const credentialsSchema = z.object({ email: z.string().email(), password: z.string().min(8) }); function limitLoginAttempt( headers: Headers | Record | undefined, email: string ) { const ip = getClientIpFromHeaders(headers ?? {}); const normalizedEmail = email.trim().toLowerCase(); return consumeRateLimit( `auth-login:${ip}:${normalizedEmail}`, 8, 10 * 60_000 ); } export const authOptions: NextAuthOptions = { session: { strategy: "jwt", maxAge: 2 * 60 * 60 // 2 Stunden }, pages: { signIn: "/anmelden" }, providers: [ CredentialsProvider({ name: "Anmeldung", credentials: { email: { label: "E-Mail", type: "email" }, password: { label: "Passwort", type: "password" } }, async authorize(credentials, req) { const parsed = credentialsSchema.safeParse(credentials); const rawEmail = typeof credentials?.email === "string" ? credentials.email : "unknown"; const limit = limitLoginAttempt(req?.headers, rawEmail); if (!limit.ok) return null; if (!parsed.success) return null; const user = await prisma.user.findUnique({ where: { email: parsed.data.email } }); if (!user || !user.isActive || user.role !== "ADMIN") return null; const valid = await compare(parsed.data.password, user.hashedPassword); if (!valid) return null; return { id: user.id, email: user.email, name: user.name, role: user.role, slug: user.slug }; } }) ], callbacks: { async jwt({ token, user }) { if (user) { token.id = user.id; token.role = user.role; token.slug = user.slug; } return token; }, async session({ session, token }) { if (session.user) { session.user.id = token.id; session.user.role = token.role; session.user.slug = token.slug; } return session; } }, secret: process.env.NEXTAUTH_SECRET };