feat: implement certificate upload and deletion functionality, enhance certificate management in the API and frontend
This commit is contained in:
@@ -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>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user