125 lines
3.6 KiB
TypeScript
125 lines
3.6 KiB
TypeScript
import { NextAuthOptions } from "next-auth";
|
|
import GoogleProvider from "next-auth/providers/google";
|
|
import AzureADProvider from "next-auth/providers/azure-ad";
|
|
import AppleProvider from "next-auth/providers/apple";
|
|
import { PrismaAdapter } from "@next-auth/prisma-adapter";
|
|
import CredentialsProvider from "next-auth/providers/credentials";
|
|
import { compare } from "bcryptjs";
|
|
import { prisma } from "@/lib/prisma";
|
|
|
|
export const authOptions: NextAuthOptions = {
|
|
adapter: PrismaAdapter(prisma),
|
|
providers: [
|
|
GoogleProvider({
|
|
clientId: process.env.GOOGLE_CLIENT_ID || "",
|
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET || "",
|
|
allowDangerousEmailAccountLinking: true,
|
|
authorization: {
|
|
params: {
|
|
prompt: "consent",
|
|
access_type: "offline",
|
|
response_type: "code",
|
|
scope: "openid profile email https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/calendar.events https://www.googleapis.com/auth/tasks"
|
|
}
|
|
}
|
|
}),
|
|
AppleProvider({
|
|
clientId: process.env.APPLE_ID || "",
|
|
clientSecret: process.env.APPLE_SECRET || "",
|
|
allowDangerousEmailAccountLinking: true,
|
|
}),
|
|
AzureADProvider({
|
|
clientId: process.env.MICROSOFT_CLIENT_ID || "",
|
|
clientSecret: process.env.MICROSOFT_CLIENT_SECRET || "",
|
|
tenantId: "common",
|
|
allowDangerousEmailAccountLinking: true,
|
|
authorization: {
|
|
params: {
|
|
prompt: "consent",
|
|
scope: "openid profile email offline_access user.read Calendars.ReadWrite Tasks.ReadWrite"
|
|
}
|
|
},
|
|
}),
|
|
CredentialsProvider({
|
|
name: "credentials",
|
|
credentials: {
|
|
email: { label: "Email", type: "email" },
|
|
password: { label: "Password", type: "password" }
|
|
},
|
|
async authorize(credentials) {
|
|
if (!credentials?.email || !credentials?.password) {
|
|
throw new Error("Invalid credentials");
|
|
}
|
|
|
|
const user = await prisma.user.findUnique({
|
|
where: {
|
|
email: credentials.email
|
|
}
|
|
});
|
|
|
|
if (!user || !user.passwordHash) {
|
|
throw new Error("Invalid credentials");
|
|
}
|
|
|
|
// Block login if email is not verified
|
|
if (!user.emailVerified) {
|
|
throw new Error("email_not_verified");
|
|
}
|
|
|
|
const isPasswordValid = await compare(
|
|
credentials.password,
|
|
user.passwordHash
|
|
);
|
|
|
|
if (!isPasswordValid) {
|
|
throw new Error("Invalid credentials");
|
|
}
|
|
|
|
return {
|
|
id: user.id,
|
|
email: user.email,
|
|
name: user.name || null,
|
|
image: user.image || null,
|
|
};
|
|
}
|
|
})
|
|
],
|
|
debug: true,
|
|
session: {
|
|
strategy: "jwt"
|
|
},
|
|
pages: {
|
|
signIn: "/auth/login",
|
|
signOut: "/auth/login",
|
|
error: "/auth/login",
|
|
},
|
|
callbacks: {
|
|
async signIn({ user, account }) {
|
|
// For OAuth providers: ensure user ID is set on the token
|
|
// allowDangerousEmailAccountLinking handles auto-linking
|
|
if (account?.provider !== "credentials" && user.email) {
|
|
const existingUser = await prisma.user.findUnique({
|
|
where: { email: user.email },
|
|
});
|
|
if (existingUser) {
|
|
// Ensure the JWT gets the correct DB user ID
|
|
user.id = existingUser.id;
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
async jwt({ token, user }) {
|
|
if (user) {
|
|
token.id = user.id;
|
|
}
|
|
return token;
|
|
},
|
|
async session({ session, token }) {
|
|
if (session.user) {
|
|
(session.user as any).id = token.id;
|
|
}
|
|
return session;
|
|
}
|
|
},
|
|
secret: process.env.NEXTAUTH_SECRET
|
|
}; |