Files
gatehouse-api/gatehouse_app/api/v1/superadmin/auth.py
T

287 lines
8.7 KiB
Python

"""Superadmin authentication endpoints."""
import logging
from flask import request, g, current_app
from marshmallow import ValidationError
from gatehouse_app.api.v1.superadmin import superadmin_bp
from gatehouse_app.extensions import limiter
from gatehouse_app.utils.response import api_response
from gatehouse_app.services.superadmin_auth_service import SuperadminAuthService
from gatehouse_app.decorators.superadmin import superadmin_required, superadmin_audit_log
from gatehouse_app.exceptions.auth_exceptions import InvalidCredentialsError
logger = logging.getLogger(__name__)
class LoginSchema:
"""Schema for superadmin login."""
@staticmethod
def load(data):
"""Validate login data."""
errors = {}
if not data.get('email'):
errors['email'] = ['Email is required']
elif '@' not in data['email']:
errors['email'] = ['Invalid email format']
if not data.get('password'):
errors['password'] = ['Password is required']
if errors:
raise ValidationError(errors)
return {
'email': data['email'].lower().strip(),
'password': data['password'],
}
@superadmin_bp.route("/auth/login", methods=["POST"])
@limiter.limit(lambda: current_app.config.get("RATELIMIT_AUTH_LOGIN", "100 per minute"))
def login():
"""Superadmin login endpoint.
Authenticates with email/password and returns a session token.
"""
try:
schema = LoginSchema()
data = schema.load(request.json)
# Authenticate
superadmin = SuperadminAuthService.authenticate(
email=data['email'],
credentials=data['password']
)
# Create session (default 8 hours)
session = SuperadminAuthService.create_session(
superadmin_id=superadmin.id,
duration_seconds=28800 # 8 hours
)
expires_str = session.expires_at.isoformat()
if not expires_str.endswith('Z'):
expires_str += 'Z'
logger.info(f"[SuperadminAuth] Login successful for: {superadmin.email}")
return api_response(
data={
"superadmin": superadmin.to_dict(),
"token": session.token,
"expires_at": expires_str,
},
message="Login successful",
status=200
)
except ValidationError as e:
return api_response(
success=False,
message="Validation failed",
status=400,
error_type="VALIDATION_ERROR",
error_details=e.messages
)
except InvalidCredentialsError:
return api_response(
success=False,
message="Invalid email or password",
status=401,
error_type="INVALID_CREDENTIALS"
)
except Exception as e:
logger.error(f"[SuperadminAuth] Login error: {e}")
return api_response(
success=False,
message="An error occurred during login",
status=500,
error_type="INTERNAL_ERROR"
)
@superadmin_bp.route("/auth/logout", methods=["POST"])
@superadmin_required
def logout():
"""Superadmin logout endpoint.
Invalidates the current session.
"""
try:
session = g.superadmin_session
if session:
SuperadminAuthService.revoke_session(session.id, reason="Superadmin logout")
return api_response(
message="Logout successful"
)
except Exception as e:
logger.error(f"[SuperadminAuth] Logout error: {e}")
return api_response(
success=False,
message="An error occurred during logout",
status=500,
error_type="INTERNAL_ERROR"
)
@superadmin_bp.route("/auth/me", methods=["GET"])
@superadmin_required
def get_current_superadmin():
"""Get current superadmin profile.
Returns the profile of the currently authenticated superadmin.
"""
try:
superadmin = g.current_superadmin
return api_response(
data={
"superadmin": superadmin.to_dict(),
},
message="Superadmin retrieved successfully"
)
except Exception as e:
logger.error(f"[SuperadminAuth] Get me error: {e}")
return api_response(
success=False,
message="An error occurred",
status=500,
error_type="INTERNAL_ERROR"
)
@superadmin_bp.route("/auth/impersonate/<user_id>", methods=["POST"])
@superadmin_required
@superadmin_audit_log(action="impersonate", resource_type="user")
def impersonate_user(user_id):
"""Create emergency access session by impersonating a user.
Creates a temporary session for the target user that allows
the superadmin to access the platform as that user.
This action is fully audited.
"""
try:
superadmin = g.current_superadmin
data = request.json or {}
reason = data.get('reason', 'Not specified')
duration_minutes = data.get('duration_minutes', 15)
# Limit duration to max 60 minutes
duration_minutes = min(duration_minutes, 60)
# Create emergency access
result = SuperadminAuthService.create_emergency_access(
superadmin_id=superadmin.id,
target_user_id=user_id,
reason=reason,
duration_minutes=duration_minutes
)
expires_str = result['expires_at'].isoformat()
if not expires_str.endswith('Z'):
expires_str += 'Z'
logger.warning(
f"[SuperadminAuth] IMPERSONATION: superadmin={superadmin.email} "
f"impersonated user_id={user_id} reason={reason}"
)
return api_response(
data={
"session_token": result['session'].token,
"expires_at": expires_str,
"target_user_id": user_id,
"reason": reason,
"duration_minutes": duration_minutes,
},
message="Emergency access session created",
status=201
)
except ValueError as e:
return api_response(
success=False,
message=str(e),
status=404,
error_type="NOT_FOUND"
)
except Exception as e:
logger.error(f"[SuperadminAuth] Impersonate error: {e}")
return api_response(
success=False,
message="An error occurred",
status=500,
error_type="INTERNAL_ERROR"
)
@superadmin_bp.route("/auth/emergency/<user_id>", methods=["POST"])
@superadmin_required
@superadmin_audit_log(action="emergency_access", resource_type="user")
def grant_emergency_access(user_id):
"""Grant temporary elevated access to a user.
Similar to impersonate but grants elevated permissions
rather than creating a session as the user.
This action is fully audited.
"""
try:
superadmin = g.current_superadmin
data = request.json or {}
reason = data.get('reason', 'Not specified')
duration_minutes = data.get('duration_minutes', 15)
# Limit duration to max 60 minutes
duration_minutes = min(duration_minutes, 60)
# Create emergency access
result = SuperadminAuthService.create_emergency_access(
superadmin_id=superadmin.id,
target_user_id=user_id,
reason=reason,
duration_minutes=duration_minutes
)
expires_str = result['expires_at'].isoformat()
if not expires_str.endswith('Z'):
expires_str += 'Z'
logger.warning(
f"[SuperadminAuth] EMERGENCY ACCESS: superadmin={superadmin.email} "
f"granted access to user_id={user_id} reason={reason}"
)
return api_response(
data={
"session_token": result['session'].token,
"expires_at": expires_str,
"target_user_id": user_id,
"reason": reason,
"duration_minutes": duration_minutes,
},
message="Emergency access granted",
status=201
)
except ValueError as e:
return api_response(
success=False,
message=str(e),
status=404,
error_type="NOT_FOUND"
)
except Exception as e:
logger.error(f"[SuperadminAuth] Emergency access error: {e}")
return api_response(
success=False,
message="An error occurred",
status=500,
error_type="INTERNAL_ERROR"
)