auth0: Google login, social auth auto-provisioning
AUTH0 SETUP (done via Management API): - Created 'Pledge Now Pay Later' app (regular_web) on quikcue.us.auth0.com - Enabled connections: Google, Apple, Username-Password - Callback: https://pledge.quikcue.com/api/auth/callback/auth0 - Client ID: hpr7JcEAAk3Q5ADkzyyZSRDxGIZTcjRJ CODE CHANGES: - Auth0Provider added to NextAuth alongside existing CredentialsProvider - findOrCreateSocialUser(): first Google login auto-creates org + user - Login page: 'Continue with Google' button at top, email/password below - Signup page: 'Sign up with Google' button at top, form below - JWT callback: resolves Auth0 users to DB users on every token refresh - Docker compose: AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET, AUTH0_ISSUER env vars FLOW: - Click 'Continue with Google' → Auth0 Universal Login → Google consent - First time: auto-creates '{Name}'s Charity' org + org_admin user - Return time: finds existing user, loads their org - Demo login still works via credentials provider
This commit is contained in:
@@ -1,15 +1,85 @@
|
||||
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",
|
||||
newUser: "/dashboard/setup",
|
||||
},
|
||||
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: {
|
||||
@@ -42,14 +112,44 @@ export const authOptions: NextAuthOptions = {
|
||||
}),
|
||||
],
|
||||
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
|
||||
token.role = u.role
|
||||
token.orgId = u.orgId
|
||||
token.orgName = u.orgName
|
||||
token.orgSlug = u.orgSlug
|
||||
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
|
||||
},
|
||||
@@ -57,7 +157,7 @@ export const authOptions: NextAuthOptions = {
|
||||
if (session.user) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const s = session as any
|
||||
s.user.id = token.sub
|
||||
s.user.id = token.dbId || token.sub
|
||||
s.user.role = token.role
|
||||
s.user.orgId = token.orgId
|
||||
s.user.orgName = token.orgName
|
||||
|
||||
Reference in New Issue
Block a user