You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

443 lines
14 KiB

import { useAtom, useAtomValue, useSetAtom } from 'jotai'
import {
deleteSslAtom, getProvider,
KeyTypeEnum,
KeyTypes,
ProviderTypeEnum,
saveOrUpdateSslAtom,
sslListAtom,
sslPageAtom,
sslSearchAtom, uploadSslAtom
} from '@/store/websites/ssl.ts'
import ListPageLayout from '@/layout/ListPageLayout.tsx'
import { BetaSchemaForm, ProColumns, ProFormColumnsType, ProTable } from '@ant-design/pro-components'
import { memo, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from '@/i18n.ts'
import { Button, Form, Popconfirm, Space } from 'antd'
import { PlusOutlined } from '@ant-design/icons'
import DrawerPicker, { DrawerPickerRef } from '@/components/drawer-picker/DrawerPicker.tsx'
import AcmeList from './acme/AcmeList.tsx'
import { acmeListAtom, AcmeType, getAcmeAccountTypeName } from '@/store/websites/acme.ts'
import { dnsListAtom, getDNSTypeName } from '@/store/websites/dns.ts'
import DNSList from './dns/DNSList.tsx'
import CAList from './ca/CAList.tsx'
import { WebSite } from '@/types'
import Switch from '@/components/switch'
import { Else, If, Then } from 'react-if'
import Action from '@/components/action/Action.tsx'
import { Status } from '@/components/status'
import SSLDetail from './components/Detail.tsx'
import { detailAtom } from './components/store.ts'
import Upload from './components/Upload.tsx'
import { FormInstance } from 'antd/lib'
import Download from '@/components/download/Download.tsx'
const SSL = () => {
const { t } = useTranslation()
const [ form ] = Form.useForm()
const uploadFormRef = useRef<FormInstance>()
const [ page, setPage ] = useAtom(sslPageAtom)
const [ search, setSearch ] = useAtom(sslSearchAtom)
const { data: acmeData, isLoading: acmeLoading } = useAtomValue(acmeListAtom)
const { data: dnsData, isLoading: dnsLoading } = useAtomValue(dnsListAtom)
const { data, isLoading, isFetching, refetch } = useAtomValue(sslListAtom)
const { mutate: saveOrUpdate, isSuccess, isPending: isSubmitting } = useAtomValue(saveOrUpdateSslAtom)
const { mutate: deleteSSL, isPending: isDeleting } = useAtomValue(deleteSslAtom)
const { mutate: uploadSSL, isSuccess: isUploadSuccess, isPending: isUploading } = useAtomValue(uploadSslAtom)
const updateDetail = useSetAtom(detailAtom)
const uploadDrawerRef = useRef<DrawerPickerRef>()
const [ open, setOpen ] = useState(false)
const columns = useMemo<ProColumns<WebSite.ISSL>[]>(() => {
return [
{
title: 'ID',
dataIndex: 'id',
hideInTable: true,
hideInSearch: false,
formItemProps: {
hidden: true,
}
},
{
title: t('website.ssl.columns.primaryDomain', '域名'),
dataIndex: 'primary_domain',
formItemProps: {
label: t('website.ssl.form.primaryDomain', '主域名'),
rules: [ { required: true, message: t('message.required', '主域名') } ]
}
},
{
title: t('website.ssl.columns.otherDomains', '其它域名'),
dataIndex: 'domains',
},
{
title: t('website.ssl.columns.acmeAccountId', 'Acme帐号'),
dataIndex: 'acme_account_id',
valueType: 'select',
fieldProps: {
loading: acmeLoading,
options: acmeData?.rows?.map(item => ({
label: `${item.email} [${getAcmeAccountTypeName(item.type as AcmeType)}]`,
value: item.id
}))
},
formItemProps: {
rules: [
{ required: true, message: t('message.required', '请选择') }
]
}
},
{
title: t('website.ssl.columns.status', '状态'),
dataIndex: 'status',
render: (_, record) => {
return <Status status={record.status}/>
},
hideInForm: true,
},
{
title: t('website.ssl.columns.keyType', '密钥算法'),
dataIndex: 'key_type',
hideInTable: true,
valueType: 'select',
fieldProps: {
options: KeyTypes
},
formItemProps: {
rules: [
{ required: true, message: t('message.required', '请选择') }
]
},
},
{
title: t('website.ssl.columns.provider', '申请方式'),
dataIndex: 'provider',
valueType: 'radio',
valueEnum: {
[ProviderTypeEnum.DnsAccount]: {
text: t('website.ssl.providerTypeEnum.DnsAccount', 'DNS帐号'),
},
[ProviderTypeEnum.DnsManual]: {
text: t('website.ssl.providerTypeEnum.DnsManual', '手动验证'),
},
[ProviderTypeEnum.Http]: {
text: t('website.ssl.providerTypeEnum.Http', 'HTTP'),
}
},
dependencies: [ 'provider' ],
renderText: (text) => {
return getProvider(text)
},
formItemProps: (form, config) => {
const val = form.getFieldValue(config.dataIndex)
const help = {
[ProviderTypeEnum.DnsAccount]: t('website.ssl.form.provider_{{v}}', '', { v: val }),
[ProviderTypeEnum.DnsManual]: t('website.ssl.form.provider_{{v}}', '手动解析模式需要在创建完之后点击申请按钮获取 DNS 解析值', { v: val }),
[ProviderTypeEnum.Http]: t('website.ssl.form.provider_{{v}}', 'HTTP 模式需要安装 OpenResty<br/><span style="color:red;">HTTP 模式无法申请泛域名证书</span>', { v: val }),
}
return {
label: t('website.ssl.form.provider', '验证方式'),
help: <span dangerouslySetInnerHTML={{ __html: help[val] }}/>,
rules: [ { required: true, message: t('message.required', '请选择') } ]
}
},
},
{
name: [ 'provider' ],
valueType: 'dependency',
hideInSetting: true,
hideInTable: true,
columns: ({ provider }) => {
if (provider === ProviderTypeEnum.DnsAccount) {
return [ {
title: t('website.ssl.columns.dnsAccountId', 'DNS帐号'),
dataIndex: 'dns_account_id',
valueType: 'select',
formItemProps: {
rules: [ { required: true, message: t('message.required', '请输入DNS帐号') } ]
},
fieldProps: {
loading: dnsLoading,
options: dnsData?.rows?.map(item => ({
label: `${item.name} [${getDNSTypeName(item.type)}]`,
value: item.id
}))
},
} ]
}
return []
}
},
{
title: t('website.ssl.columns.autoRenew', '自动续签'),
dataIndex: 'auto_renew',
valueType: 'switch',
render: (_, record) => {
return <Switch value={record.auto_renew} size={'small'}/>
}
},
{
title: t('website.ssl.columns.pushDir', '推送证书到本地目录'),
dataIndex: 'push_dir',
valueType: 'switch',
hideInTable: true,
hideInSearch: true,
},
{
name: [ 'push_dir' ],
valueType: 'dependency',
hideInSetting: true,
hideInTable: true,
columns: ({ pushDir }) => {
if (pushDir) {
return [ {
title: t('website.ssl.columns.dir', '目录'),
dataIndex: 'dir',
formItemProps: {
help: t('website.ssl.form.dir_help', '会在此目录下生成两个文件,证书文件:fullchain.pem 密钥文件:privkey.pem'),
rules: [ { required: true, message: t('message.required', '请输入目录') } ]
}
} ]
}
return []
}
},
{
title: t('website.ssl.columns.description', '备注'),
dataIndex: 'description',
},
{
title: t('website.ssl.columns.expire_date', '过期时间'),
dataIndex: 'expire_date',
valueType: 'dateTime',
hideInForm: true,
},
{
title: t('website.ssl.columns.option', '操作'), valueType: 'option',
key: 'option',
fixed: 'right',
width: 300,
render: (_, record) => [
<Action key="detail"
as={'a'}
disabled={record.status === 'init' || record.status === 'error'}
onClick={() => {
updateDetail({
open: true,
record
})
}}
>
{t('actions.detail', '详情')}
</Action>,
<If condition={() => record.status !== 'manual'}>
<Then>
<Action key="apply"
as={'a'}
disabled={record.status === 'applying' || record.status === 'manual'}
onClick={() => {
}}
>
{t('actions.apply', '申请')}
</Action>
</Then>
<Else>
<Action key="update"
as={'a'}
onClick={() => {
}}
>
{t('actions.update', '更新')}
</Action>
</Else>
</If>,
<Download key="download"
server={async () => {
}}
>
<a>{t('actions.download', '下载')}</a>
</Download>,
<Popconfirm
key={'del_confirm'}
disabled={isDeleting}
onConfirm={() => {
deleteSSL(record.id)
}}
title={t('message.deleteConfirm')}>
<a key="del">
{t('actions.delete', '删除')}
</a>
</Popconfirm>
,
],
},
]
}, [ acmeData, dnsData ])
useEffect(() => {
if (isSuccess) {
setOpen(false)
}
}, [ isSuccess ])
return (
<ListPageLayout>
<ProTable<WebSite.ISSL>
headerTitle={t('website.ssl.title', '证书列表')}
search={false}
loading={isLoading || isFetching}
rowKey={'id'}
dataSource={data?.rows ?? []}
columns={columns}
columnsState={{
defaultValue: {
option: { fixed: 'right', disable: true },
},
}}
options={{
reload: () => {
refetch()
},
}}
toolbar={{
search: {
loading: isFetching && !!search.key,
onSearch: (value: string) => {
setSearch({ key: value })
},
placeholder: t('website.ssl.search.placeholder', '输入域名')
},
actions: [
<DrawerPicker
maskClosable={false}
title={t('website.ssl.ca.title', '证书颁发机构')}
width={1000}
target={<Button type={'primary'} ghost={true}>
{t('website.ssl.actions.selfSigned', '自签证书')}
</Button>}
>
<CAList/>
</DrawerPicker>,
<DrawerPicker
maskClosable={false}
title={t('website.ssl.acme.title', 'Acme帐户')}
width={1000}
target={<Button type={'primary'} ghost={true}>
{t('website.ssl.actions.acme', 'Acme帐户')}
</Button>}
>
<AcmeList/>
</DrawerPicker>,
<DrawerPicker
maskClosable={false}
title={t('website.ssl.dns.title', 'DNS帐户')}
width={1000}
target={<Button type={'primary'} ghost={true}>
{t('website.ssl.actions.dns', 'DNS帐户')}
</Button>}>
<DNSList/>
</DrawerPicker>,
<Button type={'primary'} onClick={() => {
uploadDrawerRef.current?.open()
}}>
{t('website.ssl.actions.upload', '上传证书')}
</Button>,
<Button
key="button"
icon={<PlusOutlined/>}
onClick={() => {
form.resetFields()
form.setFieldsValue({
id: 0,
keyType: KeyTypeEnum.EC256,
})
setOpen(true)
}}
type="primary"
>
{t('actions.sslApply', '申请证书')}
</Button>,
]
}}
pagination={{
pageSize: page?.pageSize ?? 10,
total: data?.total ?? 0,
current: page?.page ?? 1,
onChange: (page, pageSize) => {
setPage(prev => ({
...prev,
page,
pageSize,
}))
},
}}
>
</ProTable>
<BetaSchemaForm<WebSite.ISSL>
shouldUpdate={false}
width={600}
form={form}
layout={'vertical'}
scrollToFirstError={true}
title={t(`website.ssl.title_${form.getFieldValue('id') !== 0 ? 'edit' : 'add'}`, form.getFieldValue('id') !== 0 ? '证书编辑' : '证书添加')}
// colProps={{ span: 24 }}
labelCol={{ span: 12 }}
wrapperCol={{ span: 24 }}
layoutType={'DrawerForm'}
open={open}
drawerProps={{
maskClosable: false,
}}
onOpenChange={(open) => {
setOpen(open)
}}
loading={isSubmitting}
onFinish={async (values) => {
// console.log('values', values)
saveOrUpdate(values)
}}
columns={columns as ProFormColumnsType[]}/>
<DrawerPicker
id={'upload-drawer-picker'}
ref={uploadDrawerRef}
maskClosable={false}
title={t('website.ssl.upload.title', '上传证书')}
width={800}
footer={[
<Space wrap={false} style={{ display: 'flex', justifyContent: 'end' }}>
<Button onClick={() => uploadDrawerRef.current?.close()}>{t('actions.cancel')}</Button>
<Button onClick={() => {
uploadFormRef.current?.validateFields?.().then(values => {
uploadSSL(values)
if (isUploadSuccess) {
uploadDrawerRef.current?.close()
}
})
}}
loading={isUploading}
type={'primary'}>{t('actions.ok')}</Button>
</Space>
]}
target={false}
>
<Upload formRef={uploadFormRef}/>
</DrawerPicker>
<SSLDetail/>
</ListPageLayout>
)
}
export default memo(SSL)