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.
This commit is contained in:
+51
-23
@@ -4,7 +4,7 @@ import { SiteHeader, SiteFooter } from "@/components/SiteChrome";
|
||||
import { isStaffKeyValid } from "@/lib/staff-auth";
|
||||
|
||||
interface PageProps {
|
||||
searchParams: Promise<{ org?: string; key?: string }>;
|
||||
searchParams: Promise<{ org?: string; key?: string; frame?: string }>;
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
@@ -13,12 +13,13 @@ export const metadata = {
|
||||
};
|
||||
|
||||
export default async function StaffReportPage({ searchParams }: PageProps) {
|
||||
const { org, key } = await searchParams;
|
||||
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 />;
|
||||
return <NotFound framed={isFramed} />;
|
||||
}
|
||||
|
||||
const orgId = Number(org);
|
||||
@@ -27,6 +28,35 @@ export default async function StaffReportPage({ searchParams }: PageProps) {
|
||||
// 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
|
||||
@@ -36,34 +66,32 @@ export default async function StaffReportPage({ searchParams }: PageProps) {
|
||||
Skip to content
|
||||
</a>
|
||||
<SiteHeader />
|
||||
<main id="main" className="flex-1">
|
||||
<div className="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} />
|
||||
</Suspense>
|
||||
) : (
|
||||
<MissingOrg />
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
{body}
|
||||
<SiteFooter />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function NotFound() {
|
||||
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't exist.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
if (framed) return <main className="flex-1">{inner}</main>;
|
||||
return (
|
||||
<>
|
||||
<SiteHeader />
|
||||
<main className="flex-1">
|
||||
<div className="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't exist.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
<main className="flex-1">{inner}</main>
|
||||
<SiteFooter />
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user