用户管理
+管理员可以创建和管理普通用户账号
+ +| 用户名 | +角色 | +创建时间 | +操作 | +
|---|---|---|---|
| {{ u.username }} | ++ + {{ u.is_admin ? '管理员' : '普通用户' }} + + | +{{ u.created_at || '-' }} | ++ + + + | +
| 暂无用户 | +|||
From 48fdca12bdd157aecb19c7e5c70eecb398080642 Mon Sep 17 00:00:00 2001 From: shen <> Date: Mon, 8 Jun 2026 21:08:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A4=9A=E7=94=A8=E6=88=B7=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E7=B3=BB=E7=BB=9F=EF=BC=8C=E7=AE=A1=E7=90=86=E5=91=98?= =?UTF-8?q?=E5=8F=AF=E5=88=86=E9=85=8D=E7=94=A8=E6=88=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 users 数据库表,存储用户名、密码哈希、角色 - 启动时自动从 .env 创建初始管理员账号 - 登录改为数据库校验,不再硬编码比对 - 新增 /api/users 管理接口(仅管理员) - ConfigView 新增用户管理页(创建、改密码、切换角色、删除) --- backend/database.py | 17 +++ backend/main.py | 3 +- backend/models.py | 11 ++ backend/routers/auth.py | 25 +++-- backend/routers/users.py | 103 +++++++++++++++++ frontend/src/views/ConfigView.vue | 178 +++++++++++++++++++++++++++++- 6 files changed, 327 insertions(+), 10 deletions(-) create mode 100644 backend/routers/users.py diff --git a/backend/database.py b/backend/database.py index a92c61f..8283125 100644 --- a/backend/database.py +++ b/backend/database.py @@ -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() diff --git a/backend/main.py b/backend/main.py index 81793d2..468f211 100644 --- a/backend/main.py +++ b/backend/main.py @@ -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) diff --git a/backend/models.py b/backend/models.py index c629d2e..4ab93e2 100644 --- a/backend/models.py +++ b/backend/models.py @@ -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) diff --git a/backend/routers/auth.py b/backend/routers/auth.py index 615b2fa..6ed60b9 100644 --- a/backend/routers/auth.py +++ b/backend/routers/auth.py @@ -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") diff --git a/backend/routers/users.py b/backend/routers/users.py new file mode 100644 index 0000000..000d839 --- /dev/null +++ b/backend/routers/users.py @@ -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": "用户已删除"} diff --git a/frontend/src/views/ConfigView.vue b/frontend/src/views/ConfigView.vue index f0f5ea6..1030fda 100644 --- a/frontend/src/views/ConfigView.vue +++ b/frontend/src/views/ConfigView.vue @@ -9,6 +9,7 @@
管理员可以创建和管理普通用户账号
+ +| 用户名 | +角色 | +创建时间 | +操作 | +
|---|---|---|---|
| {{ u.username }} | ++ + {{ u.is_admin ? '管理员' : '普通用户' }} + + | +{{ u.created_at || '-' }} | ++ + + + | +
| 暂无用户 | +|||