"""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_credentials_repository, get_user_repository, ) from baby_monitor.repositories.dependencies.get_invitation_repository import ( get_invitation_repository, ) from baby_monitor.repositories.interfaces import ( TokenRepositoryInterface, CredentialsRepositoryInterface, UserRepositoryInterface, InvitationRepositoryInterface, ) 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.""" # First try to get user from database user = user_repo.get_by_id(user_id) if user: # Database user - check is_admin field if not user.get("is_admin", False): raise HTTPException(status_code=403, detail="Admin access required") return user_id # If not in database but has valid token with user_id=1, # it's the environment-based admin (only assigned during env admin login) if user_id == 1: return user_id # User not found and not environment admin raise HTTPException(status_code=401, detail="User not found") @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)], creds_repo: Annotated[ CredentialsRepositoryInterface, Depends(get_credentials_repository) ], ) -> LoginResponse: """ API endpoint for user authentication. Returns user info and access token on successful login. Raises 401 on invalid credentials. """ # First check if it's a database user user = user_repo.get_by_username(credentials.username) if user: # TODO: Use proper password hashing (bcrypt/argon2) # For now, compare plain text (matches registration) if user["hashed_password"] == credentials.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), ) # Fallback to admin credentials from environment if creds_repo.verify_admin_credentials(credentials.username, credentials.password): # Generate a secure random token access_token = secrets.token_urlsafe(32) # Store token with user_id (hardcoded 1 for admin) token_repo.store(access_token, user_id=1, ttl=3600) return LoginResponse( message="Login successful", username=credentials.username, access_token=access_token, is_admin=True, ) raise HTTPException(status_code=401, detail="Invalid username or password") @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 # TODO: Hash password before storing (currently plain text) user = user_repo.create( username=request.username, hashed_password=request.password, ) # 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)], creds_repo: Annotated[ CredentialsRepositoryInterface, Depends(get_credentials_repository) ], ) -> dict: """Get current user info including admin status.""" user = user_repo.get_by_id(user_id) # If user not found in DB, might be env-based admin if not user: # Return admin info from environment return { "id": user_id, "username": creds_repo.get_admin_username(), "is_admin": True, # Env-based admin is always admin } return { "id": user["id"], "username": user["username"], "is_admin": user.get("is_admin", False), }