Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions RestroHub-FrontEnd/src/components/admin/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -398,4 +397,4 @@ const Header = ({ onMobileMenuClick, collapsed, onCollapseToggle }) => {
);
};

export default Header;
export default Header;
7 changes: 4 additions & 3 deletions RestroHub-FrontEnd/src/hooks/useWebSocketNotifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}` } }
Expand Down Expand Up @@ -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}` } }
Expand All @@ -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}` } }
Expand Down
39 changes: 26 additions & 13 deletions RestroHub-FrontEnd/src/pages/public/Login.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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);
Expand All @@ -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!");

Expand Down Expand Up @@ -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!");

Expand Down Expand Up @@ -374,8 +376,19 @@ const handleGoogleLogin = async (credentialResponse) => {
)}
</div>

{/* Forgot password */}
<div className="mb-6 flex justify-end">
<div className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<label className="inline-flex cursor-pointer items-center gap-2 text-sm text-gray-600 dark:text-gray-300">
<input
id="rememberMe"
name="rememberMe"
type="checkbox"
checked={formik.values.rememberMe}
onChange={formik.handleChange}
disabled={isLoading}
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 disabled:cursor-not-allowed disabled:opacity-60"
/>
Remember me
</label>
<Link
to="/forgot-password"
className="text-sm text-blue-600 hover:underline dark:text-blue-400"
Expand Down Expand Up @@ -440,4 +453,4 @@ const handleGoogleLogin = async (credentialResponse) => {
);
};

export default Login;
export default Login;
13 changes: 4 additions & 9 deletions RestroHub-FrontEnd/src/routes/ProtectedRoute.jsx
Original file line number Diff line number Diff line change
@@ -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 <Navigate to="/login" replace />;
}

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;
Expand All @@ -40,4 +35,4 @@ const ProtectedRoute = ({ children }) => {
return children;
};

export default ProtectedRoute;
export default ProtectedRoute;
11 changes: 5 additions & 6 deletions RestroHub-FrontEnd/src/services/common/api.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import axios from "axios";
import { clearAuthSession, getAccessToken } from "./authStorage";

const api = axios.create({
baseURL:
Expand All @@ -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'];
Expand All @@ -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";
}
}
Expand All @@ -37,4 +36,4 @@ api.interceptors.response.use(
}
);

export default api;
export default api;
37 changes: 37 additions & 0 deletions RestroHub-FrontEnd/src/services/common/authStorage.js
Original file line number Diff line number Diff line change
@@ -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);
});
};
1 change: 1 addition & 0 deletions RestroHub-FrontEnd/src/services/public/ApiService.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ try {
console.error("Failed to parse response:", err);
throw new Error("Invalid server response");
}
};

const ApiService = {
// ============================================
Expand Down