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.
Next 16.2.6 carried ~20 advisories fixed in 16.4.x, including
unauthenticated RCE in the Image Optimization API (GHSA-2xp9-vwfh-vxw4,
GHSA-2464-fhr4-w9xw) and SSG/ISR cache poisoning (GHSA-mcj8-r9mp-w47p).
npm audit fix clears the remaining sharp/postcss/source-map-js/nanoid
findings; npm audit now reports 0 vulnerabilities.
Node 20 is EOL and receives no fix for Node2Shell (CVE-2026-111497,
bundled Brotli decoder OOB write). Pin .nvmrc and render.yaml to Node 24
LTS; Amplify SSR compute runs the major version used at build time.
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.
Two additions, both touching the form-config story:
1. scripts/sync-help-from-civi.mjs
Diffs per-field help text in config/form.ts against CustomField rows
in CiviCRM and (with --write) updates the file in place. Reads env
from .env.local via Node's --env-file flag. Run as `npm run sync-help`
or `npm run sync-help -- --write`. A --debug mode prints the parser's
field list without calling Civi.
Rationale: this form is low-traffic and help text doesn't change
often once in production. A manual one-off sync is leaner than
coupling every page load (or every build) to a Civi API call.
2. fieldGroups: visual clustering of related fields within a section
New optional FieldGroupConfig overlay on StageSectionConfig — pure
presentation, names existing fields by name so submit/visibility
logic walks them unchanged. StageSection.tsx pulls grouped fields
out of the standalone per-field grid and renders each group as its
own bordered card with an optional heading. Stage 2 now clusters
Market Study, Pro Forma, Business Plan, and Board Self Assessment
(each a date + upload pair) into their own cards.
Removed five default Next.js scaffold SVGs from public/ that were
created by create-next-app and never referenced (file.svg, globe.svg,
next.svg, vercel.svg, window.svg). The actual brand mark
public/fci-logo.png is the only image the app uses.
Removed axios from dependencies — the app uses native fetch
everywhere, and axios hasn't been imported since the initial
scaffolding pass. Lockfile regenerated; build verified clean.
- Initialize Next.js project with Tailwind CSS
- Create CiviCRM APIv4 integration layer
- Implement stage-based form visibility (stages 0-5)
- Add field mapping configuration for CRM-to-form linking
- Create API routes for data retrieval and submission
- Record form submissions as CiviCRM activities
- Support dynamic contactId and orgId via URL parameters
- Ensure robust form state management with react-hook-form
Co-authored-by: joelbrock <52835+joelbrock@users.noreply.github.com>