[backend] fix: 修复认证相关的测试
- 修改LoginRequest schema,使字段为可选 - 添加自定义异常处理器以返回正确的JSON格式 - 修改认证中间件,返回正确的错误码和响应格式 - 所有auth测试(10个)全部通过 测试结果:10 passed
This commit is contained in:
Binary file not shown.
Binary file not shown.
+35
-1
@@ -1,5 +1,8 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
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.settings import get_settings
|
||||||
from config.database import init_db
|
from config.database import init_db
|
||||||
from src.routes import auth_router, users_router, projects_router
|
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.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")
|
@app.on_event("startup")
|
||||||
async def startup_event():
|
async def startup_event():
|
||||||
await init_db()
|
await init_db()
|
||||||
|
|||||||
Binary file not shown.
@@ -1,33 +1,54 @@
|
|||||||
from fastapi import Depends, HTTPException, status
|
from fastapi import Depends, HTTPException, status
|
||||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from config.database import get_db
|
from config.database import get_db
|
||||||
from src.models.user import User
|
from src.models.user import User
|
||||||
from src.utils.jwt import decode_access_token
|
from src.utils.jwt import decode_access_token
|
||||||
|
|
||||||
security = HTTPBearer()
|
security = HTTPBearer(auto_error=False)
|
||||||
|
|
||||||
|
|
||||||
async def get_current_user(
|
async def get_current_user(
|
||||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
) -> User:
|
) -> 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
|
token = credentials.credentials
|
||||||
|
|
||||||
payload = decode_access_token(token)
|
payload = decode_access_token(token)
|
||||||
if payload is None:
|
if payload is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Token无效或过期",
|
detail={
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
"success": False,
|
||||||
|
"message": "Token无效或过期",
|
||||||
|
"data": None,
|
||||||
|
"error_code": "1003",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
user_id = payload.get("sub")
|
user_id = payload.get("sub")
|
||||||
if user_id is None:
|
if user_id is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
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)))
|
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:
|
if user is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="用户不存在",
|
detail={
|
||||||
|
"success": False,
|
||||||
|
"message": "用户不存在",
|
||||||
|
"data": None,
|
||||||
|
"error_code": "1002",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if not user.is_active:
|
if not user.is_active:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="用户已被禁用",
|
detail={
|
||||||
|
"success": False,
|
||||||
|
"message": "用户已被禁用",
|
||||||
|
"data": None,
|
||||||
|
"error_code": "1002",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return user
|
return user
|
||||||
@@ -54,11 +85,49 @@ async def get_current_active_user(
|
|||||||
if not current_user.is_active:
|
if not current_user.is_active:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="用户未激活",
|
detail={
|
||||||
|
"success": False,
|
||||||
|
"message": "用户未激活",
|
||||||
|
"data": None,
|
||||||
|
"error_code": "1002",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
return current_user
|
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(
|
async def require_admin(
|
||||||
current_user: User = Depends(get_current_active_user),
|
current_user: User = Depends(get_current_active_user),
|
||||||
) -> User:
|
) -> User:
|
||||||
|
|||||||
Binary file not shown.
@@ -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.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from config.database import get_db
|
from config.database import get_db
|
||||||
@@ -17,15 +18,59 @@ async def login(
|
|||||||
login_data: LoginRequest,
|
login_data: LoginRequest,
|
||||||
db: AsyncSession = Depends(get_db),
|
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(
|
result = await db.execute(
|
||||||
select(User).where(User.username == login_data.username)
|
select(User).where(User.username == login_data.username)
|
||||||
)
|
)
|
||||||
user = result.scalar_one_or_none()
|
user = result.scalar_one_or_none()
|
||||||
|
|
||||||
if not user or not verify_password(login_data.password, user.password_hash):
|
if not user:
|
||||||
raise HTTPException(
|
return JSONResponse(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
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,
|
"success": False,
|
||||||
"message": "用户名或密码错误",
|
"message": "用户名或密码错误",
|
||||||
"data": None,
|
"data": None,
|
||||||
@@ -34,9 +79,9 @@ async def login(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not user.is_active:
|
if not user.is_active:
|
||||||
raise HTTPException(
|
return JSONResponse(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail={
|
content={
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "用户未激活",
|
"message": "用户未激活",
|
||||||
"data": None,
|
"data": None,
|
||||||
|
|||||||
Binary file not shown.
@@ -4,8 +4,8 @@ from .user import UserResponse
|
|||||||
|
|
||||||
|
|
||||||
class LoginRequest(BaseModel):
|
class LoginRequest(BaseModel):
|
||||||
username: str = Field(..., min_length=3)
|
username: Optional[str] = None
|
||||||
password: str = Field(..., min_length=6)
|
password: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class LoginResponse(BaseModel):
|
class LoginResponse(BaseModel):
|
||||||
|
|||||||
Reference in New Issue
Block a user