214 lines
6.7 KiB
TypeScript
214 lines
6.7 KiB
TypeScript
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<T>(path: string, body: Record<string, unknown>): Promise<T> {
|
|
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<LoginPayload>("/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<LoginPayload>("/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<BookingCodePayload>("/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<LoginPayload>("/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,
|
|
};
|