125 lines
4.4 KiB
TypeScript
125 lines
4.4 KiB
TypeScript
import { useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
import { Plus, ShieldCheck } from 'lucide-react'
|
|
import CertificateList from '../components/CertificateList'
|
|
import { uploadCertificate } from '../api/certificates'
|
|
import { toast } from '../utils/toast'
|
|
import { PageShell } from '../components/layout/PageShell'
|
|
import {
|
|
Button,
|
|
Input,
|
|
Alert,
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogFooter,
|
|
Label,
|
|
} from '../components/ui'
|
|
|
|
export default function Certificates() {
|
|
const { t } = useTranslation()
|
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
|
const [name, setName] = useState('')
|
|
const [certFile, setCertFile] = useState<File | null>(null)
|
|
const [keyFile, setKeyFile] = useState<File | null>(null)
|
|
const queryClient = useQueryClient()
|
|
|
|
const uploadMutation = useMutation({
|
|
mutationFn: async () => {
|
|
if (!certFile || !keyFile) throw new Error('Files required')
|
|
await uploadCertificate(name, certFile, keyFile)
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['certificates'] })
|
|
setIsModalOpen(false)
|
|
setName('')
|
|
setCertFile(null)
|
|
setKeyFile(null)
|
|
toast.success(t('certificates.uploadSuccess'))
|
|
},
|
|
onError: (error: Error) => {
|
|
toast.error(`${t('certificates.uploadFailed')}: ${error.message}`)
|
|
},
|
|
})
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
uploadMutation.mutate()
|
|
}
|
|
|
|
// Header actions
|
|
const headerActions = (
|
|
<Button onClick={() => setIsModalOpen(true)} data-testid="add-certificate-btn">
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
{t('certificates.addCertificate')}
|
|
</Button>
|
|
)
|
|
|
|
return (
|
|
<PageShell
|
|
title={t('certificates.title')}
|
|
description={t('certificates.description')}
|
|
actions={headerActions}
|
|
>
|
|
<Alert variant="info" icon={ShieldCheck}>
|
|
<strong>{t('certificates.note')}:</strong> {t('certificates.noteText')}
|
|
</Alert>
|
|
|
|
<CertificateList />
|
|
|
|
{/* Upload Certificate Dialog */}
|
|
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
|
<DialogContent data-testid="certificate-upload-dialog">
|
|
<DialogHeader>
|
|
<DialogTitle>{t('certificates.uploadCertificate')}</DialogTitle>
|
|
</DialogHeader>
|
|
<form onSubmit={handleSubmit} className="space-y-4 py-4">
|
|
<Input
|
|
id="certificate-name"
|
|
label={t('certificates.friendlyName')}
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="e.g. My Custom Cert"
|
|
required
|
|
/>
|
|
<div>
|
|
<Label htmlFor="cert-file">{t('certificates.certificatePem')}</Label>
|
|
<input
|
|
id="cert-file"
|
|
data-testid="certificate-file-input"
|
|
type="file"
|
|
accept=".pem,.crt,.cer"
|
|
onChange={(e) => setCertFile(e.target.files?.[0] || null)}
|
|
className="mt-1.5 block w-full text-sm text-content-secondary file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-brand-500 file:text-white hover:file:bg-brand-600 cursor-pointer"
|
|
required
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="key-file">{t('certificates.privateKeyPem')}</Label>
|
|
<input
|
|
id="key-file"
|
|
data-testid="certificate-key-input"
|
|
type="file"
|
|
accept=".pem,.key"
|
|
onChange={(e) => setKeyFile(e.target.files?.[0] || null)}
|
|
className="mt-1.5 block w-full text-sm text-content-secondary file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-brand-500 file:text-white hover:file:bg-brand-600 cursor-pointer"
|
|
required
|
|
/>
|
|
</div>
|
|
<DialogFooter className="pt-4">
|
|
<Button type="button" variant="secondary" onClick={() => setIsModalOpen(false)}>
|
|
{t('common.cancel')}
|
|
</Button>
|
|
<Button type="submit" isLoading={uploadMutation.isPending}>
|
|
{t('common.upload')}
|
|
</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</PageShell>
|
|
)
|
|
}
|