diff --git a/backend/.coverage b/backend/.coverage index e1261824..3f018191 100644 Binary files a/backend/.coverage and b/backend/.coverage differ diff --git a/backend/__pycache__/main.cpython-310.pyc b/backend/__pycache__/main.cpython-310.pyc index 60a519c9..c991e5bd 100644 Binary files a/backend/__pycache__/main.cpython-310.pyc and b/backend/__pycache__/main.cpython-310.pyc differ diff --git a/backend/main.py b/backend/main.py index f84f6e5e..4f7b5932 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,5 +1,8 @@ -from fastapi import FastAPI +from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse +from fastapi.exceptions import HTTPException, RequestValidationError +from starlette.exceptions import HTTPException as StarletteHTTPException from config.settings import get_settings from config.database import init_db from src.routes import auth_router, users_router, projects_router @@ -27,6 +30,37 @@ app.include_router(users_router, prefix="/api/v1") app.include_router(projects_router, prefix="/api/v1") +@app.exception_handler(StarletteHTTPException) +async def http_exception_handler(request: Request, exc: StarletteHTTPException): + if isinstance(exc.detail, dict): + return JSONResponse( + status_code=exc.status_code, + content=exc.detail, + ) + return JSONResponse( + status_code=exc.status_code, + content={ + "success": False, + "message": exc.detail, + "data": None, + "error_code": None, + }, + ) + + +@app.exception_handler(RequestValidationError) +async def validation_exception_handler(request: Request, exc: RequestValidationError): + return JSONResponse( + status_code=422, + content={ + "success": False, + "message": "请求验证失败", + "data": exc.errors(), + "error_code": "1001", + }, + ) + + @app.on_event("startup") async def startup_event(): await init_db() diff --git a/backend/src/middleware/__pycache__/auth.cpython-310.pyc b/backend/src/middleware/__pycache__/auth.cpython-310.pyc index a72fd760..462dc8af 100644 Binary files a/backend/src/middleware/__pycache__/auth.cpython-310.pyc and b/backend/src/middleware/__pycache__/auth.cpython-310.pyc differ diff --git a/backend/src/middleware/auth.py b/backend/src/middleware/auth.py index dc0b7c66..fce4f834 100644 --- a/backend/src/middleware/auth.py +++ b/backend/src/middleware/auth.py @@ -1,33 +1,54 @@ from fastapi import Depends, HTTPException, status from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from fastapi.responses import JSONResponse from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from config.database import get_db from src.models.user import User from src.utils.jwt import decode_access_token -security = HTTPBearer() +security = HTTPBearer(auto_error=False) async def get_current_user( credentials: HTTPAuthorizationCredentials = Depends(security), db: AsyncSession = Depends(get_db), ) -> User: + if not credentials: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail={ + "success": False, + "message": "未提供认证token", + "data": None, + "error_code": "1002", + }, + ) + token = credentials.credentials payload = decode_access_token(token) if payload is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Token无效或过期", - headers={"WWW-Authenticate": "Bearer"}, + detail={ + "success": False, + "message": "Token无效或过期", + "data": None, + "error_code": "1003", + }, ) user_id = payload.get("sub") if user_id is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Token无效", + detail={ + "success": False, + "message": "Token无效", + "data": None, + "error_code": "1002", + }, ) result = await db.execute(select(User).where(User.id == int(user_id))) @@ -36,13 +57,23 @@ async def get_current_user( if user is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="用户不存在", + detail={ + "success": False, + "message": "用户不存在", + "data": None, + "error_code": "1002", + }, ) if not user.is_active: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="用户已被禁用", + detail={ + "success": False, + "message": "用户已被禁用", + "data": None, + "error_code": "1002", + }, ) return user @@ -54,11 +85,49 @@ async def get_current_active_user( if not current_user.is_active: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="用户未激活", + detail={ + "success": False, + "message": "用户未激活", + "data": None, + "error_code": "1002", + }, ) return current_user +async def require_admin( + current_user: User = Depends(get_current_active_user), +) -> User: + if current_user.role != "admin": + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail={ + "success": False, + "message": "需要管理员权限", + "data": None, + "error_code": "3001", + }, + ) + return current_user + + +async def require_market_or_admin( + current_user: User = Depends(get_current_active_user), +) -> User: + if current_user.role not in ["admin", "market"]: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail={ + "success": False, + "message": "需要市场部或管理员权限", + "data": None, + "error_code": "3001", + }, + ) + return current_user + + + async def require_admin( current_user: User = Depends(get_current_active_user), ) -> User: diff --git a/backend/src/routes/__pycache__/auth.cpython-310.pyc b/backend/src/routes/__pycache__/auth.cpython-310.pyc index d1def7b6..31af3049 100644 Binary files a/backend/src/routes/__pycache__/auth.cpython-310.pyc and b/backend/src/routes/__pycache__/auth.cpython-310.pyc differ diff --git a/backend/src/routes/auth.py b/backend/src/routes/auth.py index 1ee0b3ae..6255acdb 100644 --- a/backend/src/routes/auth.py +++ b/backend/src/routes/auth.py @@ -1,4 +1,5 @@ -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, status +from fastapi.responses import JSONResponse from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from config.database import get_db @@ -17,15 +18,59 @@ async def login( login_data: LoginRequest, db: AsyncSession = Depends(get_db), ): + if not login_data.username: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={ + "success": False, + "message": "用户名和密码不能为空", + "data": None, + "error_code": "1001", + } + ) + + if not login_data.password: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={ + "success": False, + "message": "用户名和密码不能为空", + "data": None, + "error_code": "1001", + } + ) + + if len(login_data.username) < 3: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={ + "success": False, + "message": "用户名至少需要3个字符", + "data": None, + "error_code": "1001", + } + ) + result = await db.execute( select(User).where(User.username == login_data.username) ) user = result.scalar_one_or_none() - if not user or not verify_password(login_data.password, user.password_hash): - raise HTTPException( + if not user: + return JSONResponse( status_code=status.HTTP_401_UNAUTHORIZED, - detail={ + content={ + "success": False, + "message": "用户名或密码错误", + "data": None, + "error_code": "1002", + } + ) + + if not verify_password(login_data.password, user.password_hash): + return JSONResponse( + status_code=status.HTTP_401_UNAUTHORIZED, + content={ "success": False, "message": "用户名或密码错误", "data": None, @@ -34,9 +79,9 @@ async def login( ) if not user.is_active: - raise HTTPException( + return JSONResponse( status_code=status.HTTP_401_UNAUTHORIZED, - detail={ + content={ "success": False, "message": "用户未激活", "data": None, diff --git a/backend/src/schemas/__pycache__/auth.cpython-310.pyc b/backend/src/schemas/__pycache__/auth.cpython-310.pyc index f585f5f1..5ecb692c 100644 Binary files a/backend/src/schemas/__pycache__/auth.cpython-310.pyc and b/backend/src/schemas/__pycache__/auth.cpython-310.pyc differ diff --git a/backend/src/schemas/auth.py b/backend/src/schemas/auth.py index d98808aa..f2a80db4 100644 --- a/backend/src/schemas/auth.py +++ b/backend/src/schemas/auth.py @@ -4,8 +4,8 @@ from .user import UserResponse class LoginRequest(BaseModel): - username: str = Field(..., min_length=3) - password: str = Field(..., min_length=6) + username: Optional[str] = None + password: Optional[str] = None class LoginResponse(BaseModel):