mirror of
https://github.com/woodchen-ink/certimate.git
synced 2025-07-18 09:21:56 +08:00
commit
11a4d4f55c
@ -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
|
||||
}
|
||||
|
@ -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"`
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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})
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
|
||||
func (c *Client) getAccessToken() error {
|
||||
req := &getAPIAccessTokenRequest{
|
||||
Type: c.apiUserType,
|
||||
Type: c.apiRole,
|
||||
AccessKeyId: c.accessKeyId,
|
||||
AccessKey: c.accessKey,
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
44
migrations/1747314000_upgrade.go
Normal file
44
migrations/1747314000_upgrade.go
Normal file
@ -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
|
||||
})
|
||||
}
|
277
ui/package-lock.json
generated
277
ui/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
97
ui/src/components/CodeInput.tsx
Normal file
97
ui/src/components/CodeInput.tsx
Normal file
@ -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<ReactCodeMirrorProps, "extensions" | "lang" | "theme"> {
|
||||
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<ReactCodeMirrorRef>(null);
|
||||
const isFocusWithin = useFocusWithin(cmRef.current?.editor);
|
||||
|
||||
const cmTheme = useMemo(() => {
|
||||
if (browserTheme === "dark") {
|
||||
return vscodeDark;
|
||||
}
|
||||
return vscodeLight;
|
||||
}, [browserTheme]);
|
||||
|
||||
const cmExtensions = useMemo(() => {
|
||||
const temp: NonNullable<ReactCodeMirrorProps["extensions"]> = [
|
||||
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 (
|
||||
<div
|
||||
className={mergeCls(className, `hover:border-[${themeToken.colorPrimaryBorderHover}]`)}
|
||||
style={{
|
||||
...(style ?? {}),
|
||||
border: `1px solid ${isFocusWithin ? (themeToken.Input?.activeBorderColor ?? themeToken.colorPrimaryBorder) : themeToken.colorBorder}`,
|
||||
borderRadius: `${themeToken.borderRadius}px`,
|
||||
backgroundColor: disabled ? themeToken.colorBgContainerDisabled : themeToken.colorBgContainer,
|
||||
boxShadow: isFocusWithin ? themeToken.Input?.activeShadow : undefined,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<CodeMirror
|
||||
ref={cmRef}
|
||||
height="100%"
|
||||
style={{ height: "100%" }}
|
||||
{...props}
|
||||
basicSetup={{
|
||||
foldGutter: false,
|
||||
dropCursor: false,
|
||||
allowMultipleSelections: false,
|
||||
indentOnInput: false,
|
||||
}}
|
||||
extensions={cmExtensions}
|
||||
theme={cmTheme}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeInput;
|
51
ui/src/components/TextFileInput.tsx
Normal file
51
ui/src/components/TextFileInput.tsx
Normal file
@ -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<TextAreaProps, "onChange"> {
|
||||
accept?: UploadProps["accept"];
|
||||
uploadButtonProps?: Omit<ButtonProps, "disabled" | "onClick">;
|
||||
uploadText?: string;
|
||||
onChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
const TextFileInput = ({ className, style, accept, disabled, readOnly, uploadText, uploadButtonProps, onChange, ...props }: TextFileInputProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleButtonClick = () => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const { files } = e.target as HTMLInputElement;
|
||||
if (files?.length) {
|
||||
const value = await readFileContent(files[0]);
|
||||
onChange?.(value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Space className={mergeCls("w-full", className)} style={style} direction="vertical" size="small">
|
||||
<Input.TextArea {...props} disabled={disabled} readOnly={readOnly} onChange={(e) => onChange?.(e.target.value)} />
|
||||
{!readOnly && (
|
||||
<>
|
||||
<Button {...uploadButtonProps} block disabled={disabled} icon={<UploadOutlinedIcon />} onClick={handleButtonClick}>
|
||||
{uploadText ?? t("common.text.import_from_file")}
|
||||
</Button>
|
||||
<input ref={fileInputRef} type="file" style={{ display: "none" }} accept={accept} onChange={handleFileChange} />
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextFileInput;
|
@ -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://<your-host-addr>: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
|
||||
<Input placeholder={t("access.form.goedge_api_url.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="apiRole" label={t("access.form.goedge_api_role.label")} rules={[formRule]}>
|
||||
<Radio.Group options={["user", "admin"].map((s) => ({ label: t(`access.form.goedge_api_role.option.${s}.label`), value: s }))} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="accessKeyId"
|
||||
label={t("access.form.goedge_access_key_id.label")}
|
||||
|
@ -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, Upload, type UploadFile, type UploadProps } from "antd";
|
||||
import { Form, type FormInstance } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import TextFileInput from "@/components/TextFileInput";
|
||||
import { type AccessConfigForKubernetes } from "@/domain/access";
|
||||
import { readFileContent } from "@/utils/file";
|
||||
|
||||
type AccessFormKubernetesConfigFieldValues = Nullish<AccessConfigForKubernetes>;
|
||||
|
||||
@ -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<UploadFile[]>([]);
|
||||
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<typeof formSchema>) => {
|
||||
onValuesChange?.(values);
|
||||
};
|
||||
@ -65,16 +45,13 @@ const AccessFormKubernetesConfig = ({ form: formInst, formName, disabled, initia
|
||||
name={formName}
|
||||
onValuesChange={handleFormChange}
|
||||
>
|
||||
<Form.Item name="kubeConfig" noStyle rules={[formRule]}>
|
||||
<Input.TextArea autoComplete="new-password" hidden placeholder={t("access.form.k8s_kubeconfig.placeholder")} value={fieldKubeConfig} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="kubeConfig"
|
||||
label={t("access.form.k8s_kubeconfig.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.k8s_kubeconfig.tooltip") }}></span>}
|
||||
>
|
||||
<Upload beforeUpload={() => false} fileList={fieldKubeFileList} maxCount={1} onChange={handleKubeFileChange}>
|
||||
<Button icon={<UploadOutlinedIcon />}>{t("access.form.k8s_kubeconfig.upload")}</Button>
|
||||
</Upload>
|
||||
<TextFileInput allowClear autoSize={{ minRows: 3, maxRows: 10 }} placeholder={t("access.form.k8s_kubeconfig.placeholder")} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
|
@ -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<AccessConfigForSSH>;
|
||||
@ -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<UploadFile[]>([]);
|
||||
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<typeof formSchema>) => {
|
||||
onValuesChange?.(values);
|
||||
};
|
||||
@ -104,48 +84,36 @@ const AccessFormSSHConfig = ({ form: formInst, formName, disabled, initialValues
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<div className="w-1/2">
|
||||
<Form.Item name="username" label={t("access.form.ssh_username.label")} rules={[formRule]}>
|
||||
<Input autoComplete="new-password" placeholder={t("access.form.ssh_username.placeholder")} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item name="username" label={t("access.form.ssh_username.label")} rules={[formRule]}>
|
||||
<Input autoComplete="new-password" placeholder={t("access.form.ssh_username.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<div className="w-1/2">
|
||||
<Form.Item
|
||||
name="password"
|
||||
label={t("access.form.ssh_password.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_password.tooltip") }}></span>}
|
||||
>
|
||||
<Input.Password autoComplete="new-password" placeholder={t("access.form.ssh_password.placeholder")} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
<Form.Item
|
||||
name="password"
|
||||
label={t("access.form.ssh_password.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_password.tooltip") }}></span>}
|
||||
>
|
||||
<Input.Password allowClear autoComplete="new-password" placeholder={t("access.form.ssh_password.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<div className="w-1/2">
|
||||
<Form.Item name="key" noStyle rules={[formRule]}>
|
||||
<Input.TextArea autoComplete="new-password" hidden placeholder={t("access.form.ssh_key.placeholder")} value={fieldKey} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("access.form.ssh_key.label")} tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_key.tooltip") }}></span>}>
|
||||
<Upload beforeUpload={() => false} fileList={fieldKeyFileList} maxCount={1} onChange={handleKeyFileChange}>
|
||||
<Button icon={<UploadOutlinedIcon />}>{t("access.form.ssh_key.upload")}</Button>
|
||||
</Upload>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item
|
||||
name="key"
|
||||
label={t("access.form.ssh_key.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_key.tooltip") }}></span>}
|
||||
>
|
||||
<TextFileInput allowClear autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("access.form.ssh_key.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<div className="w-1/2">
|
||||
<Form.Item
|
||||
name="keyPassphrase"
|
||||
label={t("access.form.ssh_key_passphrase.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_key_passphrase.tooltip") }}></span>}
|
||||
>
|
||||
<Input.Password autoComplete="new-password" placeholder={t("access.form.ssh_key_passphrase.placeholder")} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
<Form.Item
|
||||
name="keyPassphrase"
|
||||
label={t("access.form.ssh_key_passphrase.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_key_passphrase.tooltip") }}></span>}
|
||||
>
|
||||
<Input.Password allowClear autoComplete="new-password" placeholder={t("access.form.ssh_key_passphrase.placeholder")} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
@ -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<HTMLTextAreaElement>) => {
|
||||
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<HTMLTextAreaElement>) => {
|
||||
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={<span dangerouslySetInnerHTML={{ __html: t("access.form.webhook_headers.tooltip") }}></span>}
|
||||
>
|
||||
<Input.TextArea autoSize={{ minRows: 3, maxRows: 5 }} placeholder={t("access.form.webhook_headers.placeholder")} onBlur={handleWebhookHeadersBlur} />
|
||||
<Input.TextArea autoSize={{ minRows: 3, maxRows: 10 }} placeholder={t("access.form.webhook_headers.placeholder")} onBlur={handleWebhookHeadersBlur} />
|
||||
</Form.Item>
|
||||
|
||||
<Show when={!usage || usage === "deployment"}>
|
||||
@ -297,9 +298,11 @@ const AccessFormWebhookConfig = ({ form: formInst, formName, disabled, initialVa
|
||||
</div>
|
||||
</label>
|
||||
<Form.Item name="defaultDataForDeployment" rules={[formRule]}>
|
||||
<Input.TextArea
|
||||
allowClear
|
||||
autoSize={{ minRows: 3, maxRows: 10 }}
|
||||
<CodeInput
|
||||
height="auto"
|
||||
minHeight="64px"
|
||||
maxHeight="256px"
|
||||
language="json"
|
||||
placeholder={t("access.form.webhook_default_data_for_deployment.placeholder")}
|
||||
onBlur={handleWebhookDataForDeploymentBlur}
|
||||
/>
|
||||
@ -338,9 +341,11 @@ const AccessFormWebhookConfig = ({ form: formInst, formName, disabled, initialVa
|
||||
</div>
|
||||
</label>
|
||||
<Form.Item name="defaultDataForNotification" rules={[formRule]}>
|
||||
<Input.TextArea
|
||||
allowClear
|
||||
autoSize={{ minRows: 3, maxRows: 10 }}
|
||||
<CodeInput
|
||||
height="auto"
|
||||
minHeight="64px"
|
||||
maxHeight="256px"
|
||||
language="json"
|
||||
placeholder={t("access.form.webhook_default_data_for_notification.placeholder")}
|
||||
onBlur={handleWebhookDataForNotificationBlur}
|
||||
/>
|
||||
|
@ -75,7 +75,7 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => {
|
||||
</CopyToClipboard>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Input.TextArea value={data.certificate} variant="filled" rows={5} autoSize={{ maxRows: 5 }} readOnly />
|
||||
<Input.TextArea value={data.certificate} variant="filled" autoSize={{ minRows: 5, maxRows: 5 }} readOnly />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
@ -92,7 +92,7 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => {
|
||||
</CopyToClipboard>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Input.TextArea value={data.privateKey} variant="filled" rows={5} autoSize={{ maxRows: 5 }} readOnly />
|
||||
<Input.TextArea value={data.privateKey} variant="filled" autoSize={{ minRows: 5, maxRows: 5 }} readOnly />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
|
@ -108,7 +108,7 @@ const NotifyTemplateForm = ({ className, style }: NotifyTemplateFormProps) => {
|
||||
rules={[formRule]}
|
||||
>
|
||||
<Input.TextArea
|
||||
autoSize={{ minRows: 3, maxRows: 5 }}
|
||||
autoSize={{ minRows: 3, maxRows: 10 }}
|
||||
placeholder={t("settings.notification.template.form.message.placeholder")}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
|
@ -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 (
|
||||
<Col key={index} xs={24} md={12} span={8}>
|
||||
<Card
|
||||
className="h-20 w-full overflow-hidden shadow-sm"
|
||||
className={mergeCls("h-20 w-full overflow-hidden shadow-sm", provider.builtin ? " cursor-not-allowed" : "")}
|
||||
styles={{ body: { height: "100%", padding: "0.5rem 1rem" } }}
|
||||
hoverable
|
||||
onClick={() => {
|
||||
if (provider.builtin) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleProviderTypeSelect(provider.type);
|
||||
}}
|
||||
>
|
||||
<Flex className="size-full overflow-hidden" align="center" gap={8}>
|
||||
<Avatar src={provider.icon} size="small" />
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<Typography.Text className="mb-1 line-clamp-1">{t(provider.name)}</Typography.Text>
|
||||
<Typography.Text className="mb-1 line-clamp-1" type={provider.builtin ? "secondary" : undefined}>
|
||||
{t(provider.name)}
|
||||
</Typography.Text>
|
||||
<div className="origin-left scale-[80%]">
|
||||
<Show when={provider.builtin}>
|
||||
<Tag>{t("access.props.provider.builtin")}</Tag>
|
||||
|
@ -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 || "<your-key-path>"}" -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 || "<your-cert-path>"}" # PFX 文件路径
|
||||
$pfxPassword = "${params?.pfxPassword || "<your-pfx-password>"}" # PFX 密码
|
||||
$pfxPath = "${params?.certPath || "<your-cert-path>"}" # PFX 文件路径(与表单中保持一致)
|
||||
$pfxPassword = "${params?.pfxPassword || "<your-pfx-password>"}" # PFX 密码(与表单中保持一致)
|
||||
$siteName = "<your-site-name>" # IIS 网站名称
|
||||
$domain = "<your-domain-name>" # 域名
|
||||
$ipaddr = "<your-binding-ip>" # 绑定 IP,“*”表示所有 IP 绑定
|
||||
$port = "<your-binding-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 || "<your-cert-path>"}" # PFX 文件路径
|
||||
$pfxPassword = "${params?.pfxPassword || "<your-pfx-password>"}" # PFX 密码
|
||||
$ipaddr = "<your-binding-ip>" # 绑定 IP,“0.0.0.0”表示所有 IP 绑定,可填入域名。
|
||||
$pfxPath = "${params?.certPath || "<your-cert-path>"}" # PFX 文件路径(与表单中保持一致)
|
||||
$pfxPassword = "${params?.pfxPassword || "<your-pfx-password>"}" # PFX 密码(与表单中保持一致)
|
||||
$ipaddr = "<your-binding-ip>" # 绑定 IP,“0.0.0.0”表示所有 IP 绑定,可填入域名
|
||||
$port = "<your-binding-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 || "<your-cert-path>"}" # PFX 文件路径
|
||||
$pfxPassword = "${params?.pfxPassword || "<your-pfx-password>"}" # PFX 密码
|
||||
$pfxPath = "${params?.certPath || "<your-cert-path>"}" # PFX 文件路径(与表单中保持一致)
|
||||
$pfxPassword = "${params?.pfxPassword || "<your-pfx-password>"}" # 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
|
||||
>
|
||||
<Input placeholder={t("workflow_node.deploy.form.local_key_path.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="certPathForServerOnly"
|
||||
label={t("workflow_node.deploy.form.local_servercert_path.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.local_servercert_path.tooltip") }}></span>}
|
||||
>
|
||||
<Input placeholder={t("workflow_node.deploy.form.local_servercert_path.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="certPathForIntermediaOnly"
|
||||
label={t("workflow_node.deploy.form.local_intermediacert_path.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.local_intermediacert_path.tooltip") }}></span>}
|
||||
>
|
||||
<Input placeholder={t("workflow_node.deploy.form.local_intermediacert_path.placeholder")} />
|
||||
</Form.Item>
|
||||
</Show>
|
||||
|
||||
<Show when={fieldFormat === FORMAT_PFX}>
|
||||
@ -407,7 +444,13 @@ const DeployNodeConfigFormLocalConfig = ({ form: formInst, formName, disabled, i
|
||||
</div>
|
||||
</label>
|
||||
<Form.Item name="preCommand" rules={[formRule]}>
|
||||
<Input.TextArea autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("workflow_node.deploy.form.local_pre_command.placeholder")} />
|
||||
<CodeInput
|
||||
height="auto"
|
||||
minHeight="64px"
|
||||
maxHeight="256px"
|
||||
language={["shell", "powershell"]}
|
||||
placeholder={t("workflow_node.deploy.form.local_pre_command.placeholder")}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
|
||||
@ -437,7 +480,13 @@ const DeployNodeConfigFormLocalConfig = ({ form: formInst, formName, disabled, i
|
||||
</div>
|
||||
</label>
|
||||
<Form.Item name="postCommand" rules={[formRule]}>
|
||||
<Input.TextArea autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("workflow_node.deploy.form.local_post_command.placeholder")} />
|
||||
<CodeInput
|
||||
height="auto"
|
||||
minHeight="64px"
|
||||
maxHeight="256px"
|
||||
language={["shell", "powershell"]}
|
||||
placeholder={t("workflow_node.deploy.form.local_post_command.placeholder")}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
@ -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<typeof _initPresetScript>[0] | "sh_replace_synologydsm_ssl" | "sh_replace_fnos_ssl",
|
||||
params?: Parameters<typeof _initPresetScript>[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 || "<your-fullchain-cert-path>"}" # 证书文件路径(与表单中保持一致)
|
||||
$tmpCertPath = "${params?.certPathForServerOnly || "<your-server-cert-path>"}" # 服务器证书文件路径(与表单中保持一致)
|
||||
$tmpKeyPath = "${params?.keyPath || "<your-key-path>"}" # 私钥文件路径(与表单中保持一致)
|
||||
|
||||
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=$(</usr/syno/etc/certificate/_archive/DEFAULT)
|
||||
if [[ -n "$default_dir_name" ]]; then
|
||||
target_cert_dirs+=("/usr/syno/etc/certificate/_archive/\${default_dir_name}")
|
||||
debug "Default cert directory found: '/usr/syno/etc/certificate/_archive/\${default_dir_name}'"
|
||||
else
|
||||
warn "No default directory found. Probably unusual? Check: 'cat /usr/syno/etc/certificate/_archive/DEFAULT'"
|
||||
fi
|
||||
|
||||
# 获取反向代理证书目录
|
||||
for proxy in /usr/syno/etc/certificate/ReverseProxy/*/; do
|
||||
debug "Found proxy dir: \${proxy}"
|
||||
target_cert_dirs+=("\${proxy}")
|
||||
done
|
||||
|
||||
[[ "\${DEBUG}" ]] && set -x
|
||||
|
||||
# 复制文件
|
||||
cp -rf "$tmpFullchainPath" "\${certs_src_dir}/fullchain.pem" || error_exit "Halting because of error moving fullchain file"
|
||||
cp -rf "$tmpCertPath" "\${certs_src_dir}/cert.pem" || error_exit "Halting because of error moving cert file"
|
||||
cp -rf "$tmpKeyPath" "\${certs_src_dir}/privkey.pem" || error_exit "Halting because of error moving privkey file"
|
||||
chown root:root "\${certs_src_dir}/"{privkey,fullchain,cert}.pem || error_exit "Halting because of error chowning files"
|
||||
info "Certs moved from /tmp & chowned."
|
||||
|
||||
# 替换证书
|
||||
for target_dir in "\${target_cert_dirs[@]}"; do
|
||||
if [[ ! -d "$target_dir" ]]; then
|
||||
debug "Target cert directory '$target_dir' not found, skipping..."
|
||||
continue
|
||||
fi
|
||||
info "Copying certificates to '$target_dir'"
|
||||
if ! (cp "\${certs_src_dir}/"{privkey,fullchain,cert}.pem "$target_dir/" && \
|
||||
chown root:root "$target_dir/"{privkey,fullchain,cert}.pem); then
|
||||
warn "Error copying or chowning certs to \${target_dir}"
|
||||
fi
|
||||
done
|
||||
|
||||
# 重启服务
|
||||
info "Rebooting all the things..."
|
||||
/usr/syno/bin/synosystemctl restart nmbd
|
||||
/usr/syno/bin/synosystemctl restart avahi
|
||||
/usr/syno/bin/synosystemctl restart ldap-server
|
||||
/usr/syno/bin/synopkg is_onoff ScsiTarget 1>/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 || "<your-fullchain-cert-path>"}" # 证书文件路径(与表单中保持一致)
|
||||
$tmpCertPath = "${params?.certPathForServerOnly || "<your-server-cert-path>"}" # 服务器证书文件路径(与表单中保持一致)
|
||||
$tmpKeyPath = "${params?.keyPath || "<your-key-path>"}" # 私钥文件路径(与表单中保持一致)
|
||||
$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 = "<your-domain-name>" # 域名
|
||||
|
||||
# 复制文件
|
||||
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<typeof _initPresetScript>[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
|
||||
>
|
||||
<Input placeholder={t("workflow_node.deploy.form.ssh_key_path.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="certPathForServerOnly"
|
||||
label={t("workflow_node.deploy.form.ssh_servercert_path.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.ssh_servercert_path.tooltip") }}></span>}
|
||||
>
|
||||
<Input placeholder={t("workflow_node.deploy.form.ssh_servercert_path.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="certPathForIntermediaOnly"
|
||||
label={t("workflow_node.deploy.form.ssh_intermediacert_path.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.ssh_intermediacert_path.tooltip") }}></span>}
|
||||
>
|
||||
<Input placeholder={t("workflow_node.deploy.form.ssh_intermediacert_path.placeholder")} />
|
||||
</Form.Item>
|
||||
</Show>
|
||||
|
||||
<Show when={fieldFormat === FORMAT_PFX}>
|
||||
@ -248,10 +417,6 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini
|
||||
</Form.Item>
|
||||
</Show>
|
||||
|
||||
<Form.Item label={t("workflow_node.deploy.form.ssh_shell_env.label")}>
|
||||
<Select options={[{ value: t("workflow_node.deploy.form.ssh_shell_env.value") }]} value={t("workflow_node.deploy.form.ssh_shell_env.value")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item className="mb-0" htmlFor="null">
|
||||
<label className="mb-1 block">
|
||||
<div className="flex w-full items-center justify-between gap-4">
|
||||
@ -278,7 +443,13 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini
|
||||
</div>
|
||||
</label>
|
||||
<Form.Item name="preCommand" rules={[formRule]}>
|
||||
<Input.TextArea autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("workflow_node.deploy.form.ssh_pre_command.placeholder")} />
|
||||
<CodeInput
|
||||
height="auto"
|
||||
minHeight="64px"
|
||||
maxHeight="256px"
|
||||
language={["shell", "powershell"]}
|
||||
placeholder={t("workflow_node.deploy.form.ssh_pre_command.placeholder")}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
|
||||
@ -291,11 +462,13 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini
|
||||
<div className="text-right">
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: ["sh_reload_nginx", "ps_binding_iis", "ps_binding_netsh", "ps_binding_rdp"].map((key) => ({
|
||||
key,
|
||||
label: t(`workflow_node.deploy.form.ssh_preset_scripts.option.${key}.label`),
|
||||
onClick: () => handlePresetPostScriptClick(key),
|
||||
})),
|
||||
items: ["sh_reload_nginx", "sh_replace_synologydsm_ssl", "sh_replace_fnos_ssl", "ps_binding_iis", "ps_binding_netsh", "ps_binding_rdp"].map(
|
||||
(key) => ({
|
||||
key,
|
||||
label: t(`workflow_node.deploy.form.ssh_preset_scripts.option.${key}.label`),
|
||||
onClick: () => handlePresetPostScriptClick(key),
|
||||
})
|
||||
),
|
||||
}}
|
||||
trigger={["click"]}
|
||||
>
|
||||
@ -308,7 +481,13 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini
|
||||
</div>
|
||||
</label>
|
||||
<Form.Item name="postCommand" rules={[formRule]}>
|
||||
<Input.TextArea autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("workflow_node.deploy.form.ssh_post_command.placeholder")} />
|
||||
<CodeInput
|
||||
height="auto"
|
||||
minHeight="64px"
|
||||
maxHeight="256px"
|
||||
language={["shell", "powershell"]}
|
||||
placeholder={t("workflow_node.deploy.form.ssh_post_command.placeholder")}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
|
||||
|
@ -51,7 +51,7 @@ const DeployNodeConfigFormTencentCloudSSLDeployConfig = ({
|
||||
if (!v) return false;
|
||||
return String(v)
|
||||
.split(MULTIPLE_INPUT_DELIMITER)
|
||||
.every((e) => /^[A-Za-z0-9*._-]+$/.test(e));
|
||||
.every((e) => /^[A-Za-z0-9*._-|]+$/.test(e));
|
||||
}, t("workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.errmsg.invalid")),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
@ -138,7 +138,7 @@ const ResourceIdsModalInput = memo(({ value, trigger, onChange }: { value?: stri
|
||||
|
||||
const formSchema = z.object({
|
||||
resourceIds: z.array(z.string()).refine((v) => {
|
||||
return v.every((e) => !e?.trim() || /^[A-Za-z0-9*._-]+$/.test(e));
|
||||
return v.every((e) => !e?.trim() || /^[A-Za-z0-9*._-|]+$/.test(e));
|
||||
}, t("workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.errmsg.invalid")),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Alert, Form, type FormInstance, Input } from "antd";
|
||||
import { Alert, Form, type FormInstance } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import CodeInput from "@/components/CodeInput";
|
||||
|
||||
type DeployNodeConfigFormWebhookConfigFieldValues = Nullish<{
|
||||
webhookData: string;
|
||||
}>;
|
||||
@ -39,8 +41,8 @@ const DeployNodeConfigFormWebhookConfig = ({ form: formInst, formName, disabled,
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
|
||||
const handleWebhookDataBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
|
||||
const value = e.target.value;
|
||||
const handleWebhookDataBlur = () => {
|
||||
const value = formInst.getFieldValue("webhookData");
|
||||
try {
|
||||
const json = JSON.stringify(JSON.parse(value), null, 2);
|
||||
formInst.setFieldValue("webhookData", json);
|
||||
@ -68,9 +70,11 @@ const DeployNodeConfigFormWebhookConfig = ({ form: formInst, formName, disabled,
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.webhook_data.tooltip") }}></span>}
|
||||
>
|
||||
<Input.TextArea
|
||||
allowClear
|
||||
autoSize={{ minRows: 3, maxRows: 10 }}
|
||||
<CodeInput
|
||||
height="auto"
|
||||
minHeight="64px"
|
||||
maxHeight="256px"
|
||||
language="json"
|
||||
placeholder={t("workflow_node.deploy.form.webhook_data.placeholder")}
|
||||
onBlur={handleWebhookDataBlur}
|
||||
/>
|
||||
|
@ -177,7 +177,7 @@ const NotifyNodeConfigForm = forwardRef<NotifyNodeConfigFormInstance, NotifyNode
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="message" label={t("workflow_node.notify.form.message.label")} rules={[formRule]}>
|
||||
<Input.TextArea autoSize={{ minRows: 3, maxRows: 5 }} placeholder={t("workflow_node.notify.form.message.placeholder")} />
|
||||
<Input.TextArea autoSize={{ minRows: 3, maxRows: 10 }} placeholder={t("workflow_node.notify.form.message.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item className="mb-0" htmlFor="null">
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Alert, Form, type FormInstance, Input } from "antd";
|
||||
import { Alert, Form, type FormInstance } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import CodeInput from "@/components/CodeInput";
|
||||
|
||||
type NotifyNodeConfigFormWebhookConfigFieldValues = Nullish<{
|
||||
webhookData: string;
|
||||
}>;
|
||||
@ -39,8 +41,8 @@ const NotifyNodeConfigFormWebhookConfig = ({ form: formInst, formName, disabled,
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
|
||||
const handleWebhookDataBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
|
||||
const value = e.target.value;
|
||||
const handleWebhookDataBlur = () => {
|
||||
const value = formInst.getFieldValue("webhookData");
|
||||
try {
|
||||
const json = JSON.stringify(JSON.parse(value), null, 2);
|
||||
formInst.setFieldValue("webhookData", json);
|
||||
@ -68,9 +70,11 @@ const NotifyNodeConfigFormWebhookConfig = ({ form: formInst, formName, disabled,
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.notify.form.webhook_data.tooltip") }}></span>}
|
||||
>
|
||||
<Input.TextArea
|
||||
allowClear
|
||||
autoSize={{ minRows: 3, maxRows: 10 }}
|
||||
<CodeInput
|
||||
height="auto"
|
||||
minHeight="64px"
|
||||
maxHeight="256px"
|
||||
language="json"
|
||||
placeholder={t("workflow_node.notify.form.webhook_data.placeholder")}
|
||||
onBlur={handleWebhookDataBlur}
|
||||
/>
|
||||
|
@ -1,15 +1,14 @@
|
||||
import { forwardRef, memo, useImperativeHandle } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UploadOutlined as UploadOutlinedIcon } from "@ant-design/icons";
|
||||
import { Button, Form, type FormInstance, Input, Upload, type UploadProps } from "antd";
|
||||
import { Form, type FormInstance, Input } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { validateCertificate, validatePrivateKey } from "@/api/certificates";
|
||||
import TextFileInput from "@/components/TextFileInput";
|
||||
import { type WorkflowNodeConfigForUpload } from "@/domain/workflow";
|
||||
import { useAntdForm } from "@/hooks";
|
||||
import { getErrMsg } from "@/utils/error";
|
||||
import { readFileContent } from "@/utils/file";
|
||||
|
||||
type UploadNodeConfigFormFieldValues = Partial<WorkflowNodeConfigForUpload>;
|
||||
|
||||
@ -70,65 +69,53 @@ const UploadNodeConfigForm = forwardRef<UploadNodeConfigFormInstance, UploadNode
|
||||
} as UploadNodeConfigFormInstance;
|
||||
});
|
||||
|
||||
const handleCertificateFileChange: UploadProps["onChange"] = async ({ file }) => {
|
||||
if (file && file.status !== "removed") {
|
||||
const certificate = await readFileContent(file.originFileObj ?? (file as unknown as File));
|
||||
|
||||
try {
|
||||
const resp = await validateCertificate(certificate);
|
||||
formInst.setFields([
|
||||
{
|
||||
name: "domains",
|
||||
value: resp.data.domains,
|
||||
},
|
||||
{
|
||||
name: "certificate",
|
||||
value: certificate,
|
||||
},
|
||||
]);
|
||||
} catch (e) {
|
||||
formInst.setFields([
|
||||
{
|
||||
name: "domains",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
name: "certificate",
|
||||
value: "",
|
||||
errors: [getErrMsg(e)],
|
||||
},
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
formInst.setFieldValue("certificate", "");
|
||||
const handleCertificateChange = async (value: string) => {
|
||||
try {
|
||||
const resp = await validateCertificate(value);
|
||||
formInst.setFields([
|
||||
{
|
||||
name: "domains",
|
||||
value: resp.data.domains,
|
||||
},
|
||||
{
|
||||
name: "certificate",
|
||||
value: value,
|
||||
},
|
||||
]);
|
||||
} catch (e) {
|
||||
formInst.setFields([
|
||||
{
|
||||
name: "domains",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
name: "certificate",
|
||||
value: value,
|
||||
errors: [getErrMsg(e)],
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
onValuesChange?.(formInst.getFieldsValue(true));
|
||||
};
|
||||
|
||||
const handlePrivateKeyFileChange: UploadProps["onChange"] = async ({ file }) => {
|
||||
if (file && file.status !== "removed") {
|
||||
const privateKey = await readFileContent(file.originFileObj ?? (file as unknown as File));
|
||||
|
||||
try {
|
||||
await validatePrivateKey(privateKey);
|
||||
formInst.setFields([
|
||||
{
|
||||
name: "privateKey",
|
||||
value: privateKey,
|
||||
},
|
||||
]);
|
||||
} catch (e) {
|
||||
formInst.setFields([
|
||||
{
|
||||
name: "privateKey",
|
||||
value: "",
|
||||
errors: [getErrMsg(e)],
|
||||
},
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
formInst.setFieldValue("privateKey", "");
|
||||
const handlePrivateKeyChange = async (value: string) => {
|
||||
try {
|
||||
await validatePrivateKey(value);
|
||||
formInst.setFields([
|
||||
{
|
||||
name: "privateKey",
|
||||
value: value,
|
||||
},
|
||||
]);
|
||||
} catch (e) {
|
||||
formInst.setFields([
|
||||
{
|
||||
name: "privateKey",
|
||||
value: value,
|
||||
errors: [getErrMsg(e)],
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
onValuesChange?.(formInst.getFieldsValue(true));
|
||||
@ -141,23 +128,19 @@ const UploadNodeConfigForm = forwardRef<UploadNodeConfigFormInstance, UploadNode
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="certificate" label={t("workflow_node.upload.form.certificate.label")} rules={[formRule]}>
|
||||
<Input.TextArea readOnly autoSize={{ minRows: 5, maxRows: 5 }} placeholder={t("workflow_node.upload.form.certificate.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Upload beforeUpload={() => false} maxCount={1} onChange={handleCertificateFileChange}>
|
||||
<Button icon={<UploadOutlinedIcon />}>{t("workflow_node.upload.form.certificate.button")}</Button>
|
||||
</Upload>
|
||||
<TextFileInput
|
||||
autoSize={{ minRows: 3, maxRows: 10 }}
|
||||
placeholder={t("workflow_node.upload.form.certificate.placeholder")}
|
||||
onChange={handleCertificateChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="privateKey" label={t("workflow_node.upload.form.private_key.label")} rules={[formRule]}>
|
||||
<Input.TextArea readOnly autoSize={{ minRows: 5, maxRows: 5 }} placeholder={t("workflow_node.upload.form.private_key.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Upload beforeUpload={() => false} maxCount={1} onChange={handlePrivateKeyFileChange}>
|
||||
<Button icon={<UploadOutlinedIcon />}>{t("workflow_node.upload.form.private_key.button")}</Button>
|
||||
</Upload>
|
||||
<TextFileInput
|
||||
autoSize={{ minRows: 3, maxRows: 10 }}
|
||||
placeholder={t("workflow_node.upload.form.private_key.placeholder")}
|
||||
onChange={handlePrivateKeyChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
|
@ -199,6 +199,7 @@ export type AccessConfigForGoDaddy = {
|
||||
|
||||
export type AccessConfigForGoEdge = {
|
||||
apiUrl: string;
|
||||
apiRole: string;
|
||||
accessKeyId: string;
|
||||
accessKey: string;
|
||||
allowInsecureConnections?: boolean;
|
||||
|
@ -201,11 +201,15 @@
|
||||
"access.form.godaddy_api_secret.tooltip": "For more information, see <a href=\"https://developer.godaddy.com/\" target=\"_blank\">https://developer.godaddy.com/</a>",
|
||||
"access.form.goedge_api_url.label": "GoEdge API URL",
|
||||
"access.form.goedge_api_url.placeholder": "Please enter GoEdge API URL",
|
||||
"access.form.goedge_access_key_id.label": "GoEdge user AccessKeyId",
|
||||
"access.form.goedge_access_key_id.placeholder": "Please enter GoEdge user AccessKeyId",
|
||||
"access.form.goedge_api_role.label": "GoEdge user role",
|
||||
"access.form.goedge_api_role.placeholder": "Please select GoEdge user role",
|
||||
"access.form.goedge_api_role.option.user.label": "Platform user",
|
||||
"access.form.goedge_api_role.option.admin.label": "Administrator user",
|
||||
"access.form.goedge_access_key_id.label": "GoEdge AccessKeyId",
|
||||
"access.form.goedge_access_key_id.placeholder": "Please enter GoEdge AccessKeyId",
|
||||
"access.form.goedge_access_key_id.tooltip": "For more information, see <a href=\"https://goedge.cloud/docs/API/Auth.md\" target=\"_blank\">https://goedge.cloud/docs/API/Auth.md</a>",
|
||||
"access.form.goedge_access_key.label": "GoEdge user AccessKey",
|
||||
"access.form.goedge_access_key.placeholder": "Please enter GoEdge user AccessKey",
|
||||
"access.form.goedge_access_key.label": "GoEdge AccessKey",
|
||||
"access.form.goedge_access_key.placeholder": "Please enter GoEdge AccessKey",
|
||||
"access.form.goedge_access_key.tooltip": "For more information, see <a href=\"https://goedge.cloud/docs/API/Auth.md\" target=\"_blank\">https://goedge.cloud/docs/API/Auth.md</a>",
|
||||
"access.form.goedge_allow_insecure_conns.label": "Insecure SSL/TLS connections",
|
||||
"access.form.goedge_allow_insecure_conns.switch.on": "Allow",
|
||||
@ -230,7 +234,6 @@
|
||||
"access.form.jdcloud_access_key_secret.tooltip": "For more information, see <a href=\"https://docs.jdcloud.com/en/account-management/accesskey-management\" target=\"_blank\">https://docs.jdcloud.com/en/account-management/accesskey-management</a>",
|
||||
"access.form.k8s_kubeconfig.label": "KubeConfig",
|
||||
"access.form.k8s_kubeconfig.placeholder": "Please enter KubeConfig file",
|
||||
"access.form.k8s_kubeconfig.upload": "Choose File ...",
|
||||
"access.form.k8s_kubeconfig.tooltip": "For more information, see <a href=\"https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/\" target=\"_blank\">https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/</a><br><br>Leave it blank to use the Pod's ServiceAccount.",
|
||||
"access.form.larkbot_webhook_url.label": "Lark bot Webhook URL",
|
||||
"access.form.larkbot_webhook_url.placeholder": "Please enter Lark bot Webhook URL",
|
||||
@ -315,7 +318,6 @@
|
||||
"access.form.ssh_password.tooltip": "Required when using password to connect to SSH.",
|
||||
"access.form.ssh_key.label": "SSH key (Optional)",
|
||||
"access.form.ssh_key.placeholder": "Please enter SSH key",
|
||||
"access.form.ssh_key.upload": "Choose file ...",
|
||||
"access.form.ssh_key.tooltip": "Required when using key to connect to SSH.",
|
||||
"access.form.ssh_key_passphrase.label": "SSH key passphrase (Optional)",
|
||||
"access.form.ssh_key_passphrase.placeholder": "Please enter SSH key passphrase",
|
||||
@ -379,7 +381,7 @@
|
||||
"access.form.webhook_default_data.errmsg.json_invalid": "Please enter a valiod JSON string",
|
||||
"access.form.webhook_default_data_for_deployment.label": "Webhook data for deployment (Optional)",
|
||||
"access.form.webhook_default_data_for_deployment.placeholder": "Please enter Webhook data",
|
||||
"access.form.webhook_default_data_for_deployment.guide": "Tips: The Webhook data should be in JSON format. <br><br>The values in JSON support template variables, which will be replaced by actual values when sent to the Webhook URL. Supported variables: <br><ol style=\"margin-left: 1.25em; list-style: disc;\"><li><strong>${DOMAIN}</strong>: The primary domain of the certificate (<i>CommonName</i>).</li><li><strong>${DOMAINS}</strong>: The domain list of the certificate (<i>SubjectAltNames</i>).</li><li><strong>${CERTIFICATE}</strong>: The PEM format content of the certificate file.</li><li><strong>${PRIVATE_KEY}</strong>: The PEM format content of the private key file.</li></ol><br>When the request method is GET, the data will be passed as query string. Otherwise, the data will be encoded in the format indicated by the Content-Type in the request headers. Supported formats: <br><ol style=\"margin-left: 1.25em; list-style: disc;\"><li>application/json (default).</li><li>application/x-www-form-urlencoded: Nested data is not supported.</li><li>multipart/form-data: Nested data is not supported.</li>",
|
||||
"access.form.webhook_default_data_for_deployment.guide": "Tips: The Webhook data should be in JSON format. <br><br>The values in JSON support template variables, which will be replaced by actual values when sent to the Webhook URL. Supported variables: <br><ol style=\"margin-left: 1.25em; list-style: disc;\"><li><strong>${DOMAIN}</strong>: The primary domain of the certificate (<i>CommonName</i>).</li><li><strong>${DOMAINS}</strong>: The domain list of the certificate (<i>SubjectAltNames</i>).</li><li><strong>${CERTIFICATE}</strong>: The PEM format content of the certificate file.</li><li><strong>${SERVER_CERTIFICATE}</strong>: The PEM format content of the server certificate file.</li><li><strong>${INTERMEDIA_CERTIFICATE}</strong>: The PEM format content of the intermediate CA certificate file.</li><li><strong>${PRIVATE_KEY}</strong>: The PEM format content of the private key file.</li></ol><br>When the request method is GET, the data will be passed as query string. Otherwise, the data will be encoded in the format indicated by the Content-Type in the request headers. Supported formats: <br><ol style=\"margin-left: 1.25em; list-style: disc;\"><li>application/json (default).</li><li>application/x-www-form-urlencoded: Nested data is not supported.</li><li>multipart/form-data: Nested data is not supported.</li>",
|
||||
"access.form.webhook_default_data_for_notification.label": "Webhook data for notification (Optional)",
|
||||
"access.form.webhook_default_data_for_notification.placeholder": "Please enter Webhook data",
|
||||
"access.form.webhook_default_data_for_notification.guide": "Tips: The Webhook data should be in JSON format. <br><br>The values in JSON support template variables, which will be replaced by actual values when sent to the Webhook URL. Supported variables: <br><ol style=\"margin-left: 1.25em; list-style: disc;\"><li><strong>${SUBJECT}</strong>: The subject of notification.</li><li><strong>${MESSAGE}</strong>: The message of notification.</li></ol><br>When the request method is GET, the data will be passed as query string. Otherwise, the data will be encoded in the format indicated by the Content-Type in the request headers. Supported formats: <br><ol style=\"margin-left: 1.25em; list-style: disc;\"><li>application/json (default).</li><li>application/x-www-form-urlencoded: Nested data is not supported.</li><li>multipart/form-data: Nested data is not supported.</li>",
|
||||
|
@ -11,6 +11,8 @@
|
||||
"common.button.submit": "Submit",
|
||||
|
||||
"common.text.copied": "Copied",
|
||||
"common.text.import_from_file": "Import from file ...",
|
||||
"common.text.happy_browser": "The browser version is too low, and Certimate WebUI is not working well. Recommend using modern browsers such as Google Chrome v119.0 or higher.",
|
||||
"common.text.nodata": "No data available",
|
||||
"common.text.operation_confirm": "Operation confirm",
|
||||
"common.text.operation_succeeded": "Operation succeeded",
|
||||
|
@ -449,9 +449,15 @@
|
||||
"workflow_node.deploy.form.local_cert_path.label": "Certificate file saving path",
|
||||
"workflow_node.deploy.form.local_cert_path.placeholder": "Please enter saving path for certificate file",
|
||||
"workflow_node.deploy.form.local_cert_path.tooltip": "Note that the path should include the complete file name, not just the directory.",
|
||||
"workflow_node.deploy.form.local_key_path.label": "Private key file saving path",
|
||||
"workflow_node.deploy.form.local_key_path.placeholder": "Please enter saving path for private key file",
|
||||
"workflow_node.deploy.form.local_key_path.label": "Certificate's private key file saving path",
|
||||
"workflow_node.deploy.form.local_key_path.placeholder": "Please enter saving path for certificate's private key file",
|
||||
"workflow_node.deploy.form.local_key_path.tooltip": "Note that the path should include the complete file name, not just the directory.",
|
||||
"workflow_node.deploy.form.local_servercert_path.label": "Server certificate file saving path (Optional)",
|
||||
"workflow_node.deploy.form.local_servercert_path.placeholder": "Please enter saving path for server certificate file",
|
||||
"workflow_node.deploy.form.local_servercert_path.tooltip": "Note that the path should include the complete file name, not just the directory.",
|
||||
"workflow_node.deploy.form.local_intermediacert_path.label": "Intermediate CA certificate file saving path (Optional)",
|
||||
"workflow_node.deploy.form.local_intermediacert_path.placeholder": "Please enter saving path for intermediate CA certificate file",
|
||||
"workflow_node.deploy.form.local_intermediacert_path.tooltip": "Note that the path should include the complete file name, not just the directory.",
|
||||
"workflow_node.deploy.form.local_pfx_password.label": "PFX password",
|
||||
"workflow_node.deploy.form.local_pfx_password.placeholder": "Please enter PFX password",
|
||||
"workflow_node.deploy.form.local_pfx_password.tooltip": "For more information, see <a href=\"https://learn.microsoft.com/en-us/windows-hardware/drivers/install/personal-information-exchange---pfx--files\" target=\"_blank\">https://learn.microsoft.com/en-us/windows-hardware/drivers/install/personal-information-exchange---pfx--files</a>",
|
||||
@ -514,9 +520,15 @@
|
||||
"workflow_node.deploy.form.ssh_cert_path.label": "Certificate file uploading path",
|
||||
"workflow_node.deploy.form.ssh_cert_path.placeholder": "Please enter uploading path for certificate file",
|
||||
"workflow_node.deploy.form.ssh_cert_path.tooltip": "Note that the path should include the complete file name, not just the directory.",
|
||||
"workflow_node.deploy.form.ssh_key_path.label": "Private key file uploading path",
|
||||
"workflow_node.deploy.form.ssh_key_path.placeholder": "Please enter uploading path for private key file",
|
||||
"workflow_node.deploy.form.ssh_key_path.label": "Certificate's private key file uploading path",
|
||||
"workflow_node.deploy.form.ssh_key_path.placeholder": "Please enter uploading path for certificate's private key file",
|
||||
"workflow_node.deploy.form.ssh_key_path.tooltip": "Note that the path should include the complete file name, not just the directory.",
|
||||
"workflow_node.deploy.form.ssh_servercert_path.label": "Server certificate file uploading path (Optional)",
|
||||
"workflow_node.deploy.form.ssh_servercert_path.placeholder": "Please enter uploading path for server certificate file",
|
||||
"workflow_node.deploy.form.ssh_servercert_path.tooltip": "Note that the path should include the complete file name, not just the directory.",
|
||||
"workflow_node.deploy.form.ssh_intermediacert_path.label": "Intermediate CA certificate file uploading path (Optional)",
|
||||
"workflow_node.deploy.form.ssh_intermediacert_path.placeholder": "Please enter uploading path for intermediate CA certificate file",
|
||||
"workflow_node.deploy.form.ssh_intermediacert_path.tooltip": "Note that the path should include the complete file name, not just the directory.",
|
||||
"workflow_node.deploy.form.ssh_pfx_password.label": "PFX password",
|
||||
"workflow_node.deploy.form.ssh_pfx_password.placeholder": "Please enter PFX password",
|
||||
"workflow_node.deploy.form.ssh_pfx_password.tooltip": "For more information, see <a href=\"https://learn.microsoft.com/en-us/windows-hardware/drivers/install/personal-information-exchange---pfx--files\" target=\"_blank\">https://learn.microsoft.com/en-us/windows-hardware/drivers/install/personal-information-exchange---pfx--files</a>",
|
||||
@ -529,8 +541,6 @@
|
||||
"workflow_node.deploy.form.ssh_jks_storepass.label": "JKS store password",
|
||||
"workflow_node.deploy.form.ssh_jks_storepass.placeholder": "Please enter JKS store password",
|
||||
"workflow_node.deploy.form.ssh_jks_storepass.tooltip": "For more information, see <a href=\"https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html\" target=\"_blank\">https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html</a>",
|
||||
"workflow_node.deploy.form.ssh_shell_env.label": "Shell",
|
||||
"workflow_node.deploy.form.ssh_shell_env.value": "POSIX Bash (on Linux / macOS)",
|
||||
"workflow_node.deploy.form.ssh_pre_command.label": "Pre-command (Optional)",
|
||||
"workflow_node.deploy.form.ssh_pre_command.placeholder": "Please enter command to be executed before uploading files",
|
||||
"workflow_node.deploy.form.ssh_post_command.label": "Post-command (Optional)",
|
||||
@ -539,6 +549,8 @@
|
||||
"workflow_node.deploy.form.ssh_preset_scripts.option.sh_backup_files.label": "POSIX Bash - Backup certificate files",
|
||||
"workflow_node.deploy.form.ssh_preset_scripts.option.ps_backup_files.label": "PowerShell - Backup certificate files",
|
||||
"workflow_node.deploy.form.ssh_preset_scripts.option.sh_reload_nginx.label": "POSIX Bash - Reload nginx",
|
||||
"workflow_node.deploy.form.ssh_preset_scripts.option.sh_replace_synologydsm_ssl.label": "POSIX Bash - Replace SynologyDSM SSL certificate",
|
||||
"workflow_node.deploy.form.ssh_preset_scripts.option.sh_replace_fnos_ssl.label": "POSIX Bash - Replace fnOS SSL certificate",
|
||||
"workflow_node.deploy.form.ssh_preset_scripts.option.ps_binding_iis.label": "PowerShell - Binding IIS",
|
||||
"workflow_node.deploy.form.ssh_preset_scripts.option.ps_binding_netsh.label": "PowerShell - Binding netsh",
|
||||
"workflow_node.deploy.form.ssh_preset_scripts.option.ps_binding_rdp.label": "PowerShell - Binding RDP",
|
||||
@ -718,7 +730,7 @@
|
||||
"workflow_node.deploy.form.webhook_data.label": "Webhook data (Optional)",
|
||||
"workflow_node.deploy.form.webhook_data.placeholder": "Please enter Webhook data to override the default value",
|
||||
"workflow_node.deploy.form.webhook_data.tooltip": "Leave it blank to use the default Webhook data provided by the authorization.",
|
||||
"workflow_node.deploy.form.webhook_data.guide": "Supported variables: <br><ol style=\"margin-left: 1.25em; list-style: disc;\"><li><strong>${DOMAIN}</strong>: The primary domain of the certificate (<i>CommonName</i>).</li><li><strong>${DOMAINS}</strong>: The domain list of the certificate (<i>SubjectAltNames</i>).</li><li><strong>${CERTIFICATE}</strong>: The PEM format content of the certificate file.</li><li><strong>${PRIVATE_KEY}</strong>: The PEM format content of the private key file.</li></ol><br>Please visit the authorization management page for addtional notes.",
|
||||
"workflow_node.deploy.form.webhook_data.guide": "Supported variables: <br><ol style=\"margin-left: 1.25em; list-style: disc;\"><li><strong>${DOMAIN}</strong>: The primary domain of the certificate (<i>CommonName</i>).</li><li><strong>${DOMAINS}</strong>: The domain list of the certificate (<i>SubjectAltNames</i>).</li><li><strong>${CERTIFICATE}</strong>: The PEM format content of the certificate file.</li><li><strong>${SERVER_CERTIFICATE}</strong>: The PEM format content of the server certificate file.</li><li><strong>${INTERMEDIA_CERTIFICATE}</strong>: The PEM format content of the intermedia certificate file.</li><li><strong>${PRIVATE_KEY}</strong>: The PEM format content of the private key file.</li></ol><br>Please visit the authorization management page for addtional notes.",
|
||||
"workflow_node.deploy.form.webhook_data.errmsg.json_invalid": "Please enter a valiod JSON string",
|
||||
"workflow_node.deploy.form.strategy_config.label": "Strategy settings",
|
||||
"workflow_node.deploy.form.skip_on_last_succeeded.label": "Repeated deployment",
|
||||
@ -732,10 +744,8 @@
|
||||
"workflow_node.upload.form.domains.placholder": "Please select certificate file",
|
||||
"workflow_node.upload.form.certificate.label": "Certificate (PEM format)",
|
||||
"workflow_node.upload.form.certificate.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----",
|
||||
"workflow_node.upload.form.certificate.button": "Choose file ...",
|
||||
"workflow_node.upload.form.private_key.label": "Private key (PEM format)",
|
||||
"workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----",
|
||||
"workflow_node.upload.form.private_key.button": "Choose file ...",
|
||||
|
||||
"workflow_node.notify.label": "Notification",
|
||||
"workflow_node.notify.form.subject.label": "Subject",
|
||||
|
@ -195,11 +195,15 @@
|
||||
"access.form.godaddy_api_secret.tooltip": "这是什么?请参阅 <a href=\"https://developer.godaddy.com/\" target=\"_blank\">https://developer.godaddy.com/</a>",
|
||||
"access.form.goedge_api_url.label": "GoEdge API URL",
|
||||
"access.form.goedge_api_url.placeholder": "请输入 GoEdge API URL",
|
||||
"access.form.goedge_access_key_id.label": "GoEdge 用户 AccessKeyId",
|
||||
"access.form.goedge_access_key_id.placeholder": "请输入 GoEdge 用户 AccessKeyId",
|
||||
"access.form.goedge_api_role.label": "GoEdge 用户角色",
|
||||
"access.form.goedge_api_role.placeholder": "请选择 GoEdge 用户角色",
|
||||
"access.form.goedge_api_role.option.user.label": "平台用户",
|
||||
"access.form.goedge_api_role.option.admin.label": "系统管理员",
|
||||
"access.form.goedge_access_key_id.label": "GoEdge AccessKeyId",
|
||||
"access.form.goedge_access_key_id.placeholder": "请输入 GoEdge AccessKeyId",
|
||||
"access.form.goedge_access_key_id.tooltip": "这是什么?请参阅 <a href=\"https://goedge.cloud/docs/API/Auth.md\" target=\"_blank\">https://goedge.cloud/docs/API/Auth.md</a>",
|
||||
"access.form.goedge_access_key.label": "GoEdge 用户 AccessKey",
|
||||
"access.form.goedge_access_key.placeholder": "请输入 GoEdge 用户 AccessKey",
|
||||
"access.form.goedge_access_key.label": "GoEdge AccessKey",
|
||||
"access.form.goedge_access_key.placeholder": "请输入 GoEdge AccessKey",
|
||||
"access.form.goedge_access_key.tooltip": "这是什么?请参阅 <a href=\"https://goedge.cloud/docs/API/Auth.md\" target=\"_blank\">https://goedge.cloud/docs/API/Auth.md</a>",
|
||||
"access.form.goedge_allow_insecure_conns.label": "忽略 SSL/TLS 证书错误",
|
||||
"access.form.goedge_allow_insecure_conns.switch.on": "允许",
|
||||
@ -223,8 +227,7 @@
|
||||
"access.form.jdcloud_access_key_secret.placeholder": "请输入京东云 AccessKeySecret",
|
||||
"access.form.jdcloud_access_key_secret.tooltip": "这是什么?请参阅 <a href=\"https://docs.jdcloud.com/cn/account-management/accesskey-management\" target=\"_blank\">https://docs.jdcloud.com/cn/account-management/accesskey-management</a>",
|
||||
"access.form.k8s_kubeconfig.label": "KubeConfig",
|
||||
"access.form.k8s_kubeconfig.placeholder": "请选择 KubeConfig 文件",
|
||||
"access.form.k8s_kubeconfig.upload": "选择文件",
|
||||
"access.form.k8s_kubeconfig.placeholder": "请输入 KubeConfig 文件内容",
|
||||
"access.form.k8s_kubeconfig.tooltip": "这是什么?请参阅 <a href=\"https://kubernetes.io/zh-cn/docs/concepts/configuration/organize-cluster-access-kubeconfig/\" target=\"_blank\">https://kubernetes.io/zh-cn/docs/concepts/configuration/organize-cluster-access-kubeconfig/</a><br><br>为空时,将使用 Pod 的 ServiceAccount 作为凭证。",
|
||||
"access.form.larkbot_webhook_url.label": "飞书群机器人 Webhook 地址",
|
||||
"access.form.larkbot_webhook_url.placeholder": "请输入飞书群机器人 Webhook 地址",
|
||||
@ -308,8 +311,7 @@
|
||||
"access.form.ssh_password.placeholder": "请输入密码",
|
||||
"access.form.ssh_password.tooltip": "使用密码连接到 SSH 时必填。<br>该字段与密钥文件字段二选一,如果同时填写优先使用 SSH 密钥登录。",
|
||||
"access.form.ssh_key.label": "SSH 密钥(可选)",
|
||||
"access.form.ssh_key.placeholder": "请输入 SSH 密钥文件",
|
||||
"access.form.ssh_key.upload": "选择文件",
|
||||
"access.form.ssh_key.placeholder": "请输入 SSH 密钥文件内容",
|
||||
"access.form.ssh_key.tooltip": "使用 SSH 密钥连接到 SSH 时必填。<br>该字段与密码字段二选一,如果同时填写优先使用 SSH 密钥登录。",
|
||||
"access.form.ssh_key_passphrase.label": "SSH 密钥口令(可选)",
|
||||
"access.form.ssh_key_passphrase.placeholder": "请输入 SSH 密钥口令",
|
||||
@ -379,10 +381,10 @@
|
||||
"access.form.webhook_default_data.errmsg.json_invalid": "请输入有效的 JSON 格式字符串",
|
||||
"access.form.webhook_default_data_for_deployment.label": "默认的 Webhook 部署证书回调数据(可选)",
|
||||
"access.form.webhook_default_data_for_deployment.placeholder": "请输入默认的 Webhook 回调数据",
|
||||
"access.form.webhook_default_data_for_deployment.guide": "小贴士:回调数据是一个 JSON 格式的数据。<br><br>其中值支持模板变量,将在被发送到指定的 Webhook URL 时被替换为实际值;其他内容将保持原样。支持的变量:<br><ol style=\"margin-left: 1.25em; list-style: disc;\"><li><strong>${DOMAIN}</strong>:证书的主域名(即 <i>CommonName</i>)。</li><li><strong>${DOMAINS}</strong>:证书的多域名列表(即 <i>SubjectAltNames</i>)。</li><li><strong>${CERTIFICATE}</strong>:证书文件 PEM 格式内容。</li><li><strong>${PRIVATE_KEY}</strong>:私钥文件 PEM 格式内容。</li></ol><br>当请求谓词为 GET 时,回调数据将作为查询参数;否则,回调数据将按照请求标头中 Content-Type 所指示的格式进行编码。支持的格式:<br><ol style=\"margin-left: 1.25em; list-style: disc;\"><li>application/json(默认)。</li><li>application/x-www-form-urlencoded:不支持嵌套数据。</li><li>multipart/form-data:不支持嵌套数据。</li>",
|
||||
"access.form.webhook_default_data_for_deployment.guide": "小贴士:回调数据是一个 JSON 格式的数据。<br><br>其中值支持模板变量,将在被发送到指定的 Webhook URL 时被替换为实际值;其他内容将保持原样。支持的变量:<br><ol style=\"margin-left: 1.25em; list-style: disc;\"><li><strong>${DOMAIN}</strong>:证书的主域名(即 <i>CommonName</i>)。</li><li><strong>${DOMAINS}</strong>:证书的多域名列表(即 <i>SubjectAltNames</i>)。</li><li><strong>${CERTIFICATE}</strong>:证书文件 PEM 格式内容。</li><li><strong>${SERVER_CERTIFICATE}</strong>:证书文件(仅含服务器证书)PEM 格式内容。</li><li><strong>${INTERMEDIA_CERTIFICATE}</strong>:证书文件(仅含中间证书)PEM 格式内容。</li><li><strong>${PRIVATE_KEY}</strong>:私钥文件 PEM 格式内容。</li></ol><br>当请求谓词为 GET 时,回调数据将作为查询参数;否则,回调数据将按照请求标头中 Content-Type 所指示的格式进行编码。支持的格式:<br><ol style=\"margin-left: 1.25em; list-style: disc;\"><li>application/json(默认)。</li><li>application/x-www-form-urlencoded:不支持嵌套数据。</li><li>multipart/form-data:不支持嵌套数据。</li>",
|
||||
"access.form.webhook_default_data_for_notification.label": "默认的 Webhook 推送通知回调数据(可选)",
|
||||
"access.form.webhook_default_data_for_notification.placeholder": "请输入默认的 Webhook 回调数据",
|
||||
"access.form.webhook_default_data_for_notification.guide": "小贴士:回调数据是一个 JSON 格式的数据。<br><br>其中值支持模板变量,将在被发送到指定的 Webhook URL 时被替换为实际值;其他内容将保持原样。支持的变量:<br><ol style=\"margin-left: 1.25em; list-style: disc;\"><li><strong>${DOMAIN}</strong>:证书的主域名(即 <i>CommonName</i>)。</li><li><strong>${SUBJECT}</strong>:通知主题。</li><li><strong>${MESSAGE}</strong>:通知内容。</ol><br>当请求谓词为 GET 时,回调数据将作为查询参数;否则,回调数据将按照请求标头中 Content-Type 所指示的格式进行编码。支持的格式:<br><ol style=\"margin-left: 1.25em; list-style: disc;\"><li>application/json(默认)。</li><li>application/x-www-form-urlencoded:不支持嵌套数据。</li><li>multipart/form-data:不支持嵌套数据。</li>",
|
||||
"access.form.webhook_default_data_for_notification.guide": "小贴士:回调数据是一个 JSON 格式的数据。<br><br>其中值支持模板变量,将在被发送到指定的 Webhook URL 时被替换为实际值;其他内容将保持原样。支持的变量:<br><ol style=\"margin-left: 1.25em; list-style: disc;\"><li><strong>${SUBJECT}</strong>:通知主题。</li><li><strong>${MESSAGE}</strong>:通知内容。</ol><br>当请求谓词为 GET 时,回调数据将作为查询参数;否则,回调数据将按照请求标头中 Content-Type 所指示的格式进行编码。支持的格式:<br><ol style=\"margin-left: 1.25em; list-style: disc;\"><li>application/json(默认)。</li><li>application/x-www-form-urlencoded:不支持嵌套数据。</li><li>multipart/form-data:不支持嵌套数据。</li>",
|
||||
"access.form.webhook_preset_data.button": "使用预设模板",
|
||||
"access.form.webhook_preset_data.option.bark.label": "Bark",
|
||||
"access.form.webhook_preset_data.option.gotify.label": "Gotify",
|
||||
|
@ -11,6 +11,8 @@
|
||||
"common.button.submit": "提交",
|
||||
|
||||
"common.text.copied": "已复制",
|
||||
"common.text.import_from_file": "从文件导入 ……",
|
||||
"common.text.happy_browser": "当前浏览器版本过低,Certimate WebUI 无法正常工作。推荐使用 Google Chrome v119.0 或更高版本的现代浏览器。",
|
||||
"common.text.nodata": "暂无数据",
|
||||
"common.text.operation_confirm": "操作确认",
|
||||
"common.text.operation_succeeded": "操作成功",
|
||||
|
@ -448,9 +448,15 @@
|
||||
"workflow_node.deploy.form.local_cert_path.label": "证书文件保存路径",
|
||||
"workflow_node.deploy.form.local_cert_path.placeholder": "请输入证书文件保存路径",
|
||||
"workflow_node.deploy.form.local_cert_path.tooltip": "注意,路径需包含完整的文件名,而不是仅目录。",
|
||||
"workflow_node.deploy.form.local_key_path.label": "私钥文件保存路径",
|
||||
"workflow_node.deploy.form.local_key_path.placeholder": "请输入私钥文件保存路径",
|
||||
"workflow_node.deploy.form.local_key_path.label": "证书私钥文件保存路径",
|
||||
"workflow_node.deploy.form.local_key_path.placeholder": "请输入证书私钥文件保存路径",
|
||||
"workflow_node.deploy.form.local_key_path.tooltip": "注意,路径需包含完整的文件名,而不是仅目录。",
|
||||
"workflow_node.deploy.form.local_servercert_path.label": "服务器证书文件保存路径(可选)",
|
||||
"workflow_node.deploy.form.local_servercert_path.placeholder": "请输入服务器证书文件保存路径",
|
||||
"workflow_node.deploy.form.local_servercert_path.tooltip": "不填写时将不会保存服务器证书。<br><br>注意,路径需包含完整的文件名,而不是仅目录。",
|
||||
"workflow_node.deploy.form.local_intermediacert_path.label": "中间证书文件保存路径(可选)",
|
||||
"workflow_node.deploy.form.local_intermediacert_path.placeholder": "请输入中间证书文件保存路径",
|
||||
"workflow_node.deploy.form.local_intermediacert_path.tooltip": "不填写时将不会保存服务器证书。<br><br>注意,路径需包含完整的文件名,而不是仅目录。",
|
||||
"workflow_node.deploy.form.local_pfx_password.label": "PFX 导出密码",
|
||||
"workflow_node.deploy.form.local_pfx_password.placeholder": "请输入 PFX 导出密码",
|
||||
"workflow_node.deploy.form.local_pfx_password.tooltip": "这是什么?请参阅 <a href=\"https://learn.microsoft.com/zh-cn/windows-hardware/drivers/install/personal-information-exchange---pfx--files\" target=\"_blank\">https://learn.microsoft.com/zh-cn/windows-hardware/drivers/install/personal-information-exchange---pfx--files</a>",
|
||||
@ -513,10 +519,16 @@
|
||||
"workflow_node.deploy.form.ssh_cert_path.label": "证书文件上传路径",
|
||||
"workflow_node.deploy.form.ssh_cert_path.placeholder": "请输入证书文件上传路径",
|
||||
"workflow_node.deploy.form.ssh_cert_path.tooltip": "注意,路径需包含完整的文件名,而不是仅目录。",
|
||||
"workflow_node.deploy.form.ssh_key_path.label": "私钥文件上传路径",
|
||||
"workflow_node.deploy.form.ssh_key_path.placeholder": "请输入私钥文件上传路径",
|
||||
"workflow_node.deploy.form.ssh_key_path.label": "证书私钥文件上传路径",
|
||||
"workflow_node.deploy.form.ssh_key_path.placeholder": "请输入证书私钥文件上传路径",
|
||||
"workflow_node.deploy.form.ssh_key_path.tooltip": "注意,路径需包含完整的文件名,而不是仅目录。",
|
||||
"workflow_node.deploy.form.ssh_pfx_password.label": "PFX 导出密码",
|
||||
"workflow_node.deploy.form.ssh_servercert_path.label": "服务器证书文件上传路径(可选)",
|
||||
"workflow_node.deploy.form.ssh_servercert_path.placeholder": "请输入服务器证书文件上传路径",
|
||||
"workflow_node.deploy.form.ssh_servercert_path.tooltip": "不填写时将不上传服务器证书。<br><br>注意,路径需包含完整的文件名,而不是仅目录。",
|
||||
"workflow_node.deploy.form.ssh_intermediacert_path.label": "中间证书文件上传路径(可选)",
|
||||
"workflow_node.deploy.form.ssh_intermediacert_path.placeholder": "请输入中间证书文件上传路径",
|
||||
"workflow_node.deploy.form.ssh_intermediacert_path.tooltip": "不填写时将不上传服务器证书。<br><br>注意,路径需包含完整的文件名,而不是仅目录。",
|
||||
"workflow_node.deploy.form.ssh_pfx_password.placeholder": "请输入 PFX 导出密码",
|
||||
"workflow_node.deploy.form.ssh_pfx_password.tooltip": "这是什么?请参阅 <a href=\"https://learn.microsoft.com/zh-cn/windows-hardware/drivers/install/personal-information-exchange---pfx--files\" target=\"_blank\">https://learn.microsoft.com/zh-cn/windows-hardware/drivers/install/personal-information-exchange---pfx--files</a>",
|
||||
"workflow_node.deploy.form.ssh_jks_alias.label": "JKS 别名",
|
||||
@ -528,16 +540,16 @@
|
||||
"workflow_node.deploy.form.ssh_jks_storepass.label": "JKS 密钥库存储口令",
|
||||
"workflow_node.deploy.form.ssh_jks_storepass.placeholder": "请输入 JKS 密钥库存储口令",
|
||||
"workflow_node.deploy.form.ssh_jks_storepass.tooltip": "这是什么?请参阅 <a href=\"https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html\" target=\"_blank\">https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html</a>",
|
||||
"workflow_node.deploy.form.ssh_shell_env.label": "命令执行环境",
|
||||
"workflow_node.deploy.form.ssh_shell_env.value": "POSIX Bash(Linux / macOS)",
|
||||
"workflow_node.deploy.form.ssh_pre_command.label": "前置命令(可选)",
|
||||
"workflow_node.deploy.form.ssh_pre_command.placeholder": "请输入保存文件前执行的命令",
|
||||
"workflow_node.deploy.form.ssh_pre_command.placeholder": "请输入上传文件前执行的命令",
|
||||
"workflow_node.deploy.form.ssh_post_command.label": "后置命令(可选)",
|
||||
"workflow_node.deploy.form.ssh_post_command.placeholder": "请输入保存文件后执行的命令",
|
||||
"workflow_node.deploy.form.ssh_post_command.placeholder": "请输入上传文件后执行的命令",
|
||||
"workflow_node.deploy.form.ssh_preset_scripts.button": "使用预设脚本",
|
||||
"workflow_node.deploy.form.ssh_preset_scripts.option.sh_backup_files.label": "POSIX Bash - 备份原证书文件",
|
||||
"workflow_node.deploy.form.ssh_preset_scripts.option.ps_backup_files.label": "PowerShell - 备份原证书文件",
|
||||
"workflow_node.deploy.form.ssh_preset_scripts.option.sh_reload_nginx.label": "POSIX Bash - 重启 nginx 进程",
|
||||
"workflow_node.deploy.form.ssh_preset_scripts.option.sh_replace_synologydsm_ssl.label": "POSIX Bash - 替换群晖 DSM 证书",
|
||||
"workflow_node.deploy.form.ssh_preset_scripts.option.sh_replace_fnos_ssl.label": "POSIX Bash - 替换飞牛 OS 证书",
|
||||
"workflow_node.deploy.form.ssh_preset_scripts.option.ps_binding_iis.label": "PowerShell - 导入并绑定到 IIS",
|
||||
"workflow_node.deploy.form.ssh_preset_scripts.option.ps_binding_netsh.label": "PowerShell - 导入并绑定到 netsh",
|
||||
"workflow_node.deploy.form.ssh_preset_scripts.option.ps_binding_rdp.label": "PowerShell - 导入并绑定到 RDP",
|
||||
@ -715,9 +727,9 @@
|
||||
"workflow_node.deploy.form.wangsu_cdnpro_webhook_id.placeholder": "请输入网宿云 CDN Pro 部署任务 Webhook ID",
|
||||
"workflow_node.deploy.form.wangsu_cdnpro_webhook_id.tooltip": "这是什么?请参阅 <a href=\"https://cdnpro.console.wangsu.com/v2/index/#/certificate\" target=\"_blank\">https://cdnpro.console.wangsu.com/v2/index/#/certificate</a>",
|
||||
"workflow_node.deploy.form.webhook_data.label": "Webhook 回调数据(可选)",
|
||||
"workflow_node.deploy.form.webhook_data.placeholder": "请输入 Webhook 回调数据",
|
||||
"workflow_node.deploy.form.webhook_data.placeholder": "请输入 Webhook 回调数据以覆盖默认值",
|
||||
"workflow_node.deploy.form.webhook_data.tooltip": "不填写时,将使用所选部署目标授权的默认 Webhook 回调数据。",
|
||||
"workflow_node.deploy.form.webhook_data.guide": "支持的变量:<br><ol style=\"margin-left: 1.25em; list-style: disc;\"><li><strong>${DOMAIN}</strong>:证书的主域名(即 <i>CommonName</i>)。</li><li><strong>${DOMAINS}</strong>:证书的多域名列表(即 <i>SubjectAltNames</i>)。</li><li><strong>${CERTIFICATE}</strong>:证书文件 PEM 格式内容。</li><li><strong>${PRIVATE_KEY}</strong>:私钥文件 PEM 格式内容。</li></ol><br>其他注意事项请前往授权管理页面查看。",
|
||||
"workflow_node.deploy.form.webhook_data.guide": "支持的变量:<br><ol style=\"margin-left: 1.25em; list-style: disc;\"><li><strong>${DOMAIN}</strong>:证书的主域名(即 <i>CommonName</i>)。</li><li><strong>${DOMAINS}</strong>:证书的多域名列表(即 <i>SubjectAltNames</i>)。</li><li><strong>${CERTIFICATE}</strong>:证书文件 PEM 格式内容。</li><li><strong>${SERVER_CERTIFICATE}</strong>:证书文件(仅含服务器证书)PEM 格式内容。</li><li><strong>${INTERMEDIA_CERTIFICATE}</strong>:证书文件(仅含中间证书)PEM 格式内容。</li><li><strong>${PRIVATE_KEY}</strong>:私钥文件 PEM 格式内容。</li></ol><br>其他注意事项请前往授权管理页面查看。",
|
||||
"workflow_node.deploy.form.webhook_data.errmsg.json_invalid": "请输入有效的 JSON 格式字符串",
|
||||
"workflow_node.deploy.form.strategy_config.label": "执行策略",
|
||||
"workflow_node.deploy.form.skip_on_last_succeeded.label": "重复部署",
|
||||
@ -731,10 +743,8 @@
|
||||
"workflow_node.upload.form.domains.placeholder": "上传证书文件后显示",
|
||||
"workflow_node.upload.form.certificate.label": "证书文件(PEM 格式)",
|
||||
"workflow_node.upload.form.certificate.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----",
|
||||
"workflow_node.upload.form.certificate.button": "选择文件",
|
||||
"workflow_node.upload.form.private_key.label": "私钥文件(PEM 格式)",
|
||||
"workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----",
|
||||
"workflow_node.upload.form.private_key.button": "选择文件",
|
||||
|
||||
"workflow_node.notify.label": "推送通知",
|
||||
"workflow_node.notify.form.subject.label": "通知主题",
|
||||
@ -765,7 +775,7 @@
|
||||
"workflow_node.notify.form.webhook_data.label": "Webhook 回调数据(可选)",
|
||||
"workflow_node.notify.form.webhook_data.placeholder": "请输入 Webhook 回调数据以覆盖默认值",
|
||||
"workflow_node.notify.form.webhook_data.tooltip": "不填写时,将使用所选部署目标授权的默认 Webhook 回调数据。",
|
||||
"workflow_node.notify.form.webhook_data.guide": "支持的变量:<br><ol style=\"margin-left: 1.25em; list-style: disc;\"><li><strong>${DOMAIN}</strong>:证书的主域名(即 <i>CommonName</i>)。</li><li><strong>${DOMAINS}</strong>:证书的多域名列表(即 <i>SubjectAltNames</i>)。</li><li><strong>${CERTIFICATE}</strong>:证书文件 PEM 格式内容。</li><li><strong>${PRIVATE_KEY}</strong>:私钥文件 PEM 格式内容。</li></ol><br>其他注意事项请前往授权管理页面查看。",
|
||||
"workflow_node.notify.form.webhook_data.guide": "支持的变量:<br><ol style=\"margin-left: 1.25em; list-style: disc;\"><li><strong>${SUBJECT}</strong>:通知主题。</li><li><strong>${MESSAGE}</strong>:通知内容。</ol><br>其他注意事项请前往授权管理页面查看。",
|
||||
"workflow_node.notify.form.webhook_data.errmsg.json_invalid": "请输入有效的 JSON 格式字符串",
|
||||
|
||||
"workflow_node.end.label": "结束",
|
||||
|
@ -1,10 +1,15 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Navigate, Outlet } from "react-router-dom";
|
||||
import { Layout } from "antd";
|
||||
import { Alert, Layout } from "antd";
|
||||
|
||||
import Show from "@/components/Show";
|
||||
import Version from "@/components/Version";
|
||||
import { getAuthStore } from "@/repository/admin";
|
||||
import { isBrowserHappy } from "@/utils/browser";
|
||||
|
||||
const AuthLayout = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const auth = getAuthStore();
|
||||
if (auth.isValid && auth.isSuperuser) {
|
||||
return <Navigate to="/" />;
|
||||
@ -12,6 +17,10 @@ const AuthLayout = () => {
|
||||
|
||||
return (
|
||||
<Layout className="h-screen">
|
||||
<Show when={!isBrowserHappy()}>
|
||||
<Alert message={t("common.text.happy_browser")} type="warning" showIcon closable />
|
||||
</Show>
|
||||
|
||||
<div className="container">
|
||||
<Outlet />
|
||||
|
||||
|
@ -13,11 +13,13 @@ import {
|
||||
SettingOutlined as SettingOutlinedIcon,
|
||||
SunOutlined as SunOutlinedIcon,
|
||||
} from "@ant-design/icons";
|
||||
import { Button, type ButtonProps, Drawer, Dropdown, Layout, Menu, type MenuProps, Tooltip, theme } from "antd";
|
||||
import { Alert, Button, type ButtonProps, Drawer, Dropdown, Layout, Menu, type MenuProps, Tooltip, theme } from "antd";
|
||||
|
||||
import Show from "@/components/Show";
|
||||
import Version from "@/components/Version";
|
||||
import { useBrowserTheme, useTriggerElement } from "@/hooks";
|
||||
import { getAuthStore } from "@/repository/admin";
|
||||
import { isBrowserHappy } from "@/utils/browser";
|
||||
|
||||
const ConsoleLayout = () => {
|
||||
const navigate = useNavigate();
|
||||
@ -50,6 +52,10 @@ const ConsoleLayout = () => {
|
||||
</Layout.Sider>
|
||||
|
||||
<Layout className="flex flex-col overflow-hidden pl-[256px] max-md:pl-0">
|
||||
<Show when={!isBrowserHappy()}>
|
||||
<Alert message={t("common.text.happy_browser")} type="warning" showIcon closable />
|
||||
</Show>
|
||||
|
||||
<Layout.Header className="p-0 shadow-sm" style={{ background: themeToken.colorBgContainer }}>
|
||||
<div className="flex size-full items-center justify-between overflow-hidden px-4">
|
||||
<div className="flex items-center gap-4">
|
||||
|
3
ui/src/utils/browser.ts
Normal file
3
ui/src/utils/browser.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const isBrowserHappy = () => {
|
||||
return typeof Promise.withResolvers === "function";
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user