Files
certigo/frontend/src/pages/Permissions.jsx

638 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react'
import { useAuth } from '../contexts/AuthContext'
import { usePermissions } from '../contexts/PermissionsContext'
const Permissions = () => {
const { authFetch } = useAuth()
const { refreshPermissions } = usePermissions()
const [groups, setGroups] = useState([])
const [spaces, setSpaces] = useState([])
const [loading, setLoading] = useState(false)
const [fetching, setFetching] = useState(true)
const [error, setError] = useState('')
const [showForm, setShowForm] = useState(false)
const [editingGroup, setEditingGroup] = useState(null)
const [showDeleteModal, setShowDeleteModal] = useState(false)
const [groupToDelete, setGroupToDelete] = useState(null)
const [confirmChecked, setConfirmChecked] = useState(false)
const [formData, setFormData] = useState({
name: '',
description: '',
permission: 'READ',
spaceIds: []
})
useEffect(() => {
// Lade Daten parallel beim Mount und beim Zurückkehren zur Seite
let isMounted = true
const loadData = async () => {
if (isMounted) {
setFetching(true)
}
await Promise.all([fetchGroups(), fetchSpaces()])
}
loadData()
return () => {
isMounted = false
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const fetchGroups = async () => {
try {
setError('')
const response = await authFetch('/api/permission-groups')
if (response.ok) {
const data = await response.json()
setGroups(Array.isArray(data) ? data : [])
} else {
setError('Fehler beim Abrufen der Berechtigungsgruppen')
}
} catch (err) {
setError('Fehler beim Abrufen der Berechtigungsgruppen')
console.error('Error fetching permission groups:', err)
} finally {
setFetching(false)
}
}
const fetchSpaces = async () => {
try {
const response = await authFetch('/api/spaces')
if (response.ok) {
const data = await response.json()
setSpaces(Array.isArray(data) ? data : [])
}
} catch (err) {
console.error('Error fetching spaces:', err)
}
}
const handleSubmit = async (e) => {
e.preventDefault()
setError('')
setLoading(true)
if (!formData.name.trim()) {
setError('Bitte geben Sie einen Namen ein')
setLoading(false)
return
}
if (!formData.permission) {
setError('Bitte wählen Sie eine Berechtigungsstufe')
setLoading(false)
return
}
try {
const url = editingGroup
? `/api/permission-groups/${editingGroup.id}`
: '/api/permission-groups'
const method = editingGroup ? 'PUT' : 'POST'
const body = {
name: formData.name,
description: formData.description,
permission: formData.permission,
spaceIds: formData.spaceIds
}
const response = await authFetch(url, {
method,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
if (response.ok) {
await fetchGroups()
setFormData({ name: '', description: '', permission: 'READ', spaceIds: [] })
setShowForm(false)
setEditingGroup(null)
// Aktualisiere Berechtigungen nach Änderung an Berechtigungsgruppen
refreshPermissions()
} else {
const errorData = await response.json()
setError(errorData.error || 'Fehler beim Speichern der Berechtigungsgruppe')
}
} catch (err) {
setError('Fehler beim Speichern der Berechtigungsgruppe')
console.error('Error saving permission group:', err)
} finally {
setLoading(false)
}
}
const handleEdit = (group) => {
setEditingGroup(group)
setFormData({
name: group.name,
description: group.description || '',
permission: group.permission,
spaceIds: group.spaceIds || []
})
setShowForm(true)
}
const handleDelete = (group) => {
setGroupToDelete(group)
setShowDeleteModal(true)
setConfirmChecked(false)
}
const confirmDelete = async () => {
if (!confirmChecked || !groupToDelete) {
return
}
try {
const response = await authFetch(`/api/permission-groups/${groupToDelete.id}`, {
method: 'DELETE',
})
if (response.ok) {
await fetchGroups()
setShowDeleteModal(false)
setGroupToDelete(null)
setConfirmChecked(false)
// Aktualisiere Berechtigungen nach Löschen einer Berechtigungsgruppe
refreshPermissions()
} else {
const errorData = await response.json().catch(() => ({ error: 'Fehler beim Löschen' }))
alert(errorData.error || 'Fehler beim Löschen der Berechtigungsgruppe')
}
} catch (err) {
console.error('Error deleting permission group:', err)
alert('Fehler beim Löschen der Berechtigungsgruppe')
}
}
const cancelDelete = () => {
setShowDeleteModal(false)
setGroupToDelete(null)
setConfirmChecked(false)
}
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
})
}
const handleSpaceToggle = (spaceId) => {
setFormData(prev => {
const spaceIds = prev.spaceIds || []
if (spaceIds.includes(spaceId)) {
return { ...prev, spaceIds: spaceIds.filter(id => id !== spaceId) }
} else {
return { ...prev, spaceIds: [...spaceIds, spaceId] }
}
})
}
const getPermissionLabel = (permission) => {
switch (permission) {
case 'READ':
return 'Lesen'
case 'READ_WRITE':
return 'Lesen/Schreiben'
case 'FULL_ACCESS':
return 'Vollzugriff'
default:
return permission
}
}
const getPermissionBadgeColor = (permission) => {
switch (permission) {
case 'READ':
return 'bg-green-600/20 text-green-300 border-green-500/30'
case 'READ_WRITE':
return 'bg-yellow-600/20 text-yellow-300 border-yellow-500/30'
case 'FULL_ACCESS':
return 'bg-purple-600/20 text-purple-300 border-purple-500/30'
default:
return 'bg-blue-600/20 text-blue-300 border-blue-500/30'
}
}
const getPermissionIcon = (permission) => {
switch (permission) {
case 'READ':
return '👁️'
case 'READ_WRITE':
return '✏️'
case 'FULL_ACCESS':
return '🔓'
default:
return '🔐'
}
}
const getPermissionDescription = (permission) => {
switch (permission) {
case 'READ':
return 'Nur CSRs und Zertifikate ansehen. Keine Requests, keine Lösch-/Erstellrechte.'
case 'READ_WRITE':
return 'FQDNs innerhalb eines Spaces erstellen (nicht löschen), CSRs requesten und ansehen. Keine Spaces löschen/erstellen.'
case 'FULL_ACCESS':
return 'Vollzugriff: Alles darf gemacht werden. Löschen, Erstellen, CSR requesten und ansehen.'
default:
return ''
}
}
return (
<div className="p-8 min-h-full bg-gradient-to-r from-slate-700 to-slate-900">
<div className="max-w-6xl mx-auto">
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-4xl font-bold text-white mb-2 flex items-center gap-3">
<span className="text-5xl">🔐</span>
Berechtigungsgruppen
</h1>
<p className="text-slate-300">Verwalten Sie Berechtigungsgruppen und weisen Sie Spaces zu</p>
</div>
<button
onClick={() => {
setShowForm(true)
setEditingGroup(null)
setFormData({ name: '', description: '', permission: 'READ', spaceIds: [] })
}}
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg shadow-lg hover:shadow-xl transition-all duration-200 flex items-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4v16m8-8H4" />
</svg>
Neue Gruppe
</button>
</div>
{error && (
<div className="mb-6 p-4 bg-red-500/20 border border-red-500/50 rounded-lg text-red-300">
{error}
</div>
)}
{showForm && (
<div className="mb-8 bg-slate-800/80 backdrop-blur-sm rounded-xl shadow-2xl border border-slate-600/50 p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-white flex items-center gap-2">
<span className="text-3xl">{editingGroup ? '✏️' : ''}</span>
{editingGroup ? 'Berechtigungsgruppe bearbeiten' : 'Neue Berechtigungsgruppe'}
</h2>
<button
onClick={() => {
setShowForm(false)
setEditingGroup(null)
setFormData({ name: '', description: '', permission: 'READ', spaceIds: [] })
}}
className="text-slate-400 hover:text-white transition-colors"
title="Schließen"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form onSubmit={handleSubmit}>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-300 mb-2 flex items-center gap-2">
<span>📝</span>
Name *
</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
required
className="w-full px-4 py-2.5 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
placeholder="z.B. Entwickler, Administratoren"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-300 mb-2 flex items-center gap-2">
<span>📄</span>
Beschreibung
</label>
<textarea
name="description"
value={formData.description}
onChange={handleChange}
rows="3"
className="w-full px-4 py-2.5 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all resize-none"
placeholder="Beschreibung der Berechtigungsgruppe"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-300 mb-3 flex items-center gap-2">
<span>🔑</span>
Berechtigungsstufe *
</label>
<div className="grid grid-cols-3 gap-3">
{[
{ value: 'READ', label: 'Lesen', icon: '👁️', activeClass: 'bg-green-600/20 border-green-500 text-green-300', inactiveClass: 'bg-slate-700/50 border-slate-600 text-slate-300' },
{ value: 'READ_WRITE', label: 'Lesen/Schreiben', icon: '✏️', activeClass: 'bg-yellow-600/20 border-yellow-500 text-yellow-300', inactiveClass: 'bg-slate-700/50 border-slate-600 text-slate-300' },
{ value: 'FULL_ACCESS', label: 'Vollzugriff', icon: '🔓', activeClass: 'bg-purple-600/20 border-purple-500 text-purple-300', inactiveClass: 'bg-slate-700/50 border-slate-600 text-slate-300' }
].map(option => (
<button
key={option.value}
type="button"
onClick={() => setFormData({ ...formData, permission: option.value })}
className={`p-4 rounded-lg border-2 transition-all duration-200 ${
formData.permission === option.value
? `${option.activeClass} shadow-lg`
: `${option.inactiveClass} hover:border-slate-500`
}`}
>
<div className="text-2xl mb-2">{option.icon}</div>
<div className="font-semibold text-sm">{option.label}</div>
</button>
))}
</div>
<div className="mt-3 p-3 bg-slate-700/30 border border-slate-600/50 rounded-lg">
<p className="text-xs text-slate-400 leading-relaxed">
{getPermissionDescription(formData.permission)}
</p>
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-300 mb-2 flex items-center gap-2">
<span>📁</span>
Spaces zuweisen
{formData.spaceIds && formData.spaceIds.length > 0 && (
<span className="px-2 py-0.5 bg-blue-600/20 text-blue-300 rounded-full text-xs font-medium">
{formData.spaceIds.length} ausgewählt
</span>
)}
</label>
<div className="bg-slate-700/50 border border-slate-600 rounded-lg p-4 max-h-60 overflow-y-auto">
{spaces.length === 0 ? (
<div className="text-center py-8">
<p className="text-slate-400 text-sm">Keine Spaces vorhanden</p>
</div>
) : (
<div className="grid grid-cols-1 gap-2">
{spaces.map(space => (
<label
key={space.id}
className={`flex items-center cursor-pointer p-3 rounded-lg border transition-all duration-200 ${
formData.spaceIds?.includes(space.id)
? 'bg-blue-600/20 border-blue-500/50 hover:bg-blue-600/30'
: 'bg-slate-600/30 border-slate-600 hover:bg-slate-600/50 hover:border-slate-500'
}`}
>
<input
type="checkbox"
checked={formData.spaceIds?.includes(space.id) || false}
onChange={() => handleSpaceToggle(space.id)}
className="w-5 h-5 text-blue-600 bg-slate-700 border-slate-500 rounded focus:ring-blue-500 focus:ring-2"
/>
<div className="ml-3 flex-1">
<div className="flex items-center gap-2">
<span className="text-slate-300 font-medium">{space.name}</span>
{formData.spaceIds?.includes(space.id) && (
<span className="text-blue-400"></span>
)}
</div>
{space.description && (
<p className="text-xs text-slate-400 mt-0.5">{space.description}</p>
)}
</div>
</label>
))}
</div>
)}
</div>
</div>
</div>
<div className="flex gap-3 mt-6 pt-6 border-t border-slate-600/50">
<button
type="submit"
disabled={loading}
className="px-6 py-2.5 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-800 disabled:cursor-not-allowed text-white font-semibold rounded-lg transition-all duration-200 flex items-center gap-2 shadow-lg hover:shadow-xl"
>
{loading ? (
<>
<svg className="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Wird gespeichert...
</>
) : (
<>
<span>{editingGroup ? '💾' : ''}</span>
{editingGroup ? 'Aktualisieren' : 'Erstellen'}
</>
)}
</button>
<button
type="button"
onClick={() => {
setShowForm(false)
setEditingGroup(null)
setFormData({ name: '', description: '', permission: 'READ', spaceIds: [] })
}}
className="px-6 py-2.5 bg-slate-600 hover:bg-slate-700 text-white font-semibold rounded-lg transition-all duration-200"
>
Abbrechen
</button>
</div>
</form>
</div>
)}
{fetching ? (
<div className="bg-slate-800/80 backdrop-blur-sm rounded-lg shadow-xl border border-slate-600/50 p-8 text-center">
<div className="flex flex-col items-center justify-center">
<svg className="animate-spin h-8 w-8 text-blue-500 mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<p className="text-slate-300">Lade Berechtigungsgruppen...</p>
</div>
</div>
) : groups.length === 0 ? (
<div className="bg-slate-800/80 backdrop-blur-sm rounded-xl shadow-xl border border-slate-600/50 p-12 text-center">
<div className="text-6xl mb-4">🔐</div>
<p className="text-slate-300 text-lg mb-2">
Noch keine Berechtigungsgruppen vorhanden
</p>
<p className="text-slate-400 text-sm mb-6">
Erstellen Sie Ihre erste Gruppe, um Berechtigungen zu verwalten
</p>
<button
onClick={() => {
setShowForm(true)
setEditingGroup(null)
setFormData({ name: '', description: '', permission: 'READ', spaceIds: [] })
}}
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg shadow-lg hover:shadow-xl transition-all duration-200"
>
+ Erste Gruppe erstellen
</button>
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{groups.map((group) => (
<div
key={group.id}
className="bg-slate-800/80 backdrop-blur-sm rounded-xl shadow-xl border border-slate-600/50 p-6 hover:border-slate-500/50 transition-all duration-200 group"
>
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<h3 className="text-xl font-bold text-white group-hover:text-blue-300 transition-colors mb-2">
{group.name}
</h3>
{group.description && (
<p className="text-sm text-slate-400 mb-2">{group.description}</p>
)}
</div>
<div className="flex gap-2">
<button
onClick={() => handleEdit(group)}
className="p-2 bg-blue-600/20 hover:bg-blue-600/30 text-blue-300 rounded-lg transition-all duration-200"
title="Bearbeiten"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button
onClick={() => handleDelete(group)}
className="p-2 bg-red-600/20 hover:bg-red-600/30 text-red-300 rounded-lg transition-all duration-200"
title="Löschen"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</div>
<div className={`inline-flex items-center gap-2 px-3 py-1.5 rounded-lg border text-sm font-medium mb-4 ${getPermissionBadgeColor(group.permission)}`}>
<span className="text-lg">{getPermissionIcon(group.permission)}</span>
{getPermissionLabel(group.permission)}
</div>
{group.spaceIds && group.spaceIds.length > 0 && (
<div className="mt-4 pt-4 border-t border-slate-600/50">
<p className="text-sm font-medium text-slate-300 mb-2 flex items-center gap-2">
<span>📁</span>
Zugewiesene Spaces ({group.spaceIds.length})
</p>
<div className="flex flex-wrap gap-2">
{group.spaceIds.map(spaceId => {
const space = spaces.find(s => s.id === spaceId)
return space ? (
<span
key={spaceId}
className="px-3 py-1.5 bg-slate-700/50 text-slate-300 rounded-lg text-xs font-medium border border-slate-600/50 hover:border-slate-500 transition-colors"
>
{space.name}
</span>
) : null
})}
</div>
</div>
)}
<div className="mt-4 pt-3 border-t border-slate-600/50">
<p className="text-xs text-slate-500">
Erstellt: {group.createdAt ? new Date(group.createdAt).toLocaleDateString('de-DE', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
}) : 'Unbekannt'}
</p>
</div>
</div>
))}
</div>
)}
{/* Delete Confirmation Modal */}
{showDeleteModal && groupToDelete && (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div className="bg-slate-800 rounded-xl shadow-2xl border border-slate-600/50 max-w-md w-full p-6">
<div className="flex items-center mb-4">
<div className="flex-shrink-0 w-12 h-12 bg-red-500/20 rounded-full flex items-center justify-center mr-4">
<svg
className="w-6 h-6 text-red-400"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<h3 className="text-xl font-bold text-white">
Berechtigungsgruppe löschen
</h3>
</div>
<div className="mb-6">
<p className="text-slate-300 mb-4">
Möchten Sie die Berechtigungsgruppe <span className="font-semibold text-white">{groupToDelete.name}</span> wirklich löschen?
</p>
<p className="text-sm text-red-400 mb-4">
Diese Aktion kann nicht rückgängig gemacht werden.
</p>
<label className="flex items-start cursor-pointer group">
<input
type="checkbox"
checked={confirmChecked}
onChange={(e) => setConfirmChecked(e.target.checked)}
className="mt-1 w-5 h-5 text-red-600 bg-slate-700 border-slate-600 rounded focus:ring-2 focus:ring-red-500 focus:ring-offset-2 focus:ring-offset-slate-800 cursor-pointer"
/>
<span className="ml-3 text-sm text-slate-300 group-hover:text-white transition-colors">
Ich bestätige, dass ich diese Berechtigungsgruppe unwiderruflich löschen möchte
</span>
</label>
</div>
<div className="flex gap-3">
<button
onClick={confirmDelete}
disabled={!confirmChecked}
className="flex-1 px-4 py-2 bg-red-600 hover:bg-red-700 disabled:bg-slate-700 disabled:text-slate-500 disabled:cursor-not-allowed text-white font-semibold rounded-lg transition-colors duration-200"
>
Löschen
</button>
<button
onClick={cancelDelete}
className="flex-1 px-4 py-2 bg-slate-600 hover:bg-slate-700 text-white font-semibold rounded-lg transition-colors duration-200"
>
Abbrechen
</button>
</div>
</div>
</div>
)}
</div>
</div>
)
}
export default Permissions