feat: implement certificate upload and deletion functionality, enhance certificate management in the API and frontend

This commit is contained in:
Wikid82
2025-11-22 23:05:23 -05:00
parent 155bedcf66
commit ce89c63afc
14 changed files with 618 additions and 66 deletions
+36 -2
View File
@@ -1,8 +1,24 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { Trash2 } from 'lucide-react'
import { useCertificates } from '../hooks/useCertificates'
import { deleteCertificate } from '../api/certificates'
import { LoadingSpinner } from './LoadingStates'
import { toast } from '../utils/toast'
export default function CertificateList() {
const { certificates, isLoading, error } = useCertificates()
const queryClient = useQueryClient()
const deleteMutation = useMutation({
mutationFn: deleteCertificate,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['certificates'] })
toast.success('Certificate deleted')
},
onError: (error: any) => {
toast.error(`Failed to delete certificate: ${error.message}`)
},
})
if (isLoading) return <LoadingSpinner />
if (error) return <div className="text-red-500">Failed to load certificates</div>
@@ -13,22 +29,25 @@ export default function CertificateList() {
<table className="w-full text-left text-sm text-gray-400">
<thead className="bg-gray-900 text-gray-200 uppercase font-medium">
<tr>
<th className="px-6 py-3">Name</th>
<th className="px-6 py-3">Domain</th>
<th className="px-6 py-3">Issuer</th>
<th className="px-6 py-3">Expires</th>
<th className="px-6 py-3">Status</th>
<th className="px-6 py-3">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-800">
{certificates.length === 0 ? (
<tr>
<td colSpan={4} className="px-6 py-8 text-center text-gray-500">
<td colSpan={6} className="px-6 py-8 text-center text-gray-500">
No certificates found.
</td>
</tr>
) : (
certificates.map((cert) => (
<tr key={cert.domain} className="hover:bg-gray-800/50 transition-colors">
<tr key={cert.id || cert.domain} className="hover:bg-gray-800/50 transition-colors">
<td className="px-6 py-4 font-medium text-white">{cert.name || '-'}</td>
<td className="px-6 py-4 font-medium text-white">{cert.domain}</td>
<td className="px-6 py-4">{cert.issuer}</td>
<td className="px-6 py-4">
@@ -37,6 +56,21 @@ export default function CertificateList() {
<td className="px-6 py-4">
<StatusBadge status={cert.status} />
</td>
<td className="px-6 py-4">
{cert.provider === 'custom' && cert.id && (
<button
onClick={() => {
if (confirm('Are you sure you want to delete this certificate?')) {
deleteMutation.mutate(cert.id!)
}
}}
className="text-red-400 hover:text-red-300 transition-colors"
title="Delete Certificate"
>
<Trash2 className="w-4 h-4" />
</button>
)}
</td>
</tr>
))
)}
+22
View File
@@ -4,6 +4,7 @@ import type { ProxyHost } from '../api/proxyHosts'
import { testProxyHostConnection } from '../api/proxyHosts'
import { useRemoteServers } from '../hooks/useRemoteServers'
import { useDomains } from '../hooks/useDomains'
import { useCertificates } from '../hooks/useCertificates'
import { useDocker } from '../hooks/useDocker'
import { parse } from 'tldts'
@@ -27,10 +28,12 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
websocket_support: host?.websocket_support ?? true,
advanced_config: host?.advanced_config || '',
enabled: host?.enabled ?? true,
certificate_id: host?.certificate_id,
})
const { servers: remoteServers } = useRemoteServers()
const { domains, createDomain } = useDomains()
const { certificates } = useCertificates()
const [connectionSource, setConnectionSource] = useState<'local' | 'custom' | string>('custom')
const [selectedDomain, setSelectedDomain] = useState('')
const [selectedContainerId, setSelectedContainerId] = useState<string>('')
@@ -355,6 +358,25 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
</div>
</div>
{/* SSL Certificate Selection */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
SSL Certificate
</label>
<select
value={formData.certificate_id || 0}
onChange={e => setFormData({ ...formData, certificate_id: parseInt(e.target.value) || null })}
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value={0}>Request a new SSL Certificate</option>
{certificates.filter(c => c.provider === 'custom').map(cert => (
<option key={cert.id} value={cert.id}>
{cert.name} ({cert.domain})
</option>
))}
</select>
</div>
{/* SSL & Security Options */}
<div className="space-y-3">
<label className="flex items-center gap-3">