mirror of
https://github.com/certimate-go/certimate.git
synced 2026-06-22 21:05:48 +08:00
Merge pull request #999 from fudiwei/main
This commit is contained in:
commit
e2dae240fa
@ -2,4 +2,7 @@ package dtos
|
||||
|
||||
type NotifyTestPushReq struct {
|
||||
Provider string `json:"provider"`
|
||||
AccessId string `json:"accessId"`
|
||||
}
|
||||
|
||||
type NotifyTestPushResp struct{}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
11
internal/notify/service_deps.go
Normal file
11
internal/notify/service_deps.go
Normal 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)
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
|
||||
@ -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")} />
|
||||
|
||||
@ -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;
|
||||
@ -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",
|
||||
|
||||
@ -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 提供商",
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user