Switch to redis db for user registration

This commit is contained in:
2025-05-05 23:33:43 -05:00
parent 424a9d0fa8
commit 0856144417
8 changed files with 85 additions and 93 deletions

View File

@@ -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"]

View File

@@ -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

View File

@@ -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"
] ]

View File

@@ -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

View File

@@ -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)

View File

@@ -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}

View File

@@ -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]

View File

@@ -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")