From a7f0a6c6603e04cab7581ee595d7cce0f31930b0 Mon Sep 17 00:00:00 2001 From: WZWren <115881359+WZWren@users.noreply.github.com> Date: Sun, 20 Oct 2024 17:19:59 +0800 Subject: [PATCH 1/3] Add functionality to match button --- peerprep/api/gateway.ts | 28 ++++++++++++++++--- peerprep/api/structs.ts | 2 +- peerprep/app/actions/server_actions.ts | 21 ++++++++++++-- peerprep/app/actions/session.ts | 4 +++ .../app/api/internal/auth/expire/route.ts | 9 ++++++ peerprep/app/api/internal/questions/route.ts | 1 - peerprep/app/questions/page.tsx | 6 ++-- .../components/questionpage/Matchmaking.tsx | 2 -- peerprep/contexts/UserInfoContext.tsx | 15 +++++----- 9 files changed, 69 insertions(+), 19 deletions(-) create mode 100644 peerprep/app/api/internal/auth/expire/route.ts diff --git a/peerprep/api/gateway.ts b/peerprep/api/gateway.ts index b80565f63b..75c11ceb65 100644 --- a/peerprep/api/gateway.ts +++ b/peerprep/api/gateway.ts @@ -1,11 +1,11 @@ import { cookies } from "next/headers"; -import { LoginResponse, Question, SigninResponse, StatusBody } from "./structs"; +import { LoginResponse, Question, UserServiceResponse, StatusBody } from "./structs"; import DOMPurify from "isomorphic-dompurify"; export function generateAuthHeaders() { return { - Authorization: `Bearer ${cookies().get("session")}`, - }; + "Authorization": `Bearer ${cookies().get("session")?.value}`, + };; } export function generateJSONHeaders() { @@ -75,7 +75,7 @@ export async function postSignupUser(validatedFields: { username: string; email: string; password: string; -}): Promise { +}): Promise { try { console.log(JSON.stringify(validatedFields)); const res = await fetch(`${process.env.NEXT_PUBLIC_USER_SERVICE}/users`, { @@ -97,3 +97,23 @@ export async function postSignupUser(validatedFields: { return { error: err.message, status: 400 }; } } + +export async function verifyUser(): Promise { + try { + const res = await fetch( + `${process.env.NEXT_PUBLIC_USER_SERVICE}/auth/verify-token`, + { + method: "GET", + headers: generateAuthHeaders(), + } + ); + const json = await res.json(); + + if (!res.ok) { + return { error: json.message, status: res.status }; + } + return json; + } catch (err: any) { + return { error: err.message, status: 400 }; + } +} diff --git a/peerprep/api/structs.ts b/peerprep/api/structs.ts index 6fbf684c38..4e0a293272 100644 --- a/peerprep/api/structs.ts +++ b/peerprep/api/structs.ts @@ -43,7 +43,7 @@ export interface LoginResponse { data: UserDataAccessToken; } -export interface SigninResponse { +export interface UserServiceResponse { message: string; data: UserData; } diff --git a/peerprep/app/actions/server_actions.ts b/peerprep/app/actions/server_actions.ts index 074b3f7651..3ae7299a06 100644 --- a/peerprep/app/actions/server_actions.ts +++ b/peerprep/app/actions/server_actions.ts @@ -1,14 +1,16 @@ "use server"; -import { getSessionLogin, postSignupUser } from "@/api/gateway"; +import { getSessionLogin, postSignupUser, verifyUser } from "@/api/gateway"; // defines the server-sided login action. import { SignupFormSchema, LoginFormSchema, FormState, isError, + UserServiceResponse, } from "@/api/structs"; -import { createSession } from "@/app/actions/session"; +import { createSession, expireSession } from "@/app/actions/session"; import { redirect } from "next/navigation"; +import { cookies } from "next/headers"; // credit - taken from Next.JS Auth tutorial export async function signup(state: FormState, formData: FormData) { @@ -59,3 +61,18 @@ export async function login(state: FormState, formData: FormData) { console.log(json.error); } } + +export async function hydrateUid(): Promise { + if (!cookies().has("session")) { + console.log("No session found - triggering switch back to login page."); + redirect("/auth/login"); + } + const json = await verifyUser(); + if (isError(json)) { + console.log("Failed to fetch user ID."); + console.log(`Error ${json.status}: ${json.error}`); + redirect("/api/internal/auth/expire"); + } + + return json.data.id; +} diff --git a/peerprep/app/actions/session.ts b/peerprep/app/actions/session.ts index c4d592552a..8509b8f7d7 100644 --- a/peerprep/app/actions/session.ts +++ b/peerprep/app/actions/session.ts @@ -11,3 +11,7 @@ export async function createSession(accessToken: string) { path: "/", }); } + +export async function expireSession() { + cookies().delete("session"); +} diff --git a/peerprep/app/api/internal/auth/expire/route.ts b/peerprep/app/api/internal/auth/expire/route.ts new file mode 100644 index 0000000000..a86c19c9a8 --- /dev/null +++ b/peerprep/app/api/internal/auth/expire/route.ts @@ -0,0 +1,9 @@ +import { expireSession } from "@/app/actions/session"; +import { NextRequest, NextResponse } from "next/server"; + +export async function GET(request: NextRequest) { + await expireSession(); + const url = request.nextUrl.clone(); + url.pathname = "/auth/login"; + return NextResponse.redirect(url); +} diff --git a/peerprep/app/api/internal/questions/route.ts b/peerprep/app/api/internal/questions/route.ts index d8f85bd78a..d7fab6c75e 100644 --- a/peerprep/app/api/internal/questions/route.ts +++ b/peerprep/app/api/internal/questions/route.ts @@ -1,5 +1,4 @@ import { generateAuthHeaders, generateJSONHeaders } from "@/api/gateway"; -import { QuestionFullBody } from "@/api/structs"; import { NextRequest, NextResponse } from "next/server"; export async function GET() { diff --git a/peerprep/app/questions/page.tsx b/peerprep/app/questions/page.tsx index 04bdabeaf2..910eb9e2bd 100644 --- a/peerprep/app/questions/page.tsx +++ b/peerprep/app/questions/page.tsx @@ -3,10 +3,12 @@ import QuestionList from "@/components/questionpage/QuestionList"; import Matchmaking from "@/components/questionpage/Matchmaking"; import { QuestionFilterProvider } from "@/contexts/QuestionFilterContext"; import { UserInfoProvider } from "@/contexts/UserInfoContext"; +import { hydrateUid } from "../actions/server_actions"; -const QuestionsPage = () => { +async function QuestionsPage() { + const userId = await hydrateUid(); return ( - + diff --git a/peerprep/components/questionpage/Matchmaking.tsx b/peerprep/components/questionpage/Matchmaking.tsx index 3d82deca83..c62467d80e 100644 --- a/peerprep/components/questionpage/Matchmaking.tsx +++ b/peerprep/components/questionpage/Matchmaking.tsx @@ -9,8 +9,6 @@ import { checkMatchStatus, findMatch, } from "@/app/api/internal/matching/helper"; -import { match } from "assert"; -import { TIMEOUT } from "dns"; const QUERY_INTERVAL_MILLISECONDS = 5000; const TIMEOUT_MILLISECONDS = 30000; diff --git a/peerprep/contexts/UserInfoContext.tsx b/peerprep/contexts/UserInfoContext.tsx index deeb51889c..8793e8a5c9 100644 --- a/peerprep/contexts/UserInfoContext.tsx +++ b/peerprep/contexts/UserInfoContext.tsx @@ -4,22 +4,23 @@ "use client"; import React, { createContext, useContext, useState, ReactNode } from "react"; +interface UserInfoProviderProps { + userid: string|undefined, + children: ReactNode +} + interface UserInfoContextType { userid: string; - setUserid: (userid: string) => void; } const UserInfoContext = createContext( undefined ); -export const UserInfoProvider: React.FC<{ children: ReactNode }> = ({ - children, -}) => { - const [userid, setUserid] = useState("test-user"); - +export function UserInfoProvider({ userid, children }: UserInfoProviderProps) { + const val = userid ? userid : ""; return ( - + {children} ); From 422e509e9ceb87a0edb29376ad922e5e548a009d Mon Sep 17 00:00:00 2001 From: alvinlim277 Date: Sun, 20 Oct 2024 20:40:26 +0800 Subject: [PATCH 2/3] Add stopwatch for matchmaking and default difficulty to easy --- peerprep/app/actions/server_actions.ts | 2 +- .../components/questionpage/Matchmaking.tsx | 31 ++++++++++----- .../components/shared/ResettingStopwatch.tsx | 38 +++++++++++++++++++ peerprep/contexts/QuestionFilterContext.tsx | 1 + peerprep/middleware.ts | 2 +- 5 files changed, 62 insertions(+), 12 deletions(-) create mode 100644 peerprep/components/shared/ResettingStopwatch.tsx diff --git a/peerprep/app/actions/server_actions.ts b/peerprep/app/actions/server_actions.ts index 3ae7299a06..ea761b5ce3 100644 --- a/peerprep/app/actions/server_actions.ts +++ b/peerprep/app/actions/server_actions.ts @@ -62,7 +62,7 @@ export async function login(state: FormState, formData: FormData) { } } -export async function hydrateUid(): Promise { +export async function hydrateUid(): Promise { if (!cookies().has("session")) { console.log("No session found - triggering switch back to login page."); redirect("/auth/login"); diff --git a/peerprep/components/questionpage/Matchmaking.tsx b/peerprep/components/questionpage/Matchmaking.tsx index c62467d80e..8c85bfe0f8 100644 --- a/peerprep/components/questionpage/Matchmaking.tsx +++ b/peerprep/components/questionpage/Matchmaking.tsx @@ -4,11 +4,17 @@ import { useRouter } from "next/navigation"; import PeerprepButton from "../shared/PeerprepButton"; import { useQuestionFilter } from "@/contexts/QuestionFilterContext"; import { useUserInfo } from "@/contexts/UserInfoContext"; -import { isError, MatchRequest, MatchResponse } from "@/api/structs"; +import { + Difficulty, + isError, + MatchRequest, + MatchResponse, +} from "@/api/structs"; import { checkMatchStatus, findMatch, } from "@/app/api/internal/matching/helper"; +import ResettingStopwatch from "../shared/ResettingStopwatch"; const QUERY_INTERVAL_MILLISECONDS = 5000; const TIMEOUT_MILLISECONDS = 30000; @@ -60,6 +66,18 @@ const Matchmaking = () => { } }; + const getMatchMakingRequest = (): MatchRequest => { + const matchRequest: MatchRequest = { + userId: userid, + // welp, just bandaid default easy + difficulty: difficulty === Difficulty.All ? Difficulty.Easy : difficulty, + topicTags: topics, + requestTime: getMatchRequestTime(), + }; + + return matchRequest; + }; + const handleMatch = async () => { if (!isMatching) { setIsMatching(true); @@ -71,12 +89,7 @@ const Matchmaking = () => { }, TIMEOUT_MILLISECONDS); // assemble the match request - const matchRequest: MatchRequest = { - userId: userid, - difficulty: difficulty, - topicTags: topics, - requestTime: getMatchRequestTime(), - }; + const matchRequest = getMatchMakingRequest(); console.log("Match attempted"); console.debug(matchRequest); @@ -126,9 +139,7 @@ const Matchmaking = () => { {isMatching ? "Cancel Match" : "Find Match"} - {isMatching && ( -
- )} + {isMatching && }
); diff --git a/peerprep/components/shared/ResettingStopwatch.tsx b/peerprep/components/shared/ResettingStopwatch.tsx new file mode 100644 index 0000000000..d074936ab6 --- /dev/null +++ b/peerprep/components/shared/ResettingStopwatch.tsx @@ -0,0 +1,38 @@ +import React, { useState, useEffect } from "react"; + +interface ResettingStopwatchProps { + isActive: boolean; +} + +// pass isActive from parent component +// +const ResettingStopwatch: React.FC = ({ + isActive, +}) => { + const [elapsedTime, setElapsedTime] = useState(0); + + useEffect(() => { + let interval: NodeJS.Timeout | null = null; + + if (isActive) { + interval = setInterval(() => { + setElapsedTime((prevTime) => prevTime + 1); + }, 1000); + } + + return () => { + if (interval) clearInterval(interval); + setElapsedTime(0); + }; + }, [isActive]); + + const formatTime = (time: number) => { + const minutes = Math.floor(time / 60); + const seconds = time % 60; + return `${minutes}:${seconds < 10 ? "0" : ""}${seconds}`; + }; + + return
{formatTime(elapsedTime)}
; +}; + +export default ResettingStopwatch; diff --git a/peerprep/contexts/QuestionFilterContext.tsx b/peerprep/contexts/QuestionFilterContext.tsx index 694f0282b1..920999a2cb 100644 --- a/peerprep/contexts/QuestionFilterContext.tsx +++ b/peerprep/contexts/QuestionFilterContext.tsx @@ -18,6 +18,7 @@ export const QuestionFilterProvider: React.FC<{ children: ReactNode }> = ({ }) => { const [difficulty, setDifficulty] = useState(Difficulty.All); // default to all const [topics, setTopics] = useState(["all"]); // I guess default set this too the whole list of topics from questionlist + //can also consider moving all the uniqu topics here? return ( Date: Mon, 21 Oct 2024 00:23:02 +0800 Subject: [PATCH 3/3] Revert filter hooks, add matching button dropdown --- .../components/questionpage/Matchmaking.tsx | 23 ++++++++++--- .../components/questionpage/QuestionList.tsx | 33 +++++++++---------- peerprep/contexts/QuestionFilterContext.tsx | 18 +++++----- 3 files changed, 44 insertions(+), 30 deletions(-) diff --git a/peerprep/components/questionpage/Matchmaking.tsx b/peerprep/components/questionpage/Matchmaking.tsx index 8c85bfe0f8..92691071af 100644 --- a/peerprep/components/questionpage/Matchmaking.tsx +++ b/peerprep/components/questionpage/Matchmaking.tsx @@ -15,6 +15,7 @@ import { findMatch, } from "@/app/api/internal/matching/helper"; import ResettingStopwatch from "../shared/ResettingStopwatch"; +import PeerprepDropdown from "../shared/PeerprepDropdown"; const QUERY_INTERVAL_MILLISECONDS = 5000; const TIMEOUT_MILLISECONDS = 30000; @@ -54,7 +55,9 @@ const usePeriodicCallback = ( const Matchmaking = () => { const router = useRouter(); const [isMatching, setIsMatching] = useState(false); - const { difficulty, topics } = useQuestionFilter(); + const [difficultyFilter, setDifficultyFilter] = useState(Difficulty.Easy); + const [topicFilter, setTopicFilter] = useState(["all"]); + const { difficulties, topicList } = useQuestionFilter(); const { userid } = useUserInfo(); const timeout = useRef(); @@ -69,9 +72,8 @@ const Matchmaking = () => { const getMatchMakingRequest = (): MatchRequest => { const matchRequest: MatchRequest = { userId: userid, - // welp, just bandaid default easy - difficulty: difficulty === Difficulty.All ? Difficulty.Easy : difficulty, - topicTags: topics, + difficulty: difficultyFilter, + topicTags: topicFilter, requestTime: getMatchRequestTime(), }; @@ -139,6 +141,19 @@ const Matchmaking = () => { {isMatching ? "Cancel Match" : "Find Match"} + {!isMatching && + setDifficultyFilter(e.target.value)} + // truthfully we don't need this difficulties list, but we are temporarily including it + options={difficulties} /> + } + {!isMatching && + setTopicFilter(e.target.value === "all" ? topicList : [e.target.value])} + options={topicList} /> + } {isMatching && } diff --git a/peerprep/components/questionpage/QuestionList.tsx b/peerprep/components/questionpage/QuestionList.tsx index 3d28d0b254..99d4af0c10 100644 --- a/peerprep/components/questionpage/QuestionList.tsx +++ b/peerprep/components/questionpage/QuestionList.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useEffect, useState } from "react"; import QuestionCard from "./QuestionCard"; -import { Question, StatusBody, Difficulty, isError } from "@/api/structs"; +import { Question, Difficulty, isError } from "@/api/structs"; import PeerprepDropdown from "../shared/PeerprepDropdown"; import PeerprepSearchBar from "../shared/PeerprepSearchBar"; import { useQuestionFilter } from "@/contexts/QuestionFilterContext"; @@ -10,9 +10,12 @@ const QuestionList: React.FC = () => { const [questions, setQuestions] = useState([]); const [loading, setLoading] = useState(true); const [searchFilter, setSearchFilter] = useState(""); - const [topicsList, setTopicsList] = useState(["all"]); + const [difficultyFilter, setDifficultyFilter] = useState( + Difficulty.All + ); + const [topicFilter, setTopicFilter] = useState("all"); - const { difficulty, setDifficulty, topics, setTopics } = useQuestionFilter(); + const { topicList, setTopicList } = useQuestionFilter(); useEffect(() => { const fetchQuestions = async () => { @@ -33,8 +36,7 @@ const QuestionList: React.FC = () => { const uniqueTopics = Array.from( new Set(data.flatMap((question) => question.topicTags)) ).sort(); - setTopicsList(["all", ...uniqueTopics]); - setTopics(["all", ...uniqueTopics]); + setTopicList(["all", ...uniqueTopics]); }; fetchQuestions(); @@ -42,10 +44,10 @@ const QuestionList: React.FC = () => { const filteredQuestions = questions.filter((question) => { const matchesDifficulty = - difficulty === Difficulty.All || - Difficulty[question.difficulty] === difficulty; + difficultyFilter === Difficulty.All || + Difficulty[question.difficulty] === difficultyFilter; const matchesTopic = - topics[0] === "all" || (question.topicTags ?? []).includes(topics[0]); + topicFilter === "all" || (question.topicTags ?? []).includes(topicFilter); const matchesSearch = searchFilter === "" || (question.title ?? "").toLowerCase().includes(searchFilter.toLowerCase()); @@ -57,16 +59,12 @@ const QuestionList: React.FC = () => { const handleSetDifficulty = (e: React.ChangeEvent) => { const diff = e.target.value; - setDifficulty(diff); + setDifficultyFilter(diff); }; const handleSetTopics = (e: React.ChangeEvent) => { const topic = e.target.value; - if (topic === "all") { - setTopics(topicsList); - } else { - setTopics([topic]); - } + setTopicFilter(topic); }; return ( @@ -79,16 +77,15 @@ const QuestionList: React.FC = () => { /> isNaN(Number(key)))} /> diff --git a/peerprep/contexts/QuestionFilterContext.tsx b/peerprep/contexts/QuestionFilterContext.tsx index 920999a2cb..d89778d6e8 100644 --- a/peerprep/contexts/QuestionFilterContext.tsx +++ b/peerprep/contexts/QuestionFilterContext.tsx @@ -3,10 +3,9 @@ import { Difficulty } from "@/api/structs"; import React, { createContext, useContext, useState, ReactNode } from "react"; interface QuestionFilterContextType { - difficulty: string; - setDifficulty: (difficulty: string) => void; - topics: string[]; - setTopics: (topics: string[]) => void; + difficulties: string[]; + topicList: string[]; + setTopicList: (topics: string[]) => void; } const QuestionFilterContext = createContext< @@ -16,13 +15,16 @@ const QuestionFilterContext = createContext< export const QuestionFilterProvider: React.FC<{ children: ReactNode }> = ({ children, }) => { - const [difficulty, setDifficulty] = useState(Difficulty.All); // default to all - const [topics, setTopics] = useState(["all"]); // I guess default set this too the whole list of topics from questionlist - //can also consider moving all the uniqu topics here? + const difficulties = [Difficulty.Easy, Difficulty.Medium, Difficulty.Hard]; + const [topicList, setTopicList] = useState([]); + // I guess default set this too the whole list of topics from questionlist + // can also consider moving all the uniqu topics here? -- yes, we are doing that now + // TODO: since QuestionFilterProvider now exists to wrap the QuestionList, + // we can move the question fetching 1 layer higher, theoretically, so look into this return ( {children}