Commit Graph
9 Commits
Author SHA1 Message Date
Joel Brock 818c979479 Replace raw staff key in URLs with signed, org-bound tokens
The Engagement Report tab wrote STAFF_REPORT_KEY into the iframe src for
every user with "access CiviCRM", without checking contact permissions.
Anyone could copy it from the DOM and read any org's report and files,
bypassing Civi ACLs; the key never expired and landed in access logs.

- Extension (0.4.0): Tab.php checks Contact_Permission::allow(VIEW) and
  mints sig = base64url(HMAC-SHA256(key, "webform-mw.staff-report.v1:
  <org>:<exp>")) with an 8h exp. The secret no longer leaves the server.
- App: lib/staff-token.mjs verifies the token (constant-time, org must
  match, rejects expired or >12h tokens). /staff/report, /api/staff/report
  and /api/staff/file share staffAuthQuery(); client links carry exp/sig.
- Legacy ?key= is still accepted (constant-time compare, logs a warning)
  so the app can deploy before the extension. Remove it once every Civi
  install runs extension 0.4.0.
- PHP/Node signature compatibility is pinned by a PHP-generated vector.
2026-10-30 11:17:28 -07:00
Joel Brock 6850ff9dee Staff report: inline lightbox for image/PDF attachments
Adds a /api/staff/file proxy that re-streams Civi attachments with
Content-Disposition: inline so a native <dialog> lightbox can preview
images and PDFs in place. Office docs keep their plain download link
and gain a "View in Google Docs" secondary link (uses the Civi-signed
URL so Google can fetch without our staff key).

Also threads mime through /api/staff/report (Attachment.get mime_type)
so the dispatcher picks the right affordance without relying solely on
filename inference.
2026-06-15 11:56:45 -07:00
Joel Brock e22c9226d8 Staff report: query Attachment.get per file id (no IN clause)
The {IN: [...]} operator object for `id` crashes Civi APIv3 on this
install — Civi's error renderer calls htmlentities() on the array
value and dies, producing the 500 with the html error page seen at
6c3e8dd's deploy.

Single-id calls succeed (confirmed in the user's API Explorer test
against id=150). So loop one call per file id and run them in
parallel via Promise.allSettled. Reports typically reference a
handful of files, so the round-trip overhead is small and individual
file failures don't poison the whole report.
2026-06-10 11:34:54 -07:00
Joel Brock 6c3e8dd6a0 Staff report: APIv3 return is a string, not an array
Previous commit (8ace5f4) passed `return: ["id", "url"]` to APIv3
Attachment.get. APIv3 expects `return` as a comma-separated string
("id,url"); arrays are v4 syntax. Civi caught the type mismatch but
its error-rendering pathway then crashed on htmlentities() (which
was passed the offending array), producing a 500 with an HTML error
page rather than a clean JSON error.

Fix: pass return as "id,url" and add sequential:1 (canonical v3 client
shape — values come back as an array). The civi3 helper already
normalizes either response shape, but sequential matches what real
APIv3 clients send.

Logs to confirm after deploy:
  - Success: no "[staff/report] Attachment.get (v3) failed" warning.
  - File links resolve directly without bouncing off the
    /civicrm/webform-mw/file extension route.
2026-06-10 11:25:06 -07:00
Joel Brock 8ace5f41fe Staff report: signed file URLs via APIv3 Attachment.get
APIv4 Attachment isn't exposed on this Civi install (confirmed in the
June 2026 upload-spike notes), so the Attachment.get call we shipped at
63e73e7 silently returned nothing and we fell through to the bare
/civicrm/file URL — which crashes Civi on a null fcs JWT decode.

APIv3 Attachment.get IS exposed and returns the signed URL with fcs
baked in (verified against id=150 in the user's API Explorer):

  "url": "https://.../civicrm/file?reset=1&id=150&fcs=<JWT>"

Changes:
- lib/civicrm.ts: add a civi3() helper that calls /civicrm/ajax/rest with
  AuthX headers, normalizing v3's array-or-keyed-object values shape into
  a plain array.
- app/api/staff/report/route.ts: replace the dead v4 Attachment.get with
  civi3("Attachment", "get", { id: {IN: [...]}, return: ["id","url"] }).
  Each file's url goes into the value payload as before, so the frontend
  needs no change.

Fallback chain remains intact: if Attachment.get fails (auth, endpoint
unavailable, etc.) the frontend still uses the /civicrm/webform-mw/file
extension route from b65bc6d/41467bd.
2026-06-10 11:05:00 -07:00
Joel Brock 63e73e7fe6 Staff report: pass Civi-signed file URLs through to clicks
Civi serves uploaded files at /civicrm/file?id=X&eid=Y&fcs=<JWT>; the fcs
is a JWT signed with the site key. Without it, Civi's file handler crashes
on a null JWT decode (Firebase\JWT\JWT::decode argument null). We don't
have the site key on the Next.js side, so let Civi mint the URLs for us.

Backend (/api/staff/report):
- Add file_name selects for org-side file fields (Certificate of
  Incorporation and friends) so org files have names alongside URLs.
- Collect every file id referenced by activity and org custom fields.
- Call APIv4 Attachment.get with select: ["id", "url"] to fetch signed
  URLs in one round trip. Build a urlByFileId map.
- Org-side file values are now wrapped in { id, file_name, url } shape
  matching the activity-side files (previously bare file ids that the
  frontend couldn't render).
- Activity-side file values gain a url property from the map.
- If Attachment.get doesn't expose url on this Civi version, the call is
  caught and we fall through to bare URLs without fcs (no regression).

Frontend (FieldValue):
- Prefer v.url when present, normalizing absolute and relative shapes
  against CIVI_BASE_URL.
- Fall back to /civicrm/file?reset=1&id=X if url wasn't provided.
2026-06-10 09:29:42 -07:00
Joel Brock 64076a145b Staff report: drop dead .url file join (client uses proxy URL) 2026-06-05 16:42:36 -07:00
Joel Brock b05d7c77e3 Staff report: live Civi branch (schema discovery + activity walk) 2026-06-05 16:27:23 -07:00
Joel Brock 5a349a4f1c Staff report: API route with stub payload and key validation 2026-06-05 16:15:30 -07:00