Built the Sentinel Tab
This commit is contained in:
190
backend/routers/vault.py
Normal file
190
backend/routers/vault.py
Normal file
@@ -0,0 +1,190 @@
|
||||
"""
|
||||
backend/routers/vault.py — Velocity Vault (Trackable Link) Router
|
||||
|
||||
Endpoints:
|
||||
POST /api/vault/generate-link → Generate a trackable URL for a shared asset
|
||||
GET /vault/{tracking_hash} → Public link accessed by the prospect;
|
||||
logs the open, fires WS_ASSET_OPENED
|
||||
|
||||
SRS Reference: Section 3C — Velocity Link Generation
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import secrets
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
import asyncpg
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from fastapi.responses import RedirectResponse, JSONResponse
|
||||
from pydantic import BaseModel, UUID4
|
||||
|
||||
from backend.auth.dependencies import UserPrincipal, require_role
|
||||
from backend.db.pool import get_pool
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# ── Pydantic models ───────────────────────────────────────────────────────────
|
||||
|
||||
class GenerateLinkRequest(BaseModel):
|
||||
lead_id: str
|
||||
asset_name: str
|
||||
asset_type: str # 'pdf' | 'image' | 'video'
|
||||
storage_path: str # relative to /opt/dlami/nvme/assets/
|
||||
|
||||
|
||||
class GenerateLinkResponse(BaseModel):
|
||||
tracking_hash: str
|
||||
vault_url: str
|
||||
asset_id: str
|
||||
|
||||
|
||||
# ── Helper: WebSocket broadcast ───────────────────────────────────────────────
|
||||
|
||||
async def _broadcast_vault_opened(
|
||||
request: Request,
|
||||
lead_id: str,
|
||||
lead_name: str,
|
||||
asset_name: str,
|
||||
tracking_hash: str,
|
||||
ip: Optional[str],
|
||||
) -> None:
|
||||
"""Fires WS_ASSET_OPENED to all broker WebSocket clients watching this lead."""
|
||||
broadcast = getattr(request.app.state, "broadcast_sentinel_event", None)
|
||||
if broadcast:
|
||||
await broadcast({
|
||||
"type": "WS_ASSET_OPENED",
|
||||
"data": {
|
||||
"lead_id": lead_id,
|
||||
"lead_name": lead_name,
|
||||
"asset_name": asset_name,
|
||||
"tracking_hash": tracking_hash,
|
||||
"opened_at": datetime.now(timezone.utc).isoformat(),
|
||||
"ip": ip,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
# ── POST /api/vault/generate-link ─────────────────────────────────────────────
|
||||
|
||||
@router.post(
|
||||
"/generate-link",
|
||||
response_model=GenerateLinkResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Generate a trackable Velocity Link for a document share",
|
||||
)
|
||||
async def generate_link(
|
||||
body: GenerateLinkRequest,
|
||||
request: Request,
|
||||
pool: asyncpg.Pool = Depends(get_pool),
|
||||
user: UserPrincipal = Depends(require_role("SENIOR_BROKER")),
|
||||
) -> GenerateLinkResponse:
|
||||
"""
|
||||
Creates a cryptographically unique URL for every document share instance.
|
||||
When the prospect opens the URL, FastAPI logs the event and fires a
|
||||
real-time WebSocket notification to the broker's Active Notification Center.
|
||||
"""
|
||||
tracking_hash = secrets.token_hex(32) # 64 character hex string
|
||||
|
||||
async with pool.acquire() as conn:
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
INSERT INTO velocity_vault_assets
|
||||
(asset_name, asset_type, storage_path, tracking_hash, lead_id, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5::uuid, $6::uuid)
|
||||
RETURNING id::text
|
||||
""",
|
||||
body.asset_name,
|
||||
body.asset_type,
|
||||
body.storage_path,
|
||||
tracking_hash,
|
||||
body.lead_id,
|
||||
user.user_id,
|
||||
)
|
||||
|
||||
base_url = os.getenv("VELOCITY_API_BASE_URL", "http://localhost:8000")
|
||||
vault_url = f"{base_url}/vault/{tracking_hash}"
|
||||
|
||||
return GenerateLinkResponse(
|
||||
tracking_hash=tracking_hash,
|
||||
vault_url=vault_url,
|
||||
asset_id=row["id"],
|
||||
)
|
||||
|
||||
|
||||
# ── GET /vault/{tracking_hash} ────────────────────────────────────────────────
|
||||
|
||||
@router.get(
|
||||
"/{tracking_hash}",
|
||||
summary="Public Velocity Link endpoint — accessed by the prospect",
|
||||
include_in_schema=False,
|
||||
)
|
||||
async def open_vault_link(
|
||||
tracking_hash: str,
|
||||
request: Request,
|
||||
pool: asyncpg.Pool = Depends(get_pool),
|
||||
) -> RedirectResponse:
|
||||
"""
|
||||
No auth required — this URL is shared with the prospect externally.
|
||||
|
||||
On access:
|
||||
1. Appends NOW() to velocity_vault_assets.opened_at
|
||||
2. Writes a WS_ASSET_OPENED entry to omnichannel_logs
|
||||
3. Broadcasts the event to all connected broker WebSocket clients
|
||||
4. Redirects the prospect to the actual asset file
|
||||
"""
|
||||
async with pool.acquire() as conn:
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
UPDATE velocity_vault_assets
|
||||
SET opened_at = array_append(opened_at, NOW())
|
||||
WHERE tracking_hash = $1
|
||||
RETURNING id::text, lead_id::text, asset_name, storage_path
|
||||
""",
|
||||
tracking_hash,
|
||||
)
|
||||
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail="Link not found or expired.")
|
||||
|
||||
lead_id = row["lead_id"]
|
||||
asset_name = row["asset_name"]
|
||||
|
||||
# Fetch lead name for the notification body
|
||||
lead_row = await conn.fetchrow(
|
||||
"SELECT name FROM leads_intelligence WHERE id = $1::uuid",
|
||||
lead_id,
|
||||
)
|
||||
lead_name = lead_row["name"] if lead_row else "Unknown Lead"
|
||||
|
||||
# Write to omnichannel_logs
|
||||
ip = request.client.host if request.client else None
|
||||
await conn.execute(
|
||||
"""
|
||||
INSERT INTO omnichannel_logs (event_type, lead_id, payload)
|
||||
VALUES ('WS_ASSET_OPENED', $1::uuid, $2::jsonb)
|
||||
""",
|
||||
lead_id,
|
||||
{
|
||||
"tracking_hash": tracking_hash,
|
||||
"asset_name": asset_name,
|
||||
"ip": ip,
|
||||
"user_agent": request.headers.get("user-agent", ""),
|
||||
},
|
||||
)
|
||||
|
||||
# Fire real-time WebSocket broadcast to all brokers
|
||||
await _broadcast_vault_opened(
|
||||
request=request,
|
||||
lead_id=lead_id,
|
||||
lead_name=lead_name,
|
||||
asset_name=asset_name,
|
||||
tracking_hash=tracking_hash,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
# Redirect to the static asset file served by FastAPI
|
||||
asset_url = f"/assets/{row['storage_path']}"
|
||||
return RedirectResponse(url=asset_url, status_code=302)
|
||||
Reference in New Issue
Block a user