From 794c2a5952b686de6bd940cfa0eeca9e1a109cef Mon Sep 17 00:00:00 2001 From: Nicki Derrick Date: Mon, 3 Nov 2025 16:08:05 +0000 Subject: [PATCH 1/2] CCM-10814 WIP --- frontend/next.config.js | 33 -------- .../LogoutWarningModal.test.tsx.snap | 2 +- .../__snapshots__/AuthLink.test.tsx.snap | 6 +- .../__snapshots__/Header.test.tsx.snap | 4 +- .../src/__tests__/utils/get-auth-url.test.ts | 80 +++++++++++++++++++ frontend/src/app/auth/signin/route.dev.ts | 7 +- frontend/src/content/content.ts | 11 ++- frontend/src/utils/get-auth-url.ts | 11 +++ .../terraform/components/app/amplify_app.tf | 1 + 9 files changed, 106 insertions(+), 49 deletions(-) create mode 100644 frontend/src/__tests__/utils/get-auth-url.test.ts create mode 100644 frontend/src/utils/get-auth-url.ts diff --git a/frontend/next.config.js b/frontend/next.config.js index d6727a938..feaf6cf72 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -4,7 +4,6 @@ const { PHASE_DEVELOPMENT_SERVER } = require('next/constants'); const amplifyConfig = require('./amplify_outputs.json'); const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? '/templates'; -const domain = process.env.NOTIFY_DOMAIN_NAME ?? 'localhost:3000'; const nextConfig = (phase) => { const isDevServer = phase === PHASE_DEVELOPMENT_SERVER; @@ -37,42 +36,10 @@ const nextConfig = (phase) => { basePath: false, permanent: false, }, - { - source: `${basePath}/auth/inactive`, - destination: '/auth/inactive', - permanent: false, - basePath: false, - }, - { - source: `${basePath}/auth/signout`, - destination: '/auth/signout', - basePath: false, - permanent: false, - }, ]; }, async rewrites() { - if (includeAuthPages) { - return [ - { - source: '/auth/inactive', - destination: `http://${domain}${basePath}/auth/idle`, - basePath: false, - }, - { - source: '/auth/signout', - destination: `http://${domain}${basePath}/auth/signout`, - basePath: false, - }, - { - source: '/auth', - destination: `http://${domain}${basePath}/auth`, - basePath: false, - }, - ]; - } - return []; }, diff --git a/frontend/src/__tests__/components/molecules/LogoutWarningModal/__snapshots__/LogoutWarningModal.test.tsx.snap b/frontend/src/__tests__/components/molecules/LogoutWarningModal/__snapshots__/LogoutWarningModal.test.tsx.snap index 1ea669db1..1f7c08313 100644 --- a/frontend/src/__tests__/components/molecules/LogoutWarningModal/__snapshots__/LogoutWarningModal.test.tsx.snap +++ b/frontend/src/__tests__/components/molecules/LogoutWarningModal/__snapshots__/LogoutWarningModal.test.tsx.snap @@ -43,7 +43,7 @@ exports[`LogoutWarningModal should match snapshot 1`] = ` Sign out diff --git a/frontend/src/__tests__/components/molecules/__snapshots__/AuthLink.test.tsx.snap b/frontend/src/__tests__/components/molecules/__snapshots__/AuthLink.test.tsx.snap index 21d22dc52..03cd61a47 100644 --- a/frontend/src/__tests__/components/molecules/__snapshots__/AuthLink.test.tsx.snap +++ b/frontend/src/__tests__/components/molecules/__snapshots__/AuthLink.test.tsx.snap @@ -5,7 +5,7 @@ exports[`AuthLink renders Sign in link when authStatus changes to unauthenticate Sign in @@ -18,7 +18,7 @@ exports[`AuthLink renders Sign in link when authStatus is configuring 1`] = ` Sign in @@ -31,7 +31,7 @@ exports[`AuthLink renders Sign out link when authStatus changes to authenticated Sign out diff --git a/frontend/src/__tests__/components/molecules/__snapshots__/Header.test.tsx.snap b/frontend/src/__tests__/components/molecules/__snapshots__/Header.test.tsx.snap index 0c62d87a4..deb18d42c 100644 --- a/frontend/src/__tests__/components/molecules/__snapshots__/Header.test.tsx.snap +++ b/frontend/src/__tests__/components/molecules/__snapshots__/Header.test.tsx.snap @@ -82,7 +82,7 @@ exports[`NhsNotifyHeader when authenticated matches snapshot (authenticated) 1`] Sign out @@ -176,7 +176,7 @@ exports[`NhsNotifyHeader when unauthenticated matches snapshot (unauthenticated) Sign in diff --git a/frontend/src/__tests__/utils/get-auth-url.test.ts b/frontend/src/__tests__/utils/get-auth-url.test.ts new file mode 100644 index 000000000..05033795d --- /dev/null +++ b/frontend/src/__tests__/utils/get-auth-url.test.ts @@ -0,0 +1,80 @@ +import { getAuthUrl } from '@utils/get-auth-url'; + +describe('getAuthUrl', () => { + const originalEnv = process.env; + + beforeEach(() => { + jest.resetModules(); + process.env = { ...originalEnv }; + }); + + afterAll(() => { + process.env = originalEnv; + }); + + describe('in production', () => { + beforeEach(() => { + Object.defineProperty(process.env, 'NODE_ENV', { + value: 'production', + configurable: true, + }); + process.env.NOTIFY_DOMAIN_NAME = 'nhsnotify.nhs.uk'; + }); + + it('should use https protocol', () => { + const result = getAuthUrl('/auth'); + expect(result).toBe('https://nhsnotify.nhs.uk/auth'); + }); + + it('should not include basePath for auth app URLs', () => { + const result = getAuthUrl('/auth/signout'); + expect(result).toBe('https://nhsnotify.nhs.uk/auth/signout'); + }); + + it('should handle query parameters', () => { + const result = getAuthUrl('/auth?redirect=%2Ftemplates%2Fcreate'); + expect(result).toBe( + 'https://nhsnotify.nhs.uk/auth?redirect=%2Ftemplates%2Fcreate' + ); + }); + + it('should fallback to localhost when NOTIFY_DOMAIN_NAME is not set', () => { + delete process.env.NOTIFY_DOMAIN_NAME; + const result = getAuthUrl('/auth'); + expect(result).toBe('https://localhost:3000/auth'); + }); + }); + + describe('in development', () => { + beforeEach(() => { + Object.defineProperty(process.env, 'NODE_ENV', { + value: 'development', + configurable: true, + }); + }); + + it('should use http protocol', () => { + const result = getAuthUrl('/auth'); + expect(result).toBe('http://localhost:3000/templates/auth'); + }); + + it('should include basePath to hit local auth pages', () => { + process.env.NEXT_PUBLIC_BASE_PATH = '/templates'; + const result = getAuthUrl('/auth'); + expect(result).toBe('http://localhost:3000/templates/auth'); + }); + + it('should fallback to /templates basePath when NEXT_PUBLIC_BASE_PATH is undefined', () => { + delete process.env.NEXT_PUBLIC_BASE_PATH; + const result = getAuthUrl('/auth'); + expect(result).toBe('http://localhost:3000/templates/auth'); + }); + + it('should handle query parameters with basePath', () => { + const result = getAuthUrl('/auth?redirect=%2Ftemplates%2Fcreate'); + expect(result).toBe( + 'http://localhost:3000/templates/auth?redirect=%2Ftemplates%2Fcreate' + ); + }); + }); +}); diff --git a/frontend/src/app/auth/signin/route.dev.ts b/frontend/src/app/auth/signin/route.dev.ts index 054f2efd6..ac31f700e 100644 --- a/frontend/src/app/auth/signin/route.dev.ts +++ b/frontend/src/app/auth/signin/route.dev.ts @@ -33,10 +33,5 @@ export const GET = async (request: NextRequest) => { } } - return NextResponse.json(null, { - status: 307, - headers: { - Location: redirectPath, - }, - }); + return NextResponse.redirect(redirectPath, { status: 307 }); }; diff --git a/frontend/src/content/content.ts b/frontend/src/content/content.ts index 225da778d..410f4cadd 100644 --- a/frontend/src/content/content.ts +++ b/frontend/src/content/content.ts @@ -1,5 +1,6 @@ import type { ContentBlock } from '@molecules/ContentRenderer/ContentRenderer'; import { getBasePath } from '@utils/get-base-path'; +import { getAuthUrl } from '@utils/get-auth-url'; import { TemplateStatus, TemplateType } from 'nhs-notify-backend-client'; const generatePageTitle = (title: string): string => { @@ -25,13 +26,15 @@ const header = { links: { signIn: { text: 'Sign in', - href: `/auth?redirect=${encodeURIComponent( - `${getBasePath()}/create-and-submit-templates` - )}`, + href: getAuthUrl( + `/auth?redirect=${encodeURIComponent( + `${getBasePath()}/create-and-submit-templates` + )}` + ), }, signOut: { text: 'Sign out', - href: '/auth/signout', + href: getAuthUrl('/auth/signout'), }, }, }, diff --git a/frontend/src/utils/get-auth-url.ts b/frontend/src/utils/get-auth-url.ts new file mode 100644 index 000000000..869d47613 --- /dev/null +++ b/frontend/src/utils/get-auth-url.ts @@ -0,0 +1,11 @@ +export function getAuthUrl(path: string): string { + const protocol = process.env.NODE_ENV === 'production' ? 'https:' : 'http:'; + const domain = process.env.NOTIFY_DOMAIN_NAME ?? 'localhost:3000'; + + const basePath = + process.env.NODE_ENV === 'development' + ? (process.env.NEXT_PUBLIC_BASE_PATH ?? '/templates') + : ''; + + return `${protocol}//${domain}${basePath}${path}`; +} diff --git a/infrastructure/terraform/components/app/amplify_app.tf b/infrastructure/terraform/components/app/amplify_app.tf index 31e186e7b..8e34f5417 100644 --- a/infrastructure/terraform/components/app/amplify_app.tf +++ b/infrastructure/terraform/components/app/amplify_app.tf @@ -34,6 +34,7 @@ resource "aws_amplify_app" "main" { CSRF_SECRET = aws_ssm_parameter.csrf_secret.value NEXT_PUBLIC_PROMPT_SECONDS_BEFORE_LOGOUT = 120 NEXT_PUBLIC_TIME_TILL_LOGOUT_SECONDS = 900 + NOTIFY_DOMAIN_NAME = local.root_domain_name NOTIFY_ENVIRONMENT = var.environment NOTIFY_GROUP = var.group USER_POOL_CLIENT_ID = jsondecode(aws_ssm_parameter.cognito_config.value)["USER_POOL_CLIENT_ID"] From de82ecff3280f7eb3bdd5f8614733eb8257ef190 Mon Sep 17 00:00:00 2001 From: Nicki Derrick Date: Thu, 20 Nov 2025 14:49:28 +0000 Subject: [PATCH 2/2] CCM-10814 WIP --- .../src/__tests__/utils/get-auth-url.test.ts | 197 ++++++++++++++---- frontend/src/utils/get-auth-url.ts | 23 +- .../terraform/components/app/amplify_app.tf | 1 + .../terraform/components/app/locals.tf | 5 + .../components/app/module_amplify_branch.tf | 5 +- .../terraform/components/app/outputs.tf | 1 + .../terraform/components/app/variables.tf | 12 ++ .../branch/module_amplify_branch.tf | 5 +- 8 files changed, 194 insertions(+), 55 deletions(-) diff --git a/frontend/src/__tests__/utils/get-auth-url.test.ts b/frontend/src/__tests__/utils/get-auth-url.test.ts index 05033795d..514b52e96 100644 --- a/frontend/src/__tests__/utils/get-auth-url.test.ts +++ b/frontend/src/__tests__/utils/get-auth-url.test.ts @@ -1,80 +1,189 @@ import { getAuthUrl } from '@utils/get-auth-url'; describe('getAuthUrl', () => { - const originalEnv = process.env; - - beforeEach(() => { - jest.resetModules(); - process.env = { ...originalEnv }; - }); + const originalWindow = { ...global.window }; + const originalEnv = { ...process.env }; afterAll(() => { - process.env = originalEnv; + Object.defineProperty(process, 'env', { + value: originalEnv, + configurable: true, + }); + + Object.defineProperty(global, 'window', { + value: originalWindow, + configurable: true, + }); }); - describe('in production', () => { + describe('client side (when window is available)', () => { beforeEach(() => { - Object.defineProperty(process.env, 'NODE_ENV', { - value: 'production', + Object.defineProperty(global, 'window', { + value: { + location: { + protocol: 'https:', + host: 'nhsnotify.national.nhs.uk', + }, + }, configurable: true, }); - process.env.NOTIFY_DOMAIN_NAME = 'nhsnotify.nhs.uk'; }); - it('should use https protocol', () => { - const result = getAuthUrl('/auth'); - expect(result).toBe('https://nhsnotify.nhs.uk/auth'); + afterAll(() => { + Object.defineProperty(global, 'window', { + value: originalWindow, + configurable: true, + }); }); - it('should not include basePath for auth app URLs', () => { - const result = getAuthUrl('/auth/signout'); - expect(result).toBe('https://nhsnotify.nhs.uk/auth/signout'); + it('should construct URL', () => { + const result = getAuthUrl('/auth'); + expect(result).toBe('https://nhsnotify.national.nhs.uk/auth'); }); it('should handle query parameters', () => { const result = getAuthUrl('/auth?redirect=%2Ftemplates%2Fcreate'); + expect(result).toBe( - 'https://nhsnotify.nhs.uk/auth?redirect=%2Ftemplates%2Fcreate' + 'https://nhsnotify.national.nhs.uk/auth?redirect=%2Ftemplates%2Fcreate' ); }); - it('should fallback to localhost when NOTIFY_DOMAIN_NAME is not set', () => { - delete process.env.NOTIFY_DOMAIN_NAME; - const result = getAuthUrl('/auth'); - expect(result).toBe('https://localhost:3000/auth'); + describe('in development env', () => { + beforeEach(() => { + Object.defineProperty(process.env, 'NODE_ENV', { + value: 'development', + configurable: true, + }); + }); + + afterAll(() => { + Object.defineProperty(process.env, 'NODE_ENV', { + value: originalEnv, + configurable: true, + }); + }); + + it('should include the base path set', () => { + process.env.NEXT_PUBLIC_BASE_PATH = '/base-path'; + + const result = getAuthUrl('/auth'); + expect(result).toBe('https://nhsnotify.national.nhs.uk/base-path/auth'); + }); + + it('should fallback to templates when no base path environment variable provided', () => { + delete process.env.NEXT_PUBLIC_BASE_PATH; + + const result = getAuthUrl('/auth'); + expect(result).toBe('https://nhsnotify.national.nhs.uk/templates/auth'); + }); }); }); - describe('in development', () => { + describe('when window is not available', () => { beforeEach(() => { - Object.defineProperty(process.env, 'NODE_ENV', { - value: 'development', - configurable: true, - }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + delete (global as any).window; }); - it('should use http protocol', () => { - const result = getAuthUrl('/auth'); - expect(result).toBe('http://localhost:3000/templates/auth'); + afterAll(() => { + global.window = originalWindow; }); - it('should include basePath to hit local auth pages', () => { - process.env.NEXT_PUBLIC_BASE_PATH = '/templates'; - const result = getAuthUrl('/auth'); - expect(result).toBe('http://localhost:3000/templates/auth'); - }); + describe('when gateway URL environment variable is available', () => { + beforeEach(() => { + process.env.NEXT_PUBLIC_GATEWAY_URL = + 'https://dev.web-gateway.nhsnotify.national.nhs.uk'; + }); - it('should fallback to /templates basePath when NEXT_PUBLIC_BASE_PATH is undefined', () => { - delete process.env.NEXT_PUBLIC_BASE_PATH; - const result = getAuthUrl('/auth'); - expect(result).toBe('http://localhost:3000/templates/auth'); + it('should use NEXT_PUBLIC_GATEWAY_URL', () => { + const result = getAuthUrl('/auth'); + expect(result).toBe( + 'https://dev.web-gateway.nhsnotify.national.nhs.uk/auth' + ); + }); + + it('should handle query parameters', () => { + const result = getAuthUrl('/auth?redirect=%2Ftemplates%2Fcreate'); + expect(result).toBe( + 'https://dev.web-gateway.nhsnotify.national.nhs.uk/auth?redirect=%2Ftemplates%2Fcreate' + ); + }); + + describe('in development env', () => { + beforeEach(() => { + Object.defineProperty(process.env, 'NODE_ENV', { + value: 'development', + configurable: true, + }); + }); + + afterAll(() => { + Object.defineProperty(process.env, 'NODE_ENV', { + value: originalEnv, + configurable: true, + }); + }); + + it('should include the base path set', () => { + process.env.NEXT_PUBLIC_BASE_PATH = '/base-path'; + + const result = getAuthUrl('/auth'); + expect(result).toBe( + 'https://dev.web-gateway.nhsnotify.national.nhs.uk/base-path/auth' + ); + }); + + it('should fallback to templates when no base path environment variable provided', () => { + delete process.env.NEXT_PUBLIC_BASE_PATH; + + const result = getAuthUrl('/auth'); + expect(result).toBe( + 'https://dev.web-gateway.nhsnotify.national.nhs.uk/templates/auth' + ); + }); + }); }); - it('should handle query parameters with basePath', () => { - const result = getAuthUrl('/auth?redirect=%2Ftemplates%2Fcreate'); - expect(result).toBe( - 'http://localhost:3000/templates/auth?redirect=%2Ftemplates%2Fcreate' - ); + describe('when no gateway URL environment variable is available', () => { + beforeEach(() => { + delete process.env.NEXT_PUBLIC_GATEWAY_URL; + }); + + it('should fallback to localhost:3000', () => { + const result = getAuthUrl('/auth'); + expect(result).toBe('http://localhost:3000/auth'); + }); + + describe('in development env', () => { + beforeEach(() => { + Object.defineProperty(process.env, 'NODE_ENV', { + value: 'development', + configurable: true, + }); + }); + + afterAll(() => { + Object.defineProperty(process.env, 'NODE_ENV', { + value: originalEnv, + configurable: true, + }); + }); + + it('should include the base path set', () => { + process.env.NEXT_PUBLIC_BASE_PATH = '/base-path'; + + const result = getAuthUrl('/auth'); + expect(result).toBe('http://localhost:3000/base-path/auth'); + }); + + it('should fallback to templates when no base path environment variable provided', () => { + delete process.env.NEXT_PUBLIC_BASE_PATH; + + const result = getAuthUrl('/auth'); + expect(result).toBe('http://localhost:3000/templates/auth'); + }); + }); }); }); }); diff --git a/frontend/src/utils/get-auth-url.ts b/frontend/src/utils/get-auth-url.ts index 869d47613..c5dc69fbf 100644 --- a/frontend/src/utils/get-auth-url.ts +++ b/frontend/src/utils/get-auth-url.ts @@ -1,11 +1,20 @@ +function getBasePath(): string { + return process.env.NODE_ENV === 'development' + ? (process.env.NEXT_PUBLIC_BASE_PATH ?? '/templates') + : ''; +} + export function getAuthUrl(path: string): string { - const protocol = process.env.NODE_ENV === 'production' ? 'https:' : 'http:'; - const domain = process.env.NOTIFY_DOMAIN_NAME ?? 'localhost:3000'; + const basePath = getBasePath(); + + if (typeof window !== 'undefined') { + return `${window.location.protocol}//${window.location.host}${basePath}${path}`; + } - const basePath = - process.env.NODE_ENV === 'development' - ? (process.env.NEXT_PUBLIC_BASE_PATH ?? '/templates') - : ''; + const gatewayUrl = process.env.NEXT_PUBLIC_GATEWAY_URL; + if (gatewayUrl) { + return `${gatewayUrl}${basePath}${path}`; + } - return `${protocol}//${domain}${basePath}${path}`; + return `http://localhost:3000${basePath}${path}`; } diff --git a/infrastructure/terraform/components/app/amplify_app.tf b/infrastructure/terraform/components/app/amplify_app.tf index 8e34f5417..e4998cc46 100644 --- a/infrastructure/terraform/components/app/amplify_app.tf +++ b/infrastructure/terraform/components/app/amplify_app.tf @@ -32,6 +32,7 @@ resource "aws_amplify_app" "main" { AMPLIFY_MONOREPO_APP_ROOT = "frontend" API_BASE_URL = module.backend_api.api_base_url CSRF_SECRET = aws_ssm_parameter.csrf_secret.value + NEXT_PUBLIC_GATEWAY_URL = local.gateway_url NEXT_PUBLIC_PROMPT_SECONDS_BEFORE_LOGOUT = 120 NEXT_PUBLIC_TIME_TILL_LOGOUT_SECONDS = 900 NOTIFY_DOMAIN_NAME = local.root_domain_name diff --git a/infrastructure/terraform/components/app/locals.tf b/infrastructure/terraform/components/app/locals.tf index e078e5392..65f4f0558 100644 --- a/infrastructure/terraform/components/app/locals.tf +++ b/infrastructure/terraform/components/app/locals.tf @@ -3,4 +3,9 @@ locals { root_domain_name = "${var.environment}.${local.acct.dns_zone["name"]}" lambdas_source_code_dir = "../../../../lambdas" log_destination_arn = "arn:aws:logs:${var.region}:${var.observability_account_id}:destination:nhs-main-obs-firehose-logs" + gateway_url = var.gateway_domain != null ? ( + var.use_environment_specific_gateway_domain + ? "https://${var.environment}.${var.gateway_domain}" + : "https://${var.gateway_domain}" + ) : "https://${aws_amplify_app.main.default_domain}" } diff --git a/infrastructure/terraform/components/app/module_amplify_branch.tf b/infrastructure/terraform/components/app/module_amplify_branch.tf index ceb0e02f8..6e4183152 100644 --- a/infrastructure/terraform/components/app/module_amplify_branch.tf +++ b/infrastructure/terraform/components/app/module_amplify_branch.tf @@ -16,7 +16,8 @@ module "amplify_branch" { enable_auto_build = false environment_variables = { - NOTIFY_SUBDOMAIN = var.environment - NEXT_PUBLIC_BASE_PATH = "/templates" + NOTIFY_SUBDOMAIN = var.environment + NEXT_PUBLIC_BASE_PATH = "/templates" + NEXT_PUBLIC_GATEWAY_URL = local.gateway_url } } diff --git a/infrastructure/terraform/components/app/outputs.tf b/infrastructure/terraform/components/app/outputs.tf index c3af0e6d4..fbebf6ff1 100644 --- a/infrastructure/terraform/components/app/outputs.tf +++ b/infrastructure/terraform/components/app/outputs.tf @@ -3,6 +3,7 @@ output "amplify" { id = aws_amplify_app.main.id domain_name = local.root_domain_name branch_name = var.branch_name + gateway_url = local.gateway_url } } diff --git a/infrastructure/terraform/components/app/variables.tf b/infrastructure/terraform/components/app/variables.tf index 03a3f705a..36bba0da8 100644 --- a/infrastructure/terraform/components/app/variables.tf +++ b/infrastructure/terraform/components/app/variables.tf @@ -212,3 +212,15 @@ variable "external_email_domain" { default = null description = "Externally managed domain used to create an SES identity for sending emails from. Validation DNS records will need to be manually configured in the DNS provider." } + +variable "gateway_domain" { + type = string + description = "The web gateway domain (e.g., notify.nhs.uk or web-gateway.nhsnotify.national.nhs.uk)" + default = "" +} + +variable "use_environment_specific_gateway_domain" { + type = bool + description = "Whether to prefix the gateway domain with the environment name" + default = false +} diff --git a/infrastructure/terraform/components/branch/module_amplify_branch.tf b/infrastructure/terraform/components/branch/module_amplify_branch.tf index f9badd863..65ba36841 100644 --- a/infrastructure/terraform/components/branch/module_amplify_branch.tf +++ b/infrastructure/terraform/components/branch/module_amplify_branch.tf @@ -18,7 +18,8 @@ module "amplify_branch" { enable_auto_build = true environment_variables = { - NOTIFY_SUBDOMAIN = var.environment - NEXT_PUBLIC_BASE_PATH = "/templates~${local.normalised_branch_name}" + NOTIFY_SUBDOMAIN = var.environment + NEXT_PUBLIC_BASE_PATH = "/templates~${local.normalised_branch_name}" + NEXT_PUBLIC_GATEWAY_URL = local.app.amplify["gateway_url"] } }