You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ErrorDetecting/backend/app/routers/users.py

279 lines
11 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update, delete, func, text
from ..db import get_db
from ..models.users import User
from ..deps.auth import get_current_user
from passlib.hash import bcrypt
from datetime import datetime, timezone
import re
from ..config import now_bj
router = APIRouter()
ROLE_OVERRIDES: dict[str, str] = {}
class CreateUserRequest(BaseModel):
username: str
email: str
role: str
status: str
sort: int = 0
class UpdateUserRequest(BaseModel):
role: str | None = None
status: str | None = None
sort: int | None = None
class ChangePasswordRequest(BaseModel):
currentPassword: str
newPassword: str
def _status_to_active(status: str) -> bool:
return status == "enabled"
def _active_to_status(active: bool) -> str:
return "enabled" if active else "disabled"
async def _get_user_id(db: AsyncSession, username: str) -> int | None:
res = await db.execute(text("SELECT id FROM users WHERE username=:u LIMIT 1"), {"u": username})
row = res.first()
return row[0] if row else None
async def _get_role_id(db: AsyncSession, role_key: str) -> int | None:
res = await db.execute(text("SELECT id FROM roles WHERE role_key=:k LIMIT 1"), {"k": role_key})
row = res.first()
return row[0] if row else None
async def _get_role_key(db: AsyncSession, username: str) -> str | None:
res = await db.execute(
text(
"SELECT r.role_key FROM roles r JOIN user_role_mapping m ON r.id=m.role_id JOIN users u ON u.id=m.user_id WHERE u.username=:u LIMIT 1"
),
{"u": username},
)
row = res.first()
return row[0] if row else None
async def _set_user_role(db: AsyncSession, username: str, role_key: str) -> bool:
uid = await _get_user_id(db, username)
if uid is None:
return False
rid = await _get_role_id(db, role_key)
if rid is None:
return False
await db.execute(text("DELETE FROM user_role_mapping WHERE user_id=:uid"), {"uid": uid})
await db.execute(text("INSERT INTO user_role_mapping(user_id, role_id) VALUES(:uid, :rid)"), {"uid": uid, "rid": rid})
await db.commit()
return True
def _role_or_default(username: str) -> str:
if username in ROLE_OVERRIDES:
return ROLE_OVERRIDES[username]
if username == "admin":
return "admin"
if username == "ops":
return "operator"
if username == "obs":
return "observer"
return "observer"
def _get_username(u) -> str:
return getattr(u, "username", None) or (u.get("username") if isinstance(u, dict) else None)
def _require_permission(user, permission: str):
perms = user.get("permissions", []) if isinstance(user, dict) else getattr(user, "permissions", [])
if permission not in perms:
raise HTTPException(status_code=403, detail=f"Permission denied: {permission}")
@router.get("/users")
async def list_users(user=Depends(get_current_user), db: AsyncSession = Depends(get_db)):
try:
_require_permission(user, "auth:manage")
result = await db.execute(select(User).order_by(User.sort.desc()).limit(500))
rows = result.scalars().all()
users = []
for u in rows:
rk = await _get_role_key(db, u.username)
users.append(
{
"username": u.username,
"email": u.email,
"role": rk or "observer",
"status": _active_to_status(u.is_active),
"sort": u.sort,
}
)
return {"users": users}
except HTTPException:
raise
except Exception:
raise HTTPException(status_code=500, detail="server_error")
@router.post("/users")
async def create_user(req: CreateUserRequest, user=Depends(get_current_user), db: AsyncSession = Depends(get_db)):
try:
_require_permission(user, "auth:manage")
errors: list[dict] = []
if not (3 <= len(req.username) <= 50) or not re.fullmatch(r"^[A-Za-z][A-Za-z0-9_]{2,49}$", req.username or ""):
errors.append({"field": "username", "message": "用户名需以字母开头,支持字母/数字/下划线长度3-50"})
if not re.fullmatch(r"^[^@\s]+@[^@\s]+\.[^@\s]+$", req.email or ""):
errors.append({"field": "email", "message": "邮箱格式不正确"})
if req.role not in {"admin", "operator", "observer"}:
errors.append({"field": "role", "message": "角色必须为 admin/operator/observer"})
if req.status not in {"enabled", "pending", "disabled"}:
errors.append({"field": "status", "message": "状态必须为 enabled/pending/disabled"})
if errors:
raise HTTPException(status_code=400, detail={"errors": errors})
exists_username = await db.execute(select(User.id).where(User.username == req.username).limit(1))
if exists_username.scalars().first():
raise HTTPException(status_code=409, detail={"errors": [{"field": "username", "message": "用户名已存在"}]})
exists_email = await db.execute(select(User.id).where(User.email == req.email).limit(1))
if exists_email.scalars().first():
raise HTTPException(status_code=409, detail={"errors": [{"field": "email", "message": "邮箱已存在"}]})
temp_password = "TempPass#123"
password_hash = bcrypt.hash(temp_password)
now = now_bj()
user_obj = User(
username=req.username,
email=req.email,
password_hash=password_hash,
full_name=req.username,
is_active=_status_to_active(req.status),
sort=req.sort,
last_login=None,
created_at=now,
updated_at=now,
)
db.add(user_obj)
await db.flush()
await db.commit()
ok = await _set_user_role(db, req.username, req.role)
if not ok:
raise HTTPException(status_code=400, detail={"errors": [{"field": "role", "message": "角色不存在"}]})
return {"ok": True}
except HTTPException:
raise
except Exception:
raise HTTPException(status_code=500, detail="server_error")
@router.patch("/users/{username}")
async def update_user(username: str, req: UpdateUserRequest, user=Depends(get_current_user), db: AsyncSession = Depends(get_db)):
try:
_require_permission(user, "auth:manage")
result = await db.execute(select(User).where(User.username == username).limit(1))
u = result.scalars().first()
if not u:
raise HTTPException(status_code=404, detail="not_found")
updates = {}
if req.status is not None:
if req.status not in {"enabled", "disabled"}:
raise HTTPException(status_code=400, detail="invalid_status")
updates["is_active"] = _status_to_active(req.status)
if req.sort is not None:
updates["sort"] = req.sort
if req.role is not None:
if req.role not in {"admin", "operator", "observer"}:
raise HTTPException(status_code=400, detail={"errors": [{"field": "role", "message": "不允许的角色"}]})
ok = await _set_user_role(db, username, req.role)
if not ok:
raise HTTPException(status_code=400, detail={"errors": [{"field": "role", "message": "角色不存在"}]})
if updates:
updates["updated_at"] = func.now()
await db.execute(update(User).where(User.id == u.id).values(**updates))
await db.commit()
return {"ok": True}
except HTTPException:
raise
except Exception:
raise HTTPException(status_code=500, detail="server_error")
@router.delete("/users/{username}")
async def delete_user(username: str, user=Depends(get_current_user), db: AsyncSession = Depends(get_db)):
try:
_require_permission(user, "auth:manage")
result = await db.execute(select(User).where(User.username == username).limit(1))
u = result.scalars().first()
if not u:
ROLE_OVERRIDES.pop(username, None)
return {"ok": True}
await db.execute(delete(User).where(User.id == u.id))
await db.commit()
ROLE_OVERRIDES.pop(username, None)
return {"ok": True}
except HTTPException:
raise
except Exception:
raise HTTPException(status_code=500, detail="server_error")
@router.get("/users/with-roles")
async def list_users_with_roles(user=Depends(get_current_user), db: AsyncSession = Depends(get_db)):
try:
_require_admin(user)
res = await db.execute(
text(
"SELECT u.username,u.email,u.is_active,r.role_key FROM users u LEFT JOIN user_role_mapping m ON u.id=m.user_id LEFT JOIN roles r ON r.id=m.role_id LIMIT 500"
)
)
rows = res.all()
users = [
{
"username": r[0],
"email": r[1],
"role": r[3] or "observer",
"status": _active_to_status(r[2]),
}
for r in rows
]
return {"users": users}
except HTTPException:
raise
except Exception:
raise HTTPException(status_code=500, detail="server_error")
@router.patch("/user/password")
async def change_password(req: ChangePasswordRequest, user=Depends(get_current_user), db: AsyncSession = Depends(get_db)):
try:
username = _get_username(user)
# 演示账号保护
if username in {"admin", "ops", "obs"}:
raise HTTPException(status_code=400, detail="demo_user_cannot_change_password")
# 密码强度校验
if not (8 <= len(req.newPassword) <= 128) or not re.search(r"[A-Z]", req.newPassword) or not re.search(r"[a-z]", req.newPassword) or not re.search(r"\d", req.newPassword):
raise HTTPException(status_code=400, detail="weak_new_password")
# 查找真实用户
res = await db.execute(select(User).where(User.username == username).limit(1))
u = res.scalars().first()
if not u:
raise HTTPException(status_code=401, detail="user_not_found")
# 验证旧密码
if not bcrypt.verify(req.currentPassword, u.password_hash):
raise HTTPException(status_code=400, detail="invalid_current_password")
# 更新密码
new_hash = bcrypt.hash(req.newPassword)
await db.execute(update(User).where(User.id == u.id).values(password_hash=new_hash, updated_at=func.now()))
await db.commit()
return {"ok": True}
except HTTPException:
raise
except Exception:
raise HTTPException(status_code=500, detail="server_error")