import logging from fastapi import APIRouter, Depends, HTTPException, Request, Response, status from pydantic import BaseModel from sqlalchemy import select from sqlalchemy.orm import Session from datetime import datetime, timezone import ulid logger = logging.getLogger(__name__) from app.core.database.db_session import get_db from app.core.permissions.RoleChecker import get_current_user, RoleChecker from app.models.UserModel import User from app.models.UserSessionModel import UserSession from app.models.AuditLogModel import AuditLog from app.repositories.setting_repository import setting_repository from app.repositories.user_repository import user_repository from app.services.audit_service import audit_service from app.utils.Mfa_util import verify_mfa_token router = APIRouter(prefix="/api/v1/admin", tags=["Admin Control & Infrastructure"]) MASTER_ADMIN_EMAILS = {"adithiyan.elan@gmail.com", "admin@ifixkart.com"} def get_client_ip(request: Request) -> str: x_forwarded_for = request.headers.get("x-forwarded-for") if x_forwarded_for: return x_forwarded_for.split(",")[0].strip() return request.client.host if request.client else "127.0.0.1" @router.post("/request_kill_switch") def request_kill_switch( approver_id: str, mfa_code: str, request: Request, current_user: User = Depends(RoleChecker(["Super Admin", "Admin"])), db: Session = Depends(get_db) ): admin = user_repository.get_by_id(db, approver_id) if not admin: raise HTTPException(status_code=404, detail="Admin not found") if admin.email not in MASTER_ADMIN_EMAILS: raise HTTPException(status_code=403, detail="Only Master Admin can trigger the kill switch") if not admin.mfa_enabled or not verify_mfa_token(admin.mfa_secret, mfa_code): raise HTTPException(status_code=403, detail="Invalid MFA verification code") # 1. Update setting key 'GLOBAL_DISABLE' to enabled=True setting = setting_repository.get_by_key(db, "GLOBAL_DISABLE") if setting: setting.setting_value = {"enabled": True, "triggered_by": admin.email, "timestamp": str(datetime.now(timezone.utc))} db.commit() # 2. Destroy all active user sessions (Self-Destruct active logins) db.query(UserSession).update({UserSession.is_active: False}) db.commit() # 3. Log event ip_address = get_client_ip(request) user_agent = request.headers.get("user-agent", "") req_id = getattr(request.state, "request_id", "unknown") audit_service.log_change( db=db, request_id=req_id, user_id=admin.user_id, entity_type="SystemSetting", entity_id="GLOBAL_DISABLE", action="kill_switch_executed", old_value={"enabled": False}, new_value={"enabled": True}, ip_address=ip_address, user_agent=user_agent ) return {"detail": "Kill switch executed. All user sessions have been terminated."} @router.post("/self_destruct") def self_destruct( admin_id: str, mfa_code: str, request: Request, current_user: User = Depends(RoleChecker(["Super Admin", "Admin"])), db: Session = Depends(get_db) ): admin = user_repository.get_by_id(db, admin_id) if not admin: raise HTTPException(status_code=404, detail="Admin not found") if admin.email not in MASTER_ADMIN_EMAILS: raise HTTPException(status_code=403, detail="Only Master Admin can force self-destruct") if not admin.mfa_enabled or not verify_mfa_token(admin.mfa_secret, mfa_code): raise HTTPException(status_code=403, detail="Invalid MFA verification code") # 1. Terminate all sessions db.query(UserSession).update({UserSession.is_active: False}) db.commit() # 2. Log event ip_address = get_client_ip(request) user_agent = request.headers.get("user-agent", "") req_id = getattr(request.state, "request_id", "unknown") audit_service.log_change( db=db, request_id=req_id, user_id=admin.user_id, entity_type="Sessions", entity_id="All", action="self_destruct_sessions", old_value=None, new_value={"sessions_terminated": True}, ip_address=ip_address, user_agent=user_agent ) return {"detail": "All sessions destroyed successfully."} @router.post("/cancel_kill_switch") def cancel_kill_switch( admin_id: str, request: Request, current_user: User = Depends(RoleChecker(["Super Admin", "Admin"])), db: Session = Depends(get_db) ): admin = user_repository.get_by_id(db, admin_id) if not admin: raise HTTPException(status_code=404, detail="Admin not found") if admin.email not in MASTER_ADMIN_EMAILS: raise HTTPException(status_code=403, detail="Only Master Admin can cancel the kill switch") # 1. Set GLOBAL_DISABLE to False setting = setting_repository.get_by_key(db, "GLOBAL_DISABLE") if setting: setting.setting_value = {"enabled": False} db.commit() # 2. Log event ip_address = get_client_ip(request) user_agent = request.headers.get("user-agent", "") req_id = getattr(request.state, "request_id", "unknown") audit_service.log_change( db=db, request_id=req_id, user_id=admin.user_id, entity_type="SystemSetting", entity_id="GLOBAL_DISABLE", action="kill_switch_cancelled", old_value={"enabled": True}, new_value={"enabled": False}, ip_address=ip_address, user_agent=user_agent ) return {"detail": "Kill switch cancelled. Standard user access restored."} @router.get("/list_sessions") def list_sessions( skip: int = 0, limit: int = 100, current_user: User = Depends(RoleChecker(["Super Admin", "Admin"])), db: Session = Depends(get_db) ): sessions = db.query(UserSession).offset(skip).limit(limit).all() return { "sessions": [ { "session_id": s.session_id, "user_id": s.user_id, "device_name": s.device_name, "device_type": s.device_type, "browser": s.browser, "operating_system": s.operating_system, "ip_address": s.ip_address, "expires_at": s.expires_at, "is_active": s.is_active } for s in sessions ] } @router.post("/revoke_session") def revoke_session( session_id: str, current_user: User = Depends(RoleChecker(["Super Admin", "Admin"])), db: Session = Depends(get_db) ): session = db.get(UserSession, session_id) if not session: raise HTTPException(status_code=404, detail="Session not found") session.is_active = False db.commit() return {"detail": f"Session {session_id} has been revoked successfully."} @router.get("/failed-logins") def get_failed_logins( limit: int = 10, current_user: User = Depends(RoleChecker(["Super Admin", "Admin"])), db: Session = Depends(get_db) ): failed_attempts = ( db.query(AuditLog) .filter(AuditLog.action == "failed_login") .order_by(AuditLog.created_at.desc()) .limit(limit) .all() ) return failed_attempts @router.get("/audit-logs") def get_all_audit_logs( limit: int = 100, current_user: User = Depends(RoleChecker(["Super Admin", "Admin"])), db: Session = Depends(get_db) ): """ Retrieve dynamic audit logs across all users and modules for ERP Activity Monitor. """ from app.models.UserModel import User as DbUser results = ( db.query(AuditLog, DbUser) .outerjoin(DbUser, AuditLog.user_id == DbUser.user_id) .order_by(AuditLog.created_at.desc()) .limit(limit) .all() ) output = [] for log, user in results: user_info = None if user: user_info = { "name": f"{user.first_name} {user.last_name}", "avatarInitials": f"{user.first_name[0].upper() if user.first_name else ''}{user.last_name[0].upper() if user.last_name else ''}" } else: user_info = { "name": "System / Guest", "avatarInitials": "SYS" } output.append({ "audit_id": log.audit_id, "request_id": log.request_id, "user_id": log.user_id, "entity_type": log.entity_type, "entity_id": log.entity_id, "action": log.action, "old_value": log.old_value, "new_value": log.new_value, "ip_address": log.ip_address, "user_agent": log.user_agent, "created_at": log.created_at.isoformat() if log.created_at else None, "user": user_info }) return output # --- Media Storage & Factory Reset Endpoints --- from app.utils.Hash_util import verify_password from app.core.database.init_db import initialize_database, Base, engine_core, engine_crm, engine_commerce from app.models.SettingModel import Setting import shutil from pathlib import Path BACKEND_ROOT = Path(__file__).resolve().parents[4] UPLOADS_DIR = BACKEND_ROOT / "uploads" @router.get("/media-settings") def get_media_settings_admin( current_user: User = Depends(RoleChecker(["Super Admin", "Admin"])), db: Session = Depends(get_db) ): keys = ["media_store_original", "media_max_size_mb", "media_webp_quality", "media_cleanup_enabled", "media_cleanup_grace_hours"] settings = {} rows = db.execute(select(Setting).where(Setting.setting_key.in_(keys))).scalars().all() for r in rows: val = r.setting_value if isinstance(val, dict) and "value" in val: val = val["value"] settings[r.setting_key] = val return settings @router.post("/media-settings") def update_media_settings_admin( payload: dict, current_user: User = Depends(RoleChecker(["Super Admin", "Admin"])), db: Session = Depends(get_db) ): keys = ["media_store_original", "media_max_size_mb", "media_webp_quality", "media_cleanup_enabled", "media_cleanup_grace_hours"] for k in keys: if k in payload: setting = db.execute(select(Setting).where(Setting.setting_key == k)).scalar_one_or_none() if not setting: setting = Setting( setting_id=str(ulid.ULID()), setting_key=k, group="media", type="json", setting_value={"value": payload[k]}, description=f"Media policy setting: {k}", is_public=False ) db.add(setting) else: setting.setting_value = {"value": payload[k]} db.commit() return {"detail": "Media settings updated successfully"} class FactoryResetRequest(BaseModel): password: str confirmation_phrase: str @router.post("/security/factory-reset") def platform_factory_reset( payload: FactoryResetRequest, request: Request, current_user: User = Depends(RoleChecker(["Super Admin"])), db: Session = Depends(get_db) ): password = payload.password confirmation_phrase = payload.confirmation_phrase if not verify_password(password, current_user.password_hash): raise HTTPException(status_code=403, detail="Invalid Super Admin password verification") if confirmation_phrase != "CONFIRM_FACTORY_RESET_WIPE_2026": raise HTTPException(status_code=400, detail="Invalid confirmation phrase. Type 'CONFIRM_FACTORY_RESET_WIPE_2026' to proceed.") logger.warning(f"CRITICAL: Super Admin {current_user.email} triggered platform factory reset!") # 1. Physical Media Uploads Wipe (Preserve folder structure) try: if UPLOADS_DIR.exists(): for item in UPLOADS_DIR.iterdir(): if item.is_dir(): shutil.rmtree(item, ignore_errors=True) item.mkdir(parents=True, exist_ok=True) else: item.unlink(missing_ok=True) UPLOADS_DIR.mkdir(parents=True, exist_ok=True) (UPLOADS_DIR / "raw").mkdir(parents=True, exist_ok=True) except Exception as err: logger.error(f"Error purging physical media directory: {err}") # 2. Database Fast Truncate & Re-initialization try: db.close() from sqlalchemy import text for eng in [engine_commerce, engine_crm, engine_core]: with eng.connect() as conn: conn.execute(text("SET FOREIGN_KEY_CHECKS = 0;")) tables = conn.execute(text("SHOW TABLES;")).fetchall() for tbl in tables: tbl_name = tbl[0] try: conn.execute(text(f"TRUNCATE TABLE `{tbl_name}`;")) except Exception: try: conn.execute(text(f"DELETE FROM `{tbl_name}`;")) except Exception: pass conn.execute(text("SET FOREIGN_KEY_CHECKS = 1;")) conn.commit() from app.core.database.init_db import initialize_database initialize_database() except Exception as err: logger.error(f"Factory reset DB wipe error: {err}") raise HTTPException(status_code=500, detail=f"Factory reset DB wipe failed: {str(err)}") return { "status": "success", "detail": "Platform factory reset completed successfully. All database tables and physical uploads have been wiped, and pristine master data has been re-seeded.", "timestamp": datetime.utcnow().isoformat() }