added main permission group feature

This commit is contained in:
2025-11-21 00:28:53 +01:00
parent 1234fbda00
commit 9c2d649adf
7 changed files with 1326 additions and 65 deletions

View File

@@ -9,6 +9,7 @@ import SpaceDetail from './pages/SpaceDetail'
import Impressum from './pages/Impressum'
import Profile from './pages/Profile'
import Users from './pages/Users'
import Permissions from './pages/Permissions'
import Login from './pages/Login'
import AuditLogs from './pages/AuditLogs'
@@ -71,6 +72,7 @@ const AppContent = () => {
<Route path="/impressum" element={<ProtectedRoute><Impressum /></ProtectedRoute>} />
<Route path="/profile" element={<ProtectedRoute><Profile /></ProtectedRoute>} />
<Route path="/settings/users" element={<ProtectedRoute><Users /></ProtectedRoute>} />
<Route path="/settings/permissions" element={<ProtectedRoute><Permissions /></ProtectedRoute>} />
<Route path="/audit-logs" element={<ProtectedRoute><AuditLogs /></ProtectedRoute>} />
</Routes>
</div>

View File

@@ -22,6 +22,7 @@ const Sidebar = ({ isOpen, setIsOpen }) => {
path: '/settings',
subItems: [
{ path: '/settings/users', label: 'User', icon: '👥' },
{ path: '/settings/permissions', label: 'Berechtigungen', icon: '🔐' },
]
}

View File

@@ -0,0 +1,550 @@
import { useState, useEffect } from 'react'
import { useAuth } from '../contexts/AuthContext'
const Permissions = () => {
const { authFetch } = useAuth()
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 [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)
} 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 = async (groupId) => {
if (!window.confirm('Möchten Sie diese Berechtigungsgruppe wirklich löschen?')) {
return
}
try {
const response = await authFetch(`/api/permission-groups/${groupId}`, {
method: 'DELETE',
})
if (response.ok) {
await fetchGroups()
} else {
const errorData = await response.json()
alert(errorData.error || 'Fehler beim Löschen der Berechtigungsgruppe')
}
} catch (err) {
alert('Fehler beim Löschen der Berechtigungsgruppe')
console.error('Error deleting permission group:', err)
}
}
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.id)}
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>
)}
</div>
</div>
)
}
export default Permissions

View File

@@ -4,6 +4,7 @@ import { useAuth } from '../contexts/AuthContext'
const Users = () => {
const { authFetch } = useAuth()
const [users, setUsers] = useState([])
const [groups, setGroups] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [showForm, setShowForm] = useState(false)
@@ -13,11 +14,13 @@ const Users = () => {
email: '',
oldPassword: '',
password: '',
confirmPassword: ''
confirmPassword: '',
groupIds: []
})
useEffect(() => {
fetchUsers()
fetchGroups()
}, [])
const fetchUsers = async () => {
@@ -36,6 +39,18 @@ const Users = () => {
}
}
const fetchGroups = async () => {
try {
const response = await authFetch('/api/permission-groups')
if (response.ok) {
const data = await response.json()
setGroups(Array.isArray(data) ? data : [])
}
} catch (err) {
console.error('Error fetching permission groups:', err)
}
}
const handleSubmit = async (e) => {
e.preventDefault()
setError('')
@@ -68,12 +83,14 @@ const Users = () => {
...(formData.password && {
password: formData.password,
oldPassword: formData.oldPassword
})
}),
...(formData.groupIds !== undefined && { groupIds: formData.groupIds })
}
: {
username: formData.username,
email: formData.email,
password: formData.password
password: formData.password,
groupIds: formData.groupIds || []
}
const response = await authFetch(url, {
@@ -86,7 +103,7 @@ const Users = () => {
if (response.ok) {
await fetchUsers()
setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '' })
setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '', groupIds: [] })
setShowForm(false)
setEditingUser(null)
} else {
@@ -108,7 +125,8 @@ const Users = () => {
email: user.email,
oldPassword: '',
password: '',
confirmPassword: ''
confirmPassword: '',
groupIds: user.groupIds || []
})
setShowForm(true)
}
@@ -142,6 +160,30 @@ const Users = () => {
})
}
const handleGroupToggle = (groupId) => {
setFormData(prev => {
const groupIds = prev.groupIds || []
if (groupIds.includes(groupId)) {
return { ...prev, groupIds: groupIds.filter(id => id !== groupId) }
} else {
return { ...prev, groupIds: [...groupIds, groupId] }
}
})
}
const getPermissionLabel = (permission) => {
switch (permission) {
case 'READ':
return 'Lesen'
case 'READ_WRITE':
return 'Lesen/Schreiben'
case 'FULL_ACCESS':
return 'Vollzugriff'
default:
return permission
}
}
return (
<div className="p-8 min-h-full bg-gradient-to-r from-slate-700 to-slate-900">
<div className="max-w-4xl mx-auto">
@@ -156,7 +198,7 @@ const Users = () => {
onClick={() => {
setShowForm(!showForm)
setEditingUser(null)
setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '' })
setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '', groupIds: [] })
}}
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"
>
@@ -284,6 +326,40 @@ const Users = () => {
<p className="mt-1 text-xs text-green-400"> Passwörter stimmen überein</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-slate-200 mb-2">
Berechtigungsgruppen
</label>
<div className="bg-slate-700/50 border border-slate-600 rounded-lg p-4 max-h-60 overflow-y-auto">
{groups.length === 0 ? (
<p className="text-slate-400 text-sm">Keine Berechtigungsgruppen vorhanden</p>
) : (
<div className="space-y-2">
{groups.map(group => (
<label key={group.id} className="flex items-start cursor-pointer hover:bg-slate-600/50 p-2 rounded">
<input
type="checkbox"
checked={formData.groupIds?.includes(group.id) || false}
onChange={() => handleGroupToggle(group.id)}
className="w-4 h-4 text-blue-600 bg-slate-600 border-slate-500 rounded focus:ring-blue-500 mt-1"
/>
<div className="ml-3 flex-1">
<div className="flex items-center gap-2">
<span className="text-slate-300 font-medium">{group.name}</span>
<span className="px-2 py-0.5 bg-blue-600/20 text-blue-300 rounded text-xs">
{getPermissionLabel(group.permission)}
</span>
</div>
{group.description && (
<p className="text-xs text-slate-400 mt-1">{group.description}</p>
)}
</div>
</label>
))}
</div>
)}
</div>
</div>
{error && (
<div className="p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-red-300 text-sm">
{error}
@@ -302,7 +378,7 @@ const Users = () => {
onClick={() => {
setShowForm(false)
setEditingUser(null)
setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '' })
setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '', groupIds: [] })
setError('')
}}
className="px-6 py-2 bg-slate-600 hover:bg-slate-700 text-white font-semibold rounded-lg transition-colors duration-200"
@@ -347,6 +423,21 @@ const Users = () => {
{user.username}
</h3>
<p className="text-slate-300 mb-2">{user.email}</p>
{user.groupIds && user.groupIds.length > 0 && (
<div className="mb-2">
<p className="text-sm text-slate-300 mb-1">Berechtigungsgruppen:</p>
<div className="flex flex-wrap gap-2">
{user.groupIds.map(groupId => {
const group = groups.find(g => g.id === groupId)
return group ? (
<span key={groupId} className="px-2 py-1 bg-blue-600/20 text-blue-300 rounded text-xs">
{group.name} ({getPermissionLabel(group.permission)})
</span>
) : null
})}
</div>
</div>
)}
<p className="text-xs text-slate-400">
Erstellt: {user.createdAt ? new Date(user.createdAt).toLocaleString('de-DE') : 'Unbekannt'}
</p>