From e3673951c6f53e7387b1e44c8c7e76236e7e68f2 Mon Sep 17 00:00:00 2001 From: shafin-r Date: Sun, 17 Aug 2025 19:59:14 +0600 Subject: [PATCH] fix(api): fix api endpoint logic #3 --- app/(auth)/register/page.tsx | 64 +++++++------- app/(tabs)/profile/page.tsx | 26 +++++- app/(tabs)/settings/page.tsx | 63 +++----------- components/Header.tsx | 38 +------- components/ProfileManager.tsx | 157 +++++++++++++++++++++++++--------- context/AuthContext.tsx | 49 +++++++++-- types/auth.d.ts | 4 +- 7 files changed, 231 insertions(+), 170 deletions(-) diff --git a/app/(auth)/register/page.tsx b/app/(auth)/register/page.tsx index 93192d3..2fb2245 100644 --- a/app/(auth)/register/page.tsx +++ b/app/(auth)/register/page.tsx @@ -12,9 +12,17 @@ import FormField from "@/components/FormField"; import DestructibleAlert from "@/components/DestructibleAlert"; import { RegisterForm } from "@/types/auth"; +interface ValidationErrorItem { + type: string; + loc: string[]; + msg: string; + input?: unknown; + ctx?: Record; +} + interface CustomError extends Error { response?: { - detail?: string; + detail?: string | ValidationErrorItem; }; } @@ -36,16 +44,27 @@ export default function RegisterPage() { }); const [error, setError] = useState(null); - const handleError = (error: { detail: string }) => { - if (error?.detail) { - const match = error.detail.match(/Key \((.*?)\)=\((.*?)\)/); - if (match) { - const field = match[1]; - return `The ${field} already exists. Please try again.`; + function formatError(error: unknown): string { + if (error && typeof error === "object" && "response" in (error as any)) { + const customError = error as CustomError; + const detail = customError.response?.detail; + + if (typeof detail === "string") { + return detail; // plain backend error string + } + + if (Array.isArray(detail)) { + // Pick the first validation error, or join them if multiple + return detail.map((d) => d.msg).join("; "); } } - return "An unexpected error occurred. Please try again."; - }; + + if (error instanceof Error) { + return error.message; + } + + return "An unexpected error occurred."; + } const validateForm = () => { const { ssc_roll, hsc_roll, password } = form; @@ -69,31 +88,12 @@ export default function RegisterPage() { try { await register(form, setToken); - router.push("/home"); - } catch (error) { - // Type guard for built-in Error type - if (error instanceof Error) { - console.error( - "Error:", - (error as CustomError).response || error.message - ); - - const response = (error as CustomError).response; - - if (response?.detail) { - const decodedError = handleError({ detail: response.detail }); - setError(decodedError); - } else { - setError(error.message || "An unexpected error occurred."); - } - } else { - // Fallback for non-standard errors - console.error("Unexpected error:", error); - setError("An unexpected error occurred."); - } + router.push("/login"); + } catch (err: unknown) { + setError(formatError(err)); + console.error("User creation error: ", err); } }; - return (
diff --git a/app/(tabs)/profile/page.tsx b/app/(tabs)/profile/page.tsx index 8b33d3c..2fe876c 100644 --- a/app/(tabs)/profile/page.tsx +++ b/app/(tabs)/profile/page.tsx @@ -3,7 +3,14 @@ import ProfileManager from "@/components/ProfileManager"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { getToken, API_URL } from "@/lib/auth"; -import { ChevronLeft, Edit2, Lock, Save } from "lucide-react"; +import { + BadgeCheck, + ChevronLeft, + Edit2, + Lock, + Save, + ShieldX, +} from "lucide-react"; import { useRouter } from "next/navigation"; import React, { useEffect, useState } from "react"; import { UserData } from "@/types/auth"; @@ -85,7 +92,22 @@ const ProfilePage = () => { -
+
+ {userData?.is_verified ? ( +
+ +

+ This account is verified. +

+
+ ) : ( +
+ +

+ This account is not verified. +

+
+ )} { const router = useRouter(); - const [userData, setUserData] = useState({ - user_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6", - username: "", - full_name: "", - email: "", - is_verified: false, - phone_number: "", - ssc_roll: 0, - ssc_board: "", - hsc_roll: 0, - hsc_board: "", - college: "", - preparation_unit: "Science", - }); + const { user, isLoading } = useAuth(); - useEffect(() => { - async function fetchUser() { - try { - const token = await getToken(); - if (!token) return; - - const response = await fetch(`${API_URL}/me/`, { - method: "GET", - headers: { - Authorization: `Bearer ${token}`, - }, - }); - - if (response.ok) { - const fetchedUserData = await response.json(); - setUserData(fetchedUserData); - } - } catch (error) { - console.error("Error fetching user data: ", error); - } - } - - fetchUser(); - }, []); + function handleLogout() { + clearAuthToken(); + router.replace("/"); + } return ( @@ -81,16 +48,14 @@ const SettingsPage = () => {
- {userData?.username - ? userData.username.charAt(0).toUpperCase() + {user?.username + ? user.username.charAt(0).toUpperCase() : ""}
-

- {userData?.full_name} -

-

{userData?.email}

+

{user?.full_name}

+

{user?.email}

@@ -181,7 +146,7 @@ const SettingsPage = () => {
-
+
+

ExamJam | Version 1.0 diff --git a/components/Header.tsx b/components/Header.tsx index 9a2f379..e2a5523 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -6,8 +6,7 @@ import styles from "@/css/Header.module.css"; import { useExam } from "@/context/ExamContext"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { useModal } from "@/context/ModalContext"; -import { API_URL, getToken } from "@/lib/auth"; -import { UserData } from "@/types/auth"; +import { useAuth } from "@/context/AuthContext"; interface HeaderProps { displayUser?: boolean; @@ -29,7 +28,7 @@ const Header = ({ examDuration ? parseInt(examDuration) * 60 : 0 ); const { stopTimer } = useTimer(); - const [userData, setUserData] = useState(); + const { user, isLoading } = useAuth(); useEffect(() => { if (!examDuration) return; @@ -47,33 +46,6 @@ const Header = ({ return () => clearInterval(timer); }, [examDuration]); - useEffect(() => { - async function fetchUser() { - try { - const token = await getToken(); - if (!token) return; - - const response = await fetch(`${API_URL}/me/`, { - method: "GET", - headers: { - Authorization: `Bearer ${token}`, - }, - }); - - if (response.ok) { - const fetchedUserData = await response.json(); - setUserData(fetchedUserData); - } - } catch (error) { - console.error("Error fetching user data:", error); - } - } - - if (displayUser) { - fetchUser(); - } - }, [displayUser]); - const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; @@ -100,13 +72,11 @@ const Header = ({

- {userData?.username - ? userData.username.charAt(0).toUpperCase() - : ""} + {user?.username ? user.username.charAt(0).toUpperCase() : ""} - Hello, {userData?.username ? userData.username.split(" ")[0] : ""} + Hello, {user?.username ? user.username.split(" ")[0] : ""}
)} diff --git a/components/ProfileManager.tsx b/components/ProfileManager.tsx index 03a9041..b5a485e 100644 --- a/components/ProfileManager.tsx +++ b/components/ProfileManager.tsx @@ -19,10 +19,15 @@ export default function ProfileManager({ setUserData((prev) => (prev ? { ...prev, [field]: value } : prev)); }; + console.log(userData); + return (
{/* Full Name */} +

+ Personal Information +

- - {/* College */}
handleChange("college", e.target.value)} + value={userData.username} + onChange={(e) => handleChange("username", e.target.value)} className="bg-gray-50 py-6" readOnly={!edit} />
{/* SSC & HSC Rolls */} -
-
- - handleChange("ssc_roll", Number(e.target.value))} - className="bg-gray-50 py-6" - readOnly={!edit} - /> -
- -
- - handleChange("hsc_roll", Number(e.target.value))} - className="bg-gray-50 py-6" - readOnly={!edit} - /> -
-
{/* Email */}
@@ -130,6 +98,111 @@ export default function ProfileManager({ readOnly={!edit} />
+

+ Educational Background +

+
+ + handleChange("preparation_unit", e.target.value)} + className="bg-gray-50 py-6" + readOnly={!edit} + /> +
+
+ + handleChange("college", e.target.value)} + className="bg-gray-50 py-6" + readOnly={!edit} + /> +
+
+
+ + handleChange("ssc_roll", Number(e.target.value))} + className="bg-gray-50 py-6" + readOnly={!edit} + /> +
+ +
+ + handleChange("ssc_board", e.target.value)} + className="bg-gray-50 py-6" + readOnly={!edit} + /> +
+
+
+
+ + handleChange("hsc_roll", Number(e.target.value))} + className="bg-gray-50 py-6" + readOnly={!edit} + /> +
+ +
+ + handleChange("hsc_board", e.target.value)} + className="bg-gray-50 py-6" + readOnly={!edit} + /> +
+
); diff --git a/context/AuthContext.tsx b/context/AuthContext.tsx index addebe3..f9323ce 100644 --- a/context/AuthContext.tsx +++ b/context/AuthContext.tsx @@ -2,12 +2,16 @@ import React, { createContext, useContext, useState, useEffect } from "react"; import { useRouter, usePathname } from "next/navigation"; +import { UserData } from "@/types/auth"; +import { API_URL } from "@/lib/auth"; interface AuthContextType { token: string | null; setToken: (token: string | null) => void; logout: () => void; isLoading: boolean; + user: UserData | null; + fetchUser: () => Promise; } const AuthContext = createContext(undefined); @@ -32,7 +36,6 @@ const setCookie = ( if (typeof document === "undefined") return; if (value === null) { - // Delete cookie by setting expiration to past date document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=Strict; Secure`; } else { const expires = new Date(); @@ -46,22 +49,46 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ }) => { const [token, setTokenState] = useState(null); const [isLoading, setIsLoading] = useState(true); + const [user, setUser] = useState(null); const router = useRouter(); const pathname = usePathname(); - // Custom setToken function that also updates cookies const setToken = (newToken: string | null) => { setTokenState(newToken); setCookie("authToken", newToken); }; - // On app load, check if there's a token in cookies + // Fetch user info from API + const fetchUser = async () => { + if (!token) return; + + try { + const res = await fetch(`${API_URL}/me/profile/`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (!res.ok) { + throw new Error("Failed to fetch user info"); + } + + const data: UserData = await res.json(); + setUser(data); + } catch (error) { + console.error("Error fetching user:", error); + setUser(null); + logout(); + } + }; + useEffect(() => { - const initializeAuth = () => { + const initializeAuth = async () => { const storedToken = getCookie("authToken"); if (storedToken) { setTokenState(storedToken); + if ( pathname === "/" || pathname === "/login" || @@ -69,6 +96,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ ) { router.replace("/home"); } + + // Fetch user info when token is found + await fetchUser(); } else { const publicPages = ["/", "/login", "/register"]; if (!publicPages.includes(pathname)) { @@ -82,21 +112,22 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ initializeAuth(); }, [pathname, router]); - // Function to log out const logout = () => { setTokenState(null); - setCookie("authToken", null); // Remove token from cookies - router.replace("/login"); // Redirect to login screen + setUser(null); + setCookie("authToken", null); + router.replace("/login"); }; return ( - + {children} ); }; -// Hook to use the AuthContext export const useAuth = () => { const context = useContext(AuthContext); if (!context) { diff --git a/types/auth.d.ts b/types/auth.d.ts index 9278740..255ab9c 100644 --- a/types/auth.d.ts +++ b/types/auth.d.ts @@ -10,7 +10,7 @@ export interface UserData { hsc_roll: number; hsc_board: string; college: string; - preparation_unit: "Science" | "Arts" | "Commerce" | string; + preparation_unit: "Science" | "Humanities" | "Business" | string; } export interface RegisterForm { @@ -24,7 +24,7 @@ export interface RegisterForm { hsc_roll: number; hsc_board: string; college: string; - preparation_unit: "Science" | "Arts" | "Commerce" | string; + preparation_unit: "Science" | "Humanities" | "Business" | string; } export interface LoginForm {