Fixing auth options export issue

This commit is contained in:
Khalim Conn-Kowlessar 2025-10-28 11:49:19 +00:00
parent b6353cb2d2
commit 8ca14c39ad
7 changed files with 250 additions and 248 deletions

View file

@ -0,0 +1,243 @@
import { 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 { MagicLinksEmail } from "@/app/email_templates/magic_link";
import { db } from "@/app/db/db";
import {
user as users,
accounts,
verificationTokens,
} from "@/app/db/schema/users";
import { eq, and } 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, // noreply email
maxAge: 60 * 60, // magic link valid for 1 hour
sendVerificationRequest: MagicLinksEmail,
}),
],
// ------------------------------------------------------------------
// 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, profile }) {
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));
// New user - next auth will handle
if (!dbUser) {
console.log("New user sign up for email:", normalisedEmail);
return true;
}
// Auto-link provider if same verified email but account not linked yet
if (account?.provider && account.type === "oauth") {
const existingLink = await db
.select()
.from(accounts)
.where(
and(
eq(accounts.userId, dbUser.id),
eq(accounts.provider, account.provider)
)
);
const emailVerified =
(profile as any)?.email_verified ?? account.provider === "google";
if (existingLink.length === 0 && emailVerified) {
// This handles the case where we had not set up accounts but
// signed up users with oauth
console.log(
`Linking ${account.provider} account for user ${normalisedEmail}`
);
await db
.insert(accounts)
.values({
userId: dbUser.id,
type: account.type,
provider: account.provider,
providerAccountId: account.providerAccountId,
access_token: account.access_token,
id_token: account.id_token,
refresh_token: account.refresh_token,
expires_at: account.expires_at,
})
.onConflictDoNothing();
}
}
// Link OAuth ID if missing (helps for older accounts)
if (account && !dbUser.oauthId) {
console.log("Linking OAuth ID for user:", normalisedEmail);
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();
user.onboarded = dbUser.onboarded ?? false;
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: userFromLogin }) {
// Initial sign-in: attach DB fields
if (userFromLogin) {
const existing = await db.query.user.findFirst({
where: eq(users.email, userFromLogin.email!),
});
if (existing) {
token.onboarded = existing.onboarded;
}
} else if (token.email) {
// On subsequent calls, keep token synced from DB
const existing = await db.query.user.findFirst({
where: eq(users.email, token.email),
});
if (existing) {
token.onboarded = existing.onboarded;
}
}
if (userFromLogin?.dbId) {
token.dbId = userFromLogin.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 }) {
// If the user has not onboarded, send them to onboarding
return `${baseUrl}/home`;
},
},
// ------------------------------------------------------------------
// General options
// ------------------------------------------------------------------
session: {
strategy: "jwt", // lightweight, avoids DB session writes
},
jwt: {
maxAge: 60 * 60 * 24 * 30, // 30 days
},
debug: false,
};

View file

@ -1,246 +1,5 @@
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 { MagicLinksEmail } from "@/app/email_templates/magic_link";
import { db } from "@/app/db/db";
import {
user as users,
accounts,
verificationTokens,
} from "@/app/db/schema/users";
import { eq, and } 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, // noreply email
maxAge: 60 * 60, // magic link valid for 1 hour
sendVerificationRequest: MagicLinksEmail,
}),
],
// ------------------------------------------------------------------
// 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, profile }) {
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));
// New user - next auth will handle
if (!dbUser) {
console.log("New user sign up for email:", normalisedEmail);
return true;
}
// Auto-link provider if same verified email but account not linked yet
if (account?.provider && account.type === "oauth") {
const existingLink = await db
.select()
.from(accounts)
.where(
and(
eq(accounts.userId, dbUser.id),
eq(accounts.provider, account.provider)
)
);
const emailVerified =
(profile as any)?.email_verified ?? account.provider === "google";
if (existingLink.length === 0 && emailVerified) {
// This handles the case where we had not set up accounts but
// signed up users with oauth
console.log(
`Linking ${account.provider} account for user ${normalisedEmail}`
);
await db
.insert(accounts)
.values({
userId: dbUser.id,
type: account.type,
provider: account.provider,
providerAccountId: account.providerAccountId,
access_token: account.access_token,
id_token: account.id_token,
refresh_token: account.refresh_token,
expires_at: account.expires_at,
})
.onConflictDoNothing();
}
}
// Link OAuth ID if missing (helps for older accounts)
if (account && !dbUser.oauthId) {
console.log("Linking OAuth ID for user:", normalisedEmail);
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();
user.onboarded = dbUser.onboarded ?? false;
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: userFromLogin }) {
// Initial sign-in: attach DB fields
if (userFromLogin) {
const existing = await db.query.user.findFirst({
where: eq(users.email, userFromLogin.email!),
});
if (existing) {
token.onboarded = existing.onboarded;
}
} else if (token.email) {
// On subsequent calls, keep token synced from DB
const existing = await db.query.user.findFirst({
where: eq(users.email, token.email),
});
if (existing) {
token.onboarded = existing.onboarded;
}
}
if (userFromLogin?.dbId) {
token.dbId = userFromLogin.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 }) {
// If the user has not onboarded, send them to onboarding
return `${baseUrl}/home`;
},
},
// ------------------------------------------------------------------
// General options
// ------------------------------------------------------------------
session: {
strategy: "jwt", // lightweight, avoids DB session writes
},
jwt: {
maxAge: 60 * 60 * 24 * 30, // 30 days
},
debug: false,
};
import NextAuth from "next-auth";
import { AuthOptions } from "./authOptions";
const handler = NextAuth(AuthOptions);
export { handler as GET, handler as POST };

View file

@ -4,7 +4,7 @@ import { db } from "@/app/db/db";
import { user } from "@/app/db/schema/users";
import { eq } from "drizzle-orm";
import { getServerSession } from "next-auth";
import { AuthOptions } from "@/app/api/auth/[...nextauth]/route";
import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions";
const OnboardedSchema = z.object({
onboarded: z.boolean(),

View file

@ -4,7 +4,7 @@ import { db } from "@/app/db/db";
import { userProfiles, user } from "@/app/db/schema/users";
import { eq } from "drizzle-orm";
import { getServerSession } from "next-auth";
import { AuthOptions } from "@/app/api/auth/[...nextauth]/route";
import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions";
const UserProfileSchema = z.object({
firstName: z.string().min(1),

View file

@ -1,6 +1,6 @@
import CardTiles from "../components/home/CardTiles";
import { getPortfolios } from "./utils";
import { AuthOptions } from "@/app/api/auth/[...nextauth]/route";
import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";

View file

@ -2,7 +2,7 @@ import "./globals.css";
import Provider from "./components/Provider";
import Nav from "./components/Navbar";
import { ReactQueryProvider } from "./ReactQueryProvider";
import { AuthOptions } from "@/app/api/auth/[...nextauth]/route";
import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import { getServerSession } from "next-auth/next";
import { cache } from "react";
import { Inter } from "next/font/google";

View file

@ -1,5 +1,5 @@
import { getServerSession } from "next-auth/next";
import { AuthOptions } from "./api/auth/[...nextauth]/route";
import { AuthOptions } from "./api/auth/[...nextauth]/authOptions";
import GoogleSignInButton from "./components/signin/GoogleSignInButton";
import MicrosoftSignInButton from "./components/signin/MicrosoftSignInButton";
import EmailSignInButton from "./components/signin/EmailSignInButton";