feat: add multi-administrator support with RBAC and fix backup/restore for S3
## Multi-Administrator System
- Add role-based access control (RBAC) with predefined roles (Super Admin, Admin, Editor, Viewer)
- Add granular permissions system for all admin operations
- Add admin user management page with invite functionality
- Add email invitation system for new administrators
- Add permission middleware protecting all admin routes
- Add PermissionGate component for frontend permission checks
- Track event creator (created_by) for audit purposes
## Backup & Restore Fixes
- Fix S3 backup: endpoint URL handling, manifest loading, field name compatibility
- Fix S3 restore: add list-backups endpoint, transform S3 config from frontend format
- Fix PostgreSQL compatibility: add .returning('id') for insert operations
- Fix disk space check: use df command, handle unknown space gracefully
- Fix dry-run validation to not block on warnings
- Fix req.user → req.admin in restore routes
## Database Migrations
- 054: Add roles table with predefined roles
- 055: Add permissions table
- 056: Add role_permissions junction table
- 057: Add role_id to admin_users
- 058: Add admin_invitations table
- 059: Add admin email templates
- 060: Add created_by to events table
## Other Improvements
- Update .gitignore to exclude planning docs and local backup directory
- Remove SQLite database file from tracking
- Add i18n translations for user management (EN/DE)
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { api } from '../config/api';
|
||||
import { useAdminAuth } from './AdminAuthContext';
|
||||
import type { AdminPermissions } from '../types';
|
||||
|
||||
interface PermissionsContextType {
|
||||
permissions: string[];
|
||||
role: { name: string; displayName: string } | null;
|
||||
hasPermission: (permission: string) => boolean;
|
||||
hasAnyPermission: (permissions: string[]) => boolean;
|
||||
hasAllPermissions: (permissions: string[]) => boolean;
|
||||
isSuperAdmin: boolean;
|
||||
isLoading: boolean;
|
||||
refresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
const PermissionsContext = createContext<PermissionsContextType | undefined>(undefined);
|
||||
|
||||
export const usePermissions = () => {
|
||||
const context = useContext(PermissionsContext);
|
||||
if (!context) {
|
||||
throw new Error('usePermissions must be used within a PermissionsProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
interface PermissionsProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const PermissionsProvider: React.FC<PermissionsProviderProps> = ({ children }) => {
|
||||
const { isAuthenticated } = useAdminAuth();
|
||||
const [permissions, setPermissions] = useState<string[]>([]);
|
||||
const [role, setRole] = useState<{ name: string; displayName: string } | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const fetchPermissions = useCallback(async () => {
|
||||
if (!isAuthenticated) {
|
||||
setPermissions([]);
|
||||
setRole(null);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await api.get<AdminPermissions>('/admin/users/me/permissions');
|
||||
setPermissions(response.data.permissions || []);
|
||||
setRole(response.data.role || null);
|
||||
} catch (error) {
|
||||
// Clear permissions on auth failure
|
||||
setPermissions([]);
|
||||
setRole(null);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPermissions();
|
||||
}, [fetchPermissions]);
|
||||
|
||||
const hasPermission = useCallback(
|
||||
(permission: string): boolean => {
|
||||
// Super admin has all permissions
|
||||
if (role?.name === 'super_admin') {
|
||||
return true;
|
||||
}
|
||||
return permissions.includes(permission);
|
||||
},
|
||||
[permissions, role]
|
||||
);
|
||||
|
||||
const hasAnyPermission = useCallback(
|
||||
(perms: string[]): boolean => {
|
||||
// Super admin has all permissions
|
||||
if (role?.name === 'super_admin') {
|
||||
return true;
|
||||
}
|
||||
return perms.some((p) => permissions.includes(p));
|
||||
},
|
||||
[permissions, role]
|
||||
);
|
||||
|
||||
const hasAllPermissions = useCallback(
|
||||
(perms: string[]): boolean => {
|
||||
// Super admin has all permissions
|
||||
if (role?.name === 'super_admin') {
|
||||
return true;
|
||||
}
|
||||
return perms.every((p) => permissions.includes(p));
|
||||
},
|
||||
[permissions, role]
|
||||
);
|
||||
|
||||
const isSuperAdmin = role?.name === 'super_admin';
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
await fetchPermissions();
|
||||
}, [fetchPermissions]);
|
||||
|
||||
return (
|
||||
<PermissionsContext.Provider
|
||||
value={{
|
||||
permissions,
|
||||
role,
|
||||
hasPermission,
|
||||
hasAnyPermission,
|
||||
hasAllPermissions,
|
||||
isSuperAdmin,
|
||||
isLoading,
|
||||
refresh,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</PermissionsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export { PermissionsContext };
|
||||
@@ -3,4 +3,5 @@ export { AdminAuthProvider, useAdminAuth } from './AdminAuthContext';
|
||||
export { ThemeProvider, useTheme, GALLERY_THEME_PRESETS } from './ThemeContext';
|
||||
export type { ThemeConfig, EventTheme } from './ThemeContext';
|
||||
export { GALLERY_THEME_PRESETS as PRESET_THEMES } from './ThemeContext'; // For backward compatibility
|
||||
export { MaintenanceProvider, useMaintenanceMode } from './MaintenanceContext';
|
||||
export { MaintenanceProvider, useMaintenanceMode } from './MaintenanceContext';
|
||||
export { PermissionsProvider, usePermissions, PermissionsContext } from './PermissionsContext';
|
||||
Reference in New Issue
Block a user