feat: implement authentication flow with JWT refresh logic, Login page, and user session management

This commit is contained in:
Your Name
2026-04-10 19:24:15 +05:30
parent c4733249fa
commit 7748cd10c9
7 changed files with 205 additions and 30 deletions
+2 -1
View File
@@ -105,7 +105,8 @@ REST_FRAMEWORK = {
} }
SIMPLE_JWT = { 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), "REFRESH_TOKEN_LIFETIME": timedelta(days=1),
"ROTATE_REFRESH_TOKENS": True, "ROTATE_REFRESH_TOKENS": True,
"BLACKLIST_AFTER_ROTATION": True, "BLACKLIST_AFTER_ROTATION": True,
+24 -1
View File
@@ -1,11 +1,33 @@
import { useEffect } from "react";
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
import Logo from "./components/Logo";
import Activate from "./pages/Activate"; import Activate from "./pages/Activate";
import Drawer from "./pages/Drawer"; import Drawer from "./pages/Drawer";
import Home from "./pages/Home"; import Home from "./pages/Home";
import Login from "./pages/Login";
import Register from "./pages/Register"; import Register from "./pages/Register";
import VerifyEmail from "./pages/VerifyEmail"; import VerifyEmail from "./pages/VerifyEmail";
import { useAuth } from "./store/useAuth";
export default function App() { export default function App() {
const { checkAuth, isInitializing } = useAuth();
useEffect(() => {
checkAuth();
}, [checkAuth]);
if (isInitializing) {
return (
<div className="min-h-screen bg-base-200 flex flex-col items-center justify-center gap-4">
<Logo />
<span className="loading loading-dots loading-lg text-primary"></span>
<p className="text-sm font-medium opacity-50 uppercase tracking-widest">
LOADING...
</p>
</div>
);
}
return ( return (
<BrowserRouter> <BrowserRouter>
<div className="min-h-screen bg-base-200 p-8 flex items-center justify-center"> <div className="min-h-screen bg-base-200 p-8 flex items-center justify-center">
@@ -14,7 +36,8 @@ export default function App() {
<Route path="/onboard" element={<Register />} /> <Route path="/onboard" element={<Register />} />
<Route path="/verify-email" element={<VerifyEmail />} /> <Route path="/verify-email" element={<VerifyEmail />} />
<Route path="/activate/:uidb64/:token" element={<Activate />} /> <Route path="/activate/:uidb64/:token" element={<Activate />} />
<Route path="/login" element={<Drawer />} /> <Route path="/login" element={<Login />} />
<Route path="/drawer" element={<Drawer />} />
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/" replace />} />
</Routes> </Routes>
</div> </div>
+12 -4
View File
@@ -13,16 +13,24 @@ authApiClient.interceptors.response.use(
(response) => response, (response) => response,
async (error: AxiosError) => { async (error: AxiosError) => {
if ( if (
error.response.status === 401 && error.response?.status === 401 &&
!error.config.url?.includes("refresh/") !error.config?.url?.includes("refresh/")
) { ) {
// token expired, refresh it
try { try {
const response = await authApiClient.post("refresh/"); const response = await authApiClient.post("refresh/");
if (response.status === 200) { if (response.status === 200) {
// refresh successful, retry the request 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); return authApiClient(error.config);
} }
}
} catch (error) { } catch (error) {
return Promise.reject(error); return Promise.reject(error);
} }
+1 -1
View File
@@ -31,7 +31,7 @@ export default function FormField({
error ? "input-error" : "" error ? "input-error" : ""
}`} }`}
/> />
{error && <p className="field-error">{error}</p>} {error && <p className="text-error-content">{error}</p>}
</div> </div>
); );
} }
+17 -11
View File
@@ -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 ( return (
<div className="glass-card w-full max-w-sm p-8 text-center fade-zoom"> <div className="glass-card w-full max-w-sm p-8 text-center fade-zoom"></div>
<h2 className="font-display text-2xl font-bold text-primary">
Login to <Logo />
</h2>
<div className="divider"></div>
<button type="button" disabled className="btn btn-primary w-full">
Sign In
</button>
</div>
); );
} }
+109
View File
@@ -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<typeof loginSchema>;
export default function Login() {
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
const [apiError, setApiError] = useState<string | null>(null);
const login = useAuth((state) => state.login);
const {
register,
handleSubmit,
formState: { errors },
} = useForm<LoginInputs>({
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 (
<div className="flex min-h-screen items-center justify-center bg-base-200">
<div className="glass-card w-full max-w-sm p-2 transition-all duration-500 hover:shadow-2xl fade-zoom">
<form onSubmit={handleSubmit(onSubmit)} className="card-body gap-4">
<h2 className="card-title font-display text-2xl font-bold justify-center text-primary tracking-tight">
Sign in to <Logo />
</h2>
{apiError && (
<div className="alert alert-error text-xs py-2 rounded-md">
<span>{apiError}</span>
</div>
)}
<FormField
label="Email"
type="email"
placeholder="you@email.com"
registration={register("email")}
error={errors.email?.message}
/>
<FormField
label="Password"
type="password"
placeholder="••••••••"
registration={register("password")}
error={errors.password?.message}
/>
<div className="card-actions mt-4">
<button
type="submit"
disabled={isLoading}
className="btn btn-primary w-full shadow-lg"
>
{isLoading ? (
<span className="loading loading-spinner loading-sm" />
) : (
"Sign In"
)}
</button>
</div>
<div className="text-center text-sm opacity-70">
Don't have an account?{" "}
<button
type="button"
onClick={() => navigate("/register")}
className="link link-primary text-primary-content no-underline hover:underline"
>
Register
</button>
</div>
</form>
</div>
</div>
);
}
+33 -5
View File
@@ -1,36 +1,64 @@
import { create } from "zustand"; import { create } from "zustand";
import authApiClient from "../api/apiClient"; import authApiClient from "../api/apiClient";
interface UserProfile {
public_id: string;
email: string;
full_name: string;
}
interface AuthState { interface AuthState {
accessToken: string | null; accessToken: string | null;
refreshToken: string | null;
isAuthenticated: boolean; isAuthenticated: boolean;
user: any | null; user: UserProfile | null;
isInitializing: boolean; // refresh in transit
login: (credentials: any) => Promise<void>; login: (credentials: any) => Promise<void>;
logout: () => Promise<void>; logout: () => Promise<void>;
checkAuth: () => Promise<void>;
} }
export const useAuth = create<AuthState>((set) => ({ export const useAuth = create<AuthState>((set) => ({
accessToken: null, accessToken: null,
refreshToken: null,
isAuthenticated: false, isAuthenticated: false,
user: null, user: null,
isInitializing: true,
login: async (credentials: any) => { login: async (credentials: any) => {
const response = await authApiClient.post("login/", credentials); const response = await authApiClient.post("login/", credentials);
set({ set({
accessToken: response.data.access, accessToken: response.data.access,
refreshToken: response.data.refresh,
isAuthenticated: true, isAuthenticated: true,
user: response.data.user, user: response.data.user,
}); });
}, },
logout: async () => { logout: async () => {
try {
await authApiClient.post("logout/"); await authApiClient.post("logout/");
} finally {
set({ set({
accessToken: null, accessToken: null,
refreshToken: null,
isAuthenticated: false, isAuthenticated: false,
user: null, 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 });
}
}, },
})); }));