Get user management page working, add permission checking in more places

This commit is contained in:
2025-03-22 22:27:50 -04:00
parent 03dc119a15
commit f421154c1d
9 changed files with 414 additions and 147 deletions

View File

@@ -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

View File

@@ -1,10 +1,26 @@
const express = require('express');
const router = express.Router();
const pool = global.pool;
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
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
const authenticate = async (req, res, next) => {
try {

View File

@@ -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>
);
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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')}`
}
});
const fetchData = async () => {
if (!token) {
setError("Authentication required. Please log in again.");
setLoading(false);
return;
}
if (!usersResponse.ok) {
throw new Error('Failed to fetch users');
// 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}`
}
});
const usersData = await usersResponse.json();
setUsers(usersData);
// Fetch permissions
const permissionsResponse = await fetch('/auth-inv/permissions/categories', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('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})`);
}
});
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 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>
);

View File

@@ -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);
};

View 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;
}

View File

@@ -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>