From cc3b912fbb29f65e1c74c3f7829f3fe6637f6f58 Mon Sep 17 00:00:00 2001 From: DavidK004 Date: Wed, 10 Dec 2025 14:41:50 +0100 Subject: [PATCH] auth context and auth api --- .env | 1 + src/App.tsx | 37 ++++++++++++----- src/api/authApi.ts | 18 ++++++++ src/api/axiosInstance.ts | 27 ++++++++++++ src/components/shared/types/AuthTypes.ts | 31 ++++++++++++++ src/context/AuthContext.tsx | 52 ++++++++++++++++++++++++ src/hooks/auth/useCurrentUser.ts | 11 +++++ src/hooks/auth/useLogin.ts | 18 ++++++++ 8 files changed, 185 insertions(+), 10 deletions(-) create mode 100644 .env create mode 100644 src/api/authApi.ts create mode 100644 src/api/axiosInstance.ts create mode 100644 src/components/shared/types/AuthTypes.ts create mode 100644 src/context/AuthContext.tsx create mode 100644 src/hooks/auth/useCurrentUser.ts create mode 100644 src/hooks/auth/useLogin.ts diff --git a/.env b/.env new file mode 100644 index 0000000..9eeb008 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +VITE_API_URL=http://127.0.0.1:8000 \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index a2a031c..143454b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,15 +1,32 @@ -import { RouterProvider } from 'react-router-dom' -import './App.css' -import router from './router/router' +import { RouterProvider } from "react-router-dom"; +import "./App.css"; +import router from "./router/router"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { AuthProvider } from "./context/AuthContext"; +import { ToastContainer } from "react-toastify"; + +const queryClient = new QueryClient(); function App() { - - return ( - <> - - - ) + + + + + + + ); } -export default App +export default App; diff --git a/src/api/authApi.ts b/src/api/authApi.ts new file mode 100644 index 0000000..c0f9108 --- /dev/null +++ b/src/api/authApi.ts @@ -0,0 +1,18 @@ +import { + type LoginPayload, + type LoginResponse, + type MeResponse, +} from "../components/shared/types/AuthTypes"; +import axiosInstance from "./axiosInstance"; + +export const loginRequest = async (data: LoginPayload) => { + const res = await axiosInstance.post("/api/auth/login", data); + return res.data; +}; + +export const fetchMe = async () => { + const res = await axiosInstance.get("/api/auth/me"); + return res.data.user; +}; + + diff --git a/src/api/axiosInstance.ts b/src/api/axiosInstance.ts new file mode 100644 index 0000000..6a38bbd --- /dev/null +++ b/src/api/axiosInstance.ts @@ -0,0 +1,27 @@ +import axios from "axios"; + +const baseURL = import.meta.env.VITE_API_URL; + +const axiosInstance = axios.create({ + baseURL, + timeout: 30 * 1000, +}); + +axiosInstance.interceptors.request.use( + (config) => { + const token = localStorage.getItem("access_token"); + if (token) config.headers.Authorization = `Bearer ${token}`; + + return config; + }, + (error) => Promise.reject(error), +); +axiosInstance.interceptors.response.use(undefined, async (error) => { + if (error.response?.status === 401) { + localStorage.removeItem("access_token"); + } + + throw error; +}); + +export default axiosInstance; diff --git a/src/components/shared/types/AuthTypes.ts b/src/components/shared/types/AuthTypes.ts new file mode 100644 index 0000000..9f71e45 --- /dev/null +++ b/src/components/shared/types/AuthTypes.ts @@ -0,0 +1,31 @@ +export interface school_index_object { + id: number; + school_index: string; +} + +export interface User { + id: number; + username: string; + index: school_index_object; + school_index: number; + email: string; + role: string; + is_superuser: boolean; +} + +export interface LoginResponse { + access_token: string; +} + +export interface LoginPayload { + email: string; + password: string; +} + +export interface MeResponse { + user: User; +} + + + + diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx new file mode 100644 index 0000000..ce79d52 --- /dev/null +++ b/src/context/AuthContext.tsx @@ -0,0 +1,52 @@ +import { useQueryClient } from "@tanstack/react-query"; +import { toast } from "react-toastify"; +import { useCurrentUser } from "../hooks/auth/useCurrentUser"; +import { useLogin } from "../hooks/auth/useLogin"; +import type { + LoginPayload, + LoginResponse, + User, +} from "../components/shared/types/AuthTypes"; +import { createContext, useContext } from "react"; +import type { OnlyChildrenProps } from "../components/shared/types/OnlyChildrenProps"; + +interface AuthContextType { + user: User | null; + login: (data: LoginPayload) => Promise; + logout: () => void; + isLoading: boolean; +} + +const AuthContext = createContext(undefined); + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) throw new Error("useAuth must be used within AuthProvider"); + return context; +}; + +export const AuthProvider = ({ children }: OnlyChildrenProps) => { + const queryClient = useQueryClient(); + const { data: user, isLoading } = useCurrentUser(); + const loginMutation = useLogin(); + + const logout = () => { + localStorage.removeItem("access_token"); + queryClient.removeQueries({ queryKey: ["me"] }); + toast.info("Logged out"); + window.location.href = "/login"; + }; + + return ( + + {children} + + ); +}; diff --git a/src/hooks/auth/useCurrentUser.ts b/src/hooks/auth/useCurrentUser.ts new file mode 100644 index 0000000..c9250a6 --- /dev/null +++ b/src/hooks/auth/useCurrentUser.ts @@ -0,0 +1,11 @@ +import { useQuery } from "@tanstack/react-query"; +import { fetchMe } from "../../api/authApi"; + +export const useCurrentUser = () => + useQuery({ + queryKey: ["me"], + queryFn: fetchMe, + enabled: !!localStorage.getItem("access_token"), + retry: false, + staleTime: 5 * 60 * 1000, + }); diff --git a/src/hooks/auth/useLogin.ts b/src/hooks/auth/useLogin.ts new file mode 100644 index 0000000..6e2fc94 --- /dev/null +++ b/src/hooks/auth/useLogin.ts @@ -0,0 +1,18 @@ +import { useMutation } from "@tanstack/react-query"; +import { loginRequest } from "../../api/authApi"; +import { toast } from "react-toastify"; + +export const useLogin = () => { + + return useMutation({ + mutationFn: loginRequest, + + onSuccess: (data) => { + localStorage.setItem("access_token", data.access_token); + }, + + onError: (err: any) => { + toast.error(err.response?.data?.message || "Login failed"); + }, + }); +};