Skip to content

Commit 5c9ee21

Browse files
committed
Add login prompt for protected course access
1 parent f1c383e commit 5c9ee21

5 files changed

Lines changed: 85 additions & 30 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Link } from "react-router-dom";
2+
3+
import { CenterPrompt } from "../placeholders";
4+
5+
interface LoginRequiredPromptProps {
6+
message?: string,
7+
}
8+
9+
export default function LoginRequiredPrompt({
10+
message = "To visit this page, you need to be logged in.",
11+
}: LoginRequiredPromptProps) {
12+
return (
13+
<CenterPrompt header="Log in to proceed">
14+
<p className="mt-2 text-center text-white">
15+
{message}
16+
</p>
17+
<div className="mt-6 flex w-full flex-col items-center justify-center gap-3 sm:flex-row">
18+
<Link
19+
to="/"
20+
className="w-full rounded-md bg-zinc-700 px-4 py-2 text-center text-white transition hover:bg-zinc-600 sm:w-auto"
21+
>
22+
Return Home
23+
</Link>
24+
<a
25+
href="/api/oauth2/google"
26+
className="w-full rounded-md bg-accent-500 px-4 py-2 text-center text-white transition hover:bg-accent-600 sm:w-auto"
27+
>
28+
Log In
29+
</a>
30+
</div>
31+
</CenterPrompt>
32+
);
33+
}

codewit/client/src/components/guards/DashboardGate.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@ import { Navigate, useParams } from 'react-router-dom';
22
import LoadingPage from '../loading/LoadingPage';
33
import { useAuth } from '../../hooks/useAuth';
44
import { useCourseRole } from '../../hooks/useCourseRole';
5+
import LoginRequiredPrompt from '../auth/LoginRequiredPrompt';
56

67
export default function DashboardGate({ children }: { children: JSX.Element }) {
78
const { user, loading: authLoading } = useAuth();
89
const { courseId } = useParams();
9-
const { role, loading: roleLoading } = useCourseRole(courseId);
10+
const { role, loading: roleLoading } = useCourseRole(user ? courseId : undefined);
1011

11-
if (authLoading || roleLoading) return <LoadingPage />;
12+
if (authLoading) return <LoadingPage />;
13+
if (!user) return <LoginRequiredPrompt />;
14+
if (roleLoading) return <LoadingPage />;
1215

13-
const allowed = user && (user.isAdmin || role === 'instructor');
16+
const allowed = user.isAdmin || role === 'instructor';
1417

1518
return allowed ? children : (
1619
<Navigate
@@ -21,4 +24,4 @@ export default function DashboardGate({ children }: { children: JSX.Element }) {
2124
}}
2225
/>
2326
);
24-
}
27+
}

codewit/client/src/hooks/fetching.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useState, useEffect } from 'react';
2-
import axios, { AxiosError } from 'axios';
2+
import axios from 'axios';
33

44
interface AxiosFetch<T> {
55
data: T,
@@ -14,21 +14,21 @@ interface AxiosFetch<T> {
1414
// this will assume that the first mount is the initial loading of the data
1515
// and will indicate that it is loading before sending the actual request to
1616
// the server.
17-
export function useAxiosFetch<T>(initialUrl: string, initialData: T): AxiosFetch<T> {
17+
export function useAxiosFetch<T>(initialUrl: string | null, initialData: T): AxiosFetch<T> {
1818
const [data, setData] = useState(initialData);
1919
const [active_requests, setActiveRequests] = useState(0);
2020
const [error, setError] = useState(false);
21-
const [initial_loading, setInitialLoading] = useState(true);
21+
const [initial_loading, setInitialLoading] = useState(initialUrl != null);
2222

23-
const fetchData = async (controller: AbortController) => {
23+
const fetchData = async (url: string, controller: AbortController) => {
2424
setActiveRequests(v => (v + 1));
2525

2626
let canceled = false;
2727

2828
setError(false);
2929

3030
try {
31-
const response = await axios.get(initialUrl, {
31+
const response = await axios.get(url, {
3232
signal: controller.signal
3333
});
3434

@@ -49,14 +49,25 @@ export function useAxiosFetch<T>(initialUrl: string, initialData: T): AxiosFetch
4949
};
5050

5151
const refetch = () => {
52+
if (initialUrl == null) {
53+
return;
54+
}
55+
5256
const controller = new AbortController();
53-
fetchData(controller);
57+
fetchData(initialUrl, controller);
5458
};
5559

5660
useEffect(() => {
57-
let controller = new AbortController();
61+
if (initialUrl == null) {
62+
setError(false);
63+
setInitialLoading(false);
64+
return;
65+
}
5866

59-
fetchData(controller);
67+
const controller = new AbortController();
68+
setInitialLoading(true);
69+
70+
fetchData(initialUrl, controller);
6071

6172
return () => {
6273
controller.abort();
@@ -75,4 +86,4 @@ export function useAxiosFetch<T>(initialUrl: string, initialData: T): AxiosFetch
7586
}, [initialUrl]);
7687

7788
return { data, setData, loading: initial_loading || active_requests > 0, error, refetch };
78-
};
89+
}

codewit/client/src/hooks/useCourseRole.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,18 @@ export type CourseRole = 'instructor' | 'student' | null;
55

66
export function useCourseRole(courseId: string | number | undefined) {
77
const [role, setRole] = useState<CourseRole>(null);
8-
const [loading, setLoading] = useState(false);
8+
const [loading, setLoading] = useState(Boolean(courseId));
99
const [error, setError] = useState<unknown>(null);
1010

1111
useEffect(() => {
12-
if (!courseId) return;
12+
if (!courseId) {
13+
setRole(null);
14+
setLoading(false);
15+
return;
16+
}
1317

1418
setLoading(true);
19+
setError(null);
1520
axios
1621
.get<{ role: CourseRole }>(`/api/courses/${courseId}/role`)
1722
.then(res => setRole(res.data.role))
@@ -23,4 +28,4 @@ export function useCourseRole(courseId: string | number | undefined) {
2328
}, [courseId]);
2429

2530
return { role, loading, error };
26-
}
31+
}

codewit/client/src/pages/CourseView.tsx

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import { StudentCourse as StuCourse} from '@codewit/interfaces';
88
import { ErrorPage } from "../components/error/Error";
99
import Loading from "../components/loading/LoadingPage";
1010
import { useAxiosFetch } from "../hooks/fetching";
11+
import { useAuth } from "../hooks/useAuth";
1112

1213
import StudentView from "./course/StudentView";
1314
import { CenterPrompt } from "../components/placeholders";
15+
import LoginRequiredPrompt from "../components/auth/LoginRequiredPrompt";
1416

1517
interface StudentCourse extends StuCourse {
1618
type: "StudentView",
@@ -59,24 +61,32 @@ interface CourseView {
5961
export default function CourseView({onCourseChange}: CourseView) {
6062
const { course_id } = useParams();
6163
const navigate = useNavigate();
64+
const { user, loading: authLoading } = useAuth();
6265

6366
if (course_id == null) {
6467
throw new Error("course_id not provided");
6568
}
6669

6770
const [refresh, set_refresh] = useState(0);
68-
const { data: course, loading, error, setData } = useAxiosFetch<GetCourse | null>(`/api/courses/${course_id}?student_view=1&r=${refresh}`, null);
71+
const { data: course, loading, error, setData } = useAxiosFetch<GetCourse | null>(
72+
user ? `/api/courses/${course_id}?student_view=1&r=${refresh}` : null,
73+
null,
74+
);
6975

7076
useEffect(() => {
7177
if (course?.type === "StudentView") {
7278
onCourseChange(course.title);
7379
}
7480
}, [course, onCourseChange]);
7581

76-
if (loading) {
82+
if (authLoading || loading) {
7783
return <Loading />;
7884
}
7985

86+
if (!user) {
87+
return <LoginRequiredPrompt />;
88+
}
89+
8090
if (error || course == null) {
8191
return <ErrorPage message="Failed to fetch courses. Please try again later."/>;
8292
}
@@ -127,14 +137,7 @@ export default function CourseView({onCourseChange}: CourseView) {
127137
/>;
128138
}
129139
default:
130-
return <CenterPrompt header={"Unknown Response"}>
131-
<p className="text-center text-white">
132-
The client does not know how to handle the response from the server. Sorry, try going back to the home page.
133-
</p>
134-
<Link to="/" className="text-white bg-accent-500 rounded-md mt-4 p-2">
135-
Home page
136-
</Link>
137-
</CenterPrompt>;
140+
return <ErrorPage message="Failed to load course information. Please try again later." />;
138141
}
139142
}
140143

@@ -151,9 +154,9 @@ interface RegError {
151154
}
152155

153156
function EnrollingView({course_id, course_title, auto_enroll, on_update}: EnrollingViewProps) {
154-
let [sending, set_sending] = useState(false);
155-
let [reg_state, set_reg_state] = useState<RegistrationResult | null>(null);
156-
let [error, set_error] = useState<RegError | null>(null);
157+
const [sending, set_sending] = useState(false);
158+
const [reg_state, set_reg_state] = useState<RegistrationResult | null>(null);
159+
const [error, set_error] = useState<RegError | null>(null);
157160

158161
async function request_enrollment() {
159162
if (sending) {
@@ -163,7 +166,7 @@ function EnrollingView({course_id, course_title, auto_enroll, on_update}: Enroll
163166
set_sending(true);
164167

165168
try {
166-
let result = await axios.post<RegistrationResult>(`/api/courses/${course_id}/register`);
169+
const result = await axios.post<RegistrationResult>(`/api/courses/${course_id}/register`);
167170

168171
set_reg_state(result.data);
169172
} catch (err) {

0 commit comments

Comments
 (0)