feat: add admin and user session listing endpoints with enriched device/network details
This commit is contained in:
@@ -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 ───────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user