From 6f25d3388455e13dec96cf33cffb9af357540b40 Mon Sep 17 00:00:00 2001 From: mzhang Date: Thu, 30 Jul 2026 14:09:07 -0500 Subject: [PATCH 1/4] updates to use database strategy --- apps/app-portal/src/app/api/v1/user/route.ts | 2 ++ apps/app-portal/src/lib/auth/config.ts | 17 ++++------------- apps/app-portal/src/lib/auth/guards.ts | 8 ++++++++ 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/apps/app-portal/src/app/api/v1/user/route.ts b/apps/app-portal/src/app/api/v1/user/route.ts index 5b2c78c1..eb516724 100644 --- a/apps/app-portal/src/app/api/v1/user/route.ts +++ b/apps/app-portal/src/app/api/v1/user/route.ts @@ -1,6 +1,8 @@ //GET current session user; returns 401 if no session import { NextResponse } from "next/server"; +import {requireUser} from "@/lib/auth/guards.ts"; export async function GET() { + await requireUser() return NextResponse.json({ error: "not implemented" }, { status: 501 }); } diff --git a/apps/app-portal/src/lib/auth/config.ts b/apps/app-portal/src/lib/auth/config.ts index 54d41de9..15b93bfc 100644 --- a/apps/app-portal/src/lib/auth/config.ts +++ b/apps/app-portal/src/lib/auth/config.ts @@ -9,7 +9,7 @@ export const authOptions: NextAuthOptions = { adapter: authAdapter, providers: [MainEmailProvider], secret: process.env.NEXTAUTH_SECRET, - session: { strategy: "jwt" }, + session: { strategy: "database", maxAge: 30 * 24 * 60 * 60 }, pages: { signIn: "/auth/signin", @@ -22,21 +22,12 @@ export const authOptions: NextAuthOptions = { async signIn() { return true; }, - // Runs when the JWT (the cookie's contents) is created/updated. - async jwt({ token, user }) { - if (user) { - token.id = user.id; - token.isAdmin = isAdminEmail(user.email); - } - return token; - }, // Runs when app code reads the session; shapes what the app sees. - async session({ session, token }) { + async session({ session, user }) { if (session.user) { - (session.user as { id?: string; isAdmin?: boolean }).id = - token.id as string; + (session.user as { id?: string; isAdmin?: boolean }).id = user.id; (session.user as { id?: string; isAdmin?: boolean }).isAdmin = - token.isAdmin === true; + isAdminEmail(user.email); } return session; }, diff --git a/apps/app-portal/src/lib/auth/guards.ts b/apps/app-portal/src/lib/auth/guards.ts index 7102573c..c5242e6b 100644 --- a/apps/app-portal/src/lib/auth/guards.ts +++ b/apps/app-portal/src/lib/auth/guards.ts @@ -1,6 +1,10 @@ //requireUser() and requireAdmin() helpers for route handlers import { getSession } from "./session"; +/** + * Check if current user is signed in and signed in as (basic) user + * @throws Error if unauthorized + */ export async function requireUser() { const session = await getSession(); if (!session?.user) { @@ -9,6 +13,10 @@ export async function requireUser() { return session.user; } +/** + * Check if current user is signed in, then checks if user is admin + * @throws Error if unauthorized or if not an admin + */ export async function requireAdmin() { const user = await requireUser(); if (!(user as { isAdmin?: boolean }).isAdmin) { From bb99b790e83525de1febb324f273d1b7898d5d7a Mon Sep 17 00:00:00 2001 From: mzhang Date: Fri, 31 Jul 2026 13:52:59 -0500 Subject: [PATCH 2/4] updates to ui and including email, signout and auto sign-back-in if cookie valid added --- .../app-portal/src/app/(applicant)/layout.tsx | 8 ++++--- apps/app-portal/src/app/auth/signin/page.tsx | 21 +++++++++++-------- .../src/components/auth/SignInForm.tsx | 9 ++++++-- .../src/components/auth/UserMenu.tsx | 6 +++--- 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/apps/app-portal/src/app/(applicant)/layout.tsx b/apps/app-portal/src/app/(applicant)/layout.tsx index 9a2d4421..7fe93e69 100644 --- a/apps/app-portal/src/app/(applicant)/layout.tsx +++ b/apps/app-portal/src/app/(applicant)/layout.tsx @@ -3,16 +3,18 @@ import Link from "next/link"; import UserMenu from "@/components/auth/UserMenu"; import Image from "next/image"; import icon from "@/app/icon.ico"; +import { getSession } from "@/lib/auth/session"; export const metadata = { title: "Applicant Portal", }; -export default function ApplicantLayout({ +export default async function ApplicantLayout({ children, }: { children: React.ReactNode; -}): JSX.Element { - const email = "applicant@example.com"; +}): Promise { + const session = await getSession(); + const email = session?.user?.email ?? ""; return (
diff --git a/apps/app-portal/src/app/auth/signin/page.tsx b/apps/app-portal/src/app/auth/signin/page.tsx index ec82f52f..ac1c9ca7 100644 --- a/apps/app-portal/src/app/auth/signin/page.tsx +++ b/apps/app-portal/src/app/auth/signin/page.tsx @@ -1,13 +1,16 @@ -//magic-link email entry form -"use client"; +//magic-link email entry — skips straight to /dashboard if already signed in import React from "react"; - +import { redirect } from "next/navigation"; +import { getSession } from "@/lib/auth/session"; import { SignInForm } from "@/components/auth/SignInForm"; +import {isAdminEmail} from "@/lib/auth/roles.ts"; + +export default async function Page(): Promise { + // read cookie - see if valid session in DB - if so, automatically redir user to logged in part + const session = await getSession(); + if (session?.user) { + redirect(isAdminEmail(session.user.email) ? "/admin" : "/dashboard"); + } -export default function Page(): JSX.Element { - return ( - <> - - - ); + return ; } diff --git a/apps/app-portal/src/components/auth/SignInForm.tsx b/apps/app-portal/src/components/auth/SignInForm.tsx index c99e6957..d2ef54aa 100644 --- a/apps/app-portal/src/components/auth/SignInForm.tsx +++ b/apps/app-portal/src/components/auth/SignInForm.tsx @@ -1,3 +1,4 @@ +"use client" //email input form, calls signIn("email") import React, { useEffect, useState } from "react"; import Image from "next/image"; @@ -102,10 +103,14 @@ export function SignInForm() { ) : status === "loading" ? (

Loading{".".repeat(dotCount)} - {" ".repeat(4 - dotCount)} + {" ".repeat(4 - dotCount)}

) : status === "sent" ? ( -

+

{/*todo: change the green*/} Check your email for a sign-in link!

diff --git a/apps/app-portal/src/components/auth/UserMenu.tsx b/apps/app-portal/src/components/auth/UserMenu.tsx index e1fc2040..2e14d84f 100644 --- a/apps/app-portal/src/components/auth/UserMenu.tsx +++ b/apps/app-portal/src/components/auth/UserMenu.tsx @@ -1,6 +1,7 @@ "use client"; //avatar + sign-out dropdown for header import React, { useEffect, useRef, useState } from "react"; +import { signOut } from "next-auth/react"; interface UserMenuProps { /** Signed-in user's email. Shown in the dropdown; first letter is the avatar. */ @@ -40,9 +41,8 @@ export default function UserMenu({ email }: UserMenuProps): JSX.Element { async function handleSignOut() { setOpen(false); - // TODO: wire to NextAuth — signOut({ callbackUrl: "/" }) once next-auth is installed. - await fetch("/auth/signout", { method: "POST" }).catch(() => {}); - window.location.href = "/"; + // hits /auth/signout, NextAuth automatically invalidates the session + await signOut({ callbackUrl: "/" }); } return ( From b440454a18962be87079cf89b17979ad0fc0adf9 Mon Sep 17 00:00:00 2001 From: mzhang Date: Fri, 31 Jul 2026 14:34:07 -0500 Subject: [PATCH 3/4] updates to ui and including email, signout and auto sign-back-in if cookie valid added --- apps/app-portal/src/app/(admin)/layout.tsx | 4 +--- apps/app-portal/src/app/(applicant)/layout.tsx | 10 +++------- apps/app-portal/src/app/api/v1/user/route.ts | 18 +++++++++++++++--- .../src/components/auth/UserMenu.tsx | 10 ++++------ 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/apps/app-portal/src/app/(admin)/layout.tsx b/apps/app-portal/src/app/(admin)/layout.tsx index 10552a96..31336bcd 100644 --- a/apps/app-portal/src/app/(admin)/layout.tsx +++ b/apps/app-portal/src/app/(admin)/layout.tsx @@ -21,8 +21,6 @@ export default async function AdminLayout({ redirect("/dashboard"); } - const email = user.email ?? ""; - return (
@@ -30,7 +28,7 @@ export default async function AdminLayout({

Admin Portal

- +
{children}
diff --git a/apps/app-portal/src/app/(applicant)/layout.tsx b/apps/app-portal/src/app/(applicant)/layout.tsx index 7fe93e69..06c42d6e 100644 --- a/apps/app-portal/src/app/(applicant)/layout.tsx +++ b/apps/app-portal/src/app/(applicant)/layout.tsx @@ -3,19 +3,15 @@ import Link from "next/link"; import UserMenu from "@/components/auth/UserMenu"; import Image from "next/image"; import icon from "@/app/icon.ico"; -import { getSession } from "@/lib/auth/session"; export const metadata = { title: "Applicant Portal", }; -export default async function ApplicantLayout({ +export default function ApplicantLayout({ children, }: { children: React.ReactNode; -}): Promise { - const session = await getSession(); - const email = session?.user?.email ?? ""; - +}): JSX.Element { return (
@@ -56,7 +52,7 @@ export default async function ApplicantLayout({ > Application - +
diff --git a/apps/app-portal/src/app/api/v1/user/route.ts b/apps/app-portal/src/app/api/v1/user/route.ts index eb516724..2d03a57a 100644 --- a/apps/app-portal/src/app/api/v1/user/route.ts +++ b/apps/app-portal/src/app/api/v1/user/route.ts @@ -1,8 +1,20 @@ //GET current session user; returns 401 if no session import { NextResponse } from "next/server"; -import {requireUser} from "@/lib/auth/guards.ts"; +import { requireUser } from "@/lib/auth/guards"; export async function GET() { - await requireUser() - return NextResponse.json({ error: "not implemented" }, { status: 501 }); + try { + const user = (await requireUser()) as { + email?: string | null; + id?: string; + isAdmin?: boolean; + }; + return NextResponse.json({ + email: user.email ?? null, + id: user.id, + isAdmin: user.isAdmin ?? false, + }); + } catch { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } } diff --git a/apps/app-portal/src/components/auth/UserMenu.tsx b/apps/app-portal/src/components/auth/UserMenu.tsx index 2e14d84f..ad0d3acc 100644 --- a/apps/app-portal/src/components/auth/UserMenu.tsx +++ b/apps/app-portal/src/components/auth/UserMenu.tsx @@ -2,13 +2,11 @@ //avatar + sign-out dropdown for header import React, { useEffect, useRef, useState } from "react"; import { signOut } from "next-auth/react"; +import useCurrentUser from "@/lib/auth/useCurrentUser"; -interface UserMenuProps { - /** Signed-in user's email. Shown in the dropdown; first letter is the avatar. */ - email: string; -} - -export default function UserMenu({ email }: UserMenuProps): JSX.Element { +export default function UserMenu(): JSX.Element { + const { user } = useCurrentUser(); + const email = user?.email ?? ""; const [open, setOpen] = useState(false); const containerRef = useRef(null); From 8c661edb77365b752b1ea62c223968bd62398146 Mon Sep 17 00:00:00 2001 From: mzhang Date: Fri, 31 Jul 2026 14:34:15 -0500 Subject: [PATCH 4/4] updates to ui and including email, signout and auto sign-back-in if cookie valid added --- .../app-portal/src/lib/auth/useCurrentUser.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 apps/app-portal/src/lib/auth/useCurrentUser.ts diff --git a/apps/app-portal/src/lib/auth/useCurrentUser.ts b/apps/app-portal/src/lib/auth/useCurrentUser.ts new file mode 100644 index 00000000..081d9f5a --- /dev/null +++ b/apps/app-portal/src/lib/auth/useCurrentUser.ts @@ -0,0 +1,36 @@ +"use client"; +//client hook: fetches the current user from /api/v1/user; 401 -> null +import { useEffect, useState } from "react"; + +export interface CurrentUser { + email: string | null; + id?: string; + isAdmin?: boolean; +} + +export default function useCurrentUser() { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let active = true; + + fetch("/api/v1/user", { cache: "no-store" }) + .then((res) => (res.ok ? (res.json() as Promise) : null)) + .then((data) => { + if (active) setUser(data); + }) + .catch(() => { + if (active) setUser(null); + }) + .finally(() => { + if (active) setLoading(false); + }); + + return () => { + active = false; + }; + }, []); + + return { user, loading }; +}