mirror of
https://github.com/sfiorini/iptv-server.git
synced 2026-04-09 06:00:43 +00:00
Created content endpoint to serve files from content directory
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
.env
|
||||
.venv/
|
||||
content/
|
||||
data/
|
||||
__pycache__/
|
||||
*.egg-info/
|
||||
|
||||
30
src/config.py
Normal file
30
src/config.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from a .env file if it exists
|
||||
load_dotenv()
|
||||
|
||||
# Super admin credentials for basic auth
|
||||
# Reads from environment variables SUPER_ADMIN_USER and SUPER_ADMIN_PASSWORD
|
||||
SUPER_ADMIN_USER = os.getenv("SUPER_ADMIN_USER", "admin")
|
||||
SUPER_ADMIN_PASSWORD = os.getenv("SUPER_ADMIN_PASSWORD", "adminpassword")
|
||||
|
||||
# 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")
|
||||
|
||||
# Ensure the directory for the content exists
|
||||
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 path
|
||||
# Reads from environment variable DATABASE_PATH or defaults to '/data/users.db'
|
||||
DATABASE_PATH = os.getenv("DATABASE_PATH", "/data/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)
|
||||
11
src/main.py
11
src/main.py
@@ -1,23 +1,18 @@
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, Depends, Query
|
||||
from fastapi import FastAPI
|
||||
from fastapi.security import HTTPBasic
|
||||
from passlib.context import CryptContext
|
||||
from dotenv import load_dotenv
|
||||
from routers import admin, user
|
||||
|
||||
load_dotenv()
|
||||
from routers import admin, content
|
||||
|
||||
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)
|
||||
app.include_router(content.router)
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
import aiosqlite
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from passlib.context import CryptContext
|
||||
from utils.auth import verify_superadmin, get_user
|
||||
from utils.database import DATABASE_PATH, get_db
|
||||
from utils.database import get_db
|
||||
from models.models import User
|
||||
|
||||
# Assuming pwd_context is initialized in auth.py and used there
|
||||
|
||||
33
src/routers/content.py
Normal file
33
src/routers/content.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from fastapi import APIRouter, Depends, Path, HTTPException
|
||||
from fastapi.responses import FileResponse
|
||||
from utils.auth import authenticate_user_query
|
||||
from pathlib import Path as FilePath
|
||||
from config import CONTENT_PATH
|
||||
|
||||
router = APIRouter(prefix="/content", tags=["content"])
|
||||
|
||||
@router.get("/{filename}")
|
||||
async def serve_content(
|
||||
username: str = Depends(authenticate_user_query),
|
||||
filename: str = Path(..., min_length=1), # Ensure filename is not empty
|
||||
):
|
||||
"""
|
||||
Serves a specific file from the configured content directory by filename only.
|
||||
Prevents directory traversal and ensures files are served from the base content directory.
|
||||
"""
|
||||
base_dir = FilePath(CONTENT_PATH).resolve()
|
||||
requested_file = base_dir / filename
|
||||
print(f"Requested file: {requested_file}")
|
||||
# Security checks
|
||||
try:
|
||||
# Resolve the full path and check it's within base directory
|
||||
requested_file = requested_file.resolve()
|
||||
if not requested_file.is_relative_to(base_dir):
|
||||
raise ValueError("Invalid path")
|
||||
except (ValueError, FileNotFoundError):
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
if not requested_file.is_file():
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
return FileResponse(requested_file)
|
||||
@@ -1,16 +0,0 @@
|
||||
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}"}
|
||||
@@ -1,16 +1,12 @@
|
||||
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()
|
||||
from fastapi import Depends, HTTPException, status, Query
|
||||
from fastapi.security import HTTPBasicCredentials, HTTPBasic
|
||||
from passlib.context import CryptContext
|
||||
from config import SUPER_ADMIN_USER, SUPER_ADMIN_PASSWORD, DATABASE_PATH
|
||||
|
||||
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
|
||||
security = HTTPBasic()
|
||||
admin_security = HTTPBasic()
|
||||
|
||||
async def get_user(username: str):
|
||||
# Use the dependency pattern even for internal calls for consistency
|
||||
@@ -53,11 +49,8 @@ async def authenticate_user_query(
|
||||
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):
|
||||
if not (credentials.username == SUPER_ADMIN_USER and
|
||||
credentials.password == SUPER_ADMIN_PASSWORD):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect superadmin credentials",
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
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)
|
||||
|
||||
from config import DATABASE_PATH
|
||||
|
||||
async def get_db():
|
||||
async with aiosqlite.connect(DATABASE_PATH) as db:
|
||||
|
||||
Reference in New Issue
Block a user