Browse Source

添加ACME帐户模块

main
dark 5 months ago
parent
commit
cf24deb6fc
  1. 38
      src/components/drawer-picker/DrawerPicker.tsx
  2. 1
      src/components/drawer-picker/index.ts
  3. 15
      src/components/drawer-picker/style.ts
  4. 11
      src/pages/system/roles/index.tsx
  5. 4
      src/pages/system/users/index.tsx
  6. 174
      src/pages/websites/ssl/components/AcmeList.tsx
  7. 67
      src/pages/websites/ssl/index.tsx
  8. 3
      src/request.ts
  9. 7
      src/service/websites.ts
  10. 49
      src/store/websites/acme.ts
  11. 16
      src/store/websites/ssl.ts
  12. 14
      src/types/website/acme.d.ts
  13. 4
      src/types/website/ca.d.ts
  14. 4
      src/types/website/dns.d.ts
  15. 4
      src/types/website/ssl.d.ts

38
src/components/drawer-picker/DrawerPicker.tsx

@ -0,0 +1,38 @@
import { Button, Drawer, DrawerProps } from 'antd'
import React, { useState } from 'react'
import { useStyle } from './style'
import { generateUUID } from '@/utils/uuid.ts'
export interface DrawerPickerProps extends DrawerProps {
target?: React.ReactNode
children?: React.ReactNode
key?: string
}
const DrawerPicker = ({ children, target, ...props }: DrawerPickerProps) => {
const { styles } = useStyle()
const [ open, setOpen ] = useState(false)
const getTarget = () => {
const def = <Button>{props.title ?? 'Target'}</Button>
return <span className={styles.target}>{target ?? def}</span>
}
return (
<div className={styles.container} key={props.key ?? generateUUID()}>
<span className={styles.target} onClick={() => {
setOpen(true)
}}>
{getTarget()}
</span>
<Drawer {...props}
open={open}
onClose={() => setOpen(false)}
>{children}</Drawer>
</div>
)
}
export default DrawerPicker

1
src/components/drawer-picker/index.ts

@ -0,0 +1 @@
export * from './DrawerPicker.tsx'

15
src/components/drawer-picker/style.ts

@ -0,0 +1,15 @@
import { createStyles } from '@/theme'
export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any) => {
const prefix = `${prefixCls}-${token?.proPrefix}-drawer-picker-page`
const container = css`
`
const target = css``
return {
container: cx(prefix, props?.className, container),
target,
}
})

11
src/pages/system/roles/index.tsx

@ -9,7 +9,7 @@ import {
BetaSchemaForm, ProFormColumnsType,
} from '@ant-design/pro-components'
import { useStyle } from './style.ts'
import { memo, useEffect, useMemo, useRef, useState } from 'react'
import { memo, useMemo, useRef, useState } from 'react'
import { useAtom, useAtomValue } from 'jotai'
import {
deleteRoleAtom,
@ -138,13 +138,6 @@ const Roles = memo(() => {
] as ProColumns[]
}, [])
useEffect(() => {
if (isSuccess) {
setOpen(false)
}
}, [ isSuccess ])
return (
<PageContainer breadcrumbRender={false} title={false} className={styles.container}>
<div className={styles.authHeight}>
@ -240,7 +233,7 @@ const Roles = memo(() => {
onFinish={async (values) => {
// console.log('values', values)
mutate(values)
return true
return isSuccess
}}
columns={columns as ProFormColumnsType[]}/>
</PageContainer>

4
src/pages/system/users/index.tsx

@ -31,7 +31,7 @@ const Users = () => {
const [ page, setPage ] = useAtom(userPageAtom)
const [ search, setSearch ] = useAtom(userSearchAtom)
const [ , setCurrent ] = useAtom(userSelectedAtom)
const { mutate: saveOrUpdate, isPending: isSubmitting } = useAtomValue(saveOrUpdateUserAtom)
const { mutate: saveOrUpdate, isSuccess, isPending: isSubmitting } = useAtomValue(saveOrUpdateUserAtom)
const { data, isFetching, isLoading, refetch } = useAtomValue(userListAtom)
const { mutate: deleteUser, isPending, } = useAtomValue(deleteUserAtom)
const { mutate: resetPass, isPending: isResetting } = useAtomValue(resetPasswordAtom)
@ -236,7 +236,7 @@ const Users = () => {
onFinish={async (values) => {
// console.log('values', values)
saveOrUpdate(values)
return true
return isSuccess
}}
columns={columns as ProFormColumnsType[]}/>
</TwoColPageLayout>

174
src/pages/websites/ssl/components/AcmeList.tsx

@ -0,0 +1,174 @@
import { useMemo, useState } from 'react'
import { BetaSchemaForm, ProColumns, ProFormColumnsType, ProTable } from '@ant-design/pro-components'
import { IAcmeAccount } from '@/types/website/acme'
import { useTranslation } from '@/i18n.ts'
import { acmeListAtom, acmePageAtom, AcmeType, saveOrUpdateAcmeAtom } from '@/store/websites/acme.ts'
import { useAtom, useAtomValue } from 'jotai'
import { Alert, Button, Form } from 'antd'
import { KeyTypeEnum } from '@/store/websites/ssl.ts'
const AcmeList = () => {
const { t } = useTranslation()
const [ form ] = Form.useForm()
const [ page, setPage ] = useAtom(acmePageAtom)
const { data, isLoading, refetch } = useAtomValue(acmeListAtom)
const { mutate: saveOrUpdate, isPending: isSubmitting, isSuccess } = useAtomValue(saveOrUpdateAcmeAtom)
const [ open, setOpen ] = useState(false)
const columns = useMemo<ProColumns<IAcmeAccount>[]>(() => {
return [
{
title: 'ID',
dataIndex: 'id',
hideInTable: true,
formItemProps: {
hidden: true,
}
},
{
title: t('website.ssl.acme.columns.email', '邮箱'),
dataIndex: 'email',
valueType: 'text',
formItemProps: {
rules: [
{ required: true, message: t('message.required', '请输入') }
]
}
},
{
title: t('website.ssl.acme.columns.type', '帐号类型'),
dataIndex: 'type',
valueType: 'select',
fieldProps: {
options: [
{ label: 'Let\'s Encrypt', value: AcmeType.LetsEncrypt },
{ label: 'ZeroSSl', value: AcmeType.ZeroSSl },
{ label: 'Buypass', value: AcmeType.Buypass },
{ label: 'Google Cloud', value: AcmeType.Google },
]
},
formItemProps: {
rules: [
{ required: true, message: t('message.required', '请选择') }
]
}
},
{
title: t('website.ssl.acme.columns.keyType', '密钥算法'),
dataIndex: 'keyType',
valueType: 'select',
initialValue: KeyTypeEnum.EC256,
fieldProps: {
options: [
{ label: t('website.ssl.keyTypeEnum.EC256', 'EC 256'), value: KeyTypeEnum.EC256 },
{ label: t('website.ssl.keyTypeEnum.EC384', 'EC 384'), value: KeyTypeEnum.EC384 },
{ label: t('website.ssl.keyTypeEnum.RSA2048', 'RSA 2048'), value: KeyTypeEnum.RSA2048 },
{ label: t('website.ssl.keyTypeEnum.RSA3072', 'RSA 3072'), value: KeyTypeEnum.RSA3072 },
{ label: t('website.ssl.keyTypeEnum.RSA4096', 'RSA 4096'), value: KeyTypeEnum.RSA4096 },
]
},
formItemProps: {
rules: [
{ required: true, message: t('message.required', '请选择') }
]
},
},
{
title: t('website.ssl.acme.columns.url', 'URL'),
dataIndex: 'url',
valueType: 'text',
ellipsis: true, // 文本溢出省略
hideInForm: true,
}, {
title: '操作',
valueType: 'option',
render: (_, record) => {
return [
<a key="edit" onClick={() => {
}}>{t('actions.edit', '编辑')}</a>,
<a key="delete" onClick={() => {
}}>{t('actions.delete', '删除')}</a>,
]
}
}
]
}, [])
return (
<>
<Alert message={t('website.ssl.acme.tip', 'Acme账户用于申请免费证书')}/>
<ProTable<IAcmeAccount>
cardProps={{
bodyStyle: {
padding: 0,
}
}}
rowKey="id"
headerTitle={
<Button
onClick={() => {
form.setFieldsValue({
id: 0,
type: AcmeType.LetsEncrypt,
keyType: KeyTypeEnum.EC256,
})
setOpen(true)
}}
type={'primary'}>{t('website.ssl.acme.add', '添加Acme帐户')}</Button>
}
loading={isLoading}
dataSource={data?.rows ?? []}
columns={columns}
search={false}
options={{
reload: () => {
refetch()
},
}}
pagination={{
total: data?.total,
pageSize: page.pageSize,
current: page.page,
onChange: (current, pageSize) => {
setPage(prev => {
return {
...prev,
page: current,
pageSize: pageSize,
}
})
},
}}
/>
<BetaSchemaForm<IAcmeAccount>
shouldUpdate={false}
width={600}
form={form}
layout={'horizontal'}
scrollToFirstError={true}
title={t(`website.ssl.acme.title_${form.getFieldValue('id') !== 0 ? 'edit' : 'add'}`, form.getFieldValue('id') !== 0 ? '证书编辑' : '证书添加')}
// colProps={{ span: 24 }}
labelCol={{ span: 6 }}
wrapperCol={{ span: 14 }}
layoutType={'ModalForm'}
open={open}
modalProps={{
maskClosable: false,
}}
onOpenChange={(open) => {
setOpen(open)
}}
loading={isSubmitting}
onFinish={async (values) => {
// console.log('values', values)
saveOrUpdate(values)
return isSuccess
}}
columns={columns as ProFormColumnsType[]}/>
</>
)
}
export default AcmeList

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

@ -1,12 +1,21 @@
import { useAtom, useAtomValue } from 'jotai'
import { ProviderTypeEnum, saveOrUpdateSslAtom, sslListAtom, sslPageAtom, sslSearchAtom } from '@/store/websites/ssl.ts'
import {
KeyTypeEnum,
ProviderTypeEnum,
saveOrUpdateSslAtom,
sslListAtom,
sslPageAtom,
sslSearchAtom
} from '@/store/websites/ssl.ts'
import ListPageLayout from '@/layout/ListPageLayout.tsx'
import { BetaSchemaForm, ProColumns, ProFormColumnsType, ProTable } from '@ant-design/pro-components'
import { memo, useMemo, useState } from 'react'
import { useTranslation } from '@/i18n.ts'
import { Button, Form, Popconfirm } from 'antd'
import { PlusOutlined } from '@ant-design/icons'
import { ISSL } from '@/types/ssl'
import { ISSL } from '@/types/website/ssl'
import DrawerPicker from '@/components/drawer-picker/DrawerPicker.tsx'
import AcmeList from '@/pages/websites/ssl/components/AcmeList.tsx'
const SSL = () => {
@ -16,7 +25,7 @@ const SSL = () => {
const [ page, setPage ] = useAtom(sslPageAtom)
const [ search, setSearch ] = useAtom(sslSearchAtom)
const { data, isLoading, isFetching, refetch } = useAtomValue(sslListAtom)
const { mutate: saveOrUpdate, isPending: isSubmitting } = useAtomValue(saveOrUpdateSslAtom)
const { mutate: saveOrUpdate, isSuccess, isPending: isSubmitting } = useAtomValue(saveOrUpdateSslAtom)
const [ open, setOpen ] = useState(false)
@ -34,7 +43,7 @@ const SSL = () => {
{
title: t('website.ssl.columns.primaryDomain', '域名'),
dataIndex: 'primaryDomain',
formItemProps:{
formItemProps: {
label: t('website.ssl.form.primaryDomain', '主域名'),
rules: [ { required: true, message: t('message.required', '主域名') } ]
}
@ -48,6 +57,27 @@ const SSL = () => {
dataIndex: 'acmeAccountId',
},
{
title: t('website.ssl.columns.keyType', '密钥算法'),
dataIndex: 'keyType',
hideInTable: true,
valueType: 'select',
initialValue: KeyTypeEnum.EC256,
fieldProps: {
options: [
{ label: t('website.ssl.keyTypeEnum.EC256', 'EC 256'), value: KeyTypeEnum.EC256 },
{ label: t('website.ssl.keyTypeEnum.EC384', 'EC 384'), value: KeyTypeEnum.EC384 },
{ label: t('website.ssl.keyTypeEnum.RSA2048', 'RSA 2048'), value: KeyTypeEnum.RSA2048 },
{ label: t('website.ssl.keyTypeEnum.RSA3072', 'RSA 3072'), value: KeyTypeEnum.RSA3072 },
{ label: t('website.ssl.keyTypeEnum.RSA4096', 'RSA 4096'), value: KeyTypeEnum.RSA4096 },
]
},
formItemProps: {
rules: [
{ required: true, message: t('message.required', '请选择') }
]
},
},
{
title: t('website.ssl.columns.provider', '申请方式'),
dataIndex: 'provider',
valueType: 'radio',
@ -63,8 +93,8 @@ const SSL = () => {
text: t('website.ssl.providerTypeEnum.Http', 'HTTP'),
}
},
dependencies : [ 'provider' ],
formItemProps: (form, config)=> {
dependencies: [ 'provider' ],
formItemProps: (form, config) => {
const val = form.getFieldValue(config.dataIndex)
const help = {
[ProviderTypeEnum.DnsAccount]: t('website.ssl.form.provider_{{v}}', '', { v: val }),
@ -73,7 +103,7 @@ const SSL = () => {
}
return {
label: t('website.ssl.form.provider', '验证方式'),
help: <span dangerouslySetInnerHTML={{__html: help[val]}} />,
help: <span dangerouslySetInnerHTML={{ __html: help[val] }}/>,
rules: [ { required: true, message: t('message.required', '请选择') } ]
}
},
@ -86,6 +116,10 @@ const SSL = () => {
return [ {
title: t('website.ssl.columns.dnsAccountId', 'DNS帐号'),
dataIndex: 'dnsAccountId',
formItemProps: {
rules: [ { required: true, message: t('message.required', '请输入DNS帐号') } ]
}
} ]
}
return []
@ -179,6 +213,21 @@ const SSL = () => {
placeholder: t('website.ssl.search.placeholder', '输入域名')
},
actions: [
<DrawerPicker
maskClosable={false}
title={t('website.ssl.actions.acme', 'Acme帐户')}
width={1000}
target={<Button>
{t('website.ssl.actions.acme', 'Acme帐户')}
</Button>}
>
<AcmeList/>
</DrawerPicker>,
<DrawerPicker target={<Button>
{t( 'website.ssl.actions.dns', 'DNS帐户')}
</Button>}>
</DrawerPicker>,
<Button
key="button"
icon={<PlusOutlined/>}
@ -191,7 +240,7 @@ const SSL = () => {
}}
type="primary"
>
{t('actions.add', '添加')}
{t('actions.add', '申请证书')}
</Button>,
]
}}
@ -232,7 +281,7 @@ const SSL = () => {
onFinish={async (values) => {
// console.log('values', values)
saveOrUpdate(values)
return true
return isSuccess
}}
columns={columns as ProFormColumnsType[]}/>
</ListPageLayout>

3
src/request.ts

@ -84,6 +84,7 @@ axiosInstance.interceptors.response.use(
}, (error) => {
// console.log('error', error)
message.destroy()
const { response } = error
if (response) {
switch (response.status) {
@ -105,7 +106,7 @@ axiosInstance.interceptors.response.use(
window.location.href = `/login?redirect=${encodeURIComponent(redirect)}`
return
default:
message.error(response.data.message)
message.error(response.data.message ?? response.data ?? error.message ?? '请求失败')
return Promise.reject(response)
}
}

7
src/service/websites.ts

@ -1,8 +1,13 @@
import { createCURD } from '@/service/base.ts'
import { ISSL } from '@/types/website/ssl'
import { IAcmeAccount } from '@/types/website/acme'
const websitesServ = {
ssl: {
...createCURD<any, ISsl>('/website/ssl')
...createCURD<any, ISSL>('/website/ssl')
},
acme:{
...createCURD<any, IAcmeAccount>('/website/acme')
}
}

49
src/store/websites/acme.ts

@ -0,0 +1,49 @@
import { atomWithMutation, atomWithQuery } from 'jotai-tanstack-query'
import { IAcmeAccount } from '@/types/website/acme'
import websitesServ from '@/service/websites.ts'
import { message } from 'antd'
import { t } from 'i18next'
import { IPage } from '@/global'
import { atom } from 'jotai'
export enum AcmeType {
LetsEncrypt = 'letsencrypt',
//zerossl
ZeroSSl = 'zerossl',
//buypass
Buypass = 'buypass',
//google
Google = 'google',
}
export const acmePageAtom = atom<IPage>({
page: 1, pageSize: 10,
})
//list
export const acmeListAtom = atomWithQuery(get => ({
queryKey: [ 'acmeList', get(acmePageAtom) ],
queryFn: async ({ queryKey: [ , page ] }) => {
return await websitesServ.acme.list(page)
},
select: (data) => {
return data.data
}
}))
//saveOrUpdate
export const saveOrUpdateAcmeAtom = atomWithMutation<any, IAcmeAccount>(get => ({
mutationKey: [ 'saveOrUpdateAcme' ],
mutationFn: async (data: IAcmeAccount) => {
if (data.id > 0) {
return await websitesServ.acme.update(data)
}
return await websitesServ.acme.add(data)
},
onSuccess: (res) => {
const isAdd = !!res.data?.id
message.success(t(isAdd ? 'message.saveSuccess' : 'message.editSuccess', '保存成功'))
get(acmeListAtom).refetch()
return res
}
}))

16
src/store/websites/ssl.ts

@ -4,7 +4,7 @@ import { atomWithMutation, atomWithQuery } from 'jotai-tanstack-query'
import websitesServ from '@/service/websites.ts'
import { message } from 'antd'
import { t } from 'i18next'
import { ISSL, SSLSearchParam } from '@/types/ssl'
import { ISSL, SSLSearchParam } from '@/types/website/ssl'
export enum ProviderTypeEnum {
DnsAccount = 'dnsAccount',
@ -12,6 +12,14 @@ export enum ProviderTypeEnum {
Http = 'http'
}
export enum KeyTypeEnum {
EC256 = 'P256',
EC384 = 'P384',
RSA2048 = '2048',
RSA3072 = '3072',
RSA4096 = '4096',
}
export const sslPageAtom = atom<IPage>({
page: 1,
pageSize: 20,
@ -42,10 +50,14 @@ export const saveOrUpdateSslAtom = atomWithMutation<IApiResult, ISSL>(get => ({
return await websitesServ.ssl.update(data)
}
},
// onError: (error: any) => {
// const msg = error?.data?.message ||t('message.saveFailed', '保存失败')
// message.error(msg)
// return error
// },
onSuccess: (res) => {
const isAdd = !!res.data?.id
message.success(t(isAdd ? 'message.saveSuccess' : 'message.editSuccess', '保存成功'))
get(sslListAtom).refetch()
return res
},

14
src/types/website/acme.d.ts

@ -0,0 +1,14 @@
export interface IAcmeAccount {
id: number;
createdAt?: string;
createdBy: number;
updatedAt?: string;
updatedBy: number;
email: string;
url: string;
privateKey: string;
type: string;
eabKid: string;
eabHmacKey: string;
keyType: string;
}

4
src/types/ca.ts → src/types/website/ca.d.ts

@ -1,8 +1,8 @@
export interface ICA {
id: number;
createdAt: Date | null;
createdAt: string | null;
createdBy: number;
updatedAt: Date | null;
updatedAt: string | null;
updatedBy: number;
csr: string;
name: string;

4
src/types/dns.ts → src/types/website/dns.d.ts

@ -1,8 +1,8 @@
export interface IDnsAccount {
id: number;
createdAt: Date | null;
createdAt: string | null;
createdBy: number;
updatedAt: Date | null;
updatedAt: string | null;
updatedBy: number;
name: string;
type: string;

4
src/types/ssl.d.ts → src/types/website/ssl.d.ts

@ -16,8 +16,8 @@ export interface ISSL {
acmeAccountId: number;
caId: number;
autoRenew: boolean;
expireDate: Date | null;
startDate: Date | null;
expireDate: string | null;
startDate: string | null;
status: string;
message: string;
keyType: string;
Loading…
Cancel
Save