18 changed files with 435 additions and 84 deletions
			
			
		- 
					38src/components/drawer-picker/DrawerPicker.tsx
 - 
					1src/components/drawer-picker/index.ts
 - 
					15src/components/drawer-picker/style.ts
 - 
					11src/pages/system/roles/index.tsx
 - 
					4src/pages/system/users/index.tsx
 - 
					174src/pages/websites/ssl/components/AcmeList.tsx
 - 
					59src/pages/websites/ssl/index.tsx
 - 
					3src/request.ts
 - 
					7src/service/websites.ts
 - 
					49src/store/websites/acme.ts
 - 
					16src/store/websites/ssl.ts
 - 
					14src/types/website/acme.d.ts
 - 
					4src/types/website/ca.d.ts
 - 
					4src/types/website/dns.d.ts
 - 
					4src/types/website/ssl.d.ts
 
@ -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 | 
			
		||||
@ -0,0 +1 @@ | 
			
		|||||
 | 
				export * from './DrawerPicker.tsx' | 
			
		||||
@ -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, | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				}) | 
			
		||||
@ -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 | 
			
		||||
@ -1,8 +1,13 @@ | 
			
		|||||
import { createCURD } from '@/service/base.ts' | 
				import { createCURD } from '@/service/base.ts' | 
			
		||||
 | 
				import { ISSL } from '@/types/website/ssl' | 
			
		||||
 | 
				import { IAcmeAccount } from '@/types/website/acme' | 
			
		||||
 | 
				
 | 
			
		||||
const websitesServ = { | 
				const websitesServ = { | 
			
		||||
    ssl: { | 
				    ssl: { | 
			
		||||
        ...createCURD<any, ISsl>('/website/ssl') | 
				 | 
			
		||||
 | 
				        ...createCURD<any, ISSL>('/website/ssl') | 
			
		||||
 | 
				    }, | 
			
		||||
 | 
				    acme:{ | 
			
		||||
 | 
				        ...createCURD<any, IAcmeAccount>('/website/acme') | 
			
		||||
    } | 
				    } | 
			
		||||
} | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
@ -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 | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				})) | 
			
		||||
@ -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; | 
			
		||||
 | 
				} | 
			
		||||
@ -1,8 +1,8 @@ | 
			
		|||||
export interface ICA { | 
				export interface ICA { | 
			
		||||
    id: number; | 
				    id: number; | 
			
		||||
    createdAt: Date | null; | 
				 | 
			
		||||
 | 
				    createdAt: string | null; | 
			
		||||
    createdBy: number; | 
				    createdBy: number; | 
			
		||||
    updatedAt: Date | null; | 
				 | 
			
		||||
 | 
				    updatedAt: string | null; | 
			
		||||
    updatedBy: number; | 
				    updatedBy: number; | 
			
		||||
    csr: string; | 
				    csr: string; | 
			
		||||
    name: string; | 
				    name: string; | 
			
		||||
@ -1,8 +1,8 @@ | 
			
		|||||
export interface IDnsAccount { | 
				export interface IDnsAccount { | 
			
		||||
    id: number; | 
				    id: number; | 
			
		||||
    createdAt: Date | null; | 
				 | 
			
		||||
 | 
				    createdAt: string | null; | 
			
		||||
    createdBy: number; | 
				    createdBy: number; | 
			
		||||
    updatedAt: Date | null; | 
				 | 
			
		||||
 | 
				    updatedAt: string | null; | 
			
		||||
    updatedBy: number; | 
				    updatedBy: number; | 
			
		||||
    name: string; | 
				    name: string; | 
			
		||||
    type: string; | 
				    type: string; | 
			
		||||
						Write
						Preview
					
					
					Loading…
					
					Cancel
						Save
					
		Reference in new issue