added fix for empty CSR history after uploading ones

This commit is contained in:
2025-11-21 23:36:37 +01:00
parent 16043e2577
commit e3398a4597
9 changed files with 199 additions and 104 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -1,7 +1,8 @@
import { useState } from 'react'
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
import { AuthProvider, useAuth } from './contexts/AuthContext'
import { PermissionsProvider, usePermissions } from './contexts/PermissionsContext'
import { PermissionsProvider } from './contexts/PermissionsContext'
import { usePermissions } from './hooks/usePermissions'
import Sidebar from './components/Sidebar'
import Footer from './components/Footer'
import Home from './pages/Home'

View File

@@ -1,6 +1,6 @@
import { Link, useLocation, useNavigate } from 'react-router-dom'
import { useAuth } from '../contexts/AuthContext'
import { usePermissions } from '../contexts/PermissionsContext'
import { usePermissions } from '../hooks/usePermissions'
import { useState, useEffect } from 'react'
const Sidebar = ({ isOpen, setIsOpen }) => {

View File

@@ -35,21 +35,31 @@ export const PermissionsProvider = ({ children }) => {
}
const response = await authFetch('/api/user/permissions')
if (response.ok && isMountedRef.current) {
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 || {},
})
try {
const data = await response.json()
// Nur Permissions aktualisieren, wenn Daten erfolgreich geparst wurden
setPermissions({
isAdmin: data.isAdmin || false,
hasFullAccess: data.hasFullAccess || false,
accessibleSpaces: Array.isArray(data.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 (parseErr) {
console.error('Error parsing permissions response:', parseErr)
// Bei Parse-Fehler Permissions nicht zurücksetzen, nur loggen
}
} else if (response.status === 401 && isMountedRef.current) {
// Bei 401 Unauthorized werden Permissions zurückgesetzt (wird von AuthContext gehandelt)
console.log('Unauthorized - permissions will be cleared by auth context')
}
} catch (err) {
console.error('Error fetching permissions:', err)
// Bei Netzwerkfehlern etc. Permissions nicht zurücksetzen
} finally {
if (isInitial && isMountedRef.current) {
setLoading(false)

View File

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

View File

@@ -1,7 +1,7 @@
import { useEffect, useState, useRef, useCallback } from 'react'
import { useLocation } from 'react-router-dom'
import { useAuth } from '../contexts/AuthContext'
import { usePermissions } from '../contexts/PermissionsContext'
import { usePermissions } from '../hooks/usePermissions'
const Home = () => {
const { authFetch } = useAuth()

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import { useAuth } from '../contexts/AuthContext'
import { usePermissions } from '../contexts/PermissionsContext'
import { usePermissions } from '../hooks/usePermissions'
const Profile = () => {
const { authFetch, user } = useAuth()

View File

@@ -232,18 +232,50 @@ const SpaceDetail = () => {
if (response.ok) {
const csr = await response.json()
// Füge den neuen CSR zur History hinzu (nur wenn der Bereich bereits geöffnet ist)
if (showCSRDropdown[fqdn.id]) {
const newCsrWithFqdnId = { ...csr, fqdnId: fqdn.id }
setCsrHistory(prev => {
const filtered = prev.filter(csrItem => csrItem.fqdnId !== fqdn.id)
// Füge den neuen CSR am Anfang hinzu (neuester zuerst)
return [newCsrWithFqdnId, ...filtered]
})
}
setCsrData(csr)
setSelectedFqdn(fqdn)
// Lade die komplette CSR History neu, um den neuen CSR anzuzeigen
// WICHTIG: Warte auf die History bevor der Dropdown geöffnet wird
try {
const historyResponse = await authFetch(`/api/spaces/${id}/fqdns/${fqdn.id}/csr`)
if (historyResponse.ok) {
const history = await historyResponse.json()
// Stelle sicher, dass history ein Array ist
const historyArray = Array.isArray(history) ? history : []
// Füge fqdnId zu jedem CSR hinzu und stelle sicher dass sie immer gesetzt ist
// Auch wenn die API fqdnId bereits zurückgibt, überschreiben wir sie mit fqdn.id für Konsistenz
// Stelle sicher dass alle CSRs gültig sind und fqdnId gesetzt ist
const historyWithFqdnId = historyArray
.filter(csrItem => csrItem && csrItem.id) // Stelle sicher dass CSR gültig ist
.map(csrItem => ({
...csrItem,
fqdnId: String(fqdn.id) // Immer als String für konsistente Filterung
}))
setCsrHistory(prev => {
// Entferne alte CSRs für diesen FQDN und füge die neuen hinzu
// Verwende String-Vergleich für Robustheit
const filtered = prev.filter(csrItem => String(csrItem?.fqdnId) !== String(fqdn.id))
return [...filtered, ...historyWithFqdnId]
})
// Öffne den CSR History Dropdown NACH dem Laden der History
setShowCSRDropdown(prev => ({ ...prev, [fqdn.id]: true }))
} else {
setCsrHistory(prev => {
// Bei Fehler, entferne nur CSRs für diesen FQDN, behalte andere
return prev.filter(csrItem => String(csrItem?.fqdnId) !== String(fqdn.id))
})
}
} catch (err) {
console.error('Error fetching CSR history after upload:', err)
// Bei Fehler, entferne nur CSRs für diesen FQDN
setCsrHistory(prev => prev.filter(csrItem => String(csrItem?.fqdnId) !== String(fqdn.id)))
}
setShowCSRModal(true)
// Aktualisiere die FQDN-Liste
@@ -298,14 +330,23 @@ const SpaceDetail = () => {
const historyResponse = await authFetch(`/api/spaces/${id}/fqdns/${fqdn.id}/csr`)
if (historyResponse.ok) {
const history = await historyResponse.json()
setCsrHistory(Array.isArray(history) ? history : [])
const historyArray = Array.isArray(history) ? history : []
// Stelle sicher dass fqdnId gesetzt ist für konsistente Filterung
const historyWithFqdnId = historyArray
.filter(csr => csr && csr.id)
.map(csr => ({ ...csr, fqdnId: String(fqdn.id) }))
setCsrHistory(prev => {
const filtered = prev.filter(csr => String(csr?.fqdnId) !== String(fqdn.id))
return [...filtered, ...historyWithFqdnId]
})
} else {
setCsrHistory([])
setCsrHistory(prev => prev.filter(csr => String(csr?.fqdnId) !== String(fqdn.id)))
}
} catch (err) {
console.error('Error fetching CSR:', err)
setCsrData(null)
setCsrHistory([])
// Entferne nur CSRs für diesen FQDN, behalte andere
setCsrHistory(prev => prev.filter(csr => String(csr?.fqdnId) !== String(fqdn.id)))
}
}
@@ -319,7 +360,7 @@ const SpaceDetail = () => {
setSelectedFqdn(null)
setCsrData(null)
setCsrError('')
setCsrHistory([])
// csrHistory NICHT zurücksetzen - bleibt für Dropdown-Anzeige erhalten
}
const handleChange = (e) => {
@@ -423,40 +464,64 @@ const SpaceDetail = () => {
}
const handleViewCertificates = async (fqdn) => {
if (!fqdn || !fqdn.id) {
console.error('Invalid FQDN provided to handleViewCertificates')
return
}
setSelectedFqdn(fqdn)
setLoadingCertificates(true)
setCertificates([])
setShowCertificatesModal(true) // Öffne Modal sofort, auch wenn noch geladen wird
try {
const response = await authFetch(`/api/spaces/${id}/fqdns/${fqdn.id}/certificates`)
if (response.ok) {
const certs = await response.json()
setCertificates(certs)
try {
const certs = await response.json()
// Stelle sicher, dass certs ein Array ist
setCertificates(Array.isArray(certs) ? certs : [])
} catch (parseErr) {
console.error('Error parsing certificates response:', parseErr)
setCertificates([])
}
} else {
console.error('Fehler beim Laden der Zertifikate')
// Bei Fehler-Response (404, 403, etc.) setze leeres Array
console.error('Fehler beim Laden der Zertifikate:', response.status, response.statusText)
setCertificates([])
}
} catch (err) {
console.error('Error fetching certificates:', err)
setCertificates([])
} finally {
setLoadingCertificates(false)
setShowCertificatesModal(true)
}
}
const handleRefreshCertificate = async (cert) => {
if (!cert || !cert.id || !selectedFqdn || !selectedFqdn.id) {
console.error('Invalid certificate or FQDN for refresh')
return
}
setRefreshingCertificate(cert.id)
try {
const response = await authFetch(`/api/spaces/${id}/fqdns/${selectedFqdn.id}/certificates/${cert.id}/refresh`, {
method: 'POST'
})
if (response.ok) {
const result = await response.json()
// Aktualisiere Zertifikat in der Liste
setCertificates(prev => prev.map(c =>
c.id === cert.id
? { ...c, certificatePEM: result.certificatePEM }
: c
))
try {
const result = await response.json()
// Aktualisiere Zertifikat in der Liste
setCertificates(prev => prev.map(c =>
c.id === cert.id
? { ...c, certificatePEM: result.certificatePEM }
: c
))
} catch (parseErr) {
console.error('Error parsing refresh response:', parseErr)
}
}
} catch (err) {
console.error('Error refreshing certificate:', err)
@@ -798,11 +863,12 @@ const SpaceDetail = () => {
if (response.ok) {
const history = await response.json()
// Speichere History mit FQDN-ID als Key
const historyWithFqdnId = Array.isArray(history)
? history.map(csr => ({ ...csr, fqdnId: fqdn.id }))
: []
const historyArray = Array.isArray(history) ? history : []
const historyWithFqdnId = historyArray
.filter(csr => csr && csr.id)
.map(csr => ({ ...csr, fqdnId: String(fqdn.id) }))
setCsrHistory(prev => {
const filtered = prev.filter(csr => csr.fqdnId !== fqdn.id)
const filtered = prev.filter(csr => String(csr?.fqdnId) !== String(fqdn.id))
return [...filtered, ...historyWithFqdnId]
})
}
@@ -866,12 +932,13 @@ const SpaceDetail = () => {
<div className="border-t border-slate-600/50 bg-slate-800/50 p-4">
<h5 className="text-sm font-semibold text-slate-300 mb-3">CSR History</h5>
{(() => {
// Filtere CSRs für diesen FQDN - verwende String-Vergleich für Robustheit
const fqdnHistory = csrHistory
.filter(csr => csr.fqdnId === fqdn.id)
.filter(csr => csr && String(csr.fqdnId) === String(fqdn.id))
.sort((a, b) => {
// Sortiere nach created_at, neueste zuerst
const dateA = new Date(a.createdAt).getTime()
const dateB = new Date(b.createdAt).getTime()
const dateA = a.createdAt ? new Date(a.createdAt).getTime() : 0
const dateB = b.createdAt ? new Date(b.createdAt).getTime() : 0
return dateB - dateA
})
return fqdnHistory.length > 0 ? (
@@ -1478,10 +1545,13 @@ const SpaceDetail = () => {
<div className="bg-slate-800 rounded-lg border border-slate-600 max-w-4xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-white">Zertifikate für {selectedFqdn.fqdn}</h2>
<h2 className="text-2xl font-bold text-white">
Zertifikate für {selectedFqdn?.fqdn || 'Unbekannter FQDN'}
</h2>
<button
onClick={closeCertificatesModal}
className="text-slate-400 hover:text-white transition-colors"
aria-label="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" />
@@ -1501,64 +1571,80 @@ const SpaceDetail = () => {
{certificates.length} {certificates.length === 1 ? 'Zertifikat' : 'Zertifikate'} gefunden
</p>
</div>
{certificates.map((cert, index) => (
<div key={cert.id} className="bg-slate-700/30 rounded-lg p-4 border border-slate-600">
<div className="flex justify-between items-start mb-3">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<span className="text-xs font-semibold text-purple-400 bg-purple-500/20 px-2 py-1 rounded">
#{certificates.length - index}
</span>
<h4 className="text-white font-semibold">CA-Zertifikat-ID: {cert.certificateId}</h4>
</div>
<div className="space-y-1">
<p className="text-slate-400 text-xs">
<span className="font-semibold text-slate-300">Interne UID:</span>{' '}
<span className="font-mono text-xs">{cert.id}</span>
</p>
<p className="text-slate-400 text-sm">
<span className="font-semibold text-slate-300">Erstellt:</span>{' '}
{new Date(cert.createdAt).toLocaleString('de-DE')}
</p>
<p className="text-slate-400 text-sm">
<span className="font-semibold text-slate-300">Status:</span>{' '}
<span className={`inline-block px-2 py-0.5 rounded text-xs ${
cert.status === 'issued'
? 'bg-green-500/20 text-green-400'
: cert.status === 'pending'
? 'bg-yellow-500/20 text-yellow-400'
: 'bg-red-500/20 text-red-400'
}`}>
{cert.status}
{certificates.map((cert, index) => {
// Sicherstellen, dass cert ein gültiges Objekt ist
if (!cert || !cert.id) {
return null
}
const certId = cert.id || 'unknown'
const certCertificateId = cert.certificateId || 'N/A'
const certCreatedAt = cert.createdAt ? new Date(cert.createdAt) : null
const certStatus = cert.status || 'unknown'
const certProviderId = cert.providerId
const certPEM = cert.certificatePEM
return (
<div key={certId} className="bg-slate-700/30 rounded-lg p-4 border border-slate-600">
<div className="flex justify-between items-start mb-3">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<span className="text-xs font-semibold text-purple-400 bg-purple-500/20 px-2 py-1 rounded">
#{certificates.length - index}
</span>
</p>
{cert.providerId && (
<p className="text-slate-400 text-sm">
<span className="font-semibold text-slate-300">Provider:</span>{' '}
{cert.providerId}
<h4 className="text-white font-semibold">CA-Zertifikat-ID: {certCertificateId}</h4>
</div>
<div className="space-y-1">
<p className="text-slate-400 text-xs">
<span className="font-semibold text-slate-300">Interne UID:</span>{' '}
<span className="font-mono text-xs">{certId}</span>
</p>
)}
{certCreatedAt && !isNaN(certCreatedAt.getTime()) && (
<p className="text-slate-400 text-sm">
<span className="font-semibold text-slate-300">Erstellt:</span>{' '}
{certCreatedAt.toLocaleString('de-DE')}
</p>
)}
<p className="text-slate-400 text-sm">
<span className="font-semibold text-slate-300">Status:</span>{' '}
<span className={`inline-block px-2 py-0.5 rounded text-xs ${
certStatus === 'issued'
? 'bg-green-500/20 text-green-400'
: certStatus === 'pending'
? 'bg-yellow-500/20 text-yellow-400'
: 'bg-red-500/20 text-red-400'
}`}>
{certStatus}
</span>
</p>
{certProviderId && (
<p className="text-slate-400 text-sm">
<span className="font-semibold text-slate-300">Provider:</span>{' '}
{certProviderId}
</p>
)}
</div>
</div>
<button
onClick={() => handleRefreshCertificate(cert)}
disabled={refreshingCertificate === certId || !selectedFqdn}
className="ml-4 px-3 py-1 bg-blue-600 hover:bg-blue-700 disabled:bg-slate-600 disabled:cursor-not-allowed text-white text-sm rounded-lg transition-colors"
title="Zertifikat von CA abrufen"
>
{refreshingCertificate === certId ? 'Aktualisiere...' : 'Aktualisieren'}
</button>
</div>
<button
onClick={() => handleRefreshCertificate(cert)}
disabled={refreshingCertificate === cert.id}
className="ml-4 px-3 py-1 bg-blue-600 hover:bg-blue-700 disabled:bg-slate-600 disabled:cursor-not-allowed text-white text-sm rounded-lg transition-colors"
title="Zertifikat von CA abrufen"
>
{refreshingCertificate === cert.id ? 'Aktualisiere...' : 'Aktualisieren'}
</button>
{certPEM && (
<div className="mt-3">
<h5 className="text-sm font-semibold text-slate-300 mb-2">Zertifikat (PEM):</h5>
<pre className="text-xs text-slate-200 bg-slate-900/50 p-3 rounded overflow-auto max-h-60 font-mono">
{certPEM}
</pre>
</div>
)}
</div>
{cert.certificatePEM && (
<div className="mt-3">
<h5 className="text-sm font-semibold text-slate-300 mb-2">Zertifikat (PEM):</h5>
<pre className="text-xs text-slate-200 bg-slate-900/50 p-3 rounded overflow-auto max-h-60 font-mono">
{cert.certificatePEM}
</pre>
</div>
)}
</div>
))}
)
})}
</div>
)}
</div>