import { useState } from 'react' import { Alert, Button, Card, Form, Input, Popconfirm, Select, Space, Table, Tabs, Tag, Typography, message } from 'antd' import type { ColumnsType } from 'antd/es/table' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import apiClient, { isEnvelope } from '../../api/client' interface TLSCert { id: number domain: string issuer: string status: 'pending' | 'active' | 'renewing' | 'expired' | 'error' cert_path?: string | null key_path?: string | null not_before?: string | null not_after?: string | null last_renewed_at?: string | null last_error?: string | null created_at: string updated_at: string } interface UploadValues { domain: string cert_pem: string chain_pem?: string key_pem: string } interface IssueValues { domain: string } interface Domain { id: number; name: string } async function listCerts(): Promise { const r = await apiClient.get('/tls-certs') if (!isEnvelope(r.data)) return [] return (r.data.data as { tls_certs?: TLSCert[] }).tls_certs ?? [] } async function listDomains(): Promise { const r = await apiClient.get('/domains') if (!isEnvelope(r.data)) return [] return (r.data.data as { domains?: Domain[] }).domains ?? [] } const STATUS_COLORS: Record = { active: 'green', renewing: 'blue', pending: 'gold', expired: 'red', error: 'red', } function daysUntil(s?: string | null): number | null { if (!s) return null const t = new Date(s).getTime() if (Number.isNaN(t)) return null return Math.round((t - Date.now()) / 86_400_000) } export default function SSLPage() { const { t } = useTranslation() const qc = useQueryClient() const { data: certs, isLoading } = useQuery({ queryKey: ['tls-certs'], queryFn: listCerts }) const { data: domains } = useQuery({ queryKey: ['domains'], queryFn: listDomains }) const [issueForm] = Form.useForm() const [uploadForm] = Form.useForm() const [issueErr, setIssueErr] = useState(null) const issueMut = useMutation({ mutationFn: async (v: IssueValues) => { const r = await apiClient.post('/tls-certs/issue', v) return r.data }, onSuccess: () => { message.success(t('ssl.issueSuccess')) issueForm.resetFields() setIssueErr(null) void qc.invalidateQueries({ queryKey: ['tls-certs'] }) }, onError: (e: Error) => setIssueErr(e.message), }) const uploadMut = useMutation({ mutationFn: async (v: UploadValues) => { const r = await apiClient.post('/tls-certs/upload', v) return r.data }, onSuccess: () => { message.success(t('ssl.uploadSuccess')) uploadForm.resetFields() void qc.invalidateQueries({ queryKey: ['tls-certs'] }) }, }) const delMut = useMutation({ mutationFn: async (id: number) => { await apiClient.delete(`/tls-certs/${id}`) }, onSuccess: () => { void qc.invalidateQueries({ queryKey: ['tls-certs'] }) }, }) const columns: ColumnsType = [ { title: t('ssl.domain'), dataIndex: 'domain', key: 'domain', render: (s: string) => {s} }, { title: t('ssl.issuer'), dataIndex: 'issuer', key: 'issuer' }, { title: t('ssl.status'), dataIndex: 'status', key: 'status', render: (s: TLSCert['status']) => {s}, }, { title: t('ssl.expiresIn'), key: 'expires', render: (_, row) => { const d = daysUntil(row.not_after) if (d == null) return '—' if (d < 0) return {t('ssl.expiredAgo', { days: -d })} if (d < 30) return {d}d return `${d}d` }, }, { title: t('ssl.actions'), key: 'actions', render: (_, row) => ( delMut.mutate(row.id)} > ), }, ] const tabs = [ { key: 'letsencrypt', label: t('ssl.tabLE'), children: ( {t('ssl.leIntro')} {issueErr && setIssueErr(null)} />}
issueMut.mutate(v)}> {t('ssl.uploadHint')}
), }, ] return (
{t('ssl.title')} {t('ssl.intro')} {t('ssl.installedTitle')} ) }