From b1a994a9d60bd805608c2d0619693e550a1b9f11 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 4 Mar 2026 17:00:19 +0100 Subject: [PATCH] fix: enforce onboarding gate for applicants and observers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applicants could bypass onboarding and land directly on the dashboard. Added onboardingCompletedAt check + redirect to /onboarding in both the applicant and observer layouts (jury/mentor already had this gate). Also removed premature status ACTIVE on magic-link first login — now only completeOnboarding sets ACTIVE. Co-Authored-By: Claude Opus 4.6 --- src/app/(applicant)/layout.tsx | 17 ++++++++++++----- src/app/(observer)/layout.tsx | 18 ++++++++++++++++++ src/lib/auth.ts | 9 ++------- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/app/(applicant)/layout.tsx b/src/app/(applicant)/layout.tsx index 81a9f6d..875947c 100644 --- a/src/app/(applicant)/layout.tsx +++ b/src/app/(applicant)/layout.tsx @@ -1,5 +1,6 @@ import { redirect } from 'next/navigation' -import { auth } from '@/lib/auth' +import { prisma } from '@/lib/prisma' +import { requireRole } from '@/lib/auth-redirect' import { ApplicantNav } from '@/components/layouts/applicant-nav' export const dynamic = 'force-dynamic' @@ -9,14 +10,20 @@ export default async function ApplicantLayout({ }: { children: React.ReactNode }) { - const session = await auth() + const session = await requireRole('APPLICANT') - if (!session?.user) { + // Check if user has completed onboarding + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { onboardingCompletedAt: true }, + }) + + if (!user) { redirect('/login') } - if (session.user.role !== 'APPLICANT') { - redirect('/login') + if (!user.onboardingCompletedAt) { + redirect('/onboarding') } return ( diff --git a/src/app/(observer)/layout.tsx b/src/app/(observer)/layout.tsx index b10b63e..183b07c 100644 --- a/src/app/(observer)/layout.tsx +++ b/src/app/(observer)/layout.tsx @@ -1,7 +1,11 @@ +import { redirect } from 'next/navigation' +import { prisma } from '@/lib/prisma' import { requireRole } from '@/lib/auth-redirect' import { ObserverNav } from '@/components/layouts/observer-nav' import { EditionProvider } from '@/components/observer/observer-edition-context' +export const dynamic = 'force-dynamic' + export default async function ObserverLayout({ children, }: { @@ -9,6 +13,20 @@ export default async function ObserverLayout({ }) { const session = await requireRole('OBSERVER') + // Check if user has completed onboarding + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { onboardingCompletedAt: true }, + }) + + if (!user) { + redirect('/login') + } + + if (!user.onboardingCompletedAt) { + redirect('/onboarding') + } + return (
diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 4953e8c..e5fc023 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -273,13 +273,8 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ return false // Block suspended users } - // Update status to ACTIVE on first login (from NONE or INVITED) - if (dbUser?.status === 'INVITED' || dbUser?.status === 'NONE') { - await prisma.user.update({ - where: { email: user.email! }, - data: { status: 'ACTIVE' }, - }) - } + // Note: status stays INVITED/NONE until onboarding completes. + // The completeOnboarding mutation sets status to ACTIVE. // Add user data for JWT callback if (dbUser) {