287 lines
8.7 KiB
Python
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"
|
|
)
|