package server import ( "context" "database/sql" "errors" "regexp" "strings" "unicode/utf8" "github.com/gin-gonic/gin" "github.com/jmoiron/sqlx" "golang.org/x/crypto/bcrypt" "vdi-cloud/platform/internal/model" "vdi-cloud/platform/internal/store" ) func (s *Server) administratorList(c *gin.Context) { items := []model.Administrator{} if err := s.DB.SelectContext(c.Request.Context(), &items, "SELECT * FROM administrators ORDER BY username"); err != nil { s.dbError(c, err) return } list(c, items) } func validNewPassword(password string) bool { return utf8.RuneCountInString(password) >= 12 && len(password) <= 72 && strings.TrimSpace(password) != "" } // All account writes share a transaction lock, then recheck the actor's current role. // This also prevents concurrent administrators from disabling each other and leaving no owner. func (s *Server) accountTransaction(c *gin.Context, superOnly bool) (*sqlx.Tx, model.Administrator) { var actor model.Administrator ctx := c.Request.Context() tx, err := s.DB.BeginTxx(ctx, nil) if err != nil { s.dbError(c, err) return nil, actor } if _, err = tx.ExecContext(ctx, "SELECT pg_advisory_xact_lock(7651092028)"); err == nil { err = tx.GetContext(ctx, &actor, "SELECT a.* FROM administrators a JOIN admin_sessions x ON x.admin_id=a.id WHERE a.id=$1 AND x.id=$2 AND x.expires_at>now() FOR UPDATE OF a", s.admin(c).ID, c.GetString("sessionId")) } if err != nil { tx.Rollback() if errors.Is(err, sql.ErrNoRows) { s.fail(c, 401, "unauthorized", "登录已失效,请重新登录") } else { s.dbError(c, err) } return nil, actor } if !actor.Enabled || superOnly && actor.Role != "super_admin" { tx.Rollback() s.fail(c, 403, "forbidden", "当前账号没有该操作权限") return nil, actor } return tx, actor } func accountAudit(ctx context.Context, tx *sqlx.Tx, actor, action, target, requestID string) error { _, err := tx.ExecContext(ctx, "INSERT INTO audit_logs(id,actor,action,target,result,request_id) VALUES($1,$2,$3,$4,'success',$5)", store.ID("audit_"), actor, action, target, requestID) return err } func (s *Server) createAdministrator(c *gin.Context) { var in struct { Username string `json:"username" binding:"required,max=80"` DisplayName string `json:"displayName" binding:"required,max=80"` Role string `json:"role" binding:"required,oneof=super_admin operator viewer"` Password string `json:"password" binding:"required,max=72"` } if !s.decode(c, &in) { return } in.Username = strings.TrimSpace(in.Username) in.DisplayName = strings.TrimSpace(in.DisplayName) if !regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]{2,79}$`).MatchString(in.Username) || in.DisplayName == "" { s.fail(c, 400, "invalid_account", "账号使用 3~80 位字母、数字、点、下划线或短横线,显示名称不能为空") return } if !validNewPassword(in.Password) { s.fail(c, 400, "invalid_password", "新密码至少 12 个字符,UTF-8 编码不超过 72 字节") return } hash, err := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost) if err != nil { s.dbError(c, err) return } tx, actor := s.accountTransaction(c, true) if tx == nil { return } defer tx.Rollback() var item model.Administrator err = tx.GetContext(c.Request.Context(), &item, "INSERT INTO administrators(id,username,display_name,role,password_hash) VALUES($1,$2,$3,$4,$5) RETURNING *", store.ID("adm_"), in.Username, in.DisplayName, in.Role, string(hash)) if conflict(err) { s.fail(c, 409, "account_conflict", "管理员账号已存在") return } if err != nil { s.dbError(c, err) return } if err = accountAudit(c.Request.Context(), tx, actor.Username, "create_administrator", item.Username, c.GetString("requestId")); err == nil { err = tx.Commit() } if err != nil { s.dbError(c, err) return } c.JSON(201, item) } func (s *Server) updateAdministrator(c *gin.Context) { var in struct { DisplayName string `json:"displayName" binding:"required,max=80"` Role string `json:"role" binding:"required,oneof=super_admin operator viewer"` Enabled *bool `json:"enabled" binding:"required"` } if !s.decode(c, &in) { return } in.DisplayName = strings.TrimSpace(in.DisplayName) if in.DisplayName == "" { s.fail(c, 400, "invalid_account", "显示名称不能为空") return } tx, actor := s.accountTransaction(c, true) if tx == nil { return } defer tx.Rollback() ctx := c.Request.Context() var item model.Administrator if err := tx.GetContext(ctx, &item, "SELECT * FROM administrators WHERE id=$1 FOR UPDATE", c.Param("id")); err != nil { s.dbError(c, err) return } if item.Enabled && item.Role == "super_admin" && (!*in.Enabled || in.Role != "super_admin") { var count int if err := tx.GetContext(ctx, &count, "SELECT count(*) FROM administrators WHERE enabled AND role='super_admin'"); err != nil { s.dbError(c, err) return } if count <= 1 { s.fail(c, 409, "last_super_admin", "至少保留一个启用的超级管理员") return } } if item.ID == actor.ID && (!*in.Enabled || in.Role != actor.Role) { s.fail(c, 409, "self_account_protected", "不能停用自己或修改自己的角色,请使用其他超级管理员账号操作") return } invalidate := item.Role != in.Role || item.Enabled != *in.Enabled err := tx.GetContext(ctx, &item, "UPDATE administrators SET display_name=$1,role=$2,enabled=$3 WHERE id=$4 RETURNING *", in.DisplayName, in.Role, *in.Enabled, item.ID) if err == nil && invalidate { _, err = tx.ExecContext(ctx, "DELETE FROM admin_sessions WHERE admin_id=$1", item.ID) } if err == nil { err = accountAudit(ctx, tx, actor.Username, "update_administrator", item.Username, c.GetString("requestId")) } if err == nil { err = tx.Commit() } if err != nil { s.dbError(c, err) return } c.JSON(200, item) } func (s *Server) resetAdministratorPassword(c *gin.Context) { var in struct { Password string `json:"password" binding:"required,max=72"` } if !s.decode(c, &in) { return } if !validNewPassword(in.Password) { s.fail(c, 400, "invalid_password", "新密码至少 12 个字符,UTF-8 编码不超过 72 字节") return } tx, actor := s.accountTransaction(c, true) if tx == nil { return } defer tx.Rollback() if c.Param("id") == actor.ID { s.fail(c, 409, "use_own_password", "修改本人密码请使用修改密码入口并验证原密码") return } var item model.Administrator ctx := c.Request.Context() if err := tx.GetContext(ctx, &item, "SELECT * FROM administrators WHERE id=$1 FOR UPDATE", c.Param("id")); err != nil { s.dbError(c, err) return } if err := replacePassword(ctx, tx, item.ID, in.Password); err != nil { s.dbError(c, err) return } err := accountAudit(ctx, tx, actor.Username, "reset_administrator_password", item.Username, c.GetString("requestId")) if err == nil { err = tx.Commit() } if err != nil { s.dbError(c, err) return } c.Status(204) } func replacePassword(ctx context.Context, tx *sqlx.Tx, id, password string) error { hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err == nil { _, err = tx.ExecContext(ctx, "UPDATE administrators SET password_hash=$1 WHERE id=$2", string(hash), id) } if err == nil { _, err = tx.ExecContext(ctx, "DELETE FROM admin_sessions WHERE admin_id=$1", id) } return err } func (s *Server) changeOwnPassword(c *gin.Context) { var in struct { CurrentPassword string `json:"currentPassword" binding:"required,max=200"` NewPassword string `json:"newPassword" binding:"required,max=72"` } if !s.decode(c, &in) { return } if !validNewPassword(in.NewPassword) { s.fail(c, 400, "invalid_password", "新密码至少 12 个字符,UTF-8 编码不超过 72 字节") return } tx, actor := s.accountTransaction(c, false) if tx == nil { return } defer tx.Rollback() if bcrypt.CompareHashAndPassword([]byte(actor.PasswordHash), []byte(in.CurrentPassword)) != nil { s.fail(c, 400, "current_password_invalid", "原密码不正确") return } ctx := c.Request.Context() if err := replacePassword(ctx, tx, actor.ID, in.NewPassword); err != nil { s.dbError(c, err) return } err := accountAudit(ctx, tx, actor.Username, "change_own_password", actor.Username, c.GetString("requestId")) if err == nil { err = tx.Commit() } if err != nil { s.dbError(c, err) return } c.Status(204) }