From e3398a459725f1912b1ea69b526935fe0b38a06a Mon Sep 17 00:00:00 2001 From: Nick Adam Date: Fri, 21 Nov 2025 23:36:37 +0100 Subject: [PATCH] added fix for empty CSR history after uploading ones --- backend/spaces.db-shm | Bin 32768 -> 32768 bytes backend/spaces.db-wal | Bin 168952 -> 906432 bytes frontend/src/App.jsx | 3 +- frontend/src/components/Sidebar.jsx | 2 +- frontend/src/contexts/PermissionsContext.jsx | 34 ++- frontend/src/hooks/usePermissions.js | 2 - frontend/src/pages/Home.jsx | 2 +- frontend/src/pages/Profile.jsx | 2 +- frontend/src/pages/SpaceDetail.jsx | 258 ++++++++++++------- 9 files changed, 199 insertions(+), 104 deletions(-) diff --git a/backend/spaces.db-shm b/backend/spaces.db-shm index 83ea5fba082ac93cc57774034164be0cd40e2689..26433658fa0ffc7b4f1efe07f9e47c8aab0d1046 100644 GIT binary patch literal 32768 zcmeI**OOF56vy!|u&`vwELk!xQ9u+>R)T;8Ndf|jNKOJu79>ax5+y2g*b$gQTRL4CvEHXj9f9-Eoi`yx; zXD6SichX)a+`OAutZ}DYy~kg1m>=rpJg7OKL%WC24q_;k^t<7Rl&Z4|UP+h++GM{97Twg)^ectyKwBPp|nfG)1 zynQyjU{Sy#a;-bduXAf6^RAZnNGtF5yZXLH6?^@XMbKLJIb|fD`mE%Db6n5fE?OUM zzpEGI;=ao+?`!!Py@#LG)k(|E?e}{k^Uf_=-p%08_cfxO=j(aj$n#Ry^WV$Jx%~Ih zos-r~{tQd&k$?mwAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J- zkbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdB=DF5E7;0T4se{makmNR z2@=Rk9=kW8y#4Qa9U9ViraVj_!J$wAJ9 zV&eE8G8=jAW{wI}qb{%0jCOS8Lk9CHpD~^JEMqf2u#ew4#jW_~h%#X*DswhUUa8m< zsLm_AL3?^Jgpo|(OP2E^$M`4qMtQ;=m!=Bmp70qe|8xZs?arbSROBt*V+LE;&*`Vz zK+K$$pI4d5D$d7D$7(b^f$Zd?B$ezgw|X?9d3wu3oG-v@%wjbcLTsSX!zz%8f~3%b z4)o@8zGfS{IK;)Ua%k>Q31qf=9^a%T9qGeRMlp#6e8YBjbC^H46eP?O6s*tc4iK*j}Lq!w2)UPcup zAb~g*XhkP_(4S$9VG6V3cv6if0SQPz0uqpb1X{CL2yrIRm$m%N)i|3<<4E9P1p4tY zqglrQVjKw?y@r^rtKF>{BAXFVdBkpOe;Hg17s57--;1yb<9F z1NIV;b6PI0j3YFVAZ1*tX`(||t93ZWHF2e;i=hW=_i+M$aS<7VDj^k#X}OpHwIUnA g0b#91Q7J$!yjf>)h*Mmm$GB9t$N!(9jXq-{Tb8s%00000 diff --git a/backend/spaces.db-wal b/backend/spaces.db-wal index 31d02e78cadebc0ac3766c212dff452a2a785865..508294c45d3be79bd88d8c0893e048b631ba5363 100644 GIT binary patch delta 161356 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 b>+;7 diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index d030f7a..9de4620 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -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' diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx index 9b7ce3a..a5968e5 100644 --- a/frontend/src/components/Sidebar.jsx +++ b/frontend/src/components/Sidebar.jsx @@ -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 }) => { diff --git a/frontend/src/contexts/PermissionsContext.jsx b/frontend/src/contexts/PermissionsContext.jsx index 8ff1ee3..a59fbb8 100644 --- a/frontend/src/contexts/PermissionsContext.jsx +++ b/frontend/src/contexts/PermissionsContext.jsx @@ -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) diff --git a/frontend/src/hooks/usePermissions.js b/frontend/src/hooks/usePermissions.js index 24ea552..92003f9 100644 --- a/frontend/src/hooks/usePermissions.js +++ b/frontend/src/hooks/usePermissions.js @@ -1,3 +1 @@ -// Re-export from PermissionsContext for backward compatibility export { usePermissions } from '../contexts/PermissionsContext' - diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index 0bcf51e..c486274 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -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() diff --git a/frontend/src/pages/Profile.jsx b/frontend/src/pages/Profile.jsx index ade551d..b5f1b63 100644 --- a/frontend/src/pages/Profile.jsx +++ b/frontend/src/pages/Profile.jsx @@ -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() diff --git a/frontend/src/pages/SpaceDetail.jsx b/frontend/src/pages/SpaceDetail.jsx index 0f28d2d..6e92f76 100644 --- a/frontend/src/pages/SpaceDetail.jsx +++ b/frontend/src/pages/SpaceDetail.jsx @@ -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 = () => {
CSR History
{(() => { + // 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 = () => {
-

Zertifikate für {selectedFqdn.fqdn}

+

+ Zertifikate für {selectedFqdn?.fqdn || 'Unbekannter FQDN'} +

- {certificates.map((cert, index) => ( -
-
-
-
- - #{certificates.length - index} - -

CA-Zertifikat-ID: {cert.certificateId}

-
-
-

- Interne UID:{' '} - {cert.id} -

-

- Erstellt:{' '} - {new Date(cert.createdAt).toLocaleString('de-DE')} -

-

- Status:{' '} - - {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 ( +

+
+
+
+ + #{certificates.length - index} -

- {cert.providerId && ( -

- Provider:{' '} - {cert.providerId} +

CA-Zertifikat-ID: {certCertificateId}

+
+
+

+ Interne UID:{' '} + {certId}

- )} + {certCreatedAt && !isNaN(certCreatedAt.getTime()) && ( +

+ Erstellt:{' '} + {certCreatedAt.toLocaleString('de-DE')} +

+ )} +

+ Status:{' '} + + {certStatus} + +

+ {certProviderId && ( +

+ Provider:{' '} + {certProviderId} +

+ )} +
+
- + {certPEM && ( +
+
Zertifikat (PEM):
+
+                              {certPEM}
+                            
+
+ )}
- {cert.certificatePEM && ( -
-
Zertifikat (PEM):
-
-                            {cert.certificatePEM}
-                          
-
- )} -
- ))} + ) + })}
)}