Added PostgreSQL RDS database. Added channels protected endpoints. Added scripts and docker config to run application locally in dev mode.
Some checks failed
AWS Deploy on Push / build (push) Failing after 41s

This commit is contained in:
2025-05-21 14:02:01 -05:00
parent b947ac67f0
commit 489281f3eb
18 changed files with 409 additions and 125 deletions

View File

@@ -1,12 +1,18 @@
from functools import wraps
from typing import Callable
import os
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from app.auth.cognito import get_user_from_token
from app.models.auth import CognitoUser
# Use mock auth for local testing if MOCK_AUTH is set
if os.getenv("MOCK_AUTH", "").lower() == "true":
from app.auth.mock_auth import mock_get_user_from_token as get_user_from_token
else:
from app.auth.cognito import get_user_from_token
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="signin",
scheme_name="Bearer"

32
app/auth/mock_auth.py Normal file
View File

@@ -0,0 +1,32 @@
from fastapi import HTTPException, status
from app.models.auth import CognitoUser
MOCK_USERS = {
"testuser": {
"username": "testuser",
"roles": ["admin"]
}
}
def mock_get_user_from_token(token: str) -> CognitoUser:
"""
Mock version of get_user_from_token for local testing
Accepts 'testuser' as a valid token and returns admin user
"""
if token == "testuser":
return CognitoUser(**MOCK_USERS["testuser"])
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid mock token - use 'testuser'"
)
def mock_initiate_auth(username: str, password: str) -> dict:
"""
Mock version of initiate_auth for local testing
Accepts any username/password and returns a mock token
"""
return {
"AccessToken": "testuser",
"ExpiresIn": 3600,
"TokenType": "Bearer"
}

View File

@@ -1,10 +1,15 @@
from fastapi.security import OAuth2PasswordBearer
import uvicorn
from fastapi import FastAPI, Depends
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.responses import RedirectResponse
from sqlalchemy.orm import Session
from typing import List
from app.auth.cognito import initiate_auth
from app.auth.dependencies import get_current_user, require_roles
from app.models.auth import CognitoUser, SigninRequest, TokenResponse
from app.models import ChannelDB, ChannelCreate, ChannelResponse
from app.utils.database import get_db
from fastapi import FastAPI, Depends, Security
from fastapi.security import OAuth2PasswordBearer
@@ -89,4 +94,83 @@ def protected_admin_endpoint(user: CognitoUser = Depends(get_current_user)):
Protected endpoint that requires the 'admin' role.
If the user has 'admin' role, returns success message.
"""
return {"message": f"Hello {user.username}, you have admin privileges!"}
return {"message": f"Hello {user.username}, you have admin privileges!"}
# Channel CRUD Endpoints
@app.post("/channels", response_model=ChannelResponse, status_code=status.HTTP_201_CREATED)
@require_roles("admin")
def create_channel(
channel: ChannelCreate,
db: Session = Depends(get_db),
user: CognitoUser = Depends(get_current_user)
):
"""Create a new channel"""
db_channel = ChannelDB(**channel.model_dump())
db.add(db_channel)
db.commit()
db.refresh(db_channel)
return db_channel
@app.get("/channels/{tvg_id}", response_model=ChannelResponse)
def get_channel(
tvg_id: str,
db: Session = Depends(get_db)
):
"""Get a channel by tvg_id"""
channel = db.query(ChannelDB).filter(ChannelDB.tvg_id == tvg_id).first()
if not channel:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Channel not found"
)
return channel
@app.put("/channels/{tvg_id}", response_model=ChannelResponse)
@require_roles("admin")
def update_channel(
tvg_id: str,
channel: ChannelCreate,
db: Session = Depends(get_db),
user: CognitoUser = Depends(get_current_user)
):
"""Update a channel"""
db_channel = db.query(ChannelDB).filter(ChannelDB.tvg_id == tvg_id).first()
if not db_channel:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Channel not found"
)
for key, value in channel.model_dump().items():
setattr(db_channel, key, value)
db.commit()
db.refresh(db_channel)
return db_channel
@app.delete("/channels/{tvg_id}", status_code=status.HTTP_204_NO_CONTENT)
@require_roles("admin")
def delete_channel(
tvg_id: str,
db: Session = Depends(get_db),
user: CognitoUser = Depends(get_current_user)
):
"""Delete a channel"""
channel = db.query(ChannelDB).filter(ChannelDB.tvg_id == tvg_id).first()
if not channel:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Channel not found"
)
db.delete(channel)
db.commit()
return None
@app.get("/channels", response_model=List[ChannelResponse])
def list_channels(
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db)
):
"""List all channels with pagination"""
return db.query(ChannelDB).offset(skip).limit(limit).all()

View File

@@ -0,0 +1,4 @@
from .db import Base, ChannelDB
from .schemas import ChannelCreate, ChannelResponse
__all__ = ["Base", "ChannelDB", "ChannelCreate", "ChannelResponse"]

18
app/models/db.py Normal file
View File

@@ -0,0 +1,18 @@
from datetime import datetime, timezone
from sqlalchemy import Column, String, JSON, DateTime
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class ChannelDB(Base):
"""SQLAlchemy model for IPTV channels"""
__tablename__ = "channels"
tvg_id = Column(String, primary_key=True)
name = Column(String, nullable=False)
group_title = Column(String)
tvg_name = Column(String)
tvg_logo = Column(String)
urls = Column(JSON) # Stores list of URLs as JSON
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))

17
app/models/schemas.py Normal file
View File

@@ -0,0 +1,17 @@
from datetime import datetime
from typing import List
from pydantic import BaseModel
class ChannelCreate(BaseModel):
"""Pydantic model for creating channels"""
urls: List[str]
name: str
group_title: str
tvg_id: str
tvg_logo: str
tvg_name: str
class ChannelResponse(ChannelCreate):
"""Pydantic model for channel responses"""
created_at: datetime
updated_at: datetime

25
app/utils/database.py Normal file
View File

@@ -0,0 +1,25 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import os
DATABASE_URL = (
f"postgresql://{os.getenv('DB_USER')}:{os.getenv('DB_PASSWORD')}"
f"@{os.getenv('DB_HOST')}/{os.getenv('DB_NAME')}"
)
engine = create_engine(DATABASE_URL)
# Create all tables
from app.models import Base
Base.metadata.create_all(bind=engine)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db():
"""Dependency for getting database session"""
db = SessionLocal()
try:
yield db
finally:
db.close()