feat: add admin and user session listing endpoints with enriched device/network details

This commit is contained in:
2026-05-29 05:30:44 +00:00
parent f869f6c06d
commit fed72f8bcd
2 changed files with 332 additions and 9 deletions
+113 -8
View File
@@ -1,8 +1,11 @@
"""ZeroTier network governance API endpoints."""
from datetime import datetime, timezone
from flask import g, request
from marshmallow import Schema, fields, validate, ValidationError
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import joinedload
from gatehouse_app.api.v1 import api_v1_bp
from gatehouse_app.extensions import db
@@ -808,6 +811,62 @@ def delete_membership(org_id, membership_id):
return api_response(success=False, message=str(e), status=404, error_type=e.error_type)
# ── Session helpers ──────────────────────────────────────────────────────────
def _session_to_dict(session, include_user=False):
"""Build a rich session dict with device, network, and timing details."""
now = datetime.now(timezone.utc)
exp = session.expires_at
if exp.tzinfo is None:
exp = exp.replace(tzinfo=timezone.utc)
remaining = (exp - now).total_seconds() if exp > now else 0
duration = (session.expires_at - session.authenticated_at).total_seconds()
auth_at = session.authenticated_at
if auth_at.tzinfo is None:
auth_at = auth_at.replace(tzinfo=timezone.utc)
exp_at = session.expires_at
if exp_at.tzinfo is None:
exp_at = exp_at.replace(tzinfo=timezone.utc)
d = {
"id": session.id,
"authenticated_at": auth_at.isoformat(),
"expires_at": exp_at.isoformat(),
"duration_seconds": int(duration),
"remaining_seconds": max(0, int(remaining)),
"is_active": session.is_active,
"is_expired": session.is_expired,
"ended_at": session.ended_at.isoformat() if session.ended_at else None,
"end_reason": session.end_reason.value if session.end_reason else None,
}
if session.access_request:
if session.access_request.device:
dev = session.access_request.device
d["device"] = {
"id": dev.id,
"node_id": dev.node_id,
"name": dev.display_name,
}
if session.access_request.portal_network:
net = session.access_request.portal_network
d["network"] = {
"id": net.id,
"name": net.name,
}
if include_user:
d["user"] = {
"id": session.user.id,
"full_name": session.user.full_name,
"email": session.user.email,
}
return d
# ── Sessions ─────────────────────────────────────────────────────────────────
@@ -820,15 +879,27 @@ def list_sessions(org_id):
if err:
return err
sessions = ActivationSession.query.filter(
ActivationSession.user_id == g.current_user.id,
ActivationSession.organization_id == org_id,
ActivationSession.ended_at.is_(None),
ActivationSession.deleted_at.is_(None),
).all()
sessions = (
ActivationSession.query.options(
joinedload(ActivationSession.access_request)
.joinedload(NetworkAccessRequest.device),
joinedload(ActivationSession.access_request)
.joinedload(NetworkAccessRequest.portal_network),
)
.filter(
ActivationSession.user_id == g.current_user.id,
ActivationSession.organization_id == org_id,
ActivationSession.ended_at.is_(None),
ActivationSession.deleted_at.is_(None),
)
.all()
)
return api_response(
data={"sessions": [s.to_dict() for s in sessions], "count": len(sessions)},
data={
"sessions": [_session_to_dict(s) for s in sessions],
"count": len(sessions),
},
message="Sessions retrieved successfully",
)
@@ -856,7 +927,6 @@ def end_session(org_id, session_id):
from gatehouse_app.services.network_access_service import _end_session
from gatehouse_app.utils.constants import ActivationEndReason
from datetime import datetime, timezone
_end_session(session, ActivationEndReason.LOGOUT)
@@ -866,6 +936,41 @@ def end_session(org_id, session_id):
return api_response(message="Session ended successfully")
@api_v1_bp.route("/organizations/<org_id>/admin/sessions", methods=["GET"])
@login_required
@require_admin
@full_access_required
def admin_list_sessions(org_id):
"""List all active activation sessions across all users (admin only)."""
org, err = _org_check(org_id)
if err:
return err
sessions = (
ActivationSession.query.options(
joinedload(ActivationSession.user),
joinedload(ActivationSession.access_request)
.joinedload(NetworkAccessRequest.device),
joinedload(ActivationSession.access_request)
.joinedload(NetworkAccessRequest.portal_network),
)
.filter(
ActivationSession.organization_id == org_id,
ActivationSession.ended_at.is_(None),
ActivationSession.deleted_at.is_(None),
)
.all()
)
return api_response(
data={
"sessions": [_session_to_dict(s, include_user=True) for s in sessions],
"count": len(sessions),
},
message="Admin sessions retrieved successfully",
)
# ── Kill Switch ───────────────────────────────────────────────────────────────