- Removed unnecessary check for session.user existence, ensuring it is always populated with token data. - Updated comments to clarify session return behavior when token validation fails, allowing NextAuth to manage invalid tokens.
140 lines
4.1 KiB
TypeScript
140 lines
4.1 KiB
TypeScript
import NextAuth from "next-auth"
|
|
import Credentials from "next-auth/providers/credentials"
|
|
import { prisma } from "./prisma"
|
|
import bcrypt from "bcryptjs"
|
|
|
|
const nextAuthSecret = process.env.NEXTAUTH_SECRET
|
|
if (!nextAuthSecret) {
|
|
throw new Error("NEXTAUTH_SECRET is not set. Define it to enable authentication.")
|
|
}
|
|
|
|
export const { handlers, auth, signIn, signOut } = NextAuth({
|
|
trustHost: true,
|
|
debug: process.env.NODE_ENV !== "production",
|
|
providers: [
|
|
Credentials({
|
|
name: "Credentials",
|
|
credentials: {
|
|
email: { label: "Email", type: "email" },
|
|
password: { label: "Password", type: "password" }
|
|
},
|
|
async authorize(credentials) {
|
|
try {
|
|
if (!credentials?.email || !credentials?.password) {
|
|
return null
|
|
}
|
|
|
|
const email = credentials.email as string
|
|
const password = credentials.password as string
|
|
|
|
const user = await prisma.user.findUnique({
|
|
where: { email }
|
|
})
|
|
|
|
if (!user || !user.passwordHash) {
|
|
return null
|
|
}
|
|
|
|
const isValid = await bcrypt.compare(password, user.passwordHash)
|
|
|
|
if (!isValid) {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
id: user.id,
|
|
email: user.email,
|
|
name: user.name,
|
|
role: user.role,
|
|
}
|
|
} catch (err) {
|
|
console.error("Auth authorize error:", err)
|
|
return null
|
|
}
|
|
}
|
|
})
|
|
],
|
|
callbacks: {
|
|
async jwt({ token, user }) {
|
|
if (user) {
|
|
token.id = user.id
|
|
token.role = (user as { role: string }).role
|
|
token.email = user.email
|
|
token.name = user.name
|
|
console.log("JWT callback: user added to token", { userId: user.id, email: user.email })
|
|
} else {
|
|
console.log("JWT callback: no user, token exists", {
|
|
hasToken: !!token,
|
|
tokenKeys: token ? Object.keys(token) : [],
|
|
tokenId: token?.id,
|
|
tokenEmail: token?.email,
|
|
tokenName: token?.name,
|
|
tokenRole: token?.role
|
|
})
|
|
}
|
|
return token
|
|
},
|
|
async session({ session, token }) {
|
|
console.log("Session callback: called", {
|
|
hasToken: !!token,
|
|
hasSession: !!session,
|
|
tokenId: token?.id,
|
|
tokenEmail: token?.email,
|
|
stackTrace: new Error().stack?.split('\n').slice(1, 4).join('\n')
|
|
})
|
|
// Always ensure session.user exists when token exists
|
|
if (token && (token.id || token.email)) {
|
|
session.user = {
|
|
...session.user,
|
|
id: token.id as string,
|
|
email: (token.email as string) || session.user?.email || "",
|
|
name: (token.name as string) || session.user?.name || "",
|
|
role: token.role as string,
|
|
}
|
|
console.log("Session callback: session created", {
|
|
userId: token.id,
|
|
email: token.email,
|
|
hasUser: !!session.user,
|
|
userKeys: session.user ? Object.keys(session.user) : [],
|
|
userRole: token.role,
|
|
sessionUser: session.user,
|
|
sessionExpires: session.expires,
|
|
fullSession: JSON.stringify(session, null, 2)
|
|
})
|
|
} else {
|
|
console.warn("Session callback: token missing or invalid", {
|
|
hasToken: !!token,
|
|
tokenKeys: token ? Object.keys(token) : [],
|
|
hasSession: !!session,
|
|
sessionKeys: session ? Object.keys(session) : [],
|
|
sessionUser: session?.user,
|
|
tokenId: token?.id,
|
|
tokenEmail: token?.email
|
|
})
|
|
// Return session even if token is invalid - NextAuth will handle validation
|
|
}
|
|
// Explicitly return session to ensure it's returned
|
|
return session
|
|
}
|
|
},
|
|
pages: {
|
|
signIn: "/login",
|
|
},
|
|
session: {
|
|
strategy: "jwt",
|
|
maxAge: 30 * 24 * 60 * 60, // 30 days
|
|
},
|
|
cookies: {
|
|
sessionToken: {
|
|
name: `__Secure-authjs.session-token`,
|
|
options: {
|
|
httpOnly: true,
|
|
sameSite: "lax",
|
|
path: "/",
|
|
secure: true, // Always secure in production (HTTPS required)
|
|
},
|
|
},
|
|
},
|
|
secret: nextAuthSecret,
|
|
})
|