import { AuthOptions } from "next-auth"; import CredentialsProvider from "next-auth/providers/credentials"; import GoogleProvider from "next-auth/providers/google"; import { PrismaAdapter } from "@next-auth/prisma-adapter"; import bcrypt from "bcryptjs"; import { prisma } from "@/lib/prisma"; import { isAdminEmail } from "@/lib/admin"; // Adapter dipakai untuk persist User + Account saat OAuth (Google). // Session tetap pakai JWT supaya kompatibel dengan CredentialsProvider. export const authOptions: AuthOptions = { adapter: PrismaAdapter(prisma), providers: [ GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, // Auto-link kalau email Google sama dengan email user yang sudah register // via Credentials. Aman karena Google selalu memverifikasi email pemilik akun. allowDangerousEmailAccountLinking: true, }), CredentialsProvider({ name: "credentials", credentials: { email: { label: "Email", type: "email" }, password: { label: "Password", type: "password" }, }, async authorize(credentials) { if (!credentials?.email || !credentials?.password) { throw new Error("Email dan password harus diisi"); } const user = await prisma.user.findUnique({ where: { email: credentials.email }, }); if (!user) { throw new Error("Email tidak ditemukan"); } if (!user.password) { throw new Error("Akun ini terdaftar via Google. Silakan login dengan Google."); } const isPasswordValid = await bcrypt.compare( credentials.password, user.password ); if (!isPasswordValid) { throw new Error("Password salah"); } return { id: user.id, name: user.name, email: user.email, image: user.image, }; }, }), ], session: { strategy: "jwt", }, callbacks: { async signIn({ user }) { // Block suspended user dari sign-in (Credentials + OAuth). // Email-based lookup karena `user.id` belum tentu ada untuk first-time // OAuth sign-in sebelum adapter persist. const email = user.email; if (!email) return true; const existing = await prisma.user.findUnique({ where: { email }, select: { suspended: true }, }); if (existing?.suspended) { // NextAuth menerjemahkan return false jadi error "AccessDenied". return false; } return true; }, async jwt({ token, user, trigger }) { if (user) { token.id = user.id; } // Hidrasi `acceptedTermsAndPrivacy` dari DB: // - login pertama (`undefined`) // - client memanggil `useSession().update()` setelah accept // - token masih `false` → selalu cek DB (mencegah infinite redirect loop // antara middleware dan /accept-terms saat DB sudah true tapi cookie stale) if ( token.id && (trigger === "update" || token.acceptedTermsAndPrivacy === undefined || token.acceptedTermsAndPrivacy === false) ) { const dbUser = await prisma.user.findUnique({ where: { id: token.id as string }, select: { acceptedTermsAndPrivacy: true }, }); token.acceptedTermsAndPrivacy = dbUser?.acceptedTermsAndPrivacy ?? false; } return token; }, async session({ session, token }) { if (session.user) { session.user.id = token.id as string; session.user.acceptedTermsAndPrivacy = token.acceptedTermsAndPrivacy ?? false; session.user.isAdmin = isAdminEmail(session.user.email); } return session; }, }, pages: { signIn: "/login", }, secret: process.env.NEXTAUTH_SECRET, };