fiixed feature instance role access
This commit is contained in:
parent
2cbee8fe57
commit
8f29bdb270
1 changed files with 220 additions and 1 deletions
|
|
@ -10,12 +10,15 @@
|
|||
* - Mandate-specific roles (mandateId=xyz) - editable permissions
|
||||
*
|
||||
* Each role can be expanded to show/edit its AccessRules via AccessRulesEditor.
|
||||
*
|
||||
* Includes a "Cleanup Duplicates" tool to find and remove duplicate AccessRules.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { useMandateRoles, type Role } from '../../hooks/useMandateRoles';
|
||||
import { useUserMandates, type Mandate } from '../../hooks/useUserMandates';
|
||||
import { AccessRulesEditor } from '../../components/AccessRules';
|
||||
import api from '../../api';
|
||||
import {
|
||||
FaUserShield,
|
||||
FaShieldAlt,
|
||||
|
|
@ -24,10 +27,35 @@ import {
|
|||
FaChevronRight,
|
||||
FaGlobe,
|
||||
FaBuilding,
|
||||
FaFilter
|
||||
FaFilter,
|
||||
FaBroom,
|
||||
FaTimes,
|
||||
FaExclamationTriangle,
|
||||
FaCheckCircle
|
||||
} from 'react-icons/fa';
|
||||
import styles from './Admin.module.css';
|
||||
|
||||
// Types for cleanup result
|
||||
interface DuplicateGroup {
|
||||
roleId: string;
|
||||
context: string;
|
||||
item: string;
|
||||
totalCount: number;
|
||||
keepId: string;
|
||||
deleteCount: number;
|
||||
deleteIds: string[];
|
||||
}
|
||||
|
||||
interface CleanupResult {
|
||||
dryRun: boolean;
|
||||
totalRules: number;
|
||||
uniqueSignatures: number;
|
||||
duplicateGroups: number;
|
||||
duplicateRulesToDelete: number;
|
||||
deletedCount: number;
|
||||
details: DuplicateGroup[];
|
||||
}
|
||||
|
||||
export const AdminMandateRolePermissionsPage: React.FC = () => {
|
||||
const {
|
||||
roles,
|
||||
|
|
@ -44,6 +72,13 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
|
|||
const [scopeFilter, setScopeFilter] = useState<'all' | 'mandate' | 'global'>('all');
|
||||
const [expandedRoleId, setExpandedRoleId] = useState<string | null>(null);
|
||||
|
||||
// Cleanup state
|
||||
const [showCleanupModal, setShowCleanupModal] = useState(false);
|
||||
const [cleanupLoading, setCleanupLoading] = useState(false);
|
||||
const [cleanupResult, setCleanupResult] = useState<CleanupResult | null>(null);
|
||||
const [cleanupError, setCleanupError] = useState<string | null>(null);
|
||||
const [cleanupPhase, setCleanupPhase] = useState<'idle' | 'preview' | 'done'>('idle');
|
||||
|
||||
// Load mandates on mount
|
||||
useEffect(() => {
|
||||
const loadMandates = async () => {
|
||||
|
|
@ -105,6 +140,49 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
// --- Cleanup functions ---
|
||||
const _openCleanupModal = useCallback(async () => {
|
||||
setShowCleanupModal(true);
|
||||
setCleanupError(null);
|
||||
setCleanupResult(null);
|
||||
setCleanupPhase('idle');
|
||||
setCleanupLoading(true);
|
||||
try {
|
||||
const response = await api.post('/api/rbac/cleanup/duplicate-rules?dryRun=true');
|
||||
setCleanupResult(response.data);
|
||||
setCleanupPhase('preview');
|
||||
} catch (err: any) {
|
||||
setCleanupError(err?.response?.data?.detail || err?.message || 'Fehler beim Laden der Duplikate');
|
||||
} finally {
|
||||
setCleanupLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const _executeCleanup = useCallback(async () => {
|
||||
setCleanupLoading(true);
|
||||
setCleanupError(null);
|
||||
try {
|
||||
const response = await api.post('/api/rbac/cleanup/duplicate-rules?dryRun=false');
|
||||
setCleanupResult(response.data);
|
||||
setCleanupPhase('done');
|
||||
// Refresh roles after cleanup
|
||||
if (selectedMandateId) {
|
||||
fetchRoles(selectedMandateId, { scopeFilter });
|
||||
}
|
||||
} catch (err: any) {
|
||||
setCleanupError(err?.response?.data?.detail || err?.message || 'Fehler beim Bereinigen');
|
||||
} finally {
|
||||
setCleanupLoading(false);
|
||||
}
|
||||
}, [selectedMandateId, scopeFilter, fetchRoles]);
|
||||
|
||||
const _closeCleanupModal = useCallback(() => {
|
||||
setShowCleanupModal(false);
|
||||
setCleanupResult(null);
|
||||
setCleanupError(null);
|
||||
setCleanupPhase('idle');
|
||||
}, []);
|
||||
|
||||
// Filter options for scope
|
||||
const scopeOptions = useMemo(() => [
|
||||
{ value: 'all', label: 'Alle Rollen' },
|
||||
|
|
@ -140,6 +218,14 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
|
|||
</p>
|
||||
</div>
|
||||
<div className={styles.headerActions}>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
onClick={_openCleanupModal}
|
||||
disabled={loading}
|
||||
title="Doppelte Regeln finden und bereinigen"
|
||||
>
|
||||
<FaBroom /> Duplikate bereinigen
|
||||
</button>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
onClick={handleRefresh}
|
||||
|
|
@ -256,6 +342,139 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
|
|||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Cleanup Duplicates Modal */}
|
||||
{showCleanupModal && (
|
||||
<div className={styles.modalOverlay} onClick={_closeCleanupModal}>
|
||||
<div className={styles.modal} style={{ maxWidth: '750px' }} onClick={(e) => e.stopPropagation()}>
|
||||
<div className={styles.modalHeader}>
|
||||
<h3 className={styles.modalTitle}>
|
||||
<FaBroom style={{ marginRight: '0.5rem' }} />
|
||||
Doppelte Regeln bereinigen
|
||||
</h3>
|
||||
<button className={styles.modalClose} onClick={_closeCleanupModal}>
|
||||
<FaTimes />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={styles.modalContent}>
|
||||
{/* Loading */}
|
||||
{cleanupLoading && (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>{cleanupPhase === 'idle' ? 'Analysiere Duplikate...' : 'Bereinige Duplikate...'}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error */}
|
||||
{cleanupError && (
|
||||
<div style={{ padding: '1rem', background: '#fed7d7', borderRadius: '6px', color: '#c53030', marginBottom: '1rem' }}>
|
||||
<FaExclamationTriangle style={{ marginRight: '0.5rem' }} />
|
||||
{cleanupError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Results */}
|
||||
{cleanupResult && !cleanupLoading && (
|
||||
<>
|
||||
{/* Summary Cards */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', gap: '0.75rem', marginBottom: '1.25rem' }}>
|
||||
<div style={{ padding: '0.75rem', background: 'var(--bg-secondary)', borderRadius: '8px', textAlign: 'center', border: '1px solid var(--border-color)' }}>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: 'var(--text-primary)' }}>{cleanupResult.totalRules}</div>
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>Regeln total</div>
|
||||
</div>
|
||||
<div style={{ padding: '0.75rem', background: 'var(--bg-secondary)', borderRadius: '8px', textAlign: 'center', border: '1px solid var(--border-color)' }}>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: 'var(--text-primary)' }}>{cleanupResult.uniqueSignatures}</div>
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>Eindeutige Regeln</div>
|
||||
</div>
|
||||
<div style={{ padding: '0.75rem', background: cleanupResult.duplicateGroups > 0 ? '#fff5f5' : '#f0fff4', borderRadius: '8px', textAlign: 'center', border: `1px solid ${cleanupResult.duplicateGroups > 0 ? '#fc8181' : '#9ae6b4'}` }}>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: cleanupResult.duplicateGroups > 0 ? '#c53030' : '#2f855a' }}>{cleanupResult.duplicateGroups}</div>
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>Duplikat-Gruppen</div>
|
||||
</div>
|
||||
<div style={{ padding: '0.75rem', background: cleanupResult.duplicateRulesToDelete > 0 ? '#fff5f5' : '#f0fff4', borderRadius: '8px', textAlign: 'center', border: `1px solid ${cleanupResult.duplicateRulesToDelete > 0 ? '#fc8181' : '#9ae6b4'}` }}>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: cleanupResult.duplicateRulesToDelete > 0 ? '#c53030' : '#2f855a' }}>
|
||||
{cleanupPhase === 'done' ? cleanupResult.deletedCount : cleanupResult.duplicateRulesToDelete}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>
|
||||
{cleanupPhase === 'done' ? 'Geloescht' : 'Zu loeschen'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Message */}
|
||||
{cleanupPhase === 'done' && (
|
||||
<div style={{ padding: '0.75rem 1rem', background: '#f0fff4', borderRadius: '6px', color: '#2f855a', marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem', border: '1px solid #9ae6b4' }}>
|
||||
<FaCheckCircle />
|
||||
<span><strong>{cleanupResult.deletedCount}</strong> doppelte Regeln wurden erfolgreich entfernt.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{cleanupPhase === 'preview' && cleanupResult.duplicateGroups === 0 && (
|
||||
<div style={{ padding: '0.75rem 1rem', background: '#f0fff4', borderRadius: '6px', color: '#2f855a', marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem', border: '1px solid #9ae6b4' }}>
|
||||
<FaCheckCircle />
|
||||
<span>Keine Duplikate gefunden. Alles sauber!</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Details Table */}
|
||||
{cleanupResult.details.length > 0 && (
|
||||
<div style={{ marginTop: '0.5rem' }}>
|
||||
<h4 style={{ fontSize: '0.875rem', fontWeight: 600, marginBottom: '0.5rem', color: 'var(--text-secondary)' }}>
|
||||
Duplikat-Details {cleanupResult.details.length < cleanupResult.duplicateGroups && `(${cleanupResult.details.length} von ${cleanupResult.duplicateGroups})`}
|
||||
</h4>
|
||||
<div style={{ maxHeight: '300px', overflowY: 'auto', border: '1px solid var(--border-color)', borderRadius: '6px' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.8125rem' }}>
|
||||
<thead>
|
||||
<tr style={{ background: 'var(--bg-secondary)', position: 'sticky', top: 0 }}>
|
||||
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'left', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>Kontext</th>
|
||||
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'left', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>Item</th>
|
||||
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'center', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>Total</th>
|
||||
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'center', fontWeight: 600, borderBottom: '1px solid var(--border-color)', color: '#c53030' }}>Duplikate</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cleanupResult.details.map((group, idx) => (
|
||||
<tr key={idx} style={{ borderBottom: '1px solid var(--border-color)' }}>
|
||||
<td style={{ padding: '0.375rem 0.75rem' }}>
|
||||
<span style={{ fontFamily: 'monospace', fontSize: '0.75rem', background: 'var(--bg-tertiary)', padding: '0.125rem 0.375rem', borderRadius: '3px' }}>
|
||||
{group.context}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '0.375rem 0.75rem' }}>
|
||||
<span style={{ fontFamily: 'monospace', fontSize: '0.75rem', background: 'var(--bg-tertiary)', padding: '0.125rem 0.375rem', borderRadius: '3px' }}>
|
||||
{group.item}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '0.375rem 0.75rem', textAlign: 'center' }}>{group.totalCount}</td>
|
||||
<td style={{ padding: '0.375rem 0.75rem', textAlign: 'center', color: '#c53030', fontWeight: 600 }}>{group.deleteCount}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.modalFooter}>
|
||||
<button className={styles.secondaryButton} onClick={_closeCleanupModal}>
|
||||
{cleanupPhase === 'done' ? 'Schliessen' : 'Abbrechen'}
|
||||
</button>
|
||||
{cleanupPhase === 'preview' && cleanupResult && cleanupResult.duplicateRulesToDelete > 0 && (
|
||||
<button
|
||||
className={styles.dangerButton}
|
||||
onClick={_executeCleanup}
|
||||
disabled={cleanupLoading}
|
||||
>
|
||||
<FaBroom /> {cleanupResult.duplicateRulesToDelete} Duplikate loeschen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue