From 05acda0adb470564fd8d00caa5bfd1b8d1487ec1 Mon Sep 17 00:00:00 2001 From: Omair Saleh Date: Tue, 3 Mar 2026 06:17:34 +0800 Subject: [PATCH] auth0: Google login, social auth auto-provisioning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../src/app/(auth)/login/page.tsx | 143 ++++++++++-------- .../src/app/(auth)/signup/page.tsx | 19 +++ pledge-now-pay-later/src/lib/auth.ts | 112 +++++++++++++- 3 files changed, 204 insertions(+), 70 deletions(-) diff --git a/pledge-now-pay-later/src/app/(auth)/login/page.tsx b/pledge-now-pay-later/src/app/(auth)/login/page.tsx index 1b267ed..bb89d74 100644 --- a/pledge-now-pay-later/src/app/(auth)/login/page.tsx +++ b/pledge-now-pay-later/src/app/(auth)/login/page.tsx @@ -33,83 +33,98 @@ function LoginForm() { } } - const handleSubmit = (e: React.FormEvent) => doLogin(e) - // Auto-login as demo if ?demo=1 // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { if (isDemo) doLogin(undefined, "demo@pnpl.app", "demo1234") }, []) return (
-
-
-
- 🤲 -
-

Welcome back

-

Sign in to your charity dashboard

-
- -
- {error && ( -
- {error} +
+ {isDemo && ( +
+
+ 🤲
- )} - -
- - setEmail(e.target.value)} - className="mt-1 w-full rounded-xl border border-gray-200 px-4 py-3 text-sm focus:border-trust-blue focus:ring-2 focus:ring-trust-blue/20 outline-none transition-all" - placeholder="you@charity.org" - required - /> +

Loading demo...

+ )} -
- - setPassword(e.target.value)} - className="mt-1 w-full rounded-xl border border-gray-200 px-4 py-3 text-sm focus:border-trust-blue focus:ring-2 focus:ring-trust-blue/20 outline-none transition-all" - placeholder="••••••••" - required - /> -
+ {!isDemo && ( + <> +
+
+ 🤲 +
+

Welcome back

+

Sign in to your charity dashboard

+
- - + {/* Social login */} +
+ +
-
-
-
or
-
+
+
+
or sign in with email
+
- + {/* Email/password form */} +
doLogin(e)} className="space-y-3"> + {error && ( +
{error}
+ )} + setEmail(e.target.value)} + className="w-full rounded-xl border border-gray-200 px-4 py-3 text-sm focus:border-trust-blue focus:ring-2 focus:ring-trust-blue/20 outline-none transition-all" + placeholder="Email" + required + /> + setPassword(e.target.value)} + className="w-full rounded-xl border border-gray-200 px-4 py-3 text-sm focus:border-trust-blue focus:ring-2 focus:ring-trust-blue/20 outline-none transition-all" + placeholder="Password" + required + /> + +
-

- Don't have an account?{" "} - - Get Started Free - -

+
+
+
or
+
+ + + +

+ Don't have an account?{" "} + Get Started Free +

+ + )}
) diff --git a/pledge-now-pay-later/src/app/(auth)/signup/page.tsx b/pledge-now-pay-later/src/app/(auth)/signup/page.tsx index a7dfe72..8e3cd94 100644 --- a/pledge-now-pay-later/src/app/(auth)/signup/page.tsx +++ b/pledge-now-pay-later/src/app/(auth)/signup/page.tsx @@ -13,6 +13,11 @@ export default function SignupPage() { const [error, setError] = useState("") const router = useRouter() + const signUpWithGoogle = () => { + setStep("loading") + signIn("auth0", { callbackUrl: "/dashboard" }) + } + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!charityName.trim() || !email.trim() || !password) return @@ -71,6 +76,20 @@ export default function SignupPage() {

Free. 30 seconds. No card.

+ {/* Google signup */} + + +
+
+
or use email
+
+
{error && (
{error}
diff --git a/pledge-now-pay-later/src/lib/auth.ts b/pledge-now-pay-later/src/lib/auth.ts index eca7934..4fb0d60 100644 --- a/pledge-now-pay-later/src/lib/auth.ts +++ b/pledge-now-pay-later/src/lib/auth.ts @@ -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