From 54df2858a0973cbc22b604fd03c865eb3dac491b Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 7 Jun 2026 21:23:19 +0530 Subject: [PATCH] Add remember me auth storage --- .../src/components/admin/Header.jsx | 7 ++-- .../src/hooks/useWebSocketNotifications.js | 7 ++-- RestroHub-FrontEnd/src/pages/public/Login.jsx | 39 ++++++++++++------- .../src/routes/ProtectedRoute.jsx | 13 ++----- RestroHub-FrontEnd/src/services/common/api.js | 11 +++--- .../src/services/common/authStorage.js | 37 ++++++++++++++++++ .../src/services/public/ApiService.js | 1 + 7 files changed, 80 insertions(+), 35 deletions(-) create mode 100644 RestroHub-FrontEnd/src/services/common/authStorage.js diff --git a/RestroHub-FrontEnd/src/components/admin/Header.jsx b/RestroHub-FrontEnd/src/components/admin/Header.jsx index 1a845635..6b3e6727 100644 --- a/RestroHub-FrontEnd/src/components/admin/Header.jsx +++ b/RestroHub-FrontEnd/src/components/admin/Header.jsx @@ -18,6 +18,7 @@ import { useAdminTheme } from '@context/AdminThemeContext'; import profileService from '../../services/user/profileService'; import useWebSocketNotifications from '@hooks/useWebSocketNotifications'; import api from '../../services/common/api'; +import { clearAuthSession } from '../../services/common/authStorage'; const Header = ({ onMobileMenuClick, collapsed, onCollapseToggle }) => { const [searchOpen, setSearchOpen] = useState(false); @@ -41,9 +42,7 @@ const Header = ({ onMobileMenuClick, collapsed, onCollapseToggle }) => { } catch (error) { console.error('Logout API failed:', error); //catches errors if API call breaks } finally { - localStorage.removeItem('accessToken'); - localStorage.removeItem('refreshToken'); - localStorage.removeItem('roles'); + clearAuthSession(); if (api?.defaults?.headers?.common?.Authorization) { //cleans up axios default auth header if it exists delete api.defaults.headers.common.Authorization; @@ -398,4 +397,4 @@ const Header = ({ onMobileMenuClick, collapsed, onCollapseToggle }) => { ); }; -export default Header; \ No newline at end of file +export default Header; diff --git a/RestroHub-FrontEnd/src/hooks/useWebSocketNotifications.js b/RestroHub-FrontEnd/src/hooks/useWebSocketNotifications.js index d5f6076f..24388c9b 100644 --- a/RestroHub-FrontEnd/src/hooks/useWebSocketNotifications.js +++ b/RestroHub-FrontEnd/src/hooks/useWebSocketNotifications.js @@ -2,6 +2,7 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import { Client } from '@stomp/stompjs'; import SockJS from 'sockjs-client/dist/sockjs'; import toast from 'react-hot-toast'; +import { getAccessToken } from '@services/common/authStorage'; // ============================================ // useWebSocketNotifications Hook @@ -55,7 +56,7 @@ const useWebSocketNotifications = (branchId) => { const fetchExisting = async () => { try { - const token = localStorage.getItem('accessToken'); + const token = getAccessToken(); const res = await fetch( `${API_BASE_URL}/secure/api/v1/service-requests/branch/${branchId}`, { headers: { Authorization: `Bearer ${token}` } } @@ -127,7 +128,7 @@ const useWebSocketNotifications = (branchId) => { // Acknowledge a request const acknowledgeRequest = useCallback(async (requestId) => { try { - const token = localStorage.getItem('accessToken'); + const token = getAccessToken(); await fetch( `${API_BASE_URL}/secure/api/v1/service-requests/${requestId}/acknowledge`, { method: 'PATCH', headers: { Authorization: `Bearer ${token}` } } @@ -145,7 +146,7 @@ const useWebSocketNotifications = (branchId) => { // Complete/dismiss a request const completeRequest = useCallback(async (requestId) => { try { - const token = localStorage.getItem('accessToken'); + const token = getAccessToken(); await fetch( `${API_BASE_URL}/secure/api/v1/service-requests/${requestId}/complete`, { method: 'PATCH', headers: { Authorization: `Bearer ${token}` } } diff --git a/RestroHub-FrontEnd/src/pages/public/Login.jsx b/RestroHub-FrontEnd/src/pages/public/Login.jsx index edf9e2ec..248d6a78 100644 --- a/RestroHub-FrontEnd/src/pages/public/Login.jsx +++ b/RestroHub-FrontEnd/src/pages/public/Login.jsx @@ -4,11 +4,11 @@ import { useState } from "react"; import { Link, useNavigate } from "react-router-dom"; import { useFormik } from "formik"; import * as Yup from "yup"; -import axios from "axios"; import toast from "react-hot-toast"; import { GoogleLogin } from "@react-oauth/google"; import { ArrowLeft } from "lucide-react"; import api from "@services/common/api"; +import { storeAuthSession } from "@services/common/authStorage"; import { useTheme } from "@context/ThemeContext"; const API_BASE_URL = @@ -159,7 +159,7 @@ const Login = () => { const [isLoading, setIsLoading] = useState(false); const formik = useFormik({ - initialValues: { username: "", password: "" }, + initialValues: { username: "", password: "", rememberMe: true }, validationSchema, onSubmit: async (values) => { setIsLoading(true); @@ -172,11 +172,12 @@ const Login = () => { if (result.success) { const { accessToken, refreshToken, roles } = result.data; - localStorage.setItem("accessToken", accessToken); - localStorage.setItem("refreshToken", refreshToken); - localStorage.setItem("roles", JSON.stringify(roles)); + storeAuthSession( + { accessToken, refreshToken, roles }, + values.rememberMe + ); - axios.defaults.headers.common["Authorization"] = `Bearer ${accessToken}`; + api.defaults.headers.common["Authorization"] = `Bearer ${accessToken}`; toast.success("Login successful!"); @@ -207,11 +208,12 @@ const handleGoogleLogin = async (credentialResponse) => { if (result.success) { const { accessToken, refreshToken, roles } = result.data; - localStorage.setItem("accessToken", accessToken); - localStorage.setItem("refreshToken", refreshToken); - localStorage.setItem("roles", JSON.stringify(roles)); + storeAuthSession( + { accessToken, refreshToken, roles }, + formik.values.rememberMe + ); - axios.defaults.headers.common["Authorization"] = `Bearer ${accessToken}`; + api.defaults.headers.common["Authorization"] = `Bearer ${accessToken}`; toast.success("Google login successful!"); @@ -374,8 +376,19 @@ const handleGoogleLogin = async (credentialResponse) => { )} - {/* Forgot password */} -
+
+ { ); }; -export default Login; \ No newline at end of file +export default Login; diff --git a/RestroHub-FrontEnd/src/routes/ProtectedRoute.jsx b/RestroHub-FrontEnd/src/routes/ProtectedRoute.jsx index 574fbe86..5c2f7f4a 100644 --- a/RestroHub-FrontEnd/src/routes/ProtectedRoute.jsx +++ b/RestroHub-FrontEnd/src/routes/ProtectedRoute.jsx @@ -1,20 +1,15 @@ import { Navigate, useLocation } from "react-router-dom"; +import { getAccessToken, getStoredRoles } from "@services/common/authStorage"; const ProtectedRoute = ({ children }) => { const location = useLocation(); - const accessToken = localStorage.getItem("accessToken"); + const accessToken = getAccessToken(); if (!accessToken) { return ; } - let roles = []; - try { - const rolesStr = localStorage.getItem("roles"); - if (rolesStr) roles = JSON.parse(rolesStr); - } catch (e) { - console.error("Failed to parse roles"); - } + const roles = getStoredRoles(); const hasRole = (roleToCheck) => { if (!Array.isArray(roles)) return false; @@ -40,4 +35,4 @@ const ProtectedRoute = ({ children }) => { return children; }; -export default ProtectedRoute; \ No newline at end of file +export default ProtectedRoute; diff --git a/RestroHub-FrontEnd/src/services/common/api.js b/RestroHub-FrontEnd/src/services/common/api.js index e51eff02..78dd3bcb 100644 --- a/RestroHub-FrontEnd/src/services/common/api.js +++ b/RestroHub-FrontEnd/src/services/common/api.js @@ -1,4 +1,5 @@ import axios from "axios"; +import { clearAuthSession, getAccessToken } from "./authStorage"; const api = axios.create({ baseURL: @@ -8,10 +9,10 @@ const api = axios.create({ // Add interceptor api.interceptors.request.use( (config) => { - const accessToken = localStorage.getItem("accessToken"); + const accessToken = getAccessToken(); // Add token only for secure APIs if (accessToken) { - config.headers.Authorization = `Bearer ${accessToken}`; + config.headers.Authorization = `Bearer ${accessToken}`; } if (config.data instanceof FormData) { delete config.headers['Content-Type']; @@ -26,9 +27,7 @@ api.interceptors.response.use( (error) => { if (error.response?.status === 401 || error.response?.status === 403) { if (!error.config?.url?.includes("/public/")) { - localStorage.removeItem("accessToken"); - localStorage.removeItem("refreshToken"); - localStorage.removeItem("roles"); + clearAuthSession(); window.location.href = "/login"; } } @@ -37,4 +36,4 @@ api.interceptors.response.use( } ); -export default api; \ No newline at end of file +export default api; diff --git a/RestroHub-FrontEnd/src/services/common/authStorage.js b/RestroHub-FrontEnd/src/services/common/authStorage.js new file mode 100644 index 00000000..93b38734 --- /dev/null +++ b/RestroHub-FrontEnd/src/services/common/authStorage.js @@ -0,0 +1,37 @@ +const AUTH_KEYS = ["accessToken", "refreshToken", "roles"]; + +const getStorage = (rememberMe) => (rememberMe ? localStorage : sessionStorage); + +export const getAuthItem = (key) => + localStorage.getItem(key) || sessionStorage.getItem(key); + +export const getAccessToken = () => getAuthItem("accessToken"); + +export const getStoredRoles = () => { + const rolesStr = getAuthItem("roles"); + if (!rolesStr) return []; + + try { + const roles = JSON.parse(rolesStr); + return Array.isArray(roles) ? roles : []; + } catch (error) { + console.error("Failed to parse roles", error); + return []; + } +}; + +export const storeAuthSession = ({ accessToken, refreshToken, roles }, rememberMe) => { + clearAuthSession(); + + const storage = getStorage(rememberMe); + storage.setItem("accessToken", accessToken); + storage.setItem("refreshToken", refreshToken); + storage.setItem("roles", JSON.stringify(roles || [])); +}; + +export const clearAuthSession = () => { + AUTH_KEYS.forEach((key) => { + localStorage.removeItem(key); + sessionStorage.removeItem(key); + }); +}; diff --git a/RestroHub-FrontEnd/src/services/public/ApiService.js b/RestroHub-FrontEnd/src/services/public/ApiService.js index 9d3c4623..2ec5bd96 100644 --- a/RestroHub-FrontEnd/src/services/public/ApiService.js +++ b/RestroHub-FrontEnd/src/services/public/ApiService.js @@ -60,6 +60,7 @@ try { console.error("Failed to parse response:", err); throw new Error("Invalid server response"); } +}; const ApiService = { // ============================================