Merge pull request #999 from fudiwei/main

This commit is contained in:
RHQYZ 2025-10-20 20:02:10 +08:00 committed by GitHub
commit e2dae240fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 118 additions and 79 deletions

View File

@ -2,4 +2,7 @@ package dtos
type NotifyTestPushReq struct {
Provider string `json:"provider"`
AccessId string `json:"accessId"`
}
type NotifyTestPushResp struct{}

View File

@ -12,13 +12,39 @@ const (
notifyTestMessage = "Welcome to use Certimate!"
)
type NotifyService struct{}
func NewNotifyService() *NotifyService {
return &NotifyService{}
type NotifyService struct {
accessRepo accessRepository
}
func (n *NotifyService) TestPush(ctx context.Context, req *dtos.NotifyTestPushReq) error {
// TODO: 测试通知
return fmt.Errorf("not implemented")
func NewNotifyService(accessRepo accessRepository) *NotifyService {
return &NotifyService{
accessRepo: accessRepo,
}
}
func (n *NotifyService) TestPush(ctx context.Context, req *dtos.NotifyTestPushReq) (*dtos.NotifyTestPushResp, error) {
accessConfig := make(map[string]any)
if access, err := n.accessRepo.GetById(ctx, req.AccessId); err != nil {
return nil, fmt.Errorf("failed to get access #%s record: %w", req.AccessId, err)
} else {
if access.Reserve != "notif" {
return nil, fmt.Errorf("access #%s is not available for notification", req.AccessId)
}
accessConfig = access.Config
}
notifier := NewClient()
notifyReq := &SendNotificationRequest{
Provider: req.Provider,
ProviderAccessConfig: accessConfig,
ProviderExtendedConfig: make(map[string]any),
Subject: notifyTestSubject,
Message: notifyTestMessage,
}
if _, err := notifier.SendNotification(ctx, notifyReq); err != nil {
return nil, err
}
return &dtos.NotifyTestPushResp{}, nil
}

View File

@ -0,0 +1,11 @@
package notify
import (
"context"
"github.com/certimate-go/certimate/internal/domain"
)
type accessRepository interface {
GetById(ctx context.Context, id string) (*domain.Access, error)
}

View File

@ -11,7 +11,7 @@ import (
)
type notifyService interface {
TestPush(ctx context.Context, req *dtos.NotifyTestPushReq) error
TestPush(ctx context.Context, req *dtos.NotifyTestPushReq) (*dtos.NotifyTestPushResp, error)
}
type NotifyHandler struct {
@ -33,9 +33,10 @@ func (handler *NotifyHandler) test(e *core.RequestEvent) error {
return resp.Err(e, err)
}
if err := handler.service.TestPush(e.Request.Context(), req); err != nil {
res, err := handler.service.TestPush(e.Request.Context(), req)
if err != nil {
return resp.Err(e, err)
}
return resp.Ok(e, nil)
return resp.Ok(e, res)
}

View File

@ -23,6 +23,7 @@ var (
)
func Register(router *router.Router[*core.RequestEvent]) {
accessRepo := repository.NewAccessRepository()
workflowRepo := repository.NewWorkflowRepository()
workflowRunRepo := repository.NewWorkflowRunRepository()
certificateRepo := repository.NewCertificateRepository()
@ -32,7 +33,7 @@ func Register(router *router.Router[*core.RequestEvent]) {
certificateSvc = certificate.NewCertificateService(certificateRepo, settingsRepo)
workflowSvc = workflow.NewWorkflowService(workflowRepo, workflowRunRepo, settingsRepo)
statisticsSvc = statistics.NewStatisticsService(statisticsRepo)
notifySvc = notify.NewNotifyService()
notifySvc = notify.NewNotifyService(accessRepo)
group := router.Group("/api")
group.Bind(apis.RequireSuperuserAuth())

View File

@ -2,7 +2,7 @@ import { ClientResponseError } from "pocketbase";
import { getPocketBase } from "@/repository/_pocketbase";
export const notifyTest = async (provider: string) => {
export const notifyTest = async ({ provider, accessId }: { provider: string; accessId: string }) => {
const pb = getPocketBase();
const resp = await pb.send<BaseResponse>("/api/notify/test", {
@ -12,6 +12,7 @@ export const notifyTest = async (provider: string) => {
},
body: {
provider,
accessId,
},
});

View File

@ -8,6 +8,7 @@ import AccessProviderPicker from "@/components/provider/AccessProviderPicker";
import Show from "@/components/Show";
import { type AccessModel } from "@/domain/access";
import { ACCESS_USAGES } from "@/domain/provider";
import { notifyTest } from "@/api/notify";
import { useTriggerElement, useZustandShallowSelector } from "@/hooks";
import { useAccessesStore } from "@/stores/access";
import { getErrMsg } from "@/utils/error";
@ -26,7 +27,7 @@ export interface AccessEditDrawerProps {
onOpenChange?: (open: boolean) => void;
}
const AccessEditDrawer = ({ afterClose, afterSubmit, mode, data, loading, trigger, usage, ...props }: AccessEditDrawerProps) => {
const AccessEditDrawer = ({ afterSubmit, mode, data, loading, trigger, usage, ...props }: AccessEditDrawerProps) => {
const { t } = useTranslation();
const { message, notification } = App.useApp();
@ -39,19 +40,33 @@ const AccessEditDrawer = ({ afterClose, afterSubmit, mode, data, loading, trigge
trigger: "onOpenChange",
});
const afterClose = () => {
setFormPending(false);
setFormChanged(false);
setIsTesting(false);
props.afterClose?.();
};
const triggerEl = useTriggerElement(trigger, { onClick: () => setOpen(true) });
const providerFilter = AccessForm.useProviderFilterByUsage(usage);
const [formInst] = Form.useForm();
const [formPending, setFormPending] = useState(false);
const [formChanged, setFormChanged] = useState(false);
const fieldProvider = Form.useWatch<string>("provider", { form: formInst, preserve: true });
const [isTesting, setIsTesting] = useState(false);
const handleProviderPick = (value: string) => {
formInst.setFieldValue("provider", value);
};
const handleFormChange = () => {
setFormChanged(true);
};
const handleOkClick = async () => {
let formValues: AccessModel;
@ -73,7 +88,7 @@ const AccessEditDrawer = ({ afterClose, afterSubmit, mode, data, loading, trigge
}
formValues = await createAccess(formValues);
} else if (mode === "edit") {
} else if (mode === "modify") {
if (!data?.id) {
throw "Invalid props: `data`";
}
@ -100,6 +115,26 @@ const AccessEditDrawer = ({ afterClose, afterSubmit, mode, data, loading, trigge
setOpen(false);
};
const handleTestPushClick = async () => {
setIsTesting(true);
try {
await formInst.validateFields();
} catch {
setIsTesting(false);
return;
}
try {
await notifyTest({ provider: fieldProvider, accessId: data?.id! });
message.success(t("common.text.operation_succeeded"));
} catch (err) {
notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
} finally {
setIsTesting(false);
}
};
return (
<>
{triggerEl}
@ -111,11 +146,22 @@ const AccessEditDrawer = ({ afterClose, afterSubmit, mode, data, loading, trigge
destroyOnHidden
footer={
fieldProvider ? (
<Flex className="px-2" justify="end" gap="small">
<Button onClick={handleCancelClick}>{t("common.button.cancel")}</Button>
<Button loading={formPending} type="primary" onClick={handleOkClick}>
{mode === "edit" ? t("common.button.save") : t("common.button.submit")}
</Button>
<Flex className="px-2" justify="space-between">
{usage === "notification" ? (
<Button disabled={mode !== "modify" || formChanged} loading={isTesting} onClick={handleTestPushClick}>
{t("access.action.test_push.button")}
</Button>
) : (
<span>{/* TODO: 测试连接 */}</span>
)}
<Flex justify="end" gap="small">
<Button disabled={isTesting} onClick={handleCancelClick}>
{t("common.button.cancel")}
</Button>
<Button disabled={isTesting} loading={formPending} type="primary" onClick={handleOkClick}>
{mode === "modify" ? t("common.button.save") : t("common.button.submit")}
</Button>
</Flex>
</Flex>
) : (
false
@ -128,7 +174,7 @@ const AccessEditDrawer = ({ afterClose, afterSubmit, mode, data, loading, trigge
title={
<Flex align="center" justify="space-between" gap="small">
<div className="flex-1 truncate">
{mode === "edit" && !!data ? t("access.action.edit.modal.title") + ` #${data.id}` : t(`access.action.${mode}.modal.title`)}
{mode === "modify" && !!data ? t("access.action.edit.modal.title") + ` #${data.id}` : t(`access.action.${mode}.modal.title`)}
</div>
<Button
className="ant-drawer-close"
@ -158,7 +204,7 @@ const AccessEditDrawer = ({ afterClose, afterSubmit, mode, data, loading, trigge
</Show>
<div style={{ display: fieldProvider || data?.provider ? "block" : "none" }}>
<AccessForm form={formInst} disabled={formPending} initialValues={data} mode={mode} usage={usage} />
<AccessForm form={formInst} disabled={formPending} initialValues={data} mode={mode} usage={usage} onFormValuesChange={handleFormChange} />
</div>
</Drawer>
</>

View File

@ -90,7 +90,7 @@ import AccessConfigFieldsProviderWeComBot from "./forms/AccessConfigFieldsProvid
import AccessConfigFieldsProviderWestcn from "./forms/AccessConfigFieldsProviderWestcn";
import AccessConfigFieldsProviderZeroSSL from "./forms/AccessConfigFieldsProviderZeroSSL";
export type AccessFormModes = "create" | "edit";
export type AccessFormModes = "create" | "modify";
export type AccessFormUsages = "dns" | "hosting" | "dns-hosting" | "ca" | "notification";
export interface AccessFormProps {
@ -101,9 +101,10 @@ export interface AccessFormProps {
form: FormInstance;
mode: AccessFormModes;
usage?: AccessFormUsages;
onFormValuesChange?: (changedValues: Nullish<MaybeModelRecord<AccessModel>>, values: Nullish<MaybeModelRecord<AccessModel>>) => void;
}
const AccessForm = ({ className, style, disabled, initialValues, mode, usage, ...props }: AccessFormProps) => {
const AccessForm = ({ className, style, disabled, initialValues, mode, usage, onFormValuesChange, ...props }: AccessFormProps) => {
const { t } = useTranslation();
const formSchema = z.object({
@ -381,6 +382,7 @@ const AccessForm = ({ className, style, disabled, initialValues, mode, usage, ..
layout="vertical"
preserve={false}
scrollToFirstError
onValuesChange={onFormValuesChange}
>
<Form.Item name="name" label={t("access.form.name.label")} rules={[formRule]}>
<Input placeholder={t("access.form.name.placeholder")} />

View File

@ -1,54 +0,0 @@
import { useTranslation } from "react-i18next";
import { useRequest } from "ahooks";
import { Button, type ButtonProps, message, notification } from "antd";
import { notifyTest } from "@/api/notify";
import { getErrMsg } from "@/utils/error";
export interface NotifyTestButtonProps {
className?: string;
style?: React.CSSProperties;
disabled?: boolean;
provider: string;
size?: ButtonProps["size"];
}
const NotifyTestButton = ({ className, style, provider, disabled, size }: NotifyTestButtonProps) => {
const { t } = useTranslation();
const [messageApi, MessageContextHolder] = message.useMessage();
const [notificationApi, NotificationContextHolder] = notification.useNotification();
const { loading, run: executeNotifyTest } = useRequest(
() => {
return notifyTest(provider);
},
{
refreshDeps: [provider],
manual: true,
onSuccess: () => {
messageApi.success(t("settings.notification.push_test.pushed"));
},
onError: (err) => {
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
},
}
);
const handleClick = () => {
executeNotifyTest();
};
return (
<>
{MessageContextHolder}
{NotificationContextHolder}
<Button className={className} style={style} disabled={disabled} loading={loading} size={size} onClick={handleClick}>
{t("settings.notification.push_test.button")}
</Button>
</>
);
};
export default NotifyTestButton;

View File

@ -20,6 +20,7 @@
"access.action.delete.modal.content": "Are you sure want to delete this credential? <br>This action cannot be undone.",
"access.action.batch_delete.modal.title": "Delete credentials",
"access.action.batch_delete.modal.content": "Are you sure want to delete these {{count}} selected credentials? <br>This action cannot be undone.",
"access.action.test_push.button": "Test push",
"access.props.name": "Name",
"access.props.provider.usage.dns": "DNS",

View File

@ -19,6 +19,7 @@
"access.action.delete.modal.content": "确定要删除该授权吗?<br>注意此操作不可撤销,请谨慎操作。",
"access.action.batch_delete.modal.title": "删除授权",
"access.action.batch_delete.modal.content": "确定要删除这 {{count}} 个被选中的授权吗?<br>注意此操作不可撤销,请谨慎操作。",
"access.action.test_push.button": "通知测试",
"access.props.name": "名称",
"access.props.provider.usage.dns": "DNS 提供商",

View File

@ -469,7 +469,7 @@ const AccessList = () => {
</div>
<AccessEditDrawer mode="create" usage={filters["usage"] as AccessUsages} {...createDrawerProps} />
<AccessEditDrawer mode="edit" usage={filters["usage"] as AccessUsages} {...detailDrawerProps} />
<AccessEditDrawer mode="modify" usage={filters["usage"] as AccessUsages} {...detailDrawerProps} />
</div>
</div>
);

View File

@ -47,7 +47,7 @@ const SettingsSSLProvider = () => {
[CA_PROVIDERS.SECTIGO, "provider.sectigo", "sectigo.com", "/imgs/providers/sectigo.svg"],
[CA_PROVIDERS.SSLCOM, "provider.sslcom", "ssl.com", "/imgs/providers/sslcom.svg"],
[CA_PROVIDERS.ZEROSSL, "provider.zerossl", "zerossl.com", "/imgs/providers/zerossl.svg"],
[CA_PROVIDERS.ACMECA, "provider.acmeca", "\u00A0", "/imgs/providers/acmeca.svg"],
[CA_PROVIDERS.ACMECA, "provider.acmeca", "ACME v2 (RFC 8555)", "/imgs/providers/acmeca.svg"],
].map(([value, name, description, icon]) => {
return {
value: value as CAProviderType,