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 { 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') | 
				
			|||
    } | 
				
			|||
} | 
				
			|||
 | 
				
			|||
@ -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 { | 
				
			|||
    id: number; | 
				
			|||
    createdAt: Date | null; | 
				
			|||
    createdAt: string | null; | 
				
			|||
    createdBy: number; | 
				
			|||
    updatedAt: Date | null; | 
				
			|||
    updatedAt: string | null; | 
				
			|||
    updatedBy: number; | 
				
			|||
    csr: string; | 
				
			|||
    name: string; | 
				
			|||
@ -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; | 
				
			|||
						Write
						Preview
					
					
					Loading…
					
					Cancel
						Save
					
		Reference in new issue