import type { NextAuthOptions } from "next-auth"; import CredentialsProvider from "next-auth/providers/credentials"; import GoogleProvider from "next-auth/providers/google"; import { getPublicApiUrl, getHotelPropertyId } from "@/lib/env"; async function postJson(path: string, body: Record): Promise { const api = getPublicApiUrl(); const res = await fetch(`${api}${path.startsWith("/") ? path : `/${path}`}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const data = (await res.json().catch(() => ({}))) as T & { message?: string }; if (!res.ok) { throw new Error( typeof data === "object" && data && "message" in data && typeof data.message === "string" ? data.message : `Auth failed (${res.status})`, ); } return data as T; } type LoginPayload = { access_token: string; user: { id: string; email: string | null; name: string | null; role: string; propertyId?: string; }; }; type AuthMethod = "otp" | "password" | "google"; type BookingCodePayload = LoginPayload & { booking?: { id: string; bookingCode: string | null; checkIn: string; checkOut: string; status: string; }; }; const providers: NextAuthOptions["providers"] = [ CredentialsProvider({ id: "credentials", name: "Email & password", credentials: { identifier: { label: "Email", type: "text" }, password: { label: "Password", type: "password" }, }, async authorize(credentials) { if (!credentials?.identifier || !credentials?.password) return null; try { const data = await postJson("/auth/login", { identifier: credentials.identifier, password: credentials.password, }); return { id: data.user.id, email: data.user.email ?? undefined, name: data.user.name ?? undefined, accessToken: data.access_token, role: data.user.role, propertyId: data.user.propertyId, authMethod: "password" as AuthMethod, }; } catch { return null; } }, }), CredentialsProvider({ id: "hotel-otp", name: "Hotel email OTP", credentials: { email: { label: "Email", type: "text" }, otp: { label: "Code", type: "text" }, }, async authorize(credentials) { if (!credentials?.email || !credentials?.otp) return null; try { const data = await postJson("/auth/hotel-user/login-email-otp", { email: credentials.email, otp: credentials.otp, }); return { id: data.user.id, email: data.user.email ?? undefined, name: data.user.name ?? undefined, accessToken: data.access_token, role: data.user.role, propertyId: data.user.propertyId, authMethod: "otp" as AuthMethod, }; } catch { return null; } }, }), CredentialsProvider({ id: "booking-code", name: "Booking code", credentials: { bookingCode: { label: "Booking code", type: "text" }, propertyId: { label: "Property", type: "text" }, }, async authorize(credentials) { const propertyId = credentials?.propertyId?.trim() || getHotelPropertyId(); const bookingCode = credentials?.bookingCode?.trim(); if (!propertyId || !bookingCode) return null; try { const data = await postJson("/auth/hotel-guest/login-booking-code", { propertyId, bookingCode, }); return { id: data.user.id, email: data.user.email ?? undefined, name: data.user.name ?? undefined, accessToken: data.access_token, role: data.user.role, propertyId: data.user.propertyId ?? propertyId, bookingCode: data.booking?.bookingCode ?? bookingCode, bookingId: data.booking?.id ?? null, }; } catch { return null; } }, }), ]; if (process.env.GOOGLE_CLIENT_ID?.trim() && process.env.GOOGLE_CLIENT_SECRET?.trim()) { providers.push( GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, }), ); } export const authOptions: NextAuthOptions = { providers, callbacks: { async jwt({ token, user, account, profile }) { if (account?.provider === "google" && profile && "email" in profile && profile.email) { try { const data = await postJson("/auth/google", { email: profile.email, name: profile.name, googleId: profile.sub, role: "CUSTOMER", }); token.accessToken = data.access_token; token.sub = data.user.id; token.email = data.user.email ?? undefined; token.name = data.user.name ?? undefined; token.role = data.user.role; token.propertyId = data.user.propertyId; token.authMethod = "google"; token.error = undefined; } catch { token.error = "GoogleSignInFailed"; } return token; } if (user) { const u = user as { accessToken?: string; role?: string; propertyId?: string; bookingCode?: string | null; bookingId?: string | null; authMethod?: AuthMethod; }; token.accessToken = u.accessToken; token.role = u.role; token.propertyId = u.propertyId; token.bookingCode = u.bookingCode ?? undefined; token.bookingId = u.bookingId ?? undefined; token.authMethod = u.authMethod ?? token.authMethod ?? "password"; } return token; }, async session({ session, token }) { if (session.user) { session.user.email = (token.email as string) ?? session.user.email; session.user.name = (token.name as string) ?? session.user.name; } session.accessToken = token.accessToken as string | undefined; session.role = token.role as string | undefined; session.propertyId = (token.propertyId as string | undefined) ?? getHotelPropertyId(); session.bookingCode = token.bookingCode ?? null; session.bookingId = token.bookingId ?? null; session.authMethod = (token.authMethod as AuthMethod | undefined) ?? "password"; session.error = token.error as string | undefined; return session; }, }, pages: { signIn: "/login", }, session: { strategy: "jwt", maxAge: 60 * 60 * 24 * 7, }, secret: process.env.NEXTAUTH_SECRET, };