diff --git a/app/models/__init__.py b/app/models/__init__.py index bb8690b..4ab99f2 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -1,4 +1,4 @@ from .db import Base, ChannelDB -from .schemas import ChannelCreate, ChannelResponse +from .schemas import ChannelCreate, ChannelUpdate, ChannelResponse -__all__ = ["Base", "ChannelDB", "ChannelCreate", "ChannelResponse"] \ No newline at end of file +__all__ = ["Base", "ChannelDB", "ChannelCreate", "ChannelUpdate", "ChannelResponse"] \ No newline at end of file diff --git a/app/models/db.py b/app/models/db.py index 6e83cf5..5beaf92 100644 --- a/app/models/db.py +++ b/app/models/db.py @@ -1,5 +1,7 @@ from datetime import datetime, timezone -from sqlalchemy import Column, String, JSON, DateTime +import uuid +from sqlalchemy import Column, String, JSON, DateTime, UniqueConstraint +from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() @@ -8,10 +10,15 @@ class ChannelDB(Base): """SQLAlchemy model for IPTV channels""" __tablename__ = "channels" - tvg_id = Column(String, primary_key=True) + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + tvg_id = Column(String, nullable=False) name = Column(String, nullable=False) - group_title = Column(String) + group_title = Column(String, nullable=False) tvg_name = Column(String) + + __table_args__ = ( + UniqueConstraint('group_title', 'name', name='uix_group_title_name'), + ) tvg_logo = Column(String) urls = Column(JSON) # Stores list of URLs as JSON created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) diff --git a/app/models/schemas.py b/app/models/schemas.py index 934bb83..34a6a1c 100644 --- a/app/models/schemas.py +++ b/app/models/schemas.py @@ -1,5 +1,6 @@ from datetime import datetime from typing import List +from uuid import UUID from pydantic import BaseModel class ChannelCreate(BaseModel): @@ -11,7 +12,15 @@ class ChannelCreate(BaseModel): tvg_logo: str tvg_name: str +class ChannelUpdate(ChannelCreate): + """Pydantic model for updating channels""" + pass + class ChannelResponse(ChannelCreate): """Pydantic model for channel responses""" + id: UUID created_at: datetime - updated_at: datetime \ No newline at end of file + updated_at: datetime + + class Config: + from_attributes = True \ No newline at end of file diff --git a/app/routers/channels.py b/app/routers/channels.py index 51f6817..023a09c 100644 --- a/app/routers/channels.py +++ b/app/routers/channels.py @@ -1,8 +1,10 @@ from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from typing import List +from uuid import UUID +from sqlalchemy import and_ -from app.models import ChannelDB, ChannelCreate, ChannelResponse +from app.models import ChannelDB, ChannelCreate, ChannelUpdate, ChannelResponse from app.utils.database import get_db from app.auth.dependencies import get_current_user, require_roles from app.models.auth import CognitoUser @@ -20,19 +22,33 @@ def create_channel( user: CognitoUser = Depends(get_current_user) ): """Create a new channel""" + # Check for duplicate channel (same group_title + name) + existing_channel = db.query(ChannelDB).filter( + and_( + ChannelDB.group_title == channel.group_title, + ChannelDB.name == channel.name + ) + ).first() + + if existing_channel: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="Channel with same group_title and name already exists" + ) + db_channel = ChannelDB(**channel.model_dump()) db.add(db_channel) db.commit() db.refresh(db_channel) return db_channel -@router.get("/{tvg_id}", response_model=ChannelResponse) +@router.get("/{channel_id}", response_model=ChannelResponse) def get_channel( - tvg_id: str, + channel_id: UUID, db: Session = Depends(get_db) ): - """Get a channel by tvg_id""" - channel = db.query(ChannelDB).filter(ChannelDB.tvg_id == tvg_id).first() + """Get a channel by id""" + channel = db.query(ChannelDB).filter(ChannelDB.id == channel_id).first() if not channel: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -40,21 +56,36 @@ def get_channel( ) return channel -@router.put("/{tvg_id}", response_model=ChannelResponse) +@router.put("/{channel_id}", response_model=ChannelResponse) @require_roles("admin") def update_channel( - tvg_id: str, - channel: ChannelCreate, + channel_id: UUID, + channel: ChannelUpdate, 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() + db_channel = db.query(ChannelDB).filter(ChannelDB.id == channel_id).first() if not db_channel: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Channel not found" ) + + # Check for duplicate channel (same group_title + name) excluding current channel + existing_channel = db.query(ChannelDB).filter( + and_( + ChannelDB.group_title == channel.group_title, + ChannelDB.name == channel.name, + ChannelDB.id != channel_id + ) + ).first() + + if existing_channel: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="Channel with same group_title and name already exists" + ) for key, value in channel.model_dump().items(): setattr(db_channel, key, value) @@ -63,15 +94,15 @@ def update_channel( db.refresh(db_channel) return db_channel -@router.delete("/{tvg_id}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete("/{channel_id}", status_code=status.HTTP_204_NO_CONTENT) @require_roles("admin") def delete_channel( - tvg_id: str, + channel_id: UUID, 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() + channel = db.query(ChannelDB).filter(ChannelDB.id == channel_id).first() if not channel: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, diff --git a/infrastructure/stack.py b/infrastructure/stack.py index c27c36b..a63e470 100644 --- a/infrastructure/stack.py +++ b/infrastructure/stack.py @@ -209,6 +209,13 @@ class IptvUpdaterStack(Stack): ec2.Port.tcp(5432), "Allow PostgreSQL access from EC2 instance" ) + + # Allow PostgreSQL access from developer's IP + rds_sg.add_ingress_rule( + ec2.Peer.ipv4("47.189.88.48/32"), # Replace with your actual IP + ec2.Port.tcp(5432), + "Allow PostgreSQL access from developer's IP" + ) # Create RDS PostgreSQL instance (free tier compatible - db.t3.micro) db = rds.DatabaseInstance(