More permissions setup, simplify to one component
This commit is contained in:
172
docs/PERMISSIONS.md
Normal file
172
docs/PERMISSIONS.md
Normal file
@@ -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
|
||||||
|
<PermissionGuard
|
||||||
|
permission="create:products"
|
||||||
|
fallback={<p>No permission</p>}
|
||||||
|
>
|
||||||
|
<button>Create Product</button>
|
||||||
|
</PermissionGuard>
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
<Route path="/products" element={
|
||||||
|
<PermissionProtectedRoute page="products">
|
||||||
|
<Products />
|
||||||
|
</PermissionProtectedRoute>
|
||||||
|
} />
|
||||||
|
```
|
||||||
|
|
||||||
|
### ProtectedSection
|
||||||
|
|
||||||
|
Protects sections within a page based on action permissions.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<ProtectedSection page="products" action="create">
|
||||||
|
<button>Add Product</button>
|
||||||
|
</ProtectedSection>
|
||||||
|
```
|
||||||
|
|
||||||
|
### PermissionButton
|
||||||
|
|
||||||
|
Button that automatically handles permissions.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<PermissionButton
|
||||||
|
page="products"
|
||||||
|
action="create"
|
||||||
|
onClick={handleCreateProduct}
|
||||||
|
>
|
||||||
|
Add Product
|
||||||
|
</PermissionButton>
|
||||||
|
```
|
||||||
|
|
||||||
|
### SettingsSection
|
||||||
|
|
||||||
|
Specific component for settings with built-in permission checks.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<SettingsSection
|
||||||
|
title="System Settings"
|
||||||
|
description="Configure global settings"
|
||||||
|
permission="edit:system_settings"
|
||||||
|
>
|
||||||
|
{/* Settings content */}
|
||||||
|
</SettingsSection>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
<Route path="/products" element={
|
||||||
|
<PermissionProtectedRoute page="products">
|
||||||
|
<Products />
|
||||||
|
</PermissionProtectedRoute>
|
||||||
|
} />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
|
<PermissionButton
|
||||||
|
page="products"
|
||||||
|
action="delete"
|
||||||
|
onClick={handleDelete}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</PermissionButton>
|
||||||
|
```
|
||||||
@@ -189,6 +189,14 @@ router.post('/users', authenticate, requirePermission('create:users'), async (re
|
|||||||
try {
|
try {
|
||||||
const { username, email, password, is_admin, is_active, permissions } = req.body;
|
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
|
// Validate required fields
|
||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
return res.status(400).json({ error: 'Username and password are required' });
|
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
|
// Assign permissions if provided and not admin
|
||||||
if (!is_admin && Array.isArray(permissions) && permissions.length > 0) {
|
if (!is_admin && Array.isArray(permissions) && permissions.length > 0) {
|
||||||
const permissionValues = permissions
|
console.log("Adding permissions for new user:", userId);
|
||||||
.map(permId => `(${userId}, ${parseInt(permId, 10)})`)
|
console.log("Permissions received:", permissions);
|
||||||
|
|
||||||
|
// 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(',');
|
.join(',');
|
||||||
|
|
||||||
|
console.log("Inserting permission values:", permissionValues);
|
||||||
|
|
||||||
|
try {
|
||||||
await client.query(`
|
await client.query(`
|
||||||
INSERT INTO user_permissions (user_id, permission_id)
|
INSERT INTO user_permissions (user_id, permission_id)
|
||||||
VALUES ${permissionValues}
|
VALUES ${permissionValues}
|
||||||
ON CONFLICT DO NOTHING
|
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');
|
await client.query('COMMIT');
|
||||||
@@ -256,6 +301,15 @@ router.put('/users/:id', authenticate, requirePermission('edit:users'), async (r
|
|||||||
const userId = req.params.id;
|
const userId = req.params.id;
|
||||||
const { username, email, password, is_admin, is_active, permissions } = req.body;
|
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
|
// Check if user exists
|
||||||
const userExists = await client.query(
|
const userExists = await client.query(
|
||||||
'SELECT id FROM users WHERE id = $1',
|
'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
|
// Update permissions if provided
|
||||||
if (Array.isArray(permissions)) {
|
if (Array.isArray(permissions)) {
|
||||||
|
console.log("Updating permissions for user:", userId);
|
||||||
|
console.log("Permissions received:", permissions);
|
||||||
|
|
||||||
// First remove existing permissions
|
// First remove existing permissions
|
||||||
await client.query(
|
await client.query(
|
||||||
'DELETE FROM user_permissions WHERE user_id = $1',
|
'DELETE FROM user_permissions WHERE user_id = $1',
|
||||||
[userId]
|
[userId]
|
||||||
);
|
);
|
||||||
|
console.log("Deleted existing permissions for user:", userId);
|
||||||
|
|
||||||
// Add new permissions if any and not admin
|
// 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;
|
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) {
|
if (!newIsAdmin && permissions.length > 0) {
|
||||||
const permissionValues = permissions
|
console.log("Adding permissions:", permissions);
|
||||||
.map(permId => `(${userId}, ${parseInt(permId, 10)})`)
|
|
||||||
|
// 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(',');
|
.join(',');
|
||||||
|
|
||||||
|
console.log("Inserting permission values:", permissionValues);
|
||||||
|
|
||||||
|
try {
|
||||||
await client.query(`
|
await client.query(`
|
||||||
INSERT INTO user_permissions (user_id, permission_id)
|
INSERT INTO user_permissions (user_id, permission_id)
|
||||||
VALUES ${permissionValues}
|
VALUES ${permissionValues}
|
||||||
ON CONFLICT DO NOTHING
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ 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';
|
import { AuthProvider } from './contexts/AuthContext';
|
||||||
|
import { Protected } from './components/auth/Protected';
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
@@ -77,16 +78,56 @@ function App() {
|
|||||||
<MainLayout />
|
<MainLayout />
|
||||||
</RequireAuth>
|
</RequireAuth>
|
||||||
}>
|
}>
|
||||||
<Route path="/" element={<Dashboard />} />
|
<Route path="/" element={
|
||||||
<Route path="/products" element={<Products />} />
|
<Protected page="dashboard">
|
||||||
<Route path="/import" element={<Import />} />
|
<Dashboard />
|
||||||
<Route path="/categories" element={<Categories />} />
|
</Protected>
|
||||||
<Route path="/vendors" element={<Vendors />} />
|
} />
|
||||||
<Route path="/purchase-orders" element={<PurchaseOrders />} />
|
<Route path="/products" element={
|
||||||
<Route path="/analytics" element={<Analytics />} />
|
<Protected page="products">
|
||||||
<Route path="/settings" element={<Settings />} />
|
<Products />
|
||||||
<Route path="/forecasting" element={<Forecasting />} />
|
</Protected>
|
||||||
<Route path="/ai-validation/debug" element={<AiValidationDebug />} />
|
} />
|
||||||
|
<Route path="/import" element={
|
||||||
|
<Protected page="import">
|
||||||
|
<Import />
|
||||||
|
</Protected>
|
||||||
|
} />
|
||||||
|
<Route path="/categories" element={
|
||||||
|
<Protected page="categories">
|
||||||
|
<Categories />
|
||||||
|
</Protected>
|
||||||
|
} />
|
||||||
|
<Route path="/vendors" element={
|
||||||
|
<Protected page="vendors">
|
||||||
|
<Vendors />
|
||||||
|
</Protected>
|
||||||
|
} />
|
||||||
|
<Route path="/purchase-orders" element={
|
||||||
|
<Protected page="purchase_orders">
|
||||||
|
<PurchaseOrders />
|
||||||
|
</Protected>
|
||||||
|
} />
|
||||||
|
<Route path="/analytics" element={
|
||||||
|
<Protected page="analytics">
|
||||||
|
<Analytics />
|
||||||
|
</Protected>
|
||||||
|
} />
|
||||||
|
<Route path="/settings" element={
|
||||||
|
<Protected page="settings">
|
||||||
|
<Settings />
|
||||||
|
</Protected>
|
||||||
|
} />
|
||||||
|
<Route path="/forecasting" element={
|
||||||
|
<Protected page="forecasting">
|
||||||
|
<Forecasting />
|
||||||
|
</Protected>
|
||||||
|
} />
|
||||||
|
<Route path="/ai-validation/debug" element={
|
||||||
|
<Protected page="ai_validation_debug">
|
||||||
|
<AiValidationDebug />
|
||||||
|
</Protected>
|
||||||
|
} />
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
104
inventory/src/components/auth/PERMISSIONS.md
Normal file
104
inventory/src/components/auth/PERMISSIONS.md
Normal file
@@ -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
|
||||||
|
<Protected
|
||||||
|
permission="create:products"
|
||||||
|
fallback={<p>No permission</p>}
|
||||||
|
>
|
||||||
|
<button>Create Product</button>
|
||||||
|
</Protected>
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
<Route element={
|
||||||
|
<RequireAuth>
|
||||||
|
<MainLayout />
|
||||||
|
</RequireAuth>
|
||||||
|
}>
|
||||||
|
{/* Protected routes */}
|
||||||
|
</Route>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
<Route path="/products" element={
|
||||||
|
<Protected page="products" fallback={<Navigate to="/" />}>
|
||||||
|
<Products />
|
||||||
|
</Protected>
|
||||||
|
} />
|
||||||
|
```
|
||||||
|
|
||||||
|
### Component Level Protection
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Protected permission="edit:products">
|
||||||
|
<form>
|
||||||
|
{/* Form fields */}
|
||||||
|
<button type="submit">Save Changes</button>
|
||||||
|
</form>
|
||||||
|
</Protected>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Button Protection
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Button
|
||||||
|
onClick={handleDelete}
|
||||||
|
disabled={!hasPermission('delete:products')}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
// With Protected component
|
||||||
|
<Protected permission="delete:products" fallback={null}>
|
||||||
|
<Button onClick={handleDelete}>Delete</Button>
|
||||||
|
</Protected>
|
||||||
|
```
|
||||||
82
inventory/src/components/auth/Protected.tsx
Normal file
82
inventory/src/components/auth/Protected.tsx
Normal file
@@ -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}</>;
|
||||||
|
}
|
||||||
@@ -1,11 +1,30 @@
|
|||||||
import { Navigate, useLocation } from "react-router-dom"
|
import { Navigate, useLocation } from "react-router-dom"
|
||||||
import { useContext, useEffect } from "react"
|
import { useContext, useEffect, useState } from "react"
|
||||||
import { AuthContext } from "@/contexts/AuthContext"
|
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 { token, user, fetchCurrentUser } = useContext(AuthContext)
|
||||||
const location = useLocation()
|
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
|
// Check if token exists but we're not logged in
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -17,6 +36,11 @@ export function RequireAuth({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
}, [token, isLoggedIn, fetchCurrentUser])
|
}, [token, isLoggedIn, fetchCurrentUser])
|
||||||
|
|
||||||
|
// If still loading user data, show nothing yet
|
||||||
|
if (isLoading) {
|
||||||
|
return <div className="p-8 flex justify-center items-center h-screen">Loading...</div>
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
import { ReactNode } from "react";
|
|
||||||
import { usePermissions } from "@/hooks/usePermissions";
|
|
||||||
|
|
||||||
interface PermissionGuardProps {
|
|
||||||
/**
|
|
||||||
* Permission code required to render children
|
|
||||||
*/
|
|
||||||
permission?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Array of permission codes - if ANY are matched, children will render
|
|
||||||
*/
|
|
||||||
anyPermissions?: string[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Array of permission codes - ALL must match to render children
|
|
||||||
*/
|
|
||||||
allPermissions?: string[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If true, renders children if the user is an admin
|
|
||||||
*/
|
|
||||||
adminOnly?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If true, renders children if the user can access the specified page
|
|
||||||
*/
|
|
||||||
page?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fallback component to render if permissions check fails
|
|
||||||
*/
|
|
||||||
fallback?: ReactNode;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Children to render if permissions check passes
|
|
||||||
*/
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component that conditionally renders its children based on user permissions
|
|
||||||
*/
|
|
||||||
export function PermissionGuard({
|
|
||||||
permission,
|
|
||||||
anyPermissions,
|
|
||||||
allPermissions,
|
|
||||||
adminOnly,
|
|
||||||
page,
|
|
||||||
fallback = null,
|
|
||||||
children
|
|
||||||
}: PermissionGuardProps) {
|
|
||||||
const { hasPermission, hasAnyPermission, hasAllPermissions, hasPageAccess, isAdmin } = usePermissions();
|
|
||||||
|
|
||||||
// Admin check
|
|
||||||
if (adminOnly && !isAdmin) {
|
|
||||||
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}</>;
|
|
||||||
}
|
|
||||||
@@ -24,7 +24,7 @@ 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";
|
import { Protected } from "@/components/auth/Protected";
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
@@ -67,7 +67,7 @@ const items = [
|
|||||||
title: "Purchase Orders",
|
title: "Purchase Orders",
|
||||||
icon: ClipboardList,
|
icon: ClipboardList,
|
||||||
url: "/purchase-orders",
|
url: "/purchase-orders",
|
||||||
permission: "access:purchase-orders"
|
permission: "access:purchase_orders"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Analytics",
|
title: "Analytics",
|
||||||
@@ -107,7 +107,7 @@ 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 (
|
||||||
<PermissionGuard
|
<Protected
|
||||||
key={item.title}
|
key={item.title}
|
||||||
permission={item.permission}
|
permission={item.permission}
|
||||||
fallback={null}
|
fallback={null}
|
||||||
@@ -126,7 +126,7 @@ export function AppSidebar() {
|
|||||||
</Link>
|
</Link>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</PermissionGuard>
|
</Protected>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
@@ -137,7 +137,7 @@ export function AppSidebar() {
|
|||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupContent>
|
<SidebarGroupContent>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<PermissionGuard
|
<Protected
|
||||||
permission="access:settings"
|
permission="access:settings"
|
||||||
fallback={null}
|
fallback={null}
|
||||||
>
|
>
|
||||||
@@ -155,7 +155,7 @@ export function AppSidebar() {
|
|||||||
</Link>
|
</Link>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</PermissionGuard>
|
</Protected>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroupContent>
|
</SidebarGroupContent>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
||||||
import { AppSidebar } from "./AppSidebar";
|
import { AppSidebar } from "./AppSidebar";
|
||||||
import { Outlet } from "react-router-dom";
|
import { Outlet } from "react-router-dom";
|
||||||
import { motion } from "motion/react";
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
export function MainLayout() {
|
export function MainLayout() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { PermissionSelector } from "./PermissionSelector";
|
import { PermissionSelector } from "./PermissionSelector";
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { ControllerRenderProps } from "react-hook-form";
|
|
||||||
|
|
||||||
interface Permission {
|
interface Permission {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -58,6 +57,32 @@ const userFormSchema = z.object({
|
|||||||
|
|
||||||
type FormValues = z.infer<typeof userFormSchema>;
|
type FormValues = z.infer<typeof userFormSchema>;
|
||||||
|
|
||||||
|
// 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) {
|
export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps) {
|
||||||
const [selectedPermissions, setSelectedPermissions] = useState<number[]>([]);
|
const [selectedPermissions, setSelectedPermissions] = useState<number[]>([]);
|
||||||
const [formError, setFormError] = useState<string | null>(null);
|
const [formError, setFormError] = useState<string | null>(null);
|
||||||
@@ -76,9 +101,15 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps)
|
|||||||
|
|
||||||
// Initialize selected permissions
|
// Initialize selected permissions
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user?.permissions && user.permissions.length > 0) {
|
console.log("User permissions:", user?.permissions);
|
||||||
setSelectedPermissions(user.permissions.map(p => p.id));
|
|
||||||
|
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 {
|
} else {
|
||||||
|
console.log("No permissions found or empty permissions array");
|
||||||
setSelectedPermissions([]);
|
setSelectedPermissions([]);
|
||||||
}
|
}
|
||||||
}, [user]);
|
}, [user]);
|
||||||
@@ -87,6 +118,7 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps)
|
|||||||
const onSubmit = (data: FormValues) => {
|
const onSubmit = (data: FormValues) => {
|
||||||
try {
|
try {
|
||||||
setFormError(null);
|
setFormError(null);
|
||||||
|
console.log("Form submitted with permissions:", selectedPermissions);
|
||||||
|
|
||||||
// Validate
|
// Validate
|
||||||
if (!user && !data.password) {
|
if (!user && !data.password) {
|
||||||
@@ -95,10 +127,10 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Prepare the data
|
// Prepare the data
|
||||||
const userData = {
|
const userData: UserSaveData = {
|
||||||
...data,
|
...data,
|
||||||
id: user?.id, // Include ID if editing existing user
|
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
|
// If editing and password is empty, remove it
|
||||||
@@ -106,6 +138,30 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps)
|
|||||||
delete userData.password;
|
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);
|
onSave(userData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : "An error occurred";
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
@@ -133,7 +195,7 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps)
|
|||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="username"
|
name="username"
|
||||||
render={({ field }: { field: ControllerRenderProps<FormValues, "username"> }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Username</FormLabel>
|
<FormLabel>Username</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -147,7 +209,7 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps)
|
|||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="email"
|
name="email"
|
||||||
render={({ field }: { field: ControllerRenderProps<FormValues, "email"> }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Email</FormLabel>
|
<FormLabel>Email</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -161,7 +223,7 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps)
|
|||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="password"
|
name="password"
|
||||||
render={({ field }: { field: ControllerRenderProps<FormValues, "password"> }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{user ? "New Password" : "Password"}</FormLabel>
|
<FormLabel>{user ? "New Password" : "Password"}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -185,7 +247,7 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps)
|
|||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="is_admin"
|
name="is_admin"
|
||||||
render={({ field }: { field: ControllerRenderProps<FormValues, "is_admin"> }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-row items-center justify-between p-4 border rounded-md">
|
<FormItem className="flex flex-row items-center justify-between p-4 border rounded-md">
|
||||||
<div>
|
<div>
|
||||||
<FormLabel>Administrator</FormLabel>
|
<FormLabel>Administrator</FormLabel>
|
||||||
@@ -206,7 +268,7 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps)
|
|||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="is_active"
|
name="is_active"
|
||||||
render={({ field }: { field: ControllerRenderProps<FormValues, "is_active"> }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-row items-center justify-between p-4 border rounded-md">
|
<FormItem className="flex flex-row items-center justify-between p-4 border rounded-md">
|
||||||
<div>
|
<div>
|
||||||
<FormLabel>Active</FormLabel>
|
<FormLabel>Active</FormLabel>
|
||||||
@@ -225,12 +287,40 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps)
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!form.watch("is_admin") && (
|
{permissions && permissions.length > 0 && (
|
||||||
|
<>
|
||||||
|
{form.watch("is_admin") ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium">Permissions</h3>
|
||||||
|
<Alert>
|
||||||
|
<AlertDescription>
|
||||||
|
Administrators have access to all permissions by default. Individual permissions cannot be edited for admin users.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
<PermissionSelector
|
||||||
|
permissionsByCategory={permissions}
|
||||||
|
selectedPermissions={getAllPermissionIds(permissions)}
|
||||||
|
onChange={() => {}}
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<PermissionSelector
|
<PermissionSelector
|
||||||
permissionsByCategory={permissions}
|
permissionsByCategory={permissions}
|
||||||
selectedPermissions={selectedPermissions}
|
selectedPermissions={selectedPermissions}
|
||||||
onChange={setSelectedPermissions}
|
onChange={setSelectedPermissions}
|
||||||
/>
|
/>
|
||||||
|
{selectedPermissions.length === 0 && (
|
||||||
|
<Alert>
|
||||||
|
<AlertDescription>
|
||||||
|
Warning: This user has no permissions selected. They won't be able to access anything.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end space-x-4">
|
<div className="flex justify-end space-x-4">
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ interface UserListProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function UserList({ users, onEdit, onDelete }: UserListProps) {
|
export function UserList({ users, onEdit, onDelete }: UserListProps) {
|
||||||
|
console.log("Rendering user list with users:", users);
|
||||||
|
|
||||||
if (users.length === 0) {
|
if (users.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { UserList } from "./UserList";
|
|||||||
import { UserForm } from "./UserForm";
|
import { UserForm } from "./UserForm";
|
||||||
import config from "@/config";
|
import config from "@/config";
|
||||||
import { AuthContext } from "@/contexts/AuthContext";
|
import { AuthContext } from "@/contexts/AuthContext";
|
||||||
import { usePermissions } from "@/hooks/usePermissions";
|
|
||||||
import { ShieldAlert } from "lucide-react";
|
import { ShieldAlert } from "lucide-react";
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
@@ -33,8 +32,7 @@ interface PermissionCategory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function UserManagement() {
|
export function UserManagement() {
|
||||||
const { token, fetchCurrentUser } = useContext(AuthContext);
|
const { token, fetchCurrentUser, user } = 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);
|
||||||
@@ -137,10 +135,47 @@ export function UserManagement() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
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();
|
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);
|
setSelectedUser(userData);
|
||||||
setIsAddingUser(false);
|
setIsAddingUser(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -155,38 +190,68 @@ export function UserManagement() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveUser = async (userData: any) => {
|
const handleSaveUser = async (userData: any) => {
|
||||||
try {
|
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);
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use PUT for updating, POST for creating
|
||||||
|
const method = userData.id ? 'PUT' : 'POST';
|
||||||
const endpoint = userData.id
|
const endpoint = userData.id
|
||||||
? `${config.authUrl}/users/${userData.id}`
|
? `${config.authUrl}/users/${userData.id}`
|
||||||
: `${config.authUrl}/users`;
|
: `${config.authUrl}/users`;
|
||||||
|
|
||||||
const method = userData.id ? 'PUT' : 'POST';
|
console.log(`${method} request to ${endpoint}`);
|
||||||
|
|
||||||
const response = await fetch(endpoint, {
|
const response = await fetch(endpoint, {
|
||||||
method,
|
method,
|
||||||
headers: {
|
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) {
|
let responseData;
|
||||||
const errorData = await response.json();
|
|
||||||
throw new Error(errorData.error || 'Failed to save user');
|
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
|
if (!response.ok) {
|
||||||
await fetchData();
|
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);
|
setSelectedUser(null);
|
||||||
setIsAddingUser(false);
|
setIsAddingUser(false);
|
||||||
|
|
||||||
|
// Refresh the user list
|
||||||
|
fetchData();
|
||||||
|
|
||||||
} 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);
|
||||||
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userData = await response.json();
|
const userData = await response.json();
|
||||||
|
console.log("Fetched current user data:", userData);
|
||||||
|
console.log("User permissions:", userData.permissions);
|
||||||
|
|
||||||
setUser(userData);
|
setUser(userData);
|
||||||
// Ensure we have the sessionStorage isLoggedIn flag set
|
// Ensure we have the sessionStorage isLoggedIn flag set
|
||||||
sessionStorage.setItem('isLoggedIn', 'true');
|
sessionStorage.setItem('isLoggedIn', 'true');
|
||||||
@@ -113,6 +116,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
console.log("Login successful, received data:", data);
|
||||||
|
console.log("User permissions:", data.user?.permissions);
|
||||||
|
|
||||||
localStorage.setItem('token', data.token);
|
localStorage.setItem('token', data.token);
|
||||||
sessionStorage.setItem('isLoggedIn', 'true');
|
sessionStorage.setItem('isLoggedIn', 'true');
|
||||||
|
|||||||
@@ -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<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;
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
import { useState } from "react";
|
import { useState, useContext } from "react";
|
||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import config from "../config";
|
|
||||||
import { Loader2, Box } from "lucide-react";
|
import { Loader2, Box } from "lucide-react";
|
||||||
import { motion } from "motion/react";
|
import { motion } from "framer-motion";
|
||||||
|
import { AuthContext } from "@/contexts/AuthContext";
|
||||||
|
|
||||||
export function Login() {
|
export function Login() {
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
@@ -15,59 +14,22 @@ export function Login() {
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
const { login } = useContext(AuthContext);
|
||||||
|
|
||||||
const handleLogin = async (e: React.FormEvent) => {
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = `${config.authUrl}/login`;
|
await login(username, password);
|
||||||
console.log("Making login request:", {
|
|
||||||
url,
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: { username, password },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
// Login successful, redirect to the requested page or home
|
||||||
method: "POST",
|
const redirectTo = searchParams.get("redirect") || "/";
|
||||||
headers: {
|
navigate(redirectTo);
|
||||||
"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") || "/"
|
|
||||||
|
|
||||||
// Navigate to the redirect URL after successful login
|
|
||||||
navigate(redirectTo)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "Login failed";
|
||||||
|
toast.error(message);
|
||||||
console.error("Login error:", error);
|
console.error("Login error:", error);
|
||||||
toast.error(
|
|
||||||
error instanceof Error ? error.message : "An unexpected error occurred"
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,25 +5,11 @@ import { PerformanceMetrics } from "@/components/settings/PerformanceMetrics";
|
|||||||
import { CalculationSettings } from "@/components/settings/CalculationSettings";
|
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 'framer-motion';
|
||||||
import { PermissionGuard } from "@/components/common/PermissionGuard";
|
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { usePagePermission } from "@/hooks/usePagePermission";
|
import { Protected } from "@/components/auth/Protected";
|
||||||
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">
|
||||||
@@ -37,20 +23,22 @@ export function Settings() {
|
|||||||
<TabsTrigger value="performance-metrics">
|
<TabsTrigger value="performance-metrics">
|
||||||
Performance Metrics
|
Performance Metrics
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<Protected permission="edit:system_settings">
|
||||||
<TabsTrigger value="calculation-settings">
|
<TabsTrigger value="calculation-settings">
|
||||||
Calculation Settings
|
Calculation Settings
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
</Protected>
|
||||||
<TabsTrigger value="templates">
|
<TabsTrigger value="templates">
|
||||||
Template Management
|
Template Management
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<PermissionGuard
|
<Protected
|
||||||
permission="manage:users"
|
permission="view:users"
|
||||||
fallback={null}
|
fallback={null}
|
||||||
>
|
>
|
||||||
<TabsTrigger value="user-management">
|
<TabsTrigger value="user-management">
|
||||||
User Management
|
User Management
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</PermissionGuard>
|
</Protected>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="data-management">
|
<TabsContent value="data-management">
|
||||||
@@ -66,7 +54,18 @@ export function Settings() {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="calculation-settings">
|
<TabsContent value="calculation-settings">
|
||||||
|
<Protected
|
||||||
|
permission="edit:system_settings"
|
||||||
|
fallback={
|
||||||
|
<Alert>
|
||||||
|
<AlertDescription>
|
||||||
|
You don't have permission to access Calculation Settings.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
>
|
||||||
<CalculationSettings />
|
<CalculationSettings />
|
||||||
|
</Protected>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="templates">
|
<TabsContent value="templates">
|
||||||
@@ -74,8 +73,8 @@ export function Settings() {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="user-management">
|
<TabsContent value="user-management">
|
||||||
<PermissionGuard
|
<Protected
|
||||||
permission="manage:users"
|
permission="view:users"
|
||||||
fallback={
|
fallback={
|
||||||
<Alert>
|
<Alert>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
@@ -85,7 +84,7 @@ export function Settings() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<UserManagement />
|
<UserManagement />
|
||||||
</PermissionGuard>
|
</Protected>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
Reference in New Issue
Block a user