assessment-model/src/app/api/auth/[...nextauth]/route.ts
2025-10-13 08:52:34 +00:00

191 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 its 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 };