Add superadmin routes to API
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
"""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"
|
||||
)
|
||||
Reference in New Issue
Block a user