feat: support jks format

This commit is contained in:
Fu Diwei 2024-10-28 11:49:44 +08:00
parent 009e8fb976
commit b47a1a13cb
9 changed files with 321 additions and 22 deletions

1
go.mod
View File

@ -19,6 +19,7 @@ require (
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114 github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61 github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61
github.com/nikoksr/notify v1.0.0 github.com/nikoksr/notify v1.0.0
github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0
github.com/pkg/sftp v1.13.6 github.com/pkg/sftp v1.13.6
github.com/pocketbase/dbx v1.10.1 github.com/pocketbase/dbx v1.10.1
github.com/pocketbase/pocketbase v0.22.18 github.com/pocketbase/pocketbase v0.22.18

2
go.sum
View File

@ -398,6 +398,8 @@ github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0 h1:2nosf3P75OZv2/ZO/9Px5ZgZ5gbKrzA3joN1QMfOGMQ=
github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0/go.mod h1:lAVhWwbNaveeJmxrxuSTxMgKpF6DjnuVpn6T8WiBwYQ=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=

View File

@ -1,11 +1,15 @@
package deployer package deployer
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"encoding/pem"
"errors" "errors"
"fmt" "fmt"
"time"
"github.com/pavlo-v-chernykh/keystore-go/v4"
"github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/models"
"software.sslmate.com/src/go-pkcs12" "software.sslmate.com/src/go-pkcs12"
@ -187,7 +191,7 @@ func getDeployVariables(conf domain.DeployConfig) map[string]string {
return rs return rs
} }
func convertPemToPfx(certificate string, privateKey string, password string) ([]byte, error) { func convertPEMToPFX(certificate string, privateKey string, password string) ([]byte, error) {
cert, err := x509.ParseCertificateFromPEM(certificate) cert, err := x509.ParseCertificateFromPEM(certificate)
if err != nil { if err != nil {
return nil, err return nil, err
@ -205,3 +209,38 @@ func convertPemToPfx(certificate string, privateKey string, password string) ([]
return pfxData, nil return pfxData, nil
} }
func convertPEMToJKS(certificate string, privateKey string, alias string, keypass string, storepass string) ([]byte, error) {
certBlock, _ := pem.Decode([]byte(certificate))
if certBlock == nil {
return nil, errors.New("failed to decode certificate PEM")
}
privkeyBlock, _ := pem.Decode([]byte(privateKey))
if privkeyBlock == nil {
return nil, errors.New("failed to decode private key PEM")
}
ks := keystore.New()
entry := keystore.PrivateKeyEntry{
CreationTime: time.Now(),
PrivateKey: privkeyBlock.Bytes,
CertificateChain: []keystore.Certificate{
{
Type: "X509",
Content: certBlock.Bytes,
},
},
}
if err := ks.SetPrivateKeyEntry(alias, entry, []byte(keypass)); err != nil {
return nil, err
}
var buf bytes.Buffer
if err := ks.Store(&buf, []byte(storepass)); err != nil {
return nil, err
}
return buf.Bytes(), nil
}

View File

@ -17,6 +17,18 @@ type LocalDeployer struct {
infos []string infos []string
} }
const (
certFormatPEM = "pem"
certFormatPFX = "pfx"
certFormatJKS = "jks"
)
const (
shellEnvSh = "sh"
shellEnvCmd = "cmd"
shellEnvPowershell = "powershell"
)
func NewLocalDeployer(option *DeployerOption) (Deployer, error) { func NewLocalDeployer(option *DeployerOption) (Deployer, error) {
return &LocalDeployer{ return &LocalDeployer{
option: option, option: option,
@ -50,8 +62,8 @@ func (d *LocalDeployer) Deploy(ctx context.Context) error {
} }
// 写入证书和私钥文件 // 写入证书和私钥文件
switch d.option.DeployConfig.GetConfigOrDefaultAsString("format", "pem") { switch d.option.DeployConfig.GetConfigOrDefaultAsString("format", certFormatPEM) {
case "pem": case certFormatPEM:
if err := fs.WriteFileString(d.option.DeployConfig.GetConfigAsString("certPath"), d.option.Certificate.Certificate); err != nil { if err := fs.WriteFileString(d.option.DeployConfig.GetConfigAsString("certPath"), d.option.Certificate.Certificate); err != nil {
return fmt.Errorf("failed to save certificate file: %w", err) return fmt.Errorf("failed to save certificate file: %w", err)
} }
@ -64,8 +76,12 @@ func (d *LocalDeployer) Deploy(ctx context.Context) error {
d.infos = append(d.infos, toStr("保存私钥成功", nil)) d.infos = append(d.infos, toStr("保存私钥成功", nil))
case "pfx": case certFormatPFX:
pfxData, err := convertPemToPfx(d.option.Certificate.Certificate, d.option.Certificate.PrivateKey, d.option.DeployConfig.GetConfigAsString("pfxPassword")) pfxData, err := convertPEMToPFX(
d.option.Certificate.Certificate,
d.option.Certificate.PrivateKey,
d.option.DeployConfig.GetConfigAsString("pfxPassword"),
)
if err != nil { if err != nil {
return fmt.Errorf("failed to convert pem to pfx %w", err) return fmt.Errorf("failed to convert pem to pfx %w", err)
} }
@ -74,6 +90,24 @@ func (d *LocalDeployer) Deploy(ctx context.Context) error {
return fmt.Errorf("failed to save certificate file: %w", err) return fmt.Errorf("failed to save certificate file: %w", err)
} }
d.infos = append(d.infos, toStr("保存证书成功", nil))
case certFormatJKS:
jksData, err := convertPEMToJKS(
d.option.Certificate.Certificate,
d.option.Certificate.PrivateKey,
d.option.DeployConfig.GetConfigAsString("jksAlias"),
d.option.DeployConfig.GetConfigAsString("jksKeypass"),
d.option.DeployConfig.GetConfigAsString("jksStorepass"),
)
if err != nil {
return fmt.Errorf("failed to convert pem to pfx %w", err)
}
if err := fs.WriteFile(d.option.DeployConfig.GetConfigAsString("certPath"), jksData); err != nil {
return fmt.Errorf("failed to save certificate file: %w", err)
}
d.infos = append(d.infos, toStr("保存证书成功", nil)) d.infos = append(d.infos, toStr("保存证书成功", nil))
} }
@ -95,13 +129,13 @@ func (d *LocalDeployer) execCommand(command string) (string, string, error) {
var cmd *exec.Cmd var cmd *exec.Cmd
switch d.option.DeployConfig.GetConfigAsString("shell") { switch d.option.DeployConfig.GetConfigAsString("shell") {
case "sh": case shellEnvSh:
cmd = exec.Command("sh", "-c", command) cmd = exec.Command("sh", "-c", command)
case "cmd": case shellEnvCmd:
cmd = exec.Command("cmd", "/C", command) cmd = exec.Command("cmd", "/C", command)
case "powershell": case shellEnvPowershell:
cmd = exec.Command("powershell", "-Command", command) cmd = exec.Command("powershell", "-Command", command)
case "": case "":

View File

@ -12,6 +12,7 @@ import (
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/utils/fs"
) )
type SSHDeployer struct { type SSHDeployer struct {
@ -61,8 +62,8 @@ func (d *SSHDeployer) Deploy(ctx context.Context) error {
} }
// 上传证书和私钥文件 // 上传证书和私钥文件
switch d.option.DeployConfig.GetConfigOrDefaultAsString("format", "pem") { switch d.option.DeployConfig.GetConfigOrDefaultAsString("format", certFormatPEM) {
case "pem": case certFormatPEM:
if err := d.writeSftpFileString(client, d.option.DeployConfig.GetConfigAsString("certPath"), d.option.Certificate.Certificate); err != nil { if err := d.writeSftpFileString(client, d.option.DeployConfig.GetConfigAsString("certPath"), d.option.Certificate.Certificate); err != nil {
return fmt.Errorf("failed to upload certificate file: %w", err) return fmt.Errorf("failed to upload certificate file: %w", err)
} }
@ -75,8 +76,12 @@ func (d *SSHDeployer) Deploy(ctx context.Context) error {
d.infos = append(d.infos, toStr("SSH 上传私钥成功", nil)) d.infos = append(d.infos, toStr("SSH 上传私钥成功", nil))
case "pfx": case certFormatPFX:
pfxData, err := convertPemToPfx(d.option.Certificate.Certificate, d.option.Certificate.PrivateKey, d.option.DeployConfig.GetConfigAsString("pfxPassword")) pfxData, err := convertPEMToPFX(
d.option.Certificate.Certificate,
d.option.Certificate.PrivateKey,
d.option.DeployConfig.GetConfigAsString("pfxPassword"),
)
if err != nil { if err != nil {
return fmt.Errorf("failed to convert pem to pfx %w", err) return fmt.Errorf("failed to convert pem to pfx %w", err)
} }
@ -86,6 +91,24 @@ func (d *SSHDeployer) Deploy(ctx context.Context) error {
} }
d.infos = append(d.infos, toStr("SSH 上传证书成功", nil)) d.infos = append(d.infos, toStr("SSH 上传证书成功", nil))
case certFormatJKS:
jksData, err := convertPEMToJKS(
d.option.Certificate.Certificate,
d.option.Certificate.PrivateKey,
d.option.DeployConfig.GetConfigAsString("jksAlias"),
d.option.DeployConfig.GetConfigAsString("jksKeypass"),
d.option.DeployConfig.GetConfigAsString("jksStorepass"),
)
if err != nil {
return fmt.Errorf("failed to convert pem to pfx %w", err)
}
if err := fs.WriteFile(d.option.DeployConfig.GetConfigAsString("certPath"), jksData); err != nil {
return fmt.Errorf("failed to save certificate file: %w", err)
}
d.infos = append(d.infos, toStr("保存证书成功", nil))
} }
// 执行命令 // 执行命令

View File

@ -26,6 +26,9 @@ const DeployToLocal = () => {
certPath: "/etc/nginx/ssl/nginx.crt", certPath: "/etc/nginx/ssl/nginx.crt",
keyPath: "/etc/nginx/ssl/nginx.key", keyPath: "/etc/nginx/ssl/nginx.key",
pfxPassword: "", pfxPassword: "",
jksAlias: "",
jksKeypass: "",
jksStorepass: "",
shell: "sh", shell: "sh",
preCommand: "", preCommand: "",
command: "sudo service nginx reload", command: "sudo service nginx reload",
@ -40,7 +43,7 @@ const DeployToLocal = () => {
const formSchema = z const formSchema = z
.object({ .object({
format: z.union([z.literal("pem"), z.literal("pfx")], { format: z.union([z.literal("pem"), z.literal("pfx"), z.literal("jks")], {
message: t("domain.deployment.form.file_format.placeholder"), message: t("domain.deployment.form.file_format.placeholder"),
}), }),
certPath: z certPath: z
@ -52,6 +55,9 @@ const DeployToLocal = () => {
.min(0, t("domain.deployment.form.file_key_path.placeholder")) .min(0, t("domain.deployment.form.file_key_path.placeholder"))
.max(255, t("common.errmsg.string_max", { max: 255 })), .max(255, t("common.errmsg.string_max", { max: 255 })),
pfxPassword: z.string().optional(), pfxPassword: z.string().optional(),
jksAlias: z.string().optional(),
jksKeypass: z.string().optional(),
jksStorepass: z.string().optional(),
shell: z.union([z.literal("sh"), z.literal("cmd"), z.literal("powershell")], { shell: z.union([z.literal("sh"), z.literal("cmd"), z.literal("powershell")], {
message: t("domain.deployment.form.shell.placeholder"), message: t("domain.deployment.form.shell.placeholder"),
}), }),
@ -65,6 +71,18 @@ const DeployToLocal = () => {
.refine((data) => (data.format === "pfx" ? !!data.pfxPassword?.trim() : true), { .refine((data) => (data.format === "pfx" ? !!data.pfxPassword?.trim() : true), {
message: t("domain.deployment.form.file_pfx_password.placeholder"), message: t("domain.deployment.form.file_pfx_password.placeholder"),
path: ["pfxPassword"], path: ["pfxPassword"],
})
.refine((data) => (data.format === "jks" ? !!data.jksAlias?.trim() : true), {
message: t("domain.deployment.form.file_jks_alias.placeholder"),
path: ["jksAlias"],
})
.refine((data) => (data.format === "jks" ? !!data.jksKeypass?.trim() : true), {
message: t("domain.deployment.form.file_jks_keypass.placeholder"),
path: ["jksKeypass"],
})
.refine((data) => (data.format === "jks" ? !!data.jksStorepass?.trim() : true), {
message: t("domain.deployment.form.file_jks_storepass.placeholder"),
path: ["jksStorepass"],
}); });
useEffect(() => { useEffect(() => {
@ -72,8 +90,13 @@ const DeployToLocal = () => {
if (!res.success) { if (!res.success) {
setError({ setError({
...error, ...error,
format: res.error.errors.find((e) => e.path[0] === "format")?.message,
certPath: res.error.errors.find((e) => e.path[0] === "certPath")?.message, certPath: res.error.errors.find((e) => e.path[0] === "certPath")?.message,
keyPath: res.error.errors.find((e) => e.path[0] === "keyPath")?.message, keyPath: res.error.errors.find((e) => e.path[0] === "keyPath")?.message,
pfxPassword: res.error.errors.find((e) => e.path[0] === "pfxPassword")?.message,
jksAlias: res.error.errors.find((e) => e.path[0] === "jksAlias")?.message,
jksKeypass: res.error.errors.find((e) => e.path[0] === "jksKeypass")?.message,
jksStorepass: res.error.errors.find((e) => e.path[0] === "jksStorepass")?.message,
shell: res.error.errors.find((e) => e.path[0] === "shell")?.message, shell: res.error.errors.find((e) => e.path[0] === "shell")?.message,
preCommand: res.error.errors.find((e) => e.path[0] === "preCommand")?.message, preCommand: res.error.errors.find((e) => e.path[0] === "preCommand")?.message,
command: res.error.errors.find((e) => e.path[0] === "command")?.message, command: res.error.errors.find((e) => e.path[0] === "command")?.message,
@ -81,8 +104,13 @@ const DeployToLocal = () => {
} else { } else {
setError({ setError({
...error, ...error,
format: undefined,
certPath: undefined, certPath: undefined,
keyPath: undefined, keyPath: undefined,
pfxPassword: undefined,
jksAlias: undefined,
jksKeypass: undefined,
jksStorepass: undefined,
shell: undefined, shell: undefined,
preCommand: undefined, preCommand: undefined,
command: undefined, command: undefined,
@ -92,18 +120,26 @@ const DeployToLocal = () => {
useEffect(() => { useEffect(() => {
if (data.config?.format === "pem") { if (data.config?.format === "pem") {
if (data.config.certPath && data.config.certPath.endsWith(".pfx")) { if (/(.pfx|.jks)$/.test(data.config.certPath)) {
const newData = produce(data, (draft) => { const newData = produce(data, (draft) => {
draft.config ??= {}; draft.config ??= {};
draft.config.certPath = data.config!.certPath.replace(/.pfx$/, ".crt"); draft.config.certPath = data.config!.certPath.replace(/(.pfx|.jks)$/, ".crt");
}); });
setDeploy(newData); setDeploy(newData);
} }
} else if (data.config?.format === "pfx") { } else if (data.config?.format === "pfx") {
if (data.config.certPath && data.config.certPath.endsWith(".crt")) { if (/(.crt|.jks)$/.test(data.config.certPath)) {
const newData = produce(data, (draft) => { const newData = produce(data, (draft) => {
draft.config ??= {}; draft.config ??= {};
draft.config.certPath = data.config!.certPath.replace(/.crt$/, ".pfx"); draft.config.certPath = data.config!.certPath.replace(/(.crt|.jks)$/, ".pfx");
});
setDeploy(newData);
}
} else if (data.config?.format === "jks") {
if (/(.crt|.pfx)$/.test(data.config.certPath)) {
const newData = produce(data, (draft) => {
draft.config ??= {};
draft.config.certPath = data.config!.certPath.replace(/(.crt|.pfx)$/, ".jks");
}); });
setDeploy(newData); setDeploy(newData);
} }
@ -194,6 +230,7 @@ Remove-Item -Path "$pfxPath" -Force
<SelectGroup> <SelectGroup>
<SelectItem value="pem">PEM</SelectItem> <SelectItem value="pem">PEM</SelectItem>
<SelectItem value="pfx">PFX</SelectItem> <SelectItem value="pfx">PFX</SelectItem>
<SelectItem value="jks">JKS</SelectItem>
</SelectGroup> </SelectGroup>
</SelectContent> </SelectContent>
</Select> </Select>
@ -259,6 +296,63 @@ Remove-Item -Path "$pfxPath" -Force
<></> <></>
)} )}
{data.config?.format === "jks" ? (
<>
<div>
<Label>{t("domain.deployment.form.file_jks_alias.label")}</Label>
<Input
placeholder={t("domain.deployment.form.file_jks_alias.placeholder")}
className="w-full mt-1"
value={data?.config?.jksAlias}
onChange={(e) => {
const newData = produce(data, (draft) => {
draft.config ??= {};
draft.config.jksAlias = e.target.value?.trim();
});
setDeploy(newData);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.jksAlias}</div>
</div>
<div>
<Label>{t("domain.deployment.form.file_jks_keypass.label")}</Label>
<Input
placeholder={t("domain.deployment.form.file_jks_keypass.placeholder")}
className="w-full mt-1"
value={data?.config?.jksKeypass}
onChange={(e) => {
const newData = produce(data, (draft) => {
draft.config ??= {};
draft.config.jksKeypass = e.target.value?.trim();
});
setDeploy(newData);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.jksKeypass}</div>
</div>
<div>
<Label>{t("domain.deployment.form.file_jks_storepass.label")}</Label>
<Input
placeholder={t("domain.deployment.form.file_jks_storepass.placeholder")}
className="w-full mt-1"
value={data?.config?.jksStorepass}
onChange={(e) => {
const newData = produce(data, (draft) => {
draft.config ??= {};
draft.config.jksStorepass = e.target.value?.trim();
});
setDeploy(newData);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.jksStorepass}</div>
</div>
</>
) : (
<></>
)}
<div> <div>
<Label>{t("domain.deployment.form.shell.label")}</Label> <Label>{t("domain.deployment.form.shell.label")}</Label>
<RadioGroup <RadioGroup

View File

@ -23,6 +23,9 @@ const DeployToSSH = () => {
certPath: "/etc/nginx/ssl/nginx.crt", certPath: "/etc/nginx/ssl/nginx.crt",
keyPath: "/etc/nginx/ssl/nginx.key", keyPath: "/etc/nginx/ssl/nginx.key",
pfxPassword: "", pfxPassword: "",
jksAlias: "",
jksKeypass: "",
jksStorepass: "",
preCommand: "", preCommand: "",
command: "sudo service nginx reload", command: "sudo service nginx reload",
}, },
@ -36,7 +39,7 @@ const DeployToSSH = () => {
const formSchema = z const formSchema = z
.object({ .object({
format: z.union([z.literal("pem"), z.literal("pfx")], { format: z.union([z.literal("pem"), z.literal("pfx"), z.literal("jks")], {
message: t("domain.deployment.form.file_format.placeholder"), message: t("domain.deployment.form.file_format.placeholder"),
}), }),
certPath: z certPath: z
@ -48,6 +51,9 @@ const DeployToSSH = () => {
.min(0, t("domain.deployment.form.file_key_path.placeholder")) .min(0, t("domain.deployment.form.file_key_path.placeholder"))
.max(255, t("common.errmsg.string_max", { max: 255 })), .max(255, t("common.errmsg.string_max", { max: 255 })),
pfxPassword: z.string().optional(), pfxPassword: z.string().optional(),
jksAlias: z.string().optional(),
jksKeypass: z.string().optional(),
jksStorepass: z.string().optional(),
preCommand: z.string().optional(), preCommand: z.string().optional(),
command: z.string().optional(), command: z.string().optional(),
}) })
@ -58,6 +64,18 @@ const DeployToSSH = () => {
.refine((data) => (data.format === "pfx" ? !!data.pfxPassword?.trim() : true), { .refine((data) => (data.format === "pfx" ? !!data.pfxPassword?.trim() : true), {
message: t("domain.deployment.form.file_pfx_password.placeholder"), message: t("domain.deployment.form.file_pfx_password.placeholder"),
path: ["pfxPassword"], path: ["pfxPassword"],
})
.refine((data) => (data.format === "jks" ? !!data.jksAlias?.trim() : true), {
message: t("domain.deployment.form.file_jks_alias.placeholder"),
path: ["jksAlias"],
})
.refine((data) => (data.format === "jks" ? !!data.jksKeypass?.trim() : true), {
message: t("domain.deployment.form.file_jks_keypass.placeholder"),
path: ["jksKeypass"],
})
.refine((data) => (data.format === "jks" ? !!data.jksStorepass?.trim() : true), {
message: t("domain.deployment.form.file_jks_storepass.placeholder"),
path: ["jksStorepass"],
}); });
useEffect(() => { useEffect(() => {
@ -65,16 +83,26 @@ const DeployToSSH = () => {
if (!res.success) { if (!res.success) {
setError({ setError({
...error, ...error,
format: res.error.errors.find((e) => e.path[0] === "format")?.message,
certPath: res.error.errors.find((e) => e.path[0] === "certPath")?.message, certPath: res.error.errors.find((e) => e.path[0] === "certPath")?.message,
keyPath: res.error.errors.find((e) => e.path[0] === "keyPath")?.message, keyPath: res.error.errors.find((e) => e.path[0] === "keyPath")?.message,
pfxPassword: res.error.errors.find((e) => e.path[0] === "pfxPassword")?.message,
jksAlias: res.error.errors.find((e) => e.path[0] === "jksAlias")?.message,
jksKeypass: res.error.errors.find((e) => e.path[0] === "jksKeypass")?.message,
jksStorepass: res.error.errors.find((e) => e.path[0] === "jksStorepass")?.message,
preCommand: res.error.errors.find((e) => e.path[0] === "preCommand")?.message, preCommand: res.error.errors.find((e) => e.path[0] === "preCommand")?.message,
command: res.error.errors.find((e) => e.path[0] === "command")?.message, command: res.error.errors.find((e) => e.path[0] === "command")?.message,
}); });
} else { } else {
setError({ setError({
...error, ...error,
format: undefined,
certPath: undefined, certPath: undefined,
keyPath: undefined, keyPath: undefined,
pfxPassword: undefined,
jksAlias: undefined,
jksKeypass: undefined,
jksStorepass: undefined,
preCommand: undefined, preCommand: undefined,
command: undefined, command: undefined,
}); });
@ -83,18 +111,26 @@ const DeployToSSH = () => {
useEffect(() => { useEffect(() => {
if (data.config?.format === "pem") { if (data.config?.format === "pem") {
if (data.config.certPath && data.config.certPath.endsWith(".pfx")) { if (/(.pfx|.jks)$/.test(data.config.certPath)) {
const newData = produce(data, (draft) => { const newData = produce(data, (draft) => {
draft.config ??= {}; draft.config ??= {};
draft.config.certPath = data.config!.certPath.replace(/.pfx$/, ".crt"); draft.config.certPath = data.config!.certPath.replace(/(.pfx|.jks)$/, ".crt");
}); });
setDeploy(newData); setDeploy(newData);
} }
} else if (data.config?.format === "pfx") { } else if (data.config?.format === "pfx") {
if (data.config.certPath && data.config.certPath.endsWith(".crt")) { if (/(.crt|.jks)$/.test(data.config.certPath)) {
const newData = produce(data, (draft) => { const newData = produce(data, (draft) => {
draft.config ??= {}; draft.config ??= {};
draft.config.certPath = data.config!.certPath.replace(/.crt$/, ".pfx"); draft.config.certPath = data.config!.certPath.replace(/(.crt|.jks)$/, ".pfx");
});
setDeploy(newData);
}
} else if (data.config?.format === "jks") {
if (/(.crt|.pfx)$/.test(data.config.certPath)) {
const newData = produce(data, (draft) => {
draft.config ??= {};
draft.config.certPath = data.config!.certPath.replace(/(.crt|.pfx)$/, ".jks");
}); });
setDeploy(newData); setDeploy(newData);
} }
@ -123,6 +159,7 @@ const DeployToSSH = () => {
<SelectGroup> <SelectGroup>
<SelectItem value="pem">PEM</SelectItem> <SelectItem value="pem">PEM</SelectItem>
<SelectItem value="pfx">PFX</SelectItem> <SelectItem value="pfx">PFX</SelectItem>
<SelectItem value="jks">JKS</SelectItem>
</SelectGroup> </SelectGroup>
</SelectContent> </SelectContent>
</Select> </Select>
@ -188,6 +225,63 @@ const DeployToSSH = () => {
<></> <></>
)} )}
{data.config?.format === "jks" ? (
<>
<div>
<Label>{t("domain.deployment.form.file_jks_alias.label")}</Label>
<Input
placeholder={t("domain.deployment.form.file_jks_alias.placeholder")}
className="w-full mt-1"
value={data?.config?.jksAlias}
onChange={(e) => {
const newData = produce(data, (draft) => {
draft.config ??= {};
draft.config.jksAlias = e.target.value?.trim();
});
setDeploy(newData);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.jksAlias}</div>
</div>
<div>
<Label>{t("domain.deployment.form.file_jks_keypass.label")}</Label>
<Input
placeholder={t("domain.deployment.form.file_jks_keypass.placeholder")}
className="w-full mt-1"
value={data?.config?.jksKeypass}
onChange={(e) => {
const newData = produce(data, (draft) => {
draft.config ??= {};
draft.config.jksKeypass = e.target.value?.trim();
});
setDeploy(newData);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.jksKeypass}</div>
</div>
<div>
<Label>{t("domain.deployment.form.file_jks_storepass.label")}</Label>
<Input
placeholder={t("domain.deployment.form.file_jks_storepass.placeholder")}
className="w-full mt-1"
value={data?.config?.jksStorepass}
onChange={(e) => {
const newData = produce(data, (draft) => {
draft.config ??= {};
draft.config.jksStorepass = e.target.value?.trim();
});
setDeploy(newData);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.jksStorepass}</div>
</div>
</>
) : (
<></>
)}
<div> <div>
<Label>{t("domain.deployment.form.shell_pre_command.label")}</Label> <Label>{t("domain.deployment.form.shell_pre_command.label")}</Label>
<Textarea <Textarea

View File

@ -128,6 +128,12 @@
"domain.deployment.form.file_key_path.placeholder": "Please enter private key save path", "domain.deployment.form.file_key_path.placeholder": "Please enter private key save path",
"domain.deployment.form.file_pfx_password.label": "PFX Output Password", "domain.deployment.form.file_pfx_password.label": "PFX Output Password",
"domain.deployment.form.file_pfx_password.placeholder": "Please enter PFX output password", "domain.deployment.form.file_pfx_password.placeholder": "Please enter PFX output password",
"domain.deployment.form.file_jks_alias.label": "JKS Alias (KeyStore Alias)",
"domain.deployment.form.file_jks_alias.placeholder": "Please enter JKS alias",
"domain.deployment.form.file_jks_keypass.label": "JKS Key Password (KeyStore Keypass)",
"domain.deployment.form.file_jks_keypass.placeholder": "Please enter JKS key password",
"domain.deployment.form.file_jks_storepass.label": "JKS Store Password (KeyStore Storepass)",
"domain.deployment.form.file_jks_storepass.placeholder": "Please enter JKS store password",
"domain.deployment.form.shell.label": "Shell", "domain.deployment.form.shell.label": "Shell",
"domain.deployment.form.shell.placeholder": "Please select shell environment", "domain.deployment.form.shell.placeholder": "Please select shell environment",
"domain.deployment.form.shell.option.sh.label": "POSIX Bash (Linux)", "domain.deployment.form.shell.option.sh.label": "POSIX Bash (Linux)",

View File

@ -128,6 +128,12 @@
"domain.deployment.form.file_key_path.placeholder": "请输入私钥保存路径", "domain.deployment.form.file_key_path.placeholder": "请输入私钥保存路径",
"domain.deployment.form.file_pfx_password.label": "PFX 导出密码", "domain.deployment.form.file_pfx_password.label": "PFX 导出密码",
"domain.deployment.form.file_pfx_password.placeholder": "请输入 PFX 导出密码", "domain.deployment.form.file_pfx_password.placeholder": "请输入 PFX 导出密码",
"domain.deployment.form.file_jks_alias.label": "JKS 别名KeyStore Alias",
"domain.deployment.form.file_jks_alias.placeholder": "请输入 JKS 别名",
"domain.deployment.form.file_jks_keypass.label": "JKS 私钥访问口令KeyStore Keypass",
"domain.deployment.form.file_jks_keypass.placeholder": "请输入 JKS 私钥访问口令",
"domain.deployment.form.file_jks_storepass.label": "JKS 密钥库存储口令KeyStore Storepass",
"domain.deployment.form.file_jks_storepass.placeholder": "请输入 JKS 密钥库存储口令",
"domain.deployment.form.shell.label": "Shell", "domain.deployment.form.shell.label": "Shell",
"domain.deployment.form.shell.placeholder": "请选择命令执行环境", "domain.deployment.form.shell.placeholder": "请选择命令执行环境",
"domain.deployment.form.shell_pre_command.label": "前置命令", "domain.deployment.form.shell_pre_command.label": "前置命令",