fix: harden user management operations
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
"""支持用户软删除并保留历史任务归属"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision: str = "20260813_0023"
|
||||
down_revision: Union[str, None] = "20260812_0022"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def _columns(bind, table_name: str) -> set[str]:
|
||||
return {column["name"] for column in sa.inspect(bind).get_columns(table_name)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
columns = _columns(bind, "users")
|
||||
if "deleted_at" not in columns:
|
||||
op.add_column("users", sa.Column("deleted_at", sa.DateTime(), nullable=True))
|
||||
op.create_index("ix_users_deleted_at", "users", ["deleted_at"])
|
||||
if "deleted_username" not in columns:
|
||||
op.add_column("users", sa.Column("deleted_username", sa.String(length=64), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
columns = _columns(bind, "users")
|
||||
if "deleted_username" in columns:
|
||||
op.drop_column("users", "deleted_username")
|
||||
if "deleted_at" in columns:
|
||||
op.drop_index("ix_users_deleted_at", table_name="users")
|
||||
op.drop_column("users", "deleted_at")
|
||||
@@ -34,6 +34,8 @@ class User(Base):
|
||||
custom_permissions = Column(JSON, nullable=True, comment="自定义权限列表,null表示使用角色默认权限")
|
||||
created_at = Column(DateTime, default=_utcnow)
|
||||
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
|
||||
deleted_at = Column(DateTime, nullable=True, index=True)
|
||||
deleted_username = Column(String(64), default="")
|
||||
|
||||
# 客服被分配的账号
|
||||
assigned_accounts = relationship("Account", back_populates="assigned_user", foreign_keys="Account.assigned_to")
|
||||
|
||||
@@ -6,6 +6,7 @@ PERMISSIONS = {
|
||||
"user:view": "查看用户列表",
|
||||
"user:create": "创建用户",
|
||||
"user:edit": "编辑用户",
|
||||
"user:rename": "修改用户名",
|
||||
"user:delete": "删除用户",
|
||||
"user:assign_permissions": "分配权限",
|
||||
# 账号管理
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
"""用户管理路由(超管)"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import User, AuditLog
|
||||
from ..models import Account, AuditLog, HuyaAccount, User
|
||||
from ..security import hash_password
|
||||
from ..permissions import ROLE_LABELS, PERMISSIONS, get_user_permissions
|
||||
from ..schemas import UserCreate, UserUpdate, UserInfo
|
||||
from ..deps import require_permission, get_current_user
|
||||
from ..permissions import PERMISSIONS, get_user_permissions
|
||||
from ..schemas import UserCreate, UserRename, UserUpdate, UserInfo
|
||||
from ..deps import require_permission
|
||||
|
||||
router = APIRouter(prefix="/api/users", tags=["用户管理"])
|
||||
|
||||
@@ -18,7 +22,7 @@ def list_users(
|
||||
db: Session = Depends(get_db),
|
||||
_: User = Depends(require_permission("user:view")),
|
||||
):
|
||||
users = db.query(User).order_by(User.id).all()
|
||||
users = db.query(User).filter(User.deleted_at.is_(None)).order_by(User.id).all()
|
||||
result = []
|
||||
for u in users:
|
||||
result.append(UserInfo(
|
||||
@@ -76,6 +80,8 @@ def update_user(
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
if user.deleted_at is not None:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
if req.password:
|
||||
user.password_hash = hash_password(req.password)
|
||||
@@ -103,6 +109,51 @@ def update_user(
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/{user_id}/username", response_model=UserInfo)
|
||||
def rename_user(
|
||||
user_id: int,
|
||||
req: UserRename,
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(require_permission("user:rename")),
|
||||
):
|
||||
"""仅变更用户名,保留用户 ID 和全部关联业务数据。"""
|
||||
user = db.query(User).filter(User.id == user_id, User.deleted_at.is_(None)).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
username = req.username.strip()
|
||||
if len(username) < 2:
|
||||
raise HTTPException(status_code=400, detail="用户名至少 2 个字符")
|
||||
if username == user.username:
|
||||
return UserInfo(
|
||||
id=user.id, username=user.username, role=user.role,
|
||||
is_active=user.is_active, remark=user.remark or "",
|
||||
permissions=get_user_permissions(user), custom_permissions=user.custom_permissions,
|
||||
)
|
||||
if db.query(User.id).filter(User.username == username).first():
|
||||
raise HTTPException(status_code=400, detail="用户名已存在")
|
||||
|
||||
old_username = user.username
|
||||
user.username = username
|
||||
db.add(AuditLog(
|
||||
user_id=current.id,
|
||||
username=current.username,
|
||||
action="user:rename",
|
||||
target=f"{old_username} -> {username}",
|
||||
))
|
||||
try:
|
||||
db.commit()
|
||||
except IntegrityError as exc:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=400, detail="用户名已存在") from exc
|
||||
db.refresh(user)
|
||||
return UserInfo(
|
||||
id=user.id, username=user.username, role=user.role,
|
||||
is_active=user.is_active, remark=user.remark or "",
|
||||
permissions=get_user_permissions(user), custom_permissions=user.custom_permissions,
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{user_id}")
|
||||
def delete_user(
|
||||
user_id: int,
|
||||
@@ -112,14 +163,39 @@ def delete_user(
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
if user.deleted_at is not None:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
if user.role == "super_admin":
|
||||
raise HTTPException(status_code=400, detail="不能删除超级管理员")
|
||||
if user.id == current.id:
|
||||
raise HTTPException(status_code=400, detail="不能删除当前登录用户")
|
||||
|
||||
# 保留任务与审计记录的创建者 ID,避免删除用户时破坏历史数据。
|
||||
released_accounts = (
|
||||
db.query(Account)
|
||||
.filter(Account.assigned_to == user.id)
|
||||
.update({Account.assigned_to: None}, synchronize_session=False)
|
||||
)
|
||||
released_huya_accounts = (
|
||||
db.query(HuyaAccount)
|
||||
.filter(HuyaAccount.assigned_to == user.id)
|
||||
.update({HuyaAccount.assigned_to: None}, synchronize_session=False)
|
||||
)
|
||||
deleted_username = user.username
|
||||
db.add(AuditLog(user_id=current.id, username=current.username,
|
||||
action="user:delete", target=user.username))
|
||||
db.delete(user)
|
||||
action="user:delete", target=deleted_username))
|
||||
user.is_active = False
|
||||
user.deleted_at = datetime.now(timezone.utc)
|
||||
user.deleted_username = deleted_username
|
||||
# 释放用户名供新用户使用,同时不改变历史任务通过 user.id 保存的归属。
|
||||
user.username = f"deleted-{user.id}-{uuid4().hex[:12]}"
|
||||
db.commit()
|
||||
return {"message": "已删除", "success": True}
|
||||
return {
|
||||
"message": "已删除",
|
||||
"success": True,
|
||||
"released_accounts": released_accounts,
|
||||
"released_huya_accounts": released_huya_accounts,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/permissions/list")
|
||||
|
||||
@@ -97,6 +97,10 @@ class UserUpdate(BaseModel):
|
||||
custom_permissions: Optional[list[str]] = None
|
||||
|
||||
|
||||
class UserRename(BaseModel):
|
||||
username: str = Field(..., min_length=2, max_length=64)
|
||||
|
||||
|
||||
# ---- 账号 ----
|
||||
class AccountImport(BaseModel):
|
||||
"""批量导入,文本格式:用户名|密码|邮箱|邮箱密码|标签(可选)
|
||||
|
||||
Reference in New Issue
Block a user