diff --git a/Dockerfile b/Dockerfile index cab5e3c..b8088d8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,9 +8,6 @@ RUN pip install uv && uv pip install -e . COPY . . ENV SUPER_ADMIN_USER=admin -ENV SUPER_ADMIN_PASSWORD=adminpassword -ENV DATABASE_FILENAME=users.db -ENV DATA_PATH=/data ENV CONTENT_PATH=/content VOLUME ["/data", "/Content"] diff --git a/README.md b/README.md index 76514e9..d5cea2c 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,8 @@ You can install and run the project either manually or using Docker. ```env SUPER_ADMIN_USER=your_super_admin_username SUPER_ADMIN_PASSWORD=your_super_admin_password - DATABASE_FILENAME=users.db - DATA_PATH=/data + KV_REST_API_TOKEN="REdIslONglOnGtOkEn" + KV_REST_API_URL="https://redis-url.upstash.io" CONTENT_PATH=/content ``` @@ -85,7 +85,7 @@ You can install and run the project either manually or using Docker. ### Running Manually 1. Ensure you have activated the virtual environment (if installed manually). -2. Ensure your environment variables (`SUPER_ADMIN_USER`, `SUPER_ADMIN_PASSWORD`, `DATABASE_FILENAME`, `DATA_PATH`, `CONTENT_PATH`) are set. +2. Ensure your environment variables (`SUPER_ADMIN_USER`, `SUPER_ADMIN_PASSWORD`, `CONTENT_PATH`) are set. 3. Run the application using uvicorn: ```bash @@ -103,20 +103,20 @@ You can install and run the project either manually or using Docker. ```bash docker run -d \ - --name user-service \ + --name iptv-server \ -p 8000:8000 \ - -v user-service-data:/data \ - -v user-service-content:/content \ + -v iptv-server-content:/content \ -e SUPER_ADMIN_USER=your_super_admin_username \ -e SUPER_ADMIN_PASSWORD=your_super_admin_password \ + -e KV_REST_API_TOKEN=REdIslONglOnGtOkEn \ + -e KV_REST_API_URL=https://redis-url.upstash.io \ iptv-server ``` Replace `your_super_admin_username` and `your_super_admin_password` with your desired credentials. Key components: - * `-v user-service-data:/data` creates/manages a Docker volume for persistent database storage - * `-v user-service-content:/content` ensures content directory persistence + * `-v iptv-server-content:/content` ensures content directory persistence * Environment variables match those expected by the application (defined in Dockerfile) The application will be available at `http://localhost:8000` (or your Docker host's IP). @@ -125,13 +125,13 @@ You can install and run the project either manually or using Docker. ```bash docker run -d \ - --name user-service \ + --name iptv-server \ -p 8000:8000 \ - -v ./local/data:/data \ -v ./local/content:/content \ -e SUPER_ADMIN_USER=admin \ -e SUPER_ADMIN_PASSWORD=securepassword \ - user-registration-service + iptv-server + ## Usage @@ -155,7 +155,8 @@ The application is configured using the following environment variables: * `SUPER_ADMIN_USER`: The username for the superadmin Basic Authentication. * `SUPER_ADMIN_PASSWORD`: The password for the superadmin Basic Authentication. -* `DATABASE_PATH`: The file path where the SQLite database will be stored. Defaults to `users.db` if not set, but the Dockerfile sets it to `/data/users.db`. +* `KV_REST_API_TOKEN`: The token for interacting with Upstash\'s Redis service. +* `KV_REST_API_URL`: The URL for Upstash\'s Redis service. ## Project Structure diff --git a/pyproject.toml b/pyproject.toml index 4e5277b..6a047c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,13 @@ [project] name = "iptv_server" -version = "0.1.0" +version = "0.1.1" dependencies = [ "fastapi>=0.110.0", "uvicorn[standard]>=0.29.0", "passlib[bcrypt]>=1.7.4", "bcrypt<4.0.0", - "aiosqlite>=0.20.0", "python-dotenv>=1.0.0", - "python-multipart>=0.0.9" + "python-multipart>=0.0.9", + "upstash-redis>=1.0.0", + "redis>=5.0.0" ] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index b4f0b89..f36e4bc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,8 @@ - fastapi>=0.110.0 - uvicorn[standard]>=0.29.0 - passlib[bcrypt]>=1.7.4 - bcrypt<4.0.0 - aiosqlite>=0.20.0 - python-dotenv>=1.0.0 - python-multipart>=0.0.9 \ No newline at end of file +fastapi>=0.110.0 +uvicorn[standard]>=0.29.0 +passlib[bcrypt]>=1.7.4 +bcrypt<4.0.0 +python-dotenv>=1.0.0 +python-multipart>=0.0.9 +upstash-redis>=1.0.0 +redis>=5.0.0 \ No newline at end of file diff --git a/src/config.py b/src/config.py index 57a7321..842edd2 100644 --- a/src/config.py +++ b/src/config.py @@ -9,6 +9,12 @@ load_dotenv() SUPER_ADMIN_USER = os.getenv("SUPER_ADMIN_USER", "admin") SUPER_ADMIN_PASSWORD = os.getenv("SUPER_ADMIN_PASSWORD", "adminpassword") +# Redis configuration +USE_LOCAL_REDIS = os.getenv('USE_LOCAL_REDIS', 'false').lower() == 'true' +LOCAL_REDIS_URL = os.getenv('LOCAL_REDIS_URL', 'redis://localhost:6379') +REDIS_URL = os.getenv("KV_REST_API_URL") +REDIS_TOKEN = os.getenv("KV_REST_API_TOKEN") + # Define the directory where content files are stored # Reads from environment variable CONTENT_PATH or defaults to '/content' CONTENT_PATH = os.getenv("CONTENT_PATH", "/content") @@ -17,18 +23,4 @@ CONTENT_PATH = os.getenv("CONTENT_PATH", "/content") content_path = os.path.dirname(CONTENT_PATH) if content_path and not os.path.exists(content_path): print(f"Creating directory for serving content: {CONTENT_PATH}") - os.makedirs(content_path, exist_ok=True) - -# Database file name -# Reads from environment variable DATABASE_FILENAME or defaults to 'users.db' -DATABASE_FILENAME= os.getenv("DATABASE_FILENAME", "users.db") - -# Define the directory where data files are stored -# Reads from environment variable DATA_PATH or defaults to '/data' -DATA_PATH = os.getenv("DATA_PATH", "/data") - -# Ensure the directory for the data files -data_dir = os.path.dirname(DATA_PATH) -if data_dir and not os.path.exists(data_dir): - print(f"Creating directory for data files: {DATA_PATH}") - os.makedirs(data_dir, exist_ok=True) + os.makedirs(content_path, exist_ok=True) \ No newline at end of file diff --git a/src/routers/admin.py b/src/routers/admin.py index 5132967..52b6a72 100644 --- a/src/routers/admin.py +++ b/src/routers/admin.py @@ -2,7 +2,7 @@ import aiosqlite from fastapi import APIRouter, Depends, HTTPException from passlib.context import CryptContext from src.utils.auth import verify_superadmin, get_user -from src.utils.database import get_db +from src.utils.database import redis from src.models.models import User # Assuming pwd_context is initialized in auth.py and used there @@ -13,17 +13,12 @@ router = APIRouter(prefix="/admin", tags=["admin"]) @router.post("/register") async def register_user( user: User, - db: aiosqlite.Connection = Depends(get_db), # Inject the database dependency - _: bool = Depends(verify_superadmin) # Requires superadmin auth + _: bool = Depends(verify_superadmin) ): - existing_user = await get_user(user.username) + existing_user = get_user(user.username) if existing_user: raise HTTPException(status_code=400, detail="Username already exists") hashed_password = pwd_context.hash(user.password) - await db.execute( - "INSERT INTO users (username, hashed_password) VALUES (?, ?)", - (user.username, hashed_password) - ) - await db.commit() + redis.set(f"user:{user.username}", hashed_password) return {"status": "User created", "username": user.username} \ No newline at end of file diff --git a/src/utils/auth.py b/src/utils/auth.py index 6ea3c8c..49c551b 100644 --- a/src/utils/auth.py +++ b/src/utils/auth.py @@ -1,38 +1,29 @@ -from pathlib import Path -import aiosqlite from fastapi import Depends, HTTPException, status, Query from fastapi.security import HTTPBasicCredentials, HTTPBasic from passlib.context import CryptContext -from src.config import SUPER_ADMIN_USER, SUPER_ADMIN_PASSWORD, DATABASE_FILENAME, DATA_PATH +from src.config import SUPER_ADMIN_USER, SUPER_ADMIN_PASSWORD +from src.utils.database import redis pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") security = HTTPBasic() admin_security = HTTPBasic() -async def get_user(username: str): - # Build proper database path - db_path = Path(DATA_PATH) / DATABASE_FILENAME - - # Ensure directory exists (security measure) - db_path.parent.mkdir(parents=True, exist_ok=True) - - async with aiosqlite.connect(db_path) as db: - cursor = await db.execute( - "SELECT username, hashed_password FROM users WHERE username = ?", - (username,) - ) - return await cursor.fetchone() +def get_user(username: str): + user_data = redis.get(f"user:{username}") + if user_data: + return (username, user_data) # Returns tuple of (username, hashed_password) + return None -async def authenticate_user(username: str, password: str): - user = await get_user(username) +def authenticate_user(username: str, password: str): + user = get_user(username) # User is a tuple (username, hashed_password) if not user or not pwd_context.verify(password, user[1]): return False return user -# Standard HTTP Basic Auth dependency (used by verify_superadmin and potentially others) -async def get_current_user(credentials: HTTPBasicCredentials = Depends(security)): - user = await authenticate_user(credentials.username, credentials.password) +# Standard HTTP Basic Auth dependency +def get_current_user(credentials: HTTPBasicCredentials = Depends(security)): + user = authenticate_user(credentials.username, credentials.password) if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -42,7 +33,7 @@ async def get_current_user(credentials: HTTPBasicCredentials = Depends(security) return user[0] # Dependency specifically for authentication via query parameters -async def authenticate_user_query( +def authenticate_user_query( username: str = Query(..., description="Username for authentication"), password: str = Query(..., description="Password for authentication") ): @@ -50,7 +41,7 @@ async def authenticate_user_query( Authenticates a user based on username and password provided as query parameters. NOTE: Passing credentials via query parameters is NOT RECOMMENDED for security reasons. """ - user = await authenticate_user(username, password) + user = authenticate_user(username, password) if not user: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials") return user[0] diff --git a/src/utils/database.py b/src/utils/database.py index 5fa27e1..85d2768 100644 --- a/src/utils/database.py +++ b/src/utils/database.py @@ -1,22 +1,36 @@ -import aiosqlite -from pathlib import Path -from src.config import DATABASE_FILENAME, DATA_PATH +from upstash_redis import Redis as UpstashRedis +from redis import Redis as LocalRedis, RedisError +from src.config import REDIS_URL, REDIS_TOKEN, USE_LOCAL_REDIS, LOCAL_REDIS_URL +from fastapi import HTTPException +import logging -async def get_db(): - # Build the full database path - db_path = Path(DATA_PATH) / DATABASE_FILENAME - - # Ensure the directory exists - db_path.parent.mkdir(parents=True, exist_ok=True) - - async with aiosqlite.connect(db_path) as db: - # Enable WAL mode for better concurrency - await db.execute("PRAGMA journal_mode=WAL;") - await db.execute(""" - CREATE TABLE IF NOT EXISTS users ( - username TEXT PRIMARY KEY, - hashed_password TEXT - ) - """) - await db.commit() - yield db \ No newline at end of file +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +try: + if USE_LOCAL_REDIS: + redis = LocalRedis.from_url(LOCAL_REDIS_URL) + logger.info("Connecting to local Redis instance...") + else: + if not REDIS_URL or not REDIS_TOKEN: + raise ValueError("Upstash Redis configuration is missing. Please check your environment variables.") + redis = UpstashRedis( + url=REDIS_URL, + token=REDIS_TOKEN + ) + logger.info("Connecting to Upstash Redis...") + + # Test the connection + redis.ping() + logger.info("Successfully connected to Redis") + +except ValueError as ve: + logger.error(f"Configuration error: {str(ve)}") + raise HTTPException(status_code=500, detail="Database configuration error") +except RedisError as re: + logger.error(f"Redis connection error: {str(re)}") + raise HTTPException(status_code=500, detail="Database connection error") +except Exception as e: + logger.error(f"Unexpected error while connecting to Redis: {str(e)}") + raise HTTPException(status_code=500, detail="Internal server error") \ No newline at end of file