Merge pull request 'feature/permissionsAndRoles' (#2) from feature/permissionsAndRoles into main

Reviewed-on: #2
This commit is contained in:
2025-11-21 00:54:59 +00:00
15 changed files with 3230 additions and 183 deletions

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 90 KiB

View File

@@ -1,6 +1,7 @@
import { useState } from 'react' import { useState } from 'react'
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom' import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
import { AuthProvider, useAuth } from './contexts/AuthContext' import { AuthProvider, useAuth } from './contexts/AuthContext'
import { PermissionsProvider, usePermissions } from './contexts/PermissionsContext'
import Sidebar from './components/Sidebar' import Sidebar from './components/Sidebar'
import Footer from './components/Footer' import Footer from './components/Footer'
import Home from './pages/Home' import Home from './pages/Home'
@@ -9,6 +10,7 @@ import SpaceDetail from './pages/SpaceDetail'
import Impressum from './pages/Impressum' import Impressum from './pages/Impressum'
import Profile from './pages/Profile' import Profile from './pages/Profile'
import Users from './pages/Users' import Users from './pages/Users'
import Permissions from './pages/Permissions'
import Login from './pages/Login' import Login from './pages/Login'
import AuditLogs from './pages/AuditLogs' import AuditLogs from './pages/AuditLogs'
@@ -33,6 +35,43 @@ const ProtectedRoute = ({ children }) => {
return isAuthenticated ? children : <Navigate to="/login" replace /> return isAuthenticated ? children : <Navigate to="/login" replace />
} }
// Admin Only Route Component
const AdminRoute = ({ children }) => {
const { isAuthenticated, loading } = useAuth()
const { isAdmin, loading: permissionsLoading } = usePermissions()
if (loading || permissionsLoading) {
return (
<div className="min-h-screen bg-gradient-to-r from-slate-700 to-slate-900 flex items-center justify-center">
<div className="text-center">
<svg className="animate-spin h-12 w-12 text-blue-500 mx-auto 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...</p>
</div>
</div>
)
}
if (!isAuthenticated) {
return <Navigate to="/login" replace />
}
if (!isAdmin) {
return (
<div className="min-h-screen bg-gradient-to-r from-slate-700 to-slate-900 flex items-center justify-center">
<div className="text-center">
<p className="text-red-400 text-xl font-semibold mb-2">Zugriff verweigert</p>
<p className="text-slate-300">Nur Administratoren haben Zugriff auf diese Seite.</p>
</div>
</div>
)
}
return children
}
// Public Route Component (redirects to home if already logged in) // Public Route Component (redirects to home if already logged in)
const PublicRoute = ({ children }) => { const PublicRoute = ({ children }) => {
const { isAuthenticated, loading } = useAuth() const { isAuthenticated, loading } = useAuth()
@@ -70,7 +109,8 @@ const AppContent = () => {
<Route path="/spaces/:id" element={<ProtectedRoute><SpaceDetail /></ProtectedRoute>} /> <Route path="/spaces/:id" element={<ProtectedRoute><SpaceDetail /></ProtectedRoute>} />
<Route path="/impressum" element={<ProtectedRoute><Impressum /></ProtectedRoute>} /> <Route path="/impressum" element={<ProtectedRoute><Impressum /></ProtectedRoute>} />
<Route path="/profile" element={<ProtectedRoute><Profile /></ProtectedRoute>} /> <Route path="/profile" element={<ProtectedRoute><Profile /></ProtectedRoute>} />
<Route path="/settings/users" element={<ProtectedRoute><Users /></ProtectedRoute>} /> <Route path="/settings/users" element={<AdminRoute><Users /></AdminRoute>} />
<Route path="/settings/permissions" element={<AdminRoute><Permissions /></AdminRoute>} />
<Route path="/audit-logs" element={<ProtectedRoute><AuditLogs /></ProtectedRoute>} /> <Route path="/audit-logs" element={<ProtectedRoute><AuditLogs /></ProtectedRoute>} />
</Routes> </Routes>
</div> </div>
@@ -85,7 +125,9 @@ function App() {
return ( return (
<Router> <Router>
<AuthProvider> <AuthProvider>
<PermissionsProvider>
<AppContent /> <AppContent />
</PermissionsProvider>
</AuthProvider> </AuthProvider>
</Router> </Router>
) )

View File

@@ -1,11 +1,13 @@
import { Link, useLocation, useNavigate } from 'react-router-dom' import { Link, useLocation, useNavigate } from 'react-router-dom'
import { useAuth } from '../contexts/AuthContext' import { useAuth } from '../contexts/AuthContext'
import { usePermissions } from '../contexts/PermissionsContext'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
const Sidebar = ({ isOpen, setIsOpen }) => { const Sidebar = ({ isOpen, setIsOpen }) => {
const location = useLocation() const location = useLocation()
const navigate = useNavigate() const navigate = useNavigate()
const { user, logout } = useAuth() const { user, logout } = useAuth()
const { isAdmin } = usePermissions()
const [expandedMenus, setExpandedMenus] = useState({}) const [expandedMenus, setExpandedMenus] = useState({})
const menuItems = [ const menuItems = [
@@ -22,6 +24,7 @@ const Sidebar = ({ isOpen, setIsOpen }) => {
path: '/settings', path: '/settings',
subItems: [ subItems: [
{ path: '/settings/users', label: 'User', icon: '👥' }, { path: '/settings/users', label: 'User', icon: '👥' },
{ path: '/settings/permissions', label: 'Berechtigungen', icon: '🔐' },
] ]
} }
@@ -127,7 +130,8 @@ const Sidebar = ({ isOpen, setIsOpen }) => {
</li> </li>
))} ))}
{/* Settings Menu mit Unterpunkten */} {/* Settings Menu mit Unterpunkten - nur für Admins */}
{isAdmin && (
<li> <li>
<button <button
onClick={() => isOpen && toggleMenu(settingsMenu.path)} onClick={() => isOpen && toggleMenu(settingsMenu.path)}
@@ -183,6 +187,7 @@ const Sidebar = ({ isOpen, setIsOpen }) => {
</ul> </ul>
)} )}
</li> </li>
)}
</ul> </ul>
{/* Profil-Eintrag und Logout am unteren Ende */} {/* Profil-Eintrag und Logout am unteren Ende */}
<div className="mt-auto pt-2 border-t border-slate-700/50 space-y-2"> <div className="mt-auto pt-2 border-t border-slate-700/50 space-y-2">

View File

@@ -0,0 +1,102 @@
import { createContext, useContext, useState, useEffect, useCallback } from 'react'
import { useAuth } from './AuthContext'
const PermissionsContext = createContext(null)
export const PermissionsProvider = ({ children }) => {
const { authFetch, isAuthenticated } = useAuth()
const [permissions, setPermissions] = useState({
isAdmin: false,
hasFullAccess: false,
accessibleSpaces: [],
canCreateSpace: false,
canDeleteSpace: false,
canCreateFqdn: {},
canDeleteFqdn: {},
canUploadCSR: {},
canSignCSR: {},
})
const [loading, setLoading] = useState(true)
const fetchPermissions = useCallback(async () => {
if (!isAuthenticated) {
setLoading(false)
return
}
try {
setLoading(true)
const response = await authFetch('/api/user/permissions')
if (response.ok) {
const data = await response.json()
setPermissions({
isAdmin: data.isAdmin || false,
hasFullAccess: data.hasFullAccess || false,
accessibleSpaces: data.accessibleSpaces || [],
canCreateSpace: data.permissions?.canCreateSpace || false,
canDeleteSpace: data.permissions?.canDeleteSpace || false,
canCreateFqdn: data.permissions?.canCreateFqdn || {},
canDeleteFqdn: data.permissions?.canDeleteFqdn || {},
canUploadCSR: data.permissions?.canUploadCSR || {},
canSignCSR: data.permissions?.canSignCSR || {},
})
}
} catch (err) {
console.error('Error fetching permissions:', err)
} finally {
setLoading(false)
}
}, [isAuthenticated, authFetch])
useEffect(() => {
if (isAuthenticated) {
fetchPermissions()
} else {
setPermissions({
isAdmin: false,
hasFullAccess: false,
accessibleSpaces: [],
canCreateSpace: false,
canDeleteSpace: false,
canCreateFqdn: {},
canDeleteFqdn: {},
canUploadCSR: {},
canSignCSR: {},
})
setLoading(false)
}
}, [isAuthenticated, fetchPermissions])
const canCreateSpace = () => permissions.canCreateSpace
const canDeleteSpace = (spaceId) => permissions.canDeleteSpace
const canCreateFqdn = (spaceId) => permissions.canCreateFqdn[spaceId] === true
const canDeleteFqdn = (spaceId) => permissions.canDeleteFqdn[spaceId] === true
const canUploadCSR = (spaceId) => permissions.canUploadCSR[spaceId] === true
const canSignCSR = (spaceId) => permissions.canSignCSR[spaceId] === true
const hasAccessToSpace = (spaceId) => permissions.accessibleSpaces.includes(spaceId)
const value = {
permissions,
loading,
refreshPermissions: fetchPermissions,
isAdmin: permissions.isAdmin,
canCreateSpace,
canDeleteSpace,
canCreateFqdn,
canDeleteFqdn,
canUploadCSR,
canSignCSR,
hasAccessToSpace,
}
return <PermissionsContext.Provider value={value}>{children}</PermissionsContext.Provider>
}
export const usePermissions = () => {
const context = useContext(PermissionsContext)
if (!context) {
throw new Error('usePermissions muss innerhalb eines PermissionsProvider verwendet werden')
}
return context
}

View File

@@ -0,0 +1,3 @@
// Re-export from PermissionsContext for backward compatibility
export { usePermissions } from '../contexts/PermissionsContext'

View File

@@ -163,6 +163,7 @@ const AuditLogs = () => {
csr: 'CSR', csr: 'CSR',
provider: 'Provider', provider: 'Provider',
certificate: 'Zertifikat', certificate: 'Zertifikat',
permission_group: 'Berechtigungsgruppen',
} }
const toggleLogExpansion = (logId) => { const toggleLogExpansion = (logId) => {
@@ -239,6 +240,7 @@ const AuditLogs = () => {
<option value="csr">CSR</option> <option value="csr">CSR</option>
<option value="provider">Provider</option> <option value="provider">Provider</option>
<option value="certificate">Zertifikat</option> <option value="certificate">Zertifikat</option>
<option value="permission_group">Berechtigungsgruppen</option>
</select> </select>
</div> </div>
<div> <div>

View File

@@ -267,6 +267,20 @@ const Home = () => {
</div> </div>
</div> </div>
</div> </div>
<div className="bg-gradient-to-br from-cyan-500/20 to-cyan-600/20 rounded-lg p-4 border border-cyan-500/30">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-300 mb-1">Benutzer</p>
<p className="text-3xl font-bold text-white">{stats.users || 0}</p>
</div>
<div className="w-12 h-12 bg-cyan-500/20 rounded-full flex items-center justify-center">
<svg className="w-6 h-6 text-cyan-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
</div>
</div>
</div>
</div> </div>
) : ( ) : (
<p className="text-slate-400">Fehler beim Laden der Statistiken</p> <p className="text-slate-400">Fehler beim Laden der Statistiken</p>

View File

@@ -0,0 +1,637 @@
import { useState, useEffect } from 'react'
import { useAuth } from '../contexts/AuthContext'
import { usePermissions } from '../hooks/usePermissions'
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

View File

@@ -1,8 +1,10 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { useAuth } from '../contexts/AuthContext' import { useAuth } from '../contexts/AuthContext'
import { usePermissions } from '../contexts/PermissionsContext'
const Profile = () => { const Profile = () => {
const { authFetch, user } = useAuth() const { authFetch, user } = useAuth()
const { isAdmin } = usePermissions()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [showSuccessAnimation, setShowSuccessAnimation] = useState(false) const [showSuccessAnimation, setShowSuccessAnimation] = useState(false)
const [error, setError] = useState('') const [error, setError] = useState('')
@@ -286,8 +288,10 @@ const Profile = () => {
try { try {
const body = { const body = {
...(formData.username && { username: formData.username }), // Nur der spezielle Admin-User mit UID 'admin': Username und Email nicht ändern
...(formData.email && { email: formData.email }), // Andere Admin-User können ihre Daten ändern
...(user?.id !== 'admin' && formData.username && { username: formData.username }),
...(user?.id !== 'admin' && formData.email && { email: formData.email }),
...(formData.password && { ...(formData.password && {
password: formData.password, password: formData.password,
oldPassword: formData.oldPassword oldPassword: formData.oldPassword
@@ -414,9 +418,15 @@ const Profile = () => {
value={formData.username} value={formData.username}
onChange={handleChange} onChange={handleChange}
required required
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" disabled={user?.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 ${
user?.id === 'admin' ? 'opacity-50 cursor-not-allowed' : ''
}`}
placeholder="Geben Sie Ihren Benutzernamen ein" placeholder="Geben Sie Ihren Benutzernamen ein"
/> />
{user?.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>
<div> <div>
@@ -430,9 +440,15 @@ const Profile = () => {
value={formData.email} value={formData.email}
onChange={handleChange} onChange={handleChange}
required required
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" disabled={user?.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 ${
user?.id === 'admin' ? 'opacity-50 cursor-not-allowed' : ''
}`}
placeholder="Geben Sie Ihre E-Mail-Adresse ein" placeholder="Geben Sie Ihre E-Mail-Adresse ein"
/> />
{user?.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> </div>
<div className="pt-4 border-t border-slate-700/50"> <div className="pt-4 border-t border-slate-700/50">

View File

@@ -1,11 +1,13 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { useParams, useNavigate } from 'react-router-dom' import { useParams, useNavigate } from 'react-router-dom'
import { useAuth } from '../contexts/AuthContext' import { useAuth } from '../contexts/AuthContext'
import { usePermissions } from '../hooks/usePermissions'
const SpaceDetail = () => { const SpaceDetail = () => {
const { id } = useParams() const { id } = useParams()
const navigate = useNavigate() const navigate = useNavigate()
const { authFetch } = useAuth() const { authFetch } = useAuth()
const { canCreateFqdn, canDeleteFqdn, canSignCSR, canUploadCSR, refreshPermissions } = usePermissions()
const [space, setSpace] = useState(null) const [space, setSpace] = useState(null)
const [fqdns, setFqdns] = useState([]) const [fqdns, setFqdns] = useState([])
const [showForm, setShowForm] = useState(false) const [showForm, setShowForm] = useState(false)
@@ -123,6 +125,8 @@ const SpaceDetail = () => {
setFqdns([...fqdns, newFqdn]) setFqdns([...fqdns, newFqdn])
setFormData({ fqdn: '', description: '' }) setFormData({ fqdn: '', description: '' })
setShowForm(false) setShowForm(false)
// Aktualisiere Berechtigungen nach dem Erstellen eines FQDNs
refreshPermissions()
} else { } else {
let errorMessage = 'Fehler beim Erstellen des FQDN' let errorMessage = 'Fehler beim Erstellen des FQDN'
try { try {
@@ -176,6 +180,8 @@ const SpaceDetail = () => {
setShowDeleteModal(false) setShowDeleteModal(false)
setFqdnToDelete(null) setFqdnToDelete(null)
setConfirmChecked(false) setConfirmChecked(false)
// Aktualisiere Berechtigungen nach dem Löschen eines FQDNs
refreshPermissions()
} else { } else {
const errorData = await response.json().catch(() => ({ error: 'Fehler beim Löschen' })) const errorData = await response.json().catch(() => ({ error: 'Fehler beim Löschen' }))
alert(errorData.error || 'Fehler beim Löschen des FQDN') alert(errorData.error || 'Fehler beim Löschen des FQDN')
@@ -586,7 +592,13 @@ const SpaceDetail = () => {
</h3> </h3>
<button <button
onClick={() => setShowForm(!showForm)} onClick={() => setShowForm(!showForm)}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg transition-all duration-200" disabled={!canCreateFqdn(id) && !showForm}
className={`px-4 py-2 font-semibold rounded-lg transition-all duration-200 ${
canCreateFqdn(id) || showForm
? 'bg-blue-600 hover:bg-blue-700 text-white'
: 'bg-slate-600 text-slate-400 cursor-not-allowed opacity-50'
}`}
title={!canCreateFqdn(id) && !showForm ? 'Keine Berechtigung zum Erstellen von FQDNs' : ''}
> >
{showForm ? 'Abbrechen' : '+ Neuer FQDN'} {showForm ? 'Abbrechen' : '+ Neuer FQDN'}
</button> </button>
@@ -630,11 +642,18 @@ const SpaceDetail = () => {
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
if (canSignCSR(id)) {
handleRequestSigning(fqdn) handleRequestSigning(fqdn)
}
}} }}
className="p-2 text-purple-400 hover:text-purple-300 hover:bg-purple-500/20 rounded-lg transition-colors" disabled={!canSignCSR(id)}
title="CSR signieren lassen" className={`p-2 rounded-lg transition-colors ${
aria-label="CSR signieren lassen" canSignCSR(id)
? 'text-purple-400 hover:text-purple-300 hover:bg-purple-500/20'
: 'text-slate-500 cursor-not-allowed opacity-50'
}`}
title={canSignCSR(id) ? 'CSR signieren lassen' : 'Keine Berechtigung zum Signieren von CSRs'}
aria-label={canSignCSR(id) ? 'CSR signieren lassen' : 'Keine Berechtigung'}
> >
<svg <svg
className="w-5 h-5" className="w-5 h-5"
@@ -692,16 +711,23 @@ const SpaceDetail = () => {
} }
e.target.value = '' e.target.value = ''
}} }}
disabled={uploadingCSR} disabled={uploadingCSR || !canUploadCSR(id)}
/> />
<button <button
className="p-2 text-blue-400 hover:text-blue-300 hover:bg-blue-500/20 rounded-lg transition-colors" disabled={!canUploadCSR(id)}
title="CSR hochladen" className={`p-2 rounded-lg transition-colors ${
aria-label="CSR hochladen" canUploadCSR(id)
? 'text-blue-400 hover:text-blue-300 hover:bg-blue-500/20'
: 'text-slate-500 cursor-not-allowed opacity-50'
}`}
title={canUploadCSR(id) ? 'CSR hochladen' : 'Keine Berechtigung zum Hochladen von CSRs'}
aria-label={canUploadCSR(id) ? 'CSR hochladen' : 'Keine Berechtigung'}
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
e.preventDefault() e.preventDefault()
if (canUploadCSR(id)) {
e.currentTarget.parentElement.querySelector('input[type="file"]')?.click() e.currentTarget.parentElement.querySelector('input[type="file"]')?.click()
}
}} }}
> >
<svg <svg
@@ -806,11 +832,18 @@ const SpaceDetail = () => {
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
if (canDeleteFqdn(id)) {
handleDelete(fqdn) handleDelete(fqdn)
}
}} }}
className="p-2 text-red-400 hover:text-red-300 hover:bg-red-500/20 rounded-lg transition-colors" disabled={!canDeleteFqdn(id)}
title="FQDN löschen" className={`p-2 rounded-lg transition-colors ${
aria-label="FQDN löschen" canDeleteFqdn(id)
? 'text-red-400 hover:text-red-300 hover:bg-red-500/20'
: 'text-slate-500 cursor-not-allowed opacity-50'
}`}
title={canDeleteFqdn(id) ? 'FQDN löschen' : 'Keine Berechtigung zum Löschen von FQDNs'}
aria-label={canDeleteFqdn(id) ? 'FQDN löschen' : 'Keine Berechtigung'}
> >
<svg <svg
className="w-5 h-5" className="w-5 h-5"

View File

@@ -1,10 +1,12 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useAuth } from '../contexts/AuthContext' import { useAuth } from '../contexts/AuthContext'
import { usePermissions } from '../hooks/usePermissions'
const Spaces = () => { const Spaces = () => {
const navigate = useNavigate() const navigate = useNavigate()
const { authFetch } = useAuth() const { authFetch } = useAuth()
const { canCreateSpace, canDeleteSpace, refreshPermissions } = usePermissions()
const [spaces, setSpaces] = useState([]) const [spaces, setSpaces] = useState([])
const [showForm, setShowForm] = useState(false) const [showForm, setShowForm] = useState(false)
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
@@ -72,6 +74,8 @@ const Spaces = () => {
setSpaces([...spaces, newSpace]) setSpaces([...spaces, newSpace])
setFormData({ name: '', description: '' }) setFormData({ name: '', description: '' })
setShowForm(false) setShowForm(false)
// Aktualisiere Berechtigungen nach dem Erstellen eines Spaces
refreshPermissions()
} else { } else {
const errorData = await response.json() const errorData = await response.json()
setError(errorData.error || 'Fehler beim Erstellen des Space') setError(errorData.error || 'Fehler beim Erstellen des Space')
@@ -140,6 +144,8 @@ const Spaces = () => {
setConfirmChecked(false) setConfirmChecked(false)
setDeleteFqdnsChecked(false) setDeleteFqdnsChecked(false)
setFqdnCount(0) setFqdnCount(0)
// Aktualisiere Berechtigungen nach dem Löschen eines Spaces
refreshPermissions()
} else { } else {
const errorText = await response.text() const errorText = await response.text()
let errorMessage = 'Fehler beim Löschen des Space' let errorMessage = 'Fehler beim Löschen des Space'
@@ -188,7 +194,13 @@ const Spaces = () => {
</div> </div>
<button <button
onClick={() => setShowForm(!showForm)} onClick={() => setShowForm(!showForm)}
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" disabled={!canCreateSpace() && !showForm}
className={`px-6 py-3 font-semibold rounded-lg shadow-lg transition-all duration-200 ${
canCreateSpace() || showForm
? 'bg-blue-600 hover:bg-blue-700 text-white hover:shadow-xl'
: 'bg-slate-600 text-slate-400 cursor-not-allowed opacity-50'
}`}
title={!canCreateSpace() && !showForm ? 'Keine Berechtigung zum Erstellen von Spaces' : ''}
> >
{showForm ? 'Abbrechen' : '+ Neuer Space'} {showForm ? 'Abbrechen' : '+ Neuer Space'}
</button> </button>
@@ -340,11 +352,18 @@ const Spaces = () => {
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
if (canDeleteSpace(space.id)) {
handleDelete(space) handleDelete(space)
}
}} }}
className="p-2 text-red-400 hover:text-red-300 hover:bg-red-500/20 rounded-lg transition-colors" disabled={!canDeleteSpace(space.id)}
title="Space löschen" className={`p-2 rounded-lg transition-colors ${
aria-label="Space löschen" canDeleteSpace(space.id)
? 'text-red-400 hover:text-red-300 hover:bg-red-500/20'
: 'text-slate-500 cursor-not-allowed opacity-50'
}`}
title={canDeleteSpace(space.id) ? 'Space löschen' : 'Keine Berechtigung zum Löschen von Spaces'}
aria-label={canDeleteSpace(space.id) ? 'Space löschen' : 'Keine Berechtigung'}
> >
<svg <svg
className="w-5 h-5" className="w-5 h-5"

View File

@@ -1,23 +1,37 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { useAuth } from '../contexts/AuthContext' import { useAuth } from '../contexts/AuthContext'
import { usePermissions } from '../hooks/usePermissions'
const Users = () => { const Users = () => {
const { authFetch } = useAuth() const { authFetch } = useAuth()
const { refreshPermissions } = usePermissions()
const [users, setUsers] = useState([]) const [users, setUsers] = useState([])
const [groups, setGroups] = useState([])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState('') const [error, setError] = useState('')
const [showForm, setShowForm] = useState(false) const [showForm, setShowForm] = useState(false)
const [editingUser, setEditingUser] = useState(null) 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({ const [formData, setFormData] = useState({
username: '', username: '',
email: '', email: '',
oldPassword: '', oldPassword: '',
password: '', password: '',
confirmPassword: '' confirmPassword: '',
isAdmin: false,
enabled: true,
groupIds: []
}) })
const [showAdminWarning, setShowAdminWarning] = useState(false)
useEffect(() => { useEffect(() => {
fetchUsers() fetchUsers()
fetchGroups()
}, []) }, [])
const fetchUsers = async () => { const fetchUsers = async () => {
@@ -36,6 +50,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) => { const handleSubmit = async (e) => {
e.preventDefault() e.preventDefault()
setError('') setError('')
@@ -63,17 +89,25 @@ const Users = () => {
const body = editingUser const body = editingUser
? { ? {
...(formData.username && { username: formData.username }), // Username/Email nur setzen wenn nicht der spezielle Admin-User mit UID 'admin'
...(formData.email && { email: formData.email }), ...(formData.username && editingUser.id !== 'admin' && { username: formData.username }),
...(formData.email && editingUser.id !== 'admin' && { email: formData.email }),
...(formData.password && { ...(formData.password && {
password: formData.password, password: formData.password,
oldPassword: formData.oldPassword 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, username: formData.username,
email: formData.email, email: formData.email,
password: formData.password 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, { const response = await authFetch(url, {
@@ -86,9 +120,12 @@ const Users = () => {
if (response.ok) { if (response.ok) {
await fetchUsers() await fetchUsers()
setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '' }) setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '', isAdmin: false, enabled: true, groupIds: [] })
setShowForm(false) setShowForm(false)
setEditingUser(null) setEditingUser(null)
setShowAdminWarning(false)
// Aktualisiere Berechtigungen nach Änderung an Benutzern (Gruppen-Zuweisungen könnten sich geändert haben)
refreshPermissions()
} else { } else {
const errorData = await response.json() const errorData = await response.json()
setError(errorData.error || 'Fehler beim Speichern des Benutzers') setError(errorData.error || 'Fehler beim Speichern des Benutzers')
@@ -108,33 +145,122 @@ const Users = () => {
email: user.email, email: user.email,
oldPassword: '', oldPassword: '',
password: '', password: '',
confirmPassword: '' 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) setShowForm(true)
} }
const handleDelete = async (userId) => { const handleDelete = (user) => {
if (!window.confirm('Möchten Sie diesen Benutzer wirklich löschen?')) { 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 return
} }
try { try {
const response = await authFetch(`/api/users/${userId}`, { const response = await authFetch(`/api/users/${userToDelete.id}`, {
method: 'DELETE', method: 'DELETE',
}) })
if (response.ok) { if (response.ok) {
await fetchUsers() await fetchUsers()
setShowDeleteModal(false)
setUserToDelete(null)
setConfirmChecked(false)
// Aktualisiere Berechtigungen nach Löschen eines Benutzers
refreshPermissions()
} else { } else {
const errorData = await response.json() const errorData = await response.json().catch(() => ({ error: 'Fehler beim Löschen' }))
alert(errorData.error || 'Fehler beim Löschen des Benutzers') 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) { } catch (err) {
alert('Fehler beim Löschen des Benutzers')
console.error('Error deleting user:', 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) => { const handleChange = (e) => {
setFormData({ setFormData({
...formData, ...formData,
@@ -142,6 +268,44 @@ 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 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 ( return (
<div className="p-8 min-h-full bg-gradient-to-r from-slate-700 to-slate-900"> <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="max-w-4xl mx-auto">
@@ -156,7 +320,8 @@ const Users = () => {
onClick={() => { onClick={() => {
setShowForm(!showForm) setShowForm(!showForm)
setEditingUser(null) setEditingUser(null)
setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '' }) 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" 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"
> >
@@ -182,9 +347,15 @@ const Users = () => {
value={formData.username} value={formData.username}
onChange={handleChange} onChange={handleChange}
required={!editingUser} 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" 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" 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>
<div> <div>
<label htmlFor="email" className="block text-sm font-medium text-slate-200 mb-2"> <label htmlFor="email" className="block text-sm font-medium text-slate-200 mb-2">
@@ -197,9 +368,15 @@ const Users = () => {
value={formData.email} value={formData.email}
onChange={handleChange} onChange={handleChange}
required={!editingUser} 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" 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" 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> </div>
{editingUser && ( {editingUser && (
<div> <div>
@@ -284,6 +461,75 @@ const Users = () => {
<p className="mt-1 text-xs text-green-400"> Passwörter stimmen überein</p> <p className="mt-1 text-xs text-green-400"> Passwörter stimmen überein</p>
)} )}
</div> </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 && ( {error && (
<div className="p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-red-300 text-sm"> <div className="p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-red-300 text-sm">
{error} {error}
@@ -302,7 +548,8 @@ const Users = () => {
onClick={() => { onClick={() => {
setShowForm(false) setShowForm(false)
setEditingUser(null) setEditingUser(null)
setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '' }) setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '', isAdmin: false, enabled: true, groupIds: [] })
setShowAdminWarning(false)
setError('') setError('')
}} }}
className="px-6 py-2 bg-slate-600 hover:bg-slate-700 text-white font-semibold rounded-lg transition-colors duration-200" className="px-6 py-2 bg-slate-600 hover:bg-slate-700 text-white font-semibold rounded-lg transition-colors duration-200"
@@ -343,10 +590,37 @@ const Users = () => {
> >
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex-1"> <div className="flex-1">
<h3 className="text-xl font-semibold text-white mb-2"> <div className="flex items-center gap-2 mb-2">
<h3 className="text-xl font-semibold text-white">
{user.username} {user.username}
</h3> </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> <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"> <p className="text-xs text-slate-400">
Erstellt: {user.createdAt ? new Date(user.createdAt).toLocaleString('de-DE') : 'Unbekannt'} Erstellt: {user.createdAt ? new Date(user.createdAt).toLocaleString('de-DE') : 'Unbekannt'}
</p> </p>
@@ -363,12 +637,26 @@ const Users = () => {
> >
Bearbeiten Bearbeiten
</button> </button>
{user.id === 'admin' ? (
<button <button
onClick={() => handleDelete(user.id)} 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" className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-sm rounded-lg transition-colors"
> >
Löschen Löschen
</button> </button>
)}
</div> </div>
</div> </div>
</div> </div>
@@ -376,6 +664,222 @@ const Users = () => {
</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>
</div> </div>
) )