Skip to content

Adding Email Unsubscribe Page#264

Open
jacob6838 wants to merge 70 commits intodevelopfrom
iapi-email-unsubscription
Open

Adding Email Unsubscribe Page#264
jacob6838 wants to merge 70 commits intodevelopfrom
iapi-email-unsubscription

Conversation

@jacob6838
Copy link
Copy Markdown
Collaborator

@jacob6838 jacob6838 commented Jan 13, 2026

PR Details

Description

Emails generated by the Intersection API include an unsubscribe url. This PR adds the "unauthenticated" webapp page /unsubscribe, which authenticates the user using a signed JWT token generated by the intersection API and embedded in the unsubscribe url. This same page now also replaces the previous notification user settings page.

  • Migrating email subscription management from the python API to the Intersection API
    • POST /users/subscriptions/email-subscriptions
    • GET /users/subscriptions/email-subscriptions
  • Creating new unsubscribe endpoints which use custom jwt authentication
    • POST /users/unsubscribe/email-subscriptions
    • GET /users/unsubscribe/email-subscriptions
  • required_role is a new field in the email_type object, denoting what user role is required to receive that type of notification
  • new webapp email setting/subscription management page using shared SubscriptionForm
  • new webapp unsubscribe page (no login required) using shared SubscriptionForm
image

How Has This Been Tested?

2026-04-21.13-21-11.mp4

This was tested using the following steps:

  1. Startup the cvmanager and make sure to include the "intersection" profile, which now includes smtp4dev
  2. Generate an email notification from the Intersection API. Without the contact-support endpoint added in 259, this is difficult. I have been grabbing the user's keycloak token from the webapp dev console network tab, and then making a Postman POST request to the IAPI /emails/send-intersection-notification-summary endpoint, with some fake notification data
  3. Navigate to smtp4dev (localhost:5000), and navigate to the unsubscribe link
  4. This should bring you to the notification management page. You should not be prompted to log into the cvmanager.
  5. Change your email notifications and hit save. Refresh the page to conform the changes took effect

Types of changes

  • Defect fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that cause existing functionality to change)

Checklist:

  • My changes require new environment variables:
    • I have updated the docker-compose, K8s YAML, and all dependent deployment configuration files.
  • My changes require updates to the documentation:
    • I have updated the documentation accordingly.
  • My changes require updates and/or additions to the unit tests:
    • I have modified/added tests to cover my changes.
  • All existing tests pass.

jacob6838 and others added 4 commits January 15, 2026 08:55
…/api/emails/generators/IntersectionNotificationSummaryEmailGenerator.java

Co-authored-by: Matt Cook <mattheworion.cook@gmail.com>
@jacob6838 jacob6838 requested a review from mcook42 April 21, 2026 19:16
@jacob6838 jacob6838 marked this pull request as ready for review April 21, 2026 19:16
Copilot AI review requested due to automatic review settings April 21, 2026 19:16
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds end-user email subscription management, including a public /unsubscribe webapp route that authenticates via a signed JWT embedded in email links, plus Intersection API endpoints/data model updates to fetch and persist subscription preferences.

Changes:

  • Introduces a public /unsubscribe page and a reusable subscription management form/UI in the webapp.
  • Adds Intersection API controllers/services/mappers to list/update subscription preferences (authenticated and unsubscribe-token flows).
  • Updates email subscription persistence model (email type metadata such as description/required role) and related SQL scripts/tests.

Reviewed changes

Copilot reviewed 53 out of 54 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
webapp/src/store.tsx Registers new RTK Query slices for subscription management/unsubscribe.
webapp/src/pages/Admin.tsx Removes notification-related prefetch tied to old admin notification UI.
webapp/src/models/email-subscriptions.d.ts Adds TS types for subscription payloads returned by Intersection API.
webapp/src/features/api/unsubscribeApiSlice.ts RTK Query slice for unauthenticated unsubscribe-token subscription fetch/update.
webapp/src/features/api/subscriptionManagementApiSlice.ts RTK Query slice for authenticated in-app subscription management.
webapp/src/features/adminNotificationTab/adminNotificationTabSlice.tsx Removes legacy admin notification state management.
webapp/src/features/adminNotificationTab/adminNotificationTabSlice.test.ts Removes tests for legacy admin notification slice.
webapp/src/features/adminNotificationTab/snapshots/AdminNotificationTab.test.tsx.snap Removes snapshots for legacy admin notification UI.
webapp/src/features/adminNotificationTab/AdminNotificationTab.tsx Removes legacy admin notification tab UI.
webapp/src/features/adminEditNotification/adminEditNotificationSlice.tsx Removes legacy notification edit slice.
webapp/src/features/adminEditNotification/adminEditNotificationSlice.test.ts Removes tests for legacy notification edit slice.
webapp/src/features/adminEditNotification/snapshots/AdminEditNotification.test.tsx.snap Removes legacy notification edit snapshots.
webapp/src/features/adminEditNotification/AdminEditNotification.tsx Removes legacy notification edit UI.
webapp/src/features/adminAddNotification/adminAddNotificationSlice.tsx Removes legacy notification add slice.
webapp/src/features/adminAddNotification/adminAddNotificationSlice.test.ts Removes tests for legacy notification add slice.
webapp/src/features/adminAddNotification/snapshots/AdminAddNotification.test.tsx.snap Removes legacy notification add snapshots.
webapp/src/features/adminAddNotification/AdminAddNotification.tsx Removes legacy notification add UI.
webapp/src/features/adminAddNotification/AdminAddNotification.test.tsx Removes legacy notification add snapshot test.
webapp/src/components/SubscriptionManagement.tsx New authenticated subscription settings page (replaces old settings page).
webapp/src/components/SubscriptionManagement.test.tsx Updates snapshot test to cover new SubscriptionManagement component.
webapp/src/components/snapshots/SubscriptionManagement.test.tsx.snap Adds snapshot baseline for SubscriptionManagement.
webapp/src/components/SubscriptionForm.tsx Adds shared subscription preferences form (save/unsubscribe-all, per-frequency toggles).
webapp/src/components/SubscriptionForm.test.tsx Adds snapshot test for SubscriptionForm.
webapp/src/components/snapshots/SubscriptionForm.test.tsx.snap Adds snapshot baseline for SubscriptionForm.
webapp/src/Unsubscribe.tsx Adds unauthenticated unsubscribe page driven by token query param.
webapp/src/Unsubscribe.test.tsx Updates snapshot test target (currently mis-imports the icon).
webapp/src/snapshots/Unsubscribe.test.tsx.snap Adds snapshot baseline for unsubscribe test.
webapp/src/Dashboard.tsx Routes /dashboard/settings/* to SubscriptionManagement instead of legacy notifications UI.
webapp/src/App.tsx Adds public /unsubscribe route outside Keycloak-protected routing.
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/services/EmailService.java Implements subscription option retrieval and subscription update logic.
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/controllers/users/UnsubscribeController.java Adds unsubscribe-token GET/POST endpoints for subscription management.
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/controllers/users/SubscriptionController.java Adds authenticated GET/POST endpoints for subscription management.
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/keycloak/config/KeycloakSecurityConfig.java Permits unauthenticated access to unsubscribe endpoints.
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/repositories/UserEmailNotificationRepository.java Adds queries for subscriptions by user and a delete-by-type-and-user method.
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/repositories/EmailTypeRepository.java Adds repository for email type metadata.
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/mappers/UserEmailNotificationMapper.java Adds MapStruct mapper between entities and DTOs.
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/models/emails/UserEmailNotificationDto.java Adds DTO used by new subscription endpoints.
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/models/emails/EmailSubscriptionGetResponse.java Adds response wrapper for subscription GET endpoints.
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/models/emails/ManageSubscriptionsBody.java Adds request body model (currently unused by new controllers).
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/models/postgres/tables/UserEmailNotification.java Adds subscribed/frequency helper methods and entity equality config.
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/models/postgres/tables/EmailType.java Adds required role relationship to email type metadata.
services/intersection-api/api/src/test/java/us/dot/its/jpo/ode/api/services/EmailServiceTest.java Expands tests to cover subscription update scenarios.
services/intersection-api/api/src/test/java/us/dot/its/jpo/ode/api/controllers/users/UnsubscribeControllerTest.java Adds controller tests for unsubscribe-token subscription management.
services/intersection-api/api/src/test/java/us/dot/its/jpo/ode/api/controllers/users/SubscriptionControllerTest.java Adds controller tests for authenticated subscription management.
services/intersection-api/api/src/test/java/us/dot/its/jpo/ode/api/models/postgres/derived/EmailSubscriptionTest.java Adds tests for subscription DTO helper methods.
services/intersection-api/api/src/test/java/us/dot/its/jpo/ode/api/emails/UnsubscribeTokenGeneratorTest.java Adds unit tests for unsubscribe token generation/validation.
services/intersection-api/api/src/test/java/us/dot/its/jpo/ode/api/services/RsuCredentialManagementServiceTest.java Formatting/import cleanup in existing tests.
services/intersection-api/api/src/test/resources/application-integration-test.yaml Tweaks integration-test config (Mongo quoting, hikari pool sizing).
services/intersection-api/api/src/test/resources/application.properties Adds default test datasource config (currently points to localhost postgres).
resources/sql_scripts/CVManager_CreateTables.sql Extends schema for email type required role FK.
resources/sql_scripts/CVManager_SampleData.sql Updates seed data for new email_type fields and inserts.
resources/sql_scripts/update_scripts/email_type_field_update.sql Adds migration for email_type description column.
docker-compose-intersection.yml Updates Intersection API emailer SMTP port setting.
.vscode/settings.json Adds “unsubscription” to workspace dictionary.
Comments suppressed due to low confidence (1)

webapp/src/Unsubscribe.test.tsx:16

  • This test is rendering the MUI Unsubscribe icon (@mui/icons-material) instead of the app's /unsubscribe page component (webapp/src/Unsubscribe.tsx). As written, the snapshot won’t exercise any of the unsubscribe page logic and will give a false sense of coverage. Import and render the local Unsubscribe component (default export from ./Unsubscribe) instead.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread webapp/src/store.tsx
Comment thread webapp/src/Unsubscribe.tsx
Comment thread webapp/src/components/SubscriptionForm.tsx
Comment thread webapp/src/components/SubscriptionManagement.tsx Outdated
Comment thread services/intersection-api/api/src/test/resources/application.properties Outdated
Comment thread resources/sql_scripts/update_scripts/email_type_field_update.sql Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 51 out of 52 changed files in this pull request and generated 6 comments.

Comments suppressed due to low confidence (1)

webapp/src/Unsubscribe.test.tsx:16

  • Unsubscribe.test.tsx is rendering the MUI Unsubscribe icon instead of the app's /unsubscribe page component. This makes the snapshot meaningless for the feature and would miss regressions in the actual unsubscribe flow. Import and render the local Unsubscribe component from ./Unsubscribe (or the correct path) instead of @mui/icons-material.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread webapp/src/features/api/unsubscribeApiSlice.ts
Comment thread webapp/src/features/api/subscriptionManagementApiSlice.ts
Comment thread webapp/src/components/SubscriptionManagement.tsx
Comment thread webapp/src/Unsubscribe.tsx
@jacob6838 jacob6838 requested a review from Copilot April 21, 2026 22:35
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 51 out of 52 changed files in this pull request and generated 5 comments.

Comments suppressed due to low confidence (1)

webapp/src/Unsubscribe.test.tsx:16

  • The snapshot test is rendering MUI’s Unsubscribe icon (@mui/icons-material) instead of the app’s Unsubscribe page component (./Unsubscribe). This makes the snapshot meaningless and won’t catch regressions in the unsubscribe flow. Import and render the page component (and provide any required router params/query string in the test).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +7 to +11
ALTER TABLE public.email_type
ADD CONSTRAINT IF NOT EXISTS fk_role_id FOREIGN KEY (required_role)
REFERENCES public.roles (role_id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION;
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostgreSQL supports ADD COLUMN IF NOT EXISTS, but it does not support ALTER TABLE ... ADD CONSTRAINT IF NOT EXISTS ... in many commonly used Postgres versions. This migration may fail at runtime. Consider guarding constraint creation with a DO $$ ... IF NOT EXISTS (SELECT 1 FROM pg_constraint ...) THEN ... END IF; $$ block, or create the constraint unconditionally in a fresh migration where you can ensure it doesn’t already exist.

Suggested change
ALTER TABLE public.email_type
ADD CONSTRAINT IF NOT EXISTS fk_role_id FOREIGN KEY (required_role)
REFERENCES public.roles (role_id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint c
JOIN pg_class t ON t.oid = c.conrelid
JOIN pg_namespace n ON n.oid = t.relnamespace
WHERE c.conname = 'fk_role_id'
AND t.relname = 'email_type'
AND n.nspname = 'public'
) THEN
ALTER TABLE public.email_type
ADD CONSTRAINT fk_role_id FOREIGN KEY (required_role)
REFERENCES public.roles (role_id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION;
END IF;
END $$;

Copilot uses AI. Check for mistakes.
Comment thread webapp/src/components/SubscriptionManagement.tsx
Comment thread resources/sql_scripts/CVManager_SampleData.sql Outdated
@jacob6838
Copy link
Copy Markdown
Collaborator Author

I am wondering if it would be worth it to manage intersection subscriptions by organization, so a user who is an admin in one org and user in another could only subscribe to support requests in the first org?

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.

2 participants