mirror of
https://github.com/sfiorini/iptv-server.git
synced 2026-04-11 11:40:45 +00:00
First commit
This commit is contained in:
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