feat: Add admin dashboard with authentication - Admin login/logout with Bearer token authentication - Secure admin dashboard page in frontend - Real-time system monitoring (memory, disk, translations) - Rate limits and cleanup service monitoring - Protected admin endpoints - Updated README with full SaaS documentation

This commit is contained in:
2025-11-30 19:33:59 +01:00
parent 500502440c
commit 54d85f0b34
5 changed files with 845 additions and 86 deletions

149
main.py
View File

@@ -3,16 +3,20 @@ Document Translation API
FastAPI application for translating complex documents while preserving formatting
SaaS-ready with rate limiting, validation, and robust error handling
"""
from fastapi import FastAPI, UploadFile, File, Form, HTTPException, Request, Depends
from fastapi import FastAPI, UploadFile, File, Form, HTTPException, Request, Depends, Header
from fastapi.responses import FileResponse, JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Optional
import asyncio
import logging
import os
import secrets
import hashlib
import time
from config import config
from translators import excel_translator, word_translator, pptx_translator
@@ -31,6 +35,57 @@ logging.basicConfig(
)
logger = logging.getLogger(__name__)
# ============== Admin Authentication ==============
ADMIN_USERNAME = os.getenv("ADMIN_USERNAME", "admin")
ADMIN_PASSWORD_HASH = os.getenv("ADMIN_PASSWORD_HASH", "") # SHA256 hash of password
ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "changeme123") # Default password (change in production!)
ADMIN_TOKEN_SECRET = os.getenv("ADMIN_TOKEN_SECRET", secrets.token_hex(32))
# Store active admin sessions (token -> expiry timestamp)
admin_sessions: dict = {}
def hash_password(password: str) -> str:
"""Hash password with SHA256"""
return hashlib.sha256(password.encode()).hexdigest()
def verify_admin_password(password: str) -> bool:
"""Verify admin password"""
if ADMIN_PASSWORD_HASH:
return hash_password(password) == ADMIN_PASSWORD_HASH
return password == ADMIN_PASSWORD
def create_admin_token() -> str:
"""Create a new admin session token"""
token = secrets.token_urlsafe(32)
# Token expires in 24 hours
admin_sessions[token] = time.time() + (24 * 60 * 60)
return token
def verify_admin_token(token: str) -> bool:
"""Verify admin token is valid and not expired"""
if token not in admin_sessions:
return False
if time.time() > admin_sessions[token]:
del admin_sessions[token]
return False
return True
async def require_admin(authorization: Optional[str] = Header(None)) -> bool:
"""Dependency to require admin authentication"""
if not authorization:
raise HTTPException(status_code=401, detail="Authorization header required")
# Expect "Bearer <token>"
parts = authorization.split(" ")
if len(parts) != 2 or parts[0].lower() != "bearer":
raise HTTPException(status_code=401, detail="Invalid authorization format. Use: Bearer <token>")
token = parts[1]
if not verify_admin_token(token):
raise HTTPException(status_code=401, detail="Invalid or expired token")
return True
# Initialize SaaS components
rate_limit_config = RateLimitConfig(
requests_per_minute=int(os.getenv("RATE_LIMIT_PER_MINUTE", "30")),
@@ -773,6 +828,87 @@ async def reconstruct_document(
# ============== SaaS Management Endpoints ==============
@app.post("/admin/login")
async def admin_login(
username: str = Form(...),
password: str = Form(...)
):
"""
Admin login endpoint
Returns a bearer token for authenticated admin access
"""
if username != ADMIN_USERNAME:
logger.warning(f"Failed admin login attempt with username: {username}")
raise HTTPException(status_code=401, detail="Invalid credentials")
if not verify_admin_password(password):
logger.warning(f"Failed admin login attempt - wrong password")
raise HTTPException(status_code=401, detail="Invalid credentials")
token = create_admin_token()
logger.info(f"Admin login successful")
return {
"status": "success",
"token": token,
"expires_in": 86400, # 24 hours in seconds
"message": "Login successful"
}
@app.post("/admin/logout")
async def admin_logout(authorization: Optional[str] = Header(None)):
"""Logout and invalidate admin token"""
if authorization:
parts = authorization.split(" ")
if len(parts) == 2 and parts[0].lower() == "bearer":
token = parts[1]
if token in admin_sessions:
del admin_sessions[token]
logger.info("Admin logout successful")
return {"status": "success", "message": "Logged out"}
@app.get("/admin/verify")
async def verify_admin_session(is_admin: bool = Depends(require_admin)):
"""Verify admin token is still valid"""
return {"status": "valid", "authenticated": True}
@app.get("/admin/dashboard")
async def get_admin_dashboard(is_admin: bool = Depends(require_admin)):
"""Get comprehensive admin dashboard data"""
health_status = await health_checker.check_health()
cleanup_stats = cleanup_manager.get_stats()
rate_limit_stats = rate_limit_manager.get_stats()
tracked_files = cleanup_manager.get_tracked_files()
return {
"timestamp": health_status.get("timestamp"),
"uptime": health_status.get("uptime_human"),
"status": health_status.get("status"),
"issues": health_status.get("issues", []),
"system": {
"memory": health_status.get("memory", {}),
"disk": health_status.get("disk", {}),
},
"translations": health_status.get("translations", {}),
"cleanup": {
**cleanup_stats,
"tracked_files_count": len(tracked_files)
},
"rate_limits": rate_limit_stats,
"config": {
"max_file_size_mb": config.MAX_FILE_SIZE_MB,
"supported_extensions": list(config.SUPPORTED_EXTENSIONS),
"translation_service": config.TRANSLATION_SERVICE,
"rate_limit_per_minute": rate_limit_config.requests_per_minute,
"translations_per_minute": rate_limit_config.translations_per_minute
}
}
@app.get("/metrics")
async def get_metrics():
"""Get system metrics and statistics for monitoring"""
@@ -815,8 +951,8 @@ async def get_rate_limit_status(request: Request):
@app.post("/admin/cleanup/trigger")
async def trigger_cleanup():
"""Trigger manual cleanup of expired files"""
async def trigger_cleanup(is_admin: bool = Depends(require_admin)):
"""Trigger manual cleanup of expired files (requires admin auth)"""
try:
cleaned = await cleanup_manager.cleanup_expired()
return {
@@ -830,8 +966,8 @@ async def trigger_cleanup():
@app.get("/admin/files/tracked")
async def get_tracked_files():
"""Get list of currently tracked files"""
async def get_tracked_files(is_admin: bool = Depends(require_admin)):
"""Get list of currently tracked files (requires admin auth)"""
tracked = cleanup_manager.get_tracked_files()
return {
"count": len(tracked),
@@ -841,5 +977,4 @@ async def get_tracked_files():
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)