Browse Source

完善自签证书

main
dark 6 months ago
parent
commit
54075f8d78
  1. 0
      src/pages/websites/ssl/acme/AcmeList.tsx
  2. 32
      src/pages/websites/ssl/ca/CAList.tsx
  3. 116
      src/pages/websites/ssl/ca/Detail.tsx
  4. 136
      src/pages/websites/ssl/ca/SelfSign.tsx
  5. 20
      src/pages/websites/ssl/ca/store.ts
  6. 23
      src/pages/websites/ssl/ca/style.ts
  7. 0
      src/pages/websites/ssl/dns/DNSList.tsx
  8. 40
      src/pages/websites/ssl/index.tsx
  9. 15
      src/service/websites.ts
  10. 8
      src/store/websites/acme.ts
  11. 15
      src/store/websites/ca.ts
  12. 21
      src/store/websites/ssl.ts
  13. 2
      src/types/index.d.ts
  14. 13
      src/types/website/ca.d.ts

0
src/pages/websites/ssl/components/AcmeList.tsx → src/pages/websites/ssl/acme/AcmeList.tsx

32
src/pages/websites/ssl/components/CAList.tsx → src/pages/websites/ssl/ca/CAList.tsx

@ -2,11 +2,14 @@ import { useMemo, useState } from 'react'
import { BetaSchemaForm, ProColumns, ProFormColumnsType, ProTable } from '@ant-design/pro-components'
import { useTranslation } from '@/i18n.ts'
import { deleteCaAtom } from '@/store/websites/ca.ts'
import { useAtom, useAtomValue } from 'jotai'
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
import { Button, Form, Popconfirm } from 'antd'
import { KeyTypeEnum, KeyTypes } from '@/store/websites/ssl.ts'
import { caListAtom, caPageAtom, saveOrUpdateCaAtom } from '@/store/websites/ca.ts'
import { WebSite } from '@/types'
import Detail from './Detail.tsx'
import { detailAtom, selfSignAtom } from './store.ts'
import SelfSign from './SelfSign.tsx'
const CAList = () => {
@ -17,7 +20,8 @@ const CAList = () => {
const { mutate: saveOrUpdate, isPending: isSubmitting, isSuccess } = useAtomValue(saveOrUpdateCaAtom)
const { mutate: deleteCA, isPending: isDeleting } = useAtomValue(deleteCaAtom)
const [ open, setOpen ] = useState(false)
const updateUI = useSetAtom(detailAtom)
const selfSignUI = useSetAtom(selfSignAtom)
const columns = useMemo<ProColumns<WebSite.ICA>[]>(() => {
return [
{
@ -104,7 +108,7 @@ const CAList = () => {
{
title: t('website.ssl.ca.columns.keyType', '密钥算法'),
dataIndex: 'keyType',
dataIndex: 'key_type',
valueType: 'select',
fieldProps: {
options: KeyTypes
@ -117,7 +121,7 @@ const CAList = () => {
},
{
title: t('website.ssl.ca.columns.createAt', '时间'),
dataIndex: 'create_at',
dataIndex: 'created_at',
valueType: 'dateTime',
hideInForm: true,
},
@ -126,6 +130,24 @@ const CAList = () => {
valueType: 'option',
render: (_, record) => {
return [
<a key="view" onClick={() => {
selfSignUI({
open: true,
record: {
id: record.id,
key_type: KeyTypeEnum.EC256,
unit: 'year',
auto_renew: true,
time: 10,
}
})
}}>{t('website.actions.selfSign', '签发证书')}</a>,
<a key="edit" onClick={() => {
updateUI({
open: true, record,
})
}}>{t('website.actions.detail', '详情')}</a>,
<Popconfirm
key={'del_confirm'}
disabled={isDeleting}
@ -214,6 +236,8 @@ const CAList = () => {
return isSuccess
}}
columns={columns as ProFormColumnsType[]}/>
<Detail/>
<SelfSign/>
</>
)
}

116
src/pages/websites/ssl/ca/Detail.tsx

@ -0,0 +1,116 @@
import { Segmented, Drawer, DrawerProps, Input, Button, Flex, message, Descriptions } from 'antd'
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from '@/i18n.ts'
import { useAtom } from 'jotai'
import { detailAtom } from './store.ts'
import { useStyle } from './style.ts'
import { useCopyToClipboard } from 'react-use'
const Detail = (props: DrawerProps) => {
const prefix = 'website.ssl.ca.detail'
const { t } = useTranslation()
const { styles } = useStyle()
const [ key, setKey ] = useState('base')
const [ ui, setUI ] = useAtom(detailAtom)
const [ copyState, setCopy ] = useCopyToClipboard()
const options = useMemo(() => {
return [
{ label: t(`${prefix}.base`, '机构详情'), value: 'base' },
{ label: t(`${prefix}.src`, 'src'), value: 'csr' },
{ label: t(`${prefix}.private_key`, '私钥'), value: 'private_key' },
]
}, [])
useEffect(() => {
if (copyState.error) {
message.error(t('message.copyError', '复制失败'))
} else if (copyState.value) {
message.success(t('message.copySuccess', '复制成功'))
}
}, [ copyState ])
const render = useMemo(() => {
switch (key) {
case 'base':
return <div>
<Descriptions bordered={true} column={1}>
<Descriptions.Item label={t(`${prefix}.name`, '名称')}>
{ui.record?.name}
</Descriptions.Item>
<Descriptions.Item label={t(`${prefix}.common_name`, '证书主体名称(CN)')}>
{ui.record?.common_name}
</Descriptions.Item>
<Descriptions.Item label={t(`${prefix}.organization`, '颁发组织')}>
{ui.record?.organization}
</Descriptions.Item>
<Descriptions.Item label={t(`${prefix}.organizationUint`, '部门')}>
{ui.record?.organizationUint}
</Descriptions.Item>
<Descriptions.Item label={t(`${prefix}.country`, '国家代号')}>
{ui.record?.country}
</Descriptions.Item>
<Descriptions.Item label={t(`${prefix}.province`, '省份')}>
{ui.record?.province}
</Descriptions.Item>
<Descriptions.Item label={t(`${prefix}.city`, '城市')}>
{ui.record?.city}
</Descriptions.Item>
</Descriptions>
</div>
case 'csr':
return <Flex gap={15} vertical={true}>
<Input.TextArea
rows={20}
value={ui.record.csr}
/>
<span>
<Button type={'primary'} onClick={() => {
setCopy(ui.record?.csr)
}}> {t('actions.copy', '复制')}</Button>
</span>
</Flex>
case 'private_key':
return <Flex gap={15} vertical={true}>
<Input.TextArea
rows={20}
value={ui.record?.private_key}
/>
<span>
<Button type={'primary'} onClick={() => {
setCopy(ui.record?.private_key)
}}> {t('actions.copy', '复制')}</Button>
</span>
</Flex>
default:
return null
}
}, [ key, ui.record ])
return (
<Drawer {...props}
destroyOnClose={true}
title={t(`${prefix}.title`, '机构详情')}
width={800}
open={ui.open}
onClose={() =>
setUI(prev => ({
...prev,
open: false
}))
}
>
<Segmented
value={key}
options={options}
onChange={setKey}
/>
<div className={styles.content}>
{render}
</div>
</Drawer>
)
}
export default Detail

136
src/pages/websites/ssl/ca/SelfSign.tsx

@ -0,0 +1,136 @@
import { Form, Input, InputNumber, Modal, ModalProps, Select, Space, Switch } from 'antd'
import { WebSite } from '@/types'
import { useTranslation } from '@/i18n.ts'
import { useAtom, useAtomValue } from 'jotai'
import { selfSignAtom } from './store.ts'
import { KeyTypes } from '@/store/websites/ssl.ts'
import { useEffect } from 'react'
import { obtainSslAtom } from '@/store/websites/ca.ts'
const SelfSign = (props: ModalProps) => {
const prefix = 'website.ssl.ca.selfSign'
const [ form ] = Form.useForm()
const { t } = useTranslation()
const [ ui, setUI ] = useAtom(selfSignAtom)
const { mutate, isPending, isSuccess } = useAtomValue(obtainSslAtom)
useEffect(() => {
form.setFieldsValue(ui.record)
}, [ ui.open ])
return (
<Modal
{...props}
title={t(`${prefix}.title`, '自签证书')}
open={ui.open}
width={800}
destroyOnClose={true}
onCancel={() => {
setUI({ open: false, record: {} })
}}
confirmLoading={isPending}
onOk={() => {
form.validateFields().then(() => {
mutate(ui.record)
})
return isSuccess
}}
>
<Form<WebSite.ISSLObtainByCA>
form={form}
labelCol={{ span: 6 }}
wrapperCol={{ span: 14 }}
onValuesChange={value => {
setUI(prev => {
return {
...prev,
record: {
...prev.record,
...value,
},
}
})
}}
>
<Form.Item
label={t(`${prefix}.domains.label`, '域名')}
name={'domains'}
rules={[
{
required: true,
},
]}
>
<Input.TextArea rows={4}
placeholder={t(`${prefix}.domains.placeholder`, '一行一个域名,支持*和IP地址')}/>
</Form.Item>
<Form.Item
label={t(`${prefix}.description.label`, '备注')}
name={'description'}
>
<Input placeholder={t(`${prefix}.description.placeholder`, '')}/>
</Form.Item>
<Form.Item
shouldUpdate={true}
label={t(`${prefix}.key_type.label`, '密钥算法')}
name={'key_type'}
rules={[
{
required: true,
},
]}
>
<Select options={KeyTypes}
placeholder={t(`${prefix}.key_type.placeholder`, '')}/>
</Form.Item>
<Form.Item
label={t(`${prefix}.exp.label`, '有效期')}
>
<Space.Compact style={{ display: 'flex' }}>
<Form.Item
name={'time'}
noStyle
rules={[ { required: true } ]}
>
<InputNumber min={1} style={{ flex: 1 }}/>
</Form.Item>
<Form.Item
name={'unit'}
noStyle
rules={[ { required: true } ]}
>
<Select options={[
{ value: 'year', label: t(`${prefix}.unit_year`, '年') },
{ value: 'day', label: t(`${prefix}.unit_day`, '天') },
]} style={{ width: 100 }}>
</Select>
</Form.Item>
</Space.Compact>
</Form.Item>
<Form.Item label={t(`${prefix}.auto_renew.label`, '自动续签')} name={'auto_renew'}>
<Switch/>
</Form.Item>
<Form.Item label={t(`${prefix}.push_dir.label`, '推送证书到本地目录')} name={'push_dir'}>
<Switch/>
</Form.Item>
<Form.Item
hidden={!ui.record?.push_dir}
help={t(`${prefix}.dir.help`, '会在此目录下生成两个文件,证书文件:fullchain.pem 密钥文件:privkey.pem')}
label={t(`${prefix}.dir.label`, '目录')} name={'dir'}
rules={[
{
required: !!ui.record?.push_dir,
},
]}
>
<Input/>
</Form.Item>
</Form>
</Modal>
)
}
export default SelfSign

20
src/pages/websites/ssl/ca/store.ts

@ -0,0 +1,20 @@
import { atom } from 'jotai'
import { WebSite } from '@/types'
type DetailUI = {
open: boolean,
record?: WebSite.ICA,
}
type SelfSignUI = {
open: boolean,
record?: WebSite.ISSLObtainByCA,
}
export const detailAtom = atom<DetailUI>({
open: false,
})
export const selfSignAtom = atom<SelfSignUI>({
open: false,
})

23
src/pages/websites/ssl/ca/style.ts

@ -0,0 +1,23 @@
import { createStyles } from '@/theme'
export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any) => {
const prefix = `${prefixCls}-${token?.proPrefix}-ca-detail-page`
const container = css`
`
const content = css`
padding: 20px 0;
.ant-descriptions-item-label{
font-weight: bold;
width: 50%;
}
`
return {
container: cx(prefix, props?.className, container),
content,
}
})

0
src/pages/websites/ssl/components/DNSList.tsx → src/pages/websites/ssl/dns/DNSList.tsx

40
src/pages/websites/ssl/index.tsx

@ -1,6 +1,6 @@
import { useAtom, useAtomValue } from 'jotai'
import {
deleteSslAtom,
deleteSslAtom, getProvider,
KeyTypeEnum,
KeyTypes,
ProviderTypeEnum,
@ -16,12 +16,13 @@ import { useTranslation } from '@/i18n.ts'
import { Button, Form, Popconfirm } from 'antd'
import { PlusOutlined } from '@ant-design/icons'
import DrawerPicker from '@/components/drawer-picker/DrawerPicker.tsx'
import AcmeList from './components/AcmeList.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 './components/DNSList.tsx'
import CAList from '@/pages/websites/ssl/components/CAList.tsx'
import DNSList from './dns/DNSList.tsx'
import CAList from './ca/CAList.tsx'
import { WebSite } from '@/types'
import Switch from '@/components/switch'
const SSL = () => {
@ -51,7 +52,7 @@ const SSL = () => {
},
{
title: t('website.ssl.columns.primaryDomain', '域名'),
dataIndex: 'primaryDomain',
dataIndex: 'primary_domain',
formItemProps: {
label: t('website.ssl.form.primaryDomain', '主域名'),
rules: [ { required: true, message: t('message.required', '主域名') } ]
@ -59,11 +60,11 @@ const SSL = () => {
},
{
title: t('website.ssl.columns.otherDomains', '其它域名'),
dataIndex: 'otherDomains',
dataIndex: 'domains',
},
{
title: t('website.ssl.columns.acmeAccountId', 'Acme帐号'),
dataIndex: 'acmeAccountId',
dataIndex: 'acme_account_id',
valueType: 'select',
fieldProps: {
loading: acmeLoading,
@ -80,7 +81,7 @@ const SSL = () => {
},
{
title: t('website.ssl.columns.keyType', '密钥算法'),
dataIndex: 'keyType',
dataIndex: 'key_type',
hideInTable: true,
valueType: 'select',
fieldProps: {
@ -96,7 +97,6 @@ const SSL = () => {
title: t('website.ssl.columns.provider', '申请方式'),
dataIndex: 'provider',
valueType: 'radio',
initialValue: ProviderTypeEnum.DnsAccount,
valueEnum: {
[ProviderTypeEnum.DnsAccount]: {
text: t('website.ssl.providerTypeEnum.DnsAccount', 'DNS帐号'),
@ -109,6 +109,9 @@ const SSL = () => {
}
},
dependencies: [ 'provider' ],
renderText: (text) => {
return getProvider(text)
},
formItemProps: (form, config) => {
const val = form.getFieldValue(config.dataIndex)
const help = {
@ -127,11 +130,12 @@ const SSL = () => {
name: [ 'provider' ],
valueType: 'dependency',
hideInSetting: true,
hideInTable: true,
columns: ({ provider }) => {
if (provider === ProviderTypeEnum.DnsAccount) {
return [ {
title: t('website.ssl.columns.dnsAccountId', 'DNS帐号'),
dataIndex: 'dnsAccountId',
dataIndex: 'dns_account_id',
valueType: 'select',
formItemProps: {
rules: [ { required: true, message: t('message.required', '请输入DNS帐号') } ]
@ -151,22 +155,25 @@ const SSL = () => {
},
{
title: t('website.ssl.columns.autoRenew', '自动续签'),
dataIndex: 'autoRenew',
dataIndex: 'auto_renew',
valueType: 'switch',
render: (_, record) => {
return <Switch value={record.auto_renew} size={'small'}/>
}
},
{
title: t('website.ssl.columns.pushDir', '推送证书到本地目录'),
dataIndex: 'pushDir',
dataIndex: 'push_dir',
valueType: 'switch',
hideInTable: true,
hideInSearch: true,
},
{
name: [ 'pushDir' ],
name: [ 'push_dir' ],
valueType: 'dependency',
hideInSetting: true,
hideInTable: true,
columns: ({ pushDir }) => {
if (pushDir) {
return [ {
@ -187,6 +194,11 @@ const SSL = () => {
dataIndex: 'description',
},
{
title: t('website.ssl.columns.expire_date', '过期时间'),
dataIndex: 'expire_date',
valueType: 'dateTime'
},
{
title: t('website.ssl.columns.option', '操作'), valueType: 'option',
key: 'option',
render: (_, record) => [

15
src/service/websites.ts

@ -1,18 +1,23 @@
import { createCURD } from '@/service/base.ts'
import { WebSite } from '@/types'
import request from '@/request.ts'
const websitesServ = {
ssl: {
...createCURD<any, WebSite.ISSL>('/website/ssl')
},
acme:{
acme: {
...createCURD<any, WebSite.IAcmeAccount>('/website/acme')
},
dns:{
...createCURD<any, WebSite.IDnsAccount>('/website/dns')
dns: {
...createCURD<any, WebSite.IDnsAccount>('/website/dns_account')
},
ca:{
...createCURD<any, WebSite.ICA>('/website/ca')
ca: {
...createCURD<any, WebSite.ICA>('/website/ca'),
obtainSsl: async (params: WebSite.ISSLObtainByCA) => {
return request.post<any, WebSite.ISSLObtainByCA>('/website/ca/obtain_ssl', params)
},
}
}

8
src/store/websites/acme.ts

@ -7,13 +7,13 @@ import { atom } from 'jotai'
import { WebSite } from '@/types'
export enum AcmeType {
LetsEncrypt = 'letsencrypt',
LetsEncrypt = 'LetsEncrypt',
//zerossl
ZeroSSl = 'zerossl',
ZeroSSl = 'ZeroSSl',
//buypass
Buypass = 'buypass',
Buypass = 'Buypass',
//google
Google = 'google',
Google = 'Google',
}
export const AcmeAccountTypes = [

15
src/store/websites/ca.ts

@ -5,6 +5,8 @@ import websitesServ from '@/service/websites.ts'
import { message, } from 'antd'
import { t } from 'i18next'
import { WebSite } from '@/types'
import { ISSLObtainByCA } from '@/types/website/ca'
import { sslListAtom } from '@/store/websites/ssl.ts'
export const caPageAtom = atom<IPage>({
page: 1, pageSize: 10,
@ -53,3 +55,16 @@ export const deleteCaAtom = atomWithMutation<IApiResult, number>(get => ({
get(caListAtom).refetch()
}
}))
export const obtainSslAtom = atomWithMutation<any, ISSLObtainByCA, IApiResult>(get => ({
mutationKey: [ 'obtainSsl' ],
mutationFn: async (data) => {
return await websitesServ.ca.obtainSsl(data)
},
onSuccess: (res) => {
message.success(t('message.obtainSsl', '签发成功'))
get(sslListAtom).refetch()
return res
}
}))

21
src/store/websites/ssl.ts

@ -12,6 +12,22 @@ export enum ProviderTypeEnum {
Http = 'http'
}
export function getProvider(provider: string): string {
switch (provider) {
case 'dnsAccount':
return t('website.dnsAccount', 'DNS账号')
case 'dnsManual':
return t('website.dnsManual', '手动解析')
case 'http':
return 'HTTP'
case 'selfSigned':
return t('website.ssl.selfSigned', '自签证书')
default:
return t('website.ssl.manualCreate', '手动创建')
}
}
export enum KeyTypeEnum {
EC256 = 'P256',
EC384 = 'P384',
@ -26,8 +42,11 @@ export const KeyTypes = [
{ label: 'RSA 2048', value: '2048' },
{ label: 'RSA 3072', value: '3072' },
{ label: 'RSA 4096', value: '4096' },
];
]
export const getKeyType = (key: string): string => {
return KeyTypes.find(item => item.value === key)?.label || ''
}
export const sslPageAtom = atom<IPage>({
page: 1,

2
src/types/index.d.ts

@ -8,7 +8,7 @@ export namespace System {
export namespace WebSite {
export { IAcmeAccount } from './website/acme'
export { ICA } from './website/ca'
export { ICA, ISSLObtainByCA } from './website/ca'
export { IDnsAccount } from './website/dns'
export { ISSL, ProviderType, SSLSearchParam } from './website/ssl'
}

13
src/types/website/ca.d.ts

@ -15,3 +15,16 @@ export interface ICA {
private_key: string;
key_type: string;
}
export interface ISSLObtainByCA {
id: number;
domains: string;
key_type: string;
time: number;
unit: string;
push_dir: boolean;
dir: string;
auto_renew: boolean;
description: string;
}
Loading…
Cancel
Save