feat: 支持管理员自助修改密码 (#57)

This commit is contained in:
chengma
2026-08-09 16:49:28 +08:00
parent 2cbb98f09c
commit 008bb87620
12 changed files with 446 additions and 19 deletions
+39 -5
View File
@@ -12,11 +12,12 @@ import (
var (
// ErrUsersAlreadyExist 表示首次初始化入口已经永久关闭。
ErrUsersAlreadyExist = errors.New("管理员已经初始化")
ErrUserNotFound = errors.New("用户不存在")
ErrSessionNotFound = errors.New("网页登录会话不存在")
ErrUsernameExists = errors.New("用户名已经存在")
ErrLastActiveAdmin = errors.New("不能禁用最后一个有效管理员")
ErrUsersAlreadyExist = errors.New("管理员已经初始化")
ErrUserNotFound = errors.New("用户不存在")
ErrSessionNotFound = errors.New("网页登录会话不存在")
ErrUsernameExists = errors.New("用户名已经存在")
ErrLastActiveAdmin = errors.New("不能禁用最后一个有效管理员")
ErrUserPasswordChanged = errors.New("用户密码已经变更")
)
// CountUsers 返回所有用户数量,disabled 也算。即使所有账号被禁用,
@@ -29,6 +30,39 @@ func CountUsers(q Execer) (int, error) {
return count, nil
}
// ChangePasswordAndRevokeSessions 只在旧哈希仍匹配时更新密码,并在同一事务中
// 撤销该用户的全部 Session。旧哈希条件用于阻止两个并发改密请求互相覆盖。
func ChangePasswordAndRevokeSessions(db *sql.DB, userID, expectedHash, passwordHash, updatedAt string) error {
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("开始修改密码事务失败: %w", err)
}
defer tx.Rollback()
result, err := tx.Exec(`
UPDATE users
SET password_hash = ?, password_changed_at = ?, updated_at = ?
WHERE user_id = ? AND password_hash = ? AND role = 'admin' AND status = 'active'`,
passwordHash, updatedAt, updatedAt, userID, expectedHash)
if err != nil {
return fmt.Errorf("修改管理员密码失败: %w", err)
}
affected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("确认修改密码结果失败: %w", err)
}
if affected == 0 {
return ErrUserPasswordChanged
}
if _, err := tx.Exec(`DELETE FROM web_sessions WHERE user_id = ?`, userID); err != nil {
return fmt.Errorf("撤销管理员 Session 失败: %w", err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("提交修改密码事务失败: %w", err)
}
return nil
}
// CreateInitialAdmin 在写事务里再次确认 users 为空后创建首位管理员。
// Open 的 _txlock=immediate 让并发事务从一开始就串行,最多一个能成功。
func CreateInitialAdmin(db *sql.DB, user model.User) error {