External portal flow (the arguido's view)
The accused employee (or external counsel) accesses the case through an unauthenticated,
token-based portal — no Supabase login. Backed by the portal-access edge function
(service-role for everything) and the route /portal/:token.
Tokens
process_portal_links: token (default encode(gen_random_bytes(32),'hex'), UNIQUE),
invited_email, invited_name, invited_role (EMPLOYEE / EXTERNAL_COUNSEL),
expires_at, revoked_at, otp_required (default false, unused), allow_submission.
validateToken requires revoked_at IS NULL and checks expires_at > now in code.
What's exposed
portal_items control visibility per link, with a visibility window
(visible_from / visible_until) enforced in two places (list query + item re-check).
item_type maps to a backing table: CASE_ATTACHMENT, EVIDENCE_ITEM, DELIVERABLE,
CHECKLIST_ATTACHMENT. Files are served via 1-hour signed URLs from case-attachments;
download requires allow_download. Deliverables with no file fall back to inline_content
(the text description).
Submission
If allow_submission, the employee can POST a message (max 5000 chars) and/or files
(→ case_messages + case_attachments, both is_from_employee: true). Handlers are then
notified via notifications rows (type: 'PORTAL_SUBMISSION'); the email notification
is a console.log stub.
Access logging
Every action → portal_access_log (LOGIN / VIEW_ITEM / DOWNLOAD_ITEM /
SUBMIT_RESPONSE, with ip_address, user_agent).
Gotchas / dangers
- No rate limiting or brute-force protection on token guessing (portal no rate-limit).
- The route the UI actually links to for the "public consultation" is inconsistent —
buttons point at
/consulta/:slug, which isn't routed (broken links). - Legacy
cases.access_password(plaintext) survives from the whistleblower era but the portal is now token-based (access password).
Rebuild implication
Keep opaque-token access + visibility windows + access log; add rate limiting, decide the URL scheme once, wire the notification email, and drop the legacy password.