Get user management page working, add permission checking in more places
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Routes, Route, useNavigate, Navigate } from 'react-router-dom';
|
||||
import { Routes, Route, useNavigate, Navigate, useLocation } from 'react-router-dom';
|
||||
import { MainLayout } from './components/layout/MainLayout';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { Products } from './pages/Products';
|
||||
@@ -16,62 +16,81 @@ import { Vendors } from '@/pages/Vendors';
|
||||
import { Categories } from '@/pages/Categories';
|
||||
import { Import } from '@/pages/Import';
|
||||
import { AiValidationDebug } from "@/pages/AiValidationDebug"
|
||||
import { AuthProvider } from './contexts/AuthContext';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
function App() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuth = async () => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
const isLoggedIn = sessionStorage.getItem('isLoggedIn') === 'true';
|
||||
|
||||
// If we have a token but aren't logged in yet, verify the token
|
||||
if (token && !isLoggedIn) {
|
||||
try {
|
||||
const response = await fetch(`${config.authUrl}/me`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.removeItem('isLoggedIn');
|
||||
navigate('/login');
|
||||
|
||||
// Only navigate to login if we're not already there
|
||||
if (!location.pathname.includes('/login')) {
|
||||
navigate(`/login?redirect=${encodeURIComponent(location.pathname + location.search)}`);
|
||||
}
|
||||
} else {
|
||||
// If token is valid, set the login flag
|
||||
sessionStorage.setItem('isLoggedIn', 'true');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Token verification failed:', error);
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.removeItem('isLoggedIn');
|
||||
navigate('/login');
|
||||
|
||||
// Only navigate to login if we're not already there
|
||||
if (!location.pathname.includes('/login')) {
|
||||
navigate(`/login?redirect=${encodeURIComponent(location.pathname + location.search)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
}, [navigate]);
|
||||
}, [navigate, location.pathname, location.search]);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Toaster richColors position="top-center" />
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route element={
|
||||
<RequireAuth>
|
||||
<MainLayout />
|
||||
</RequireAuth>
|
||||
}>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/products" element={<Products />} />
|
||||
<Route path="/import" element={<Import />} />
|
||||
<Route path="/categories" element={<Categories />} />
|
||||
<Route path="/vendors" element={<Vendors />} />
|
||||
<Route path="/purchase-orders" element={<PurchaseOrders />} />
|
||||
<Route path="/analytics" element={<Analytics />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="/forecasting" element={<Forecasting />} />
|
||||
<Route path="/ai-validation/debug" element={<AiValidationDebug />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
<AuthProvider>
|
||||
<Toaster richColors position="top-center" />
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route element={
|
||||
<RequireAuth>
|
||||
<MainLayout />
|
||||
</RequireAuth>
|
||||
}>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/products" element={<Products />} />
|
||||
<Route path="/import" element={<Import />} />
|
||||
<Route path="/categories" element={<Categories />} />
|
||||
<Route path="/vendors" element={<Vendors />} />
|
||||
<Route path="/purchase-orders" element={<PurchaseOrders />} />
|
||||
<Route path="/analytics" element={<Analytics />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="/forecasting" element={<Forecasting />} />
|
||||
<Route path="/ai-validation/debug" element={<AiValidationDebug />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
import { Navigate, useLocation } from "react-router-dom"
|
||||
import { useContext, useEffect } from "react"
|
||||
import { AuthContext } from "@/contexts/AuthContext"
|
||||
|
||||
export function RequireAuth({ children }: { children: React.ReactNode }) {
|
||||
const isLoggedIn = sessionStorage.getItem("isLoggedIn") === "true"
|
||||
const { token, fetchCurrentUser } = useContext(AuthContext)
|
||||
const location = useLocation()
|
||||
|
||||
// Check if token exists but we're not logged in
|
||||
useEffect(() => {
|
||||
if (token && !isLoggedIn) {
|
||||
// Verify the token and fetch user data
|
||||
fetchCurrentUser().catch(() => {
|
||||
// Do nothing - the AuthContext will handle errors
|
||||
})
|
||||
}
|
||||
}, [token, isLoggedIn, fetchCurrentUser])
|
||||
|
||||
if (!isLoggedIn) {
|
||||
// Redirect to login with the current path in the redirect parameter
|
||||
return <Navigate
|
||||
|
||||
@@ -24,47 +24,56 @@ import {
|
||||
SidebarSeparator,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { useLocation, useNavigate, Link } from "react-router-dom";
|
||||
import { PermissionGuard } from "@/components/common/PermissionGuard";
|
||||
|
||||
const items = [
|
||||
{
|
||||
title: "Overview",
|
||||
icon: Home,
|
||||
url: "/",
|
||||
permission: "access:dashboard"
|
||||
},
|
||||
{
|
||||
title: "Products",
|
||||
icon: Package,
|
||||
url: "/products",
|
||||
permission: "access:products"
|
||||
},
|
||||
{
|
||||
title: "Import",
|
||||
icon: FileSpreadsheet,
|
||||
url: "/import",
|
||||
permission: "access:import"
|
||||
},
|
||||
{
|
||||
title: "Forecasting",
|
||||
icon: IconCrystalBall,
|
||||
url: "/forecasting",
|
||||
permission: "access:forecasting"
|
||||
},
|
||||
{
|
||||
title: "Categories",
|
||||
icon: Tags,
|
||||
url: "/categories",
|
||||
permission: "access:categories"
|
||||
},
|
||||
{
|
||||
title: "Vendors",
|
||||
icon: Users,
|
||||
url: "/vendors",
|
||||
permission: "access:vendors"
|
||||
},
|
||||
{
|
||||
title: "Purchase Orders",
|
||||
icon: ClipboardList,
|
||||
url: "/purchase-orders",
|
||||
permission: "access:purchase-orders"
|
||||
},
|
||||
{
|
||||
title: "Analytics",
|
||||
icon: BarChart2,
|
||||
url: "/analytics",
|
||||
permission: "access:analytics"
|
||||
},
|
||||
];
|
||||
|
||||
@@ -73,8 +82,8 @@ export function AppSidebar() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.removeItem('isLoggedIn');
|
||||
sessionStorage.removeItem('token');
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
@@ -98,20 +107,26 @@ export function AppSidebar() {
|
||||
location.pathname === item.url ||
|
||||
(item.url !== "/" && location.pathname.startsWith(item.url));
|
||||
return (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
tooltip={item.title}
|
||||
isActive={isActive}
|
||||
>
|
||||
<Link to={item.url}>
|
||||
<item.icon className="h-4 w-4" />
|
||||
<span className="group-data-[collapsible=icon]:hidden">
|
||||
{item.title}
|
||||
</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<PermissionGuard
|
||||
key={item.title}
|
||||
permission={item.permission}
|
||||
fallback={null}
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
tooltip={item.title}
|
||||
isActive={isActive}
|
||||
>
|
||||
<Link to={item.url}>
|
||||
<item.icon className="h-4 w-4" />
|
||||
<span className="group-data-[collapsible=icon]:hidden">
|
||||
{item.title}
|
||||
</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</PermissionGuard>
|
||||
);
|
||||
})}
|
||||
</SidebarMenu>
|
||||
@@ -122,20 +137,25 @@ export function AppSidebar() {
|
||||
<SidebarGroup>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
tooltip="Settings"
|
||||
isActive={location.pathname === "/settings"}
|
||||
>
|
||||
<Link to="/settings">
|
||||
<Settings className="h-4 w-4" />
|
||||
<span className="group-data-[collapsible=icon]:hidden">
|
||||
Settings
|
||||
</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<PermissionGuard
|
||||
permission="access:settings"
|
||||
fallback={null}
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
tooltip="Settings"
|
||||
isActive={location.pathname === "/settings"}
|
||||
>
|
||||
<Link to="/settings">
|
||||
<Settings className="h-4 w-4" />
|
||||
<span className="group-data-[collapsible=icon]:hidden">
|
||||
Settings
|
||||
</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</PermissionGuard>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useContext } from "react";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { UserList } from "./UserList";
|
||||
import { UserForm } from "./UserForm";
|
||||
import config from "@/config";
|
||||
import { AuthContext } from "@/contexts/AuthContext";
|
||||
import { usePermissions } from "@/hooks/usePermissions";
|
||||
import { ShieldAlert } from "lucide-react";
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
@@ -29,6 +33,8 @@ interface PermissionCategory {
|
||||
}
|
||||
|
||||
export function UserManagement() {
|
||||
const { token, fetchCurrentUser } = useContext(AuthContext);
|
||||
const { hasPermission } = usePermissions();
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
||||
const [isAddingUser, setIsAddingUser] = useState(false);
|
||||
@@ -37,53 +43,96 @@ export function UserManagement() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Fetch users and permissions
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
// Fetch users
|
||||
const usersResponse = await fetch('/auth-inv/users', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!usersResponse.ok) {
|
||||
throw new Error('Failed to fetch users');
|
||||
}
|
||||
|
||||
const usersData = await usersResponse.json();
|
||||
setUsers(usersData);
|
||||
|
||||
// Fetch permissions
|
||||
const permissionsResponse = await fetch('/auth-inv/permissions/categories', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!permissionsResponse.ok) {
|
||||
throw new Error('Failed to fetch permissions');
|
||||
}
|
||||
|
||||
const permissionsData = await permissionsResponse.json();
|
||||
setPermissions(permissionsData);
|
||||
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'An error occurred';
|
||||
setError(errorMessage);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const fetchData = async () => {
|
||||
if (!token) {
|
||||
setError("Authentication required. Please log in again.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// The PermissionGuard component already handles permission checks,
|
||||
// so we don't need to duplicate that logic here
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Fetch users
|
||||
const usersResponse = await fetch(`${config.authUrl}/users`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!usersResponse.ok) {
|
||||
if (usersResponse.status === 401) {
|
||||
throw new Error('Authentication failed. Please log in again.');
|
||||
} else if (usersResponse.status === 403) {
|
||||
throw new Error('You don\'t have permission to access the user list.');
|
||||
} else {
|
||||
// Try to get more detailed error message from response
|
||||
try {
|
||||
const errorData = await usersResponse.json();
|
||||
throw new Error(errorData.error || `Failed to fetch users (${usersResponse.status})`);
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to fetch users (${usersResponse.status})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const usersData = await usersResponse.json();
|
||||
setUsers(usersData);
|
||||
|
||||
// Fetch permissions
|
||||
const permissionsResponse = await fetch(`${config.authUrl}/permissions/categories`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!permissionsResponse.ok) {
|
||||
if (permissionsResponse.status === 401) {
|
||||
throw new Error('Authentication failed. Please log in again.');
|
||||
} else if (permissionsResponse.status === 403) {
|
||||
throw new Error('You don\'t have permission to access permissions.');
|
||||
} else {
|
||||
// Try to get more detailed error message from response
|
||||
try {
|
||||
const errorData = await permissionsResponse.json();
|
||||
throw new Error(errorData.error || `Failed to fetch permissions (${permissionsResponse.status})`);
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to fetch permissions (${permissionsResponse.status})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const permissionsData = await permissionsResponse.json();
|
||||
setPermissions(permissionsData);
|
||||
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'An error occurred';
|
||||
setError(errorMessage);
|
||||
setLoading(false);
|
||||
|
||||
// If authentication error, refresh the token
|
||||
if (err instanceof Error && err.message.includes('Authentication failed')) {
|
||||
fetchCurrentUser().catch(() => {
|
||||
// Handle failed token refresh
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
}, [token]);
|
||||
|
||||
const handleEditUser = async (userId: number) => {
|
||||
try {
|
||||
const response = await fetch(`/auth-inv/users/${userId}`, {
|
||||
const response = await fetch(`${config.authUrl}/users/${userId}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -110,15 +159,15 @@ export function UserManagement() {
|
||||
setLoading(true);
|
||||
|
||||
const endpoint = userData.id
|
||||
? `/auth-inv/users/${userData.id}`
|
||||
: '/auth-inv/users';
|
||||
? `${config.authUrl}/users/${userData.id}`
|
||||
: `${config.authUrl}/users`;
|
||||
|
||||
const method = userData.id ? 'PUT' : 'POST';
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(userData)
|
||||
@@ -129,20 +178,12 @@ export function UserManagement() {
|
||||
throw new Error(errorData.error || 'Failed to save user');
|
||||
}
|
||||
|
||||
// Refresh user list
|
||||
const usersResponse = await fetch('/auth-inv/users', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
}
|
||||
});
|
||||
|
||||
const updatedUsers = await usersResponse.json();
|
||||
setUsers(updatedUsers);
|
||||
// Refresh user list after a successful save
|
||||
await fetchData();
|
||||
|
||||
// Reset form state
|
||||
setSelectedUser(null);
|
||||
setIsAddingUser(false);
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to save user';
|
||||
setError(errorMessage);
|
||||
@@ -158,10 +199,10 @@ export function UserManagement() {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const response = await fetch(`/auth-inv/users/${userId}`, {
|
||||
const response = await fetch(`${config.authUrl}/users/${userId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -169,16 +210,8 @@ export function UserManagement() {
|
||||
throw new Error('Failed to delete user');
|
||||
}
|
||||
|
||||
// Refresh user list
|
||||
const usersResponse = await fetch('/auth-inv/users', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
}
|
||||
});
|
||||
|
||||
const updatedUsers = await usersResponse.json();
|
||||
setUsers(updatedUsers);
|
||||
setLoading(false);
|
||||
// Refresh user list after a successful delete
|
||||
await fetchData();
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to delete user';
|
||||
setError(errorMessage);
|
||||
@@ -207,9 +240,14 @@ export function UserManagement() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<Alert variant="destructive">
|
||||
<Alert variant="destructive" className="mb-4">
|
||||
<ShieldAlert className="h-4 w-4" />
|
||||
<AlertTitle>Permission Error</AlertTitle>
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
<div className="flex justify-center">
|
||||
<Button onClick={fetchData}>Retry</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createContext, useState, useEffect, ReactNode } from 'react';
|
||||
import { createContext, useState, useEffect, ReactNode, useCallback } from 'react';
|
||||
import config from '@/config';
|
||||
|
||||
export interface Permission {
|
||||
id: number;
|
||||
@@ -44,52 +45,61 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Load token from localStorage on init
|
||||
useEffect(() => {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
if (storedToken) {
|
||||
setToken(storedToken);
|
||||
fetchCurrentUser();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchCurrentUser = async () => {
|
||||
const fetchCurrentUser = useCallback(async () => {
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await fetch('/auth-inv/me', {
|
||||
const response = await fetch(`${config.authUrl}/me`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch user data');
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || 'Failed to fetch user data');
|
||||
}
|
||||
|
||||
const userData = await response.json();
|
||||
setUser(userData);
|
||||
// Ensure we have the sessionStorage isLoggedIn flag set
|
||||
sessionStorage.setItem('isLoggedIn', 'true');
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'An unknown error occurred';
|
||||
setError(errorMessage);
|
||||
console.error('Auth error:', errorMessage);
|
||||
|
||||
// Clear token if authentication failed
|
||||
if (err instanceof Error && err.message.includes('authentication')) {
|
||||
if (err instanceof Error &&
|
||||
(err.message.includes('authentication') ||
|
||||
err.message.includes('token') ||
|
||||
err.message.includes('401'))) {
|
||||
logout();
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
}, [token]);
|
||||
|
||||
// Load token and fetch user data on init
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
fetchCurrentUser();
|
||||
} else {
|
||||
// Clear sessionStorage if no token exists
|
||||
sessionStorage.removeItem('isLoggedIn');
|
||||
}
|
||||
}, [token, fetchCurrentUser]);
|
||||
|
||||
const login = async (username: string, password: string) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await fetch('/auth-inv/login', {
|
||||
const response = await fetch(`${config.authUrl}/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -98,18 +108,20 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || 'Login failed');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
localStorage.setItem('token', data.token);
|
||||
sessionStorage.setItem('isLoggedIn', 'true');
|
||||
setToken(data.token);
|
||||
setUser(data.user);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Login failed';
|
||||
setError(errorMessage);
|
||||
console.error('Login error:', errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -118,6 +130,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.removeItem('isLoggedIn');
|
||||
setToken(null);
|
||||
setUser(null);
|
||||
};
|
||||
|
||||
101
inventory/src/hooks/usePagePermission.ts
Normal file
101
inventory/src/hooks/usePagePermission.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { usePermissions } from './usePermissions';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface PagePermissionConfig {
|
||||
// The permission required to access the specific page
|
||||
permission?: string;
|
||||
|
||||
// Array of permissions where ANY must be present
|
||||
anyPermissions?: string[];
|
||||
|
||||
// Array of permissions where ALL must be present
|
||||
allPermissions?: string[];
|
||||
|
||||
// Whether this page is admin-only
|
||||
adminOnly?: boolean;
|
||||
|
||||
// Page identifier for page-specific access check
|
||||
page?: string;
|
||||
|
||||
// Redirect path if permission check fails
|
||||
redirectTo?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to check if a user has permission to access a specific page
|
||||
* Will automatically redirect if permission is denied
|
||||
*/
|
||||
export function usePagePermission(config: PagePermissionConfig) {
|
||||
const {
|
||||
permission,
|
||||
anyPermissions,
|
||||
allPermissions,
|
||||
adminOnly = false,
|
||||
page,
|
||||
redirectTo = '/'
|
||||
} = config;
|
||||
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
hasPermission,
|
||||
hasPageAccess,
|
||||
hasAnyPermission,
|
||||
hasAllPermissions,
|
||||
isAdmin
|
||||
} = usePermissions();
|
||||
|
||||
const [hasAccess, setHasAccess] = useState<boolean | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Check permissions
|
||||
let permitted = true;
|
||||
|
||||
// Admin check
|
||||
if (adminOnly && !isAdmin) {
|
||||
permitted = false;
|
||||
}
|
||||
|
||||
// Page access check
|
||||
if (page && !hasPageAccess(page)) {
|
||||
permitted = false;
|
||||
}
|
||||
|
||||
// Single permission check
|
||||
if (permission && !hasPermission(permission)) {
|
||||
permitted = false;
|
||||
}
|
||||
|
||||
// Any permissions check
|
||||
if (anyPermissions && !hasAnyPermission(anyPermissions)) {
|
||||
permitted = false;
|
||||
}
|
||||
|
||||
// All permissions check
|
||||
if (allPermissions && !hasAllPermissions(allPermissions)) {
|
||||
permitted = false;
|
||||
}
|
||||
|
||||
setHasAccess(permitted);
|
||||
|
||||
// Redirect if no permission
|
||||
if (permitted === false) {
|
||||
navigate(redirectTo);
|
||||
}
|
||||
}, [
|
||||
permission,
|
||||
anyPermissions,
|
||||
allPermissions,
|
||||
adminOnly,
|
||||
page,
|
||||
redirectTo,
|
||||
hasPermission,
|
||||
hasPageAccess,
|
||||
hasAnyPermission,
|
||||
hasAllPermissions,
|
||||
isAdmin,
|
||||
navigate
|
||||
]);
|
||||
|
||||
return hasAccess;
|
||||
}
|
||||
@@ -6,8 +6,24 @@ import { CalculationSettings } from "@/components/settings/CalculationSettings";
|
||||
import { TemplateManagement } from "@/components/settings/TemplateManagement";
|
||||
import { UserManagement } from "@/components/settings/UserManagement";
|
||||
import { motion } from 'motion/react';
|
||||
import { PermissionGuard } from "@/components/common/PermissionGuard";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { usePagePermission } from "@/hooks/usePagePermission";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export function Settings() {
|
||||
// Check if the user has permission to access the Settings page
|
||||
// This will automatically redirect if permission is denied
|
||||
const hasAccess = usePagePermission({
|
||||
page: 'settings',
|
||||
redirectTo: '/'
|
||||
});
|
||||
|
||||
// Prevent flash of content before redirect
|
||||
if (hasAccess === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div layout className="container mx-auto py-6">
|
||||
<div className="mb-6">
|
||||
@@ -27,9 +43,14 @@ export function Settings() {
|
||||
<TabsTrigger value="templates">
|
||||
Template Management
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="user-management">
|
||||
User Management
|
||||
</TabsTrigger>
|
||||
<PermissionGuard
|
||||
permission="manage:users"
|
||||
fallback={null}
|
||||
>
|
||||
<TabsTrigger value="user-management">
|
||||
User Management
|
||||
</TabsTrigger>
|
||||
</PermissionGuard>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="data-management">
|
||||
@@ -53,7 +74,18 @@ export function Settings() {
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="user-management">
|
||||
<UserManagement />
|
||||
<PermissionGuard
|
||||
permission="manage:users"
|
||||
fallback={
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
You don't have permission to access User Management.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
}
|
||||
>
|
||||
<UserManagement />
|
||||
</PermissionGuard>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</motion.div>
|
||||
|
||||
Reference in New Issue
Block a user