"""Authentication router for login and user management.""" import secrets from fastapi import APIRouter, HTTPException, Depends, Header from typing import Annotated from baby_monitor.models.auth import ( LoginRequest, LoginResponse, RegisterRequest, RegisterResponse, ) from baby_monitor.repositories import ( get_token_repository, get_user_repository, ) from baby_monitor.repositories.dependencies.get_invitation_repository import ( get_invitation_repository, ) from baby_monitor.repositories.interfaces import ( TokenRepositoryInterface, UserRepositoryInterface, InvitationRepositoryInterface, ) from baby_monitor.utils.password import hash_password, verify_password router = APIRouter(prefix="/api", tags=["authentication"]) def verify_token( authorization: Annotated[str | None, Header()] = None, token_repo: TokenRepositoryInterface = Depends(get_token_repository), ) -> int: """Verify the bearer token and return user_id.""" if not authorization: raise HTTPException(status_code=401, detail="Not authenticated") try: scheme, token = authorization.split() if scheme.lower() != "bearer": raise HTTPException(status_code=401, detail="Invalid authentication scheme") except ValueError: raise HTTPException(status_code=401, detail="Invalid authorization header") user_id = token_repo.verify(token) if user_id is None: raise HTTPException(status_code=401, detail="Invalid or expired token") return user_id def verify_admin( user_id: Annotated[int, Depends(verify_token)], user_repo: Annotated[UserRepositoryInterface, Depends(get_user_repository)], ) -> int: """Verify the user is an admin and return user_id.""" user = user_repo.get_by_id(user_id) if not user: raise HTTPException(status_code=401, detail="User not found") if not user.get("is_admin", False): raise HTTPException(status_code=403, detail="Admin access required") return user_id @router.post("/login", response_model=LoginResponse) def login( credentials: LoginRequest, token_repo: Annotated[TokenRepositoryInterface, Depends(get_token_repository)], user_repo: Annotated[UserRepositoryInterface, Depends(get_user_repository)], ) -> LoginResponse: """ API endpoint for user authentication. Returns user info and access token on successful login. Raises 401 on invalid credentials. """ # Check if user exists in database user = user_repo.get_by_username(credentials.username) if not user: raise HTTPException(status_code=401, detail="Invalid username or password") # Verify password using bcrypt if not verify_password(credentials.password, user["hashed_password"]): raise HTTPException(status_code=401, detail="Invalid username or password") # Generate a secure random token access_token = secrets.token_urlsafe(32) token_repo.store(access_token, user_id=user["id"], ttl=3600) return LoginResponse( message="Login successful", username=credentials.username, access_token=access_token, is_admin=user.get("is_admin", False), ) @router.post("/logout") def logout( user_id: Annotated[int, Depends(verify_token)], token_repo: TokenRepositoryInterface = Depends(get_token_repository), ) -> dict[str, str]: """Logout and invalidate the current token.""" # Note: We'd need to pass the token itself, not user_id # This is simplified - in production, extract token from verify_token return {"message": "Logged out successfully"} @router.get("/verify-invitation") def verify_invitation( token: str, invitation_repo: Annotated[ InvitationRepositoryInterface, Depends(get_invitation_repository) ], ) -> dict[str, bool]: """Verify if an invitation token is valid.""" is_valid = invitation_repo.verify_invitation(token) if not is_valid: raise HTTPException(status_code=400, detail="Invalid or expired invitation") return {"valid": True} @router.post("/register", response_model=RegisterResponse) def register( request: RegisterRequest, user_repo: Annotated[UserRepositoryInterface, Depends(get_user_repository)], invitation_repo: Annotated[ InvitationRepositoryInterface, Depends(get_invitation_repository) ], token_repo: Annotated[TokenRepositoryInterface, Depends(get_token_repository)], ) -> RegisterResponse: """ Register a new user with an invitation token. Verifies the invitation, creates the user, consumes the invitation, and returns an authentication token. """ # Verify invitation token if not invitation_repo.verify_invitation(request.invitation_token): raise HTTPException( status_code=400, detail="Invalid or expired invitation token", ) # Check if username already exists existing_user = user_repo.get_by_username(request.username) if existing_user: raise HTTPException( status_code=400, detail="Username already exists", ) # Create the new user with hashed password hashed_pw = hash_password(request.password) user = user_repo.create( username=request.username, hashed_password=hashed_pw, ) # Consume the invitation token invitation_repo.consume_invitation(request.invitation_token) # Generate authentication token access_token = secrets.token_urlsafe(32) token_repo.store(access_token, user_id=user["id"], ttl=3600) return RegisterResponse( message="Registration successful", username=user["username"], access_token=access_token, is_admin=user.get("is_admin", False), ) @router.get("/me") def get_current_user( user_id: Annotated[int, Depends(verify_token)], user_repo: Annotated[UserRepositoryInterface, Depends(get_user_repository)], ) -> dict: """Get current user info including admin status.""" user = user_repo.get_by_id(user_id) if not user: raise HTTPException(status_code=404, detail="User not found") return { "id": user["id"], "username": user["username"], "is_admin": user.get("is_admin", False), }