dark
7 months ago
17 changed files with 1096 additions and 713 deletions
-
0src/pages/websites/ssl/acme/AcmeList.tsx
-
32src/pages/websites/ssl/ca/CAList.tsx
-
116src/pages/websites/ssl/ca/Detail.tsx
-
136src/pages/websites/ssl/ca/SelfSign.tsx
-
20src/pages/websites/ssl/ca/store.ts
-
23src/pages/websites/ssl/ca/style.ts
-
0src/pages/websites/ssl/dns/DNSList.tsx
-
40src/pages/websites/ssl/index.tsx
-
15src/service/websites.ts
-
8src/store/websites/acme.ts
-
15src/store/websites/ca.ts
-
21src/store/websites/ssl.ts
-
2src/types/index.d.ts
-
13src/types/website/ca.d.ts
@ -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 |
@ -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 |
@ -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, |
|||
}) |
|||
|
@ -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, |
|||
} |
|||
}) |
@ -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) |
|||
}, |
|||
|
|||
} |
|||
|
|||
} |
|||
|
Write
Preview
Loading…
Cancel
Save
Reference in new issue