ifixkart-backend/Backend/app/api/v1/routers/AdminSecurityRouter.py

379 lines
13 KiB
Python

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()
}