Files
certigo/frontend/src/pages/SpaceDetail.jsx

1574 lines
69 KiB
JavaScript

import { useState, useEffect } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { useAuth } from '../contexts/AuthContext'
import { usePermissions } from '../hooks/usePermissions'
const SpaceDetail = () => {
const { id } = useParams()
const navigate = useNavigate()
const { authFetch } = useAuth()
const { canCreateFqdn, canDeleteFqdn, canSignCSR, canUploadCSR, refreshPermissions } = usePermissions()
const [space, setSpace] = useState(null)
const [fqdns, setFqdns] = useState([])
const [showForm, setShowForm] = useState(false)
const [formData, setFormData] = useState({
fqdn: '',
description: ''
})
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [fetchError, setFetchError] = useState('')
const [loadingSpace, setLoadingSpace] = useState(true)
const [showDeleteModal, setShowDeleteModal] = useState(false)
const [fqdnToDelete, setFqdnToDelete] = useState(null)
const [confirmChecked, setConfirmChecked] = useState(false)
const [selectedFqdn, setSelectedFqdn] = useState(null)
const [csrData, setCsrData] = useState(null)
const [csrHistory, setCsrHistory] = useState([])
const [uploadingCSR, setUploadingCSR] = useState(false)
const [csrError, setCsrError] = useState('')
const [showCSRModal, setShowCSRModal] = useState(false)
const [showCSRDropdown, setShowCSRDropdown] = useState({})
const [copiedFqdnId, setCopiedFqdnId] = useState(null)
const [showSignCSRModal, setShowSignCSRModal] = useState(false)
const [signCSRStep, setSignCSRStep] = useState(1)
const [selectedProvider, setSelectedProvider] = useState(null)
const [providers, setProviders] = useState([])
const [providerTestResult, setProviderTestResult] = useState(null)
const [signingCSR, setSigningCSR] = useState(false)
const [signResult, setSignResult] = useState(null)
const [showCertificatesModal, setShowCertificatesModal] = useState(false)
const [certificates, setCertificates] = useState([])
const [loadingCertificates, setLoadingCertificates] = useState(false)
const [refreshingCertificate, setRefreshingCertificate] = useState(null)
useEffect(() => {
fetchSpace()
fetchFqdns()
}, [id])
const fetchSpace = async () => {
try {
setLoadingSpace(true)
const response = await authFetch('/api/spaces')
if (response.ok) {
const spaces = await response.json()
const foundSpace = spaces.find(s => s.id === id)
if (foundSpace) {
setSpace(foundSpace)
} else {
setFetchError('Space nicht gefunden')
}
} else {
setFetchError('Fehler beim Laden des Space')
}
} catch (err) {
console.error('Error fetching space:', err)
setFetchError('Fehler beim Laden des Space')
} finally {
setLoadingSpace(false)
}
}
const fetchFqdns = async () => {
try {
setFetchError('')
const response = await authFetch(`/api/spaces/${id}/fqdns`)
if (response.ok) {
const data = await response.json()
setFqdns(Array.isArray(data) ? data : [])
} else {
if (response.status !== 404) {
const errorText = `Fehler beim Abrufen der FQDNs: ${response.status}`
console.error(errorText)
setFetchError(errorText)
}
setFqdns([])
}
} catch (err) {
console.error('Error fetching fqdns:', err)
setFqdns([])
}
}
const handleSubmit = async (e) => {
e.preventDefault()
setError('')
setLoading(true)
if (!formData.fqdn.trim()) {
setError('Bitte geben Sie einen FQDN ein.')
setLoading(false)
return
}
// Einfache FQDN-Validierung
const fqdnPattern = /^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/
if (!fqdnPattern.test(formData.fqdn.trim())) {
setError('Bitte geben Sie einen gültigen FQDN ein (z.B. example.com, subdomain.example.com)')
setLoading(false)
return
}
try {
const response = await authFetch(`/api/spaces/${id}/fqdns`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
})
if (response.ok) {
const newFqdn = await response.json()
setFqdns([...fqdns, newFqdn])
setFormData({ fqdn: '', description: '' })
setShowForm(false)
// Aktualisiere Berechtigungen nach dem Erstellen eines FQDNs
refreshPermissions()
} else {
let errorMessage = 'Fehler beim Erstellen des FQDN'
try {
const errorText = await response.text()
if (errorText) {
// Versuche JSON zu parsen
try {
const errorData = JSON.parse(errorText)
errorMessage = errorData.error || errorText
} catch {
// Wenn kein JSON, verwende den Text direkt
errorMessage = errorText
}
} else if (response.status === 409) {
errorMessage = 'Dieser FQDN existiert bereits'
}
} catch (err) {
// Fallback auf Standard-Fehlermeldung
if (response.status === 409) {
errorMessage = 'Dieser FQDN existiert bereits'
}
}
setError(errorMessage)
}
} catch (err) {
setError('Fehler beim Erstellen des FQDN')
console.error('Error creating fqdn:', err)
} finally {
setLoading(false)
}
}
const handleDelete = (fqdn) => {
setFqdnToDelete(fqdn)
setShowDeleteModal(true)
setConfirmChecked(false)
}
const confirmDelete = async () => {
if (!confirmChecked || !fqdnToDelete) {
return
}
try {
const response = await authFetch(`/api/spaces/${id}/fqdns/${fqdnToDelete.id}`, {
method: 'DELETE',
})
if (response.ok) {
setFqdns(fqdns.filter(fqdn => fqdn.id !== fqdnToDelete.id))
setShowDeleteModal(false)
setFqdnToDelete(null)
setConfirmChecked(false)
// Aktualisiere Berechtigungen nach dem Löschen eines FQDNs
refreshPermissions()
} else {
const errorData = await response.json().catch(() => ({ error: 'Fehler beim Löschen' }))
alert(errorData.error || 'Fehler beim Löschen des FQDN')
}
} catch (err) {
console.error('Error deleting fqdn:', err)
alert('Fehler beim Löschen des FQDN')
}
}
const cancelDelete = () => {
setShowDeleteModal(false)
setFqdnToDelete(null)
setConfirmChecked(false)
}
const copyFqdnIdToClipboard = async (fqdnId, e) => {
e.stopPropagation()
try {
await navigator.clipboard.writeText(fqdnId)
setCopiedFqdnId(fqdnId)
setTimeout(() => setCopiedFqdnId(null), 2000)
} catch (err) {
console.error('Fehler beim Kopieren:', err)
}
}
const handleCSRUpload = async (fqdn, file) => {
if (!file) {
setCsrError('Bitte wählen Sie eine Datei aus')
return
}
setUploadingCSR(true)
setCsrError('')
const formData = new FormData()
formData.append('csr', file)
formData.append('spaceId', id)
formData.append('fqdn', fqdn.fqdn)
try {
const response = await authFetch(`/api/spaces/${id}/fqdns/${fqdn.id}/csr`, {
method: 'POST',
body: formData,
})
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)
setShowCSRModal(true)
// Aktualisiere die FQDN-Liste
fetchFqdns()
} else {
const errorText = await response.text()
setCsrError(errorText || 'Fehler beim Hochladen des CSR')
}
} catch (err) {
console.error('Error uploading CSR:', err)
setCsrError('Fehler beim Hochladen des CSR')
} finally {
setUploadingCSR(false)
}
}
const fetchCSR = async (fqdn) => {
try {
const response = await authFetch(`/api/spaces/${id}/fqdns/${fqdn.id}/csr`)
if (response.ok) {
const csr = await response.json()
if (csr) {
setCsrData(csr)
setSelectedFqdn(fqdn)
} else {
setCsrData(null)
setSelectedFqdn(fqdn)
}
}
} catch (err) {
console.error('Error fetching CSR:', err)
}
}
const handleViewCSR = async (fqdn) => {
setSelectedFqdn(fqdn)
setCsrError('')
setShowCSRModal(true)
// Lade neuesten CSR und alle CSRs für History
try {
// Lade neuesten CSR
const latestResponse = await authFetch(`/api/spaces/${id}/fqdns/${fqdn.id}/csr?latest=true`)
if (latestResponse.ok) {
const csr = await latestResponse.json()
setCsrData(csr || null)
} else {
setCsrData(null)
}
// Lade alle CSRs für History
const historyResponse = await authFetch(`/api/spaces/${id}/fqdns/${fqdn.id}/csr`)
if (historyResponse.ok) {
const history = await historyResponse.json()
setCsrHistory(Array.isArray(history) ? history : [])
} else {
setCsrHistory([])
}
} catch (err) {
console.error('Error fetching CSR:', err)
setCsrData(null)
setCsrHistory([])
}
}
const handleSelectCSR = (csr) => {
setCsrData(csr)
setShowCSRDropdown({})
}
const closeCSRModal = () => {
setShowCSRModal(false)
setSelectedFqdn(null)
setCsrData(null)
setCsrError('')
setCsrHistory([])
}
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
})
}
const handleRequestSigning = async (fqdn) => {
setSelectedFqdn(fqdn)
setSignCSRStep(1)
setSelectedProvider(null)
setProviderTestResult(null)
setSignResult(null)
// Lade neuesten CSR
try {
const response = await authFetch(`/api/spaces/${id}/fqdns/${fqdn.id}/csr?latest=true`)
if (response.ok) {
const csr = await response.json()
setCsrData(csr)
}
} catch (err) {
console.error('Error fetching CSR:', err)
}
// Lade Provider
try {
const response = await authFetch('/api/providers')
if (response.ok) {
const providersData = await response.json()
setProviders(providersData.filter(p => p.enabled))
}
} catch (err) {
console.error('Error fetching providers:', err)
}
setShowSignCSRModal(true)
}
const handleTestProvider = async (providerId) => {
setProviderTestResult(null)
try {
const response = await authFetch(`/api/providers/${providerId}/test`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
})
const result = await response.json()
setProviderTestResult(result)
} catch (err) {
setProviderTestResult({ success: false, message: 'Fehler beim Testen des Providers' })
}
}
const handleSignCSR = async () => {
if (!selectedProvider || !selectedFqdn) return
setSigningCSR(true)
setSignResult(null)
try {
const response = await authFetch(`/api/spaces/${id}/fqdns/${selectedFqdn.id}/csr/sign`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
providerId: selectedProvider.id
})
})
const result = await response.json()
setSignResult(result)
if (result.success) {
// Lade Zertifikate automatisch neu, um das neue Zertifikat anzuzeigen
try {
const certResponse = await authFetch(`/api/spaces/${id}/fqdns/${selectedFqdn.id}/certificates`)
if (certResponse.ok) {
const certs = await certResponse.json()
setCertificates(certs)
}
} catch (err) {
console.error('Fehler beim Laden der Zertifikate nach Signierung:', err)
}
}
} catch (err) {
setSignResult({ success: false, message: 'Fehler beim Signieren des CSR' })
} finally {
setSigningCSR(false)
}
}
const closeSignCSRModal = () => {
setShowSignCSRModal(false)
setSignCSRStep(1)
setSelectedProvider(null)
setProviderTestResult(null)
setSignResult(null)
setSelectedFqdn(null)
}
const handleViewCertificates = async (fqdn) => {
setSelectedFqdn(fqdn)
setLoadingCertificates(true)
setCertificates([])
try {
const response = await authFetch(`/api/spaces/${id}/fqdns/${fqdn.id}/certificates`)
if (response.ok) {
const certs = await response.json()
setCertificates(certs)
} else {
console.error('Fehler beim Laden der Zertifikate')
}
} catch (err) {
console.error('Error fetching certificates:', err)
} finally {
setLoadingCertificates(false)
setShowCertificatesModal(true)
}
}
const handleRefreshCertificate = async (cert) => {
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
))
}
} catch (err) {
console.error('Error refreshing certificate:', err)
} finally {
setRefreshingCertificate(null)
}
}
const closeCertificatesModal = () => {
setShowCertificatesModal(false)
setCertificates([])
setSelectedFqdn(null)
}
if (loadingSpace) {
return (
<div className="p-8 min-h-full bg-gradient-to-r from-slate-700 to-slate-900">
<div className="max-w-4xl mx-auto">
<p className="text-slate-300 text-center py-8">Lade Space...</p>
</div>
</div>
)
}
if (!space) {
return (
<div className="p-8 min-h-full bg-gradient-to-r from-slate-700 to-slate-900">
<div className="max-w-4xl mx-auto">
<div className="bg-slate-800/80 backdrop-blur-sm rounded-lg shadow-xl border border-slate-600/50 p-6">
<p className="text-red-300 mb-4">{fetchError || 'Space nicht gefunden'}</p>
<button
onClick={() => navigate('/spaces')}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-all duration-200"
>
Zurück zu Spaces
</button>
</div>
</div>
</div>
)
}
return (
<div className="p-8 min-h-full bg-gradient-to-r from-slate-700 to-slate-900">
<div className="max-w-4xl mx-auto">
<div className="mb-6">
<button
onClick={() => navigate('/spaces')}
className="mb-4 px-4 py-2 bg-slate-600 hover:bg-slate-700 text-white rounded-lg transition-colors flex items-center"
>
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 19l-7-7 7-7" />
</svg>
Zurück zu Spaces
</button>
<h1 className="text-4xl font-bold text-white mb-2">{space.name}</h1>
{space.description && (
<p className="text-lg text-slate-200 mb-4">{space.description}</p>
)}
</div>
{/* Create FQDN Form */}
{showForm && (
<div className="bg-slate-800/80 backdrop-blur-sm rounded-lg shadow-xl border border-slate-600/50 p-6 mb-6">
<h3 className="text-2xl font-semibold text-white mb-4">
Neuen FQDN erstellen
</h3>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="fqdn" className="block text-sm font-medium text-slate-200 mb-2">
FQDN (Fully Qualified Domain Name) *
</label>
<input
type="text"
id="fqdn"
name="fqdn"
value={formData.fqdn}
onChange={handleChange}
className="w-full px-4 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="z.B. example.com oder subdomain.example.com"
required
/>
</div>
<div>
<label htmlFor="description" className="block text-sm font-medium text-slate-200 mb-2">
Beschreibung
</label>
<textarea
id="description"
name="description"
value={formData.description}
onChange={handleChange}
rows="3"
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 resize-none"
placeholder="Optionale Beschreibung für diesen FQDN"
/>
</div>
{error && (
<div className="p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-red-300 text-sm">
{error}
</div>
)}
<div className="flex gap-3">
<button
type="submit"
disabled={loading}
className="px-6 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-800 disabled:cursor-not-allowed text-white font-semibold rounded-lg transition-all duration-200"
>
{loading ? 'Wird erstellt...' : 'FQDN erstellen'}
</button>
<button
type="button"
onClick={() => {
setShowForm(false)
setFormData({ fqdn: '', description: '' })
setError('')
}}
className="px-6 py-2 bg-slate-600 hover:bg-slate-700 text-white font-semibold rounded-lg transition-colors duration-200"
>
Abbrechen
</button>
</div>
</form>
</div>
)}
{/* FQDNs List */}
<div className="bg-slate-800/80 backdrop-blur-sm rounded-lg shadow-xl border border-slate-600/50 p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-2xl font-semibold text-white">
FQDN-Liste
</h3>
<button
onClick={() => setShowForm(!showForm)}
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'}
</button>
</div>
{fetchError && (
<div className="mb-4 p-4 bg-red-500/20 border border-red-500/50 rounded-lg">
<p className="text-red-300 mb-2">{fetchError}</p>
<button
onClick={fetchFqdns}
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-all duration-200"
>
Erneut versuchen
</button>
</div>
)}
{!fetchError && fqdns.length === 0 ? (
<p className="text-slate-300 text-center py-8">
Noch keine FQDNs vorhanden. Erstellen Sie Ihren ersten FQDN!
</p>
) : (
<div className="space-y-4">
{fqdns.map((fqdn) => (
<div
key={fqdn.id || Math.random()}
className="bg-slate-700/50 rounded-lg border border-slate-600/50 hover:border-slate-500 transition-colors overflow-hidden shadow-soft"
>
<div className="p-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<h4 className="text-lg font-semibold text-white mb-1 font-mono">
{fqdn.fqdn || 'Unbenannt'}
</h4>
{fqdn.description && (
<p className="text-slate-300 mb-2">{fqdn.description}</p>
)}
<p className="text-xs text-slate-400">
Erstellt: {fqdn.createdAt ? new Date(fqdn.createdAt).toLocaleString('de-DE') : 'Unbekannt'}
</p>
</div>
<div className="flex gap-2 ml-4">
<button
onClick={(e) => {
e.stopPropagation()
if (canSignCSR(id)) {
handleRequestSigning(fqdn)
}
}}
disabled={!canSignCSR(id)}
className={`p-2 rounded-lg transition-colors ${
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
className="w-5 h-5"
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>
</button>
<button
onClick={(e) => copyFqdnIdToClipboard(fqdn.id, e)}
className="p-2 text-blue-400 hover:text-blue-300 hover:bg-blue-500/20 rounded-lg transition-colors"
title={copiedFqdnId === fqdn.id ? 'Kopiert!' : 'FQDN-ID kopieren'}
aria-label="FQDN-ID kopieren"
>
{copiedFqdnId === fqdn.id ? (
<svg
className="w-5 h-5"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path d="M5 13l4 4L19 7" />
</svg>
) : (
<svg
className="w-5 h-5"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
)}
</button>
<label className="cursor-pointer">
<input
type="file"
accept=".pem,.csr"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0]
if (file) {
handleCSRUpload(fqdn, file)
}
e.target.value = ''
}}
disabled={uploadingCSR || !canUploadCSR(id)}
/>
<button
disabled={!canUploadCSR(id)}
className={`p-2 rounded-lg transition-colors ${
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) => {
e.stopPropagation()
e.preventDefault()
if (canUploadCSR(id)) {
e.currentTarget.parentElement.querySelector('input[type="file"]')?.click()
}
}}
>
<svg
className="w-5 h-5"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
</button>
</label>
<button
onClick={(e) => {
e.stopPropagation()
handleViewCertificates(fqdn)
}}
className="p-2 text-yellow-400 hover:text-yellow-300 hover:bg-yellow-500/20 rounded-lg transition-colors"
title="Zertifikate anzeigen"
aria-label="Zertifikate anzeigen"
>
<svg
className="w-5 h-5"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</button>
<button
onClick={(e) => {
e.stopPropagation()
handleViewCSR(fqdn)
}}
className="p-2 text-green-400 hover:text-green-300 hover:bg-green-500/20 rounded-lg transition-colors"
title="CSR anzeigen"
aria-label="CSR anzeigen"
>
<svg
className="w-5 h-5"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</button>
<button
onClick={async (e) => {
e.stopPropagation()
const isOpen = showCSRDropdown[fqdn.id]
if (!isOpen) {
// Lade CSR-History wenn Bereich erweitert wird
try {
const response = await authFetch(`/api/spaces/${id}/fqdns/${fqdn.id}/csr`)
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 }))
: []
setCsrHistory(prev => {
const filtered = prev.filter(csr => csr.fqdnId !== fqdn.id)
return [...filtered, ...historyWithFqdnId]
})
}
} catch (err) {
console.error('Error fetching CSR history:', err)
}
}
setShowCSRDropdown({ ...showCSRDropdown, [fqdn.id]: !isOpen })
}}
className="p-2 text-purple-400 hover:text-purple-300 hover:bg-purple-500/20 rounded-lg transition-colors"
title="CSR History anzeigen"
aria-label="CSR History anzeigen"
>
<svg
className={`w-5 h-5 transition-transform duration-200 ${showCSRDropdown[fqdn.id] ? 'rotate-180' : ''}`}
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path d="M19 9l-7 7-7-7" />
</svg>
</button>
<button
onClick={(e) => {
e.stopPropagation()
if (canDeleteFqdn(id)) {
handleDelete(fqdn)
}
}}
disabled={!canDeleteFqdn(id)}
className={`p-2 rounded-lg transition-colors ${
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
className="w-5 h-5"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path 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>
{/* Erweiterter Bereich für CSR History */}
{showCSRDropdown[fqdn.id] && (
<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>
{(() => {
const fqdnHistory = csrHistory
.filter(csr => csr.fqdnId === 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()
return dateB - dateA
})
return fqdnHistory.length > 0 ? (
<div className="space-y-2">
{fqdnHistory.map((csr) => (
<button
key={csr.id}
onClick={(e) => {
e.stopPropagation()
handleSelectCSR(csr)
setShowCSRModal(true)
setSelectedFqdn(fqdn)
}}
className="w-full text-left p-3 bg-slate-700/50 rounded-lg hover:bg-slate-700 transition-colors border border-slate-600/50"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="font-mono text-sm text-white mb-1 break-all">
{csr.subject || 'Kein Subject'}
</div>
<div className="text-xs text-slate-400">
{new Date(csr.createdAt).toLocaleString('de-DE')}
</div>
<div className="text-xs text-slate-400 mt-1">
{csr.publicKeyAlgorithm} {csr.keySize} bits
</div>
</div>
<svg
className="w-5 h-5 text-slate-400 ml-2 flex-shrink-0"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</div>
</button>
))}
</div>
) : (
<div className="text-center py-6 text-slate-400 text-sm">
Keine CSRs vorhanden. Laden Sie einen CSR hoch.
</div>
)
})()}
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
{/* CSR Details Modal */}
{showCSRModal && selectedFqdn && (
<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-4xl w-full max-h-[90vh] overflow-y-auto p-6">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center">
<div className="flex-shrink-0 w-12 h-12 bg-blue-500/20 rounded-full flex items-center justify-center mr-4">
<svg
className="w-6 h-6 text-blue-400"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<h3 className="text-xl font-bold text-white">
CSR Details: <span className="font-mono">{selectedFqdn.fqdn}</span>
</h3>
</div>
<button
onClick={closeCSRModal}
className="p-2 text-slate-400 hover:text-white hover:bg-slate-700/50 rounded-lg transition-colors"
>
<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>
{uploadingCSR ? (
<div className="text-center py-12">
<p className="text-slate-300">CSR wird hochgeladen...</p>
</div>
) : csrError ? (
<div className="p-4 bg-red-500/20 border border-red-500/50 rounded-lg">
<p className="text-red-300 text-sm">{csrError}</p>
</div>
) : csrData ? (
<div className="space-y-6">
<div>
<h4 className="text-sm font-semibold text-slate-300 mb-2">Subject</h4>
<p className="text-white font-mono text-sm bg-slate-700/50 p-3 rounded break-all">
{csrData.subject}
</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<h4 className="text-sm font-semibold text-slate-300 mb-2">Public Key Algorithm</h4>
<p className="text-white text-sm">{csrData.publicKeyAlgorithm}</p>
</div>
<div>
<h4 className="text-sm font-semibold text-slate-300 mb-2">Signature Algorithm</h4>
<p className="text-white text-sm">{csrData.signatureAlgorithm}</p>
</div>
<div>
<h4 className="text-sm font-semibold text-slate-300 mb-2">Key Size</h4>
<p className="text-white text-sm">{csrData.keySize} bits</p>
</div>
<div>
<h4 className="text-sm font-semibold text-slate-300 mb-2">Upload Timestamp</h4>
<p className="text-white text-sm">
{new Date(csrData.createdAt).toLocaleString('de-DE')}
</p>
</div>
</div>
{csrData.dnsNames && csrData.dnsNames.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-slate-300 mb-2">DNS Names (SAN)</h4>
<ul className="list-disc list-inside text-white text-sm space-y-1 bg-slate-700/30 p-3 rounded">
{csrData.dnsNames.map((dns, idx) => (
<li key={idx} className="font-mono">{dns}</li>
))}
</ul>
</div>
)}
{csrData.emailAddresses && csrData.emailAddresses.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-slate-300 mb-2">Email Addresses</h4>
<ul className="list-disc list-inside text-white text-sm space-y-1 bg-slate-700/30 p-3 rounded">
{csrData.emailAddresses.map((email, idx) => (
<li key={idx} className="font-mono">{email}</li>
))}
</ul>
</div>
)}
{csrData.ipAddresses && csrData.ipAddresses.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-slate-300 mb-2">IP Addresses</h4>
<ul className="list-disc list-inside text-white text-sm space-y-1 bg-slate-700/30 p-3 rounded">
{csrData.ipAddresses.map((ip, idx) => (
<li key={idx} className="font-mono">{ip}</li>
))}
</ul>
</div>
)}
{csrData.uris && csrData.uris.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-slate-300 mb-2">URIs</h4>
<ul className="list-disc list-inside text-white text-sm space-y-1 bg-slate-700/30 p-3 rounded">
{csrData.uris.map((uri, idx) => (
<li key={idx} className="font-mono break-all">{uri}</li>
))}
</ul>
</div>
)}
{csrData.extensions && csrData.extensions.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-slate-300 mb-3">Requested Extensions:</h4>
<div className="space-y-4 bg-slate-700/30 p-4 rounded">
{csrData.extensions.map((ext, idx) => (
<div key={idx} className="border-l-2 border-slate-500/50 pl-4">
<div className="flex items-center gap-2 mb-2">
<span className="text-sm font-semibold text-white">
{ext.name || ext.oid || ext.id}:
</span>
{ext.critical && (
<span className="px-2 py-0.5 bg-red-500/20 text-red-300 text-xs rounded">
critical
</span>
)}
</div>
{ext.description && (
<div className="text-sm text-slate-200 ml-4 whitespace-pre-line">
{ext.description.includes('\n') ? (
ext.description.split('\n').map((line, i) => (
<div key={i} className={i === 0 ? "" : "ml-4"}>
{line.trim()}
</div>
))
) : (
ext.description.split(', ').map((item, i) => (
<div key={i} className={i === 0 ? "" : "ml-4"}>
{item}
</div>
))
)}
</div>
)}
{!ext.description && ext.purposes && ext.purposes.length > 0 && (
<div className="text-sm text-slate-200 ml-4">
{ext.purposes.map((purpose, pIdx) => (
<div key={pIdx} className="ml-4">
{purpose}
</div>
))}
</div>
)}
{!ext.description && !ext.purposes && (
<div className="text-sm text-slate-400 ml-4 font-mono break-all">
{ext.value}
</div>
)}
</div>
))}
</div>
</div>
)}
<div>
<h4 className="text-sm font-semibold text-slate-300 mb-2">CSR PEM</h4>
<pre className="text-xs text-white bg-slate-700/50 p-4 rounded overflow-x-auto max-h-64">
{csrData.csrPem}
</pre>
</div>
</div>
) : (
<div className="text-center py-12">
<p className="text-slate-300 mb-4">
Kein CSR für diesen FQDN vorhanden.
</p>
<p className="text-slate-400 text-sm">
Laden Sie einen CSR über den Upload-Button hoch.
</p>
</div>
)}
<div className="mt-6 flex justify-end">
<button
onClick={closeCSRModal}
className="px-6 py-2 bg-slate-600 hover:bg-slate-700 text-white font-semibold rounded-lg transition-colors duration-200"
>
Schließen
</button>
</div>
</div>
</div>
)}
{/* Delete Confirmation Modal */}
{showDeleteModal && fqdnToDelete && (
<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">
FQDN löschen
</h3>
</div>
<div className="mb-6">
<p className="text-slate-300 mb-4">
Möchten Sie den FQDN <span className="font-mono font-semibold text-white">{fqdnToDelete.fqdn}</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 FQDN 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>
)}
{/* Sign CSR Modal */}
{showSignCSRModal && selectedFqdn && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<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">CSR signieren lassen</h2>
<button
onClick={closeSignCSRModal}
className="text-slate-400 hover:text-white transition-colors"
>
<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>
{/* Step Indicator */}
<div className="flex items-center justify-between mb-8">
{[1, 2, 3, 4].map((step) => (
<div key={step} className="flex items-center flex-1">
<div className={`flex items-center justify-center w-10 h-10 rounded-full border-2 ${
signCSRStep >= step
? 'bg-purple-600 border-purple-600 text-white'
: 'border-slate-600 text-slate-400'
}`}>
{signCSRStep > step ? (
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
) : (
<span>{step}</span>
)}
</div>
{step < 4 && (
<div className={`flex-1 h-0.5 mx-2 ${
signCSRStep > step ? 'bg-purple-600' : 'bg-slate-600'
}`} />
)}
</div>
))}
</div>
{/* Step 1: CSR Details */}
{signCSRStep === 1 && (
<div className="space-y-4">
<h3 className="text-xl font-semibold text-white mb-4">1. CSR Details überprüfen</h3>
{csrData ? (
<div className="space-y-4 bg-slate-700/30 p-4 rounded-lg">
<div>
<h4 className="text-sm font-semibold text-slate-300 mb-2">Subject</h4>
<p className="text-white font-mono text-sm">{csrData.subject}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<h4 className="text-sm font-semibold text-slate-300 mb-2">Public Key Algorithm</h4>
<p className="text-white text-sm">{csrData.publicKeyAlgorithm}</p>
</div>
<div>
<h4 className="text-sm font-semibold text-slate-300 mb-2">Key Size</h4>
<p className="text-white text-sm">{csrData.keySize} bits</p>
</div>
</div>
{csrData.dnsNames && csrData.dnsNames.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-slate-300 mb-2">DNS Names</h4>
<ul className="list-disc list-inside text-white text-sm">
{csrData.dnsNames.map((dns, idx) => (
<li key={idx} className="font-mono">{dns}</li>
))}
</ul>
</div>
)}
</div>
) : (
<p className="text-slate-300">Kein CSR gefunden für diesen FQDN.</p>
)}
<div className="flex justify-end mt-6">
<button
onClick={() => setSignCSRStep(2)}
disabled={!csrData}
className="px-4 py-2 bg-purple-600 hover:bg-purple-700 disabled:bg-slate-600 disabled:cursor-not-allowed text-white rounded-lg transition-colors"
>
Weiter
</button>
</div>
</div>
)}
{/* Step 2: Provider Selection */}
{signCSRStep === 2 && (
<div className="space-y-4">
<h3 className="text-xl font-semibold text-white mb-4">2. Provider auswählen</h3>
<div className="space-y-2">
{providers.length > 0 ? (
providers.map((provider) => (
<button
key={provider.id}
onClick={() => setSelectedProvider(provider)}
className={`w-full p-4 rounded-lg border-2 text-left transition-colors ${
selectedProvider?.id === provider.id
? 'border-purple-600 bg-purple-600/20'
: 'border-slate-600 hover:border-slate-500 bg-slate-700/30'
}`}
>
<div className="flex items-center justify-between">
<div>
<h4 className="text-white font-semibold">{provider.displayName}</h4>
<p className="text-slate-400 text-sm">{provider.description}</p>
</div>
{selectedProvider?.id === provider.id && (
<svg className="w-6 h-6 text-purple-400" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
)}
</div>
</button>
))
) : (
<p className="text-slate-300">Keine aktivierten Provider verfügbar.</p>
)}
</div>
<div className="flex justify-between mt-6">
<button
onClick={() => setSignCSRStep(1)}
className="px-4 py-2 bg-slate-600 hover:bg-slate-700 text-white rounded-lg transition-colors"
>
Zurück
</button>
<button
onClick={() => setSignCSRStep(3)}
disabled={!selectedProvider}
className="px-4 py-2 bg-purple-600 hover:bg-purple-700 disabled:bg-slate-600 disabled:cursor-not-allowed text-white rounded-lg transition-colors"
>
Weiter
</button>
</div>
</div>
)}
{/* Step 3: Quick Check */}
{signCSRStep === 3 && (
<div className="space-y-4">
<h3 className="text-xl font-semibold text-white mb-4">3. Provider-Verbindung testen</h3>
{selectedProvider && (
<div className="bg-slate-700/30 p-4 rounded-lg mb-4">
<h4 className="text-white font-semibold mb-2">{selectedProvider.displayName}</h4>
<p className="text-slate-400 text-sm">{selectedProvider.description}</p>
</div>
)}
<button
onClick={() => handleTestProvider(selectedProvider?.id)}
disabled={!selectedProvider}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-slate-600 disabled:cursor-not-allowed text-white rounded-lg transition-colors"
>
Verbindung testen
</button>
{providerTestResult && (
<div className={`p-4 rounded-lg ${
providerTestResult.success
? 'bg-green-500/20 border border-green-500/50'
: 'bg-red-500/20 border border-red-500/50'
}`}>
<p className={providerTestResult.success ? 'text-green-300' : 'text-red-300'}>
{providerTestResult.message || (providerTestResult.success ? 'Verbindung erfolgreich' : 'Verbindung fehlgeschlagen')}
</p>
</div>
)}
<div className="flex justify-between mt-6">
<button
onClick={() => {
setSignCSRStep(2)
setProviderTestResult(null)
}}
className="px-4 py-2 bg-slate-600 hover:bg-slate-700 text-white rounded-lg transition-colors"
>
Zurück
</button>
<button
onClick={() => setSignCSRStep(4)}
disabled={!providerTestResult?.success}
className="px-4 py-2 bg-purple-600 hover:bg-purple-700 disabled:bg-slate-600 disabled:cursor-not-allowed text-white rounded-lg transition-colors"
>
Weiter
</button>
</div>
</div>
)}
{/* Step 4: Submit */}
{signCSRStep === 4 && (
<div className="space-y-4">
<h3 className="text-xl font-semibold text-white mb-4">4. CSR einreichen</h3>
<div className="bg-slate-700/30 p-4 rounded-lg mb-4">
<p className="text-slate-300 mb-2">Bereit zum Einreichen:</p>
<ul className="text-slate-400 text-sm space-y-1">
<li> FQDN: {selectedFqdn?.fqdn}</li>
<li> Provider: {selectedProvider?.displayName}</li>
<li> CSR: {csrData?.subject}</li>
</ul>
</div>
<button
onClick={handleSignCSR}
disabled={signingCSR || signResult?.success}
className={`w-full px-4 py-3 rounded-lg flex items-center justify-center gap-2 ${
signResult?.success
? 'bg-green-600 cursor-not-allowed'
: signingCSR
? 'bg-purple-600 cursor-not-allowed'
: 'bg-purple-600 hover:bg-purple-700'
} disabled:cursor-not-allowed text-white font-semibold`}
>
{signingCSR ? (
<>
<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>
<span>Wird signiert...</span>
</>
) : signResult?.success ? (
<>
<div className="relative w-6 h-6 flex items-center justify-center">
<svg className="h-6 w-6 text-white absolute" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" fill="none" className="opacity-20" />
</svg>
<svg className="h-6 w-6 text-white absolute checkmark-animated" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" fill="none" className="circle-draw" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="3" d="M9 12l2 2 4-4" className="check-draw" />
</svg>
</div>
<span>Erfolgreich signiert!</span>
</>
) : (
'CSR signieren lassen'
)}
</button>
{signResult && !signResult.success && (
<div className="p-4 rounded-lg bg-red-500/20 border border-red-500/50">
<p className="text-red-300">
{signResult.message || 'Fehler beim Signieren'}
</p>
</div>
)}
<div className="flex justify-between mt-6">
<button
onClick={() => {
setSignCSRStep(3)
setSignResult(null)
}}
disabled={signingCSR || signResult?.success}
className="px-4 py-2 bg-slate-600 hover:bg-slate-700 disabled:bg-slate-600 disabled:cursor-not-allowed text-white rounded-lg transition-colors"
>
Zurück
</button>
{signResult?.success ? (
<button
onClick={closeSignCSRModal}
className="px-4 py-2 bg-green-600 hover:bg-green-700 text-white font-semibold rounded-lg transition-colors 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="M5 13l4 4L19 7" />
</svg>
Fertig
</button>
) : (
<button
onClick={closeSignCSRModal}
className="px-4 py-2 bg-slate-600 hover:bg-slate-700 text-white rounded-lg transition-colors"
>
Abbrechen
</button>
)}
</div>
</div>
)}
</div>
</div>
</div>
)}
{/* Certificates Modal */}
{showCertificatesModal && selectedFqdn && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<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>
<button
onClick={closeCertificatesModal}
className="text-slate-400 hover:text-white transition-colors"
>
<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>
{loadingCertificates ? (
<p className="text-slate-300 text-center py-8">Lade Zertifikate...</p>
) : certificates.length === 0 ? (
<p className="text-slate-300 text-center py-8">Keine Zertifikate vorhanden</p>
) : (
<div className="space-y-4">
<div className="mb-4 pb-4 border-b border-slate-600">
<h3 className="text-lg font-semibold text-white mb-2">Zertifikat-History</h3>
<p className="text-slate-400 text-sm">
{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}
</span>
</p>
{cert.providerId && (
<p className="text-slate-400 text-sm">
<span className="font-semibold text-slate-300">Provider:</span>{' '}
{cert.providerId}
</p>
)}
</div>
</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>
</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>
</div>
</div>
)}
</div>
)
}
export default SpaceDetail