feat(auth): add bearer current user dependency
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 14s
Deploy / Deploy Staging (push) Successful in 46s
Deploy / Deploy Production (push) Has been skipped

This commit is contained in:
Xiaoxia AI
2026-06-21 08:35:25 +08:00
parent 2c3856a5a0
commit b084e5b468
3 changed files with 131 additions and 5 deletions
+20 -5
View File
@@ -5,6 +5,7 @@ The route layer is intentionally thin: repository construction lives in
app.dependencies and authentication behavior lives in application use cases.
"""
from app.auth import AuthenticatedUser, get_current_user
from app.config import settings
from app.dependencies import get_user_repository
from fastapi import APIRouter, Depends, HTTPException, status
@@ -49,6 +50,14 @@ class LoginResponse(BaseModel):
expires_in: int
class CurrentUserResponse(BaseModel):
user_id: str
email: str
username: str
display_name: str
email_verified: bool
@router.post("/register", response_model=RegisterResponse, status_code=status.HTTP_201_CREATED)
async def register(
request: RegisterRequest,
@@ -103,11 +112,17 @@ async def login(
)
@router.get("/me")
async def get_current_user_info():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="/auth/me requires bearer-token dependency integration",
@router.get("/me", response_model=CurrentUserResponse)
async def get_current_user_info(
authenticated_user: AuthenticatedUser = Depends(get_current_user),
):
user = authenticated_user.user
return CurrentUserResponse(
user_id=user.id,
email=user.email,
username=user.username,
display_name=user.display_name,
email_verified=user.email_verified,
)
+67
View File
@@ -0,0 +1,67 @@
from __future__ import annotations
from dataclasses import dataclass
import jwt
from app.config import settings
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jwt import ExpiredSignatureError, InvalidTokenError
from packages.domain.entities import User
from packages.ports.user_repository import UserRepository
from .dependencies import get_user_repository
bearer_scheme = HTTPBearer(auto_error=False)
@dataclass(frozen=True, slots=True)
class AuthenticatedUser:
user: User
session_id: str | None = None
token_type: str | None = None
async def get_current_user(
credentials: HTTPAuthorizationCredentials | None = Depends(bearer_scheme),
user_repository: UserRepository = Depends(get_user_repository),
) -> AuthenticatedUser:
if credentials is None or credentials.scheme.lower() != "bearer":
raise _unauthorized("Missing bearer token")
payload = _decode_user_token(credentials.credentials)
user_id = payload.get("sub")
if not isinstance(user_id, str) or not user_id:
raise _unauthorized("Invalid token subject")
user = user_repository.find_by_id(user_id)
if user is None:
raise _unauthorized("User no longer exists")
return AuthenticatedUser(
user=user,
session_id=payload.get("sid"),
token_type=payload.get("type"),
)
def _decode_user_token(token: str) -> dict:
try:
payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=["HS256"])
except ExpiredSignatureError:
raise _unauthorized("Token expired") from None
except InvalidTokenError:
raise _unauthorized("Invalid token") from None
if payload.get("type") not in {"user_auth", "access"}:
raise _unauthorized("Invalid token type")
return payload
def _unauthorized(detail: str) -> HTTPException:
return HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=detail,
headers={"WWW-Authenticate": "Bearer"},
)
+44
View File
@@ -1,13 +1,18 @@
import asyncio
import sys
from pathlib import Path
import jwt
import pytest
from fastapi import HTTPException
from fastapi.security import HTTPAuthorizationCredentials
ROOT = Path(__file__).resolve().parents[2]
API_ROOT = ROOT / "apps" / "api"
if str(API_ROOT) not in sys.path:
sys.path.insert(0, str(API_ROOT))
from app.auth import get_current_user
from app.config import settings
from packages.application.auth.login_use_case import LoginRequest, LoginUseCase
@@ -140,3 +145,42 @@ def test_login_use_case_rejects_wrong_password():
assert response is None
assert error == "Invalid email or password"
def test_current_user_dependency_resolves_bearer_user_token():
repo = InMemoryUserRepository()
repo.save(
User(
id="user-1",
email="user@example.com",
username="user",
display_name="User",
password_hash=password_hasher.hash_password("Password1"),
)
)
token = jwt.encode({"sub": "user-1", "sid": "session-1", "type": "user_auth"}, settings.JWT_SECRET_KEY, "HS256")
authenticated_user = asyncio.run(
get_current_user(HTTPAuthorizationCredentials(scheme="Bearer", credentials=token), repo)
)
assert authenticated_user.user.id == "user-1"
assert authenticated_user.session_id == "session-1"
def test_current_user_dependency_rejects_missing_token():
with pytest.raises(HTTPException) as error:
asyncio.run(get_current_user(None, InMemoryUserRepository()))
assert error.value.status_code == 401
def test_current_user_dependency_rejects_wrong_token_type():
token = jwt.encode({"sub": "user-1", "type": "refresh"}, settings.JWT_SECRET_KEY, "HS256")
with pytest.raises(HTTPException) as error:
asyncio.run(
get_current_user(HTTPAuthorizationCredentials(scheme="Bearer", credentials=token), InMemoryUserRepository())
)
assert error.value.status_code == 401