diff --git a/.gitignore b/.gitignore index 35862ba..9908c0d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .env .venv/ +content/ data/ __pycache__/ *.egg-info/ diff --git a/src/config.py b/src/config.py new file mode 100644 index 0000000..7bd3aaa --- /dev/null +++ b/src/config.py @@ -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) diff --git a/src/main.py b/src/main.py index c58b871..d17cd74 100644 --- a/src/main.py +++ b/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( diff --git a/src/routers/admin.py b/src/routers/admin.py index 605cea6..307fefc 100644 --- a/src/routers/admin.py +++ b/src/routers/admin.py @@ -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 diff --git a/src/routers/content.py b/src/routers/content.py new file mode 100644 index 0000000..72508fb --- /dev/null +++ b/src/routers/content.py @@ -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) diff --git a/src/routers/user.py b/src/routers/user.py deleted file mode 100644 index c3775df..0000000 --- a/src/routers/user.py +++ /dev/null @@ -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}"} diff --git a/src/utils/auth.py b/src/utils/auth.py index a635f0a..4875b70 100644 --- a/src/utils/auth.py +++ b/src/utils/auth.py @@ -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", diff --git a/src/utils/database.py b/src/utils/database.py index f068292..c2535c7 100644 --- a/src/utils/database.py +++ b/src/utils/database.py @@ -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: