mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
191 lines
5.2 KiB
TypeScript
191 lines
5.2 KiB
TypeScript
import NextAuth, { NextAuthOptions } from "next-auth";
|
||
import GoogleProvider from "next-auth/providers/google";
|
||
import AzureADB2CProvider from "next-auth/providers/azure-ad-b2c";
|
||
import EmailProvider from "next-auth/providers/email";
|
||
import DrizzleEmailAdapter from "./DrizzleEmailAdapter";
|
||
|
||
import { db } from "@/app/db/db";
|
||
import {
|
||
user as users,
|
||
accounts,
|
||
verificationTokens,
|
||
} from "@/app/db/schema/users";
|
||
import { eq } from "drizzle-orm";
|
||
|
||
// ------------------------------------------------------------------
|
||
// Environment variables
|
||
// ------------------------------------------------------------------
|
||
const {
|
||
GOOGLE_CLIENT_ID = "",
|
||
GOOGLE_CLIENT_SECRET = "",
|
||
AZURE_AD_B2C_TENANT_NAME = "",
|
||
AZURE_AD_B2C_CLIENT_ID = "",
|
||
AZURE_AD_B2C_CLIENT_SECRET = "",
|
||
AZURE_AD_B2C_PRIMARY_USER_FLOW = "",
|
||
EMAIL_SERVER_HOST = "",
|
||
EMAIL_SERVER_PORT = "",
|
||
EMAIL_SERVER_USER = "",
|
||
EMAIL_SERVER_PASSWORD = "",
|
||
EMAIL_FROM = "",
|
||
} = process.env;
|
||
|
||
type OauthProvider = "google" | "azure-ad-b2c";
|
||
|
||
// ------------------------------------------------------------------
|
||
// NextAuth configuration
|
||
// ------------------------------------------------------------------
|
||
export const AuthOptions: NextAuthOptions = {
|
||
adapter: DrizzleEmailAdapter(db, {
|
||
user: users,
|
||
accounts,
|
||
verificationTokens,
|
||
}),
|
||
|
||
providers: [
|
||
// ------------------ Google ------------------
|
||
GoogleProvider({
|
||
clientId: GOOGLE_CLIENT_ID,
|
||
clientSecret: GOOGLE_CLIENT_SECRET,
|
||
authorization: {
|
||
params: {
|
||
access_type: "offline",
|
||
prompt: "consent",
|
||
response_type: "code",
|
||
},
|
||
},
|
||
}),
|
||
|
||
// ------------------ Azure AD B2C ------------------
|
||
AzureADB2CProvider({
|
||
tenantId: AZURE_AD_B2C_TENANT_NAME,
|
||
clientId: AZURE_AD_B2C_CLIENT_ID,
|
||
clientSecret: AZURE_AD_B2C_CLIENT_SECRET,
|
||
primaryUserFlow: AZURE_AD_B2C_PRIMARY_USER_FLOW,
|
||
authorization: {
|
||
params: {
|
||
scope: "openid profile offline_access",
|
||
prompt: "login",
|
||
},
|
||
},
|
||
}),
|
||
|
||
// ------------------ Email (SES Magic Link) ------------------
|
||
EmailProvider({
|
||
server: {
|
||
host: EMAIL_SERVER_HOST,
|
||
port: Number(EMAIL_SERVER_PORT),
|
||
auth: {
|
||
user: EMAIL_SERVER_USER,
|
||
pass: EMAIL_SERVER_PASSWORD,
|
||
},
|
||
},
|
||
from: EMAIL_FROM,
|
||
maxAge: 60 * 60, // magic link valid for 1 hour
|
||
}),
|
||
],
|
||
|
||
// ------------------------------------------------------------------
|
||
// Pages
|
||
// ------------------------------------------------------------------
|
||
pages: {
|
||
signIn: "/", // your landing/login page
|
||
verifyRequest: "/auth/verify-request",
|
||
error: "/auth/error",
|
||
},
|
||
|
||
// ------------------------------------------------------------------
|
||
// Callbacks
|
||
// ------------------------------------------------------------------
|
||
callbacks: {
|
||
/**
|
||
* Sign in callback — ensures user exists and links OAuth provider if needed
|
||
*/
|
||
async signIn({ user, account }) {
|
||
try {
|
||
if (!user?.email) return false;
|
||
|
||
const normalisedEmail = user.email.toLowerCase();
|
||
|
||
// Fetch the user (NextAuth will have created them already if new)
|
||
const [dbUser] = await db
|
||
.select()
|
||
.from(users)
|
||
.where(eq(users.email, normalisedEmail));
|
||
|
||
if (!dbUser) {
|
||
console.warn(`User not found for ${normalisedEmail} after sign-in.`);
|
||
return false;
|
||
}
|
||
|
||
// Link OAuth ID if missing (helps for older accounts)
|
||
if (account && !dbUser.oauthId) {
|
||
const provider = account.provider as OauthProvider;
|
||
await db
|
||
.update(users)
|
||
.set({ oauthId: user.id, oauthProvider: provider })
|
||
.where(eq(users.email, normalisedEmail));
|
||
}
|
||
|
||
// Always update last login timestamp
|
||
await db
|
||
.update(users)
|
||
.set({ lastLogin: new Date() })
|
||
.where(eq(users.id, dbUser.id));
|
||
|
||
// Pass bigint ID into NextAuth session/jwt
|
||
user.dbId = dbUser.id.toString();
|
||
|
||
// If the user isn't onboarded yet, redirect to onboarding
|
||
if (!dbUser.onboarded) {
|
||
return "/onboarding";
|
||
}
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error("Error during sign-in:", error);
|
||
return false;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Persist dbId in the JWT so it’s available in sessions
|
||
*/
|
||
async jwt({ token, user }) {
|
||
if (user?.dbId) {
|
||
token.dbId = user.dbId;
|
||
}
|
||
return token;
|
||
},
|
||
|
||
/**
|
||
* Attach dbId to session.user
|
||
*/
|
||
async session({ session, token }) {
|
||
if (session.user && token.dbId) {
|
||
session.user.dbId = token.dbId;
|
||
}
|
||
return session;
|
||
},
|
||
|
||
/**
|
||
* Redirect users after login
|
||
*/
|
||
async redirect({ baseUrl }) {
|
||
return `${baseUrl}/home`;
|
||
},
|
||
},
|
||
|
||
// ------------------------------------------------------------------
|
||
// General options
|
||
// ------------------------------------------------------------------
|
||
session: {
|
||
strategy: "jwt", // lightweight, avoids DB session writes
|
||
},
|
||
jwt: {
|
||
maxAge: 60 * 60 * 24 * 30, // 30 days
|
||
},
|
||
debug: false,
|
||
};
|
||
|
||
const handler = NextAuth(AuthOptions);
|
||
export { handler as GET, handler as POST };
|