mirror of
https://github.com/sfiorini/iptv-server.git
synced 2026-04-09 07:30:44 +00:00
Switch to redis db for user registration
This commit is contained in:
@@ -8,9 +8,6 @@ RUN pip install uv && uv pip install -e .
|
|||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
ENV SUPER_ADMIN_USER=admin
|
ENV SUPER_ADMIN_USER=admin
|
||||||
ENV SUPER_ADMIN_PASSWORD=adminpassword
|
|
||||||
ENV DATABASE_FILENAME=users.db
|
|
||||||
ENV DATA_PATH=/data
|
|
||||||
ENV CONTENT_PATH=/content
|
ENV CONTENT_PATH=/content
|
||||||
|
|
||||||
VOLUME ["/data", "/Content"]
|
VOLUME ["/data", "/Content"]
|
||||||
|
|||||||
25
README.md
25
README.md
@@ -54,8 +54,8 @@ You can install and run the project either manually or using Docker.
|
|||||||
```env
|
```env
|
||||||
SUPER_ADMIN_USER=your_super_admin_username
|
SUPER_ADMIN_USER=your_super_admin_username
|
||||||
SUPER_ADMIN_PASSWORD=your_super_admin_password
|
SUPER_ADMIN_PASSWORD=your_super_admin_password
|
||||||
DATABASE_FILENAME=users.db
|
KV_REST_API_TOKEN="REdIslONglOnGtOkEn"
|
||||||
DATA_PATH=/data
|
KV_REST_API_URL="https://redis-url.upstash.io"
|
||||||
CONTENT_PATH=/content
|
CONTENT_PATH=/content
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ You can install and run the project either manually or using Docker.
|
|||||||
### Running Manually
|
### Running Manually
|
||||||
|
|
||||||
1. Ensure you have activated the virtual environment (if installed 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:
|
3. Run the application using uvicorn:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -103,20 +103,20 @@ You can install and run the project either manually or using Docker.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d \
|
docker run -d \
|
||||||
--name user-service \
|
--name iptv-server \
|
||||||
-p 8000:8000 \
|
-p 8000:8000 \
|
||||||
-v user-service-data:/data \
|
-v iptv-server-content:/content \
|
||||||
-v user-service-content:/content \
|
|
||||||
-e SUPER_ADMIN_USER=your_super_admin_username \
|
-e SUPER_ADMIN_USER=your_super_admin_username \
|
||||||
-e SUPER_ADMIN_PASSWORD=your_super_admin_password \
|
-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
|
iptv-server
|
||||||
```
|
```
|
||||||
|
|
||||||
Replace `your_super_admin_username` and `your_super_admin_password` with your desired credentials.
|
Replace `your_super_admin_username` and `your_super_admin_password` with your desired credentials.
|
||||||
|
|
||||||
Key components:
|
Key components:
|
||||||
* `-v user-service-data:/data` creates/manages a Docker volume for persistent database storage
|
* `-v iptv-server-content:/content` ensures content directory persistence
|
||||||
* `-v user-service-content:/content` ensures content directory persistence
|
|
||||||
* Environment variables match those expected by the application (defined in Dockerfile)
|
* 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).
|
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
|
```bash
|
||||||
docker run -d \
|
docker run -d \
|
||||||
--name user-service \
|
--name iptv-server \
|
||||||
-p 8000:8000 \
|
-p 8000:8000 \
|
||||||
-v ./local/data:/data \
|
|
||||||
-v ./local/content:/content \
|
-v ./local/content:/content \
|
||||||
-e SUPER_ADMIN_USER=admin \
|
-e SUPER_ADMIN_USER=admin \
|
||||||
-e SUPER_ADMIN_PASSWORD=securepassword \
|
-e SUPER_ADMIN_PASSWORD=securepassword \
|
||||||
user-registration-service
|
iptv-server
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
## 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_USER`: The username for the superadmin Basic Authentication.
|
||||||
* `SUPER_ADMIN_PASSWORD`: The password 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
|
## Project Structure
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "iptv_server"
|
name = "iptv_server"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastapi>=0.110.0",
|
"fastapi>=0.110.0",
|
||||||
"uvicorn[standard]>=0.29.0",
|
"uvicorn[standard]>=0.29.0",
|
||||||
"passlib[bcrypt]>=1.7.4",
|
"passlib[bcrypt]>=1.7.4",
|
||||||
"bcrypt<4.0.0",
|
"bcrypt<4.0.0",
|
||||||
"aiosqlite>=0.20.0",
|
|
||||||
"python-dotenv>=1.0.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"
|
||||||
]
|
]
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
fastapi>=0.110.0
|
fastapi>=0.110.0
|
||||||
uvicorn[standard]>=0.29.0
|
uvicorn[standard]>=0.29.0
|
||||||
passlib[bcrypt]>=1.7.4
|
passlib[bcrypt]>=1.7.4
|
||||||
bcrypt<4.0.0
|
bcrypt<4.0.0
|
||||||
aiosqlite>=0.20.0
|
python-dotenv>=1.0.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
|
||||||
@@ -9,6 +9,12 @@ load_dotenv()
|
|||||||
SUPER_ADMIN_USER = os.getenv("SUPER_ADMIN_USER", "admin")
|
SUPER_ADMIN_USER = os.getenv("SUPER_ADMIN_USER", "admin")
|
||||||
SUPER_ADMIN_PASSWORD = os.getenv("SUPER_ADMIN_PASSWORD", "adminpassword")
|
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
|
# Define the directory where content files are stored
|
||||||
# Reads from environment variable CONTENT_PATH or defaults to '/content'
|
# Reads from environment variable CONTENT_PATH or defaults to '/content'
|
||||||
CONTENT_PATH = os.getenv("CONTENT_PATH", "/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)
|
content_path = os.path.dirname(CONTENT_PATH)
|
||||||
if content_path and not os.path.exists(content_path):
|
if content_path and not os.path.exists(content_path):
|
||||||
print(f"Creating directory for serving content: {CONTENT_PATH}")
|
print(f"Creating directory for serving content: {CONTENT_PATH}")
|
||||||
os.makedirs(content_path, exist_ok=True)
|
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)
|
|
||||||
@@ -2,7 +2,7 @@ import aiosqlite
|
|||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from passlib.context import CryptContext
|
from passlib.context import CryptContext
|
||||||
from src.utils.auth import verify_superadmin, get_user
|
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
|
from src.models.models import User
|
||||||
|
|
||||||
# Assuming pwd_context is initialized in auth.py and used there
|
# Assuming pwd_context is initialized in auth.py and used there
|
||||||
@@ -13,17 +13,12 @@ router = APIRouter(prefix="/admin", tags=["admin"])
|
|||||||
@router.post("/register")
|
@router.post("/register")
|
||||||
async def register_user(
|
async def register_user(
|
||||||
user: User,
|
user: User,
|
||||||
db: aiosqlite.Connection = Depends(get_db), # Inject the database dependency
|
_: bool = Depends(verify_superadmin)
|
||||||
_: bool = Depends(verify_superadmin) # Requires superadmin auth
|
|
||||||
):
|
):
|
||||||
existing_user = await get_user(user.username)
|
existing_user = get_user(user.username)
|
||||||
if existing_user:
|
if existing_user:
|
||||||
raise HTTPException(status_code=400, detail="Username already exists")
|
raise HTTPException(status_code=400, detail="Username already exists")
|
||||||
|
|
||||||
hashed_password = pwd_context.hash(user.password)
|
hashed_password = pwd_context.hash(user.password)
|
||||||
await db.execute(
|
redis.set(f"user:{user.username}", hashed_password)
|
||||||
"INSERT INTO users (username, hashed_password) VALUES (?, ?)",
|
|
||||||
(user.username, hashed_password)
|
|
||||||
)
|
|
||||||
await db.commit()
|
|
||||||
return {"status": "User created", "username": user.username}
|
return {"status": "User created", "username": user.username}
|
||||||
@@ -1,38 +1,29 @@
|
|||||||
from pathlib import Path
|
|
||||||
import aiosqlite
|
|
||||||
from fastapi import Depends, HTTPException, status, Query
|
from fastapi import Depends, HTTPException, status, Query
|
||||||
from fastapi.security import HTTPBasicCredentials, HTTPBasic
|
from fastapi.security import HTTPBasicCredentials, HTTPBasic
|
||||||
from passlib.context import CryptContext
|
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")
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
security = HTTPBasic()
|
security = HTTPBasic()
|
||||||
admin_security = HTTPBasic()
|
admin_security = HTTPBasic()
|
||||||
|
|
||||||
async def get_user(username: str):
|
def get_user(username: str):
|
||||||
# Build proper database path
|
user_data = redis.get(f"user:{username}")
|
||||||
db_path = Path(DATA_PATH) / DATABASE_FILENAME
|
if user_data:
|
||||||
|
return (username, user_data) # Returns tuple of (username, hashed_password)
|
||||||
# Ensure directory exists (security measure)
|
return None
|
||||||
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()
|
|
||||||
|
|
||||||
async def authenticate_user(username: str, password: str):
|
def authenticate_user(username: str, password: str):
|
||||||
user = await get_user(username)
|
user = get_user(username)
|
||||||
# User is a tuple (username, hashed_password)
|
# User is a tuple (username, hashed_password)
|
||||||
if not user or not pwd_context.verify(password, user[1]):
|
if not user or not pwd_context.verify(password, user[1]):
|
||||||
return False
|
return False
|
||||||
return user
|
return user
|
||||||
|
|
||||||
# Standard HTTP Basic Auth dependency (used by verify_superadmin and potentially others)
|
# Standard HTTP Basic Auth dependency
|
||||||
async def get_current_user(credentials: HTTPBasicCredentials = Depends(security)):
|
def get_current_user(credentials: HTTPBasicCredentials = Depends(security)):
|
||||||
user = await authenticate_user(credentials.username, credentials.password)
|
user = authenticate_user(credentials.username, credentials.password)
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
@@ -42,7 +33,7 @@ async def get_current_user(credentials: HTTPBasicCredentials = Depends(security)
|
|||||||
return user[0]
|
return user[0]
|
||||||
|
|
||||||
# Dependency specifically for authentication via query parameters
|
# Dependency specifically for authentication via query parameters
|
||||||
async def authenticate_user_query(
|
def authenticate_user_query(
|
||||||
username: str = Query(..., description="Username for authentication"),
|
username: str = Query(..., description="Username for authentication"),
|
||||||
password: str = Query(..., description="Password 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.
|
Authenticates a user based on username and password provided as query parameters.
|
||||||
NOTE: Passing credentials via query parameters is NOT RECOMMENDED for security reasons.
|
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:
|
if not user:
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
|
||||||
return user[0]
|
return user[0]
|
||||||
|
|||||||
@@ -1,22 +1,36 @@
|
|||||||
import aiosqlite
|
from upstash_redis import Redis as UpstashRedis
|
||||||
from pathlib import Path
|
from redis import Redis as LocalRedis, RedisError
|
||||||
from src.config import DATABASE_FILENAME, DATA_PATH
|
from src.config import REDIS_URL, REDIS_TOKEN, USE_LOCAL_REDIS, LOCAL_REDIS_URL
|
||||||
|
from fastapi import HTTPException
|
||||||
|
import logging
|
||||||
|
|
||||||
async def get_db():
|
# Configure logging
|
||||||
# Build the full database path
|
logging.basicConfig(level=logging.INFO)
|
||||||
db_path = Path(DATA_PATH) / DATABASE_FILENAME
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Ensure the directory exists
|
try:
|
||||||
db_path.parent.mkdir(parents=True, exist_ok=True)
|
if USE_LOCAL_REDIS:
|
||||||
|
redis = LocalRedis.from_url(LOCAL_REDIS_URL)
|
||||||
async with aiosqlite.connect(db_path) as db:
|
logger.info("Connecting to local Redis instance...")
|
||||||
# Enable WAL mode for better concurrency
|
else:
|
||||||
await db.execute("PRAGMA journal_mode=WAL;")
|
if not REDIS_URL or not REDIS_TOKEN:
|
||||||
await db.execute("""
|
raise ValueError("Upstash Redis configuration is missing. Please check your environment variables.")
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
redis = UpstashRedis(
|
||||||
username TEXT PRIMARY KEY,
|
url=REDIS_URL,
|
||||||
hashed_password TEXT
|
token=REDIS_TOKEN
|
||||||
)
|
)
|
||||||
""")
|
logger.info("Connecting to Upstash Redis...")
|
||||||
await db.commit()
|
|
||||||
yield db
|
# 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")
|
||||||
Reference in New Issue
Block a user