diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index 30cf9d41..825cfedf 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -581,6 +581,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pGoEdge.NewDeployer(&pGoEdge.DeployerConfig{ ApiUrl: access.ApiUrl, + ApiRole: access.ApiRole, AccessKeyId: access.AccessKeyId, AccessKey: access.AccessKey, AllowInsecureConnections: access.AllowInsecureConnections, @@ -693,16 +694,18 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer case domain.DeploymentProviderTypeLocal: { deployer, err := pLocal.NewDeployer(&pLocal.DeployerConfig{ - ShellEnv: pLocal.ShellEnvType(maputil.GetString(options.ProviderExtendedConfig, "shellEnv")), - PreCommand: maputil.GetString(options.ProviderExtendedConfig, "preCommand"), - PostCommand: maputil.GetString(options.ProviderExtendedConfig, "postCommand"), - OutputFormat: pLocal.OutputFormatType(maputil.GetOrDefaultString(options.ProviderExtendedConfig, "format", string(pLocal.OUTPUT_FORMAT_PEM))), - OutputCertPath: maputil.GetString(options.ProviderExtendedConfig, "certPath"), - OutputKeyPath: maputil.GetString(options.ProviderExtendedConfig, "keyPath"), - PfxPassword: maputil.GetString(options.ProviderExtendedConfig, "pfxPassword"), - JksAlias: maputil.GetString(options.ProviderExtendedConfig, "jksAlias"), - JksKeypass: maputil.GetString(options.ProviderExtendedConfig, "jksKeypass"), - JksStorepass: maputil.GetString(options.ProviderExtendedConfig, "jksStorepass"), + ShellEnv: pLocal.ShellEnvType(maputil.GetString(options.ProviderExtendedConfig, "shellEnv")), + PreCommand: maputil.GetString(options.ProviderExtendedConfig, "preCommand"), + PostCommand: maputil.GetString(options.ProviderExtendedConfig, "postCommand"), + OutputFormat: pLocal.OutputFormatType(maputil.GetOrDefaultString(options.ProviderExtendedConfig, "format", string(pLocal.OUTPUT_FORMAT_PEM))), + OutputCertPath: maputil.GetString(options.ProviderExtendedConfig, "certPath"), + OutputServerCertPath: maputil.GetString(options.ProviderExtendedConfig, "certPathForServerOnly"), + OutputIntermediaCertPath: maputil.GetString(options.ProviderExtendedConfig, "certPathForIntermediaOnly"), + OutputKeyPath: maputil.GetString(options.ProviderExtendedConfig, "keyPath"), + PfxPassword: maputil.GetString(options.ProviderExtendedConfig, "pfxPassword"), + JksAlias: maputil.GetString(options.ProviderExtendedConfig, "jksAlias"), + JksKeypass: maputil.GetString(options.ProviderExtendedConfig, "jksKeypass"), + JksStorepass: maputil.GetString(options.ProviderExtendedConfig, "jksStorepass"), }) return deployer, err } @@ -819,22 +822,24 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer } deployer, err := pSSH.NewDeployer(&pSSH.DeployerConfig{ - SshHost: access.Host, - SshPort: access.Port, - SshUsername: access.Username, - SshPassword: access.Password, - SshKey: access.Key, - SshKeyPassphrase: access.KeyPassphrase, - UseSCP: maputil.GetBool(options.ProviderExtendedConfig, "useSCP"), - PreCommand: maputil.GetString(options.ProviderExtendedConfig, "preCommand"), - PostCommand: maputil.GetString(options.ProviderExtendedConfig, "postCommand"), - OutputFormat: pSSH.OutputFormatType(maputil.GetOrDefaultString(options.ProviderExtendedConfig, "format", string(pSSH.OUTPUT_FORMAT_PEM))), - OutputCertPath: maputil.GetString(options.ProviderExtendedConfig, "certPath"), - OutputKeyPath: maputil.GetString(options.ProviderExtendedConfig, "keyPath"), - PfxPassword: maputil.GetString(options.ProviderExtendedConfig, "pfxPassword"), - JksAlias: maputil.GetString(options.ProviderExtendedConfig, "jksAlias"), - JksKeypass: maputil.GetString(options.ProviderExtendedConfig, "jksKeypass"), - JksStorepass: maputil.GetString(options.ProviderExtendedConfig, "jksStorepass"), + SshHost: access.Host, + SshPort: access.Port, + SshUsername: access.Username, + SshPassword: access.Password, + SshKey: access.Key, + SshKeyPassphrase: access.KeyPassphrase, + UseSCP: maputil.GetBool(options.ProviderExtendedConfig, "useSCP"), + PreCommand: maputil.GetString(options.ProviderExtendedConfig, "preCommand"), + PostCommand: maputil.GetString(options.ProviderExtendedConfig, "postCommand"), + OutputFormat: pSSH.OutputFormatType(maputil.GetOrDefaultString(options.ProviderExtendedConfig, "format", string(pSSH.OUTPUT_FORMAT_PEM))), + OutputCertPath: maputil.GetString(options.ProviderExtendedConfig, "certPath"), + OutputServerCertPath: maputil.GetString(options.ProviderExtendedConfig, "certPathForServerOnly"), + OutputIntermediaCertPath: maputil.GetString(options.ProviderExtendedConfig, "certPathForIntermediaOnly"), + OutputKeyPath: maputil.GetString(options.ProviderExtendedConfig, "keyPath"), + PfxPassword: maputil.GetString(options.ProviderExtendedConfig, "pfxPassword"), + JksAlias: maputil.GetString(options.ProviderExtendedConfig, "jksAlias"), + JksKeypass: maputil.GetString(options.ProviderExtendedConfig, "jksKeypass"), + JksStorepass: maputil.GetString(options.ProviderExtendedConfig, "jksStorepass"), }) return deployer, err } diff --git a/internal/domain/access.go b/internal/domain/access.go index 84afd292..b2ff5e94 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -149,6 +149,7 @@ type AccessConfigForGoDaddy struct { type AccessConfigForGoEdge struct { ApiUrl string `json:"apiUrl"` + ApiRole string `json:"apiRole"` AccessKeyId string `json:"accessKeyId"` AccessKey string `json:"accessKey"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` diff --git a/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go b/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go index 41a78968..d443514e 100644 --- a/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go +++ b/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "log/slog" - "strings" aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" alislb "github.com/alibabacloud-go/slb-20140515/v4/client" @@ -310,22 +309,10 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*alislb.Clien } func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Uploader, error) { - casRegion := region - if casRegion != "" { - // 阿里云 CAS 服务接入点是独立于 CLB 服务的 - // 国内版固定接入点:华东一杭州 - // 国际版固定接入点:亚太东南一新加坡 - if !strings.HasPrefix(casRegion, "cn-") { - casRegion = "ap-southeast-1" - } else { - casRegion = "cn-hangzhou" - } - } - uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ AccessKeyId: accessKeyId, AccessKeySecret: accessKeySecret, - Region: casRegion, + Region: region, }) return uploader, err } diff --git a/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go b/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go index 3dd202a3..3425f05e 100644 --- a/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go +++ b/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go @@ -57,7 +57,7 @@ func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) { // 提取 Edgio 所需的服务端证书和中间证书内容 - privateCertPEM, intermediateCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM) + serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM) if err != nil { return nil, err } @@ -66,8 +66,8 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE // REF: https://docs.edg.io/rest_api/#tag/tls-certs/operation/postConfigV01TlsCerts uploadTlsCertReq := edgiodtos.UploadTlsCertRequest{ EnvironmentID: d.config.EnvironmentId, - PrimaryCert: privateCertPEM, - IntermediateCert: intermediateCertPEM, + PrimaryCert: serverCertPEM, + IntermediateCert: intermediaCertPEM, PrivateKey: privkeyPEM, } uploadTlsCertResp, err := d.sdkClient.UploadTlsCert(uploadTlsCertReq) diff --git a/internal/pkg/core/deployer/providers/gcore-cdn/gcore_cdn.go b/internal/pkg/core/deployer/providers/gcore-cdn/gcore_cdn.go index af2db885..d9671e12 100644 --- a/internal/pkg/core/deployer/providers/gcore-cdn/gcore_cdn.go +++ b/internal/pkg/core/deployer/providers/gcore-cdn/gcore_cdn.go @@ -112,7 +112,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE ValidateRootCA: false, } changeCertificateResp, err := d.sdkClients.SSLCerts.Update(context.TODO(), getCertificateDetailResp.ID, changeCertificateReq) - d.logger.Debug("sdk request 'sslcerts.Create'", slog.Any("request", changeCertificateReq), slog.Any("response", changeCertificateResp)) + d.logger.Debug("sdk request 'sslcerts.Update'", slog.Any("sslId", getCertificateDetailResp.ID), slog.Any("request", changeCertificateReq), slog.Any("response", changeCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'sslcerts.Update': %w", err) } diff --git a/internal/pkg/core/deployer/providers/goedge/goedge.go b/internal/pkg/core/deployer/providers/goedge/goedge.go index 61153b1b..73eade64 100644 --- a/internal/pkg/core/deployer/providers/goedge/goedge.go +++ b/internal/pkg/core/deployer/providers/goedge/goedge.go @@ -18,9 +18,11 @@ import ( type DeployerConfig struct { // GoEdge URL。 ApiUrl string `json:"apiUrl"` - // GoEdge 用户 AccessKeyId。 + // GoEdge 用户角色。 + ApiRole string `json:"apiRole"` + // GoEdge AccessKeyId。 AccessKeyId string `json:"accessKeyId"` - // GoEdge 用户 AccessKey。 + // GoEdge AccessKey。 AccessKey string `json:"accessKey"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` @@ -44,7 +46,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { panic("config is nil") } - client, err := createSdkClient(config.ApiUrl, config.AccessKeyId, config.AccessKey, config.AllowInsecureConnections) + client, err := createSdkClient(config.ApiUrl, config.ApiRole, config.AccessKeyId, config.AccessKey, config.AllowInsecureConnections) if err != nil { return nil, fmt.Errorf("failed to create sdk client: %w", err) } @@ -116,11 +118,15 @@ func (d *DeployerProvider) deployToCertificate(ctx context.Context, certPEM stri return nil } -func createSdkClient(apiUrl, accessKeyId, accessKey string, skipTlsVerify bool) (*goedgesdk.Client, error) { +func createSdkClient(apiUrl, apiRole, accessKeyId, accessKey string, skipTlsVerify bool) (*goedgesdk.Client, error) { if _, err := url.Parse(apiUrl); err != nil { return nil, errors.New("invalid goedge api url") } + if apiRole != "user" && apiRole != "admin" { + return nil, errors.New("invalid goedge api role") + } + if accessKeyId == "" { return nil, errors.New("invalid goedge access key id") } @@ -129,7 +135,7 @@ func createSdkClient(apiUrl, accessKeyId, accessKey string, skipTlsVerify bool) return nil, errors.New("invalid goedge access key") } - client := goedgesdk.NewClient(apiUrl, "user", accessKeyId, accessKey) + client := goedgesdk.NewClient(apiUrl, apiRole, accessKeyId, accessKey) if skipTlsVerify { client.WithTLSConfig(&tls.Config{InsecureSkipVerify: true}) } diff --git a/internal/pkg/core/deployer/providers/local/local.go b/internal/pkg/core/deployer/providers/local/local.go index 77f96543..a71ad9d3 100644 --- a/internal/pkg/core/deployer/providers/local/local.go +++ b/internal/pkg/core/deployer/providers/local/local.go @@ -25,6 +25,12 @@ type DeployerConfig struct { OutputFormat OutputFormatType `json:"outputFormat,omitempty"` // 输出证书文件路径。 OutputCertPath string `json:"outputCertPath,omitempty"` + // 输出服务器证书文件路径。 + // 选填。 + OutputServerCertPath string `json:"outputServerCertPath,omitempty"` + // 输出中间证书文件路径。 + // 选填。 + OutputIntermediaCertPath string `json:"outputIntermediaCertPath,omitempty"` // 输出私钥文件路径。 OutputKeyPath string `json:"outputKeyPath,omitempty"` // PFX 导出密码。 @@ -69,6 +75,12 @@ func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { } func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) { + // 提取服务器证书和中间证书 + serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM) + if err != nil { + return nil, fmt.Errorf("failed to extract certs: %w", err) + } + // 执行前置命令 if d.config.PreCommand != "" { stdout, stderr, err := execCommand(d.config.ShellEnv, d.config.PreCommand) @@ -86,6 +98,20 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE } d.logger.Info("ssl certificate file saved", slog.String("path", d.config.OutputCertPath)) + if d.config.OutputServerCertPath != "" { + if err := fileutil.WriteString(d.config.OutputServerCertPath, serverCertPEM); err != nil { + return nil, fmt.Errorf("failed to save server certificate file: %w", err) + } + d.logger.Info("ssl server certificate file saved", slog.String("path", d.config.OutputServerCertPath)) + } + + if d.config.OutputIntermediaCertPath != "" { + if err := fileutil.WriteString(d.config.OutputIntermediaCertPath, intermediaCertPEM); err != nil { + return nil, fmt.Errorf("failed to save intermedia certificate file: %w", err) + } + d.logger.Info("ssl intermedia certificate file saved", slog.String("path", d.config.OutputIntermediaCertPath)) + } + if err := fileutil.WriteString(d.config.OutputKeyPath, privkeyPEM); err != nil { return nil, fmt.Errorf("failed to save private key file: %w", err) } diff --git a/internal/pkg/core/deployer/providers/ssh/ssh.go b/internal/pkg/core/deployer/providers/ssh/ssh.go index 4b8b433d..cf09214b 100644 --- a/internal/pkg/core/deployer/providers/ssh/ssh.go +++ b/internal/pkg/core/deployer/providers/ssh/ssh.go @@ -41,6 +41,12 @@ type DeployerConfig struct { OutputFormat OutputFormatType `json:"outputFormat,omitempty"` // 输出证书文件路径。 OutputCertPath string `json:"outputCertPath,omitempty"` + // 输出服务器证书文件路径。 + // 选填。 + OutputServerCertPath string `json:"outputServerCertPath,omitempty"` + // 输出中间证书文件路径。 + // 选填。 + OutputIntermediaCertPath string `json:"outputIntermediaCertPath,omitempty"` // 输出私钥文件路径。 OutputKeyPath string `json:"outputKeyPath,omitempty"` // PFX 导出密码。 @@ -85,6 +91,12 @@ func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { } func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) { + // 提取服务器证书和中间证书 + serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM) + if err != nil { + return nil, fmt.Errorf("failed to extract certs: %w", err) + } + // 连接 client, err := createSshClient( d.config.SshHost, @@ -118,6 +130,20 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE } d.logger.Info("ssl certificate file uploaded", slog.String("path", d.config.OutputCertPath)) + if d.config.OutputServerCertPath != "" { + if err := writeFileString(client, d.config.UseSCP, d.config.OutputServerCertPath, serverCertPEM); err != nil { + return nil, fmt.Errorf("failed to save server certificate file: %w", err) + } + d.logger.Info("ssl server certificate file uploaded", slog.String("path", d.config.OutputServerCertPath)) + } + + if d.config.OutputIntermediaCertPath != "" { + if err := writeFileString(client, d.config.UseSCP, d.config.OutputIntermediaCertPath, intermediaCertPEM); err != nil { + return nil, fmt.Errorf("failed to save intermedia certificate file: %w", err) + } + d.logger.Info("ssl intermedia certificate file uploaded", slog.String("path", d.config.OutputIntermediaCertPath)) + } + if err := writeFileString(client, d.config.UseSCP, d.config.OutputKeyPath, privkeyPEM); err != nil { return nil, fmt.Errorf("failed to upload private key file: %w", err) } diff --git a/internal/pkg/core/deployer/providers/webhook/webhook.go b/internal/pkg/core/deployer/providers/webhook/webhook.go index 07b2eaaa..418b2c1a 100644 --- a/internal/pkg/core/deployer/providers/webhook/webhook.go +++ b/internal/pkg/core/deployer/providers/webhook/webhook.go @@ -75,6 +75,12 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE return nil, fmt.Errorf("failed to parse x509: %w", err) } + // 提取服务器证书和中间证书 + serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM) + if err != nil { + return nil, fmt.Errorf("failed to extract certs: %w", err) + } + // 处理 Webhook URL webhookUrl, err := url.Parse(d.config.WebhookUrl) if err != nil { @@ -134,6 +140,8 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE replaceJsonValueRecursively(webhookData, "${DOMAIN}", certX509.Subject.CommonName) replaceJsonValueRecursively(webhookData, "${DOMAINS}", strings.Join(certX509.DNSNames, ";")) replaceJsonValueRecursively(webhookData, "${CERTIFICATE}", certPEM) + replaceJsonValueRecursively(webhookData, "${SERVER_CERTIFICATE}", serverCertPEM) + replaceJsonValueRecursively(webhookData, "${INTERMEDIA_CERTIFICATE}", intermediaCertPEM) replaceJsonValueRecursively(webhookData, "${PRIVATE_KEY}", privkeyPEM) if webhookMethod == http.MethodGet || webhookContentType == CONTENT_TYPE_FORM || webhookContentType == CONTENT_TYPE_MULTIPART { diff --git a/internal/pkg/sdk3rd/goedge/api.go b/internal/pkg/sdk3rd/goedge/api.go index 67ee9194..c217e8ae 100644 --- a/internal/pkg/sdk3rd/goedge/api.go +++ b/internal/pkg/sdk3rd/goedge/api.go @@ -9,7 +9,7 @@ import ( func (c *Client) getAccessToken() error { req := &getAPIAccessTokenRequest{ - Type: c.apiUserType, + Type: c.apiRole, AccessKeyId: c.accessKeyId, AccessKey: c.accessKey, } diff --git a/internal/pkg/sdk3rd/goedge/client.go b/internal/pkg/sdk3rd/goedge/client.go index 96291fb3..e32c7482 100644 --- a/internal/pkg/sdk3rd/goedge/client.go +++ b/internal/pkg/sdk3rd/goedge/client.go @@ -14,7 +14,7 @@ import ( type Client struct { apiHost string - apiUserType string + apiRole string accessKeyId string accessKey string @@ -25,12 +25,12 @@ type Client struct { client *resty.Client } -func NewClient(apiHost, apiUserType, accessKeyId, accessKey string) *Client { +func NewClient(apiHost, apiRole, accessKeyId, accessKey string) *Client { client := resty.New() return &Client{ apiHost: strings.TrimRight(apiHost, "/"), - apiUserType: apiUserType, + apiRole: apiRole, accessKeyId: accessKeyId, accessKey: accessKey, client: client, diff --git a/internal/pkg/utils/cert/extractor.go b/internal/pkg/utils/cert/extractor.go index 110f4772..94d0a8da 100644 --- a/internal/pkg/utils/cert/extractor.go +++ b/internal/pkg/utils/cert/extractor.go @@ -12,9 +12,9 @@ import ( // // 出参: // - serverCertPEM: 服务器证书的 PEM 内容。 -// - interCertPEM: 中间证书的 PEM 内容。 +// - intermediaCertPEM: 中间证书的 PEM 内容。 // - err: 错误。 -func ExtractCertificatesFromPEM(certPEM string) (serverCertPEM string, interCertPEM string, err error) { +func ExtractCertificatesFromPEM(certPEM string) (serverCertPEM string, intermediaCertPEM string, err error) { pemBlocks := make([]*pem.Block, 0) pemData := []byte(certPEM) for { @@ -28,7 +28,7 @@ func ExtractCertificatesFromPEM(certPEM string) (serverCertPEM string, interCert } serverCertPEM = "" - interCertPEM = "" + intermediaCertPEM = "" if len(pemBlocks) == 0 { return "", "", errors.New("failed to decode PEM block") @@ -40,9 +40,9 @@ func ExtractCertificatesFromPEM(certPEM string) (serverCertPEM string, interCert if len(pemBlocks) > 1 { for i := 1; i < len(pemBlocks); i++ { - interCertPEM += string(pem.EncodeToMemory(pemBlocks[i])) + intermediaCertPEM += string(pem.EncodeToMemory(pemBlocks[i])) } } - return serverCertPEM, interCertPEM, nil + return serverCertPEM, intermediaCertPEM, nil } diff --git a/migrations/1747314000_upgrade.go b/migrations/1747314000_upgrade.go new file mode 100644 index 00000000..19a25bb2 --- /dev/null +++ b/migrations/1747314000_upgrade.go @@ -0,0 +1,44 @@ +package migrations + +import ( + "github.com/pocketbase/pocketbase/core" + m "github.com/pocketbase/pocketbase/migrations" +) + +func init() { + m.Register(func(app core.App) error { + // migrate data + { + accesses, err := app.FindAllRecords("access") + if err != nil { + return err + } + + for _, access := range accesses { + changed := false + + if access.GetString("provider") == "goedge" { + config := make(map[string]any) + if err := access.UnmarshalJSONField("config", &config); err != nil { + return err + } + + config["apiRole"] = "user" + access.Set("config", config) + changed = true + } + + if changed { + err = app.Save(access) + if err != nil { + return err + } + } + } + } + + return nil + }, func(app core.App) error { + return nil + }) +} diff --git a/ui/package-lock.json b/ui/package-lock.json index 03ad4927..44a4d2b6 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -10,6 +10,13 @@ "dependencies": { "@ant-design/icons": "^6.0.0", "@ant-design/pro-components": "^2.8.7", + "@codemirror/lang-json": "^6.0.1", + "@codemirror/lang-yaml": "^6.1.2", + "@codemirror/language": "^6.11.0", + "@codemirror/legacy-modes": "^6.5.1", + "@uiw/codemirror-extensions-basic-setup": "^4.23.12", + "@uiw/codemirror-theme-vscode": "^4.23.12", + "@uiw/react-codemirror": "^4.23.12", "ahooks": "^3.8.4", "antd": "^5.25.1", "antd-zod": "^6.1.0", @@ -2107,6 +2114,121 @@ "react": ">=16.12.0" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.18.6", + "resolved": "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz", + "integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.8.1", + "resolved": "https://registry.npmmirror.com/@codemirror/commands/-/commands-6.8.1.tgz", + "integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-json": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/@codemirror/lang-json/-/lang-json-6.0.1.tgz", + "integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-yaml": { + "version": "6.1.2", + "resolved": "https://registry.npmmirror.com/@codemirror/lang-yaml/-/lang-yaml-6.1.2.tgz", + "integrity": "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.2.0", + "@lezer/lr": "^1.0.0", + "@lezer/yaml": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.11.0", + "resolved": "https://registry.npmmirror.com/@codemirror/language/-/language-6.11.0.tgz", + "integrity": "sha512-A7+f++LodNNc1wGgoRDTt78cOwWm9KVezApgjOMp1W4hM0898nsqBXwF+sbePE7ZRcjN7Sa1Z5m2oN27XkmEjQ==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/legacy-modes": { + "version": "6.5.1", + "resolved": "https://registry.npmmirror.com/@codemirror/legacy-modes/-/legacy-modes-6.5.1.tgz", + "integrity": "sha512-DJYQQ00N1/KdESpZV7jg9hafof/iBNp9h7TYo1SLMk86TWl9uDsVdho2dzd81K+v4retmK6mdC7WpuOQDytQqw==", + "dependencies": { + "@codemirror/language": "^6.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.8.5", + "resolved": "https://registry.npmmirror.com/@codemirror/lint/-/lint-6.8.5.tgz", + "integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.5.10", + "resolved": "https://registry.npmmirror.com/@codemirror/search/-/search-6.5.10.tgz", + "integrity": "sha512-RMdPdmsrUf53pb2VwflKGHEe1XVM07hI7vV2ntgw1dmqhimpatSJKva4VA9h4TLUDOD4EIF02201oZurpnEFsg==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.2", + "resolved": "https://registry.npmmirror.com/@codemirror/state/-/state-6.5.2.tgz", + "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.2", + "resolved": "https://registry.npmmirror.com/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz", + "integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.36.8", + "resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.36.8.tgz", + "integrity": "sha512-yoRo4f+FdnD01fFt4XpfpMCcCAo9QvZOtbrXExn4SqzH32YC6LgzqxfLZw/r6Ge65xyY03mK/UfUqrVw1gFiFg==", + "dependencies": { + "@codemirror/state": "^6.5.0", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@ctrl/tinycolor": { "version": "3.6.1", "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", @@ -2839,6 +2961,52 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lezer/common": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@lezer/common/-/common-1.2.3.tgz", + "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/json": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@lezer/json/-/json-1.0.3.tgz", + "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/yaml": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@lezer/yaml/-/yaml-1.0.3.tgz", + "integrity": "sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.4.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3628,6 +3796,86 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@uiw/codemirror-extensions-basic-setup": { + "version": "4.23.12", + "resolved": "https://registry.npmmirror.com/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.23.12.tgz", + "integrity": "sha512-l9vuiXOTFDBetYrRLDmz3jDxQHDsrVAZ2Y6dVfmrqi2AsulsDu+y7csW0JsvaMqo79rYkaIZg8yeqmDgMb7VyQ==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/autocomplete": ">=6.0.0", + "@codemirror/commands": ">=6.0.0", + "@codemirror/language": ">=6.0.0", + "@codemirror/lint": ">=6.0.0", + "@codemirror/search": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/codemirror-theme-vscode": { + "version": "4.23.12", + "resolved": "https://registry.npmmirror.com/@uiw/codemirror-theme-vscode/-/codemirror-theme-vscode-4.23.12.tgz", + "integrity": "sha512-ePBaUQiixrpmSoZJWCGXUStKmcM8G0VBv3UqwPR+kNGBjqDife76Gbhv77izSeEI3zRPzL+683BOdclkvWnsMg==", + "dependencies": { + "@uiw/codemirror-themes": "4.23.12" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-themes": { + "version": "4.23.12", + "resolved": "https://registry.npmmirror.com/@uiw/codemirror-themes/-/codemirror-themes-4.23.12.tgz", + "integrity": "sha512-8etEByfS9yttFZW0rcWhdZc7/JXJKRWlU5lHmJCI3GydZNGCzydNA+HtK9nWKpJUndVc58Q2sqSC5OIcwq8y6A==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/language": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/react-codemirror": { + "version": "4.23.12", + "resolved": "https://registry.npmmirror.com/@uiw/react-codemirror/-/react-codemirror-4.23.12.tgz", + "integrity": "sha512-yseqWdzoAAGAW7i/NiU8YrfSLVOEBjQvSx1KpDTFVV/nn0AlAZoDVTIPEBgdXrPlVUQoCrwgpEaj3uZCklk9QA==", + "dependencies": { + "@babel/runtime": "^7.18.6", + "@codemirror/commands": "^6.1.0", + "@codemirror/state": "^6.1.1", + "@codemirror/theme-one-dark": "^6.0.0", + "@uiw/codemirror-extensions-basic-setup": "4.23.12", + "codemirror": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.11.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/theme-one-dark": ">=6.0.0", + "@codemirror/view": ">=6.0.0", + "codemirror": ">=6.0.0", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@umijs/route-utils": { "version": "4.0.1", "resolved": "https://registry.npmmirror.com/@umijs/route-utils/-/route-utils-4.0.1.tgz", @@ -4337,6 +4585,20 @@ "node": ">=6" } }, + "node_modules/codemirror": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz", @@ -4403,6 +4665,11 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" + }, "node_modules/cron-parser": { "version": "5.2.0", "resolved": "https://registry.npmmirror.com/cron-parser/-/cron-parser-5.2.0.tgz", @@ -8689,6 +8956,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==" + }, "node_modules/stylis": { "version": "4.3.4", "resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.3.4.tgz", @@ -9363,6 +9635,11 @@ "node": ">=0.10.0" } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmmirror.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" + }, "node_modules/warning": { "version": "4.0.3", "resolved": "https://registry.npmmirror.com/warning/-/warning-4.0.3.tgz", diff --git a/ui/package.json b/ui/package.json index 194ffb31..bec7eda9 100644 --- a/ui/package.json +++ b/ui/package.json @@ -12,6 +12,13 @@ "dependencies": { "@ant-design/icons": "^6.0.0", "@ant-design/pro-components": "^2.8.7", + "@codemirror/lang-json": "^6.0.1", + "@codemirror/lang-yaml": "^6.1.2", + "@codemirror/language": "^6.11.0", + "@codemirror/legacy-modes": "^6.5.1", + "@uiw/codemirror-extensions-basic-setup": "^4.23.12", + "@uiw/codemirror-theme-vscode": "^4.23.12", + "@uiw/react-codemirror": "^4.23.12", "ahooks": "^3.8.4", "antd": "^5.25.1", "antd-zod": "^6.1.0", diff --git a/ui/src/components/CodeInput.tsx b/ui/src/components/CodeInput.tsx new file mode 100644 index 00000000..a784af46 --- /dev/null +++ b/ui/src/components/CodeInput.tsx @@ -0,0 +1,97 @@ +import { useMemo, useRef } from "react"; +import { json } from "@codemirror/lang-json"; +import { yaml } from "@codemirror/lang-yaml"; +import { StreamLanguage } from "@codemirror/language"; +import { powerShell } from "@codemirror/legacy-modes/mode/powershell"; +import { shell } from "@codemirror/legacy-modes/mode/shell"; +import { basicSetup } from "@uiw/codemirror-extensions-basic-setup"; +import { vscodeDark, vscodeLight } from "@uiw/codemirror-theme-vscode"; +import CodeMirror, { type ReactCodeMirrorProps, type ReactCodeMirrorRef } from "@uiw/react-codemirror"; +import { useFocusWithin } from "ahooks"; +import { theme } from "antd"; + +import { useBrowserTheme } from "@/hooks"; +import { mergeCls } from "@/utils/css"; + +export interface CodeInputProps extends Omit { + disabled?: boolean; + language?: string | string[]; +} + +const CodeInput = ({ className, style, disabled, language, ...props }: CodeInputProps) => { + const { token: themeToken } = theme.useToken(); + + const { theme: browserTheme } = useBrowserTheme(); + + const cmRef = useRef(null); + const isFocusWithin = useFocusWithin(cmRef.current?.editor); + + const cmTheme = useMemo(() => { + if (browserTheme === "dark") { + return vscodeDark; + } + return vscodeLight; + }, [browserTheme]); + + const cmExtensions = useMemo(() => { + const temp: NonNullable = [ + basicSetup({ + foldGutter: false, + dropCursor: false, + allowMultipleSelections: false, + indentOnInput: false, + }), + ]; + + const langs = Array.isArray(language) ? language : [language]; + langs.forEach((lang) => { + switch (lang) { + case "shell": + temp.push(StreamLanguage.define(shell)); + break; + case "json": + temp.push(json()); + break; + case "powershell": + temp.push(StreamLanguage.define(powerShell)); + break; + case "yaml": + temp.push(yaml()); + break; + } + }); + + return temp; + }, [language]); + + return ( +
+ +
+ ); +}; + +export default CodeInput; diff --git a/ui/src/components/TextFileInput.tsx b/ui/src/components/TextFileInput.tsx new file mode 100644 index 00000000..bae83c56 --- /dev/null +++ b/ui/src/components/TextFileInput.tsx @@ -0,0 +1,51 @@ +import { type ChangeEvent, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { UploadOutlined as UploadOutlinedIcon } from "@ant-design/icons"; +import { Button, type ButtonProps, Input, Space, type UploadProps } from "antd"; +import { type TextAreaProps } from "antd/es/input/TextArea"; + +import { mergeCls } from "@/utils/css"; +import { readFileContent } from "@/utils/file"; + +export interface TextFileInputProps extends Omit { + accept?: UploadProps["accept"]; + uploadButtonProps?: Omit; + uploadText?: string; + onChange?: (value: string) => void; +} + +const TextFileInput = ({ className, style, accept, disabled, readOnly, uploadText, uploadButtonProps, onChange, ...props }: TextFileInputProps) => { + const { t } = useTranslation(); + + const fileInputRef = useRef(null); + + const handleButtonClick = () => { + if (fileInputRef.current) { + fileInputRef.current.click(); + } + }; + + const handleFileChange = async (e: ChangeEvent) => { + const { files } = e.target as HTMLInputElement; + if (files?.length) { + const value = await readFileContent(files[0]); + onChange?.(value); + } + }; + + return ( + + onChange?.(e.target.value)} /> + {!readOnly && ( + <> + + + + )} + + ); +}; + +export default TextFileInput; diff --git a/ui/src/components/access/AccessFormGoEdgeConfig.tsx b/ui/src/components/access/AccessFormGoEdgeConfig.tsx index eb4140f4..ced9b09a 100644 --- a/ui/src/components/access/AccessFormGoEdgeConfig.tsx +++ b/ui/src/components/access/AccessFormGoEdgeConfig.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Switch } from "antd"; +import { Form, type FormInstance, Input, Radio, Switch } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; @@ -18,6 +18,7 @@ export type AccessFormGoEdgeConfigProps = { const initFormModel = (): AccessFormGoEdgeConfigFieldValues => { return { apiUrl: "http://:7788/", + apiRole: "user", accessKeyId: "", accessKey: "", }; @@ -28,6 +29,9 @@ const AccessFormGoEdgeConfig = ({ form: formInst, formName, disabled, initialVal const formSchema = z.object({ apiUrl: z.string().url(t("common.errmsg.url_invalid")), + role: z.union([z.literal("user"), z.literal("admin")], { + message: t("access.form.goedge_api_role.placeholder"), + }), accessKeyId: z .string() .min(1, t("access.form.goedge_access_key_id.placeholder")) @@ -59,6 +63,10 @@ const AccessFormGoEdgeConfig = ({ form: formInst, formName, disabled, initialVal + + ({ label: t(`access.form.goedge_api_role.option.${s}.label`), value: s }))} /> + + ; @@ -34,24 +32,6 @@ const AccessFormKubernetesConfig = ({ form: formInst, formName, disabled, initia }); const formRule = createSchemaFieldRule(formSchema); - const fieldKubeConfig = Form.useWatch("kubeConfig", formInst); - const [fieldKubeFileList, setFieldKubeFileList] = useState([]); - useEffect(() => { - setFieldKubeFileList(initialValues?.kubeConfig?.trim() ? [{ uid: "-1", name: "kubeconfig", status: "done" }] : []); - }, [initialValues?.kubeConfig]); - - const handleKubeFileChange: UploadProps["onChange"] = async ({ file }) => { - if (file && file.status !== "removed") { - formInst.setFieldValue("kubeConfig", await readFileContent(file.originFileObj ?? (file as unknown as File))); - setFieldKubeFileList([file]); - } else { - formInst.setFieldValue("kubeConfig", ""); - setFieldKubeFileList([]); - } - - onValuesChange?.(formInst.getFieldsValue(true)); - }; - const handleFormChange = (_: unknown, values: z.infer) => { onValuesChange?.(values); }; @@ -65,16 +45,13 @@ const AccessFormKubernetesConfig = ({ form: formInst, formName, disabled, initia name={formName} onValuesChange={handleFormChange} > - - } > - false} fileList={fieldKubeFileList} maxCount={1} onChange={handleKubeFileChange}> - - + ); diff --git a/ui/src/components/access/AccessFormSSHConfig.tsx b/ui/src/components/access/AccessFormSSHConfig.tsx index ebaeba90..db1790a4 100644 --- a/ui/src/components/access/AccessFormSSHConfig.tsx +++ b/ui/src/components/access/AccessFormSSHConfig.tsx @@ -1,12 +1,10 @@ -import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { UploadOutlined as UploadOutlinedIcon } from "@ant-design/icons"; -import { Button, Form, type FormInstance, Input, InputNumber, Upload, type UploadFile, type UploadProps } from "antd"; +import { Form, type FormInstance, Input, InputNumber } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; +import TextFileInput from "@/components/TextFileInput"; import { type AccessConfigForSSH } from "@/domain/access"; -import { readFileContent } from "@/utils/file"; import { validDomainName, validIPv4Address, validIPv6Address, validPortNumber } from "@/utils/validators"; type AccessFormSSHConfigFieldValues = Nullish; @@ -59,24 +57,6 @@ const AccessFormSSHConfig = ({ form: formInst, formName, disabled, initialValues }); const formRule = createSchemaFieldRule(formSchema); - const fieldKey = Form.useWatch("key", formInst); - const [fieldKeyFileList, setFieldKeyFileList] = useState([]); - useEffect(() => { - setFieldKeyFileList(initialValues?.key?.trim() ? [{ uid: "-1", name: "sshkey", status: "done" }] : []); - }, [initialValues?.key]); - - const handleKeyFileChange: UploadProps["onChange"] = async ({ file }) => { - if (file && file.status !== "removed") { - formInst.setFieldValue("key", await readFileContent(file.originFileObj ?? (file as unknown as File))); - setFieldKeyFileList([file]); - } else { - formInst.setFieldValue("key", ""); - setFieldKeyFileList([]); - } - - onValuesChange?.(formInst.getFieldsValue(true)); - }; - const handleFormChange = (_: unknown, values: z.infer) => { onValuesChange?.(values); }; @@ -104,48 +84,36 @@ const AccessFormSSHConfig = ({ form: formInst, formName, disabled, initialValues -
-
- - - -
+ + + -
- } - > - - -
-
+ } + > + + -
-
- - - }> - false} fileList={fieldKeyFileList} maxCount={1} onChange={handleKeyFileChange}> - - - -
+ } + > + + -
- } - > - - -
-
+ } + > + + ); }; diff --git a/ui/src/components/access/AccessFormWebhookConfig.tsx b/ui/src/components/access/AccessFormWebhookConfig.tsx index 0108d7b3..69286aa8 100644 --- a/ui/src/components/access/AccessFormWebhookConfig.tsx +++ b/ui/src/components/access/AccessFormWebhookConfig.tsx @@ -4,6 +4,7 @@ import { Alert, Button, Dropdown, Form, type FormInstance, Input, Select, Switch import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; +import CodeInput from "@/components/CodeInput"; import Show from "@/components/Show"; import { type AccessConfigForWebhook } from "@/domain/access"; @@ -105,8 +106,8 @@ const AccessFormWebhookConfig = ({ form: formInst, formName, disabled, initialVa formInst.setFieldValue("headers", value); }; - const handleWebhookDataForDeploymentBlur = (e: React.FocusEvent) => { - const value = e.target.value; + const handleWebhookDataForDeploymentBlur = () => { + const value = formInst.getFieldValue("defaultDataForDeployment"); try { const json = JSON.stringify(JSON.parse(value), null, 2); formInst.setFieldValue("defaultDataForDeployment", json); @@ -115,8 +116,8 @@ const AccessFormWebhookConfig = ({ form: formInst, formName, disabled, initialVa } }; - const handleWebhookDataForNotificationBlur = (e: React.FocusEvent) => { - const value = e.target.value; + const handleWebhookDataForNotificationBlur = () => { + const value = formInst.getFieldValue("defaultDataForNotification"); try { const json = JSON.stringify(JSON.parse(value), null, 2); formInst.setFieldValue("defaultDataForNotification", json); @@ -279,7 +280,7 @@ const AccessFormWebhookConfig = ({ form: formInst, formName, disabled, initialVa rules={[formRule]} tooltip={} > - +
@@ -297,9 +298,11 @@ const AccessFormWebhookConfig = ({ form: formInst, formName, disabled, initialVa - @@ -338,9 +341,11 @@ const AccessFormWebhookConfig = ({ form: formInst, formName, disabled, initialVa - diff --git a/ui/src/components/certificate/CertificateDetail.tsx b/ui/src/components/certificate/CertificateDetail.tsx index 6c842a36..1023bf16 100644 --- a/ui/src/components/certificate/CertificateDetail.tsx +++ b/ui/src/components/certificate/CertificateDetail.tsx @@ -75,7 +75,7 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => { - + @@ -92,7 +92,7 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => { - + diff --git a/ui/src/components/notification/NotifyTemplate.tsx b/ui/src/components/notification/NotifyTemplate.tsx index b7cf4e6d..9921cda5 100644 --- a/ui/src/components/notification/NotifyTemplate.tsx +++ b/ui/src/components/notification/NotifyTemplate.tsx @@ -108,7 +108,7 @@ const NotifyTemplateForm = ({ className, style }: NotifyTemplateFormProps) => { rules={[formRule]} > diff --git a/ui/src/components/provider/AccessProviderPicker.tsx b/ui/src/components/provider/AccessProviderPicker.tsx index 86563008..002d2519 100644 --- a/ui/src/components/provider/AccessProviderPicker.tsx +++ b/ui/src/components/provider/AccessProviderPicker.tsx @@ -4,6 +4,7 @@ import { Avatar, Card, Col, Empty, Flex, Input, type InputRef, Row, Tag, Typogra import Show from "@/components/Show"; import { ACCESS_USAGES, type AccessProvider, type AccessUsageType, accessProvidersMap } from "@/domain/provider"; +import { mergeCls } from "@/utils/css"; export type AccessProviderPickerProps = { className?: string; @@ -73,17 +74,23 @@ const AccessProviderPicker = ({ className, style, autoFocus, filter, placeholder return ( { + if (provider.builtin) { + return; + } + handleProviderTypeSelect(provider.type); }} >
- {t(provider.name)} + + {t(provider.name)} +
{t("access.props.provider.builtin")} diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormLocalConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormLocalConfig.tsx index cf34f94b..75853eb7 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormLocalConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormLocalConfig.tsx @@ -4,20 +4,23 @@ import { Alert, Button, Dropdown, Form, type FormInstance, Input, Select } from import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; +import CodeInput from "@/components/CodeInput"; import Show from "@/components/Show"; import { CERTIFICATE_FORMATS } from "@/domain/certificate"; type DeployNodeConfigFormLocalConfigFieldValues = Nullish<{ format: string; certPath: string; - keyPath?: string | null; - pfxPassword?: string | null; - jksAlias?: string | null; - jksKeypass?: string | null; - jksStorepass?: string | null; - shellEnv?: string | null; - preCommand?: string | null; - postCommand?: string | null; + certPathForServerOnly?: string; + certPathForIntermediaOnly?: string; + keyPath?: string; + pfxPassword?: string; + jksAlias?: string; + jksKeypass?: string; + jksStorepass?: string; + shellEnv?: string; + preCommand?: string; + postCommand?: string; }>; export type DeployNodeConfigFormLocalConfigProps = { @@ -49,6 +52,8 @@ export const initPresetScript = ( key: "sh_backup_files" | "ps_backup_files" | "sh_reload_nginx" | "ps_binding_iis" | "ps_binding_netsh" | "ps_binding_rdp", params?: { certPath?: string; + certPathForServerOnly?: string; + certPathForIntermediaOnly?: string; keyPath?: string; pfxPassword?: string; jksAlias?: string; @@ -74,19 +79,22 @@ if (Test-Path -Path "${params?.keyPath || ""}" -PathType Leaf) { `.trim(); case "sh_reload_nginx": - return `sudo service nginx reload`; + return `# *** 需要 root 权限 *** + +sudo service nginx reload + `.trim(); case "ps_binding_iis": - return `# 需要管理员权限 + return `# *** 需要管理员权限 *** + # 请将以下变量替换为实际值 -$pfxPath = "${params?.certPath || ""}" # PFX 文件路径 -$pfxPassword = "${params?.pfxPassword || ""}" # PFX 密码 +$pfxPath = "${params?.certPath || ""}" # PFX 文件路径(与表单中保持一致) +$pfxPassword = "${params?.pfxPassword || ""}" # PFX 密码(与表单中保持一致) $siteName = "" # IIS 网站名称 $domain = "" # 域名 $ipaddr = "" # 绑定 IP,“*”表示所有 IP 绑定 $port = "" # 绑定端口 - # 导入证书到本地计算机的个人存储区 $cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable # 获取 Thumbprint @@ -108,16 +116,16 @@ Remove-Item -Path "$pfxPath" -Force `.trim(); case "ps_binding_netsh": - return `# 需要管理员权限 + return `# *** 需要管理员权限 *** + # 请将以下变量替换为实际值 -$pfxPath = "${params?.certPath || ""}" # PFX 文件路径 -$pfxPassword = "${params?.pfxPassword || ""}" # PFX 密码 -$ipaddr = "" # 绑定 IP,“0.0.0.0”表示所有 IP 绑定,可填入域名。 +$pfxPath = "${params?.certPath || ""}" # PFX 文件路径(与表单中保持一致) +$pfxPassword = "${params?.pfxPassword || ""}" # PFX 密码(与表单中保持一致) +$ipaddr = "" # 绑定 IP,“0.0.0.0”表示所有 IP 绑定,可填入域名 $port = "" # 绑定端口 -$addr = $ipaddr + ":" + $port - # 导入证书到本地计算机的个人存储区 +$addr = $ipaddr + ":" + $port $cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable # 获取 Thumbprint $thumbprint = $cert.Thumbprint @@ -131,10 +139,11 @@ Remove-Item -Path "$pfxPath" -Force `.trim(); case "ps_binding_rdp": - return `# 需要管理员权限 + return `# *** 需要管理员权限 *** + # 请将以下变量替换为实际值 -$pfxPath = "${params?.certPath || ""}" # PFX 文件路径 -$pfxPassword = "${params?.pfxPassword || ""}" # PFX 密码 +$pfxPath = "${params?.certPath || ""}" # PFX 文件路径(与表单中保持一致) +$pfxPassword = "${params?.pfxPassword || ""}" # PFX 密码(与表单中保持一致) # 导入证书到本地计算机的个人存储区 $cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable @@ -159,6 +168,16 @@ const DeployNodeConfigFormLocalConfig = ({ form: formInst, formName, disabled, i .min(1, t("workflow_node.deploy.form.local_cert_path.tooltip")) .max(256, t("common.errmsg.string_max", { max: 256 })) .trim(), + certPathForServerOnly: z + .string() + .max(256, t("common.errmsg.string_max", { max: 256 })) + .trim() + .nullish(), + certPathForIntermediaOnly: z + .string() + .max(256, t("common.errmsg.string_max", { max: 256 })) + .trim() + .nullish(), keyPath: z .string() .max(256, t("common.errmsg.string_max", { max: 256 })) @@ -325,6 +344,24 @@ const DeployNodeConfigFormLocalConfig = ({ form: formInst, formName, disabled, i > + + } + > + + + + } + > + + @@ -407,7 +444,13 @@ const DeployNodeConfigFormLocalConfig = ({ form: formInst, formName, disabled, i
- + @@ -437,7 +480,13 @@ const DeployNodeConfigFormLocalConfig = ({ form: formInst, formName, disabled, i
- +
diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx index 042e40c5..0f3f3082 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx @@ -4,21 +4,24 @@ import { Button, Dropdown, Form, type FormInstance, Input, Select, Switch } from import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; +import CodeInput from "@/components/CodeInput"; import Show from "@/components/Show"; import { CERTIFICATE_FORMATS } from "@/domain/certificate"; -import { initPresetScript } from "./DeployNodeConfigFormLocalConfig"; +import { initPresetScript as _initPresetScript } from "./DeployNodeConfigFormLocalConfig"; type DeployNodeConfigFormSSHConfigFieldValues = Nullish<{ format: string; certPath: string; - keyPath?: string | null; - pfxPassword?: string | null; - jksAlias?: string | null; - jksKeypass?: string | null; - jksStorepass?: string | null; - preCommand?: string | null; - postCommand?: string | null; + certPathForServerOnly?: string; + certPathForIntermediaOnly?: string; + keyPath?: string; + pfxPassword?: string; + jksAlias?: string; + jksKeypass?: string; + jksStorepass?: string; + preCommand?: string; + postCommand?: string; useSCP?: boolean; }>; @@ -42,6 +45,126 @@ const initFormModel = (): DeployNodeConfigFormSSHConfigFieldValues => { }; }; +const initPresetScript = ( + key: Parameters[0] | "sh_replace_synologydsm_ssl" | "sh_replace_fnos_ssl", + params?: Parameters[1] +) => { + switch (key) { + case "sh_replace_synologydsm_ssl": + return `# *** 需要 root 权限 *** +# 脚本参考 https://github.com/catchdave/ssl-certs/blob/main/replace_synology_ssl_certs.sh + +# 请将以下变量替换为实际值 +$tmpFullchainPath = "${params?.certPath || ""}" # 证书文件路径(与表单中保持一致) +$tmpCertPath = "${params?.certPathForServerOnly || ""}" # 服务器证书文件路径(与表单中保持一致) +$tmpKeyPath = "${params?.keyPath || ""}" # 私钥文件路径(与表单中保持一致) + +DEBUG=1 +error_exit() { echo "[ERROR] $1"; exit 1; } +warn() { echo "[WARN] $1"; } +info() { echo "[INFO] $1"; } +debug() { [[ "\${DEBUG}" ]] && echo "[DEBUG] $1"; } + +certs_src_dir="/usr/syno/etc/certificate/system/default" +target_cert_dirs=( + "/usr/syno/etc/certificate/system/FQDN" + "/usr/local/etc/certificate/ScsiTarget/pkg-scsi-plugin-server/" + "/usr/local/etc/certificate/SynologyDrive/SynologyDrive/" + "/usr/local/etc/certificate/WebDAVServer/webdav/" + "/usr/local/etc/certificate/ActiveBackup/ActiveBackup/" + "/usr/syno/etc/certificate/smbftpd/ftpd/") + +# 获取证书目录 +default_dir_name=$(/dev/null && /usr/syno/bin/synopkg restart ScsiTarget +/usr/syno/bin/synopkg is_onoff SynologyDrive 1>/dev/null && /usr/syno/bin/synopkg restart SynologyDrive +/usr/syno/bin/synopkg is_onoff WebDAVServer 1>/dev/null && /usr/syno/bin/synopkg restart WebDAVServer +/usr/syno/bin/synopkg is_onoff ActiveBackup 1>/dev/null && /usr/syno/bin/synopkg restart ActiveBackup +if ! /usr/syno/bin/synow3tool --gen-all && sudo /usr/syno/bin/synosystemctl restart nginx; then + warn "nginx failed to restart" +fi + +info "Completed" + `.trim(); + + case "sh_replace_fnos_ssl": + return `# *** 需要 root 权限 *** +# 脚本参考 https://github.com/lfgyx/fnos_certificate_update/blob/main/src/update_cert.sh + + +# 请将以下变量替换为实际值 +# 飞牛证书实际存放路径请在 \`/usr/trim/etc/network_cert_all.conf\` 中查看,注意不要修改文件名 +$tmpFullchainPath = "${params?.certPath || ""}" # 证书文件路径(与表单中保持一致) +$tmpCertPath = "${params?.certPathForServerOnly || ""}" # 服务器证书文件路径(与表单中保持一致) +$tmpKeyPath = "${params?.keyPath || ""}" # 私钥文件路径(与表单中保持一致) +$fnFullchainPath = "/usr/trim/var/trim_connect/ssls/example.com/1234567890/fullchain.crt" # 飞牛证书文件路径 +$fnCertPath = "/usr/trim/var/trim_connect/ssls/example.com/1234567890/example.com.crt" # 飞牛服务器证书文件路径 +$fnKeyPath = "/usr/trim/var/trim_connect/ssls/example.com/1234567890/example.com.key" # 飞牛私钥文件路径 +$domain = "" # 域名 + +# 复制文件 +cp -rf "$tmpFullchainPath" "$fnFullchainPath" +cp -rf "$tmpCertPath" "$fnCertPath" +cp -rf "$tmpKeyPath" "$fnKeyPath" +chmod 755 "$fnCertPath" +chmod 755 "$fnKeyPath" +chmod 755 "$fnFullchainPath" + +# 更新数据库 +NEW_EXPIRY_DATE=$(openssl x509 -enddate -noout -in "$fnCertPath" | sed "s/^.*=\\(.*\\)$/\\1/") +NEW_EXPIRY_TIMESTAMP=$(date -d "$NEW_EXPIRY_DATE" +%s%3N) +psql -U postgres -d trim_connect -c "UPDATE cert SET valid_to=$NEW_EXPIRY_TIMESTAMP WHERE domain='$domain'" + +# 重启服务 +systemctl restart webdav.service +systemctl restart smbftpd.service +systemctl restart trim_nginx.service + `.trim(); + } + + return _initPresetScript(key as Parameters[0], params); +}; + const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormSSHConfigProps) => { const { t } = useTranslation(); @@ -60,6 +183,16 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini .trim() .nullish() .refine((v) => fieldFormat !== FORMAT_PEM || !!v?.trim(), { message: t("workflow_node.deploy.form.ssh_key_path.tooltip") }), + certPathForServerOnly: z + .string() + .max(256, t("common.errmsg.string_max", { max: 256 })) + .trim() + .nullish(), + certPathForIntermediaOnly: z + .string() + .max(256, t("common.errmsg.string_max", { max: 256 })) + .trim() + .nullish(), pfxPassword: z .string() .max(64, t("common.errmsg.string_max", { max: 256 })) @@ -147,6 +280,24 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini const handlePresetPostScriptClick = (key: string) => { switch (key) { case "sh_reload_nginx": + { + formInst.setFieldValue("postCommand", initPresetScript(key)); + } + break; + + case "sh_replace_synologydsm_ssl": + case "sh_replace_fnos_ssl": + { + const presetScriptParams = { + certPath: formInst.getFieldValue("certPath"), + certPathForServerOnly: formInst.getFieldValue("certPathForServerOnly"), + certPathForIntermediaOnly: formInst.getFieldValue("certPathForIntermediaOnly"), + keyPath: formInst.getFieldValue("keyPath"), + }; + formInst.setFieldValue("postCommand", initPresetScript(key, presetScriptParams)); + } + break; + case "ps_binding_iis": case "ps_binding_netsh": case "ps_binding_rdp": @@ -206,6 +357,24 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini > + + } + > + + + + } + > + +
@@ -248,10 +417,6 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini - -