diff --git a/internal/domain/dtos/notify.go b/internal/domain/dtos/notify.go index 1a2d11a0..afa3479e 100644 --- a/internal/domain/dtos/notify.go +++ b/internal/domain/dtos/notify.go @@ -2,4 +2,7 @@ package dtos type NotifyTestPushReq struct { Provider string `json:"provider"` + AccessId string `json:"accessId"` } + +type NotifyTestPushResp struct{} diff --git a/internal/notify/service.go b/internal/notify/service.go index 286bab07..f89f914a 100644 --- a/internal/notify/service.go +++ b/internal/notify/service.go @@ -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 } diff --git a/internal/notify/service_deps.go b/internal/notify/service_deps.go new file mode 100644 index 00000000..492686d0 --- /dev/null +++ b/internal/notify/service_deps.go @@ -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) +} diff --git a/internal/rest/handlers/notify.go b/internal/rest/handlers/notify.go index cc203d5f..469fe0e9 100644 --- a/internal/rest/handlers/notify.go +++ b/internal/rest/handlers/notify.go @@ -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) } diff --git a/internal/rest/routes/routes.go b/internal/rest/routes/routes.go index 960297b3..7d5f5c69 100644 --- a/internal/rest/routes/routes.go +++ b/internal/rest/routes/routes.go @@ -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()) diff --git a/ui/src/api/notify.ts b/ui/src/api/notify.ts index b46f7177..d8b91a4d 100644 --- a/ui/src/api/notify.ts +++ b/ui/src/api/notify.ts @@ -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("/api/notify/test", { @@ -12,6 +12,7 @@ export const notifyTest = async (provider: string) => { }, body: { provider, + accessId, }, }); diff --git a/ui/src/components/access/AccessEditDrawer.tsx b/ui/src/components/access/AccessEditDrawer.tsx index 5797bb42..faff133c 100644 --- a/ui/src/components/access/AccessEditDrawer.tsx +++ b/ui/src/components/access/AccessEditDrawer.tsx @@ -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("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 ? ( - - - + + {usage === "notification" ? ( + + ) : ( + {/* TODO: 测试连接 */} + )} + + + + ) : ( false @@ -128,7 +174,7 @@ const AccessEditDrawer = ({ afterClose, afterSubmit, mode, data, loading, trigge title={
- {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`)}
- - ); -}; - -export default NotifyTestButton; diff --git a/ui/src/i18n/locales/en/nls.access.json b/ui/src/i18n/locales/en/nls.access.json index 3de9748b..cdda2558 100644 --- a/ui/src/i18n/locales/en/nls.access.json +++ b/ui/src/i18n/locales/en/nls.access.json @@ -20,6 +20,7 @@ "access.action.delete.modal.content": "Are you sure want to delete this credential?
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?
This action cannot be undone.", + "access.action.test_push.button": "Test push", "access.props.name": "Name", "access.props.provider.usage.dns": "DNS", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index f8229a8f..48b35010 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -19,6 +19,7 @@ "access.action.delete.modal.content": "确定要删除该授权吗?
注意此操作不可撤销,请谨慎操作。", "access.action.batch_delete.modal.title": "删除授权", "access.action.batch_delete.modal.content": "确定要删除这 {{count}} 个被选中的授权吗?
注意此操作不可撤销,请谨慎操作。", + "access.action.test_push.button": "通知测试", "access.props.name": "名称", "access.props.provider.usage.dns": "DNS 提供商", diff --git a/ui/src/pages/accesses/AccessList.tsx b/ui/src/pages/accesses/AccessList.tsx index 2bbd8e1b..69711792 100644 --- a/ui/src/pages/accesses/AccessList.tsx +++ b/ui/src/pages/accesses/AccessList.tsx @@ -469,7 +469,7 @@ const AccessList = () => { - + ); diff --git a/ui/src/pages/settings/SettingsSSLProvider.tsx b/ui/src/pages/settings/SettingsSSLProvider.tsx index 8e3af614..342e6245 100644 --- a/ui/src/pages/settings/SettingsSSLProvider.tsx +++ b/ui/src/pages/settings/SettingsSSLProvider.tsx @@ -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,