diff --git a/src/components/auth/protected-content.tsx b/src/components/auth/protected-content.tsx
new file mode 100644
index 000000000..46ca38233
--- /dev/null
+++ b/src/components/auth/protected-content.tsx
@@ -0,0 +1,68 @@
+import Spinner from "@ui/spinner";
+import { useRouter } from "next/router";
+import { useSession } from "next-auth/react";
+import { type ReactNode, useEffect } from "react";
+
+type UserRole = "STUDENT" | "INSTRUCTOR" | "ADMIN" | "MENTOR";
+
+interface ProtectedContentProps {
+ children: ReactNode;
+ /** Override the default centered-spinner placeholder. */
+ fallback?: ReactNode;
+ /** When set, signed-in users without this role see a "not authorized" message. */
+ requiredRole?: UserRole;
+}
+
+/**
+ * Wraps a protected page body. While the session resolves and during a
+ * redirect to /login, renders `fallback` (default: a centered spinner) — never
+ * the page shell. That prevents the flash-of-unauthenticated-content pattern
+ * where a page renders fully and then bounces from a `useEffect`.
+ *
+ * Prefer `requireAuthSSR` in `getServerSideProps` when possible — server-side
+ * redirects don't render at all. Use this component only when the page is
+ * client-rendered (no `getServerSideProps`) or when an inner section needs
+ * gating below an unauthenticated layout.
+ */
+export default function ProtectedContent({
+ children,
+ fallback,
+ requiredRole,
+}: ProtectedContentProps) {
+ const { data: session, status } = useSession();
+ const router = useRouter();
+
+ useEffect(() => {
+ if (status === "unauthenticated") {
+ const callbackUrl = encodeURIComponent(router.asPath);
+ router.replace(`/login?callbackUrl=${callbackUrl}`);
+ }
+ }, [status, router]);
+
+ if (status === "loading" || status === "unauthenticated") {
+ return <>{fallback ??
You don't have access to this section.
+Loading assignment...
-AI teaching assistant for the Hashflag Stack
@@ -242,7 +394,10 @@ const JodiePage: PageWithLayout = () => { {error && (
{codeResult}
@@ -456,7 +634,10 @@ const JodiePage: PageWithLayout = () => {