gateway/modules/routes/routeAdmin.py
2025-09-22 07:44:39 +02:00

299 lines
12 KiB
Python

from fastapi import APIRouter, Response, Depends, Request, Body
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
import os
import logging
from pathlib import Path as FilePath
from typing import Dict, Any, List
from fastapi import HTTPException, status
from datetime import datetime
from modules.shared.configuration import APP_CONFIG
from modules.security.auth import limiter, getCurrentUser
from modules.interfaces.interfaceAppModel import User
from modules.interfaces.interfaceAppObjects import getRootInterface
from modules.interfaces.interfaceChatObjects import getInterface as getChatInterface
from modules.interfaces.interfaceComponentObjects import getInterface as getComponentInterface
# Static folder setup - using absolute path from app root
baseDir = FilePath(__file__).parent.parent.parent # Go up to gateway root
staticFolder = baseDir / "static"
os.makedirs(staticFolder, exist_ok=True)
logger = logging.getLogger(__name__)
router = APIRouter(
prefix="",
tags=["Administration"],
responses={404: {"description": "Not found"}}
)
# Mount static files
router.mount("/static", StaticFiles(directory=str(staticFolder), html=True), name="static")
def get_interface_for_database(database_name: str, currentUser: User):
"""
Get the appropriate interface based on database name.
Args:
database_name: Name of the database
currentUser: Current user for interface initialization
Returns:
Interface object for the specified database
Raises:
HTTPException: If database name is unknown or interface cannot be created
"""
# Get database names from configuration
appDbName = APP_CONFIG.get("DB_APP_DATABASE")
chatDbName = APP_CONFIG.get("DB_CHAT_DATABASE")
managementDbName = APP_CONFIG.get("DB_MANAGEMENT_DATABASE")
if not appDbName:
raise HTTPException(status_code=500, detail="DB_APP_DATABASE configuration is required")
# Map database names to their corresponding interfaces
if database_name == appDbName:
return getRootInterface()
elif chatDbName and database_name == chatDbName:
return getChatInterface(currentUser)
elif managementDbName and database_name == managementDbName:
return getComponentInterface(currentUser)
else:
available_dbs = [appDbName]
if chatDbName:
available_dbs.append(chatDbName)
if managementDbName:
available_dbs.append(managementDbName)
raise HTTPException(status_code=400, detail=f"Unknown database. Available: {', '.join(available_dbs)}")
@router.get("/")
@limiter.limit("30/minute")
async def root(request: Request) -> Dict[str, str]:
"""API status endpoint"""
# Validate required configuration values
allowedOrigins = APP_CONFIG.get("APP_ALLOWED_ORIGINS")
if not allowedOrigins:
raise HTTPException(status_code=500, detail="APP_ALLOWED_ORIGINS configuration is required")
return {
"status": "online",
"message": "Data Platform API is active",
"allowedOrigins": f"Allowed origins are {allowedOrigins}"
}
@router.get("/api/environment")
@limiter.limit("30/minute")
async def get_environment(request: Request, currentUser: Dict[str, Any] = Depends(getCurrentUser)) -> Dict[str, str]:
"""Get environment configuration for frontend"""
# Validate required configuration values
apiBaseUrl = APP_CONFIG.get("APP_API_URL")
if not apiBaseUrl:
raise HTTPException(status_code=500, detail="APP_API_URL configuration is required")
environment = APP_CONFIG.get("APP_ENV")
if not environment:
raise HTTPException(status_code=500, detail="APP_ENV configuration is required")
instanceLabel = APP_CONFIG.get("APP_ENV_LABEL")
if not instanceLabel:
raise HTTPException(status_code=500, detail="APP_ENV_LABEL configuration is required")
return {
"apiBaseUrl": apiBaseUrl,
"environment": environment,
"instanceLabel": instanceLabel,
# Add other environment variables the frontend might need
}
@router.options("/{fullPath:path}")
@limiter.limit("60/minute")
async def options_route(request: Request, fullPath: str) -> Response:
return Response(status_code=200)
@router.get("/favicon.ico")
@limiter.limit("30/minute")
async def favicon(request: Request) -> FileResponse:
return FileResponse(str(staticFolder / "favicon.ico"), media_type="image/x-icon")
# ----------------------
# Log Management
# ----------------------
@router.get("/api/logs/app")
@limiter.limit("10/minute")
async def download_app_log(request: Request, currentUser: User = Depends(getCurrentUser)) -> FileResponse:
"""Download the current day's application log file"""
# Check if user has admin privileges
if not hasattr(currentUser, 'privilege') or currentUser.privilege not in ('admin', 'sysadmin'):
raise HTTPException(status_code=403, detail="Admin privileges required")
# Get log directory from config
logDir = APP_CONFIG.get("APP_LOGGING_LOG_DIR")
if not logDir:
raise HTTPException(status_code=500, detail="APP_LOGGING_LOG_DIR configuration is required")
if not os.path.isabs(logDir):
# If relative path, make it relative to the gateway directory
gatewayDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
logDir = os.path.join(gatewayDir, logDir)
# Get current date for log file
today = datetime.now().strftime("%Y%m%d")
logFile = os.path.join(logDir, f"log_app_{today}.log")
if not os.path.exists(logFile):
raise HTTPException(status_code=404, detail=f"Application log file for today not found: {logFile}")
return FileResponse(
path=logFile,
filename=f"log_app_{today}.log",
media_type="text/plain"
)
@router.get("/api/logs/audit")
@limiter.limit("10/minute")
async def download_audit_log(request: Request, currentUser: User = Depends(getCurrentUser)) -> FileResponse:
"""Download the current day's audit log file"""
# Check if user has admin privileges
if not hasattr(currentUser, 'privilege') or currentUser.privilege not in ('admin', 'sysadmin'):
raise HTTPException(status_code=403, detail="Admin privileges required")
# Get log directory from config
logDir = APP_CONFIG.get("APP_LOGGING_LOG_DIR")
if not logDir:
raise HTTPException(status_code=500, detail="APP_LOGGING_LOG_DIR configuration is required")
if not os.path.isabs(logDir):
# If relative path, make it relative to the gateway directory
gatewayDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
logDir = os.path.join(gatewayDir, logDir)
# Get current date for log file
today = datetime.now().strftime("%Y%m%d")
logFile = os.path.join(logDir, f"log_audit_{today}.log")
if not os.path.exists(logFile):
raise HTTPException(status_code=404, detail=f"Audit log file for today not found: {logFile}")
return FileResponse(
path=logFile,
filename=f"log_audit_{today}.log",
media_type="text/plain"
)
# ----------------------
# Database Management
# ----------------------
@router.get("/api/databases")
@limiter.limit("10/minute")
async def list_databases(request: Request, currentUser: User = Depends(getCurrentUser)) -> Dict[str, Any]:
"""List available databases"""
# Check if user has admin privileges
if not hasattr(currentUser, 'privilege') or currentUser.privilege not in ('admin', 'sysadmin'):
raise HTTPException(status_code=403, detail="Admin privileges required")
try:
# Get configured database names from configuration
databases = []
# App database - required configuration
appDb = APP_CONFIG.get("DB_APP_DATABASE")
if not appDb:
raise HTTPException(status_code=500, detail="DB_APP_DATABASE configuration is required")
databases.append(appDb)
# Chat database - optional configuration
chatDb = APP_CONFIG.get("DB_CHAT_DATABASE")
if chatDb and chatDb not in databases:
databases.append(chatDb)
# Management database - optional configuration
managementDb = APP_CONFIG.get("DB_MANAGEMENT_DATABASE")
if managementDb and managementDb not in databases:
databases.append(managementDb)
return {"databases": databases}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error listing databases: {e}")
raise HTTPException(status_code=500, detail="Failed to list databases")
@router.get("/api/databases/{database_name}/tables")
@limiter.limit("10/minute")
async def list_tables(
request: Request,
database_name: str,
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:
"""List tables in a specific database"""
# Check if user has admin privileges
if not hasattr(currentUser, 'privilege') or currentUser.privilege not in ('admin', 'sysadmin'):
raise HTTPException(status_code=403, detail="Admin privileges required")
try:
# Get the appropriate interface based on database name
interface = get_interface_for_database(database_name, currentUser)
# Check if interface and database connection exist
if not interface or not interface.db:
raise HTTPException(status_code=500, detail="Database interface not available")
# Get tables from database
tables = interface.db.getTables()
return {"database": database_name, "tables": tables}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error listing tables for database {database_name}: {e}")
raise HTTPException(status_code=500, detail=f"Failed to list tables for database {database_name}")
@router.post("/api/databases/{database_name}/tables/drop")
@limiter.limit("5/minute")
async def drop_table(
request: Request,
database_name: str,
currentUser: User = Depends(getCurrentUser),
payload: Dict[str, Any] = Body(...)
) -> Dict[str, Any]:
"""Drop a specific table from a database"""
# Check if user has admin privileges
if not hasattr(currentUser, 'privilege') or currentUser.privilege not in ('admin', 'sysadmin'):
raise HTTPException(status_code=403, detail="Admin privileges required")
table_name = payload.get("table")
if not table_name:
raise HTTPException(status_code=400, detail="Table name is required")
try:
# Get the appropriate interface based on database name
interface = get_interface_for_database(database_name, currentUser)
# Check if interface and database connection exist
if not interface or not interface.db:
raise HTTPException(status_code=500, detail="Database interface not available")
# Check if table exists
tables = interface.db.getTables()
if table_name not in tables:
raise HTTPException(status_code=404, detail=f"Table '{table_name}' not found in database '{database_name}'")
# Drop the table
with interface.db.connection.cursor() as cursor:
cursor.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE')
interface.db.connection.commit()
logger.warning(f"Admin drop_table executed by {currentUser.id}: dropped table '{table_name}' from database '{database_name}'")
return {"message": f"Table '{table_name}' dropped successfully from database '{database_name}'"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error dropping table {table_name} from database {database_name}: {e}")
if 'interface' in locals() and interface.db.connection:
interface.db.connection.rollback()
raise HTTPException(status_code=500, detail=f"Failed to drop table '{table_name}' from database '{database_name}'")