1574 lines
69 KiB
JavaScript
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
|
|
|