Newer
Older
navi-1 / navi / api / routes / webhooks.py
"""Webhook receiver for gnexus-auth events."""

import structlog
from fastapi import APIRouter, HTTPException, Request
from gnexus_gauth.exceptions import WebhookPayloadException

from navi.auth.client import get_gauth_client
from navi.config import settings

log = structlog.get_logger()
router = APIRouter(prefix="/webhooks", tags=["webhooks"])


@router.post("/gnexus-auth")
async def gnexus_auth_webhook(request: Request) -> dict:
    """Receive and handle webhooks from gnexus-auth.

    Events handled:
    - user.blocked / user.archived / user.deleted → invalidate user sessions
    - auth.global_logout → invalidate all sessions
    - session.revoked → invalidate matching session
    - client.roles_changed / client.permissions_changed → update user role/permissions
    """
    from navi.config import settings
    if not settings.gnauth_client_id or not settings.gnauth_client_secret:
        raise HTTPException(status_code=503, detail="OAuth is not configured")

    raw_body = await request.body()
    body_text = raw_body.decode("utf-8")

    client = get_gauth_client()

    # For now, log and acknowledge. Full HMAC verification can be added when webhook
    # secret is configured in gnexus-auth admin panel.
    try:
        event = client.parse_webhook(body_text)
    except WebhookPayloadException:
        raise HTTPException(status_code=400, detail="Invalid JSON payload")

    event_type = event.event_type
    target = event.target_identifiers
    log.info("webhook.received", webhook_event=event_type)

    if event_type in ("user.blocked", "user.archived", "user.deleted"):
        user_id = target.get("user_id")
        if user_id:
            await _invalidate_user_sessions(user_id)
            log.info("webhook.user_invalidated", user_id=user_id, webhook_event=event_type)

    elif event_type == "auth.global_logout":
        await _invalidate_all_sessions()
        log.info("webhook.all_sessions_invalidated")

    elif event_type == "session.revoked":
        # gnexus-auth session revoked — we don't have a direct mapping, so invalidate all
        # for the user if user_id is present
        user_id = target.get("user_id")
        if user_id:
            await _invalidate_user_sessions(user_id)
            log.info("webhook.session_revoked", user_id=user_id)

    elif event_type in ("client.roles_changed", "client.permissions_changed"):
        # Update affected users' cached role/permissions
        user_id = target.get("user_id")
        if user_id:
            await _update_user_permissions(user_id)
            log.info("webhook.permissions_updated", user_id=user_id, webhook_event=event_type)

    return {"ok": True}


async def _invalidate_user_sessions(user_id: str) -> None:
    try:
        from navi.api.deps import get_session_store
        store = get_session_store()
        pool = await store._get_pool()
        async with pool.acquire() as conn:
            await conn.execute("DELETE FROM user_auth_sessions WHERE user_id = $1", user_id)
    except Exception:
        log.warning("webhook.invalidate_user_failed", user_id=user_id, exc_info=True)


async def _invalidate_all_sessions() -> None:
    try:
        from navi.api.deps import get_session_store
        store = get_session_store()
        pool = await store._get_pool()
        async with pool.acquire() as conn:
            await conn.execute("DELETE FROM user_auth_sessions")
    except Exception:
        log.warning("webhook.invalidate_all_failed", exc_info=True)


async def _update_user_permissions(user_id: str) -> None:
    """Clear cached permissions so next request re-fetches from gnexus-auth."""
    try:
        from navi.api.deps import get_session_store
        store = get_session_store()
        pool = await store._get_pool()
        async with pool.acquire() as conn:
            await conn.execute(
                "UPDATE navi_users SET permissions = '[]', updated_at = $1 WHERE id = $2",
                __import__("datetime").datetime.now(__import__("datetime").timezone.utc),
                user_id,
            )
    except Exception:
        log.warning("webhook.update_permissions_failed", user_id=user_id, exc_info=True)