fix: master key deploy now logged in deployment history, add flash feedback after deploy
This commit is contained in:
+3
-1
@@ -77,7 +77,9 @@ The key is deployed to all servers in the group sequentially.
|
||||
|
||||
### Deployment History
|
||||
|
||||
The deploy page shows the last 50 deployment results with status (success/failed) and error messages.
|
||||
The deploy page shows the last 50 deployment results with status (success/failed) and error messages. Master key deployments are included in the history as **[MASTER] System Master Key**.
|
||||
|
||||
After each deployment (single host or group), a flash message is displayed at the top of the page indicating success or failure.
|
||||
|
||||
## Access Assignments
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ func (d *DB) ExportAll() (*BackupData, error) {
|
||||
{`SELECT id, user_id, name, hostname, port, username, description, created_at, updated_at FROM servers ORDER BY id`, &backup.Servers},
|
||||
{`SELECT id, user_id, name, description, created_at, updated_at FROM server_groups ORDER BY id`, &backup.ServerGroups},
|
||||
{`SELECT id, group_id, server_id FROM server_group_members ORDER BY id`, &backup.GroupMembers},
|
||||
{`SELECT id, ssh_key_id, server_id, deployed_at, status, message FROM key_deployments ORDER BY id`, &backup.KeyDeployments},
|
||||
{`SELECT id, ssh_key_id, server_id, deployed_at, status, message, key_name FROM key_deployments ORDER BY id`, &backup.KeyDeployments},
|
||||
{`SELECT id, user_id, action, details, ip_address, created_at FROM audit_log ORDER BY id`, &backup.AuditLog},
|
||||
{`SELECT key, value, updated_at FROM settings ORDER BY key`, &backup.Settings},
|
||||
{`SELECT id, user_id, ssh_key_id, server_id, group_id, system_user, desired_state, sudo, create_user, initial_password, status, last_sync_at, created_at, updated_at FROM access_assignments ORDER BY id`, &backup.AccessAssign},
|
||||
@@ -115,7 +115,7 @@ func (d *DB) ImportAll(backup *BackupData) error {
|
||||
{"servers", []string{"id", "user_id", "name", "hostname", "port", "username", "description", "created_at", "updated_at"}, backup.Servers},
|
||||
{"server_groups", []string{"id", "user_id", "name", "description", "created_at", "updated_at"}, backup.ServerGroups},
|
||||
{"server_group_members", []string{"id", "group_id", "server_id"}, backup.GroupMembers},
|
||||
{"key_deployments", []string{"id", "ssh_key_id", "server_id", "deployed_at", "status", "message"}, backup.KeyDeployments},
|
||||
{"key_deployments", []string{"id", "ssh_key_id", "server_id", "deployed_at", "status", "message", "key_name"}, backup.KeyDeployments},
|
||||
{"audit_log", []string{"id", "user_id", "action", "details", "ip_address", "created_at"}, backup.AuditLog},
|
||||
{"settings", []string{"key", "value", "updated_at"}, backup.Settings},
|
||||
{"access_assignments", []string{"id", "user_id", "ssh_key_id", "server_id", "group_id", "system_user", "desired_state", "sudo", "create_user", "initial_password", "status", "last_sync_at", "created_at", "updated_at"}, backup.AccessAssign},
|
||||
@@ -145,7 +145,12 @@ func (d *DB) ImportAll(backup *BackupData) error {
|
||||
for _, row := range imp.data {
|
||||
args := make([]interface{}, len(imp.columns))
|
||||
for i, col := range imp.columns {
|
||||
args[i] = row[col]
|
||||
v := row[col]
|
||||
// For columns with NOT NULL DEFAULT '', treat nil as empty string
|
||||
if v == nil && col == "key_name" {
|
||||
v = ""
|
||||
}
|
||||
args[i] = v
|
||||
}
|
||||
if _, err := stmt.Exec(args...); err != nil {
|
||||
stmt.Close()
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -266,5 +267,40 @@ func (d *DB) migrate() error {
|
||||
}
|
||||
}
|
||||
|
||||
// Migration: recreate key_deployments with nullable ssh_key_id and key_name column
|
||||
// This is needed so the system master key (which has no ssh_keys row) can be logged.
|
||||
{
|
||||
var migCount int
|
||||
d.QueryRow(`SELECT COUNT(*) FROM _migrations WHERE name = 'key_deployments_nullable_keyid'`).Scan(&migCount)
|
||||
if migCount == 0 {
|
||||
ctx := context.Background()
|
||||
conn, err := d.DB.Conn(ctx)
|
||||
if err == nil {
|
||||
conn.ExecContext(ctx, `PRAGMA foreign_keys = OFF`)
|
||||
conn.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS key_deployments_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ssh_key_id INTEGER,
|
||||
server_id INTEGER NOT NULL,
|
||||
deployed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
message TEXT,
|
||||
key_name TEXT NOT NULL DEFAULT '',
|
||||
FOREIGN KEY (ssh_key_id) REFERENCES ssh_keys(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE
|
||||
)`)
|
||||
conn.ExecContext(ctx, `INSERT INTO key_deployments_new (id, ssh_key_id, server_id, deployed_at, status, message, key_name)
|
||||
SELECT kd.id, kd.ssh_key_id, kd.server_id, kd.deployed_at, kd.status, kd.message,
|
||||
COALESCE(sk.name, '')
|
||||
FROM key_deployments kd
|
||||
LEFT JOIN ssh_keys sk ON kd.ssh_key_id = sk.id`)
|
||||
conn.ExecContext(ctx, `DROP TABLE key_deployments`)
|
||||
conn.ExecContext(ctx, `ALTER TABLE key_deployments_new RENAME TO key_deployments`)
|
||||
conn.ExecContext(ctx, `PRAGMA foreign_keys = ON`)
|
||||
conn.Close()
|
||||
}
|
||||
d.Exec(`INSERT INTO _migrations (name) VALUES ('key_deployments_nullable_keyid')`)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -615,20 +615,32 @@ func (s *Service) TestSSHAuth(hostname string, port int, username string, privat
|
||||
|
||||
// logDeployment records a deployment attempt
|
||||
func (s *Service) logDeployment(keyID, serverID int64, status, message string) {
|
||||
s.db.Exec(
|
||||
`INSERT INTO key_deployments (ssh_key_id, server_id, status, message) VALUES (?, ?, ?, ?)`,
|
||||
keyID, serverID, status, message,
|
||||
)
|
||||
if keyID <= 0 {
|
||||
// Master key or virtual key: store with NULL ssh_key_id
|
||||
s.db.Exec(
|
||||
`INSERT INTO key_deployments (ssh_key_id, server_id, status, message, key_name) VALUES (NULL, ?, ?, ?, '[MASTER] System Master Key')`,
|
||||
serverID, status, message,
|
||||
)
|
||||
} else {
|
||||
var keyName string
|
||||
s.db.QueryRow(`SELECT name FROM ssh_keys WHERE id = ?`, keyID).Scan(&keyName)
|
||||
s.db.Exec(
|
||||
`INSERT INTO key_deployments (ssh_key_id, server_id, status, message, key_name) VALUES (?, ?, ?, ?, ?)`,
|
||||
keyID, serverID, status, message, keyName,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// GetDeployments returns deployment history for a user's keys
|
||||
func (s *Service) GetDeployments(userID int64) ([]map[string]interface{}, error) {
|
||||
rows, err := s.db.Query(
|
||||
`SELECT kd.id, sk.name as key_name, srv.name as server_name, kd.status, kd.message, kd.deployed_at
|
||||
`SELECT kd.id,
|
||||
CASE WHEN kd.key_name != '' THEN kd.key_name ELSE COALESCE(sk.name, 'Unknown') END as key_name,
|
||||
srv.name as server_name, kd.status, kd.message, kd.deployed_at
|
||||
FROM key_deployments kd
|
||||
JOIN ssh_keys sk ON kd.ssh_key_id = sk.id
|
||||
LEFT JOIN ssh_keys sk ON kd.ssh_key_id = sk.id
|
||||
JOIN servers srv ON kd.server_id = srv.id
|
||||
WHERE sk.user_id = ?
|
||||
WHERE sk.user_id = ? OR kd.ssh_key_id IS NULL
|
||||
ORDER BY kd.deployed_at DESC LIMIT 50`, userID,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -1624,7 +1624,18 @@ func (h *Handler) handleDeploy(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
logging.Info("Deploy successful: key='%s' target=%s@%s:%d", key.Name, server.Username, server.Hostname, server.Port)
|
||||
h.audit.Log(userID, audit.ActionDeploySuccess, fmt.Sprintf("Deployed key '%s' to %s@%s:%d", key.Name, server.Username, server.Hostname, server.Port), clientIP(r))
|
||||
http.Redirect(w, r, "/deploy", http.StatusSeeOther)
|
||||
deployments, _ = h.deploy.GetDeployments(userID)
|
||||
data := &PageData{
|
||||
Title: "Deploy Keys",
|
||||
Active: "deploy",
|
||||
User: user,
|
||||
Keys: keyList,
|
||||
Servers: serverList,
|
||||
Groups: groups,
|
||||
Deployments: deployments,
|
||||
Flash: &Flash{Type: "success", Message: fmt.Sprintf("Key '%s' successfully deployed to %s@%s:%d.", key.Name, server.Username, server.Hostname, server.Port)},
|
||||
}
|
||||
h.templates["deploy"].ExecuteTemplate(w, "base", data)
|
||||
}
|
||||
|
||||
func (h *Handler) handleDeployGroup(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user