import { PrismaAdapter } from '@next-auth/prisma-adapter'; import { compare } from 'bcrypt'; import { DateTime } from 'luxon'; import { AuthOptions, Session, User } from 'next-auth'; import CredentialsProvider from 'next-auth/providers/credentials'; import GoogleProvider, { GoogleProfile } from 'next-auth/providers/google'; import { prisma } from '@documenso/prisma'; import { getUserByEmail } from '../server-only/user/get-user-by-email'; import { ErrorCode } from './error-codes'; export const NEXT_AUTH_OPTIONS: AuthOptions = { adapter: PrismaAdapter(prisma), secret: process.env.NEXTAUTH_SECRET ?? 'secret', session: { strategy: 'jwt', }, providers: [ CredentialsProvider({ name: 'Credentials', credentials: { email: { label: 'Email', type: 'email' }, password: { label: 'Password', type: 'password' }, }, authorize: async (credentials, _req) => { if (!credentials) { throw new Error(ErrorCode.CREDENTIALS_NOT_FOUND); } const { email, password } = credentials; const user = await getUserByEmail({ email }).catch(() => { throw new Error(ErrorCode.INCORRECT_EMAIL_PASSWORD); }); if (!user.password) { throw new Error(ErrorCode.USER_MISSING_PASSWORD); } const isPasswordsSame = await compare(password, user.password); if (!isPasswordsSame) { throw new Error(ErrorCode.INCORRECT_EMAIL_PASSWORD); } return { id: Number(user.id), email: user.email, name: user.name, } satisfies User; }, }), GoogleProvider({ clientId: process.env.NEXT_PRIVATE_GOOGLE_CLIENT_ID ?? '', clientSecret: process.env.NEXT_PRIVATE_GOOGLE_CLIENT_SECRET ?? '', allowDangerousEmailAccountLinking: true, profile(profile) { return { id: Number(profile.sub), name: profile.name || `${profile.given_name} ${profile.family_name}`.trim(), email: profile.email, }; }, }), ], callbacks: { async jwt({ token, user }) { const merged = { ...token, ...user, }; if (!merged.email) { const userId = Number(merged.id ?? token.sub); const retrieved = await prisma.user.findFirst({ where: { id: userId, }, }); if (!retrieved) { return token; } merged.id = retrieved.id; merged.name = retrieved.name; merged.email = retrieved.email; } if ( merged.id && (!merged.lastSignedIn || DateTime.fromISO(merged.lastSignedIn).plus({ hours: 1 }) <= DateTime.now()) ) { merged.lastSignedIn = new Date().toISOString(); await prisma.user.update({ where: { id: Number(merged.id), }, data: { lastSignedIn: merged.lastSignedIn, }, }); } return { id: merged.id, name: merged.name, email: merged.email, lastSignedIn: merged.lastSignedIn, }; }, session({ token, session }) { if (token && token.email) { return { ...session, user: { id: Number(token.id), name: token.name, email: token.email, }, } satisfies Session; } return session; }, }, };