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:
149
main.py
149
main.py
@@ -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)
|
||||
Reference in New Issue
Block a user