From 2bb0b1d1338f6ba7796529728a1143f7c8e8d2c9 Mon Sep 17 00:00:00 2001 From: Caden Cheng Date: Mon, 25 May 2026 20:24:02 -0700 Subject: [PATCH] added fields --- app/api/users/route.ts | 30 ++++++++++++-- app/onboarding/page.tsx | 90 +++++++++++++++++++++++++++++++++++++++++ models/User.ts | 10 +++-- 3 files changed, 124 insertions(+), 6 deletions(-) diff --git a/app/api/users/route.ts b/app/api/users/route.ts index a8600a9..d581cd1 100644 --- a/app/api/users/route.ts +++ b/app/api/users/route.ts @@ -14,10 +14,13 @@ import { z } from "zod"; 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. +// Validation for new-user creation. ucsdId is required and must be unique +// (Google OAuth doesn't give us a real PID, so users enter it at signup). const userCreateSchema = z.object({ - ucsdId: z.string().min(1).optional(), + ucsdId: z + .string() + .trim() + .regex(/^[A-Za-z][0-9]{8}$/, "UCSD PID must be 1 letter + 8 digits"), email: z .string() .email() @@ -27,6 +30,11 @@ const userCreateSchema = z.object({ last: z.string().min(1), }), role: z.enum(["PI", "LAB_MANAGER", "RESEARCHER", "VIEWER"]), + profile: z.object({ + pronouns: z.string().max(50).optional(), + phone: z.string().max(30).optional(), + description: z.string().min(1, "Description is required").max(500), + }), }); /** @@ -98,6 +106,22 @@ export async function POST(request: Request) { }); return NextResponse.json(newUser, { status: 201 }); } catch (err) { + // Surface duplicate-key collisions (e.g. ucsdId already taken) as 409 + // instead of a generic 500. + if ( + typeof err === "object" && + err !== null && + "code" in err && + (err as { code?: number }).code === 11000 + ) { + const field = + Object.keys((err as { keyPattern?: Record }) + .keyPattern ?? {})[0] ?? "field"; + return NextResponse.json( + { message: `That ${field} is already in use.` }, + { status: 409 }, + ); + } console.error(err); return NextResponse.json( { message: "Internal server error" }, diff --git a/app/onboarding/page.tsx b/app/onboarding/page.tsx index 0d32310..343ce91 100644 --- a/app/onboarding/page.tsx +++ b/app/onboarding/page.tsx @@ -39,7 +39,11 @@ export default function OnboardingPage() { const [firstName, setFirstName] = useState(""); const [lastName, setLastName] = useState(""); + const [ucsdId, setUcsdId] = useState(""); const [role, setRole] = useState(""); + const [pronouns, setPronouns] = useState(""); + const [phone, setPhone] = useState(""); + const [description, setDescription] = useState(""); const [submitting, setSubmitting] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState(null); @@ -95,6 +99,14 @@ export default function OnboardingPage() { setError("First and last name are required."); return; } + if (!/^[A-Za-z][0-9]{8}$/.test(ucsdId.trim())) { + setError("UCSD PID must be 1 letter followed by 8 digits (e.g. A12345678)."); + return; + } + if (!description.trim()) { + setError("Please add a short description about yourself."); + return; + } setSubmitting(true); try { @@ -103,11 +115,17 @@ export default function OnboardingPage() { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: session.user.email, + ucsdId: ucsdId.trim().toUpperCase(), name: { first: firstName.trim(), last: lastName.trim(), }, role, + profile: { + pronouns: pronouns.trim(), + phone: phone.trim(), + description: description.trim(), + }, }), }); @@ -187,6 +205,25 @@ export default function OnboardingPage() { +
+ + + setUcsdId(e.target.value.toUpperCase()) + } + placeholder="A12345678" + maxLength={9} + required + pattern="[A-Za-z][0-9]{8}" + title="1 letter followed by 8 digits" + className="block w-full px-3 py-2.5 rounded-md bg-[#f3f4f6] text-gray-900 focus:bg-white focus:ring-2 focus:ring-[#ea7032]/50 outline-none transition-colors sm:text-sm font-mono tracking-wider" + /> +
+
+
+
+ + setPronouns(e.target.value)} + placeholder="she/her, he/him, they/them…" + maxLength={50} + className="block w-full px-3 py-2.5 rounded-md bg-[#f3f4f6] text-gray-900 focus:bg-white focus:ring-2 focus:ring-[#ea7032]/50 outline-none transition-colors sm:text-sm" + /> +
+
+ + setPhone(e.target.value)} + placeholder="(555) 123-4567" + maxLength={30} + className="block w-full px-3 py-2.5 rounded-md bg-[#f3f4f6] text-gray-900 focus:bg-white focus:ring-2 focus:ring-[#ea7032]/50 outline-none transition-colors sm:text-sm" + /> +
+
+ +
+ +