diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..35862ba --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.env +.venv/ +data/ +__pycache__/ +*.egg-info/ +*.pyc diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3925d62 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,19 @@ +{ + "cSpell.words": [ + "adminpassword", + "aiosqlite", + "ASGI", + "Dockerized", + "dotenv", + "fastapi", + "iptv", + "newuser", + "passlib", + "Pydantic", + "pyproject", + "securepassword", + "superadmin", + "uvicorn", + "venv" + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..dd7a9c9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY pyproject.toml . +RUN pip install uv && uv pip install -e . + +COPY . . + +ENV SUPER_ADMIN_USER=admin +ENV SUPER_ADMIN_PASSWORD=adminpassword +ENV DATABASE_PATH=/data/users.db + +VOLUME /data + +EXPOSE 8000 + +CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4d82bbd --- /dev/null +++ b/README.md @@ -0,0 +1,155 @@ +# FastAPI IPTV Service + +This project is a FastAPI application to manage IPTV services, including user registration and authentication. It uses `aiosqlite` for a lightweight database, `passlib` for password hashing, and `uvicorn` as the ASGI server. It's designed with containerization in mind using Docker and manages dependencies with `uv`. + +## Features + +* Health check endpoint (`/health`) +* Admin-only user registration endpoint (`/admin/register`) using Basic Authentication for the superadmin. +* Basic user authentication mechanism (though the user endpoint details are not fully provided). +* Lightweight SQLite database for user storage. +* Dockerized deployment readiness. +* Dependency management with `uv`. + +## Prerequisites + +* Python 3.10+ +* uv (for manual installation) +* Docker (for Docker installation) + +## Installation + +You can install and run the project either manually or using Docker. + +### Manual Installation + +1. **Clone the repository:** + + ```bash + git clone + cd + ``` + +2. **Install `uv`:** + If you don't have `uv` installed, you can install it via pip: + + ```bash + pip install uv + ``` + +3. **Create a virtual environment and install dependencies:** + `uv` will automatically create a `.venv` directory and install dependencies listed in `pyproject.toml`. + + ```bash + uv sync + ``` + + Activate the virtual environment: + * On macOS/Linux: `source .venv/bin/activate` + * On Windows: `.venv\Scripts\activate` + +4. **Configure Environment Variables:** + The application requires environment variables for superadmin credentials and the database path. Create a `.env` file in the project root or set them in your shell environment. + + ```env + SUPER_ADMIN_USER=your_super_admin_username + SUPER_ADMIN_PASSWORD=your_super_admin_password + DATABASE_PATH=data/users.db + ``` + + Note: The `DATABASE_PATH` uses a relative path. For persistence and consistency, it's recommended to use an absolute path or ensure the volume mount covers this path if using Docker. The `utils/database.py` script will attempt to create the directory `data` if it doesn't exist relative to where the script is run. + +### Docker Installation + +1. **Clone the repository:** + + ```bash + git clone + cd + ``` + +2. **Build the Docker image:** + + ```bash + docker build -t user-registration-service . + ``` + +3. **Configure Environment Variables:** + You can pass environment variables directly when running the container or use a `.env` file with `docker run --env-file .env ...`. + +## Running the Application + +### 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_PATH`) are set. +3. Run the application using uvicorn: + + ```bash + uvicorn src.main:app --host 0.0.0.0 --port 8000 --reload --workers 2 + ``` + + The application will be available at `http://0.0.0.0:8000`. + + *(Note: The `if __name__ == "__main__":` block in `src/main.py` runs uvicorn on port 8080, but the `CMD` in the Dockerfile and the manual command above uses 8000. The command line argument takes precedence.)* + +### Running with Docker + +1. Ensure you have built the Docker image. +2. Run the Docker container, mapping ports and volumes: + + ```bash + docker run -d \ + --name user-service \ + -p 8000:8000 \ + -v user-service-data:/data \ + -e SUPER_ADMIN_USER=your_super_admin_username \ + -e SUPER_ADMIN_PASSWORD=your_super_admin_password \ + user-registration-service + ``` + + Replace `your_super_admin_username` and `your_super_admin_password` with your desired credentials. The `-v user-service-data:/data` maps a Docker volume for persistent storage of the SQLite database defined by `DATABASE_PATH=/data/users.db` in the Dockerfile. + + The application will be available at `http://localhost:8000` (or your Docker host's IP). + +## Usage + +The API documentation will be available at `http://localhost:8000/docs` (Swagger UI) or `http://localhost:8000/redoc` (ReDoc) once the server is running. + +* **GET /health**: Checks the application status. Returns `{"status": "healthy"}` if running. +* **POST /admin/register**: Registers a new user. This endpoint is protected by Basic Authentication. You must authenticate using the `SUPER_ADMIN_USER` and `SUPER_ADMIN_PASSWORD` configured via environment variables. The request body should be a JSON object containing the `username` and `password` of the new user to register. + + ```json + { + "username": "newuser", + "password": "securepassword123" + } + ``` + +* **GET /user**: (Details not fully provided in the code) This endpoint is included via a router but its specific functionality and authentication requirements are not shown in the provided code snippets. Based on the `get_current_user` function, it likely requires basic user authentication using credentials stored in the database. + +## Configuration + +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`. + +## Project Structure + +* `Dockerfile`: Defines the Docker image build process. +* `pyproject.toml`: Manages project dependencies using `uv`. +* `src/`: Contains the main application source code. + * `main.py`: The main FastAPI application instance, includes routers and defines the entry point for uvicorn. + * `auth.py`: Contains authentication logic (though the provided snippet seems incomplete and potentially duplicated with `utils/auth.py`). + * `database.py`: Handles database connection setup and table creation (again, potentially duplicated with `utils/database.py`). + * `routers/`: Contains API route definitions. + * `admin.py`: Defines admin-specific routes, like `/admin/register`. + * `user.py`: (Content not provided) Placeholder for user-specific routes. + * `utils/`: Contains utility functions. + * `auth.py`: Contains utility functions for authentication (like `verify_superadmin`). + * `database.py`: Contains utility functions for database interaction (like `get_db`). + * `models/`: (Content not provided, but implied by `models.models.User`) Likely contains Pydantic models for data validation (e.g., the User model used in `/admin/register`). + +*(Note: There appear to be duplicate `auth.py` and `database.py` files at `src/` and `src/utils/`. You should consolidate these into `src/utils/` and update imports accordingly for a cleaner structure).* diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a83c3e3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +version: '3.8' + +services: + iptv: + build: . + ports: + - "8000:8000" + environment: + - SUPER_ADMIN_USER=${SUPER_ADMIN_USER:-admin} + - SUPER_ADMIN_PASSWORD=${SUPER_ADMIN_PASSWORD:-adminpassword} + volumes: + - iptv_data:/data + +volumes: + iptv_data: \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4e5277b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "iptv_server" +version = "0.1.0" +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" +] \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..c58b871 --- /dev/null +++ b/src/main.py @@ -0,0 +1,29 @@ +import uvicorn +from fastapi import FastAPI, Depends, Query +from fastapi.security import HTTPBasic +from passlib.context import CryptContext +from dotenv import load_dotenv +from routers import admin, user + +load_dotenv() + +app = FastAPI() +security = HTTPBasic() +admin_security = HTTPBasic() +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +@app.get("/health") +async def health_check(): + return {"status": "healthy"} + +app.include_router(admin.router) +app.include_router(user.router) + +if __name__ == "__main__": + uvicorn.run( + "main:app", + host="0.0.0.0", + port=8080, + reload=True, + workers=2 + ) \ No newline at end of file diff --git a/src/models/__init__.py b/src/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/models/models.py b/src/models/models.py new file mode 100644 index 0000000..b088c3d --- /dev/null +++ b/src/models/models.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + +class User(BaseModel): + username: str + password: str \ No newline at end of file diff --git a/src/routers/__init__.py b/src/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/routers/admin.py b/src/routers/admin.py new file mode 100644 index 0000000..605cea6 --- /dev/null +++ b/src/routers/admin.py @@ -0,0 +1,29 @@ +from fastapi import APIRouter, Depends, HTTPException +import aiosqlite +from passlib.context import CryptContext +from utils.auth import verify_superadmin, get_user +from utils.database import DATABASE_PATH, get_db +from models.models import User + +# Assuming pwd_context is initialized in auth.py and used there +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") # Re-initialize or import + +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 +): + existing_user = await 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() + return {"status": "User created", "username": user.username} \ No newline at end of file diff --git a/src/routers/user.py b/src/routers/user.py new file mode 100644 index 0000000..c3775df --- /dev/null +++ b/src/routers/user.py @@ -0,0 +1,16 @@ +from fastapi import APIRouter, Depends, HTTPException, status, Query +# Import the specific query authentication dependency +from utils.auth import authenticate_user_query + +router = APIRouter(prefix="/user", tags=["user"]) + +@router.get("/stream") +async def stream_content( + # Use the dependency that gets username/password from query params + # The dependency handles the authentication check and raises HTTPException on failure + username: str = Depends(authenticate_user_query) +): + # If the dependency returns, authentication succeeded. + # The `username` variable holds the authenticated user's username. + # You can now use `username` for any user-specific logic. + return {"content": f"protected stream data for user {username}"} diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/auth.py b/src/utils/auth.py new file mode 100644 index 0000000..a635f0a --- /dev/null +++ b/src/utils/auth.py @@ -0,0 +1,66 @@ +import os +from fastapi import Depends, HTTPException, status, Query +from fastapi.security import HTTPBasic, HTTPBasicCredentials +from passlib.context import CryptContext +import aiosqlite +from utils.database import get_db, DATABASE_PATH +from dotenv import load_dotenv + +load_dotenv() + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +security = HTTPBasic() # For general user authentication +admin_security = HTTPBasic() # Could potentially use a separate one if needed, but HTTPBasic is fine + +async def get_user(username: str): + # Use the dependency pattern even for internal calls for consistency + async with aiosqlite.connect(DATABASE_PATH) as db: + cursor = await db.execute( + "SELECT * FROM users WHERE username = ?", (username,) + ) + return await cursor.fetchone() + +async def authenticate_user(username: str, password: str): + user = await 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) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid credentials", + headers={"WWW-Authenticate": "Basic"}, + ) + return user[0] + +# Dependency specifically for authentication via query parameters +async def authenticate_user_query( + username: str = Query(..., description="Username for authentication"), + password: str = Query(..., description="Password for authentication") +): + """ + 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) + if not user: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials") + return user[0] + +def verify_superadmin(credentials: HTTPBasicCredentials = Depends(admin_security)): + correct_username = os.getenv("SUPER_ADMIN_USER") + correct_password = os.getenv("SUPER_ADMIN_PASSWORD") + + if not (credentials.username == correct_username and + credentials.password == correct_password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect superadmin credentials", + headers={"WWW-Authenticate": "Basic"}, + ) + return True # Return True if authenticated as superadmin \ No newline at end of file diff --git a/src/utils/database.py b/src/utils/database.py new file mode 100644 index 0000000..f068292 --- /dev/null +++ b/src/utils/database.py @@ -0,0 +1,26 @@ +import aiosqlite +import os +from dotenv import load_dotenv + +load_dotenv() +DATABASE_PATH = os.getenv("DATABASE_PATH", "users.db") + +# Ensure the directory for the database exists +db_dir = os.path.dirname(DATABASE_PATH) +if db_dir and not os.path.exists(db_dir): + print(f"Creating directory for database: {DATABASE_PATH}") + os.makedirs(db_dir, exist_ok=True) + + +async def get_db(): + async with aiosqlite.connect(DATABASE_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