diff --git a/src/ea_chatbot/api/dependencies.py b/src/ea_chatbot/api/dependencies.py new file mode 100644 index 0000000..84fdddd --- /dev/null +++ b/src/ea_chatbot/api/dependencies.py @@ -0,0 +1,46 @@ +import os +from fastapi import Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from ea_chatbot.config import Settings +from ea_chatbot.history.manager import HistoryManager +from ea_chatbot.auth import OIDCClient +from ea_chatbot.api.utils import decode_access_token +from ea_chatbot.history.models import User + +settings = Settings() + +# Shared instances +history_manager = HistoryManager(settings.history_db_url) + +oidc_client = None +if settings.oidc_client_id and settings.oidc_client_secret and settings.oidc_server_metadata_url: + oidc_client = OIDCClient( + client_id=settings.oidc_client_id, + client_secret=settings.oidc_client_secret, + server_metadata_url=settings.oidc_server_metadata_url, + redirect_uri=os.getenv("OIDC_REDIRECT_URI", "http://localhost:3000/auth/callback") + ) + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login") + +async def get_current_user(token: str = Depends(oauth2_scheme)) -> User: + """Dependency to get the current authenticated user from the JWT token.""" + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + payload = decode_access_token(token) + if payload is None: + raise credentials_exception + + username: str | None = payload.get("sub") + if username is None: + raise credentials_exception + + user = history_manager.get_user(username) + if user is None: + raise credentials_exception + + return user \ No newline at end of file diff --git a/src/ea_chatbot/api/routers/auth.py b/src/ea_chatbot/api/routers/auth.py index 63bc865..9cefe00 100644 --- a/src/ea_chatbot/api/routers/auth.py +++ b/src/ea_chatbot/api/routers/auth.py @@ -1,30 +1,13 @@ from fastapi import APIRouter, Depends, HTTPException, status -from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from fastapi.security import OAuth2PasswordRequestForm from pydantic import BaseModel, EmailStr from typing import Optional -from ea_chatbot.config import Settings -from ea_chatbot.history.manager import HistoryManager -from ea_chatbot.auth import OIDCClient from ea_chatbot.api.utils import create_access_token - -settings = Settings() -history_manager = HistoryManager(settings.history_db_url) - -# Initialize OIDC Client if configured -oidc_client = None -if settings.oidc_client_id and settings.oidc_client_secret and settings.oidc_server_metadata_url: - oidc_client = OIDCClient( - client_id=settings.oidc_client_id, - client_secret=settings.oidc_client_secret, - server_metadata_url=settings.oidc_server_metadata_url, - # This will be updated to the frontend URL later - redirect_uri="http://localhost:3000/auth/callback" - ) +from ea_chatbot.api.dependencies import history_manager, oidc_client, get_current_user +from ea_chatbot.history.models import User as UserDB router = APIRouter(prefix="/auth", tags=["auth"]) -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login") - class Token(BaseModel): access_token: str token_type: str @@ -79,7 +62,7 @@ async def oidc_login(): """Get the OIDC authorization URL.""" if not oidc_client: raise HTTPException( - status_code=status.HTTP_501_NOT_IMPLEMENTED, + status_code=status.HTTP_510_NOT_EXTENDED, detail="OIDC is not configured" ) @@ -87,12 +70,10 @@ async def oidc_login(): return {"url": url} @router.get("/me", response_model=UserResponse) -async def get_me(token: str = Depends(oauth2_scheme)): +async def get_me(current_user: UserDB = Depends(get_current_user)): """Get the current authenticated user's profile.""" - # This currently only validates the token exists via oauth2_scheme - # The next task will implement the full dependency to decode JWT and fetch user return { - "id": "unknown", - "email": "unknown", - "display_name": "unknown" - } + "id": str(current_user.id), + "email": current_user.username, + "display_name": current_user.display_name + } \ No newline at end of file diff --git a/tests/api/test_auth.py b/tests/api/test_auth.py index 80b836b..7fa418f 100644 --- a/tests/api/test_auth.py +++ b/tests/api/test_auth.py @@ -20,7 +20,7 @@ def mock_user(): def test_register_user_success(): """Test successful user registration.""" - # We'll need to mock history_manager.get_user and create_user + # We mock it where it is used in the router with patch("ea_chatbot.api.routers.auth.history_manager") as mock_hm: mock_hm.get_user.return_value = None mock_hm.create_user.return_value = User(id="1", username="new@example.com", display_name="New") @@ -30,7 +30,6 @@ def test_register_user_success(): json={"email": "new@example.com", "password": "password123", "display_name": "New"} ) - # This will fail now because the router doesn't exist assert response.status_code == 201 assert response.json()["email"] == "new@example.com" @@ -74,3 +73,20 @@ def test_oidc_login_redirect(): response = client.get("/auth/oidc/login") assert response.status_code == 200 assert response.json()["url"] == "https://oidc-provider.com/auth" + +def test_get_me_success(): + """Test getting current user with a valid token.""" + from ea_chatbot.api.utils import create_access_token + token = create_access_token(data={"sub": "test@example.com", "user_id": "123"}) + + with patch("ea_chatbot.api.dependencies.history_manager") as mock_hm: + mock_hm.get_user.return_value = User(id="123", username="test@example.com", display_name="Test") + + response = client.get( + "/auth/me", + headers={"Authorization": f"Bearer {token}"} + ) + + assert response.status_code == 200 + assert response.json()["email"] == "test@example.com" + assert response.json()["id"] == "123"