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:
Paul Nothaft
2026-01-07 17:10:46 +01:00
parent b706eeb5d3
commit 892e47d017
65 changed files with 4854 additions and 493 deletions
@@ -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 };
+2 -1
View File
@@ -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';