保存部署服务器密码认证支持与页面改造前检查点
This commit is contained in:
parent
24b9e7abdc
commit
be6bc63102
@ -9,7 +9,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed migrations/sqlite/0001_initial.sql migrations/sqlite/0002_domain_options.sql migrations/sqlite/0003_deployment.sql migrations/sqlite/0004_task_attempts.sql migrations/sqlite/0005_users.sql migrations/sqlite/0006_server_validation.sql migrations/mysql/0001_initial.sql migrations/mysql/0002_domain_options.sql migrations/mysql/0003_deployment.sql migrations/mysql/0004_task_attempts.sql migrations/mysql/0005_users.sql migrations/mysql/0006_server_validation.sql migrations/postgres/0001_initial.sql migrations/postgres/0002_domain_options.sql migrations/postgres/0003_deployment.sql migrations/postgres/0004_task_attempts.sql migrations/postgres/0005_users.sql migrations/postgres/0006_server_validation.sql
|
||||
//go:embed migrations/sqlite/0001_initial.sql migrations/sqlite/0002_domain_options.sql migrations/sqlite/0003_deployment.sql migrations/sqlite/0004_task_attempts.sql migrations/sqlite/0005_users.sql migrations/sqlite/0006_server_validation.sql migrations/sqlite/0007_server_password_auth.sql migrations/mysql/0001_initial.sql migrations/mysql/0002_domain_options.sql migrations/mysql/0003_deployment.sql migrations/mysql/0004_task_attempts.sql migrations/mysql/0005_users.sql migrations/mysql/0006_server_validation.sql migrations/mysql/0007_server_password_auth.sql migrations/postgres/0001_initial.sql migrations/postgres/0002_domain_options.sql migrations/postgres/0003_deployment.sql migrations/postgres/0004_task_attempts.sql migrations/postgres/0005_users.sql migrations/postgres/0006_server_validation.sql migrations/postgres/0007_server_password_auth.sql
|
||||
var migrationFiles embed.FS
|
||||
|
||||
func Migrate(ctx context.Context, db *sql.DB, driver string) error {
|
||||
@ -38,7 +38,7 @@ func Migrate(ctx context.Context, db *sql.DB, driver string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
const latestMigrationVersion = 6
|
||||
const latestMigrationVersion = 7
|
||||
|
||||
func applyMigration(ctx context.Context, db *sql.DB, driver string, version int) error {
|
||||
path := fmt.Sprintf("migrations/%s/%04d_%s.sql", driver, version, migrationName(version))
|
||||
@ -131,6 +131,8 @@ func migrationName(version int) string {
|
||||
return "users"
|
||||
case 6:
|
||||
return "server_validation"
|
||||
case 7:
|
||||
return "server_password_auth"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
ALTER TABLE servers ADD COLUMN auth_method VARCHAR(32) NOT NULL DEFAULT 'private_key';
|
||||
ALTER TABLE servers ADD COLUMN password_ref VARCHAR(255) NOT NULL DEFAULT '';
|
||||
@ -0,0 +1,2 @@
|
||||
ALTER TABLE servers ADD COLUMN auth_method TEXT NOT NULL DEFAULT 'private_key';
|
||||
ALTER TABLE servers ADD COLUMN password_ref TEXT NOT NULL DEFAULT '';
|
||||
@ -0,0 +1,2 @@
|
||||
ALTER TABLE servers ADD COLUMN auth_method TEXT NOT NULL DEFAULT 'private_key';
|
||||
ALTER TABLE servers ADD COLUMN password_ref TEXT NOT NULL DEFAULT '';
|
||||
@ -12,6 +12,13 @@ import (
|
||||
|
||||
type WebServer string
|
||||
|
||||
type SSHAuthMethod string
|
||||
|
||||
const (
|
||||
SSHAuthPrivateKey SSHAuthMethod = "private_key"
|
||||
SSHAuthPassword SSHAuthMethod = "password"
|
||||
)
|
||||
|
||||
const (
|
||||
WebServerNginx WebServer = "nginx"
|
||||
WebServerCaddy WebServer = "caddy"
|
||||
@ -28,21 +35,24 @@ const (
|
||||
var serverPathPlaceholderPattern = regexp.MustCompile(`\{[^{}]+\}`)
|
||||
|
||||
type Server struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Address string `json:"address"`
|
||||
Port int `json:"port"`
|
||||
Username string `json:"username"`
|
||||
PrivateKeyRef string `json:"private_key_ref"`
|
||||
HostKeyFingerprint string `json:"host_key_fingerprint"`
|
||||
CertificatePath string `json:"certificate_path"`
|
||||
PrivateKeyPath string `json:"private_key_path"`
|
||||
CertificateChainPath string `json:"certificate_chain_path"`
|
||||
WebServer WebServer `json:"web_server"`
|
||||
ValidationCommand []string `json:"validation_command,omitempty"`
|
||||
ReloadCommand []string `json:"reload_command,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Address string `json:"address"`
|
||||
Port int `json:"port"`
|
||||
Username string `json:"username"`
|
||||
AuthMethod SSHAuthMethod `json:"auth_method"`
|
||||
PrivateKeyRef string `json:"private_key_ref"`
|
||||
PasswordRef string `json:"-"`
|
||||
PasswordConfigured bool `json:"password_configured"`
|
||||
HostKeyFingerprint string `json:"host_key_fingerprint"`
|
||||
CertificatePath string `json:"certificate_path"`
|
||||
PrivateKeyPath string `json:"private_key_path"`
|
||||
CertificateChainPath string `json:"certificate_chain_path"`
|
||||
WebServer WebServer `json:"web_server"`
|
||||
ValidationCommand []string `json:"validation_command,omitempty"`
|
||||
ReloadCommand []string `json:"reload_command,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (s Server) Normalize() Server {
|
||||
@ -50,7 +60,17 @@ func (s Server) Normalize() Server {
|
||||
s.Name = strings.TrimSpace(s.Name)
|
||||
s.Address = strings.TrimSpace(s.Address)
|
||||
s.Username = strings.TrimSpace(s.Username)
|
||||
s.AuthMethod = SSHAuthMethod(strings.ToLower(strings.TrimSpace(string(s.AuthMethod))))
|
||||
s.PrivateKeyRef = strings.TrimSpace(s.PrivateKeyRef)
|
||||
s.PasswordRef = strings.TrimSpace(s.PasswordRef)
|
||||
s.PasswordConfigured = s.PasswordRef != ""
|
||||
if s.AuthMethod == "" {
|
||||
if s.PasswordRef != "" {
|
||||
s.AuthMethod = SSHAuthPassword
|
||||
} else {
|
||||
s.AuthMethod = SSHAuthPrivateKey
|
||||
}
|
||||
}
|
||||
s.HostKeyFingerprint = strings.TrimSpace(s.HostKeyFingerprint)
|
||||
s.CertificatePath = strings.TrimSpace(s.CertificatePath)
|
||||
s.PrivateKeyPath = strings.TrimSpace(s.PrivateKeyPath)
|
||||
@ -164,8 +184,17 @@ func (s Server) Validate() error {
|
||||
if s.Username == "" {
|
||||
return errors.New("SSH 用户不能为空")
|
||||
}
|
||||
if s.PrivateKeyRef == "" {
|
||||
return errors.New("SSH 私钥引用不能为空")
|
||||
switch s.AuthMethod {
|
||||
case SSHAuthPrivateKey:
|
||||
if s.PrivateKeyRef == "" {
|
||||
return errors.New("SSH \u79c1\u94a5\u5f15\u7528\u4e0d\u80fd\u4e3a\u7a7a")
|
||||
}
|
||||
case SSHAuthPassword:
|
||||
if s.PasswordRef == "" {
|
||||
return errors.New("SSH \u5bc6\u7801\u5f15\u7528\u4e0d\u80fd\u4e3a\u7a7a")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("SSH \u8ba4\u8bc1\u65b9\u5f0f\u65e0\u6548\uff1a%s", s.AuthMethod)
|
||||
}
|
||||
if s.HostKeyFingerprint == "" {
|
||||
return errors.New("SSH 主机指纹不能为空")
|
||||
|
||||
@ -33,23 +33,37 @@ func (f SSHFactory) Connect(ctx context.Context, server Server) (Client, error)
|
||||
if f.Secrets == nil {
|
||||
return nil, errors.New("SSH 敏感信息存储未初始化")
|
||||
}
|
||||
secrets, err := f.Secrets.Get(ctx, server.PrivateKeyRef)
|
||||
secretReference := server.PrivateKeyRef
|
||||
if server.AuthMethod == SSHAuthPassword {
|
||||
secretReference = server.PasswordRef
|
||||
}
|
||||
secrets, err := f.Secrets.Get(ctx, secretReference)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取 SSH 私钥失败:%w", err)
|
||||
return nil, fmt.Errorf("读取 SSH 认证信息失败:%w", err)
|
||||
}
|
||||
privateKey := strings.TrimSpace(secrets["private_key"])
|
||||
if privateKey == "" {
|
||||
return nil, errors.New("SSH 私钥内容不能为空")
|
||||
}
|
||||
var signer ssh.Signer
|
||||
passphrase := secrets["passphrase"]
|
||||
if passphrase != "" {
|
||||
signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(privateKey), []byte(passphrase))
|
||||
var authMethods []ssh.AuthMethod
|
||||
if server.AuthMethod == SSHAuthPassword {
|
||||
password := secrets["password"]
|
||||
if password == "" {
|
||||
return nil, errors.New("SSH 密码不能为空")
|
||||
}
|
||||
authMethods = []ssh.AuthMethod{ssh.Password(password)}
|
||||
} else {
|
||||
signer, err = ssh.ParsePrivateKey([]byte(privateKey))
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.New("解析 SSH 私钥失败:私钥格式或口令无效")
|
||||
privateKey := strings.TrimSpace(secrets["private_key"])
|
||||
if privateKey == "" {
|
||||
return nil, errors.New("SSH 私钥内容不能为空")
|
||||
}
|
||||
var signer ssh.Signer
|
||||
passphrase := secrets["passphrase"]
|
||||
if passphrase != "" {
|
||||
signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(privateKey), []byte(passphrase))
|
||||
} else {
|
||||
signer, err = ssh.ParsePrivateKey([]byte(privateKey))
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.New("解析 SSH 私钥失败:私钥格式或口令无效")
|
||||
}
|
||||
authMethods = []ssh.AuthMethod{ssh.PublicKeys(signer)}
|
||||
}
|
||||
timeout := f.Timeout
|
||||
if timeout <= 0 {
|
||||
@ -61,7 +75,7 @@ func (f SSHFactory) Connect(ctx context.Context, server Server) (Client, error)
|
||||
}
|
||||
timeoutContext, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
sshClient, err := connectSSHClient(timeoutContext, server, signer, algorithms)
|
||||
sshClient, err := connectSSHClient(timeoutContext, server, authMethods, algorithms)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -108,7 +122,7 @@ func (f SSHFactory) secureHostKeyAlgorithms() ([]string, error) {
|
||||
func connectSSHClient(
|
||||
ctx context.Context,
|
||||
server Server,
|
||||
signer ssh.Signer,
|
||||
authMethods []ssh.AuthMethod,
|
||||
algorithms []string,
|
||||
) (*ssh.Client, error) {
|
||||
address := net.JoinHostPort(server.Address, fmt.Sprintf("%d", server.Port))
|
||||
@ -131,7 +145,7 @@ func connectSSHClient(
|
||||
hostKeyMismatch := ""
|
||||
config := &ssh.ClientConfig{
|
||||
User: server.Username,
|
||||
Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
|
||||
Auth: authMethods,
|
||||
HostKeyAlgorithms: []string{algorithm},
|
||||
HostKeyCallback: func(_ string, _ net.Addr, key ssh.PublicKey) error {
|
||||
fingerprint := ssh.FingerprintSHA256(key)
|
||||
|
||||
@ -377,7 +377,9 @@ type serverRequest struct {
|
||||
Address string `json:"address" binding:"required"`
|
||||
Port int `json:"port"`
|
||||
Username string `json:"username" binding:"required"`
|
||||
PrivateKeyRef string `json:"private_key_ref" binding:"required"`
|
||||
AuthMethod string `json:"auth_method"`
|
||||
PrivateKeyRef string `json:"private_key_ref"`
|
||||
Password string `json:"password"`
|
||||
HostKeyFingerprint string `json:"host_key_fingerprint" binding:"required"`
|
||||
CertificatePath string `json:"certificate_path"`
|
||||
PrivateKeyPath string `json:"private_key_path"`
|
||||
@ -700,11 +702,13 @@ func (s *Server) createServer(c *gin.Context) {
|
||||
now := time.Now().UTC()
|
||||
item.CreatedAt = now
|
||||
item.UpdatedAt = now
|
||||
if err := s.validateSSHKeyReference(c.Request.Context(), item.PrivateKeyRef); err != nil {
|
||||
s.writeError(c, http.StatusBadRequest, "SSH 私钥引用无效", err)
|
||||
item, cleanup, err := s.prepareServerCredential(c.Request.Context(), item, request.Password, nil)
|
||||
if err != nil {
|
||||
s.writeError(c, http.StatusBadRequest, "SSH 认证信息无效", err)
|
||||
return
|
||||
}
|
||||
if err := s.servers.CreateServer(c.Request.Context(), item); err != nil {
|
||||
cleanup()
|
||||
s.writeError(c, serverErrorStatus(err), "保存服务器失败", err)
|
||||
return
|
||||
}
|
||||
@ -735,14 +739,21 @@ func (s *Server) updateServer(c *gin.Context) {
|
||||
}
|
||||
item.CreatedAt = existing.CreatedAt
|
||||
item.UpdatedAt = time.Now().UTC()
|
||||
if err := s.validateSSHKeyReference(c.Request.Context(), item.PrivateKeyRef); err != nil {
|
||||
s.writeError(c, http.StatusBadRequest, "SSH 私钥引用无效", err)
|
||||
item, cleanup, err := s.prepareServerCredential(c.Request.Context(), item, request.Password, &existing)
|
||||
if err != nil {
|
||||
s.writeError(c, http.StatusBadRequest, "SSH 认证信息无效", err)
|
||||
return
|
||||
}
|
||||
if err := s.servers.UpdateServer(c.Request.Context(), item); err != nil {
|
||||
cleanup()
|
||||
s.writeError(c, serverErrorStatus(err), "更新服务器失败", err)
|
||||
return
|
||||
}
|
||||
if existing.PasswordRef != "" && existing.PasswordRef != item.PasswordRef && s.secrets != nil {
|
||||
if err := s.secrets.Delete(c.Request.Context(), existing.PasswordRef); err != nil && s.logger != nil {
|
||||
s.logger.Warn("删除旧 SSH 密码失败", "error", err)
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
@ -754,11 +765,52 @@ func (s *Server) validateSSHKeyReference(ctx context.Context, reference string)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Server) prepareServerCredential(ctx context.Context, item deployment.Server, password string, existing *deployment.Server) (deployment.Server, func(), error) {
|
||||
item = item.Normalize()
|
||||
cleanup := func() {}
|
||||
if item.AuthMethod == deployment.SSHAuthPrivateKey {
|
||||
if err := s.validateSSHKeyReference(ctx, item.PrivateKeyRef); err != nil {
|
||||
return deployment.Server{}, cleanup, err
|
||||
}
|
||||
return item, cleanup, nil
|
||||
}
|
||||
if item.AuthMethod != deployment.SSHAuthPassword {
|
||||
return deployment.Server{}, cleanup, errors.New("SSH \u8ba4\u8bc1\u65b9\u5f0f\u65e0\u6548")
|
||||
}
|
||||
if s.secrets == nil {
|
||||
return deployment.Server{}, cleanup, errors.New("SSH \u5bc6\u7801\u5b58\u50a8\u672a\u521d\u59cb\u5316")
|
||||
}
|
||||
if password == "" && existing != nil && existing.PasswordRef != "" {
|
||||
item.PasswordRef = existing.PasswordRef
|
||||
return item, cleanup, nil
|
||||
}
|
||||
if password == "" {
|
||||
return deployment.Server{}, cleanup, errors.New("SSH \u5bc6\u7801\u4e0d\u80fd\u4e3a\u7a7a")
|
||||
}
|
||||
item.PasswordRef = "secret://ssh/password/" + domain.NewID()
|
||||
if err := s.secrets.Put(ctx, item.PasswordRef, map[string]string{"kind": "ssh_password", "password": password}); err != nil {
|
||||
return deployment.Server{}, cleanup, fmt.Errorf("\u4fdd\u5b58 SSH \u5bc6\u7801\u5931\u8d25\uff1a%w", err)
|
||||
}
|
||||
cleanup = func() {
|
||||
_ = s.secrets.Delete(context.Background(), item.PasswordRef)
|
||||
}
|
||||
return item, cleanup, nil
|
||||
}
|
||||
|
||||
func (s *Server) deleteServer(c *gin.Context) {
|
||||
if s.servers == nil {
|
||||
s.writeError(c, http.StatusNotImplemented, "服务器管理接口尚未配置", errors.New("服务器仓储未配置"))
|
||||
return
|
||||
}
|
||||
item, err := s.servers.GetServerByID(c.Request.Context(), c.Param("id"))
|
||||
if err != nil {
|
||||
if repository.IsNotFound(err) {
|
||||
s.writeError(c, http.StatusNotFound, "服务器不存在", err)
|
||||
return
|
||||
}
|
||||
s.writeError(c, http.StatusInternalServerError, "读取服务器失败", err)
|
||||
return
|
||||
}
|
||||
if err := s.servers.DeleteServer(c.Request.Context(), c.Param("id")); err != nil {
|
||||
if repository.IsNotFound(err) {
|
||||
s.writeError(c, http.StatusNotFound, "服务器不存在", err)
|
||||
@ -767,6 +819,11 @@ func (s *Server) deleteServer(c *gin.Context) {
|
||||
s.writeError(c, http.StatusInternalServerError, "删除服务器失败", err)
|
||||
return
|
||||
}
|
||||
if item.PasswordRef != "" && s.secrets != nil {
|
||||
if err := s.secrets.Delete(c.Request.Context(), item.PasswordRef); err != nil && s.logger != nil {
|
||||
s.logger.Warn("删除 SSH 密码失败", "error", err)
|
||||
}
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
@ -830,7 +887,7 @@ func (s *Server) replaceDomainServers(c *gin.Context) {
|
||||
func serverFromRequest(request serverRequest) deployment.Server {
|
||||
return deployment.Server{
|
||||
ID: domain.NewID(), Name: request.Name, Address: request.Address, Port: request.Port,
|
||||
Username: request.Username, PrivateKeyRef: request.PrivateKeyRef, HostKeyFingerprint: request.HostKeyFingerprint,
|
||||
Username: request.Username, AuthMethod: deployment.SSHAuthMethod(request.AuthMethod), PrivateKeyRef: request.PrivateKeyRef, HostKeyFingerprint: request.HostKeyFingerprint,
|
||||
CertificatePath: request.CertificatePath, PrivateKeyPath: request.PrivateKeyPath,
|
||||
CertificateChainPath: request.CertificateChainPath, WebServer: deployment.WebServer(request.WebServer),
|
||||
ValidationCommand: request.ValidationCommand, ReloadCommand: request.ReloadCommand,
|
||||
|
||||
@ -58,8 +58,8 @@ func (r *SQLServerRepository) CreateServer(ctx context.Context, server deploymen
|
||||
updatedAt = createdAt
|
||||
}
|
||||
_, err = r.db.ExecContext(ctx, r.query(`INSERT INTO servers
|
||||
(id, name, address, port, username, private_key_ref, host_key_fingerprint, certificate_path, private_key_path, certificate_chain_path, web_server, validation_command_json, reload_command_json, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`), server.ID, server.Name, server.Address, server.Port, server.Username, server.PrivateKeyRef,
|
||||
(id, name, address, port, username, auth_method, private_key_ref, password_ref, host_key_fingerprint, certificate_path, private_key_path, certificate_chain_path, web_server, validation_command_json, reload_command_json, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`), server.ID, server.Name, server.Address, server.Port, server.Username, server.AuthMethod, server.PrivateKeyRef, server.PasswordRef,
|
||||
server.HostKeyFingerprint, server.CertificatePath, server.PrivateKeyPath, server.CertificateChainPath, server.WebServer, string(validationCommand), string(command), createdAt, updatedAt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存服务器失败:%w", err)
|
||||
@ -84,8 +84,8 @@ func (r *SQLServerRepository) UpdateServer(ctx context.Context, server deploymen
|
||||
if updatedAt.IsZero() {
|
||||
updatedAt = time.Now().UTC()
|
||||
}
|
||||
result, err := r.db.ExecContext(ctx, r.query(`UPDATE servers SET name = ?, address = ?, port = ?, username = ?, private_key_ref = ?, host_key_fingerprint = ?, certificate_path = ?, private_key_path = ?, certificate_chain_path = ?, web_server = ?, validation_command_json = ?, reload_command_json = ?, updated_at = ? WHERE id = ?`),
|
||||
server.Name, server.Address, server.Port, server.Username, server.PrivateKeyRef, server.HostKeyFingerprint, server.CertificatePath, server.PrivateKeyPath, server.CertificateChainPath, server.WebServer, string(validationCommand), string(command), updatedAt, server.ID)
|
||||
result, err := r.db.ExecContext(ctx, r.query(`UPDATE servers SET name = ?, address = ?, port = ?, username = ?, auth_method = ?, private_key_ref = ?, password_ref = ?, host_key_fingerprint = ?, certificate_path = ?, private_key_path = ?, certificate_chain_path = ?, web_server = ?, validation_command_json = ?, reload_command_json = ?, updated_at = ? WHERE id = ?`),
|
||||
server.Name, server.Address, server.Port, server.Username, server.AuthMethod, server.PrivateKeyRef, server.PasswordRef, server.HostKeyFingerprint, server.CertificatePath, server.PrivateKeyPath, server.CertificateChainPath, server.WebServer, string(validationCommand), string(command), updatedAt, server.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新服务器失败:%w", err)
|
||||
}
|
||||
@ -235,8 +235,8 @@ func (r *SQLServerRepository) ReplaceDomainServers(ctx context.Context, domainID
|
||||
return nil
|
||||
}
|
||||
|
||||
const serverSelect = `SELECT id, name, address, port, username, private_key_ref, host_key_fingerprint, certificate_path, private_key_path, certificate_chain_path, web_server, validation_command_json, reload_command_json, created_at, updated_at FROM servers`
|
||||
const serverSelectQualified = `SELECT servers.id, servers.name, servers.address, servers.port, servers.username, servers.private_key_ref, servers.host_key_fingerprint, servers.certificate_path, servers.private_key_path, servers.certificate_chain_path, servers.web_server, servers.validation_command_json, servers.reload_command_json, servers.created_at, servers.updated_at FROM servers`
|
||||
const serverSelect = `SELECT id, name, address, port, username, auth_method, private_key_ref, password_ref, host_key_fingerprint, certificate_path, private_key_path, certificate_chain_path, web_server, validation_command_json, reload_command_json, created_at, updated_at FROM servers`
|
||||
const serverSelectQualified = `SELECT servers.id, servers.name, servers.address, servers.port, servers.username, servers.auth_method, servers.private_key_ref, servers.password_ref, servers.host_key_fingerprint, servers.certificate_path, servers.private_key_path, servers.certificate_chain_path, servers.web_server, servers.validation_command_json, servers.reload_command_json, servers.created_at, servers.updated_at FROM servers`
|
||||
|
||||
type rowScanner interface {
|
||||
Scan(dest ...any) error
|
||||
@ -245,14 +245,16 @@ type rowScanner interface {
|
||||
func scanServer(row rowScanner) (deployment.Server, error) {
|
||||
var server deployment.Server
|
||||
var webServer string
|
||||
var authMethod string
|
||||
var validationCommandJSON string
|
||||
var commandJSON string
|
||||
if err := row.Scan(&server.ID, &server.Name, &server.Address, &server.Port, &server.Username, &server.PrivateKeyRef, &server.HostKeyFingerprint, &server.CertificatePath, &server.PrivateKeyPath, &server.CertificateChainPath, &webServer, &validationCommandJSON, &commandJSON, &server.CreatedAt, &server.UpdatedAt); err != nil {
|
||||
if err := row.Scan(&server.ID, &server.Name, &server.Address, &server.Port, &server.Username, &authMethod, &server.PrivateKeyRef, &server.PasswordRef, &server.HostKeyFingerprint, &server.CertificatePath, &server.PrivateKeyPath, &server.CertificateChainPath, &webServer, &validationCommandJSON, &commandJSON, &server.CreatedAt, &server.UpdatedAt); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return deployment.Server{}, sql.ErrNoRows
|
||||
}
|
||||
return deployment.Server{}, fmt.Errorf("读取服务器失败:%w", err)
|
||||
}
|
||||
server.AuthMethod = deployment.SSHAuthMethod(authMethod)
|
||||
server.WebServer = deployment.WebServer(webServer)
|
||||
if err := json.Unmarshal([]byte(validationCommandJSON), &server.ValidationCommand); err != nil {
|
||||
return deployment.Server{}, fmt.Errorf("解析配置校验命令失败:%w", err)
|
||||
|
||||
@ -336,7 +336,10 @@ export function useDashboard() {
|
||||
address: "",
|
||||
port: 22,
|
||||
username: "",
|
||||
auth_method: "password",
|
||||
private_key_ref: "",
|
||||
password: "",
|
||||
passwordConfigured: false,
|
||||
host_key_fingerprint: "",
|
||||
certificate_path: defaultCertificatePath,
|
||||
private_key_path: defaultPrivateKeyPath,
|
||||
@ -529,6 +532,7 @@ export function useDashboard() {
|
||||
serverEditingId.value = item.id;
|
||||
serverForm.value = {
|
||||
...item,
|
||||
password: "",
|
||||
validation_command: (item.validation_command || []).join("\n"),
|
||||
reload_command: (item.reload_command || []).join("\n"),
|
||||
server_ids: [],
|
||||
@ -643,6 +647,7 @@ export function useDashboard() {
|
||||
address: serverForm.value.address,
|
||||
port: Number(serverForm.value.port),
|
||||
username: serverForm.value.username,
|
||||
auth_method: serverForm.value.auth_method,
|
||||
private_key_ref: serverForm.value.private_key_ref,
|
||||
host_key_fingerprint: serverForm.value.host_key_fingerprint,
|
||||
certificate_path: serverForm.value.certificate_path,
|
||||
@ -652,6 +657,9 @@ export function useDashboard() {
|
||||
validation_command: parseCommand(serverForm.value.validation_command),
|
||||
reload_command: parseCommand(serverForm.value.reload_command),
|
||||
};
|
||||
if (serverForm.value.auth_method === "password") {
|
||||
payload.password = serverForm.value.password;
|
||||
}
|
||||
const path = serverEditingId.value
|
||||
? `/api/v1/servers/${serverEditingId.value}`
|
||||
: "/api/v1/servers";
|
||||
|
||||
@ -89,6 +89,25 @@ const {
|
||||
required
|
||||
type="text" /></label
|
||||
><label class="field"
|
||||
><span>认证方式</span
|
||||
><select v-model="serverForm.auth_method">
|
||||
<option value="password">SSH 密码</option>
|
||||
<option value="private_key">SSH 私钥</option>
|
||||
</select></label
|
||||
>
|
||||
</div>
|
||||
<label v-if="serverForm.auth_method === 'password'" class="field">
|
||||
<span>SSH 密码</span>
|
||||
<input
|
||||
v-model="serverForm.password"
|
||||
type="password"
|
||||
:required="!serverEditingId || !serverForm.passwordConfigured"
|
||||
autocomplete="new-password"
|
||||
placeholder="编辑时留空表示保留原密码"
|
||||
/>
|
||||
</label>
|
||||
<div v-else class="form-grid">
|
||||
<label class="field"
|
||||
><span class="field-label-row"
|
||||
><span>SSH 私钥</span
|
||||
><button
|
||||
|
||||
Loading…
Reference in New Issue
Block a user