From f51408c62e0d186755c61b9e2a6287b7cfcf8591 Mon Sep 17 00:00:00 2001 From: NickK21 Date: Fri, 27 Mar 2026 06:21:19 -0700 Subject: [PATCH] Add login prompt for protected course access --- .../components/auth/LoginRequiredPrompt.tsx | 33 +++++++++++++++++++ .../src/components/guards/DashboardGate.tsx | 11 ++++--- codewit/client/src/hooks/fetching.ts | 29 +++++++++++----- codewit/client/src/hooks/useCourseRole.ts | 11 +++++-- codewit/client/src/pages/CourseView.tsx | 31 +++++++++-------- 5 files changed, 85 insertions(+), 30 deletions(-) create mode 100644 codewit/client/src/components/auth/LoginRequiredPrompt.tsx diff --git a/codewit/client/src/components/auth/LoginRequiredPrompt.tsx b/codewit/client/src/components/auth/LoginRequiredPrompt.tsx new file mode 100644 index 0000000..5112dde --- /dev/null +++ b/codewit/client/src/components/auth/LoginRequiredPrompt.tsx @@ -0,0 +1,33 @@ +import { Link } from "react-router-dom"; + +import { CenterPrompt } from "../placeholders"; + +interface LoginRequiredPromptProps { + message?: string, +} + +export default function LoginRequiredPrompt({ + message = "To visit this page, you need to be logged in.", +}: LoginRequiredPromptProps) { + return ( + +

+ {message} +

+
+ + Return Home + + + Log In + +
+
+ ); +} diff --git a/codewit/client/src/components/guards/DashboardGate.tsx b/codewit/client/src/components/guards/DashboardGate.tsx index a065237..d869385 100644 --- a/codewit/client/src/components/guards/DashboardGate.tsx +++ b/codewit/client/src/components/guards/DashboardGate.tsx @@ -2,15 +2,18 @@ import { Navigate, useParams } from 'react-router-dom'; import LoadingPage from '../loading/LoadingPage'; import { useAuth } from '../../hooks/useAuth'; import { useCourseRole } from '../../hooks/useCourseRole'; +import LoginRequiredPrompt from '../auth/LoginRequiredPrompt'; export default function DashboardGate({ children }: { children: JSX.Element }) { const { user, loading: authLoading } = useAuth(); const { courseId } = useParams(); - const { role, loading: roleLoading } = useCourseRole(courseId); + const { role, loading: roleLoading } = useCourseRole(user ? courseId : undefined); - if (authLoading || roleLoading) return ; + if (authLoading) return ; + if (!user) return ; + if (roleLoading) return ; - const allowed = user && (user.isAdmin || role === 'instructor'); + const allowed = user.isAdmin || role === 'instructor'; return allowed ? children : ( ); -} \ No newline at end of file +} diff --git a/codewit/client/src/hooks/fetching.ts b/codewit/client/src/hooks/fetching.ts index 6ea617c..02c917c 100644 --- a/codewit/client/src/hooks/fetching.ts +++ b/codewit/client/src/hooks/fetching.ts @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import axios, { AxiosError } from 'axios'; +import axios from 'axios'; interface AxiosFetch { data: T, @@ -14,13 +14,13 @@ interface AxiosFetch { // this will assume that the first mount is the initial loading of the data // and will indicate that it is loading before sending the actual request to // the server. -export function useAxiosFetch(initialUrl: string, initialData: T): AxiosFetch { +export function useAxiosFetch(initialUrl: string | null, initialData: T): AxiosFetch { const [data, setData] = useState(initialData); const [active_requests, setActiveRequests] = useState(0); const [error, setError] = useState(false); - const [initial_loading, setInitialLoading] = useState(true); + const [initial_loading, setInitialLoading] = useState(initialUrl != null); - const fetchData = async (controller: AbortController) => { + const fetchData = async (url: string, controller: AbortController) => { setActiveRequests(v => (v + 1)); let canceled = false; @@ -28,7 +28,7 @@ export function useAxiosFetch(initialUrl: string, initialData: T): AxiosFetch setError(false); try { - const response = await axios.get(initialUrl, { + const response = await axios.get(url, { signal: controller.signal }); @@ -49,14 +49,25 @@ export function useAxiosFetch(initialUrl: string, initialData: T): AxiosFetch }; const refetch = () => { + if (initialUrl == null) { + return; + } + const controller = new AbortController(); - fetchData(controller); + fetchData(initialUrl, controller); }; useEffect(() => { - let controller = new AbortController(); + if (initialUrl == null) { + setError(false); + setInitialLoading(false); + return; + } - fetchData(controller); + const controller = new AbortController(); + setInitialLoading(true); + + fetchData(initialUrl, controller); return () => { controller.abort(); @@ -75,4 +86,4 @@ export function useAxiosFetch(initialUrl: string, initialData: T): AxiosFetch }, [initialUrl]); return { data, setData, loading: initial_loading || active_requests > 0, error, refetch }; -}; +} diff --git a/codewit/client/src/hooks/useCourseRole.ts b/codewit/client/src/hooks/useCourseRole.ts index 3e8ec73..4ae2add 100644 --- a/codewit/client/src/hooks/useCourseRole.ts +++ b/codewit/client/src/hooks/useCourseRole.ts @@ -5,13 +5,18 @@ export type CourseRole = 'instructor' | 'student' | null; export function useCourseRole(courseId: string | number | undefined) { const [role, setRole] = useState(null); - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(Boolean(courseId)); const [error, setError] = useState(null); useEffect(() => { - if (!courseId) return; + if (!courseId) { + setRole(null); + setLoading(false); + return; + } setLoading(true); + setError(null); axios .get<{ role: CourseRole }>(`/api/courses/${courseId}/role`) .then(res => setRole(res.data.role)) @@ -23,4 +28,4 @@ export function useCourseRole(courseId: string | number | undefined) { }, [courseId]); return { role, loading, error }; -} \ No newline at end of file +} diff --git a/codewit/client/src/pages/CourseView.tsx b/codewit/client/src/pages/CourseView.tsx index e1c16f3..6e68746 100644 --- a/codewit/client/src/pages/CourseView.tsx +++ b/codewit/client/src/pages/CourseView.tsx @@ -8,9 +8,11 @@ import { StudentCourse as StuCourse} from '@codewit/interfaces'; import { ErrorPage } from "../components/error/Error"; import Loading from "../components/loading/LoadingPage"; import { useAxiosFetch } from "../hooks/fetching"; +import { useAuth } from "../hooks/useAuth"; import StudentView from "./course/StudentView"; import { CenterPrompt } from "../components/placeholders"; +import LoginRequiredPrompt from "../components/auth/LoginRequiredPrompt"; interface StudentCourse extends StuCourse { type: "StudentView", @@ -59,13 +61,17 @@ interface CourseView { export default function CourseView({onCourseChange}: CourseView) { const { course_id } = useParams(); const navigate = useNavigate(); + const { user, loading: authLoading } = useAuth(); if (course_id == null) { throw new Error("course_id not provided"); } const [refresh, set_refresh] = useState(0); - const { data: course, loading, error, setData } = useAxiosFetch(`/api/courses/${course_id}?student_view=1&r=${refresh}`, null); + const { data: course, loading, error, setData } = useAxiosFetch( + user ? `/api/courses/${course_id}?student_view=1&r=${refresh}` : null, + null, + ); useEffect(() => { if (course?.type === "StudentView") { @@ -73,10 +79,14 @@ export default function CourseView({onCourseChange}: CourseView) { } }, [course, onCourseChange]); - if (loading) { + if (authLoading || loading) { return ; } + if (!user) { + return ; + } + if (error || course == null) { return ; } @@ -127,14 +137,7 @@ export default function CourseView({onCourseChange}: CourseView) { />; } default: - return -

- The client does not know how to handle the response from the server. Sorry, try going back to the home page. -

- - Home page - -
; + return ; } } @@ -151,9 +154,9 @@ interface RegError { } function EnrollingView({course_id, course_title, auto_enroll, on_update}: EnrollingViewProps) { - let [sending, set_sending] = useState(false); - let [reg_state, set_reg_state] = useState(null); - let [error, set_error] = useState(null); + const [sending, set_sending] = useState(false); + const [reg_state, set_reg_state] = useState(null); + const [error, set_error] = useState(null); async function request_enrollment() { if (sending) { @@ -163,7 +166,7 @@ function EnrollingView({course_id, course_title, auto_enroll, on_update}: Enroll set_sending(true); try { - let result = await axios.post(`/api/courses/${course_id}/register`); + const result = await axios.post(`/api/courses/${course_id}/register`); set_reg_state(result.data); } catch (err) {