Files
gatehouse-api/gatehouse_app/services/superadmin_user_service.py
T

372 lines
12 KiB
Python

"""User management service for superadmin operations."""
import logging
from datetime import datetime, timezone
from gatehouse_app.models.user.user import User
from gatehouse_app.models.user.session import Session
from gatehouse_app.models.organization.organization import Organization
from gatehouse_app.models.organization.organization_member import OrganizationMember
from gatehouse_app.extensions import db
logger = logging.getLogger(__name__)
class SuperadminUserService:
"""Service for managing users across the platform."""
@staticmethod
def list_users(
page: int = 1,
per_page: int = 20,
org_id: str = None,
status: str = None,
search: str = None,
) -> dict:
"""List users with filters and pagination.
Args:
page: Page number
per_page: Items per page
org_id: Filter by organization
status: Filter by status (active/suspended)
search: Search by email or name
Returns:
Paginated user list with metadata
"""
query = User.query.filter(User.deleted_at.is_(None))
# Filter by organization
if org_id:
member_user_ids = db.session.query(OrganizationMember.user_id).filter(
OrganizationMember.organization_id == org_id,
OrganizationMember.deleted_at.is_(None),
).all()
user_ids = [m.user_id for m in member_user_ids]
query = query.filter(User.id.in_(user_ids))
# Filter by status
if status == "suspended":
query = query.filter(User.status == "GLOBAL_SUSPENDED")
elif status == "active":
query = query.filter(User.status != "GLOBAL_SUSPENDED")
# Search
if search:
search_filter = f"%{search}%"
query = query.filter(
db.or_(
User.email.ilike(search_filter),
User.full_name.ilike(search_filter),
)
)
query = query.order_by(User.created_at.desc())
total = query.count()
users = query.offset((page - 1) * per_page).limit(per_page).all()
items = []
for user in users:
# Get org memberships
memberships = OrganizationMember.query.filter(
OrganizationMember.user_id == user.id,
OrganizationMember.deleted_at.is_(None),
).all()
orgs = []
for m in memberships:
org = Organization.query.get(m.organization_id)
if org:
orgs.append({
"org_id": org.id,
"org_name": org.name,
"role": m.role,
})
# Get active sessions count
active_sessions = Session.query.filter(
Session.user_id == user.id,
Session.deleted_at.is_(None),
Session.status == "active",
).count()
items.append({
"id": user.id,
"email": user.email,
"full_name": user.full_name,
"status": user.status,
"org_count": len(orgs),
"orgs": orgs,
"active_sessions": active_sessions,
"last_login_at": user.last_login_at.isoformat() + "Z" if user.last_login_at else None,
"created_at": user.created_at.isoformat() + "Z" if user.created_at else None,
})
return {
"items": items,
"total": total,
"page": page,
"per_page": per_page,
"pages": (total + per_page - 1) // per_page if per_page > 0 else 0,
}
@staticmethod
def get_user_detail(user_id: str) -> dict:
"""Get detailed user information.
Args:
user_id: User UUID
Returns:
User detail with orgs, sessions, security methods
"""
user = User.query.get(user_id)
if not user or user.deleted_at is not None:
raise ValueError("User not found")
# Get org memberships
memberships = OrganizationMember.query.filter(
OrganizationMember.user_id == user_id,
OrganizationMember.deleted_at.is_(None),
).all()
orgs = []
for m in memberships:
org = Organization.query.get(m.organization_id)
if org:
orgs.append({
"org_id": org.id,
"org_name": org.name,
"org_slug": org.slug,
"role": m.role,
"joined_at": m.created_at.isoformat() + "Z" if m.created_at else None,
})
# Get active sessions
sessions = Session.query.filter(
Session.user_id == user_id,
Session.deleted_at.is_(None),
Session.status == "active",
).all()
active_sessions = [{
"id": s.id,
"ip_address": s.ip_address,
"user_agent": s.user_agent,
"created_at": s.created_at.isoformat() + "Z" if s.created_at else None,
} for s in sessions]
# Security methods
security_methods = []
if hasattr(user, 'totp_enabled') and user.totp_enabled:
security_methods.append({"type": "totp", "enabled": True})
if hasattr(user, 'webauthn_enabled') and user.webauthn_enabled:
security_methods.append({"type": "webauthn", "enabled": True})
return {
"user": {
"id": user.id,
"email": user.email,
"full_name": user.full_name,
"status": user.status,
"mfa_enabled": user.mfa_enabled if hasattr(user, 'mfa_enabled') else False,
"last_login_at": user.last_login_at.isoformat() + "Z" if user.last_login_at else None,
"created_at": user.created_at.isoformat() + "Z" if user.created_at else None,
},
"organizations": orgs,
"active_sessions": active_sessions,
"security_methods": security_methods,
}
@staticmethod
def suspend_user(user_id: str) -> dict:
"""Globally suspend a user.
Args:
user_id: User UUID
Returns:
Updated user info and count of revoked sessions
"""
user = User.query.get(user_id)
if not user or user.deleted_at is not None:
raise ValueError("User not found")
if user.status == "GLOBAL_SUSPENDED":
raise ValueError("User is already suspended")
user.status = "GLOBAL_SUSPENDED"
db.session.commit()
# Revoke all sessions
revoked_count = Session.query.filter(
Session.user_id == user_id,
Session.deleted_at.is_(None),
).update({"status": "revoked", "deleted_at": db.func.now()})
db.session.commit()
return {
"user": {
"id": user.id,
"email": user.email,
"status": user.status,
},
"sessions_revoked": revoked_count,
}
@staticmethod
def unsuspend_user(user_id: str) -> dict:
"""Remove global suspension from a user.
Args:
user_id: User UUID
Returns:
Updated user info
"""
user = User.query.get(user_id)
if not user or user.deleted_at is not None:
raise ValueError("User not found")
if user.status != "GLOBAL_SUSPENDED":
raise ValueError("User is not suspended")
user.status = "active"
db.session.commit()
return {
"user": {
"id": user.id,
"email": user.email,
"status": user.status,
},
}
@staticmethod
def reset_password(user_id: str) -> dict:
"""Trigger password reset for user.
Args:
user_id: User UUID
Returns:
Email of user
"""
user = User.query.get(user_id)
if not user or user.deleted_at is not None:
raise ValueError("User not found")
# In production, this would call AuthService.send_password_reset_email
logger.info(f"[SuperadminUserService] Password reset requested for {user.email}")
return {"email": user.email}
@staticmethod
def revoke_all_sessions(user_id: str) -> dict:
"""Revoke all sessions for a user.
Args:
user_id: User UUID
Returns:
Count of revoked sessions
"""
user = User.query.get(user_id)
if not user or user.deleted_at is not None:
raise ValueError("User not found")
result = Session.query.filter(
Session.user_id == user_id,
Session.deleted_at.is_(None),
).update({"status": "revoked", "deleted_at": db.func.now()})
db.session.commit()
return {
"user_id": user_id,
"count": result,
}
@staticmethod
def add_to_org(user_id: str, org_id: str, role: str = "member") -> dict:
"""Add a user to an organization.
Args:
user_id: User UUID
org_id: Organization UUID
role: Membership role
Returns:
Membership details
"""
user = User.query.get(user_id)
if not user or user.deleted_at is not None:
raise ValueError("User not found")
org = Organization.query.get(org_id)
if not org or org.deleted_at is not None:
raise ValueError("Organization not found")
# Check if already a member
existing = OrganizationMember.query.filter(
OrganizationMember.user_id == user_id,
OrganizationMember.organization_id == org_id,
OrganizationMember.deleted_at.is_(None),
).first()
if existing:
raise ValueError("User is already a member of this organization")
membership = OrganizationMember(
user_id=user_id,
organization_id=org_id,
role=role,
)
db.session.add(membership)
db.session.commit()
return {
"user_id": user_id,
"organization_id": org_id,
"role": role,
"joined_at": membership.created_at.isoformat() + "Z" if membership.created_at else None,
}
@staticmethod
def remove_from_org(user_id: str, org_id: str) -> dict:
"""Remove a user from an organization.
Args:
user_id: User UUID
org_id: Organization UUID
Returns:
Confirmation
"""
membership = OrganizationMember.query.filter(
OrganizationMember.user_id == user_id,
OrganizationMember.organization_id == org_id,
OrganizationMember.deleted_at.is_(None),
).first()
if not membership:
raise ValueError("User is not a member of this organization")
# Check if user is the only owner
if membership.role == "owner":
owner_count = OrganizationMember.query.filter(
OrganizationMember.organization_id == org_id,
OrganizationMember.role == "owner",
OrganizationMember.deleted_at.is_(None),
).count()
if owner_count <= 1:
raise ValueError("Cannot remove the only owner from an organization. Transfer ownership first.")
membership.deleted_at = db.func.now()
db.session.commit()
return {
"user_id": user_id,
"organization_id": org_id,
}