Files
WebForm-mw/app/staff/report/page.tsx
T
Joel Brock 4ca3c194d7 Staff report: CSP frame-ancestors + frame-mode + WebForm-mw Civi extension
App side:
- Per-route CSP: /staff/report now sets frame-ancestors 'self'
  <CIVI_BASE_URL origin> and drops X-Frame-Options so the CiviCRM
  extension can iframe it. All other routes keep frame-ancestors
  'none' + X-Frame-Options: DENY via a path-negation source.
- Staff page recognises ?frame=1 and renders without SiteHeader/
  SiteFooter so it fills the iframe cleanly.
- StaffReportView posts its scrollHeight to the parent window via
  postMessage when framed; the Civi tab listens and auto-resizes
  the iframe (no nested scrollbar). Anchor strip drops its sticky
  positioning in frame mode since there's no internal scroll.

CiviCRM extension (civi-extension/webform-mw/, key webform-mw):
- info.xml + main hook file (webform_mw.php) implementing
  hook_civicrm_tabset to add an 'Engagement Report' tab to
  Organization contact-view pages.
- CRM/WebformMw/Page/Tab.php + Smarty template render an iframe
  pointing at <WEBFORM_MW_APP_URL>/staff/report?org=<cid>&key=&frame=1,
  with a postMessage listener that validates event.origin against
  the configured app URL before resizing.
- Config via PHP constants in civicrm.settings.php (WEBFORM_MW_APP_URL,
  WEBFORM_MW_STAFF_KEY) or matching env vars. Help banner shown when
  unconfigured.
- README documents install, config, behaviour, security caveats.
2026-06-05 17:42:35 -07:00

114 lines
3.2 KiB
TypeScript

import { Suspense } from "react";
import { StaffReportView } from "@/components/StaffReportView";
import { SiteHeader, SiteFooter } from "@/components/SiteChrome";
import { isStaffKeyValid } from "@/lib/staff-auth";
interface PageProps {
searchParams: Promise<{ org?: string; key?: string; frame?: string }>;
}
export const metadata = {
title: "Staff report — Food Co-op Initiative",
robots: { index: false, follow: false },
};
export default async function StaffReportPage({ searchParams }: PageProps) {
const { org, key, frame } = await searchParams;
const isFramed = frame === "1";
// Generic "not found" if the key is missing or wrong — don't confirm
// route existence.
if (!isStaffKeyValid(key)) {
return <NotFound framed={isFramed} />;
}
const orgId = Number(org);
const orgValid = !!org && Number.isFinite(orgId) && orgId > 0;
// CIVI_BASE_URL flows from server config to client only as a base for
// outbound file links. No secret material is exposed.
const civiBaseUrl = process.env.CIVI_BASE_URL ?? "";
// When embedded in CiviCRM (?frame=1), drop the site header/footer so the
// report fills the iframe cleanly. Standalone visits keep full chrome.
const body = (
<main id="main" className="flex-1">
<div
className={
isFramed
? "mx-auto max-w-5xl px-3 py-4"
: "mx-auto max-w-5xl px-4 py-10 sm:px-6 sm:py-14"
}
>
{orgValid ? (
<Suspense fallback={null}>
<StaffReportView
org={orgId}
authKey={key!}
civiBaseUrl={civiBaseUrl}
framed={isFramed}
/>
</Suspense>
) : (
<MissingOrg />
)}
</div>
</main>
);
if (isFramed) return body;
return (
<>
<a
href="#main"
className="sr-only focus:not-sr-only focus:absolute focus:left-4 focus:top-4 focus:z-50 focus:rounded focus:bg-paper focus:px-3 focus:py-2 focus:text-ink focus:shadow"
>
Skip to content
</a>
<SiteHeader />
{body}
<SiteFooter />
</>
);
}
function NotFound({ framed }: { framed: boolean }) {
const inner = (
<div
className={
framed
? "mx-auto max-w-2xl px-4 py-8 text-center"
: "mx-auto max-w-2xl px-4 py-20 text-center"
}
>
<h1 className="font-display text-3xl text-ink">Not found</h1>
<p className="mt-3 text-ink-soft">
The page you requested doesn&apos;t exist.
</p>
</div>
);
if (framed) return <main className="flex-1">{inner}</main>;
return (
<>
<SiteHeader />
<main className="flex-1">{inner}</main>
<SiteFooter />
</>
);
}
function MissingOrg() {
return (
<div role="alert" className="rounded-lg border-2 border-clay-200 bg-clay-100/30 px-6 py-7">
<h2 className="font-display text-xl font-medium text-clay-700">
Missing or invalid org id.
</h2>
<p className="mt-3 leading-relaxed text-ink-soft">
Add <code className="font-mono">?org=&lt;civi-org-id&gt;</code> to the URL. The org
id is the Civi Contact id of the organization (visible in the URL when
viewing the org in CiviCRM).
</p>
</div>
);
}