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.
452 lines
15 KiB
452 lines
15 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 } 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'
|
|
import { Table as ProTable } from '@/components/table'
|
|
|
|
|
|
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>,
|
|
]
|
|
}}
|
|
scroll={{}}
|
|
pagination={{
|
|
pageSize: page?.pageSize ?? 10,
|
|
total: data?.total ?? 0,
|
|
current: page?.page ?? 1,
|
|
onShowSizeChange: (current: number, size: number) => {
|
|
setPage({
|
|
...page,
|
|
pageSize: size,
|
|
page: current
|
|
})
|
|
},
|
|
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)
|