Skip to content

ENG-3005: Add Privacy Center configuration UI to Property page#7670

Draft
jjdaurora wants to merge 10 commits intomainfrom
ENG-3005-privacy-center-config-ui
Draft

ENG-3005: Add Privacy Center configuration UI to Property page#7670
jjdaurora wants to merge 10 commits intomainfrom
ENG-3005-privacy-center-config-ui

Conversation

@jjdaurora
Copy link
Contributor

@jjdaurora jjdaurora commented Mar 16, 2026

Ticket ENG-3005

Description Of Changes

Adds a full-featured Privacy Center configuration UI to the property edit page (/properties/[id]). Previously, privacy_center_config could only be set via direct API calls.

Changes include:

  • Privacy Center toggle — enables/disables the Privacy Center on a property; initializes with sensible defaults when turned on
  • General config fields — title, description, description subtext (repeatable), logo path/URL, favicon path, privacy policy URL and text
  • Actions editor — repeatable action entries, each with: policy key dropdown (populated from GET /dsr/policy), icon path, title, description, confirm/cancel button text, identity inputs (email/phone with Required toggle), and custom privacy request fields
  • Custom fields editor — repeatable per-action custom fields with type branching: Location (Required + IP geolocation hint checkboxes) or Other (text/select/multiselect type, Required, Hidden, default value when hidden)
  • Paths editor — repeatable text inputs for the property's URL paths array
  • Live preview panel — split-pane layout with a right-side preview component that reads from Formik context and updates in real-time, showing logo, title, description, and action cards
  • Split-pane layout — property edit page is now full-width with config form on the left and live preview on the right

No backend changes needed — PUT /api/v1/plus/property/{propertyId} already accepts privacy_center_config and paths via the PropertyCreate schema.

Note: The consent section of PrivacyCenterConfig is out of scope for this ticket. The default config stub includes a minimal consent placeholder to satisfy the schema; a follow-up ticket should address the consent UI or make the field optional.

Code Changes

  • src/features/properties/PropertyForm.tsx — Extended PropertyFormValues with privacy_center_config and paths; added Privacy Center toggle section and split-pane layout support via optional rightPanel prop
  • src/pages/properties/[id].tsx — Wired in PrivacyCenterPreview as right panel; removed narrow maxWidth constraint
  • src/features/properties/privacy-center/helpers.tsDEFAULT_ACTION and DEFAULT_PRIVACY_CENTER_CONFIG factory objects
  • src/features/properties/privacy-center/PathsFieldArray.tsx — Repeatable paths editor using Formik FieldArray
  • src/features/properties/privacy-center/PrivacyCenterConfigForm.tsx — Top-level config fields + wires in ActionsFieldArray
  • src/features/properties/privacy-center/ActionsFieldArray.tsx — Repeatable actions editor with add/remove
  • src/features/properties/privacy-center/ActionEntryForm.tsx — Per-action fields: policy key dropdown, identity inputs, custom fields
  • src/features/properties/privacy-center/CustomPrivacyFieldsArray.tsx — Repeatable custom fields with Location / Other type branching
  • src/features/properties/privacy-center/PrivacyCenterPreview.tsx — Live preview rendering logo, title, description, action cards from Formik context

Steps to Confirm

  1. Navigate to an existing property at /properties/[id]
  2. Verify the page now shows a split-pane layout (form left, preview right)
  3. Scroll to the Paths section — add a path (e.g. /ja) and verify it appears
  4. In the Privacy Center section, toggle the switch ON
  5. Fill in Title and Description — verify the preview updates live on the right
  6. Set a Logo URL — verify the logo renders in the preview
  7. Expand the Actions section, fill in an action's title and description — verify the preview action card updates
  8. In an action, select a Policy key from the dropdown (populated from existing policies)
  9. Toggle the email identity input ON and verify the Required checkbox appears
  10. Add a custom field, set type to "Location", verify IP geolocation hint checkbox appears
  11. Add a custom field, set type to "Text", mark it Hidden — verify Default value field appears
  12. Click Save — verify PUT /api/v1/plus/property/{propertyId} is called with privacy_center_config and paths in the payload
  13. Reload the page — verify all entered values are restored from the API

Pre-Merge Checklist

  • Issue requirements met
  • All CI pipelines succeeded
  • CHANGELOG.md updated
    • Add a db-migration This indicates that a change includes a database migration label to the entry if your change includes a DB migration
    • Add a high-risk This issue suggests changes that have a high-probability of breaking existing code label to the entry if your change includes a high-risk change (i.e. potential for performance impact or unexpected regression) that should be flagged
    • Updates unreleased work already in Changelog, no new entry necessary
  • UX feedback:
    • All UX related changes have been reviewed by a designer
    • No UX review needed
  • Followup issues:
    • Followup issues created
    • No followup issues
  • Database migrations:
    • Ensure that your downrev is up to date with the latest revision on main
    • Ensure that your downgrade() migration is correct and works
      • If a downgrade migration is not possible for this change, please call this out in the PR description!
    • No migrations
  • Documentation:
    • Documentation complete, PR opened in fidesdocs
    • Documentation issue created in fidesdocs
    • If there are any new client scopes created as part of the pull request, remember to update public-facing documentation that references our scope registry
    • No documentation updates required

Adds a configurable Privacy Center section to the property edit page,
including a live preview panel that updates as the user types.

- Extends PropertyFormValues with privacy_center_config and paths
- Adds toggle to enable/disable the Privacy Center on a property
- Top-level config fields: title, description, logo, favicon, privacy policy URL
- Repeatable actions editor with policy key dropdown, identity inputs (email/phone),
  and custom privacy request fields (Location or Other type)
- Repeatable paths field array
- Split-pane layout: config form on left, live preview on right

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel
Copy link
Contributor

vercel bot commented Mar 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
fides-plus-nightly Building Building Preview, Comment Mar 20, 2026 9:17pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
fides-privacy-center Ignored Ignored Mar 20, 2026 9:17pm

Request Review

<Box flexShrink={0} mt={0.5}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={iconPath}

Check failure

Code scanning / CodeQL

DOM text reinterpreted as HTML High

DOM text
is reinterpreted as HTML without escaping meta-characters.

Copilot Autofix

AI 4 days ago

In general, the fix is to ensure that untrusted data used in DOM-related contexts (including URL-bearing attributes like src) are validated and/or sanitized before use. Instead of directly passing user-controlled iconPath into the <img src> attribute, restrict it to a safe set of URL schemes and/or patterns, or fall back to a benign value if it does not meet those constraints.

The best fix here without changing functionality is to add a small helper that “normalizes” the icon path: it will accept only HTTP(S) URLs or relative paths and reject anything else (such as javascript:, data:, or other exotic schemes). If the value is unsafe or malformed, we simply do not render the image. This preserves existing behavior for legitimate values while blocking dangerous ones. The change is localized to ActionCard in PrivacyCenterPreview.tsx.

Concretely:

  • Inside PrivacyCenterPreview.tsx, define a helper function (above ActionCard) called sanitizeIconPath that:
    • Accepts string | null | undefined.
    • Returns string | undefined.
    • For empty/falsy values, returns undefined.
    • Parses the URL; if it has an explicit scheme, only allows http: and https:.
    • If parsing fails (e.g., a plain relative path), treat it as a relative URL and allow it.
    • Returns undefined for disallowed schemes.
  • In ActionCard, compute const safeIconPath = sanitizeIconPath(iconPath); and only render the <img> if safeIconPath is truthy; use safeIconPath as the src value.

No new imports are strictly needed; we can use the built-in URL constructor.

Suggested changeset 1
clients/admin-ui/src/features/properties/privacy-center/PrivacyCenterPreview.tsx

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/clients/admin-ui/src/features/properties/privacy-center/PrivacyCenterPreview.tsx b/clients/admin-ui/src/features/properties/privacy-center/PrivacyCenterPreview.tsx
--- a/clients/admin-ui/src/features/properties/privacy-center/PrivacyCenterPreview.tsx
+++ b/clients/admin-ui/src/features/properties/privacy-center/PrivacyCenterPreview.tsx
@@ -10,6 +10,29 @@
 
 import type { PropertyFormValues } from "../PropertyForm";
 
+const sanitizeIconPath = (iconPath?: string | null): string | undefined => {
+  if (!iconPath) {
+    return undefined;
+  }
+
+  try {
+    // Try to parse as an absolute URL; if a protocol is present, only allow http/https.
+    const url = new URL(iconPath);
+    if (url.protocol === "http:" || url.protocol === "https:") {
+      return iconPath;
+    }
+    return undefined;
+  } catch {
+    // If parsing fails, treat it as a relative URL and allow it.
+    // This still prevents dangerous protocols like "javascript:" from being used.
+    if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(iconPath)) {
+      // Has a scheme but URL constructor failed; reject.
+      return undefined;
+    }
+    return iconPath;
+  }
+};
+
 const ActionCard = ({
   title,
   description,
@@ -18,45 +41,49 @@
   title: string;
   description: string;
   iconPath?: string | null;
-}) => (
-  <Flex
-    border="1px solid"
-    borderColor="gray.200"
-    borderRadius="lg"
-    p={5}
-    gap={3}
-    flex="1"
-    minWidth="120px"
-    flexDirection="column"
-    alignItems="flex-start"
-    textAlign="left"
-    backgroundColor="white"
-    _hover={{ borderColor: "gray.400", boxShadow: "sm" }}
-    cursor="default"
-    data-testid="preview-action-card"
-  >
-    {iconPath && (
-      <Box flexShrink={0} mt={0.5}>
-        {/* eslint-disable-next-line @next/next/no-img-element */}
-        <img
-          src={iconPath}
-          alt=""
-          style={{ width: 24, height: 24, objectFit: "contain" }}
-        />
+}) => {
+  const safeIconPath = sanitizeIconPath(iconPath);
+
+  return (
+    <Flex
+      border="1px solid"
+      borderColor="gray.200"
+      borderRadius="lg"
+      p={5}
+      gap={3}
+      flex="1"
+      minWidth="120px"
+      flexDirection="column"
+      alignItems="flex-start"
+      textAlign="left"
+      backgroundColor="white"
+      _hover={{ borderColor: "gray.400", boxShadow: "sm" }}
+      cursor="default"
+      data-testid="preview-action-card"
+    >
+      {safeIconPath && (
+        <Box flexShrink={0} mt={0.5}>
+          {/* eslint-disable-next-line @next/next/no-img-element */}
+          <img
+            src={safeIconPath}
+            alt=""
+            style={{ width: 24, height: 24, objectFit: "contain" }}
+          />
+        </Box>
+      )}
+      <Box>
+        <Text fontWeight="semibold" fontSize="sm" color="gray.800">
+          {title || <span style={{ color: "#aaa" }}>Action title</span>}
+        </Text>
+        <Text fontSize="xs" color="gray.600" mt={1}>
+          {description || (
+            <span style={{ color: "#aaa" }}>Action description</span>
+          )}
+        </Text>
       </Box>
-    )}
-    <Box>
-      <Text fontWeight="semibold" fontSize="sm" color="gray.800">
-        {title || <span style={{ color: "#aaa" }}>Action title</span>}
-      </Text>
-      <Text fontSize="xs" color="gray.600" mt={1}>
-        {description || (
-          <span style={{ color: "#aaa" }}>Action description</span>
-        )}
-      </Text>
-    </Box>
-  </Flex>
-);
+    </Flex>
+  );
+};
 
 const PrivacyCenterPreview = () => {
   const { values } = useFormikContext<PropertyFormValues>();
EOF
Copilot is powered by AI and may make mistakes. Always verify output.
jjdaurora and others added 7 commits March 16, 2026 16:46
When a user selects default_access_policy, default_erasure_policy, or
default_consent_policy and icon_path is currently empty, pre-populate
the field with the standard Ethyca privacy center icon URL. A tooltip
on the icon_path field shows the default URL for the selected policy key.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant