diff --git a/internal/certapply/applicators/registry.go b/internal/certapply/applicators/registry.go index 9e871bbd9..31a7f2e8a 100644 --- a/internal/certapply/applicators/registry.go +++ b/internal/certapply/applicators/registry.go @@ -12,9 +12,9 @@ type ProviderFactoryFunc func(options *ProviderFactoryOptions) (core.ACMEChallen type ProviderFactoryOptions struct { ProviderAccessConfig map[string]any ProviderExtendedConfig map[string]any - DnsPropagationWait int32 - DnsPropagationTimeout int32 - DnsTTL int32 + DnsPropagationWait int + DnsPropagationTimeout int + DnsTTL int } type Registry[T comparable] interface { diff --git a/internal/certapply/applicators/sp_akamai_edgedns.go b/internal/certapply/applicators/sp_akamai_edgedns.go index 64f858236..895666875 100644 --- a/internal/certapply/applicators/sp_akamai_edgedns.go +++ b/internal/certapply/applicators/sp_akamai_edgedns.go @@ -22,8 +22,8 @@ func init() { ClientToken: credentials.ClientToken, ClientSecret: credentials.ClientSecret, AccessToken: credentials.AccessToken, - DnsPropagationTimeout: int(options.DnsPropagationTimeout), - DnsTTL: int(options.DnsTTL), + DnsPropagationTimeout: options.DnsPropagationTimeout, + DnsTTL: options.DnsTTL, }) return provider, err }); err != nil { diff --git a/internal/certapply/client_certifier.go b/internal/certapply/client_certifier.go index fea3659bf..688c96bca 100644 --- a/internal/certapply/client_certifier.go +++ b/internal/certapply/client_certifier.go @@ -23,10 +23,10 @@ import ( ) type ObtainCertificateRequest struct { - Domains []string - PrivateKeyType certcrypto.KeyType - PrivateKeyPEM string - ValidityTo time.Time + Domains []string + PrivateKeyType certcrypto.KeyType + PrivateKeyPEM string + ValidityNotAfter time.Time // 提供商相关 ChallengeType string @@ -39,12 +39,12 @@ type ObtainCertificateRequest struct { Nameservers []string // DNS-01 质询相关 - DnsPropagationWait int32 - DnsPropagationTimeout int32 - DnsTTL int32 + DnsPropagationWait int + DnsPropagationTimeout int + DnsTTL int // HTTP-01 质询相关 - HttpDelayWait int32 + HttpDelayWait int // ACME 相关 PreferredChain string @@ -169,7 +169,7 @@ func (c *ACMEClient) sendObtainCertificateRequest(request *ObtainCertificateRequ Bundle: true, PreferredChain: request.PreferredChain, Profile: request.ACMEProfile, - NotAfter: request.ValidityTo, + NotAfter: request.ValidityNotAfter, ReplacesCertID: lo.If(request.ARIReplacesAcctUrl == c.account.ACMEAcctUrl, request.ARIReplacesCertId).Else(""), } resp, err := c.client.Certificate.Obtain(req) diff --git a/internal/certapply/config.go b/internal/certapply/config.go index 58509d163..b1c160b67 100644 --- a/internal/certapply/config.go +++ b/internal/certapply/config.go @@ -53,7 +53,7 @@ func NewACMEConfig(options *ACMEConfigOptions) (*ACMEConfig, error) { if options.CAProvider == "" { settingsRepo := repository.NewSettingsRepository() - settings, _ := settingsRepo.GetByName(context.Background(), "sslProvider") + settings, _ := settingsRepo.GetByName(context.Background(), domain.SettingsNameSSLProvider) if settings != nil { sslProviderSettings := settings.Content.AsSSLProvider() caProvider = string(sslProviderSettings.Provider) diff --git a/internal/certdeploy/deployers/sp_rainyun_rcdn.go b/internal/certdeploy/deployers/sp_rainyun_rcdn.go index 0a90c0502..76983a893 100644 --- a/internal/certdeploy/deployers/sp_rainyun_rcdn.go +++ b/internal/certdeploy/deployers/sp_rainyun_rcdn.go @@ -18,7 +18,7 @@ func init() { provider, err := rainyunrcdn.NewSSLDeployerProvider(&rainyunrcdn.SSLDeployerProviderConfig{ ApiKey: credentials.ApiKey, - InstanceId: xmaps.GetInt32(options.ProviderExtendedConfig, "instanceId"), + InstanceId: xmaps.GetInt64(options.ProviderExtendedConfig, "instanceId"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err diff --git a/internal/certdeploy/deployers/sp_safeline.go b/internal/certdeploy/deployers/sp_safeline.go index c604b735f..dac842625 100644 --- a/internal/certdeploy/deployers/sp_safeline.go +++ b/internal/certdeploy/deployers/sp_safeline.go @@ -21,7 +21,7 @@ func init() { ApiToken: credentials.ApiToken, AllowInsecureConnections: credentials.AllowInsecureConnections, ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), - CertificateId: xmaps.GetInt32(options.ProviderExtendedConfig, "certificateId"), + CertificateId: xmaps.GetInt64(options.ProviderExtendedConfig, "certificateId"), }) return provider, err }); err != nil { diff --git a/internal/certdeploy/deployers/sp_webhook.go b/internal/certdeploy/deployers/sp_webhook.go index c53ff7304..c0a316707 100644 --- a/internal/certdeploy/deployers/sp_webhook.go +++ b/internal/certdeploy/deployers/sp_webhook.go @@ -43,7 +43,7 @@ func init() { WebhookData: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "webhookData", credentials.DataString), Method: credentials.Method, Headers: mergedHeaders, - Timeout: xmaps.GetInt32(options.ProviderExtendedConfig, "timeout"), + Timeout: xmaps.GetInt(options.ProviderExtendedConfig, "timeout"), AllowInsecureConnections: credentials.AllowInsecureConnections, }) return provider, err diff --git a/internal/certificate/service.go b/internal/certificate/service.go index 54e52ba53..28602fd4c 100644 --- a/internal/certificate/service.go +++ b/internal/certificate/service.go @@ -5,6 +5,7 @@ import ( "bytes" "context" "crypto/x509" + "errors" "fmt" "log/slog" "strings" @@ -255,17 +256,21 @@ func (s *CertificateService) ValidatePrivateKey(ctx context.Context, req *dtos.C } func (s *CertificateService) cleanupExpiredCertificates(ctx context.Context) error { - settings, err := s.settingsRepo.GetByName(ctx, "persistence") + settings, err := s.settingsRepo.GetByName(ctx, domain.SettingsNamePersistence) if err != nil { + if errors.Is(err, domain.ErrRecordNotFound) { + return nil + } + app.GetLogger().Error("failed to get persistence settings", slog.Any("error", err)) return err } persistenceSettings := settings.Content.AsPersistence() - if persistenceSettings.ExpiredCertificatesMaxDaysRetention != 0 { + if persistenceSettings.CertificatesRetentionMaxDays != 0 { ret, err := s.certificateRepo.DeleteWhere( context.Background(), - dbx.NewExp(fmt.Sprintf("validityNotAfter DATETIME('now') and validityNotAfter < DATETIME('now', '+20 days') AND deleted = ''"). - One(&certExpiringSoonTotal); err != nil { + NewQuery(fmt.Sprintf("SELECT COUNT(*) AS total FROM certificate WHERE validityNotAfter <= DATETIME('now', '+%d days') AND validityNotAfter > DATETIME('now') AND isRevoked = 0 AND deleted = ''", persistenceSettings.CertificatesWarningDaysBeforeExpire)). + One(&rsCertExpiringSoonTotal); err != nil { return nil, err } - rs.CertificateExpiringSoon = certExpiringSoonTotal.Total + statistics.CertificateExpiringSoon = rsCertExpiringSoonTotal.Total - // 已过期证书 - certExpiredTotal := struct { + // 统计已过期证书 + rsCertExpiredTotal := struct { Total int `db:"total"` }{} if err := app.GetDB(). - NewQuery("SELECT COUNT(*) AS total FROM certificate WHERE validityNotAfter < DATETIME('now') AND deleted = ''"). - One(&certExpiredTotal); err != nil { + NewQuery("SELECT COUNT(*) AS total FROM certificate WHERE validityNotAfter <= DATETIME('now') AND deleted = ''"). + One(&rsCertExpiredTotal); err != nil { return nil, err } - rs.CertificateExpired = certExpiredTotal.Total + statistics.CertificateExpired = rsCertExpiredTotal.Total - // 所有工作流 - workflowTotal := struct { + // 统计所有工作流 + rsWorkflowTotal := struct { Total int `db:"total"` }{} if err := app.GetDB(). NewQuery("SELECT COUNT(*) AS total FROM workflow"). - One(&workflowTotal); err != nil { + One(&rsWorkflowTotal); err != nil { return nil, err } - rs.WorkflowTotal = workflowTotal.Total + statistics.WorkflowTotal = rsWorkflowTotal.Total - // 已启用工作流 - workflowEnabledTotal := struct { + // 统计已启用工作流 + rsWorkflowEnabledTotal := struct { Total int `db:"total"` }{} if err := app.GetDB(). NewQuery("SELECT COUNT(*) AS total FROM workflow WHERE enabled IS TRUE"). - One(&workflowEnabledTotal); err != nil { + One(&rsWorkflowEnabledTotal); err != nil { return nil, err } - rs.WorkflowEnabled = workflowEnabledTotal.Total - rs.WorkflowDisabled = workflowTotal.Total - workflowEnabledTotal.Total + statistics.WorkflowEnabled = rsWorkflowEnabledTotal.Total + statistics.WorkflowDisabled = rsWorkflowTotal.Total - rsWorkflowEnabledTotal.Total - return rs, nil + return statistics, nil } diff --git a/internal/workflow/engine/executor_bizapply.go b/internal/workflow/engine/executor_bizapply.go index e92518763..a211ba6fa 100644 --- a/internal/workflow/engine/executor_bizapply.go +++ b/internal/workflow/engine/executor_bizapply.go @@ -100,13 +100,6 @@ func (ne *bizApplyNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExe return execRes, err } - // 解析证书 - certX509, err := xcert.ParseCertificateFromPEM(obtainResp.FullChainCertificate) - if err != nil { - ne.logger.Warn("could not parse certificate, may be the CA responded error") - return execRes, err - } - // 保存证书实体 certificate := &domain.Certificate{ Source: domain.CertificateSourceTypeRequest, @@ -120,7 +113,7 @@ func (ne *bizApplyNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExe WorkflowRunId: execCtx.RunId, WorkflowNodeId: execCtx.Node.Id, } - certificate.PopulateFromX509(certX509) + certificate.PopulateFromPEM(obtainResp.FullChainCertificate, obtainResp.PrivateKey) if certificate, err := ne.certificateRepo.Save(execCtx.ctx, certificate); err != nil { ne.logger.Warn("could not save certificate") return execRes, err @@ -308,7 +301,7 @@ func (ne *bizApplyNodeExecutor) executeObtain(execCtx *NodeExecutionContext, nod } return "" }), - ValidityTo: lo. + ValidityNotAfter: lo. If(nodeCfg.ValidityLifetime == "", time.Time{}). ElseF(func() time.Time { duration, err := str2duration.ParseDuration(nodeCfg.ValidityLifetime) diff --git a/internal/workflow/service.go b/internal/workflow/service.go index f450a28e1..3fcb75e64 100644 --- a/internal/workflow/service.go +++ b/internal/workflow/service.go @@ -150,19 +150,23 @@ func (s *WorkflowService) Shutdown(ctx context.Context) { } func (s *WorkflowService) cleanupHistoryRuns(ctx context.Context) error { - settings, err := s.settingsRepo.GetByName(ctx, "persistence") + settings, err := s.settingsRepo.GetByName(ctx, domain.SettingsNamePersistence) if err != nil { + if errors.Is(err, domain.ErrRecordNotFound) { + return nil + } + app.GetLogger().Error("failed to get persistence settings", slog.Any("error", err)) return err } persistenceSettings := settings.Content.AsPersistence() - if persistenceSettings.WorkflowRunsMaxDaysRetention != 0 { + if persistenceSettings.WorkflowRunsRetentionMaxDays != 0 { ret, err := s.workflowRunRepo.DeleteWhere( ctx, dbx.NewExp(fmt.Sprintf("status!='%s'", string(domain.WorkflowRunStatusTypePending))), dbx.NewExp(fmt.Sprintf("status!='%s'", string(domain.WorkflowRunStatusTypeProcessing))), - dbx.NewExp(fmt.Sprintf("endedAt - - - - - - - - - - - - - - - - - - + diff --git a/ui/src/components/workflow/designer/forms/BizApplyNodeConfigForm.tsx b/ui/src/components/workflow/designer/forms/BizApplyNodeConfigForm.tsx index 187d63d39..d8a6ce87b 100644 --- a/ui/src/components/workflow/designer/forms/BizApplyNodeConfigForm.tsx +++ b/ui/src/components/workflow/designer/forms/BizApplyNodeConfigForm.tsx @@ -653,7 +653,7 @@ const BizApplyNodeConfigForm = ({ node, ...props }: BizApplyNodeConfigFormProps) }) skipBeforeExpiryDays: z.coerce .number() .int(t("workflow_node.apply.form.skip_before_expiry_days.placeholder")) - .nonnegative(t("workflow_node.apply.form.skip_before_expiry_days.placeholder")), + .positive(t("workflow_node.apply.form.skip_before_expiry_days.placeholder")), }) .superRefine((values, ctx) => { if (values.domains) { diff --git a/ui/src/domain/settings.ts b/ui/src/domain/settings.ts index 376637127..d83a90881 100644 --- a/ui/src/domain/settings.ts +++ b/ui/src/domain/settings.ts @@ -30,7 +30,8 @@ export type SSLProviderSettingsContent = { // #region Settings: Persistence export type PersistenceSettingsContent = { - workflowRunsMaxDaysRetention?: number; - expiredCertificatesMaxDaysRetention?: number; + certificatesWarningDaysBeforeExpire?: number; + certificatesRetentionMaxDays?: number; + workflowRunsRetentionMaxDays?: number; }; // #endregion diff --git a/ui/src/i18n/locales/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json index 3bb558a8e..352dbbf60 100644 --- a/ui/src/i18n/locales/en/nls.provider.json +++ b/ui/src/i18n/locales/en/nls.provider.json @@ -8,7 +8,7 @@ "provider.actalisssl": "Actalis SSL", "provider.akamai": "Akamai", "provider.akamai.cdn": "Akamai - CDN (Content Delivery Network)", - "provider.akamai.edgedns": "Akamai - Edge DNS", + "provider.akamai.edgedns": "Akamai - EdgeDNS", "provider.aliyun": "Alibaba Cloud", "provider.aliyun.alb": "Alibaba Cloud - ALB (Application Load Balancer)", "provider.aliyun.apigw": "Alibaba Cloud - API Gateway", diff --git a/ui/src/i18n/locales/en/nls.settings.json b/ui/src/i18n/locales/en/nls.settings.json index 6cff94790..2a57e9f7e 100644 --- a/ui/src/i18n/locales/en/nls.settings.json +++ b/ui/src/i18n/locales/en/nls.settings.json @@ -43,15 +43,20 @@ "settings.sslprovider.form.letsencryptstaging_alert": "The staging environment can reduce the chance of your running up against rate limits.

Learn more:
https://letsencrypt.org/docs/staging-environment/", "settings.persistence.tab": "Persistence", - "settings.persistence.title": "Data retention", - "settings.persistence.form.workflow_runs_max_days.label": "Max days retention of workflow history runs", - "settings.persistence.form.workflow_runs_max_days.placeholder": "Please enter the maximum retention days of workflow history runs", - "settings.persistence.form.workflow_runs_max_days.unit": "days", - "settings.persistence.form.workflow_runs_max_days.help": "Set to 0 to disable cleanup workflow history runs. It is recommended to set it to 180 days or more.", - "settings.persistence.form.expired_certificates_max_days.label": "Max days retention of expired certificates", - "settings.persistence.form.expired_certificates_max_days.placeholder": "Please enter the maximum retention days of expired certificates", - "settings.persistence.form.expired_certificates_max_days.unit": "days", - "settings.persistence.form.expired_certificates_max_days.help": "Set to 0 to disable cleanup expired certificates.", + "settings.persistence.alerting.title": "Alerting", + "settings.persistence.alerting.form.certificates_warning_days_before_expire.label": "Certificate expiration warning threshold", + "settings.persistence.alerting.form.certificates_warning_days_before_expire.placeholder": "Please enter the certificate expiration warning threshold", + "settings.persistence.alerting.form.certificates_warning_days_before_expire.unit": "days", + "settings.persistence.alerting.form.certificates_warning_days_before_expire.help": "Notes: It determines when the certificate will be marked as \"Expiring-Soon\".", + "settings.persistence.data_retention.title": "Data retention", + "settings.persistence.data_retention.form.workflow_runs_retention_max_days.label": "Workflow runs retention max days", + "settings.persistence.data_retention.form.workflow_runs_retention_max_days.placeholder": "Please enter the maximum number of retention days for workflow history runs", + "settings.persistence.data_retention.form.workflow_runs_retention_max_days.unit": "days", + "settings.persistence.data_retention.form.workflow_runs_retention_max_days.help": "Notes: Set to 0 to disable cleanup workflow history runs. Recommend setting to 180 days or more.", + "settings.persistence.data_retention.form.certificates_retention_max_days.label": "Expired certificates retention max days", + "settings.persistence.data_retention.form.certificates_retention_max_days.placeholder": "Please enter the maximum number of retention days for expired certificates", + "settings.persistence.data_retention.form.certificates_retention_max_days.unit": "days", + "settings.persistence.data_retention.form.certificates_retention_max_days.help": "Notes: Set to 0 to disable cleanup expired certificates.", "settings.diagnostics.tab": "Diagnostics", "settings.diagnostics.logs.title": "System logs", diff --git a/ui/src/i18n/locales/en/nls.workflow.runs.json b/ui/src/i18n/locales/en/nls.workflow.runs.json index 8633053bf..18c4dd41d 100644 --- a/ui/src/i18n/locales/en/nls.workflow.runs.json +++ b/ui/src/i18n/locales/en/nls.workflow.runs.json @@ -9,7 +9,7 @@ "workflow_run.action.batch_delete.modal.title": "Delete workflow runs", "workflow_run.action.batch_delete.modal.content": "Are you sure want to delete these {{count}} selected workflow runs?
This action cannot be undone.", - "workflow_run.deletion.alert": "The workflow run contains the execution results of each node. Deleting it may trigger re-application or re-deployment of certificates due to the inability to find the previous execution result. Please do not delete unless necessary. It is recommended to keep it for at least 180 days. ", + "workflow_run.deletion.alert": "The workflow run contains the execution results of each node. Deleting it may trigger re-application or re-deployment of certificates due to the inability to find the previous execution result. Please do not delete unless necessary. Recommend keeping it for at least 180 days. ", "workflow_run.cancellation.alert": "If the process is unexpectedly terminated or the server times out, you can manually cancel long-hanging runs to prevent blocking subsequent executions.", "workflow_run.nodata.description": "It looks like you don't have any runs. Get started by running this workflow.", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index c982700fb..92e8561c8 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -8,7 +8,7 @@ "provider.actalisssl": "Actalis SSL", "provider.akamai": "Akamai", "provider.akamai.cdn": "Akamai - 内容分发网络 CDN", - "provider.akamai.edgedns": "Akamai - Edge DNS", + "provider.akamai.edgedns": "Akamai - EdgeDNS", "provider.aliyun": "阿里云", "provider.aliyun.alb": "阿里云 - 应用型负载均衡 ALB", "provider.aliyun.apigw": "阿里云 - API 网关", diff --git a/ui/src/i18n/locales/zh/nls.settings.json b/ui/src/i18n/locales/zh/nls.settings.json index 367fbe254..e7586bfbe 100644 --- a/ui/src/i18n/locales/zh/nls.settings.json +++ b/ui/src/i18n/locales/zh/nls.settings.json @@ -43,15 +43,20 @@ "settings.sslprovider.form.letsencryptstaging_alert": "测试环境比生产环境有更宽松的速率限制,可进行测试性部署。

点击下方链接了解更多:
https://letsencrypt.org/zh-cn/docs/staging-environment/", "settings.persistence.tab": "数据持久化", - "settings.persistence.title": "定期清理数据", - "settings.persistence.form.workflow_runs_max_days.label": "工作流运行历史保留天数", - "settings.persistence.form.workflow_runs_max_days.placeholder": "请输入运行历史保留天数", - "settings.persistence.form.workflow_runs_max_days.unit": "天", - "settings.persistence.form.workflow_runs_max_days.help": "设置为 0 表示永久保留,不会自动清理。建议设置为 180 天以上。", - "settings.persistence.form.expired_certificates_max_days.label": "证书过期后保留天数", - "settings.persistence.form.expired_certificates_max_days.placeholder": "请输入过期证书保留天数", - "settings.persistence.form.expired_certificates_max_days.unit": "天", - "settings.persistence.form.expired_certificates_max_days.help": "设置为 0 表示永久保留,不会自动清理。", + "settings.persistence.alerting.title": "警报策略", + "settings.persistence.alerting.form.certificates_warning_days_before_expire.label": "证书即将过期预警阈值", + "settings.persistence.alerting.form.certificates_warning_days_before_expire.placeholder": "请输入证书即将过期预警阈值", + "settings.persistence.alerting.form.certificates_warning_days_before_expire.unit": "天", + "settings.persistence.alerting.form.certificates_warning_days_before_expire.help": "提示:该选项将决定将证书过期前多久视为「即将过期」。", + "settings.persistence.data_retention.title": "数据保留策略", + "settings.persistence.data_retention.form.workflow_runs_retention_max_days.label": "工作流运行历史保留期限", + "settings.persistence.data_retention.form.workflow_runs_retention_max_days.placeholder": "请输入运行历史保留期限", + "settings.persistence.data_retention.form.workflow_runs_retention_max_days.unit": "天", + "settings.persistence.data_retention.form.workflow_runs_retention_max_days.help": "提示:设置为 0 表示永久保留,不会自动清理。建议设置为 180 天以上。", + "settings.persistence.data_retention.form.certificates_retention_max_days.label": "证书过期后保留期限", + "settings.persistence.data_retention.form.certificates_retention_max_days.placeholder": "请输入过期证书保留期限", + "settings.persistence.data_retention.form.certificates_retention_max_days.unit": "天", + "settings.persistence.data_retention.form.certificates_retention_max_days.help": "提示:设置为 0 表示永久保留,不会自动清理。", "settings.diagnostics.tab": "系统诊断", "settings.diagnostics.logs.title": "系统日志", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 2b30afa12..ca3de6a47 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -635,7 +635,7 @@ "workflow_node.deploy.form.lecdn_client_id.label": "LeCDN 客户 ID(可选)", "workflow_node.deploy.form.lecdn_client_id.placeholder": "请输入 LeCDN 客户 ID", "workflow_node.deploy.form.lecdn_client_id.tooltip": "请登录 LeCDN 控制台查看。
使用的是系统管理员的授权信息时必填,需与证书所属客户相同。", - "workflow_node.deploy.form.local.guide": "如果你正在使用 Docker 运行 Certimate,“本地”指的是容器内而非宿主机。", + "workflow_node.deploy.form.local.guide": "如果你正在使用 Docker 运行 Certimate,「本地」指的是容器内而非宿主机。", "workflow_node.deploy.form.local_format.label": "文件格式", "workflow_node.deploy.form.local_format.placeholder": "请选择文件格式", "workflow_node.deploy.form.local_format.option.pem.label": "PEM 格式(*.pem, *.crt, *.key)", diff --git a/ui/src/pages/certificates/CertificateList.tsx b/ui/src/pages/certificates/CertificateList.tsx index 8642eb691..10e8d509f 100644 --- a/ui/src/pages/certificates/CertificateList.tsx +++ b/ui/src/pages/certificates/CertificateList.tsx @@ -12,8 +12,10 @@ import CertificateDetailDrawer from "@/components/certificate/CertificateDetailD import Empty from "@/components/Empty"; import Show from "@/components/Show"; import { CERTIFICATE_SOURCES, type CertificateModel } from "@/domain/certificate"; +import { SETTINGS_NAMES } from "@/domain/settings"; import { useAppSettings } from "@/hooks"; import { get as getCertificate, list as listCertificates, remove as removeCertificate } from "@/repository/certificate"; +import { get as getSettings } from "@/repository/settings"; import { getErrMsg } from "@/utils/error"; const CertificateList = () => { @@ -28,6 +30,8 @@ const CertificateList = () => { const { appSettings: globalAppSettings } = useAppSettings(); + const [expiryThreshold, setExpiryThreshold] = useState(0); + const [filters, setFilters] = useState>(() => { return { keyword: searchParams.get("keyword"), @@ -64,7 +68,7 @@ const CertificateList = () => { return (
{!isRevoked && !isExpired ? ( - leftDays >= 20 ? ( + leftDays >= expiryThreshold ? (   {t("certificate.props.validity.left_days", { left: leftDays, total })} @@ -240,6 +244,7 @@ const CertificateList = () => { return listCertificates({ keyword: filters["keyword"] as string, state: filters["state"] as Parameters[0]["state"], + stateThreshold: expiryThreshold, sort: sort, page: page, perPage: pageSize, @@ -247,7 +252,7 @@ const CertificateList = () => { }, { refreshDeps: [filters, sorter, page, pageSize], - onBefore: () => { + onBefore: async () => { setSearchParams((prev) => { if (filters["keyword"]) { prev.set("keyword", filters["keyword"] as string); @@ -266,6 +271,11 @@ const CertificateList = () => { return prev; }); + + if (expiryThreshold === 0) { + const settings = await getSettings(SETTINGS_NAMES.PERSISTENCE); + setExpiryThreshold(settings?.content?.certificatesWarningDaysBeforeExpire ?? 0); + } }, onSuccess: (res) => { setTableData(res.items); diff --git a/ui/src/pages/settings/SettingsPersistence.tsx b/ui/src/pages/settings/SettingsPersistence.tsx index ab3adbcfd..99f80dce9 100644 --- a/ui/src/pages/settings/SettingsPersistence.tsx +++ b/ui/src/pages/settings/SettingsPersistence.tsx @@ -1,6 +1,6 @@ -import { useEffect, useState } from "react"; +import { createContext, useContext, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { App, Button, Form, InputNumber, Skeleton } from "antd"; +import { App, Button, Divider, Form, InputNumber, Skeleton } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { produce } from "immer"; import { z } from "zod"; @@ -18,11 +18,12 @@ const SettingsPersistence = () => { const [settings, setSettings] = useState>(); const [loading, setLoading] = useState(true); + useEffect(() => { const fetchData = async () => { setLoading(true); - const settings = await getSettings(SETTINGS_NAMES.PERSISTENCE); + const settings = await getSettings(SETTINGS_NAMES.PERSISTENCE); setSettings(settings); setLoading(false); @@ -31,13 +32,43 @@ const SettingsPersistence = () => { fetchData(); }, []); + const updateContextSettings = async (settings: MaybeModelRecordWithId>) => { + try { + const resp = await saveSettings(settings); + setSettings(resp); + + message.success(t("common.text.operation_succeeded")); + } catch (err) { + notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); + } + }; + + return ( + +

{t("settings.persistence.alerting.title")}

+ + + + +

{t("settings.persistence.data_retention.title")}

+ +
+ ); +}; + +const SettingsPersistenceAlerting = ({ className, style }: { className?: string; style?: React.CSSProperties }) => { + const { t } = useTranslation(); + + const { loading, settings, updateSettings } = useContext(InternalSettingsContext); + const formSchema = z.object({ - workflowRunsMaxDaysRetention: z - .number(t("settings.persistence.form.workflow_runs_max_days.placeholder")) - .gte(0, t("settings.persistence.form.workflow_runs_max_days.placeholder")), - expiredCertificatesMaxDaysRetention: z - .number(t("settings.persistence.form.expired_certificates_max_days.placeholder")) - .gte(0, t("settings.persistence.form.expired_certificates_max_days.placeholder")), + certificatesWarningDaysBeforeExpire: z.number().int().positive(), }); const formRule = createSchemaFieldRule(formSchema); const { @@ -46,70 +77,41 @@ const SettingsPersistence = () => { formProps, } = useAntdForm>({ initialValues: { - workflowRunsMaxDaysRetention: settings?.content?.workflowRunsMaxDaysRetention ?? 0, - expiredCertificatesMaxDaysRetention: settings?.content?.expiredCertificatesMaxDaysRetention ?? 0, + certificatesWarningDaysBeforeExpire: settings?.content?.certificatesWarningDaysBeforeExpire, }, onSubmit: async (values) => { - try { - await saveSettings( - produce(settings!, (draft) => { - draft.content ??= {} as PersistenceSettingsContent; - draft.content.workflowRunsMaxDaysRetention = values.workflowRunsMaxDaysRetention; - draft.content.expiredCertificatesMaxDaysRetention = values.expiredCertificatesMaxDaysRetention; - }) - ); - - message.success(t("common.text.operation_succeeded")); - } catch (err) { - notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); - - throw err; - } + updateSettings( + produce(settings!, (draft) => { + draft.content ??= {} as PersistenceSettingsContent; + draft.content.certificatesWarningDaysBeforeExpire = values.certificatesWarningDaysBeforeExpire; + }) + ); }, }); const [formChanged, setFormChanged] = useState(false); const handleInputChange = () => { - const changed = - formInst.getFieldValue("workflowRunsMaxDaysRetention") !== formProps.initialValues?.workflowRunsMaxDaysRetention || - formInst.getFieldValue("expiredCertificatesMaxDaysRetention") !== formProps.initialValues?.workflowRunsMaxDaysRetention; + const changed = formInst.getFieldValue("certificatesWarningDaysBeforeExpire") !== formProps.initialValues?.certificatesWarningDaysBeforeExpire; setFormChanged(changed); }; return ( <> -

{t("settings.persistence.title")}

- }> -
+
+ }>
} + name="certificatesWarningDaysBeforeExpire" + label={t("settings.persistence.alerting.form.certificates_warning_days_before_expire.label")} + extra={} rules={[formRule]} > - - - } - rules={[formRule]} - > - @@ -120,10 +122,105 @@ const SettingsPersistence = () => {
-
- + +
); }; +const SettingsPersistenceDataRetention = ({ className, style }: { className?: string; style?: React.CSSProperties }) => { + const { t } = useTranslation(); + + const { loading, settings, updateSettings } = useContext(InternalSettingsContext); + + const formSchema = z.object({ + certificatesRetentionMaxDays: z.number().int().nonnegative(), + workflowRunsRetentionMaxDays: z.number().int().nonnegative(), + }); + const formRule = createSchemaFieldRule(formSchema); + const { + form: formInst, + formPending, + formProps, + } = useAntdForm>({ + initialValues: { + certificatesRetentionMaxDays: settings?.content?.certificatesRetentionMaxDays, + workflowRunsRetentionMaxDays: settings?.content?.workflowRunsRetentionMaxDays, + }, + onSubmit: async (values) => { + updateSettings( + produce(settings!, (draft) => { + draft.content ??= {} as PersistenceSettingsContent; + draft.content.certificatesRetentionMaxDays = values.certificatesRetentionMaxDays; + draft.content.workflowRunsRetentionMaxDays = values.workflowRunsRetentionMaxDays; + }) + ); + }, + }); + const [formChanged, setFormChanged] = useState(false); + + const handleInputChange = () => { + const changed = + formInst.getFieldValue("certificatesRetentionMaxDays") !== formProps.initialValues?.certificatesRetentionMaxDays || + formInst.getFieldValue("workflowRunsRetentionMaxDays") !== formProps.initialValues?.workflowRunsRetentionMaxDays; + setFormChanged(changed); + }; + + return ( + <> +
+ }> +
+ } + rules={[formRule]} + > + + + + } + rules={[formRule]} + > + + + + + + +
+
+
+ + ); +}; + +const InternalSettingsContext = createContext( + {} as { + loading: boolean; + settings: SettingsModel; + updateSettings: (settings: MaybeModelRecordWithId>) => Promise; + } +); + export default SettingsPersistence; diff --git a/ui/src/pages/settings/SettingsSSLProvider.tsx b/ui/src/pages/settings/SettingsSSLProvider.tsx index ebf1001cf..097aa18e0 100644 --- a/ui/src/pages/settings/SettingsSSLProvider.tsx +++ b/ui/src/pages/settings/SettingsSSLProvider.tsx @@ -19,16 +19,17 @@ const SettingsSSLProvider = () => { const { message, notification } = App.useApp(); + const [settings, setSettings] = useState>(); + const [loading, setLoading] = useState(true); + const [formInst] = Form.useForm<{ provider?: string }>(); const [formPending, setFormPending] = useState(false); - const [settings, setSettings] = useState>(); - const [loading, setLoading] = useState(true); useEffect(() => { const fetchData = async () => { setLoading(true); - const settings = await getSettings(SETTINGS_NAMES.SSL_PROVIDER); + const settings = await getSettings(SETTINGS_NAMES.SSL_PROVIDER); setSettings(settings); setProviderValue(settings.content?.provider || CA_PROVIDERS.LETSENCRYPT); @@ -99,6 +100,7 @@ const SettingsSSLProvider = () => { return ( { const InternalSettingsContext = createContext( {} as { + loading: boolean; pending: boolean; settings: SettingsModel; updateSettings: (settings: MaybeModelRecordWithId>) => Promise; diff --git a/ui/src/repository/certificate.ts b/ui/src/repository/certificate.ts index 05898816b..9c0b4708a 100644 --- a/ui/src/repository/certificate.ts +++ b/ui/src/repository/certificate.ts @@ -25,12 +25,14 @@ const _expandFields = ["expand.workflowRef.id", "expand.workflowRef.name", "expa export const list = async ({ keyword, state, + stateThreshold, sort = "-created", page = 1, perPage = 10, }: { keyword?: string; state?: "expiringSoon" | "expired"; + stateThreshold?: number; sort?: string; page?: number; perPage?: number; @@ -42,9 +44,11 @@ export const list = async ({ filters.push(pb.filter("(id={:keyword} || serialNumber={:keyword} || subjectAltNames~{:keyword})", { keyword: keyword })); } if (state === "expiringSoon") { - filters.push(pb.filter("validityNotAfter<{:expiredAt} && validityNotAfter>@now", { expiredAt: dayjs().add(20, "d").toDate() })); + filters.push(pb.filter("validityNotAfter<={:expiredAt}", { expiredAt: dayjs().add(stateThreshold!, "d").toDate() })); + filters.push(pb.filter("validityNotAfter>@now")); + filters.push(pb.filter("isRevoked=0")); } else if (state === "expired") { - filters.push(pb.filter("validityNotAfter<={:expiredAt}", { expiredAt: new Date() })); + filters.push(pb.filter("validityNotAfter<=@now")); } return pb.collection(COLLECTION_NAME_CERTIFICATE).getList(page, perPage, { diff --git a/ui/src/repository/settings.ts b/ui/src/repository/settings.ts index df9bafe0f..d0be509d4 100644 --- a/ui/src/repository/settings.ts +++ b/ui/src/repository/settings.ts @@ -1,24 +1,70 @@ import { ClientResponseError } from "pocketbase"; -import { type SettingsModel, type SettingsNames } from "@/domain/settings"; +import { CA_PROVIDERS } from "@/domain/provider"; +import { + type EmailsSettingsContent, + type PersistenceSettingsContent, + SETTINGS_NAMES, + type SSLProviderSettingsContent, + type SettingsModel, + type SettingsNames, +} from "@/domain/settings"; + import { COLLECTION_NAME_SETTINGS, getPocketBase } from "./_pocketbase"; -export const get = async >(name: SettingsNames) => { +interface SettingsContentMap { + [SETTINGS_NAMES.EMAILS]: EmailsSettingsContent; + [SETTINGS_NAMES.SSL_PROVIDER]: SSLProviderSettingsContent; + [SETTINGS_NAMES.PERSISTENCE]: PersistenceSettingsContent; +} + +export const get = async >( + name: K +): Promise : SettingsModel> => { + let resp: K extends keyof SettingsContentMap ? SettingsModel : SettingsModel; try { - const resp = await getPocketBase().collection(COLLECTION_NAME_SETTINGS).getFirstListItem>(`name='${name}'`, { + resp = await getPocketBase().collection(COLLECTION_NAME_SETTINGS).getFirstListItem(`name='${name}'`, { requestKey: null, }); return resp; } catch (err) { if (err instanceof ClientResponseError && err.status === 404) { - return { + resp = { name: name, - content: {} as T, - } as SettingsModel; + content: {}, + } as unknown as typeof resp; + } else { + throw err; } - - throw err; } + + // 兜底设置一些默认值(需确保与后端默认值保持一致),防止视图层空指针 + switch (name) { + case SETTINGS_NAMES.EMAILS: + { + resp.content ??= {}; + (resp.content as EmailsSettingsContent).emails ??= []; + } + break; + + case SETTINGS_NAMES.SSL_PROVIDER: + { + resp.content ??= {}; + (resp.content as SSLProviderSettingsContent).provider ??= CA_PROVIDERS.LETSENCRYPT; + } + break; + + case SETTINGS_NAMES.PERSISTENCE: + { + resp.content ??= {}; + (resp.content as PersistenceSettingsContent).certificatesWarningDaysBeforeExpire ??= 21; + (resp.content as PersistenceSettingsContent).certificatesRetentionMaxDays ??= 0; + (resp.content as PersistenceSettingsContent).workflowRunsRetentionMaxDays ??= 0; + } + break; + } + + return resp; }; export const save = async >(record: MaybeModelRecordWithId>) => { diff --git a/ui/src/stores/contact/index.ts b/ui/src/stores/contact/index.ts index edf65687e..886169eab 100644 --- a/ui/src/stores/contact/index.ts +++ b/ui/src/stores/contact/index.ts @@ -22,7 +22,7 @@ export const useContactEmailsStore = create((set, get) => { } } - fetcher ??= getSettings(SETTINGS_NAMES.EMAILS); + fetcher ??= getSettings(SETTINGS_NAMES.EMAILS); try { set({ loading: true }); @@ -35,7 +35,7 @@ export const useContactEmailsStore = create((set, get) => { }, setEmails: async (emails) => { - settings ??= await getSettings(SETTINGS_NAMES.EMAILS); + settings ??= await getSettings(SETTINGS_NAMES.EMAILS); settings = await saveSettings({ ...settings, content: {