379 lines
13 KiB
Python
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()
|
|
}
|
|
|