mirror of
https://github.com/sfiorini/iptv-server.git
synced 2026-04-11 11:50:53 +00:00
First commit
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.env
|
||||||
|
.venv/
|
||||||
|
data/
|
||||||
|
__pycache__/
|
||||||
|
*.egg-info/
|
||||||
|
*.pyc
|
||||||
19
.vscode/settings.json
vendored
Normal file
19
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"cSpell.words": [
|
||||||
|
"adminpassword",
|
||||||
|
"aiosqlite",
|
||||||
|
"ASGI",
|
||||||
|
"Dockerized",
|
||||||
|
"dotenv",
|
||||||
|
"fastapi",
|
||||||
|
"iptv",
|
||||||
|
"newuser",
|
||||||
|
"passlib",
|
||||||
|
"Pydantic",
|
||||||
|
"pyproject",
|
||||||
|
"securepassword",
|
||||||
|
"superadmin",
|
||||||
|
"uvicorn",
|
||||||
|
"venv"
|
||||||
|
]
|
||||||
|
}
|
||||||
18
Dockerfile
Normal file
18
Dockerfile
Normal file
@@ -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"]
|
||||||
155
README.md
Normal file
155
README.md
Normal file
@@ -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 <repository-url>
|
||||||
|
cd <repository-directory>
|
||||||
|
```
|
||||||
|
|
||||||
|
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 <repository-url>
|
||||||
|
cd <repository-directory>
|
||||||
|
```
|
||||||
|
|
||||||
|
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).*
|
||||||
15
docker-compose.yml
Normal file
15
docker-compose.yml
Normal file
@@ -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:
|
||||||
12
pyproject.toml
Normal file
12
pyproject.toml
Normal file
@@ -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"
|
||||||
|
]
|
||||||
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
29
src/main.py
Normal file
29
src/main.py
Normal file
@@ -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
|
||||||
|
)
|
||||||
0
src/models/__init__.py
Normal file
0
src/models/__init__.py
Normal file
5
src/models/models.py
Normal file
5
src/models/models.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class User(BaseModel):
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
0
src/routers/__init__.py
Normal file
0
src/routers/__init__.py
Normal file
29
src/routers/admin.py
Normal file
29
src/routers/admin.py
Normal file
@@ -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}
|
||||||
16
src/routers/user.py
Normal file
16
src/routers/user.py
Normal file
@@ -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}"}
|
||||||
0
src/utils/__init__.py
Normal file
0
src/utils/__init__.py
Normal file
66
src/utils/auth.py
Normal file
66
src/utils/auth.py
Normal file
@@ -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
|
||||||
26
src/utils/database.py
Normal file
26
src/utils/database.py
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user