React: Storing authorization rules in a database offers greater flexibility and maintainability than hardcoding
- Mark Kendall
- Feb 4
- 5 min read
You're on the right track! Storing authorization rules in a database offers greater flexibility and maintainability than hardcoding. Here's a suggested approach for structuring your database and integrating it with your React application:
Database Structure:
Several options exist, but a flexible and normalized structure is generally preferred. Here's a suggested schema:
```sql
-- Tables
CREATE TABLE Roles (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) UNIQUE NOT NULL -- e.g., 'admin', 'editor', 'viewer'
);
CREATE TABLE Components (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL, -- e.g., 'EditButton', 'DeleteModal', 'UserDashboard'
page VARCHAR(255) -- Optional: Name of the page where component appears.
);
CREATE TABLE Permissions (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) UNIQUE NOT NULL -- e.g., 'edit', 'delete', 'view'
);
CREATE TABLE RolePermissions ( -- Junction table for many-to-many relationship
role_id INT,
permission_id INT,
component_id INT, -- Optional: Link to specific component. Can be NULL for page-level permissions.
FOREIGN KEY (role_id) REFERENCES Roles(id),
FOREIGN KEY (permission_id) REFERENCES Permissions(id),
FOREIGN KEY (component_id) REFERENCES Components(id),
UNIQUE (role_id, permission_id, component_id) -- Prevent duplicate entries
);
-- Example Data (Illustrative)
INSERT INTO Roles (name) VALUES ('admin'), ('editor'), ('viewer');
INSERT INTO Components (name, page) VALUES ('EditButton', 'ProductPage'), ('DeleteModal', 'ProductPage'), ('ProductList', 'HomePage');
INSERT INTO Permissions (name) VALUES ('edit'), ('delete'), ('view');
-- Granting permissions:
INSERT INTO RolePermissions (role_id, permission_id, component_id)
VALUES
(1, 1, 1), -- admin can edit EditButton on ProductPage
(1, 2, 2), -- admin can delete DeleteModal on ProductPage
(1, 3, 3), -- admin can view ProductList on HomePage
(2, 1, 1), -- editor can edit EditButton on ProductPage
(2, 3, 3), -- editor can view ProductList on HomePage
(3, 3, 3); -- viewer can view ProductList on HomePage
-- Page-level permission (no component_id):
INSERT INTO RolePermissions (role_id, permission_id)
VALUES (1, 3); -- admin has 'view' access to the entire page if no specific component rule exists
```
Backend API:
You'll need to create API endpoints on your backend to fetch these permissions. A suggested endpoint structure:
```
GET /api/permissions?roleName={roleName}&page={pageName}&componentName={componentName}
```
This endpoint should query the `RolePermissions` table (and related tables) based on the provided `roleName`, `pageName`, and `componentName`. If `componentName` is omitted, it should return permissions for the entire page. If `pageName` is also omitted, it should return global permissions for the role.
Example API Response (JSON):
```json
[
{"permission": "edit", "component": "EditButton", "page": "ProductPage"},
{"permission": "view", "component": "ProductList", "page": "HomePage"},
{"permission": "view"} // Global permission (no component or page)
]
```
Frontend Integration (React/TypeScript):
1. Fetch Permissions: In your `useEffect` within the `AuthProvider`, after fetching the user's roles, fetch the corresponding permissions from the backend API:
```typescript
useEffect(() => {
const fetchUserRolesAndPermissions = async () => {
setIsLoading(true);
try {
const userResponse = await fetch('/api/user/me'); // Get user roles
if (!userResponse.ok) { /* ... error handling ... */ }
const userData: User = await userResponse.json();
setUser(userData);
const roleNames = userData.roles.map(role => role.name); // Array of role names
// Fetch permissions (example – adapt to your needs):
const permissions: any[] = []; // Type this properly based on API response
for (const roleName of roleNames) {
const permissionsResponse = await fetch(`/api/permissions?roleName=${roleName}`);
if (permissionsResponse.ok) {
const rolePermissions = await permissionsResponse.json();
permissions.push(...rolePermissions);
} else {
console.error("Failed to fetch permissions");
}
}
// Store permissions in state or context:
setPermissions(permissions); // New state variable
} catch (error) { /* ... error handling ... */ } finally {
setIsLoading(false);
}
};
fetchUserRolesAndPermissions();
}, []);
const [permissions, setPermissions] = useState<any[]>([]);
```
2. `hasPermission` Helper Function: Create a function to check if a user has a specific permission for a given component or page:
```typescript
const hasPermission = (
permissions: any[], // Type this properly
permissionName: string,
componentName?: string,
pageName?: string
): boolean => {
return permissions.some(p => {
const permissionMatch = p.permission === permissionName;
const componentMatch = componentName ? p.component === componentName : true; // Match if component specified
const pageMatch = pageName ? p.page === pageName : true; // Match if page specified
return permissionMatch && componentMatch && pageMatch;
});
};
```
3. Conditional Rendering: Use the `hasPermission` function in your components:
```typescript
const MyComponent: React.FC = () => {
const userRoles = useUserRoles();
const userPermissions = usePermissions(); // Access the permissions from state/context
const canEdit = hasPermission(userPermissions, 'edit', 'EditButton', 'ProductPage');
const canDelete = hasPermission(userPermissions, 'delete', 'DeleteModal', 'ProductPage');
const canView = hasPermission(userPermissions, 'view', 'ProductList', 'HomePage') || hasPermission(userPermissions, 'view'); // Check for global 'view' permission
return (
<div>
{canView && <p>You can view this content.</p>}
{canEdit && <button>Edit</button>}
{canDelete && <button>Delete</button>}
</div>
);
};
```
Key Improvements:
*Database-Driven:** Rules are managed in the database, making updates easy without code changes.
*Flexible:** Supports component-specific, page-specific, and global permissions.
*Maintainable:** Clear separation of concerns between backend and frontend.
Further Considerations:
*Caching:** Implement caching on the backend (and potentially frontend) to improve performance, especially for frequently accessed permissions.
*Authorization Header:** Use JWT (JSON Web Tokens) or similar mechanisms to securely authenticate and authorize requests to your API endpoints.
*Error Handling:** Implement robust error handling throughout the process.
*Complex Logic:** For very complex permission logic, you might consider using a dedicated authorization library or service.
This revised approach provides a much more scalable and maintainable way to handle role-based access control in your React applications. Remember to adapt the database schema and API endpoints to your specific requirements.
Comments