Get user management page working, add permission checking in more places
This commit is contained in:
@@ -1,4 +1,19 @@
|
|||||||
const pool = global.pool;
|
// Get pool from global or create a new one if not available
|
||||||
|
let pool;
|
||||||
|
if (typeof global.pool !== 'undefined') {
|
||||||
|
pool = global.pool;
|
||||||
|
} else {
|
||||||
|
// If global pool is not available, create a new connection
|
||||||
|
const { Pool } = require('pg');
|
||||||
|
pool = new Pool({
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
user: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
port: process.env.DB_PORT,
|
||||||
|
});
|
||||||
|
console.log('Created new database pool in permissions.js');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a user has a specific permission
|
* Check if a user has a specific permission
|
||||||
|
|||||||
@@ -1,10 +1,26 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const pool = global.pool;
|
|
||||||
const bcrypt = require('bcrypt');
|
const bcrypt = require('bcrypt');
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const { requirePermission, getUserPermissions } = require('./permissions');
|
const { requirePermission, getUserPermissions } = require('./permissions');
|
||||||
|
|
||||||
|
// Get pool from global or create a new one if not available
|
||||||
|
let pool;
|
||||||
|
if (typeof global.pool !== 'undefined') {
|
||||||
|
pool = global.pool;
|
||||||
|
} else {
|
||||||
|
// If global pool is not available, create a new connection
|
||||||
|
const { Pool } = require('pg');
|
||||||
|
pool = new Pool({
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
user: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
port: process.env.DB_PORT,
|
||||||
|
});
|
||||||
|
console.log('Created new database pool in routes.js');
|
||||||
|
}
|
||||||
|
|
||||||
// Authentication middleware
|
// Authentication middleware
|
||||||
const authenticate = async (req, res, next) => {
|
const authenticate = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -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 { MainLayout } from './components/layout/MainLayout';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { Products } from './pages/Products';
|
import { Products } from './pages/Products';
|
||||||
@@ -16,62 +16,81 @@ import { Vendors } from '@/pages/Vendors';
|
|||||||
import { Categories } from '@/pages/Categories';
|
import { Categories } from '@/pages/Categories';
|
||||||
import { Import } from '@/pages/Import';
|
import { Import } from '@/pages/Import';
|
||||||
import { AiValidationDebug } from "@/pages/AiValidationDebug"
|
import { AiValidationDebug } from "@/pages/AiValidationDebug"
|
||||||
|
import { AuthProvider } from './contexts/AuthContext';
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkAuth = async () => {
|
const checkAuth = async () => {
|
||||||
const token = localStorage.getItem('token');
|
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 {
|
try {
|
||||||
const response = await fetch(`${config.authUrl}/me`, {
|
const response = await fetch(`${config.authUrl}/me`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
sessionStorage.removeItem('isLoggedIn');
|
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) {
|
} catch (error) {
|
||||||
console.error('Token verification failed:', error);
|
console.error('Token verification failed:', error);
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
sessionStorage.removeItem('isLoggedIn');
|
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();
|
checkAuth();
|
||||||
}, [navigate]);
|
}, [navigate, location.pathname, location.search]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Toaster richColors position="top-center" />
|
<AuthProvider>
|
||||||
<Routes>
|
<Toaster richColors position="top-center" />
|
||||||
<Route path="/login" element={<Login />} />
|
<Routes>
|
||||||
<Route element={
|
<Route path="/login" element={<Login />} />
|
||||||
<RequireAuth>
|
<Route element={
|
||||||
<MainLayout />
|
<RequireAuth>
|
||||||
</RequireAuth>
|
<MainLayout />
|
||||||
}>
|
</RequireAuth>
|
||||||
<Route path="/" element={<Dashboard />} />
|
}>
|
||||||
<Route path="/products" element={<Products />} />
|
<Route path="/" element={<Dashboard />} />
|
||||||
<Route path="/import" element={<Import />} />
|
<Route path="/products" element={<Products />} />
|
||||||
<Route path="/categories" element={<Categories />} />
|
<Route path="/import" element={<Import />} />
|
||||||
<Route path="/vendors" element={<Vendors />} />
|
<Route path="/categories" element={<Categories />} />
|
||||||
<Route path="/purchase-orders" element={<PurchaseOrders />} />
|
<Route path="/vendors" element={<Vendors />} />
|
||||||
<Route path="/analytics" element={<Analytics />} />
|
<Route path="/purchase-orders" element={<PurchaseOrders />} />
|
||||||
<Route path="/settings" element={<Settings />} />
|
<Route path="/analytics" element={<Analytics />} />
|
||||||
<Route path="/forecasting" element={<Forecasting />} />
|
<Route path="/settings" element={<Settings />} />
|
||||||
<Route path="/ai-validation/debug" element={<AiValidationDebug />} />
|
<Route path="/forecasting" element={<Forecasting />} />
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="/ai-validation/debug" element={<AiValidationDebug />} />
|
||||||
</Route>
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Routes>
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
</AuthProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
import { Navigate, useLocation } from "react-router-dom"
|
import { Navigate, useLocation } from "react-router-dom"
|
||||||
|
import { useContext, useEffect } from "react"
|
||||||
|
import { AuthContext } from "@/contexts/AuthContext"
|
||||||
|
|
||||||
export function RequireAuth({ children }: { children: React.ReactNode }) {
|
export function RequireAuth({ children }: { children: React.ReactNode }) {
|
||||||
const isLoggedIn = sessionStorage.getItem("isLoggedIn") === "true"
|
const isLoggedIn = sessionStorage.getItem("isLoggedIn") === "true"
|
||||||
|
const { token, fetchCurrentUser } = useContext(AuthContext)
|
||||||
const location = useLocation()
|
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) {
|
if (!isLoggedIn) {
|
||||||
// Redirect to login with the current path in the redirect parameter
|
// Redirect to login with the current path in the redirect parameter
|
||||||
return <Navigate
|
return <Navigate
|
||||||
|
|||||||
@@ -24,47 +24,56 @@ import {
|
|||||||
SidebarSeparator,
|
SidebarSeparator,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { useLocation, useNavigate, Link } from "react-router-dom";
|
import { useLocation, useNavigate, Link } from "react-router-dom";
|
||||||
|
import { PermissionGuard } from "@/components/common/PermissionGuard";
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
title: "Overview",
|
title: "Overview",
|
||||||
icon: Home,
|
icon: Home,
|
||||||
url: "/",
|
url: "/",
|
||||||
|
permission: "access:dashboard"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Products",
|
title: "Products",
|
||||||
icon: Package,
|
icon: Package,
|
||||||
url: "/products",
|
url: "/products",
|
||||||
|
permission: "access:products"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Import",
|
title: "Import",
|
||||||
icon: FileSpreadsheet,
|
icon: FileSpreadsheet,
|
||||||
url: "/import",
|
url: "/import",
|
||||||
|
permission: "access:import"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Forecasting",
|
title: "Forecasting",
|
||||||
icon: IconCrystalBall,
|
icon: IconCrystalBall,
|
||||||
url: "/forecasting",
|
url: "/forecasting",
|
||||||
|
permission: "access:forecasting"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Categories",
|
title: "Categories",
|
||||||
icon: Tags,
|
icon: Tags,
|
||||||
url: "/categories",
|
url: "/categories",
|
||||||
|
permission: "access:categories"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Vendors",
|
title: "Vendors",
|
||||||
icon: Users,
|
icon: Users,
|
||||||
url: "/vendors",
|
url: "/vendors",
|
||||||
|
permission: "access:vendors"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Purchase Orders",
|
title: "Purchase Orders",
|
||||||
icon: ClipboardList,
|
icon: ClipboardList,
|
||||||
url: "/purchase-orders",
|
url: "/purchase-orders",
|
||||||
|
permission: "access:purchase-orders"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Analytics",
|
title: "Analytics",
|
||||||
icon: BarChart2,
|
icon: BarChart2,
|
||||||
url: "/analytics",
|
url: "/analytics",
|
||||||
|
permission: "access:analytics"
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -73,8 +82,8 @@ export function AppSidebar() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
|
localStorage.removeItem('token');
|
||||||
sessionStorage.removeItem('isLoggedIn');
|
sessionStorage.removeItem('isLoggedIn');
|
||||||
sessionStorage.removeItem('token');
|
|
||||||
navigate('/login');
|
navigate('/login');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -98,20 +107,26 @@ export function AppSidebar() {
|
|||||||
location.pathname === item.url ||
|
location.pathname === item.url ||
|
||||||
(item.url !== "/" && location.pathname.startsWith(item.url));
|
(item.url !== "/" && location.pathname.startsWith(item.url));
|
||||||
return (
|
return (
|
||||||
<SidebarMenuItem key={item.title}>
|
<PermissionGuard
|
||||||
<SidebarMenuButton
|
key={item.title}
|
||||||
asChild
|
permission={item.permission}
|
||||||
tooltip={item.title}
|
fallback={null}
|
||||||
isActive={isActive}
|
>
|
||||||
>
|
<SidebarMenuItem>
|
||||||
<Link to={item.url}>
|
<SidebarMenuButton
|
||||||
<item.icon className="h-4 w-4" />
|
asChild
|
||||||
<span className="group-data-[collapsible=icon]:hidden">
|
tooltip={item.title}
|
||||||
{item.title}
|
isActive={isActive}
|
||||||
</span>
|
>
|
||||||
</Link>
|
<Link to={item.url}>
|
||||||
</SidebarMenuButton>
|
<item.icon className="h-4 w-4" />
|
||||||
</SidebarMenuItem>
|
<span className="group-data-[collapsible=icon]:hidden">
|
||||||
|
{item.title}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</PermissionGuard>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
@@ -122,20 +137,25 @@ export function AppSidebar() {
|
|||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupContent>
|
<SidebarGroupContent>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
<PermissionGuard
|
||||||
<SidebarMenuButton
|
permission="access:settings"
|
||||||
asChild
|
fallback={null}
|
||||||
tooltip="Settings"
|
>
|
||||||
isActive={location.pathname === "/settings"}
|
<SidebarMenuItem>
|
||||||
>
|
<SidebarMenuButton
|
||||||
<Link to="/settings">
|
asChild
|
||||||
<Settings className="h-4 w-4" />
|
tooltip="Settings"
|
||||||
<span className="group-data-[collapsible=icon]:hidden">
|
isActive={location.pathname === "/settings"}
|
||||||
Settings
|
>
|
||||||
</span>
|
<Link to="/settings">
|
||||||
</Link>
|
<Settings className="h-4 w-4" />
|
||||||
</SidebarMenuButton>
|
<span className="group-data-[collapsible=icon]:hidden">
|
||||||
</SidebarMenuItem>
|
Settings
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</PermissionGuard>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroupContent>
|
</SidebarGroupContent>
|
||||||
</SidebarGroup>
|
</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 { 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 { Button } from "@/components/ui/button";
|
||||||
import { UserList } from "./UserList";
|
import { UserList } from "./UserList";
|
||||||
import { UserForm } from "./UserForm";
|
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 {
|
interface User {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -29,6 +33,8 @@ interface PermissionCategory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function UserManagement() {
|
export function UserManagement() {
|
||||||
|
const { token, fetchCurrentUser } = useContext(AuthContext);
|
||||||
|
const { hasPermission } = usePermissions();
|
||||||
const [users, setUsers] = useState<User[]>([]);
|
const [users, setUsers] = useState<User[]>([]);
|
||||||
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
||||||
const [isAddingUser, setIsAddingUser] = useState(false);
|
const [isAddingUser, setIsAddingUser] = useState(false);
|
||||||
@@ -37,53 +43,96 @@ export function UserManagement() {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
// Fetch users and permissions
|
// Fetch users and permissions
|
||||||
useEffect(() => {
|
const fetchData = async () => {
|
||||||
const fetchData = async () => {
|
if (!token) {
|
||||||
try {
|
setError("Authentication required. Please log in again.");
|
||||||
// Fetch users
|
setLoading(false);
|
||||||
const usersResponse = await fetch('/auth-inv/users', {
|
return;
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// 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();
|
fetchData();
|
||||||
}, []);
|
}, [token]);
|
||||||
|
|
||||||
const handleEditUser = async (userId: number) => {
|
const handleEditUser = async (userId: number) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/auth-inv/users/${userId}`, {
|
const response = await fetch(`${config.authUrl}/users/${userId}`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
'Authorization': `Bearer ${token}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -110,15 +159,15 @@ export function UserManagement() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const endpoint = userData.id
|
const endpoint = userData.id
|
||||||
? `/auth-inv/users/${userData.id}`
|
? `${config.authUrl}/users/${userData.id}`
|
||||||
: '/auth-inv/users';
|
: `${config.authUrl}/users`;
|
||||||
|
|
||||||
const method = userData.id ? 'PUT' : 'POST';
|
const method = userData.id ? 'PUT' : 'POST';
|
||||||
|
|
||||||
const response = await fetch(endpoint, {
|
const response = await fetch(endpoint, {
|
||||||
method,
|
method,
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
'Authorization': `Bearer ${token}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify(userData)
|
body: JSON.stringify(userData)
|
||||||
@@ -129,20 +178,12 @@ export function UserManagement() {
|
|||||||
throw new Error(errorData.error || 'Failed to save user');
|
throw new Error(errorData.error || 'Failed to save user');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh user list
|
// Refresh user list after a successful save
|
||||||
const usersResponse = await fetch('/auth-inv/users', {
|
await fetchData();
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedUsers = await usersResponse.json();
|
|
||||||
setUsers(updatedUsers);
|
|
||||||
|
|
||||||
// Reset form state
|
// Reset form state
|
||||||
setSelectedUser(null);
|
setSelectedUser(null);
|
||||||
setIsAddingUser(false);
|
setIsAddingUser(false);
|
||||||
setLoading(false);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMessage = err instanceof Error ? err.message : 'Failed to save user';
|
const errorMessage = err instanceof Error ? err.message : 'Failed to save user';
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
@@ -158,10 +199,10 @@ export function UserManagement() {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const response = await fetch(`/auth-inv/users/${userId}`, {
|
const response = await fetch(`${config.authUrl}/users/${userId}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
'Authorization': `Bearer ${token}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -169,16 +210,8 @@ export function UserManagement() {
|
|||||||
throw new Error('Failed to delete user');
|
throw new Error('Failed to delete user');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh user list
|
// Refresh user list after a successful delete
|
||||||
const usersResponse = await fetch('/auth-inv/users', {
|
await fetchData();
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedUsers = await usersResponse.json();
|
|
||||||
setUsers(updatedUsers);
|
|
||||||
setLoading(false);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMessage = err instanceof Error ? err.message : 'Failed to delete user';
|
const errorMessage = err instanceof Error ? err.message : 'Failed to delete user';
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
@@ -207,9 +240,14 @@ export function UserManagement() {
|
|||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="pt-6">
|
<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>
|
<AlertDescription>{error}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Button onClick={fetchData}>Retry</Button>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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 {
|
export interface Permission {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -44,52 +45,61 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
// Load token from localStorage on init
|
const fetchCurrentUser = useCallback(async () => {
|
||||||
useEffect(() => {
|
|
||||||
const storedToken = localStorage.getItem('token');
|
|
||||||
if (storedToken) {
|
|
||||||
setToken(storedToken);
|
|
||||||
fetchCurrentUser();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const fetchCurrentUser = async () => {
|
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const response = await fetch('/auth-inv/me', {
|
const response = await fetch(`${config.authUrl}/me`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${token}`
|
'Authorization': `Bearer ${token}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
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();
|
const userData = await response.json();
|
||||||
setUser(userData);
|
setUser(userData);
|
||||||
|
// Ensure we have the sessionStorage isLoggedIn flag set
|
||||||
|
sessionStorage.setItem('isLoggedIn', 'true');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMessage = err instanceof Error ? err.message : 'An unknown error occurred';
|
const errorMessage = err instanceof Error ? err.message : 'An unknown error occurred';
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
|
console.error('Auth error:', errorMessage);
|
||||||
|
|
||||||
// Clear token if authentication failed
|
// 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();
|
logout();
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
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) => {
|
const login = async (username: string, password: string) => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const response = await fetch('/auth-inv/login', {
|
const response = await fetch(`${config.authUrl}/login`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@@ -98,18 +108,20 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json();
|
const errorData = await response.json().catch(() => ({}));
|
||||||
throw new Error(errorData.error || 'Login failed');
|
throw new Error(errorData.error || 'Login failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
localStorage.setItem('token', data.token);
|
localStorage.setItem('token', data.token);
|
||||||
|
sessionStorage.setItem('isLoggedIn', 'true');
|
||||||
setToken(data.token);
|
setToken(data.token);
|
||||||
setUser(data.user);
|
setUser(data.user);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMessage = err instanceof Error ? err.message : 'Login failed';
|
const errorMessage = err instanceof Error ? err.message : 'Login failed';
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
|
console.error('Login error:', errorMessage);
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -118,6 +130,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
|
sessionStorage.removeItem('isLoggedIn');
|
||||||
setToken(null);
|
setToken(null);
|
||||||
setUser(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 { TemplateManagement } from "@/components/settings/TemplateManagement";
|
||||||
import { UserManagement } from "@/components/settings/UserManagement";
|
import { UserManagement } from "@/components/settings/UserManagement";
|
||||||
import { motion } from 'motion/react';
|
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() {
|
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 (
|
return (
|
||||||
<motion.div layout className="container mx-auto py-6">
|
<motion.div layout className="container mx-auto py-6">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
@@ -27,9 +43,14 @@ export function Settings() {
|
|||||||
<TabsTrigger value="templates">
|
<TabsTrigger value="templates">
|
||||||
Template Management
|
Template Management
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="user-management">
|
<PermissionGuard
|
||||||
User Management
|
permission="manage:users"
|
||||||
</TabsTrigger>
|
fallback={null}
|
||||||
|
>
|
||||||
|
<TabsTrigger value="user-management">
|
||||||
|
User Management
|
||||||
|
</TabsTrigger>
|
||||||
|
</PermissionGuard>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="data-management">
|
<TabsContent value="data-management">
|
||||||
@@ -53,7 +74,18 @@ export function Settings() {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="user-management">
|
<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>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
Reference in New Issue
Block a user