From 7eae4a0b29a560755720528d32e3677fa1e61441 Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 23 Mar 2025 16:04:32 -0400 Subject: [PATCH] More permissions setup, simplify to one component --- docs/PERMISSIONS.md | 172 ++++++++++++++++++ inventory-server/auth/routes.js | 126 +++++++++++-- inventory/src/App.tsx | 61 ++++++- inventory/src/components/auth/PERMISSIONS.md | 104 +++++++++++ inventory/src/components/auth/Protected.tsx | 82 +++++++++ inventory/src/components/auth/RequireAuth.tsx | 28 ++- .../src/components/common/PermissionGuard.tsx | 82 --------- .../src/components/layout/AppSidebar.tsx | 12 +- .../src/components/layout/MainLayout.tsx | 2 +- .../src/components/settings/UserForm.tsx | 122 +++++++++++-- .../src/components/settings/UserList.tsx | 2 + .../components/settings/UserManagement.tsx | 97 ++++++++-- inventory/src/contexts/AuthContext.tsx | 5 + inventory/src/hooks/usePagePermission.ts | 101 ---------- inventory/src/hooks/usePermissions.ts | 80 -------- inventory/src/pages/Login.tsx | 58 +----- inventory/src/pages/Settings.tsx | 51 +++--- 17 files changed, 781 insertions(+), 404 deletions(-) create mode 100644 docs/PERMISSIONS.md create mode 100644 inventory/src/components/auth/PERMISSIONS.md create mode 100644 inventory/src/components/auth/Protected.tsx delete mode 100644 inventory/src/components/common/PermissionGuard.tsx delete mode 100644 inventory/src/hooks/usePagePermission.ts delete mode 100644 inventory/src/hooks/usePermissions.ts diff --git a/docs/PERMISSIONS.md b/docs/PERMISSIONS.md new file mode 100644 index 0000000..687d4f5 --- /dev/null +++ b/docs/PERMISSIONS.md @@ -0,0 +1,172 @@ +# Permission System Documentation + +This document outlines the permission system implemented in the Inventory Manager application. + +## Permission Structure + +Permissions follow this naming convention: + +- Page access: `access:{page_name}` +- Actions: `{action}:{resource}` + +Examples: +- `access:products` - Can access the Products page +- `create:products` - Can create new products +- `edit:users` - Can edit user accounts + +## Permission Components + +### PermissionGuard + +The core component that conditionally renders content based on permissions. + +```tsx +No permission

} +> + +
+``` + +Options: +- `permission`: Single permission code +- `anyPermissions`: Array of permissions (ANY match grants access) +- `allPermissions`: Array of permissions (ALL required) +- `adminOnly`: For admin-only sections +- `page`: Page name (checks `access:{page}` permission) +- `fallback`: Content to show if permission check fails + +### PermissionProtectedRoute + +Protects entire pages based on page access permissions. + +```tsx + + + +} /> +``` + +### ProtectedSection + +Protects sections within a page based on action permissions. + +```tsx + + + +``` + +### PermissionButton + +Button that automatically handles permissions. + +```tsx + + Add Product + +``` + +### SettingsSection + +Specific component for settings with built-in permission checks. + +```tsx + + {/* Settings content */} + +``` + +## Permission Hooks + +### usePermissions + +Core hook for checking any permission. + +```tsx +const { hasPermission, hasPageAccess, isAdmin } = usePermissions(); +if (hasPermission('delete:products')) { + // Can delete products +} +``` + +### usePagePermission + +Specialized hook for page-level permissions. + +```tsx +const { canView, canCreate, canEdit, canDelete } = usePagePermission('products'); +if (canEdit()) { + // Can edit products +} +``` + +## Database Schema + +Permissions are stored in the database: +- `permissions` table: Stores all available permissions +- `user_permissions` junction table: Maps permissions to users + +Admin users automatically have all permissions. + +## Common Permission Codes + +| Code | Description | +|------|-------------| +| `access:dashboard` | Access to Dashboard page | +| `access:products` | Access to Products page | +| `create:products` | Create new products | +| `edit:products` | Edit existing products | +| `delete:products` | Delete products | +| `view:users` | View user accounts | +| `edit:users` | Edit user accounts | +| `manage:permissions` | Assign permissions to users | + +## Implementation Examples + +### Page Protection + +In `App.tsx`: +```tsx + + + +} /> +``` + +### Component Level Protection + +```tsx +const { canEdit } = usePagePermission('products'); + +function handleEdit() { + if (!canEdit()) { + toast.error("You don't have permission"); + return; + } + // Edit logic +} +``` + +### UI Element Protection + +```tsx + + Delete + +``` \ No newline at end of file diff --git a/inventory-server/auth/routes.js b/inventory-server/auth/routes.js index f492763..3d7ef62 100644 --- a/inventory-server/auth/routes.js +++ b/inventory-server/auth/routes.js @@ -189,6 +189,14 @@ router.post('/users', authenticate, requirePermission('create:users'), async (re try { const { username, email, password, is_admin, is_active, permissions } = req.body; + console.log("Create user request:", { + username, + email, + is_admin, + is_active, + permissions: permissions || [] + }); + // Validate required fields if (!username || !password) { return res.status(400).json({ error: 'Username and password are required' }); @@ -222,15 +230,52 @@ router.post('/users', authenticate, requirePermission('create:users'), async (re // Assign permissions if provided and not admin if (!is_admin && Array.isArray(permissions) && permissions.length > 0) { - const permissionValues = permissions - .map(permId => `(${userId}, ${parseInt(permId, 10)})`) - .join(','); + console.log("Adding permissions for new user:", userId); + console.log("Permissions received:", permissions); - await client.query(` - INSERT INTO user_permissions (user_id, permission_id) - VALUES ${permissionValues} - ON CONFLICT DO NOTHING - `); + // Check permission format + const permissionIds = permissions.map(p => { + if (typeof p === 'object' && p.id) { + console.log("Permission is an object with ID:", p.id); + return parseInt(p.id, 10); + } else if (typeof p === 'number') { + console.log("Permission is a number:", p); + return p; + } else if (typeof p === 'string' && !isNaN(parseInt(p, 10))) { + console.log("Permission is a string that can be parsed as a number:", p); + return parseInt(p, 10); + } else { + console.log("Unknown permission format:", typeof p, p); + // If it's a permission code, we need to look up the ID + return null; + } + }).filter(id => id !== null); + + console.log("Filtered permission IDs:", permissionIds); + + if (permissionIds.length > 0) { + const permissionValues = permissionIds + .map(permId => `(${userId}, ${permId})`) + .join(','); + + console.log("Inserting permission values:", permissionValues); + + try { + await client.query(` + INSERT INTO user_permissions (user_id, permission_id) + VALUES ${permissionValues} + ON CONFLICT DO NOTHING + `); + console.log("Successfully inserted permissions for new user:", userId); + } catch (err) { + console.error("Error inserting permissions for new user:", err); + throw err; + } + } else { + console.log("No valid permission IDs found to insert for new user"); + } + } else { + console.log("Not adding permissions: is_admin =", is_admin, "permissions array:", Array.isArray(permissions), "length:", permissions ? permissions.length : 0); } await client.query('COMMIT'); @@ -256,6 +301,15 @@ router.put('/users/:id', authenticate, requirePermission('edit:users'), async (r const userId = req.params.id; const { username, email, password, is_admin, is_active, permissions } = req.body; + console.log("Update user request:", { + userId, + username, + email, + is_admin, + is_active, + permissions: permissions || [] + }); + // Check if user exists const userExists = await client.query( 'SELECT id FROM users WHERE id = $1', @@ -315,25 +369,65 @@ router.put('/users/:id', authenticate, requirePermission('edit:users'), async (r // Update permissions if provided if (Array.isArray(permissions)) { + console.log("Updating permissions for user:", userId); + console.log("Permissions received:", permissions); + // First remove existing permissions await client.query( 'DELETE FROM user_permissions WHERE user_id = $1', [userId] ); + console.log("Deleted existing permissions for user:", userId); // Add new permissions if any and not admin const newIsAdmin = is_admin !== undefined ? is_admin : (await client.query('SELECT is_admin FROM users WHERE id = $1', [userId])).rows[0].is_admin; + console.log("User is admin:", newIsAdmin); + if (!newIsAdmin && permissions.length > 0) { - const permissionValues = permissions - .map(permId => `(${userId}, ${parseInt(permId, 10)})`) - .join(','); + console.log("Adding permissions:", permissions); - await client.query(` - INSERT INTO user_permissions (user_id, permission_id) - VALUES ${permissionValues} - ON CONFLICT DO NOTHING - `); + // Check permission format + const permissionIds = permissions.map(p => { + if (typeof p === 'object' && p.id) { + console.log("Permission is an object with ID:", p.id); + return parseInt(p.id, 10); + } else if (typeof p === 'number') { + console.log("Permission is a number:", p); + return p; + } else if (typeof p === 'string' && !isNaN(parseInt(p, 10))) { + console.log("Permission is a string that can be parsed as a number:", p); + return parseInt(p, 10); + } else { + console.log("Unknown permission format:", typeof p, p); + // If it's a permission code, we need to look up the ID + return null; + } + }).filter(id => id !== null); + + console.log("Filtered permission IDs:", permissionIds); + + if (permissionIds.length > 0) { + const permissionValues = permissionIds + .map(permId => `(${userId}, ${permId})`) + .join(','); + + console.log("Inserting permission values:", permissionValues); + + try { + await client.query(` + INSERT INTO user_permissions (user_id, permission_id) + VALUES ${permissionValues} + ON CONFLICT DO NOTHING + `); + console.log("Successfully inserted permissions for user:", userId); + } catch (err) { + console.error("Error inserting permissions:", err); + throw err; + } + } else { + console.log("No valid permission IDs found to insert"); + } } } diff --git a/inventory/src/App.tsx b/inventory/src/App.tsx index dc4fc52..4609f6a 100644 --- a/inventory/src/App.tsx +++ b/inventory/src/App.tsx @@ -17,6 +17,7 @@ import { Categories } from '@/pages/Categories'; import { Import } from '@/pages/Import'; import { AiValidationDebug } from "@/pages/AiValidationDebug" import { AuthProvider } from './contexts/AuthContext'; +import { Protected } from './components/auth/Protected'; const queryClient = new QueryClient(); @@ -77,16 +78,56 @@ function App() { }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + + + + } /> + + + + } /> + + + + } /> + + + + } /> + + + + } /> + + + + } /> + + + + } /> + + + + } /> + + + + } /> + + + + } /> } /> diff --git a/inventory/src/components/auth/PERMISSIONS.md b/inventory/src/components/auth/PERMISSIONS.md new file mode 100644 index 0000000..254aecf --- /dev/null +++ b/inventory/src/components/auth/PERMISSIONS.md @@ -0,0 +1,104 @@ +# Permission System Documentation + +This document outlines the simplified permission system implemented in the Inventory Manager application. + +## Permission Structure + +Permissions follow this naming convention: + +- Page access: `access:{page_name}` +- Actions: `{action}:{resource}` + +Examples: +- `access:products` - Can access the Products page +- `create:products` - Can create new products +- `edit:users` - Can edit user accounts + +## Permission Component + +### Protected + +The core component that conditionally renders content based on permissions. + +```tsx +No permission

} +> + +
+``` + +Options: +- `permission`: Single permission code (e.g., "create:products") +- `page`: Page name (checks `access:{page}` permission) +- `resource` + `action`: Resource and action (checks `{action}:{resource}` permission) +- `adminOnly`: For admin-only sections +- `fallback`: Content to show if permission check fails + +### RequireAuth + +Used for basic authentication checks (is user logged in?). + +```tsx + + + +}> + {/* Protected routes */} + +``` + +## Common Permission Codes + +| Code | Description | +|------|-------------| +| `access:dashboard` | Access to Dashboard page | +| `access:products` | Access to Products page | +| `create:products` | Create new products | +| `edit:products` | Edit existing products | +| `delete:products` | Delete products | +| `view:users` | View user accounts | +| `edit:users` | Edit user accounts | +| `manage:permissions` | Assign permissions to users | + +## Implementation Examples + +### Page Protection + +In `App.tsx`: +```tsx +}> + + +} /> +``` + +### Component Level Protection + +```tsx + +
+ {/* Form fields */} + +
+
+``` + +### Button Protection + +```tsx + + +// With Protected component + + + +``` \ No newline at end of file diff --git a/inventory/src/components/auth/Protected.tsx b/inventory/src/components/auth/Protected.tsx new file mode 100644 index 0000000..7244719 --- /dev/null +++ b/inventory/src/components/auth/Protected.tsx @@ -0,0 +1,82 @@ +import { ReactNode, useContext } from "react"; +import { AuthContext } from "@/contexts/AuthContext"; + +interface ProtectedProps { + // For specific permission code + permission?: string; + + // For page access permission format: access:{page} + page?: string; + + // For action permission format: {action}:{resource} + resource?: string; + action?: "view" | "create" | "edit" | "delete" | string; + + // For admin-only access + adminOnly?: boolean; + + // Content to render if permission check passes + children: ReactNode; + + // Optional fallback content + fallback?: ReactNode; +} + +/** + * A simplified component that conditionally renders content based on user permissions + */ +export function Protected({ + permission, + page, + resource, + action, + adminOnly, + children, + fallback = null +}: ProtectedProps) { + const { user } = useContext(AuthContext); + + // If user isn't loaded yet, don't render anything + if (!user) { + return null; + } + + // Admin check - admins always have access to everything + if (user.is_admin) { + return <>{children}; + } + + // Admin-only check + if (adminOnly) { + return <>{fallback}; + } + + // Check permissions array exists + if (!user.permissions) { + return <>{fallback}; + } + + // Page access check (access:page) + if (page) { + const pagePermission = `access:${page.toLowerCase()}`; + if (!user.permissions.includes(pagePermission)) { + return <>{fallback}; + } + } + + // Resource action check (action:resource) + if (resource && action) { + const resourcePermission = `${action}:${resource.toLowerCase()}`; + if (!user.permissions.includes(resourcePermission)) { + return <>{fallback}; + } + } + + // Single permission check + if (permission && !user.permissions.includes(permission)) { + return <>{fallback}; + } + + // If all checks pass, render children + return <>{children}; +} \ No newline at end of file diff --git a/inventory/src/components/auth/RequireAuth.tsx b/inventory/src/components/auth/RequireAuth.tsx index 5bc5b55..8d94364 100644 --- a/inventory/src/components/auth/RequireAuth.tsx +++ b/inventory/src/components/auth/RequireAuth.tsx @@ -1,11 +1,30 @@ import { Navigate, useLocation } from "react-router-dom" -import { useContext, useEffect } from "react" +import { useContext, useEffect, useState } 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 { token, user, fetchCurrentUser } = useContext(AuthContext) const location = useLocation() + const [isLoading, setIsLoading] = useState(!!token && !user) + + // This will make sure the user data is loaded the first time + useEffect(() => { + const loadUserData = async () => { + if (token && !user) { + setIsLoading(true) + try { + await fetchCurrentUser() + } catch (error) { + console.error("Failed to fetch user data:", error) + } finally { + setIsLoading(false) + } + } + } + + loadUserData() + }, [token, user, fetchCurrentUser]) // Check if token exists but we're not logged in useEffect(() => { @@ -17,6 +36,11 @@ export function RequireAuth({ children }: { children: React.ReactNode }) { } }, [token, isLoggedIn, fetchCurrentUser]) + // If still loading user data, show nothing yet + if (isLoading) { + return
Loading...
+ } + if (!isLoggedIn) { // Redirect to login with the current path in the redirect parameter return {fallback}; - } - - // Page access check - if (page && !hasPageAccess(page)) { - return <>{fallback}; - } - - // Single permission check - if (permission && !hasPermission(permission)) { - return <>{fallback}; - } - - // Any permissions check - if (anyPermissions && !hasAnyPermission(anyPermissions)) { - return <>{fallback}; - } - - // All permissions check - if (allPermissions && !hasAllPermissions(allPermissions)) { - return <>{fallback}; - } - - // If all checks pass, render children - return <>{children}; -} \ No newline at end of file diff --git a/inventory/src/components/layout/AppSidebar.tsx b/inventory/src/components/layout/AppSidebar.tsx index 2f3c44b..785961e 100644 --- a/inventory/src/components/layout/AppSidebar.tsx +++ b/inventory/src/components/layout/AppSidebar.tsx @@ -24,7 +24,7 @@ import { SidebarSeparator, } from "@/components/ui/sidebar"; import { useLocation, useNavigate, Link } from "react-router-dom"; -import { PermissionGuard } from "@/components/common/PermissionGuard"; +import { Protected } from "@/components/auth/Protected"; const items = [ { @@ -67,7 +67,7 @@ const items = [ title: "Purchase Orders", icon: ClipboardList, url: "/purchase-orders", - permission: "access:purchase-orders" + permission: "access:purchase_orders" }, { title: "Analytics", @@ -107,7 +107,7 @@ export function AppSidebar() { location.pathname === item.url || (item.url !== "/" && location.pathname.startsWith(item.url)); return ( - - + ); })} @@ -137,7 +137,7 @@ export function AppSidebar() { - @@ -155,7 +155,7 @@ export function AppSidebar() { - + diff --git a/inventory/src/components/layout/MainLayout.tsx b/inventory/src/components/layout/MainLayout.tsx index 71fdaa7..140eb08 100644 --- a/inventory/src/components/layout/MainLayout.tsx +++ b/inventory/src/components/layout/MainLayout.tsx @@ -1,7 +1,7 @@ import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; import { AppSidebar } from "./AppSidebar"; import { Outlet } from "react-router-dom"; -import { motion } from "motion/react"; +import { motion } from "framer-motion"; export function MainLayout() { return ( diff --git a/inventory/src/components/settings/UserForm.tsx b/inventory/src/components/settings/UserForm.tsx index e567b1c..5ce6282 100644 --- a/inventory/src/components/settings/UserForm.tsx +++ b/inventory/src/components/settings/UserForm.tsx @@ -16,7 +16,6 @@ import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { PermissionSelector } from "./PermissionSelector"; import { Alert, AlertDescription } from "@/components/ui/alert"; -import { ControllerRenderProps } from "react-hook-form"; interface Permission { id: number; @@ -58,6 +57,32 @@ const userFormSchema = z.object({ type FormValues = z.infer; +// Helper function to get all permission IDs from all categories +const getAllPermissionIds = (permissionCategories: PermissionCategory[]): number[] => { + const allIds: number[] = []; + + if (permissionCategories && permissionCategories.length > 0) { + permissionCategories.forEach(category => { + category.permissions.forEach(permission => { + allIds.push(permission.id); + }); + }); + } + + return allIds; +}; + +// User save data interface (represents the data structure for saving users) +interface UserSaveData { + id?: number; + username: string; + email?: string; + password?: string; + is_admin: boolean; + is_active: boolean; + permissions: Permission[]; +} + export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps) { const [selectedPermissions, setSelectedPermissions] = useState([]); const [formError, setFormError] = useState(null); @@ -76,9 +101,15 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps) // Initialize selected permissions useEffect(() => { - if (user?.permissions && user.permissions.length > 0) { - setSelectedPermissions(user.permissions.map(p => p.id)); + console.log("User permissions:", user?.permissions); + + if (user?.permissions && Array.isArray(user.permissions) && user.permissions.length > 0) { + // Extract IDs from the permissions + const permissionIds = user.permissions.map(p => p.id); + console.log("Setting selected permissions:", permissionIds); + setSelectedPermissions(permissionIds); } else { + console.log("No permissions found or empty permissions array"); setSelectedPermissions([]); } }, [user]); @@ -87,6 +118,7 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps) const onSubmit = (data: FormValues) => { try { setFormError(null); + console.log("Form submitted with permissions:", selectedPermissions); // Validate if (!user && !data.password) { @@ -95,10 +127,10 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps) } // Prepare the data - const userData = { + const userData: UserSaveData = { ...data, id: user?.id, // Include ID if editing existing user - permissions: data.is_admin ? [] : selectedPermissions, + permissions: [] // Initialize with empty array }; // If editing and password is empty, remove it @@ -106,6 +138,30 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps) delete userData.password; } + // Add permissions if not admin + if (!data.is_admin) { + // Find the actual permission objects from selectedPermissions IDs + const selectedPermissionObjects: Permission[] = []; + + if (permissions && permissions.length > 0) { + // Loop through all available permissions to find the selected ones + permissions.forEach(category => { + category.permissions.forEach(permission => { + // If this permission's ID is in the selectedPermissions array, add it + if (selectedPermissions.includes(permission.id)) { + selectedPermissionObjects.push(permission); + } + }); + }); + } + + userData.permissions = selectedPermissionObjects; + } else { + // For admin users, don't send permissions as they're implied + userData.permissions = []; + } + + console.log("Saving user data:", userData); onSave(userData); } catch (error) { const errorMessage = error instanceof Error ? error.message : "An error occurred"; @@ -113,6 +169,12 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps) } }; + // For debugging + console.log("Current form state:", form.getValues()); + console.log("Available permissions categories:", permissions); + console.log("Selected permissions:", selectedPermissions); + console.log("Is admin:", form.watch("is_admin")); + return (
@@ -133,7 +195,7 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps) }) => ( + render={({ field }) => ( Username @@ -147,7 +209,7 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps) }) => ( + render={({ field }) => ( Email @@ -161,7 +223,7 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps) }) => ( + render={({ field }) => ( {user ? "New Password" : "Password"} @@ -185,7 +247,7 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps) }) => ( + render={({ field }) => (
Administrator @@ -206,7 +268,7 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps) }) => ( + render={({ field }) => (
Active @@ -225,12 +287,40 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps) />
- {!form.watch("is_admin") && ( - + {permissions && permissions.length > 0 && ( + <> + {form.watch("is_admin") ? ( +
+

Permissions

+ + + Administrators have access to all permissions by default. Individual permissions cannot be edited for admin users. + + + {}} + disabled={true} + /> +
+ ) : ( + <> + + {selectedPermissions.length === 0 && ( + + + Warning: This user has no permissions selected. They won't be able to access anything. + + + )} + + )} + )}
diff --git a/inventory/src/components/settings/UserList.tsx b/inventory/src/components/settings/UserList.tsx index 17bbf97..1b4d479 100644 --- a/inventory/src/components/settings/UserList.tsx +++ b/inventory/src/components/settings/UserList.tsx @@ -20,6 +20,8 @@ interface UserListProps { } export function UserList({ users, onEdit, onDelete }: UserListProps) { + console.log("Rendering user list with users:", users); + if (users.length === 0) { return (
diff --git a/inventory/src/components/settings/UserManagement.tsx b/inventory/src/components/settings/UserManagement.tsx index 151b307..0db495c 100644 --- a/inventory/src/components/settings/UserManagement.tsx +++ b/inventory/src/components/settings/UserManagement.tsx @@ -6,7 +6,6 @@ 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 { @@ -33,8 +32,7 @@ interface PermissionCategory { } export function UserManagement() { - const { token, fetchCurrentUser } = useContext(AuthContext); - const { hasPermission } = usePermissions(); + const { token, fetchCurrentUser, user } = useContext(AuthContext); const [users, setUsers] = useState([]); const [selectedUser, setSelectedUser] = useState(null); const [isAddingUser, setIsAddingUser] = useState(false); @@ -137,10 +135,47 @@ export function UserManagement() { }); if (!response.ok) { - throw new Error('Failed to fetch user details'); + if (response.status === 401) { + throw new Error('Authentication failed. Please log in again.'); + } else if (response.status === 403) { + throw new Error('You don\'t have permission to edit users.'); + } else { + throw new Error(`Failed to fetch user details (${response.status})`); + } } const userData = await response.json(); + console.log("Fetched user data for editing:", userData); + + // Ensure permissions is always an array + if (!userData.permissions) { + userData.permissions = []; + } + + // Make sure the permissions are in the right format for the form + // The server might send either an array of permission objects or just permission codes + if (userData.permissions && userData.permissions.length > 0) { + // Check if permissions are objects with id property + if (typeof userData.permissions[0] === 'string') { + // If we just have permission codes, we need to convert them to objects with ids + // by looking them up in the permissions data + const permissionObjects = []; + + // Go through each permission category + for (const category of permissions) { + // For each permission in the category + for (const permission of category.permissions) { + // If this permission's code is in the user's permission codes + if (userData.permissions.includes(permission.code)) { + permissionObjects.push(permission); + } + } + } + + userData.permissions = permissionObjects; + } + } + setSelectedUser(userData); setIsAddingUser(false); } catch (err) { @@ -155,38 +190,68 @@ export function UserManagement() { }; const handleSaveUser = async (userData: any) => { + console.log("Saving user data:", userData); + + // Format permissions for the API - convert from permission objects to IDs + let formattedUserData = { ...userData }; + + if (userData.permissions && Array.isArray(userData.permissions)) { + // Check if permissions are objects (from the form) and convert to IDs for the API + if (userData.permissions.length > 0 && typeof userData.permissions[0] === 'object') { + // The backend expects permission IDs, not just the code strings + formattedUserData.permissions = userData.permissions.map(p => p.id); + } + } + + console.log("Formatted user data for API:", formattedUserData); + + setLoading(true); + try { - setLoading(true); - + // Use PUT for updating, POST for creating + const method = userData.id ? 'PUT' : 'POST'; const endpoint = userData.id ? `${config.authUrl}/users/${userData.id}` : `${config.authUrl}/users`; - const method = userData.id ? 'PUT' : 'POST'; + console.log(`${method} request to ${endpoint}`); const response = await fetch(endpoint, { method, headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` }, - body: JSON.stringify(userData) + body: JSON.stringify(formattedUserData) }); - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.error || 'Failed to save user'); + let responseData; + + try { + responseData = await response.json(); + } catch (e) { + console.error("Error parsing response JSON:", e); + throw new Error("Invalid response from server"); } - // Refresh user list after a successful save - await fetchData(); + if (!response.ok) { + console.error("Error response from server:", responseData); + throw new Error(responseData.error || responseData.message || `Failed to save user (${response.status})`); + } - // Reset form state + console.log("Server response after saving user:", responseData); + + // Reset the form state setSelectedUser(null); setIsAddingUser(false); + + // Refresh the user list + fetchData(); + } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Failed to save user'; setError(errorMessage); + } finally { setLoading(false); } }; diff --git a/inventory/src/contexts/AuthContext.tsx b/inventory/src/contexts/AuthContext.tsx index bd2c88a..621aeff 100644 --- a/inventory/src/contexts/AuthContext.tsx +++ b/inventory/src/contexts/AuthContext.tsx @@ -64,6 +64,9 @@ export function AuthProvider({ children }: { children: ReactNode }) { } const userData = await response.json(); + console.log("Fetched current user data:", userData); + console.log("User permissions:", userData.permissions); + setUser(userData); // Ensure we have the sessionStorage isLoggedIn flag set sessionStorage.setItem('isLoggedIn', 'true'); @@ -113,6 +116,8 @@ export function AuthProvider({ children }: { children: ReactNode }) { } const data = await response.json(); + console.log("Login successful, received data:", data); + console.log("User permissions:", data.user?.permissions); localStorage.setItem('token', data.token); sessionStorage.setItem('isLoggedIn', 'true'); diff --git a/inventory/src/hooks/usePagePermission.ts b/inventory/src/hooks/usePagePermission.ts deleted file mode 100644 index 87c67ae..0000000 --- a/inventory/src/hooks/usePagePermission.ts +++ /dev/null @@ -1,101 +0,0 @@ -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(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; -} \ No newline at end of file diff --git a/inventory/src/hooks/usePermissions.ts b/inventory/src/hooks/usePermissions.ts deleted file mode 100644 index 82ad1a0..0000000 --- a/inventory/src/hooks/usePermissions.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { useContext, useCallback } from "react"; -import { AuthContext } from "@/contexts/AuthContext"; - -/** - * Hook for checking user permissions - * @returns Functions for checking permissions - */ -export function usePermissions() { - const { user } = useContext(AuthContext); - - /** - * Check if the user has a specific permission - * @param permissionCode The permission code to check - * @returns Whether the user has the permission - */ - const hasPermission = useCallback((permissionCode: string): boolean => { - // If not authenticated or no permissions, return false - if (!user || !user.permissions) { - return false; - } - - // Admin users have all permissions - if (user.is_admin) { - return true; - } - - // Check specific permission - return user.permissions.includes(permissionCode); - }, [user]); - - /** - * Check if the user has access to a specific page - * @param pageName The page name (e.g., 'products', 'settings') - * @returns Whether the user has access to the page - */ - const hasPageAccess = useCallback((pageName: string): boolean => { - return hasPermission(`access:${pageName.toLowerCase()}`); - }, [hasPermission]); - - /** - * Check if the user has ANY of the specified permissions - * @param permissionCodes Array of permission codes to check - * @returns Whether the user has any of the permissions - */ - const hasAnyPermission = useCallback((permissionCodes: string[]): boolean => { - // If admin or no permissions to check, return true - if (!permissionCodes.length || (user && user.is_admin)) { - return true; - } - - return permissionCodes.some(code => hasPermission(code)); - }, [user, hasPermission]); - - /** - * Check if the user has ALL of the specified permissions - * @param permissionCodes Array of permission codes to check - * @returns Whether the user has all the permissions - */ - const hasAllPermissions = useCallback((permissionCodes: string[]): boolean => { - // If no permissions to check, return true - if (!permissionCodes.length) { - return true; - } - - // If admin, return true - if (user && user.is_admin) { - return true; - } - - return permissionCodes.every(code => hasPermission(code)); - }, [user, hasPermission]); - - return { - hasPermission, - hasPageAccess, - hasAnyPermission, - hasAllPermissions, - isAdmin: user?.is_admin || false - }; -} \ No newline at end of file diff --git a/inventory/src/pages/Login.tsx b/inventory/src/pages/Login.tsx index be2da22..665037f 100644 --- a/inventory/src/pages/Login.tsx +++ b/inventory/src/pages/Login.tsx @@ -1,13 +1,12 @@ -import { useState } from "react"; +import { useState, useContext } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { toast } from "sonner"; -import config from "../config"; import { Loader2, Box } from "lucide-react"; -import { motion } from "motion/react"; - +import { motion } from "framer-motion"; +import { AuthContext } from "@/contexts/AuthContext"; export function Login() { const [username, setUsername] = useState(""); @@ -15,59 +14,22 @@ export function Login() { const [isLoading, setIsLoading] = useState(false); const navigate = useNavigate(); const [searchParams] = useSearchParams(); + const { login } = useContext(AuthContext); const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); try { - const url = `${config.authUrl}/login`; - console.log("Making login request:", { - url, - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: { username, password }, - config, - }); - - const response = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ username, password }), - credentials: "include", - }); - - console.log("Login response status:", response.status); - - if (!response.ok) { - const data = await response - .json() - .catch(() => ({ error: "Failed to parse error response" })); - console.error("Login failed:", data); - throw new Error(data.error || "Login failed"); - } - - const data = await response.json(); - console.log("Login successful:", data); - - localStorage.setItem("token", data.token); - sessionStorage.setItem("isLoggedIn", "true"); - toast.success("Successfully logged in"); - - // Get the redirect URL from the URL parameters, defaulting to "/" - const redirectTo = searchParams.get("redirect") || "/" + await login(username, password); - // Navigate to the redirect URL after successful login - navigate(redirectTo) + // Login successful, redirect to the requested page or home + const redirectTo = searchParams.get("redirect") || "/"; + navigate(redirectTo); } catch (error) { + const message = error instanceof Error ? error.message : "Login failed"; + toast.error(message); console.error("Login error:", error); - toast.error( - error instanceof Error ? error.message : "An unexpected error occurred" - ); } finally { setIsLoading(false); } diff --git a/inventory/src/pages/Settings.tsx b/inventory/src/pages/Settings.tsx index 3573075..48cabac 100644 --- a/inventory/src/pages/Settings.tsx +++ b/inventory/src/pages/Settings.tsx @@ -5,25 +5,11 @@ import { PerformanceMetrics } from "@/components/settings/PerformanceMetrics"; 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 { motion } from 'framer-motion'; import { Alert, AlertDescription } from "@/components/ui/alert"; -import { usePagePermission } from "@/hooks/usePagePermission"; -import { useEffect } from "react"; +import { Protected } from "@/components/auth/Protected"; 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 (
@@ -37,20 +23,22 @@ export function Settings() { Performance Metrics - - Calculation Settings - + + + Calculation Settings + + Template Management - User Management - + @@ -66,7 +54,18 @@ export function Settings() { - + + + You don't have permission to access Calculation Settings. + + + } + > + + @@ -74,8 +73,8 @@ export function Settings() { - @@ -85,7 +84,7 @@ export function Settings() { } > - +