feat: 多用户管理系统,管理员可分配用户
- 新增 users 数据库表,存储用户名、密码哈希、角色 - 启动时自动从 .env 创建初始管理员账号 - 登录改为数据库校验,不再硬编码比对 - 新增 /api/users 管理接口(仅管理员) - ConfigView 新增用户管理页(创建、改密码、切换角色、删除)
This commit is contained in:
@@ -34,7 +34,24 @@ def _migrate_db():
|
||||
pass # 列已存在则忽略
|
||||
|
||||
|
||||
def _seed_admin():
|
||||
"""初始化管理员账号(从 .env 配置)"""
|
||||
from .models import User
|
||||
from .config import ADMIN_USERNAME, ADMIN_PASSWORD
|
||||
import hashlib
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
if not db.query(User).filter(User.username == ADMIN_USERNAME).first():
|
||||
pw_hash = hashlib.sha256(ADMIN_PASSWORD.encode()).hexdigest()
|
||||
db.add(User(id="admin-001", username=ADMIN_USERNAME, password_hash=pw_hash, is_admin=True))
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def init_db():
|
||||
"""初始化数据库"""
|
||||
Base.metadata.create_all(bind=engine)
|
||||
_migrate_db()
|
||||
_seed_admin()
|
||||
|
||||
+2
-1
@@ -7,7 +7,7 @@ from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from .database import init_db
|
||||
from .routers import config, apps, tasks, auth
|
||||
from .routers import config, apps, tasks, auth, users
|
||||
from .services.log_streamer import log_streamer
|
||||
from .config import BACKEND_PORT
|
||||
|
||||
@@ -38,6 +38,7 @@ app.add_middleware(
|
||||
|
||||
# 路由
|
||||
app.include_router(auth.router)
|
||||
app.include_router(users.router)
|
||||
app.include_router(config.router)
|
||||
app.include_router(apps.router)
|
||||
app.include_router(tasks.router)
|
||||
|
||||
@@ -49,3 +49,14 @@ class BuildConfig(Base):
|
||||
key = Column(String, primary_key=True)
|
||||
value = Column(String)
|
||||
updated_at = Column(TIMESTAMP, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""用户表"""
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
username = Column(String, unique=True, nullable=False)
|
||||
password_hash = Column(String, nullable=False)
|
||||
is_admin = Column(Boolean, default=False)
|
||||
created_at = Column(TIMESTAMP, default=datetime.utcnow)
|
||||
|
||||
+18
-7
@@ -2,10 +2,13 @@
|
||||
import jwt
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..schemas import LoginRequest, LoginResponse
|
||||
from ..config import ADMIN_USERNAME, ADMIN_PASSWORD, JWT_SECRET, JWT_ALGORITHM, JWT_EXPIRE_HOURS
|
||||
from ..config import JWT_SECRET, JWT_ALGORITHM, JWT_EXPIRE_HOURS
|
||||
from ..deps import get_current_user
|
||||
from ..database import get_db
|
||||
from ..models import User
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||||
|
||||
@@ -20,13 +23,21 @@ def _create_token(username: str, is_admin: bool) -> str:
|
||||
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
|
||||
|
||||
|
||||
def _verify_password(password: str, password_hash: str) -> bool:
|
||||
"""校验密码"""
|
||||
import hashlib
|
||||
return hashlib.sha256(password.encode()).hexdigest() == password_hash
|
||||
|
||||
|
||||
@router.post("/login", response_model=LoginResponse)
|
||||
async def login(request: LoginRequest):
|
||||
"""管理员登录"""
|
||||
if request.username == ADMIN_USERNAME and request.password == ADMIN_PASSWORD:
|
||||
token = _create_token(request.username, is_admin=True)
|
||||
return LoginResponse(token=token, username=request.username, is_admin=True)
|
||||
raise HTTPException(status_code=401, detail="用户名或密码错误")
|
||||
async def login(request: LoginRequest, db: Session = Depends(get_db)):
|
||||
"""用户登录"""
|
||||
user = db.query(User).filter(User.username == request.username).first()
|
||||
if not user or not _verify_password(request.password, user.password_hash):
|
||||
raise HTTPException(status_code=401, detail="用户名或密码错误")
|
||||
|
||||
token = _create_token(user.username, user.is_admin)
|
||||
return LoginResponse(token=token, username=user.username, is_admin=user.is_admin)
|
||||
|
||||
|
||||
@router.get("/me")
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
"""用户管理 API(仅管理员)"""
|
||||
import uuid
|
||||
import hashlib
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import User
|
||||
from ..deps import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/api/users", tags=["users"], dependencies=[Depends(get_current_user)])
|
||||
|
||||
|
||||
def _require_admin(user: dict):
|
||||
if not user.get("is_admin"):
|
||||
raise HTTPException(status_code=403, detail="需要管理员权限")
|
||||
|
||||
|
||||
def _hash_password(password: str) -> str:
|
||||
return hashlib.sha256(password.encode()).hexdigest()
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_users(user: dict = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
"""获取用户列表"""
|
||||
_require_admin(user)
|
||||
users = db.query(User).order_by(User.created_at.desc()).all()
|
||||
return [{"id": u.id, "username": u.username, "is_admin": u.is_admin, "created_at": str(u.created_at) if u.created_at else None} for u in users]
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_user(data: dict, user: dict = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
"""创建用户"""
|
||||
_require_admin(user)
|
||||
|
||||
username = data.get("username", "").strip()
|
||||
password = data.get("password", "").strip()
|
||||
is_admin = data.get("is_admin", False)
|
||||
|
||||
if not username or not password:
|
||||
raise HTTPException(status_code=400, detail="用户名和密码不能为空")
|
||||
if len(password) < 6:
|
||||
raise HTTPException(status_code=400, detail="密码至少 6 位")
|
||||
if db.query(User).filter(User.username == username).first():
|
||||
raise HTTPException(status_code=400, detail="用户名已存在")
|
||||
|
||||
new_user = User(
|
||||
id=str(uuid.uuid4()),
|
||||
username=username,
|
||||
password_hash=_hash_password(password),
|
||||
is_admin=is_admin,
|
||||
)
|
||||
db.add(new_user)
|
||||
db.commit()
|
||||
return {"message": "用户已创建", "id": new_user.id}
|
||||
|
||||
|
||||
@router.put("/{user_id}/password")
|
||||
async def change_password(user_id: str, data: dict, user: dict = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
"""修改密码(管理员可改任意用户)"""
|
||||
_require_admin(user)
|
||||
|
||||
target = db.query(User).filter(User.id == user_id).first()
|
||||
if not target:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
new_password = data.get("password", "").strip()
|
||||
if not new_password or len(new_password) < 6:
|
||||
raise HTTPException(status_code=400, detail="密码至少 6 位")
|
||||
|
||||
target.password_hash = _hash_password(new_password)
|
||||
db.commit()
|
||||
return {"message": "密码已更新"}
|
||||
|
||||
|
||||
@router.put("/{user_id}/role")
|
||||
async def change_role(user_id: str, data: dict, user: dict = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
"""切换管理员状态"""
|
||||
_require_admin(user)
|
||||
|
||||
target = db.query(User).filter(User.id == user_id).first()
|
||||
if not target:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
target.is_admin = data.get("is_admin", not target.is_admin)
|
||||
db.commit()
|
||||
return {"message": "角色已更新", "is_admin": target.is_admin}
|
||||
|
||||
|
||||
@router.delete("/{user_id}")
|
||||
async def delete_user(user_id: str, user: dict = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
"""删除用户"""
|
||||
_require_admin(user)
|
||||
|
||||
target = db.query(User).filter(User.id == user_id).first()
|
||||
if not target:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
if target.username == user["username"]:
|
||||
raise HTTPException(status_code=400, detail="不能删除自己")
|
||||
|
||||
db.delete(target)
|
||||
db.commit()
|
||||
return {"message": "用户已删除"}
|
||||
Reference in New Issue
Block a user