From 7748cd10c9f42c75c69bc04e0ef317900cd1e154 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 10 Apr 2026 19:24:15 +0530 Subject: [PATCH] feat: implement authentication flow with JWT refresh logic, Login page, and user session management --- backend/config/settings.py | 3 +- frontend/src/App.tsx | 25 +++++- frontend/src/api/apiClient.ts | 18 ++-- frontend/src/components/ui/FormField.tsx | 2 +- frontend/src/pages/Drawer.tsx | 28 +++--- frontend/src/pages/Login.tsx | 109 +++++++++++++++++++++++ frontend/src/store/useAuth.ts | 50 ++++++++--- 7 files changed, 205 insertions(+), 30 deletions(-) create mode 100644 frontend/src/pages/Login.tsx diff --git a/backend/config/settings.py b/backend/config/settings.py index 1d0a026..258a607 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -105,7 +105,8 @@ REST_FRAMEWORK = { } SIMPLE_JWT = { - "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60), + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=30), + # "ACCESS_TOKEN_LIFETIME": timedelta(seconds=10), # lazy testing "REFRESH_TOKEN_LIFETIME": timedelta(days=1), "ROTATE_REFRESH_TOKENS": True, "BLACKLIST_AFTER_ROTATION": True, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3aa2c45..fb3fbdb 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,11 +1,33 @@ +import { useEffect } from "react"; import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; +import Logo from "./components/Logo"; import Activate from "./pages/Activate"; import Drawer from "./pages/Drawer"; import Home from "./pages/Home"; +import Login from "./pages/Login"; import Register from "./pages/Register"; import VerifyEmail from "./pages/VerifyEmail"; +import { useAuth } from "./store/useAuth"; export default function App() { + const { checkAuth, isInitializing } = useAuth(); + + useEffect(() => { + checkAuth(); + }, [checkAuth]); + + if (isInitializing) { + return ( +
+ + +

+ LOADING... +

+
+ ); + } + return (
@@ -14,7 +36,8 @@ export default function App() { } /> } /> } /> - } /> + } /> + } /> } />
diff --git a/frontend/src/api/apiClient.ts b/frontend/src/api/apiClient.ts index 51b4c41..3707016 100644 --- a/frontend/src/api/apiClient.ts +++ b/frontend/src/api/apiClient.ts @@ -13,15 +13,23 @@ authApiClient.interceptors.response.use( (response) => response, async (error: AxiosError) => { if ( - error.response.status === 401 && - !error.config.url?.includes("refresh/") + error.response?.status === 401 && + !error.config?.url?.includes("refresh/") ) { - // token expired, refresh it try { const response = await authApiClient.post("refresh/"); if (response.status === 200) { - // refresh successful, retry the request - return authApiClient(error.config); + const newAccessToken = response.data.access; + + // update the auth store so the retry uses the new token + useAuth.setState({ + accessToken: newAccessToken, + isAuthenticated: true, + }); + if (error.config) { + error.config.headers.Authorization = `Bearer ${newAccessToken}`; + return authApiClient(error.config); + } } } catch (error) { return Promise.reject(error); diff --git a/frontend/src/components/ui/FormField.tsx b/frontend/src/components/ui/FormField.tsx index 539e4a9..6d86a91 100644 --- a/frontend/src/components/ui/FormField.tsx +++ b/frontend/src/components/ui/FormField.tsx @@ -31,7 +31,7 @@ export default function FormField({ error ? "input-error" : "" }`} /> - {error &&

{error}

} + {error &&

{error}

} ); } diff --git a/frontend/src/pages/Drawer.tsx b/frontend/src/pages/Drawer.tsx index b89172c..123e5ae 100644 --- a/frontend/src/pages/Drawer.tsx +++ b/frontend/src/pages/Drawer.tsx @@ -1,15 +1,21 @@ -import Logo from "../components/Logo"; +import { useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { useAuth } from "../store/useAuth"; + +export default function Drawer() { + const { user, isAuthenticated } = useAuth(); + const navigate = useNavigate(); + + // Redirect to login if not authenticated + useEffect(() => { + if (!isAuthenticated) { + navigate("/login"); + } + }, [isAuthenticated, navigate]); + + if (!user) return null; -export default function Login() { return ( -
-

- Login to -

-
- -
+
); } diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx new file mode 100644 index 0000000..7efe40e --- /dev/null +++ b/frontend/src/pages/Login.tsx @@ -0,0 +1,109 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import axios from "axios"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { useNavigate } from "react-router-dom"; +import { z } from "zod"; +import Logo from "../components/Logo"; +import FormField from "../components/ui/FormField"; +import { useAuth } from "../store/useAuth"; + +const loginSchema = z.object({ + email: z.email("Please enter a valid email"), + password: z.string().min(1, "Password is required"), +}); + +type LoginInputs = z.infer; + +export default function Login() { + const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(false); + const [apiError, setApiError] = useState(null); + const login = useAuth((state) => state.login); + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(loginSchema), + }); + + const onSubmit = async (data: LoginInputs) => { + setIsLoading(true); + setApiError(null); + try { + await login(data); + navigate("/drawer"); + } catch (err) { + console.error("Login error:", err); + let message = "Invalid email or password"; + if (axios.isAxiosError(err)) { + message = + err.response?.data?.detail || err.response?.data?.message || message; + } + setApiError(message); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+
+

+ Sign in to +

+ + {apiError && ( +
+ {apiError} +
+ )} + + + + + +
+ +
+ +
+ Don't have an account?{" "} + +
+ +
+
+ ); +} diff --git a/frontend/src/store/useAuth.ts b/frontend/src/store/useAuth.ts index 4233d41..11a938b 100644 --- a/frontend/src/store/useAuth.ts +++ b/frontend/src/store/useAuth.ts @@ -1,36 +1,64 @@ import { create } from "zustand"; import authApiClient from "../api/apiClient"; +interface UserProfile { + public_id: string; + email: string; + full_name: string; +} + interface AuthState { accessToken: string | null; - refreshToken: string | null; isAuthenticated: boolean; - user: any | null; + user: UserProfile | null; + isInitializing: boolean; // refresh in transit login: (credentials: any) => Promise; logout: () => Promise; + checkAuth: () => Promise; } export const useAuth = create((set) => ({ accessToken: null, - refreshToken: null, isAuthenticated: false, user: null, + isInitializing: true, + login: async (credentials: any) => { const response = await authApiClient.post("login/", credentials); set({ accessToken: response.data.access, - refreshToken: response.data.refresh, isAuthenticated: true, user: response.data.user, }); }, + logout: async () => { - await authApiClient.post("logout/"); - set({ - accessToken: null, - refreshToken: null, - isAuthenticated: false, - user: null, - }); + try { + await authApiClient.post("logout/"); + } finally { + set({ + accessToken: null, + isAuthenticated: false, + user: null, + }); + } + }, + + checkAuth: async () => { + try { + const response = await authApiClient.get("me/"); + set({ + user: response.data, + isAuthenticated: true, + }); + } catch (err) { + console.error("Check auth error:", err); + set({ + user: null, + isAuthenticated: false, + }); + } finally { + set({ isInitializing: false }); + } }, }));