fix/sslProviderPermission #3
1914
backend/main.go
1914
backend/main.go
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 |
@@ -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>
|
||||||
<AppContent />
|
<PermissionsProvider>
|
||||||
|
<AppContent />
|
||||||
|
</PermissionsProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</Router>
|
</Router>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
102
frontend/src/contexts/PermissionsContext.jsx
Normal file
102
frontend/src/contexts/PermissionsContext.jsx
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
3
frontend/src/hooks/usePermissions.js
Normal file
3
frontend/src/hooks/usePermissions.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Re-export from PermissionsContext for backward compatibility
|
||||||
|
export { usePermissions } from '../contexts/PermissionsContext'
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
637
frontend/src/pages/Permissions.jsx
Normal file
637
frontend/src/pages/Permissions.jsx
Normal 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
|
||||||
|
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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()
|
||||||
handleRequestSigning(fqdn)
|
if (canSignCSR(id)) {
|
||||||
|
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()
|
||||||
e.currentTarget.parentElement.querySelector('input[type="file"]')?.click()
|
if (canUploadCSR(id)) {
|
||||||
|
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()
|
||||||
handleDelete(fqdn)
|
if (canDeleteFqdn(id)) {
|
||||||
|
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"
|
||||||
|
|||||||
@@ -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()
|
||||||
handleDelete(space)
|
if (canDeleteSpace(space.id)) {
|
||||||
|
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"
|
||||||
|
|||||||
@@ -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">
|
||||||
{user.username}
|
<h3 className="text-xl font-semibold text-white">
|
||||||
</h3>
|
{user.username}
|
||||||
<p className="text-slate-300 mb-2">{user.email}</p>
|
</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">
|
<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>
|
||||||
<button
|
{user.id === 'admin' ? (
|
||||||
onClick={() => handleDelete(user.id)}
|
<button
|
||||||
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-sm rounded-lg transition-colors"
|
onClick={() => handleToggleEnabled(user)}
|
||||||
>
|
className={`px-4 py-2 text-white text-sm rounded-lg transition-colors ${
|
||||||
Löschen
|
user.enabled
|
||||||
</button>
|
? '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>
|
</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>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user