From 5ee39a733fcf1f8294c9dc752b44f6ecde4daf12 Mon Sep 17 00:00:00 2001 From: Caden Cheng Date: Thu, 21 May 2026 22:31:03 -0700 Subject: [PATCH] Role select --- app/api/users/route.ts | 85 +++++++++++--- app/layout.tsx | 3 +- app/login/page.tsx | 2 +- app/onboarding/page.tsx | 234 +++++++++++++++++++++++++++++++++++++++ app/signup/page.tsx | 2 +- auth.ts | 67 ++++++++++- components/Providers.tsx | 12 ++ middleware.ts | 50 +++++++++ models/User.ts | 210 +++++++++++++++++------------------ types/next-auth.d.ts | 28 +++++ 10 files changed, 562 insertions(+), 131 deletions(-) create mode 100644 app/onboarding/page.tsx create mode 100644 components/Providers.tsx create mode 100644 middleware.ts create mode 100644 types/next-auth.d.ts diff --git a/app/api/users/route.ts b/app/api/users/route.ts index 47a2e36..a8600a9 100644 --- a/app/api/users/route.ts +++ b/app/api/users/route.ts @@ -6,16 +6,22 @@ * It includes handlers for fetching all users and creating new users. */ -'use server' +"use server"; -import { NextResponse} from "next/server"; +import { NextResponse } from "next/server"; import { z } from "zod"; -import { getUsers, createUser } from "@/services/user"; -// Define a Zod schema for validating new user creation requests +import { auth } from "@/auth"; +import { createUser, getUserByEmail, getUsers } from "@/services/user"; + +// Validation for new-user creation. ucsdId is optional because Google OAuth +// can't give us a real PID — users can fill it in later from their profile. const userCreateSchema = z.object({ - ucsdId: z.string().min(1), - email: z.string().email().regex(/@ucsd\.edu$/, "Must be a UCSD email"), + ucsdId: z.string().min(1).optional(), + email: z + .string() + .email() + .regex(/@ucsd\.edu$/, "Must be a UCSD email"), name: z.object({ first: z.string().min(1), last: z.string().min(1), @@ -27,32 +33,75 @@ const userCreateSchema = z.object({ * Fetch all users * @return response with user data */ -export async function GET(){ +export async function GET() { try { const users = await getUsers(); return NextResponse.json(users, { status: 200 }); - } - catch (err){ + } catch (err) { console.error(err); - return NextResponse.json({ message: "Internal server error" }, {status: 500}); + return NextResponse.json( + { message: "Internal server error" }, + { status: 500 }, + ); } } /** - * Create a new user - * @param request request object - * @return response after creating new user + * Create a new user. + * + * Used by the onboarding flow after Google sign-in: takes the Google email + * from the authenticated session, the role the user selected from the enum + * (PI / LAB_MANAGER / RESEARCHER / VIEWER), and the name (pre-filled from + * Google but editable). */ export async function POST(request: Request) { try { + const session = await auth(); + if (!session?.user?.email) { + return NextResponse.json( + { message: "Not authenticated" }, + { status: 401 }, + ); + } + const parsed = userCreateSchema.safeParse(await request.json()); - if (parsed.success === false){ - return NextResponse.json({ message: "Invalid data "}, {status: 400}); + if (parsed.success === false) { + return NextResponse.json( + { message: "Invalid data", issues: parsed.error.flatten() }, + { status: 400 }, + ); } - const newUser = await createUser(parsed.data); + + // Users can only create an account for themselves + if ( + parsed.data.email.toLowerCase() !== + session.user.email.toLowerCase() + ) { + return NextResponse.json( + { message: "Email does not match signed-in user" }, + { status: 403 }, + ); + } + + // Prevent duplicate user docs if onboarding is submitted twice + const existing = await getUserByEmail(parsed.data.email); + if (existing) { + return NextResponse.json( + { message: "User already exists" }, + { status: 409 }, + ); + } + + const newUser = await createUser({ + ...parsed.data, + lastLoginAt: new Date(), + }); return NextResponse.json(newUser, { status: 201 }); } catch (err) { console.error(err); - return NextResponse.json({ message: "Internal server error " }, {status: 500}); + return NextResponse.json( + { message: "Internal server error" }, + { status: 500 }, + ); } -} \ No newline at end of file +} diff --git a/app/layout.tsx b/app/layout.tsx index f116187..c745ff8 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import localFont from "next/font/local"; +import Providers from "@/components/Providers"; import "./globals.css"; const geistSans = localFont({ @@ -27,7 +28,7 @@ export default function RootLayout({ return ( - {children} + {children} ); diff --git a/app/login/page.tsx b/app/login/page.tsx index 94bcef2..6aab20a 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -114,7 +114,7 @@ export default function SignInPage() { {/* SSO Sign In Button */} + + + + + + ); +} diff --git a/app/signup/page.tsx b/app/signup/page.tsx index 8534e57..ae5716b 100644 --- a/app/signup/page.tsx +++ b/app/signup/page.tsx @@ -33,7 +33,7 @@ export default function SignUpPage() { {/* sign in */}