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
-
9src/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, |
||||
|
} |
||||
|
}) |
Write
Preview
Loading…
Cancel
Save
Reference in new issue