890 lines
39 KiB
JavaScript
890 lines
39 KiB
JavaScript
import { useState, useEffect } from 'react'
|
||
import { useAuth } from '../contexts/AuthContext'
|
||
import { usePermissions } from '../contexts/PermissionsContext'
|
||
|
||
const Users = () => {
|
||
const { authFetch } = useAuth()
|
||
const { refreshPermissions } = usePermissions()
|
||
const [users, setUsers] = useState([])
|
||
const [groups, setGroups] = useState([])
|
||
const [loading, setLoading] = useState(false)
|
||
const [error, setError] = useState('')
|
||
const [showForm, setShowForm] = useState(false)
|
||
const [editingUser, setEditingUser] = useState(null)
|
||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||
const [userToDelete, setUserToDelete] = useState(null)
|
||
const [confirmChecked, setConfirmChecked] = useState(false)
|
||
const [showToggleModal, setShowToggleModal] = useState(false)
|
||
const [userToToggle, setUserToToggle] = useState(null)
|
||
const [confirmToggleChecked, setConfirmToggleChecked] = useState(false)
|
||
const [formData, setFormData] = useState({
|
||
username: '',
|
||
email: '',
|
||
oldPassword: '',
|
||
password: '',
|
||
confirmPassword: '',
|
||
isAdmin: false,
|
||
enabled: true,
|
||
groupIds: []
|
||
})
|
||
const [showAdminWarning, setShowAdminWarning] = useState(false)
|
||
|
||
useEffect(() => {
|
||
fetchUsers()
|
||
fetchGroups()
|
||
}, [])
|
||
|
||
const fetchUsers = async () => {
|
||
try {
|
||
setError('')
|
||
const response = await authFetch('/api/users')
|
||
if (response.ok) {
|
||
const data = await response.json()
|
||
setUsers(Array.isArray(data) ? data : [])
|
||
} else {
|
||
setError('Fehler beim Abrufen der Benutzer')
|
||
}
|
||
} catch (err) {
|
||
setError('Fehler beim Abrufen der Benutzer')
|
||
console.error('Error fetching users:', err)
|
||
}
|
||
}
|
||
|
||
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('')
|
||
setLoading(true)
|
||
|
||
// Validierung: Passwort-Bestätigung muss übereinstimmen
|
||
if (formData.password && formData.password !== formData.confirmPassword) {
|
||
setError('Die Passwörter stimmen nicht überein')
|
||
setLoading(false)
|
||
return
|
||
}
|
||
|
||
// Validierung: Wenn Passwort geändert wird, muss altes Passwort vorhanden sein
|
||
if (editingUser && formData.password && !formData.oldPassword) {
|
||
setError('Bitte geben Sie das alte Passwort ein, um das Passwort zu ändern')
|
||
setLoading(false)
|
||
return
|
||
}
|
||
|
||
try {
|
||
const url = editingUser
|
||
? `/api/users/${editingUser.id}`
|
||
: '/api/users'
|
||
const method = editingUser ? 'PUT' : 'POST'
|
||
|
||
const body = editingUser
|
||
? {
|
||
// Username/Email nur setzen wenn nicht der spezielle Admin-User mit UID 'admin'
|
||
...(formData.username && editingUser.id !== 'admin' && { username: formData.username }),
|
||
...(formData.email && editingUser.id !== 'admin' && { email: formData.email }),
|
||
...(formData.password && {
|
||
password: formData.password,
|
||
oldPassword: formData.oldPassword
|
||
}),
|
||
// isAdmin nur setzen wenn nicht UID 'admin' (UID 'admin' ist immer Admin)
|
||
...(formData.isAdmin !== undefined && editingUser.id !== 'admin' && { isAdmin: formData.isAdmin }),
|
||
// enabled wird nicht über das Bearbeitungsformular geändert, nur über den Button in der Liste
|
||
...(formData.groupIds !== undefined && { groupIds: formData.groupIds })
|
||
}
|
||
: {
|
||
username: formData.username,
|
||
email: formData.email,
|
||
password: formData.password,
|
||
isAdmin: formData.isAdmin || false,
|
||
enabled: true, // Neue User sind immer aktiviert, enabled kann nur für UID 'admin' geändert werden
|
||
groupIds: formData.groupIds || []
|
||
}
|
||
|
||
const response = await authFetch(url, {
|
||
method,
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify(body),
|
||
})
|
||
|
||
if (response.ok) {
|
||
await fetchUsers()
|
||
setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '', isAdmin: false, enabled: true, groupIds: [] })
|
||
setShowForm(false)
|
||
setEditingUser(null)
|
||
setShowAdminWarning(false)
|
||
// Aktualisiere Berechtigungen nach Änderung an Benutzern (Gruppen-Zuweisungen könnten sich geändert haben)
|
||
refreshPermissions()
|
||
} else {
|
||
const errorData = await response.json()
|
||
setError(errorData.error || 'Fehler beim Speichern des Benutzers')
|
||
}
|
||
} catch (err) {
|
||
setError('Fehler beim Speichern des Benutzers')
|
||
console.error('Error saving user:', err)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
const handleEdit = (user) => {
|
||
setEditingUser(user)
|
||
setFormData({
|
||
username: user.username,
|
||
email: user.email,
|
||
oldPassword: '',
|
||
password: '',
|
||
confirmPassword: '',
|
||
isAdmin: user.isAdmin || false,
|
||
enabled: user.enabled !== undefined ? user.enabled : true, // Wird nicht im Formular angezeigt, nur für internen Zustand
|
||
groupIds: user.groupIds || []
|
||
})
|
||
setShowForm(true)
|
||
}
|
||
|
||
const handleDelete = (user) => {
|
||
setUserToDelete(user)
|
||
setShowDeleteModal(true)
|
||
setConfirmChecked(false)
|
||
}
|
||
|
||
const handleToggleEnabled = (user) => {
|
||
if (user.id !== 'admin') {
|
||
return
|
||
}
|
||
setUserToToggle(user)
|
||
setShowToggleModal(true)
|
||
setConfirmToggleChecked(false)
|
||
}
|
||
|
||
const confirmToggle = async () => {
|
||
if (!confirmToggleChecked || !userToToggle) {
|
||
return
|
||
}
|
||
|
||
const newEnabled = !userToToggle.enabled
|
||
const action = newEnabled ? 'aktivieren' : 'deaktivieren'
|
||
|
||
try {
|
||
setLoading(true)
|
||
const response = await authFetch(`/api/users/${userToToggle.id}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
enabled: newEnabled
|
||
}),
|
||
})
|
||
|
||
if (response.ok) {
|
||
await fetchUsers()
|
||
setShowToggleModal(false)
|
||
setUserToToggle(null)
|
||
setConfirmToggleChecked(false)
|
||
// Aktualisiere Berechtigungen nach Änderung
|
||
refreshPermissions()
|
||
} else {
|
||
const errorData = await response.json().catch(() => ({ error: `Fehler beim ${action}` }))
|
||
const errorMessage = errorData.error || `Fehler beim ${action} des Admin-Users`
|
||
setError(errorMessage)
|
||
// Schließe Modal bei Fehler
|
||
if (response.status === 403) {
|
||
setShowToggleModal(false)
|
||
setUserToToggle(null)
|
||
setConfirmToggleChecked(false)
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.error(`Error toggling enabled for admin user:`, err)
|
||
setError(`Fehler beim ${action} des Admin-Users`)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
const cancelToggle = () => {
|
||
setShowToggleModal(false)
|
||
setUserToToggle(null)
|
||
setConfirmToggleChecked(false)
|
||
}
|
||
|
||
const confirmDelete = async () => {
|
||
if (!confirmChecked || !userToDelete) {
|
||
return
|
||
}
|
||
|
||
try {
|
||
const response = await authFetch(`/api/users/${userToDelete.id}`, {
|
||
method: 'DELETE',
|
||
})
|
||
|
||
if (response.ok) {
|
||
await fetchUsers()
|
||
setShowDeleteModal(false)
|
||
setUserToDelete(null)
|
||
setConfirmChecked(false)
|
||
// Aktualisiere Berechtigungen nach Löschen eines Benutzers
|
||
refreshPermissions()
|
||
} else {
|
||
const errorData = await response.json().catch(() => ({ error: 'Fehler beim Löschen' }))
|
||
const errorMessage = errorData.error || 'Fehler beim Löschen des Benutzers'
|
||
// Zeige Fehlermeldung
|
||
setError(errorMessage)
|
||
// Wenn Admin-Löschung verhindert wurde, schließe Modal
|
||
if (response.status === 403) {
|
||
setShowDeleteModal(false)
|
||
setUserToDelete(null)
|
||
setConfirmChecked(false)
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.error('Error deleting user:', err)
|
||
setError('Fehler beim Löschen des Benutzers')
|
||
}
|
||
}
|
||
|
||
const cancelDelete = () => {
|
||
setShowDeleteModal(false)
|
||
setUserToDelete(null)
|
||
setConfirmChecked(false)
|
||
}
|
||
|
||
const handleChange = (e) => {
|
||
setFormData({
|
||
...formData,
|
||
[e.target.name]: e.target.value
|
||
})
|
||
}
|
||
|
||
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 handleAdminToggle = (e) => {
|
||
const isAdmin = e.target.checked
|
||
if (isAdmin && !showAdminWarning) {
|
||
setShowAdminWarning(true)
|
||
}
|
||
setFormData(prev => ({
|
||
...prev,
|
||
isAdmin,
|
||
// Wenn Admin aktiviert wird, entferne alle Gruppen und stelle sicher dass enabled=true
|
||
groupIds: isAdmin ? [] : prev.groupIds,
|
||
enabled: isAdmin ? true : (prev.enabled !== undefined ? prev.enabled : true) // Admin muss immer enabled sein
|
||
}))
|
||
}
|
||
|
||
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">
|
||
<div className="flex items-center justify-between mb-8">
|
||
<div>
|
||
<h1 className="text-4xl font-bold text-white mb-2">Benutzerverwaltung</h1>
|
||
<p className="text-lg text-slate-200">
|
||
Verwalten Sie lokale Benutzer und deren Zugangsdaten.
|
||
</p>
|
||
</div>
|
||
<button
|
||
onClick={() => {
|
||
setShowForm(!showForm)
|
||
setEditingUser(null)
|
||
setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '', isAdmin: false, enabled: true, groupIds: [] })
|
||
setShowAdminWarning(false)
|
||
}}
|
||
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"
|
||
>
|
||
{showForm ? 'Abbrechen' : '+ Neuer Benutzer'}
|
||
</button>
|
||
</div>
|
||
|
||
{/* Create/Edit User Form */}
|
||
{showForm && (
|
||
<div className="bg-slate-800/80 backdrop-blur-sm rounded-lg shadow-xl border border-slate-600/50 p-6 mb-6">
|
||
<h2 className="text-2xl font-semibold text-white mb-4">
|
||
{editingUser ? 'Benutzer bearbeiten' : 'Neuen Benutzer erstellen'}
|
||
</h2>
|
||
<form onSubmit={handleSubmit} className="space-y-4">
|
||
<div>
|
||
<label htmlFor="username" className="block text-sm font-medium text-slate-200 mb-2">
|
||
Benutzername {!editingUser && '*'}
|
||
</label>
|
||
<input
|
||
type="text"
|
||
id="username"
|
||
name="username"
|
||
value={formData.username}
|
||
onChange={handleChange}
|
||
required={!editingUser}
|
||
disabled={editingUser && editingUser.id === 'admin'}
|
||
className={`w-full px-4 py-2 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 ${
|
||
editingUser && editingUser.id === 'admin' ? 'opacity-50 cursor-not-allowed' : ''
|
||
}`}
|
||
placeholder="Geben Sie einen Benutzernamen ein"
|
||
/>
|
||
{editingUser && editingUser.id === 'admin' && (
|
||
<p className="mt-1 text-xs text-slate-400">Der Benutzername des Admin-Users mit UID 'admin' kann nicht geändert werden</p>
|
||
)}
|
||
</div>
|
||
<div>
|
||
<label htmlFor="email" className="block text-sm font-medium text-slate-200 mb-2">
|
||
E-Mail {!editingUser && '*'}
|
||
</label>
|
||
<input
|
||
type="email"
|
||
id="email"
|
||
name="email"
|
||
value={formData.email}
|
||
onChange={handleChange}
|
||
required={!editingUser}
|
||
disabled={editingUser && editingUser.id === 'admin'}
|
||
className={`w-full px-4 py-2 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 ${
|
||
editingUser && editingUser.id === 'admin' ? 'opacity-50 cursor-not-allowed' : ''
|
||
}`}
|
||
placeholder="Geben Sie eine E-Mail-Adresse ein"
|
||
/>
|
||
{editingUser && editingUser.id === 'admin' && (
|
||
<p className="mt-1 text-xs text-slate-400">Die E-Mail-Adresse des Admin-Users mit UID 'admin' kann nicht geändert werden</p>
|
||
)}
|
||
</div>
|
||
{editingUser && (
|
||
<div>
|
||
<label htmlFor="oldPassword" className="block text-sm font-medium text-slate-200 mb-2">
|
||
Altes Passwort {formData.password && '*'}
|
||
</label>
|
||
<input
|
||
type="password"
|
||
id="oldPassword"
|
||
name="oldPassword"
|
||
value={formData.oldPassword}
|
||
onChange={handleChange}
|
||
required={!!formData.password}
|
||
className="w-full px-4 py-2 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"
|
||
placeholder="Geben Sie Ihr aktuelles Passwort ein"
|
||
/>
|
||
</div>
|
||
)}
|
||
<div>
|
||
<label htmlFor="password" className="block text-sm font-medium text-slate-200 mb-2">
|
||
{editingUser ? 'Neues Passwort' : 'Passwort'} {!editingUser && '*'}
|
||
{editingUser && <span className="text-xs text-slate-400 ml-2">(leer lassen, um nicht zu ändern)</span>}
|
||
</label>
|
||
<input
|
||
type="password"
|
||
id="password"
|
||
name="password"
|
||
value={formData.password}
|
||
onChange={handleChange}
|
||
required={!editingUser}
|
||
className="w-full px-4 py-2 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"
|
||
placeholder={editingUser ? "Geben Sie ein neues Passwort ein" : "Geben Sie ein Passwort ein"}
|
||
/>
|
||
{/* Passwortrichtlinie - nur anzeigen wenn Passwort eingegeben wird */}
|
||
{(formData.password || !editingUser) && (
|
||
<div className="mt-2 p-3 bg-slate-700/30 border border-slate-600/50 rounded-lg">
|
||
<p className="text-xs font-semibold text-slate-300 mb-2">Passwortrichtlinie:</p>
|
||
<ul className="text-xs text-slate-400 space-y-1">
|
||
<li className={`flex items-center gap-2 ${formData.password.length >= 8 ? 'text-green-400' : ''}`}>
|
||
{formData.password.length >= 8 ? '✓' : '○'} Mindestens 8 Zeichen
|
||
</li>
|
||
<li className={`flex items-center gap-2 ${/[A-Z]/.test(formData.password) ? 'text-green-400' : ''}`}>
|
||
{/[A-Z]/.test(formData.password) ? '✓' : '○'} Mindestens ein Großbuchstabe
|
||
</li>
|
||
<li className={`flex items-center gap-2 ${/[a-z]/.test(formData.password) ? 'text-green-400' : ''}`}>
|
||
{/[a-z]/.test(formData.password) ? '✓' : '○'} Mindestens ein Kleinbuchstabe
|
||
</li>
|
||
<li className={`flex items-center gap-2 ${/[0-9]/.test(formData.password) ? 'text-green-400' : ''}`}>
|
||
{/[0-9]/.test(formData.password) ? '✓' : '○'} Mindestens eine Zahl
|
||
</li>
|
||
<li className={`flex items-center gap-2 ${/[^A-Za-z0-9]/.test(formData.password) ? 'text-green-400' : ''}`}>
|
||
{/[^A-Za-z0-9]/.test(formData.password) ? '✓' : '○'} Mindestens ein Sonderzeichen
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div>
|
||
<label htmlFor="confirmPassword" className="block text-sm font-medium text-slate-200 mb-2">
|
||
{editingUser ? 'Neues Passwort bestätigen' : 'Passwort bestätigen'} {!editingUser && '*'}
|
||
</label>
|
||
<input
|
||
type="password"
|
||
id="confirmPassword"
|
||
name="confirmPassword"
|
||
value={formData.confirmPassword}
|
||
onChange={handleChange}
|
||
required={!editingUser || !!formData.password}
|
||
className={`w-full px-4 py-2 bg-slate-700/50 border rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
|
||
formData.confirmPassword && formData.password !== formData.confirmPassword
|
||
? 'border-red-500'
|
||
: formData.confirmPassword && formData.password === formData.confirmPassword
|
||
? 'border-green-500'
|
||
: 'border-slate-600'
|
||
}`}
|
||
placeholder={editingUser ? "Bestätigen Sie das neue Passwort" : "Bestätigen Sie das Passwort"}
|
||
/>
|
||
{formData.confirmPassword && formData.password !== formData.confirmPassword && (
|
||
<p className="mt-1 text-xs text-red-400">Die Passwörter stimmen nicht überein</p>
|
||
)}
|
||
{formData.confirmPassword && formData.password === formData.confirmPassword && formData.password && (
|
||
<p className="mt-1 text-xs text-green-400">✓ Passwörter stimmen überein</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* Admin Checkbox - nicht für UID 'admin' */}
|
||
{(!editingUser || editingUser.id !== 'admin') && (
|
||
<div className="bg-slate-700/30 border border-slate-600/50 rounded-lg p-4">
|
||
<label className="flex items-start cursor-pointer group">
|
||
<input
|
||
type="checkbox"
|
||
checked={formData.isAdmin || false}
|
||
onChange={handleAdminToggle}
|
||
disabled={editingUser && editingUser.username === 'admin'} // Admin user kann seinen Status nicht ändern
|
||
className="mt-1 w-5 h-5 text-blue-600 bg-slate-700 border-slate-600 rounded focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-slate-800 cursor-pointer"
|
||
/>
|
||
<div className="ml-3 flex-1">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-slate-200 font-semibold">Administrator</span>
|
||
<span className="px-2 py-0.5 bg-red-600/20 text-red-300 rounded text-xs font-medium">
|
||
VOLLZUGRIFF
|
||
</span>
|
||
</div>
|
||
<p className="text-xs text-slate-400 mt-1">
|
||
Ein Administrator hat vollständigen Zugriff auf alle Funktionen und kann alle Einstellungen verwalten.
|
||
</p>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
)}
|
||
|
||
|
||
{/* Berechtigungsgruppen - ausgegraut wenn Admin oder UID 'admin' */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-200 mb-2">
|
||
Berechtigungsgruppen
|
||
{(formData.isAdmin || (editingUser && editingUser.id === 'admin')) && <span className="text-xs text-slate-400 ml-2">(nicht verfügbar für Administratoren)</span>}
|
||
</label>
|
||
<div className={`bg-slate-700/50 border border-slate-600 rounded-lg p-4 max-h-60 overflow-y-auto ${
|
||
formData.isAdmin || (editingUser && editingUser.id === 'admin') ? 'opacity-50 pointer-events-none' : ''
|
||
}`}>
|
||
{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 p-2 rounded ${
|
||
formData.isAdmin || (editingUser && editingUser.id === 'admin') ? 'cursor-not-allowed' : 'cursor-pointer hover:bg-slate-600/50'
|
||
}`}>
|
||
<input
|
||
type="checkbox"
|
||
checked={formData.groupIds?.includes(group.id) || false}
|
||
onChange={() => handleGroupToggle(group.id)}
|
||
disabled={formData.isAdmin || (editingUser && editingUser.id === 'admin')}
|
||
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}
|
||
</div>
|
||
)}
|
||
<div className="flex gap-3">
|
||
<button
|
||
type="submit"
|
||
disabled={loading}
|
||
className="px-6 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-800 disabled:cursor-not-allowed text-white font-semibold rounded-lg transition-colors duration-200"
|
||
>
|
||
{loading ? 'Wird gespeichert...' : editingUser ? 'Aktualisieren' : 'Benutzer erstellen'}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setShowForm(false)
|
||
setEditingUser(null)
|
||
setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '', isAdmin: false, enabled: true, groupIds: [] })
|
||
setShowAdminWarning(false)
|
||
setError('')
|
||
}}
|
||
className="px-6 py-2 bg-slate-600 hover:bg-slate-700 text-white font-semibold rounded-lg transition-colors duration-200"
|
||
>
|
||
Abbrechen
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
)}
|
||
|
||
{/* Users List */}
|
||
<div className="bg-slate-800/80 backdrop-blur-sm rounded-lg shadow-xl border border-slate-600/50 p-6">
|
||
<h2 className="text-2xl font-semibold text-white mb-4">
|
||
Benutzer
|
||
</h2>
|
||
{error && !showForm && (
|
||
<div className="mb-4 p-4 bg-red-500/20 border border-red-500/50 rounded-lg">
|
||
<p className="text-red-300 mb-2">{error}</p>
|
||
<button
|
||
onClick={fetchUsers}
|
||
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors"
|
||
>
|
||
Erneut versuchen
|
||
</button>
|
||
</div>
|
||
)}
|
||
{users.length === 0 ? (
|
||
<p className="text-slate-300 text-center py-8">
|
||
Noch keine Benutzer vorhanden. Erstellen Sie Ihren ersten Benutzer!
|
||
</p>
|
||
) : (
|
||
<div className="space-y-4">
|
||
{users.map((user) => (
|
||
<div
|
||
key={user.id}
|
||
className="bg-slate-700/50 rounded-lg p-4 border border-slate-600/50 hover:border-slate-500 transition-colors"
|
||
>
|
||
<div className="flex items-start justify-between">
|
||
<div className="flex-1">
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<h3 className="text-xl font-semibold text-white">
|
||
{user.username}
|
||
</h3>
|
||
{user.isAdmin && (
|
||
<span className="px-2 py-0.5 bg-red-600/20 text-red-300 rounded text-xs font-medium border border-red-500/30">
|
||
ADMIN
|
||
</span>
|
||
)}
|
||
{user.id === 'admin' && user.enabled === false && (
|
||
<span className="px-2 py-0.5 bg-red-600/20 text-red-300 rounded text-xs font-medium border border-red-500/30">
|
||
DEAKTIVIERT
|
||
</span>
|
||
)}
|
||
</div>
|
||
<p className="text-slate-300 mb-2">{user.email}</p>
|
||
{!user.isAdmin && 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>
|
||
{user.id && (
|
||
<p className="text-xs text-slate-500 font-mono mt-1">
|
||
ID: {user.id}
|
||
</p>
|
||
)}
|
||
</div>
|
||
<div className="flex gap-2 ml-4">
|
||
<button
|
||
onClick={() => handleEdit(user)}
|
||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-lg transition-colors"
|
||
>
|
||
Bearbeiten
|
||
</button>
|
||
{user.id === 'admin' ? (
|
||
<button
|
||
onClick={() => handleToggleEnabled(user)}
|
||
className={`px-4 py-2 text-white text-sm rounded-lg transition-colors ${
|
||
user.enabled
|
||
? 'bg-yellow-600 hover:bg-yellow-700'
|
||
: 'bg-green-600 hover:bg-green-700'
|
||
}`}
|
||
title={user.enabled ? "Admin-User deaktivieren" : "Admin-User aktivieren"}
|
||
>
|
||
{user.enabled ? 'Deaktivieren' : 'Aktivieren'}
|
||
</button>
|
||
) : (
|
||
<button
|
||
onClick={() => handleDelete(user)}
|
||
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-sm rounded-lg transition-colors"
|
||
>
|
||
Löschen
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Admin Warning Modal */}
|
||
{showAdminWarning && (
|
||
<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-red-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">
|
||
Administrator-Berechtigung
|
||
</h3>
|
||
</div>
|
||
|
||
<div className="mb-6">
|
||
<p className="text-slate-300 mb-3">
|
||
Sie sind dabei, diesem Benutzer <span className="font-semibold text-red-400">Administrator-Rechte</span> zu gewähren.
|
||
</p>
|
||
<div className="bg-red-500/10 border border-red-500/30 rounded-lg p-3 mb-4">
|
||
<p className="text-sm font-semibold text-red-300 mb-2">⚠️ Mögliche Gefahren:</p>
|
||
<ul className="text-xs text-slate-300 space-y-1 list-disc list-inside">
|
||
<li>Vollständiger Zugriff auf alle Funktionen und Einstellungen</li>
|
||
<li>Möglichkeit, andere Benutzer zu erstellen, zu bearbeiten oder zu löschen</li>
|
||
<li>Zugriff auf alle Spaces, FQDNs und Zertifikate</li>
|
||
<li>Möglichkeit, Berechtigungsgruppen zu verwalten</li>
|
||
<li>Keine Einschränkungen durch Berechtigungsgruppen</li>
|
||
</ul>
|
||
</div>
|
||
<p className="text-sm text-slate-400">
|
||
Möchten Sie wirklich fortfahren?
|
||
</p>
|
||
</div>
|
||
|
||
<div className="flex gap-3">
|
||
<button
|
||
onClick={() => setShowAdminWarning(false)}
|
||
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>
|
||
<button
|
||
onClick={() => setShowAdminWarning(false)}
|
||
className="flex-1 px-4 py-2 bg-red-600 hover:bg-red-700 text-white font-semibold rounded-lg transition-colors duration-200"
|
||
>
|
||
Verstanden, fortfahren
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Delete Confirmation Modal */}
|
||
{showDeleteModal && userToDelete && (
|
||
<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">
|
||
Benutzer löschen
|
||
</h3>
|
||
</div>
|
||
|
||
<div className="mb-6">
|
||
<p className="text-slate-300 mb-4">
|
||
Möchten Sie den Benutzer <span className="font-semibold text-white">{userToDelete.username}</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 diesen Benutzer 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>
|
||
)}
|
||
|
||
{/* Toggle Enabled Confirmation Modal */}
|
||
{showToggleModal && userToToggle && (
|
||
<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 max-w-md w-full p-6 ${
|
||
userToToggle.enabled ? 'border-yellow-600/50' : 'border-green-600/50'
|
||
}`}>
|
||
<div className="flex items-center mb-4">
|
||
<div className={`flex-shrink-0 w-12 h-12 rounded-full flex items-center justify-center mr-4 ${
|
||
userToToggle.enabled ? 'bg-yellow-500/20' : 'bg-green-500/20'
|
||
}`}>
|
||
{userToToggle.enabled ? (
|
||
<svg
|
||
className="w-6 h-6 text-yellow-400"
|
||
fill="none"
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
strokeWidth="2"
|
||
viewBox="0 0 24 24"
|
||
stroke="currentColor"
|
||
>
|
||
<path d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
|
||
</svg>
|
||
) : (
|
||
<svg
|
||
className="w-6 h-6 text-green-400"
|
||
fill="none"
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
strokeWidth="2"
|
||
viewBox="0 0 24 24"
|
||
stroke="currentColor"
|
||
>
|
||
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
)}
|
||
</div>
|
||
<h3 className="text-xl font-bold text-white">
|
||
Admin-User {userToToggle.enabled ? 'deaktivieren' : 'aktivieren'}
|
||
</h3>
|
||
</div>
|
||
|
||
<div className="mb-6">
|
||
<p className="text-slate-300 mb-4">
|
||
Möchten Sie den Admin-User <span className="font-semibold text-white">{userToToggle.username}</span> (UID: {userToToggle.id}) wirklich {userToToggle.enabled ? 'deaktivieren' : 'aktivieren'}?
|
||
</p>
|
||
<p className={`text-sm mb-4 ${
|
||
userToToggle.enabled ? 'text-yellow-400' : 'text-green-400'
|
||
}`}>
|
||
{userToToggle.enabled
|
||
? 'Der Admin-User kann sich nach der Deaktivierung nicht mehr anmelden und keine API-Calls durchführen.'
|
||
: 'Der Admin-User kann sich nach der Aktivierung wieder anmelden und API-Calls durchführen.'}
|
||
</p>
|
||
|
||
<label className="flex items-start cursor-pointer group">
|
||
<input
|
||
type="checkbox"
|
||
checked={confirmToggleChecked}
|
||
onChange={(e) => setConfirmToggleChecked(e.target.checked)}
|
||
className={`mt-1 w-5 h-5 bg-slate-700 border-slate-600 rounded focus:ring-2 focus:ring-offset-2 focus:ring-offset-slate-800 cursor-pointer ${
|
||
userToToggle.enabled
|
||
? 'text-yellow-600 focus:ring-yellow-500'
|
||
: 'text-green-600 focus:ring-green-500'
|
||
}`}
|
||
/>
|
||
<span className="ml-3 text-sm text-slate-300 group-hover:text-white transition-colors">
|
||
Ich bestätige, dass ich den Admin-User {userToToggle.enabled ? 'deaktivieren' : 'aktivieren'} möchte
|
||
</span>
|
||
</label>
|
||
</div>
|
||
|
||
<div className="flex gap-3">
|
||
<button
|
||
onClick={confirmToggle}
|
||
disabled={!confirmToggleChecked}
|
||
className={`flex-1 px-4 py-2 disabled:bg-slate-700 disabled:text-slate-500 disabled:cursor-not-allowed text-white font-semibold rounded-lg transition-colors duration-200 ${
|
||
userToToggle.enabled
|
||
? 'bg-yellow-600 hover:bg-yellow-700'
|
||
: 'bg-green-600 hover:bg-green-700'
|
||
}`}
|
||
>
|
||
{userToToggle.enabled ? 'Deaktivieren' : 'Aktivieren'}
|
||
</button>
|
||
<button
|
||
onClick={cancelToggle}
|
||
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 Users
|
||
|