This commit is contained in:
Logan Cusano
2025-07-13 15:26:18 -04:00
parent eb0d6fcf5c
commit ecd2631eec
4 changed files with 64 additions and 38 deletions

View File

@@ -1,7 +1,8 @@
from fastapi import FastAPI from fastapi import FastAPI
from .routers import auth, users, videos from .routers import auth, users, videos
app = FastAPI(title="Video Voting App") app = FastAPI(title="Video Voting App",
swagger_ui_parameters={"persistAuthorization": True})
app.include_router(auth.router, prefix="/auth", tags=["Authentication"]) app.include_router(auth.router, prefix="/auth", tags=["Authentication"])
app.include_router(users.router, prefix="/users", tags=["Users"]) app.include_router(users.router, prefix="/users", tags=["Users"])

View File

@@ -1,7 +1,12 @@
from pydantic import BaseModel, Field, EmailStr from pydantic import BaseModel, Field, EmailStr
from typing import Optional, List from typing import Optional, List
class LoginRequest(BaseModel): class LoginPasswordRequest(BaseModel):
email: EmailStr
password: str
class LoginResponse(BaseModel):
uid: str
id_token: str id_token: str
class UserCreate(BaseModel): class UserCreate(BaseModel):

View File

@@ -1,15 +1,24 @@
import os
import httpx
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
# Import the new dependency
from fastapi.security import OAuth2PasswordRequestForm
from firebase_admin import auth from firebase_admin import auth
from ..firebase_config import get_db from ..firebase_config import get_db
from ..models import UserCreate, UserRecord, LoginRequest # The LoginPasswordRequest model is no longer needed for this endpoint
from ..models import UserCreate, UserRecord, LoginResponse
router = APIRouter() router = APIRouter()
FIREBASE_WEB_API_KEY = os.environ.get("FIREBASE_WEB_API_KEY")
FIREBASE_SIGN_IN_URL = f"https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key={FIREBASE_WEB_API_KEY}"
@router.post("/register", response_model=UserRecord, status_code=status.HTTP_201_CREATED) @router.post("/register", response_model=UserRecord, status_code=status.HTTP_201_CREATED)
async def register_user(user: UserCreate): async def register_user(user: UserCreate):
""" """
Registers a user in Firebase Auth and creates a corresponding user document in Firestore. Registers a user in Firebase Auth and creates a corresponding user document in Firestore.
""" """
# ... (this function remains the same)
try: try:
user_record = auth.create_user( user_record = auth.create_user(
email=user.email, email=user.email,
@@ -34,40 +43,50 @@ async def register_user(user: UserCreate):
detail="Email already registered", detail="Email already registered",
) )
@router.post("/login", response_model=UserRecord) # The signature of this function is updated
async def login(login_data: LoginRequest): @router.post("/login", response_model=LoginResponse)
async def login_with_password(form_data: OAuth2PasswordRequestForm = Depends()):
""" """
Verifies a Firebase ID token, checks user role from Firestore, and returns user data. Authenticates a user with email/password via Firebase REST API,
checks their role in Firestore, and returns their UID and ID token.
""" """
try: payload = {
decoded_token = auth.verify_id_token(login_data.id_token) # Use form_data.username as the email
uid = decoded_token['uid'] "email": form_data.username,
"password": form_data.password,
"returnSecureToken": True
}
db = get_db() async with httpx.AsyncClient() as client:
user_doc = db.collection('users').document(uid).get() try:
response = await client.post(FIREBASE_SIGN_IN_URL, json=payload)
response.raise_for_status()
if not user_doc.exists: auth_data = response.json()
uid = auth_data['localId']
# Check user role in Firestore to ensure account is active
db = get_db()
user_doc = db.collection('users').document(uid).get()
if not user_doc.exists:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="User authenticated but not found in database.",
)
user = user_doc.to_dict()
if user.get("role") == "member":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Account is not activated. Please contact an administrator."
)
return LoginResponse(uid=uid, id_token=auth_data['idToken'])
except httpx.HTTPStatusError as e:
error_detail = e.response.json().get("error", {}).get("message", "Authentication failed.")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found in the database.", detail=error_detail
) )
user = user_doc.to_dict()
if user.get("role") == "member":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Account is not activated. Please contact an administrator."
)
return UserRecord(**user)
except auth.InvalidIdTokenError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid Firebase ID token",
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)

View File

@@ -1,14 +1,15 @@
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from firebase_admin import auth from firebase_admin import auth
from .firebase_config import get_db from .firebase_config import get_db
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token") http_bearer_scheme = HTTPBearer()
async def get_current_user(token: str = Depends(oauth2_scheme)): async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(http_bearer_scheme)):
""" """
Verifies the Firebase ID token and retrieves the user document from Firestore. Verifies the Firebase ID token and retrieves the user document from Firestore.
""" """
token = credentials.credentials
try: try:
decoded_token = auth.verify_id_token(token) decoded_token = auth.verify_id_token(token)
uid = decoded_token['uid'] uid = decoded_token['uid']