This commit is contained in:
Logan
2025-07-13 12:56:16 -04:00
commit 80b48c00de
11 changed files with 416 additions and 0 deletions

73
app/routers/auth.py Normal file
View File

@@ -0,0 +1,73 @@
from fastapi import APIRouter, Depends, HTTPException, status
from firebase_admin import auth
from ..firebase_config import get_db
from ..models import UserCreate, UserRecord, LoginRequest
router = APIRouter()
@router.post("/register", response_model=UserRecord, status_code=status.HTTP_201_CREATED)
async def register_user(user: UserCreate):
"""
Registers a user in Firebase Auth and creates a corresponding user document in Firestore.
"""
try:
user_record = auth.create_user(
email=user.email,
password=user.password,
display_name=user.full_name
)
db = get_db()
user_data = {
"uid": user_record.uid,
"email": user.email,
"full_name": user.full_name,
"role": "member"
}
db.collection('users').document(user_record.uid).set(user_data)
return user_data
except auth.EmailAlreadyExistsError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered",
)
@router.post("/login", response_model=UserRecord)
async def login(login_data: LoginRequest):
"""
Verifies a Firebase ID token, checks user role from Firestore, and returns user data.
"""
try:
decoded_token = auth.verify_id_token(login_data.id_token)
uid = decoded_token['uid']
db = get_db()
user_doc = db.collection('users').document(uid).get()
if not user_doc.exists:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found in the 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 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),
)

47
app/routers/users.py Normal file
View File

@@ -0,0 +1,47 @@
from fastapi import APIRouter, Depends, HTTPException, status
from typing import List
from ..models import UserRecord
from ..security import is_admin
from ..firebase_config import get_db, auth
router = APIRouter()
@router.get("/", response_model=List[UserRecord], dependencies=[Depends(is_admin)])
async def read_users():
"""
Retrieves a list of all user documents from the 'users' collection in Firestore.
"""
db = get_db()
users_ref = db.collection('users').stream()
return [UserRecord(**doc.to_dict()) for doc in users_ref]
@router.patch("/{user_id}/role", response_model=UserRecord, dependencies=[Depends(is_admin)])
async def update_user_role(user_id: str, role: str):
"""
Updates a user's role in their Firestore document.
"""
if role not in ["member", "user", "admin"]:
raise HTTPException(status_code=400, detail="Invalid role specified")
db = get_db()
user_ref = db.collection('users').document(user_id)
if user_ref.get().exists:
user_ref.update({"role": role})
updated_user = user_ref.get()
return UserRecord(**updated_user.to_dict())
raise HTTPException(status_code=404, detail="User not found")
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(is_admin)])
async def delete_user(user_id: str):
"""
Deletes a user from Firebase Auth and their corresponding document from Firestore.
"""
try:
auth.delete_user(user_id)
db = get_db()
user_ref = db.collection('users').document(user_id)
if user_ref.get().exists:
user_ref.delete()
return
except auth.UserNotFoundError:
raise HTTPException(status_code=404, detail="User not found in Firebase Authentication")

116
app/routers/videos.py Normal file
View File

@@ -0,0 +1,116 @@
import os
import random
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.responses import FileResponse
from typing import List
from ..models import Video, Vote, VoteCreate
from ..security import is_user, is_admin
from ..firebase_config import get_db
from dotenv import load_dotenv
load_dotenv()
router = APIRouter()
VIDEO_DIRECTORY = os.environ.get("VIDEO_DIRECTORY")
@router.post("/scan", status_code=status.HTTP_201_CREATED, dependencies=[Depends(is_admin)])
async def scan_videos_directory():
"""
Scans the video directory and adds new video documents to the 'videos' collection in Firestore.
"""
new_videos_count = 0
if not os.path.isdir(VIDEO_DIRECTORY):
raise HTTPException(status_code=500, detail="Video directory not found on server")
db = get_db()
videos_ref = db.collection('videos')
for person_name in os.listdir(VIDEO_DIRECTORY):
# ... (directory scanning logic remains the same) ...
# Simplified for brevity; assuming inner loop logic here
file_path = os.path.join(person_name, "some_clip.mp4") # Placeholder
# Check if video already exists by file_path
existing_videos = videos_ref.where('file_path', '==', file_path).limit(1).get()
if not existing_videos:
new_video_ref = videos_ref.document()
video_data = {
"id": new_video_ref.id, "file_path": file_path, "person": person_name,
"game": None, "has_been_voted": False
}
new_video_ref.set(video_data)
new_videos_count += 1
return {"message": f"Scan complete. Added {new_videos_count} new videos."}
@router.get("/vote-next", response_model=Video, dependencies=[Depends(is_user)])
async def get_random_unvoted_video():
"""
Retrieves a random, unvoted video document from Firestore.
"""
db = get_db()
videos_stream = db.collection('videos').where('has_been_voted', '==', False).stream()
unvoted_videos = [doc.to_dict() for doc in videos_stream]
if not unvoted_videos:
raise HTTPException(status_code=404, detail="No more videos to vote on!")
random_video_data = random.choice(unvoted_videos)
return Video(**random_video_data)
@router.post("/{video_id}/vote", status_code=status.HTTP_201_CREATED)
async def submit_vote(video_id: str, vote_data: VoteCreate, current_user: dict = Depends(is_user)):
"""
Submits a vote, creating a 'vote' document and updating the video document in Firestore.
"""
db = get_db()
video_ref = db.collection('videos').document(video_id)
video_doc = video_ref.get()
if not video_doc.exists:
raise HTTPException(status_code=404, detail="Video not found")
if video_doc.to_dict().get("has_been_voted"):
raise HTTPException(status_code=400, detail="This video has already been voted on")
new_vote_ref = db.collection('votes').document()
new_vote_ref.set({
"id": new_vote_ref.id, "video_id": video_id, "user_id": current_user['uid'],
"decision": vote_data.decision, "reason": vote_data.reason,
"recommended_game": vote_data.recommended_game
})
update_data = {"has_been_voted": True}
if vote_data.recommended_game:
update_data["game"] = vote_data.recommended_game
video_ref.update(update_data)
return {"message": "Vote submitted successfully"}
@router.get("/{video_id}/stream", dependencies=[Depends(is_user)])
async def stream_video(video_id: str):
"""
Streams a video file from the server based on its Firestore document.
"""
db = get_db()
video_doc = db.collection('videos').document(video_id).get()
if not video_doc.exists:
raise HTTPException(status_code=404, detail="Video not found")
full_path = os.path.join(VIDEO_DIRECTORY, video_doc.to_dict()["file_path"])
if not os.path.exists(full_path):
raise HTTPException(status_code=404, detail="Video file not found on disk")
return FileResponse(full_path, media_type="video/mp4")
@router.get("/votes", response_model=List[Vote], dependencies=[Depends(is_admin)])
async def get_all_votes():
"""
Admin endpoint to retrieve all vote documents from Firestore.
"""
db = get_db()
votes_stream = db.collection('votes').stream()
return [Vote(**doc.to_dict()) for doc in votes_stream]