feat: Add database migrations and auth system

- Add Alembic for database migrations
- Implement user authentication system
- Update frontend styles and components
- Add new test audio functionality
- Update stream management and UI
This commit is contained in:
oib
2025-07-02 09:37:03 +02:00
parent 39934115a1
commit 17616ac5b8
49 changed files with 5059 additions and 804 deletions

146
main.py
View File

@ -1,6 +1,6 @@
# main.py — FastAPI backend entrypoint for dicta2stream
from fastapi import FastAPI, Request, Response, status, Form, UploadFile, File, Depends
from fastapi import FastAPI, Request, Response, status, Form, UploadFile, File, Depends, HTTPException
from fastapi.responses import HTMLResponse, RedirectResponse, StreamingResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
@ -40,12 +40,20 @@ app = FastAPI(debug=debug_mode)
# --- CORS Middleware for SSE and API access ---
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
# Add GZip middleware for compression
app.add_middleware(GZipMiddleware, minimum_size=1000)
# Configure CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["https://dicta2stream.net", "http://localhost:8000", "http://127.0.0.1:8000"],
allow_credentials=True,
allow_methods=["*"],
allow_methods=["GET", "POST", "OPTIONS"],
allow_headers=["*"],
expose_headers=["Content-Type", "Content-Length", "Cache-Control", "ETag", "Last-Modified"],
max_age=3600, # 1 hour
)
from fastapi.staticfiles import StaticFiles
@ -125,7 +133,9 @@ from list_user_files import router as list_user_files_router
app.include_router(streams_router)
from list_streams import router as list_streams_router
from account_router import router as account_router
app.include_router(account_router)
app.include_router(register_router)
app.include_router(magic_router)
app.include_router(upload_router)
@ -135,6 +145,10 @@ app.include_router(list_streams_router)
# Serve static files
app.mount("/static", StaticFiles(directory="static"), name="static")
# Serve audio files
os.makedirs("data", exist_ok=True) # Ensure the data directory exists
app.mount("/audio", StaticFiles(directory="data"), name="audio")
@app.post("/log-client")
async def log_client(request: Request):
try:
@ -224,68 +238,7 @@ def debug(request: Request):
MAX_QUOTA_BYTES = 100 * 1024 * 1024
@app.post("/delete-account")
async def delete_account(data: dict, request: Request, db: Session = Depends(get_db)):
uid = data.get("uid")
if not uid:
raise HTTPException(status_code=400, detail="Missing UID")
ip = request.client.host
user = get_user_by_uid(uid)
if not user or user.ip != ip:
raise HTTPException(status_code=403, detail="Unauthorized")
# Delete user quota and user using ORM
quota = db.get(UserQuota, uid)
if quota:
db.delete(quota)
user_obj = db.get(User, user.email)
if user_obj:
db.delete(user_obj)
db.commit()
import shutil
user_dir = os.path.join('data', user.username)
real_user_dir = os.path.realpath(user_dir)
if not real_user_dir.startswith(os.path.realpath('data')):
raise HTTPException(status_code=400, detail="Invalid user directory")
if os.path.exists(real_user_dir):
shutil.rmtree(real_user_dir, ignore_errors=True)
return {"message": "User deleted"}
from fastapi.concurrency import run_in_threadpool
# from detect_content_type_whisper_ollama import detect_content_type_whisper_ollama # Broken import: module not found
content_type = None
if content_type in ["music", "singing"]:
os.remove(raw_path)
log_violation("UPLOAD", ip, uid, f"Rejected content: {content_type}")
return JSONResponse(status_code=403, content={"error": f"{content_type.capitalize()} uploads are not allowed."})
try:
subprocess.run([
"ffmpeg", "-y", "-i", raw_path,
"-ac", "1", "-ar", "48000",
"-c:a", "libopus", "-b:a", "60k",
final_path
], check=True)
except subprocess.CalledProcessError as e:
os.remove(raw_path)
log_violation("FFMPEG", ip, uid, f"ffmpeg failed: {e}")
raise HTTPException(status_code=500, detail="Encoding failed")
os.remove(raw_path)
try:
actual_bytes = int(subprocess.check_output(["du", "-sb", user_dir]).split()[0])
q = db.get(UserQuota, uid)
if q:
q.storage_bytes = actual_bytes
db.add(q)
db.commit()
except Exception as e:
log_violation("QUOTA", ip, uid, f"Quota update failed: {e}")
return {}
# Delete account endpoint has been moved to account_router.py
@app.delete("/uploads/{uid}/{filename}")
def delete_file(uid: str, filename: str, request: Request, db: Session = Depends(get_db)):
@ -333,24 +286,51 @@ def confirm_user(uid: str, request: Request):
@app.get("/me/{uid}")
def get_me(uid: str, request: Request, db: Session = Depends(get_db)):
ip = request.client.host
user = get_user_by_uid(uid)
if not user or user.ip != ip:
raise HTTPException(status_code=403, detail="Unauthorized access")
print(f"[DEBUG] GET /me/{uid} - Client IP: {request.client.host}")
try:
# Get user info
user = get_user_by_uid(uid)
if not user:
print(f"[ERROR] User with UID {uid} not found")
raise HTTPException(status_code=403, detail="User not found")
if user.ip != request.client.host:
print(f"[ERROR] IP mismatch for UID {uid}: {request.client.host} != {user.ip}")
raise HTTPException(status_code=403, detail="IP address mismatch")
user_dir = os.path.join('data', user.username)
files = []
if os.path.exists(user_dir):
for f in os.listdir(user_dir):
path = os.path.join(user_dir, f)
if os.path.isfile(path):
files.append({"name": f, "size": os.path.getsize(path)})
# Get all upload logs for this user
upload_logs = db.exec(
select(UploadLog)
.where(UploadLog.uid == uid)
.order_by(UploadLog.created_at.desc())
).all()
print(f"[DEBUG] Found {len(upload_logs)} upload logs for UID {uid}")
# Build file list from database records
files = []
for log in upload_logs:
if log.filename and log.processed_filename:
# The actual filename on disk might have the log ID prepended
stored_filename = f"{log.id}_{log.processed_filename}"
files.append({
"name": stored_filename,
"original_name": log.filename,
"size": log.size_bytes
})
print(f"[DEBUG] Added file from DB: {log.filename} (stored as {stored_filename}, {log.size_bytes} bytes)")
# Get quota info
q = db.get(UserQuota, uid)
quota_mb = round(q.storage_bytes / (1024 * 1024), 2) if q else 0
print(f"[DEBUG] Quota for UID {uid}: {quota_mb} MB")
q = db.get(UserQuota, uid)
quota_mb = round(q.storage_bytes / (1024 * 1024), 2) if q else 0
return {
"files": files,
"quota": quota_mb
}
response_data = {
"files": files,
"quota": quota_mb
}
print(f"[DEBUG] Returning {len(files)} files and quota info")
return response_data
except Exception as e:
print(f"[ERROR] Error in /me/{uid} endpoint: {str(e)}", exc_info=True)
raise