- Backend: ConnectionManager, Redis-backed bus with in-process fallback, channel authorization, and typed emit helpers wired into workers (ai_runtime, sandbox_gateway), workforce/sandbox/billing/audit/error services. - Frontend: single shared WS client with backoff + heartbeat, RealtimeProvider, useLiveChannel/useLiveEvent hooks, live indicators on task detail, manager chat, dashboard, traces, sandbox terminal, admin errors, and billing. - Contracts: WsEvent/WsServerFrame/WsClientFrame types mirroring the port. All 255 backend tests pass; web typecheck and build pass.
48 lines
1.3 KiB
Python
48 lines
1.3 KiB
Python
"""Audit helper used by the HTTP layer.
|
|
|
|
Every sensitive operation calls this. It is deliberately cheap (one INSERT) so
|
|
there is no excuse to skip it.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.application.ports.realtime import emit_audit_recorded
|
|
from app.domain.audit.models import AuditEvent
|
|
from app.infrastructure.repositories.audit import AuditRepository
|
|
|
|
|
|
async def record_audit(
|
|
session: AsyncSession,
|
|
*,
|
|
action: str,
|
|
organization_id: str | None = None,
|
|
actor_user_id: str | None = None,
|
|
target_type: str = "",
|
|
target_id: str = "",
|
|
metadata: dict | None = None,
|
|
ip: str = "",
|
|
user_agent: str = "",
|
|
) -> AuditEvent:
|
|
event = AuditEvent(
|
|
action=action,
|
|
organization_id=organization_id,
|
|
actor_user_id=actor_user_id,
|
|
target_type=target_type,
|
|
target_id=target_id,
|
|
metadata_json=metadata or {},
|
|
ip_address=ip,
|
|
user_agent=user_agent,
|
|
)
|
|
recorded = await AuditRepository(session).record(event=event)
|
|
# Live push: the audit timelines (dashboard, traces) update as actions
|
|
# happen instead of waiting for the next reload.
|
|
await emit_audit_recorded(
|
|
organization_id=organization_id,
|
|
action=action,
|
|
target_type=target_type,
|
|
target_id=target_id,
|
|
)
|
|
return recorded
|