262 lines
8.3 KiB
Go
262 lines
8.3 KiB
Go
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)
|
||
}
|