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 ?? }; + } + + if (requiredRole && session?.user?.role !== requiredRole) { + return ; + } + + return <>{children}; +} + +function DefaultLoadingFallback() { + return ( +
+ +
+ ); +} + +function DefaultUnauthorizedFallback() { + return ( +
+

Not authorized

+

You don't have access to this section.

+
+ ); +} diff --git a/src/pages/assessment.tsx b/src/pages/assessment.tsx index 13e506fed..79a343802 100644 --- a/src/pages/assessment.tsx +++ b/src/pages/assessment.tsx @@ -1,12 +1,9 @@ +import ProtectedContent from "@components/auth/protected-content"; import Breadcrumb from "@components/breadcrumb"; import CodeEditor from "@components/code-editor"; import SEO from "@components/seo/page-seo"; -import { useMount } from "@hooks"; import Layout01 from "@layout/layout-01"; -import Spinner from "@ui/spinner"; import type { GetStaticProps, NextPage } from "next"; -import { useRouter } from "next/router"; -import { useSession } from "next-auth/react"; import React, { useEffect, useState } from "react"; import { type AssessmentQuestion, @@ -27,10 +24,6 @@ type PageWithLayout = NextPage & { }; const Assessment: PageWithLayout = () => { - const mounted = useMount(); - const { data: session, status } = useSession(); - const router = useRouter(); - const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); const [userCode, setUserCode] = useState(""); const [answers, setAnswers] = useState>({}); @@ -47,12 +40,6 @@ const Assessment: PageWithLayout = () => { message: string; } | null>(null); - useEffect(() => { - if (status === "unauthenticated") { - router.replace("/login"); - } - }, [status, router]); - const currentQuestion: AssessmentQuestion = assessmentQuestions[currentQuestionIndex]; // Initialize code with starter code when question changes @@ -64,22 +51,6 @@ const Assessment: PageWithLayout = () => { } }, [currentQuestionIndex, currentQuestion, answers]); - if (!mounted || status === "loading") { - return ( -
- -
- ); - } - - if (!session) { - return ( -
- -
- ); - } - const handleCodeChange = (newCode: string) => { setUserCode(newCode); }; @@ -213,7 +184,7 @@ const Assessment: PageWithLayout = () => { if (isComplete) { return ( - <> + { - + ); } return ( - <> + { - + ); }; diff --git a/src/pages/assignments/submit/[assignmentId].tsx b/src/pages/assignments/submit/[assignmentId].tsx index d1132f8cd..5f3ad0343 100644 --- a/src/pages/assignments/submit/[assignmentId].tsx +++ b/src/pages/assignments/submit/[assignmentId].tsx @@ -3,10 +3,8 @@ import SEO from "@components/seo/page-seo"; import Layout01 from "@layout/layout-01"; import type { GetServerSideProps, NextPage } from "next"; import Link from "next/link"; -import { useRouter } from "next/router"; import { getServerSession } from "next-auth/next"; -import { useSession } from "next-auth/react"; -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import prisma from "@/lib/prisma"; import { options } from "@/pages/api/auth/options"; @@ -40,8 +38,6 @@ type PageWithLayout = NextPage & { }; const AssignmentSubmissionPage: PageWithLayout = ({ assignment }) => { - const { data: session, status } = useSession(); - const router = useRouter(); const [submitting, setSubmitting] = useState(false); const [submitted, setSubmitted] = useState(false); const [error, setError] = useState(null); @@ -97,24 +93,6 @@ const AssignmentSubmissionPage: PageWithLayout = ({ assignment }) => { setFiles(e.target.files); }; - useEffect(() => { - if (status !== "loading" && !session) { - const callbackUrl = encodeURIComponent(router.asPath); - router.replace(`/login?callbackUrl=${callbackUrl}`); - } - }, [session, status, router]); - - if (status === "loading" || !session) { - return ( -
-
-
-

Loading assignment...

-
-
- ); - } - if (submitted) { return ( <> diff --git a/src/pages/jodie/index.tsx b/src/pages/jodie/index.tsx index 12ca01a4f..471f04251 100644 --- a/src/pages/jodie/index.tsx +++ b/src/pages/jodie/index.tsx @@ -2,10 +2,9 @@ import Breadcrumb from "@components/breadcrumb"; import SEO from "@components/seo/page-seo"; import Layout01 from "@layout/layout-01"; import type { GetServerSideProps, NextPage } from "next"; -import { getServerSession } from "next-auth/next"; -import { useCallback, useState } from "react"; -import { options } from "@/pages/api/auth/options"; import Link from "next/link"; +import { useCallback, useState } from "react"; +import { requireAuthSSR } from "@/lib/auth-guards"; type Pillar = "learn" | "code" | "debug"; @@ -29,37 +28,162 @@ type PageWithLayout = NextPage & { // Hashflag Stack: 25 modules across 4 phases. Modules take variable time — no fixed weekly schedule. const HASHFLAG_MODULES = [ // Phase 1: Foundations (Modules 1–8) - { module: 1, phase: "Foundations", title: "Terminal Mastery", topics: "File system navigation, file operations, text processing (grep/sed/awk), piping & redirection, shell config (.zshrc), package management, process management, SSH, shell scripting" }, - { module: 2, phase: "Foundations", title: "VS Code Mastery", topics: "Core navigation, Command Palette, extensions (ESLint, Prettier, GitLens), workspace config, integrated terminal, debugging (JS/Python), keyboard shortcuts" }, - { module: 3, phase: "Foundations", title: "Git & GitHub", topics: "Git architecture, staging/commits, branching strategies, merging/rebasing, conflict resolution, GitHub collaboration, PRs, code review, advanced Git (stash, cherry-pick, bisect, reflog)" }, - { module: 4, phase: "Foundations", title: "HTML & CSS Fundamentals", topics: "Semantic HTML5, accessibility, forms, CSS selectors & specificity, Box Model, Flexbox, Grid, responsive design, media queries, typography, transitions & animations" }, - { module: 5, phase: "Foundations", title: "JavaScript Fundamentals", topics: "Variables, data types, operators, control flow, functions, closures, arrays (map/filter/reduce), objects, destructuring, DOM manipulation, events, error handling, async/await, Promises, fetch" }, - { module: 6, phase: "Foundations", title: "Python Fundamentals", topics: "Variables, data types, control flow, functions (*args/**kwargs), lists, tuples, dicts, sets, string processing, file I/O, JSON, error handling, OOP basics, classes" }, - { module: 7, phase: "Foundations", title: "Software Development Lifecycle", topics: "User stories, requirements, Agile/Scrum/Kanban, sprint planning, standups, retros, technical documentation, READMEs, code review best practices" }, - { module: 8, phase: "Foundations", title: "Code Challenges", topics: "Data structures (arrays, linked lists, stacks, queues, hash maps, trees), algorithm patterns (two pointers, sliding window, recursion), Big O, 50+ easy + 25+ medium LeetCode in JS & Python. Gate: 3 problems in 90 min" }, + { + module: 1, + phase: "Foundations", + title: "Terminal Mastery", + topics: "File system navigation, file operations, text processing (grep/sed/awk), piping & redirection, shell config (.zshrc), package management, process management, SSH, shell scripting", + }, + { + module: 2, + phase: "Foundations", + title: "VS Code Mastery", + topics: "Core navigation, Command Palette, extensions (ESLint, Prettier, GitLens), workspace config, integrated terminal, debugging (JS/Python), keyboard shortcuts", + }, + { + module: 3, + phase: "Foundations", + title: "Git & GitHub", + topics: "Git architecture, staging/commits, branching strategies, merging/rebasing, conflict resolution, GitHub collaboration, PRs, code review, advanced Git (stash, cherry-pick, bisect, reflog)", + }, + { + module: 4, + phase: "Foundations", + title: "HTML & CSS Fundamentals", + topics: "Semantic HTML5, accessibility, forms, CSS selectors & specificity, Box Model, Flexbox, Grid, responsive design, media queries, typography, transitions & animations", + }, + { + module: 5, + phase: "Foundations", + title: "JavaScript Fundamentals", + topics: "Variables, data types, operators, control flow, functions, closures, arrays (map/filter/reduce), objects, destructuring, DOM manipulation, events, error handling, async/await, Promises, fetch", + }, + { + module: 6, + phase: "Foundations", + title: "Python Fundamentals", + topics: "Variables, data types, control flow, functions (*args/**kwargs), lists, tuples, dicts, sets, string processing, file I/O, JSON, error handling, OOP basics, classes", + }, + { + module: 7, + phase: "Foundations", + title: "Software Development Lifecycle", + topics: "User stories, requirements, Agile/Scrum/Kanban, sprint planning, standups, retros, technical documentation, READMEs, code review best practices", + }, + { + module: 8, + phase: "Foundations", + title: "Code Challenges", + topics: "Data structures (arrays, linked lists, stacks, queues, hash maps, trees), algorithm patterns (two pointers, sliding window, recursion), Big O, 50+ easy + 25+ medium LeetCode in JS & Python. Gate: 3 problems in 90 min", + }, // Phase 2: Software Engineering (Modules 9–13) - { module: 9, phase: "Software Engineering", title: "Advanced JavaScript & TypeScript", topics: "ES6+ features, advanced async (Promise.all/race/allSettled), modules, classes, TypeScript fundamentals (interfaces, generics, utility types), advanced TS (conditional types, mapped types), debugging" }, - { module: 10, phase: "Software Engineering", title: "Next.js Application Development", topics: "App Router, layouts, Server & Client Components, data fetching (ISR, Suspense, SWR), Server Actions, API routes, Tailwind CSS styling, performance optimization (next/image, next/font, code splitting)" }, - { module: 11, phase: "Software Engineering", title: "Testing Fundamentals", topics: "Testing pyramid, TDD, Jest (matchers, mocking), React Testing Library, integration testing, E2E with Playwright (Page Object Model, visual regression), CI integration" }, - { module: 12, phase: "Software Engineering", title: "Deployment & CI/CD", topics: "Vercel deployment, preview deploys, GitHub Actions (triggers, jobs, secrets, matrix, caching), CI pipeline (lint, typecheck, test), CD pipeline (feature flags, rollbacks, blue-green)" }, - { module: 13, phase: "Software Engineering", title: "Media Management & Analytics", topics: "Cloudinary (upload, transforms, optimization), image formats (WebP/AVIF), lazy loading, Microsoft Clarity (session recordings, heatmaps), Google Analytics 4, event tracking" }, + { + module: 9, + phase: "Software Engineering", + title: "Advanced JavaScript & TypeScript", + topics: "ES6+ features, advanced async (Promise.all/race/allSettled), modules, classes, TypeScript fundamentals (interfaces, generics, utility types), advanced TS (conditional types, mapped types), debugging", + }, + { + module: 10, + phase: "Software Engineering", + title: "Next.js Application Development", + topics: "App Router, layouts, Server & Client Components, data fetching (ISR, Suspense, SWR), Server Actions, API routes, Tailwind CSS styling, performance optimization (next/image, next/font, code splitting)", + }, + { + module: 11, + phase: "Software Engineering", + title: "Testing Fundamentals", + topics: "Testing pyramid, TDD, Jest (matchers, mocking), React Testing Library, integration testing, E2E with Playwright (Page Object Model, visual regression), CI integration", + }, + { + module: 12, + phase: "Software Engineering", + title: "Deployment & CI/CD", + topics: "Vercel deployment, preview deploys, GitHub Actions (triggers, jobs, secrets, matrix, caching), CI pipeline (lint, typecheck, test), CD pipeline (feature flags, rollbacks, blue-green)", + }, + { + module: 13, + phase: "Software Engineering", + title: "Media Management & Analytics", + topics: "Cloudinary (upload, transforms, optimization), image formats (WebP/AVIF), lazy loading, Microsoft Clarity (session recordings, heatmaps), Google Analytics 4, event tracking", + }, // Phase 3: AI Engineering (Modules 14–21) - { module: 14, phase: "AI Engineering", title: "AI Foundations", topics: "AI vs ML vs Deep Learning, learning paradigms (supervised/unsupervised/reinforcement), Transformer architecture, self-attention, tokens & embeddings, context windows, LLM landscape (GPT-4, Claude, Gemini, Llama)" }, - { module: 15, phase: "AI Engineering", title: "Advanced Python for AI", topics: "Python 3.11+ patterns, uv, type hints, Pydantic v2 (BaseModel, validators, serialization, BaseSettings, computed fields, JSON schema), async Python (asyncio, httpx)" }, - { module: 16, phase: "AI Engineering", title: "FastAPI: Production AI APIs", topics: "Routing, path/query params, Pydantic request/response, dependency injection, OpenAPI 3.1, streaming responses (SSE, AsyncIterator), middleware, CORS, API key auth, rate limiting, background tasks" }, - { module: 17, phase: "AI Engineering", title: "Google Gemini Integration", topics: "Gemini Pro/Flash, auth, generation config (temperature, top_p, top_k), text generation, streaming, chat sessions, structured output (JSON mode + Pydantic schema), multimodal (images, PDF, video, audio), function calling" }, - { module: 18, phase: "AI Engineering", title: "Professional Prompt Engineering", topics: "Zero/one/few-shot, Chain of Thought, Self-Consistency, Tree of Thoughts, ReAct, XML tags & delimiters, schema-guided generation, role assignment, prompt templates (Jinja2), A/B testing, evaluation" }, - { module: 19, phase: "AI Engineering", title: "RAG Systems", topics: "Document ingestion, chunking strategies, embeddings, pgvector (Postgres), ChromaDB, hybrid search (keyword + semantic), reranking, query expansion (HyDE), evaluation (Precision@k, Recall@k, MRR, faithfulness)" }, - { module: 20, phase: "AI Engineering", title: "AI Agents & Workflows", topics: "Agent architecture (perception, reasoning, action, reflection), tool use & function calling, memory systems (short/long-term, episodic, semantic), LangChain, LangGraph, multi-agent systems, human-in-the-loop" }, - { module: 21, phase: "AI Engineering", title: "Full-Stack AI Integration", topics: "Next.js AI patterns, Server Components for AI, streaming chat UI (SSE, token-by-token, markdown rendering), type-safe integration (OpenAPI client gen, Zod ↔ Pydantic), file upload (drag-and-drop, multimodal)" }, + { + module: 14, + phase: "AI Engineering", + title: "AI Foundations", + topics: "AI vs ML vs Deep Learning, learning paradigms (supervised/unsupervised/reinforcement), Transformer architecture, self-attention, tokens & embeddings, context windows, LLM landscape (GPT-4, Claude, Gemini, Llama)", + }, + { + module: 15, + phase: "AI Engineering", + title: "Advanced Python for AI", + topics: "Python 3.11+ patterns, uv, type hints, Pydantic v2 (BaseModel, validators, serialization, BaseSettings, computed fields, JSON schema), async Python (asyncio, httpx)", + }, + { + module: 16, + phase: "AI Engineering", + title: "FastAPI: Production AI APIs", + topics: "Routing, path/query params, Pydantic request/response, dependency injection, OpenAPI 3.1, streaming responses (SSE, AsyncIterator), middleware, CORS, API key auth, rate limiting, background tasks", + }, + { + module: 17, + phase: "AI Engineering", + title: "Google Gemini Integration", + topics: "Gemini Pro/Flash, auth, generation config (temperature, top_p, top_k), text generation, streaming, chat sessions, structured output (JSON mode + Pydantic schema), multimodal (images, PDF, video, audio), function calling", + }, + { + module: 18, + phase: "AI Engineering", + title: "Professional Prompt Engineering", + topics: "Zero/one/few-shot, Chain of Thought, Self-Consistency, Tree of Thoughts, ReAct, XML tags & delimiters, schema-guided generation, role assignment, prompt templates (Jinja2), A/B testing, evaluation", + }, + { + module: 19, + phase: "AI Engineering", + title: "RAG Systems", + topics: "Document ingestion, chunking strategies, embeddings, pgvector (Postgres), ChromaDB, hybrid search (keyword + semantic), reranking, query expansion (HyDE), evaluation (Precision@k, Recall@k, MRR, faithfulness)", + }, + { + module: 20, + phase: "AI Engineering", + title: "AI Agents & Workflows", + topics: "Agent architecture (perception, reasoning, action, reflection), tool use & function calling, memory systems (short/long-term, episodic, semantic), LangChain, LangGraph, multi-agent systems, human-in-the-loop", + }, + { + module: 21, + phase: "AI Engineering", + title: "Full-Stack AI Integration", + topics: "Next.js AI patterns, Server Components for AI, streaming chat UI (SSE, token-by-token, markdown rendering), type-safe integration (OpenAPI client gen, Zod ↔ Pydantic), file upload (drag-and-drop, multimodal)", + }, // Phase 4: Production Mastery (Modules 22–25) - { module: 22, phase: "Production Mastery", title: "Testing AI Systems", topics: "pytest, fixtures, async testing, FastAPI TestClient, streaming endpoint tests, K6 load testing (virtual users, scenarios, thresholds), LLM output evaluation (factuality, relevance, automated pipelines)" }, - { module: 23, phase: "Production Mastery", title: "LLMOps & Observability", topics: "LangSmith (tracing, evaluation datasets, prompt versioning, cost tracking), structured logging, Prometheus metrics, AI-specific metrics (latency, tokens, errors), cost management, budget alerts" }, - { module: 24, phase: "Production Mastery", title: "Production Deployment", topics: "Docker (Dockerfile best practices, multi-stage builds), Google Cloud Run (config, secrets, scaling, Cloud SQL), GitHub Actions for Python, CI/CD automation, rollbacks, production checklist" }, - { module: 25, phase: "Production Mastery", title: "Ethics, Safety & Governance", topics: "Bias & fairness, prompt injection defense, data poisoning, model extraction, PII leakage, explainability, GDPR for AI, ISO 42001, model cards, content safety filters, incident response" }, + { + module: 22, + phase: "Production Mastery", + title: "Testing AI Systems", + topics: "pytest, fixtures, async testing, FastAPI TestClient, streaming endpoint tests, K6 load testing (virtual users, scenarios, thresholds), LLM output evaluation (factuality, relevance, automated pipelines)", + }, + { + module: 23, + phase: "Production Mastery", + title: "LLMOps & Observability", + topics: "LangSmith (tracing, evaluation datasets, prompt versioning, cost tracking), structured logging, Prometheus metrics, AI-specific metrics (latency, tokens, errors), cost management, budget alerts", + }, + { + module: 24, + phase: "Production Mastery", + title: "Production Deployment", + topics: "Docker (Dockerfile best practices, multi-stage builds), Google Cloud Run (config, secrets, scaling, Cloud SQL), GitHub Actions for Python, CI/CD automation, rollbacks, production checklist", + }, + { + module: 25, + phase: "Production Mastery", + title: "Ethics, Safety & Governance", + topics: "Bias & fairness, prompt injection defense, data poisoning, model extraction, PII leakage, explainability, GDPR for AI, ISO 42001, model cards, content safety filters, incident response", + }, ]; const JodiePage: PageWithLayout = () => { @@ -73,7 +197,9 @@ const JodiePage: PageWithLayout = () => { // Code pillar state const [codeInput, setCodeInput] = useState(""); const [codeLanguage, setCodeLanguage] = useState("javascript"); - const [codeAction, setCodeAction] = useState<"review" | "refactor" | "explain" | "generate" | "architecture">("review"); + const [codeAction, setCodeAction] = useState< + "review" | "refactor" | "explain" | "generate" | "architecture" + >("review"); const [codeResult, setCodeResult] = useState(null); const [isCodeLoading, setIsCodeLoading] = useState(false); @@ -108,12 +234,18 @@ const JodiePage: PageWithLayout = () => { if (!res.ok) { const errData = await res.json(); - const msg = errData.error || (Array.isArray(errData.detail) ? errData.detail.map((d: any) => d.msg).join(", ") : errData.detail) || "Failed to get response"; + const msg = + errData.error || + (Array.isArray(errData.detail) + ? errData.detail.map((d: any) => d.msg).join(", ") + : errData.detail) || + "Failed to get response"; throw new Error(msg); } const data = await res.json(); - const content = data.response || data.explanation || data.answer || JSON.stringify(data, null, 2); + const content = + data.response || data.explanation || data.answer || JSON.stringify(data, null, 2); setMessages((prev) => [...prev, { role: "assistant", content }]); } catch (err) { setError(err instanceof Error ? err.message : "Something went wrong"); @@ -152,7 +284,12 @@ const JodiePage: PageWithLayout = () => { const data = await res.json(); setCodeResult( - data.response || data.review || data.refactored_code || data.explanation || data.code || JSON.stringify(data, null, 2) + data.response || + data.review || + data.refactored_code || + data.explanation || + data.code || + JSON.stringify(data, null, 2) ); } catch (err) { setError(err instanceof Error ? err.message : "Something went wrong"); @@ -184,7 +321,9 @@ const JodiePage: PageWithLayout = () => { } const data = await res.json(); - setDebugResult(data.response || data.explanation || data.fix || JSON.stringify(data, null, 2)); + setDebugResult( + data.response || data.explanation || data.fix || JSON.stringify(data, null, 2) + ); } catch (err) { setError(err instanceof Error ? err.message : "Something went wrong"); } finally { @@ -193,9 +332,24 @@ const JodiePage: PageWithLayout = () => { }, [debugCode, debugError, debugLanguage]); const pillars = [ - { key: "learn" as const, label: "Learn", icon: "fa-graduation-cap", desc: "Ask about the Hashflag curriculum" }, - { key: "code" as const, label: "Code", icon: "fa-code", desc: "Review, refactor, explain, or generate code" }, - { key: "debug" as const, label: "Debug", icon: "fa-bug", desc: "Paste broken code and get a fix" }, + { + key: "learn" as const, + label: "Learn", + icon: "fa-graduation-cap", + desc: "Ask about the Hashflag curriculum", + }, + { + key: "code" as const, + label: "Code", + icon: "fa-code", + desc: "Review, refactor, explain, or generate code", + }, + { + key: "debug" as const, + label: "Debug", + icon: "fa-bug", + desc: "Paste broken code and get a fix", + }, ]; return ( @@ -213,9 +367,7 @@ const JodiePage: PageWithLayout = () => {
{/* Header */}
-

- J0d!e -

+

J0d!e

AI teaching assistant for the Hashflag Stack

@@ -242,7 +394,10 @@ const JodiePage: PageWithLayout = () => { {error && (
{error} -
@@ -275,25 +430,36 @@ const JodiePage: PageWithLayout = () => { Curriculum Module
- {["Foundations", "Software Engineering", "AI Engineering", "Production Mastery"].map((phase) => ( + {[ + "Foundations", + "Software Engineering", + "AI Engineering", + "Production Mastery", + ].map((phase) => (
{phase}
- {HASHFLAG_MODULES.filter((m) => m.phase === phase).map((m) => ( - - ))} + {HASHFLAG_MODULES.filter((m) => m.phase === phase).map( + (m) => ( + + ) + )}
))}
@@ -381,13 +547,18 @@ const JodiePage: PageWithLayout = () => {
-
-
-