import { type NextAuthOptions } from "next-auth" import CredentialsProvider from "next-auth/providers/credentials" import Auth0Provider from "next-auth/providers/auth0" import { compare } from "bcryptjs" import prisma from "@/lib/prisma" /** * Find or create a user+org from an Auth0 social login. * First login creates the org; subsequent logins find existing. */ async function findOrCreateSocialUser(profile: { email: string; name?: string; picture?: string }) { if (!prisma || !profile.email) return null const email = profile.email.toLowerCase().trim() // Check if user exists const existing = await prisma.user.findUnique({ where: { email }, include: { organization: { select: { id: true, name: true, slug: true } } }, }) if (existing) { return { id: existing.id, email: existing.email, name: existing.name, role: existing.role, orgId: existing.organizationId, orgName: existing.organization.name, orgSlug: existing.organization.slug, } } // First-time social login → create org + user const name = profile.name || email.split("@")[0] const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 30) + "-" + Date.now().toString(36) const result = await prisma.$transaction(async (tx) => { const org = await tx.organization.create({ data: { name: `${name}'s Charity`, slug, country: "GB", refPrefix: slug.substring(0, 4).toUpperCase(), }, }) const user = await tx.user.create({ data: { email, name, role: "org_admin", organizationId: org.id, }, }) return { user, org } }) return { id: result.user.id, email: result.user.email, name: result.user.name, role: result.user.role, orgId: result.org.id, orgName: result.org.name, orgSlug: result.org.slug, } } export const authOptions: NextAuthOptions = { session: { strategy: "jwt" }, pages: { signIn: "/login", }, providers: [ // Auth0 — Google, Apple, email/password via Universal Login Auth0Provider({ clientId: process.env.AUTH0_CLIENT_ID || "hpr7JcEAAk3Q5ADkzyyZSRDxGIZTcjRJ", clientSecret: process.env.AUTH0_CLIENT_SECRET || "ha6Q5bK1B-YaluwznBvgi8jaCpqwdNmLq-UAca_-WHVy6Yfscf1tfNCrHPxKwvAh", issuer: process.env.AUTH0_ISSUER || "https://quikcue.us.auth0.com", }), // Keep credentials for demo login + existing password users CredentialsProvider({ name: "credentials", credentials: { email: { label: "Email", type: "email" }, password: { label: "Password", type: "password" }, }, async authorize(credentials) { if (!credentials?.email || !credentials?.password || !prisma) return null const user = await prisma.user.findUnique({ where: { email: credentials.email.toLowerCase().trim() }, include: { organization: { select: { id: true, name: true, slug: true } } }, }) if (!user || !user.hashedPassword) return null const valid = await compare(credentials.password, user.hashedPassword) if (!valid) return null return { id: user.id, email: user.email, name: user.name, role: user.role, orgId: user.organizationId, orgName: user.organization.name, orgSlug: user.organization.slug, } }, }), ], callbacks: { async signIn({ user, account, profile }) { // For Auth0 social logins, find/create user in our DB if (account?.provider === "auth0" && profile?.email) { const dbUser = await findOrCreateSocialUser({ email: profile.email, name: (profile as { name?: string }).name || undefined, picture: (profile as { picture?: string }).picture || undefined, }) if (dbUser) { // Attach our DB fields to the user object for the jwt callback Object.assign(user, dbUser) } return true } return true }, async jwt({ token, user }) { if (user) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const u = user as any if (u.orgId) { token.role = u.role token.orgId = u.orgId token.orgName = u.orgName token.orgSlug = u.orgSlug token.dbId = u.id } } // For Auth0 users on first token creation, look up from DB if (!token.orgId && token.email) { const dbUser = await findOrCreateSocialUser({ email: token.email as string, name: token.name || undefined }) if (dbUser) { token.role = dbUser.role token.orgId = dbUser.orgId token.orgName = dbUser.orgName token.orgSlug = dbUser.orgSlug token.dbId = dbUser.id } } return token }, async session({ session, token }) { if (session.user) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const s = session as any s.user.id = token.dbId || token.sub s.user.role = token.role s.user.orgId = token.orgId s.user.orgName = token.orgName s.user.orgSlug = token.orgSlug } return session }, }, }