feat(v1.4): 初始管理员(id=1)可修改其他管理员的用户名、昵称和密码
build / build-and-test (push) Successful in 3m25s

- 设置 → 管理员:仅 id=1 的账户在“其他人”那一行看到「编辑」入口,自己那行不显示
- 新增编辑页 /settings/users/{id}/edit:可改用户名、显示名称,密码留空则不修改
- 重置密码后立即清除该账户的所有登录会话,强制对方用新密码重新登录;只改昵称/用户名不影响会话
- 服务端三重校验:非 id=1 拒绝、不能编辑自己、用户名 3-32 字符且唯一、密码 ≥6 位且两次一致
- 所有修改写入操作日志(旧用户名/昵称 → 新值,是否重置密码)
- 设置页根据当前账户显示不同提示:超管看到可编辑说明,其他人只看到“只有初始管理员可以修改”

数据库:无需变更(复用 users 表与 sessions 表)
测试:
- 新增单测 TestUpdateUserByAdmin:昵称更新、会话保留、改密后旧会话失效、新密码可登录旧密码失效、重名返回 ErrDuplicate、空用户名拒绝
- 功能验证 20 项:入口显示/隐藏、普通管理员与自我编辑被拒、改密踢下线、新旧密码、长度/一致性/重名校验、留空密码、日志记录
- 原有 119 项验收回归全绿;go vet / go test ./... 通过;版本号 1.4.0
This commit is contained in:
subtrack
2026-10-19 00:21:47 +08:00
parent 8cfc9f5891
commit 95fa4fd6fd
8 changed files with 244 additions and 1 deletions
+62
View File
@@ -2,6 +2,7 @@ package store
import (
"database/sql"
"errors"
"os"
"path/filepath"
"strconv"
@@ -957,3 +958,64 @@ func TestUserColorAndAssignee(t *testing.T) {
}
}
}
func TestUpdateUserByAdmin(t *testing.T) {
db := newTestDB(t)
admin, err := db.CreateUser("admin", "老板", "daili123")
if err != nil {
t.Fatal(err)
}
u, err := db.CreateUser("dashou", "阿强", "dashou123")
if err != nil {
t.Fatal(err)
}
// 建一个会话,稍后用密码重置把它踢掉
token := "tok-" + domain.Date(domain.Today())
if err := db.CreateSession(token, u.ID, time.Hour, "127.0.0.1"); err != nil {
t.Fatal(err)
}
if _, err := db.SessionUser(token); err != nil {
t.Fatalf("会话应有效: %v", err)
}
// 只改昵称:会话保留
if err := db.UpdateUserByAdmin(u.ID, "dashou", "强哥", ""); err != nil {
t.Fatal(err)
}
got, _ := db.UserByID(u.ID)
if got.DisplayName != "强哥" || got.Username != "dashou" {
t.Fatalf("昵称未更新: %+v", got)
}
if _, err := db.SessionUser(token); err != nil {
t.Fatalf("未改密码不应踢下线: %v", err)
}
// 改用户名 + 重置密码:会话失效,新密码可登录
if err := db.UpdateUserByAdmin(u.ID, "laoban", "强哥", "newpass123"); err != nil {
t.Fatal(err)
}
if _, err := db.SessionUser(token); err == nil {
t.Fatal("改密后旧会话应失效")
}
if _, err := db.Authenticate("laoban", "newpass123"); err != nil {
t.Fatalf("新密码应可登录: %v", err)
}
if _, err := db.Authenticate("laoban", "dashou123"); err == nil {
t.Fatal("旧密码不应还能登录")
}
// 用户名冲突
if err := db.UpdateUserByAdmin(u.ID, "admin", "", ""); !errors.Is(err, ErrDuplicate) {
t.Fatalf("重名应返回 ErrDuplicate, got %v", err)
}
// 空用户名
if err := db.UpdateUserByAdmin(u.ID, " ", "", ""); err == nil {
t.Fatal("空用户名应报错")
}
// 超管自己也在表里,但接口层才限制“只能改别人”
if admin.ID != 1 {
t.Fatalf("首个账户 id 应为 1,实际 %d", admin.ID)
}
}
+40
View File
@@ -148,6 +148,46 @@ func (db *DB) ActiveUsers() ([]User, error) {
return out, rows.Err()
}
// DeleteUserSessions 清除某个账户的全部登录会话。
func (db *DB) DeleteUserSessions(userID int64) error {
_, err := db.sql.Exec(`DELETE FROM sessions WHERE user_id=?`, userID)
return err
}
// UpdateUserByAdmin 由超管修改用户名/昵称,password 非空时同时重置密码。
// 重置密码会强制该账户下线。
func (db *DB) UpdateUserByAdmin(userID int64, username, displayName, password string) error {
username = strings.TrimSpace(username)
displayName = strings.TrimSpace(displayName)
if username == "" {
return errors.New("用户名不能为空")
}
hash := ""
if password != "" {
var err error
hash, err = pw.Hash(password)
if err != nil {
return err
}
}
if _, err := db.sql.Exec(`UPDATE users SET username=?, display_name=? WHERE id=?`,
username, displayName, userID); err != nil {
if isUnique(err) {
return ErrDuplicate
}
return err
}
if hash != "" {
if _, err := db.sql.Exec(`UPDATE users SET password_hash=? WHERE id=?`, hash, userID); err != nil {
return err
}
if err := db.DeleteUserSessions(userID); err != nil {
return err
}
}
return nil
}
// Authenticate 校验用户名口令。
func (db *DB) Authenticate(username, password string) (*User, error) {
var hash string