From 07e00b930fcc09d069147f5df54c8207c847e531 Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Fri, 3 Jan 2025 10:50:54 +0000 Subject: [PATCH 01/33] Add logout button when user is logged in. Also create a lambda to delete records from the dynamo table, but unsure if its needed --- ...eps-prescription-tracker-ui.code-workspace | 4 + packages/cpt-ui/components/EpsHeader.tsx | 37 +++++- packages/cpt-ui/context/AuthProvider.tsx | 14 +-- packages/logoutLambda/.vscode/launch.json | 32 +++++ packages/logoutLambda/.vscode/settings.json | 7 ++ packages/logoutLambda/package-lock.json | 48 ++++++++ packages/logoutLambda/package.json | 19 +++ packages/logoutLambda/src/index.tsx | 76 ++++++++++++ packages/logoutLambda/tsconfig.json | 111 ++++++++++++++++++ 9 files changed, 335 insertions(+), 13 deletions(-) create mode 100644 packages/logoutLambda/.vscode/launch.json create mode 100644 packages/logoutLambda/.vscode/settings.json create mode 100644 packages/logoutLambda/package-lock.json create mode 100644 packages/logoutLambda/package.json create mode 100644 packages/logoutLambda/src/index.tsx create mode 100644 packages/logoutLambda/tsconfig.json diff --git a/.vscode/eps-prescription-tracker-ui.code-workspace b/.vscode/eps-prescription-tracker-ui.code-workspace index b001080157..5886f2eb00 100644 --- a/.vscode/eps-prescription-tracker-ui.code-workspace +++ b/.vscode/eps-prescription-tracker-ui.code-workspace @@ -43,6 +43,10 @@ { "name": "packages/trackerUserInfoLambda", "path": "../packages/trackerUserInfoLambda" + }, + { + "name": "packages/logoutLambda", + "path": "../packages/logoutLambda" } ], diff --git a/packages/cpt-ui/components/EpsHeader.tsx b/packages/cpt-ui/components/EpsHeader.tsx index d64beacdb0..a42a0b56aa 100644 --- a/packages/cpt-ui/components/EpsHeader.tsx +++ b/packages/cpt-ui/components/EpsHeader.tsx @@ -1,5 +1,5 @@ 'use client' -import React from 'react' +import React, { useContext } from 'react' import Link from 'next/link'; import "@/assets/styles/header.scss" import { useRouter, usePathname } from 'next/navigation'; @@ -14,11 +14,23 @@ import { HEADER_SELECT_YOUR_ROLE_TARGET } from "@/constants/ui-strings/HeaderStrings" +import { AuthContext } from "@/context/AuthProvider"; + export default function EpsHeader() { const router = useRouter() const pathname = usePathname(); + + const auth = useContext(AuthContext); + + const handleLogoutClick = async (e: React.MouseEvent) => { + e.preventDefault(); + await auth?.cognitoSignOut(); + router.push("/"); + }; + console.log(router); // Query parameters - return ( + + return (
@@ -32,9 +44,11 @@ export default function EpsHeader() {
  • Placeholder 1
  • +
  • Placeholder 2
  • + {pathname != '/' ? (
  • {HEADER_CHANGE_ROLE_BUTTON} @@ -46,6 +60,7 @@ export default function EpsHeader() {
  • ) } + {pathname === '/selectyourrole' ? (
  • {HEADER_CONFIRM_ROLE_BUTTON} @@ -55,10 +70,20 @@ export default function EpsHeader() { {HEADER_SELECT_YOUR_ROLE_BUTTON}
  • )} -
  • - Placeholder 3 -
  • - {/* Placeholder 3 */} + + {auth?.isSignedIn ? ( +
  • + + Log out + +
  • + ) : null} +
    diff --git a/packages/cpt-ui/context/AuthProvider.tsx b/packages/cpt-ui/context/AuthProvider.tsx index 26323eebf6..4c2b2fab01 100644 --- a/packages/cpt-ui/context/AuthProvider.tsx +++ b/packages/cpt-ui/context/AuthProvider.tsx @@ -155,17 +155,17 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { */ const cognitoSignOut = async () => { console.log("Signing out..."); - // Immediately reset state to signed out. - setUser(null); - setAccessToken(null); - setIdToken(null); - setIsSignedIn(false); - setError(null) try { await signOut({ global: true }); console.log("Signed out successfully!"); - setError(null); + + // Immediately reset state to signed out. + setUser(null); + setAccessToken(null); + setIdToken(null); + setIsSignedIn(false); + setError(null) } catch (err) { console.error("Failed to sign out:", err); setError(String(err)); diff --git a/packages/logoutLambda/.vscode/launch.json b/packages/logoutLambda/.vscode/launch.json new file mode 100644 index 0000000000..5d1ee3c94c --- /dev/null +++ b/packages/logoutLambda/.vscode/launch.json @@ -0,0 +1,32 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "name": "vscode-jest-tests.v2", + "request": "launch", + "args": [ + "--runInBand", + "--watchAll=false", + "--testNamePattern", + "${jest.testNamePattern}", + "--runTestsByPath", + "${jest.testFile}", + "--config", + "${workspaceFolder}/jest.debug.config.ts" + ], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "program": "${workspaceFolder}/../../node_modules/.bin/jest", + "windows": { + "program": "${workspaceFolder}/node_modules/jest/bin/jest" + }, + "env": { + "POWERTOOLS_DEV": true, + "NODE_OPTIONS": "--experimental-vm-modules" + } + } + ] +} diff --git a/packages/logoutLambda/.vscode/settings.json b/packages/logoutLambda/.vscode/settings.json new file mode 100644 index 0000000000..657fd71293 --- /dev/null +++ b/packages/logoutLambda/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "jest.jestCommandLine": "/workspaces/eps-prescription-tracker-ui/node_modules/.bin/jest --no-cache", + "jest.nodeEnv": { + "POWERTOOLS_DEV": true, + "NODE_OPTIONS": "--experimental-vm-modules" + } + } diff --git a/packages/logoutLambda/package-lock.json b/packages/logoutLambda/package-lock.json new file mode 100644 index 0000000000..3014086918 --- /dev/null +++ b/packages/logoutLambda/package-lock.json @@ -0,0 +1,48 @@ +{ + "name": "logoutlambda", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "logoutlambda", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@types/node": "^22.10.3", + "typescript": "^5.7.2" + } + }, + "node_modules/@types/node": { + "version": "22.10.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.3.tgz", + "integrity": "sha512-DifAyw4BkrufCILvD3ucnuN8eydUfc/C1GlyrnI+LK6543w5/L3VeVgf05o3B4fqSXP1dKYLOZsKfutpxPzZrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/packages/logoutLambda/package.json b/packages/logoutLambda/package.json new file mode 100644 index 0000000000..2f855f1596 --- /dev/null +++ b/packages/logoutLambda/package.json @@ -0,0 +1,19 @@ +{ + "name": "logoutlambda", + "version": "1.0.0", + "description": "Handles serverside token revokation for cpt-ui", + "main": "index.js", + "author": "NHS Digital", + "license": "MIT", + "scripts": { + "unit": "POWERTOOLS_DEV=true NODE_OPTIONS=--experimental-vm-modules jest --no-cache --coverage", + "lint": "eslint --max-warnings 0 --fix --config ../../eslint.config.mjs .", + "compile": "tsc", + "test": "npm run compile && npm run unit", + "check-licenses": "license-checker --failOn GPL --failOn LGPL --start ../.." + }, + "devDependencies": { + "@types/node": "^22.10.3", + "typescript": "^5.7.2" + } +} diff --git a/packages/logoutLambda/src/index.tsx b/packages/logoutLambda/src/index.tsx new file mode 100644 index 0000000000..a0ec588696 --- /dev/null +++ b/packages/logoutLambda/src/index.tsx @@ -0,0 +1,76 @@ +import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda" +import { Logger } from "@aws-lambda-powertools/logger" +import { injectLambdaContext } from "@aws-lambda-powertools/logger/middleware" +import middy from "@middy/core" +import inputOutputLogger from "@middy/input-output-logger" +import { MiddyErrorHandler } from "@cpt-ui-common/middyErrorHandler" + +import { DynamoDBClient } from "@aws-sdk/client-dynamodb" +import { DynamoDBDocumentClient, DeleteCommand } from "@aws-sdk/lib-dynamodb" + +const logger = new Logger({ serviceName: "logout" }) + +const TokenMappingTableName = process.env["TokenMappingTableName"] as string + +// Initialize DynamoDB clients +const dynamoClient = new DynamoDBClient({}) +const documentClient = DynamoDBDocumentClient.from(dynamoClient) + +const errorResponseBody = { + message: "A system error has occurred" +} + +const middyErrorHandler = new MiddyErrorHandler(errorResponseBody) + +/** + * The core Lambda handler (logoutHandler): + * Parses the username from the request. + * Deletes the record in DynamoDB where the partition key is 'username'. + */ +const logoutHandler = async ( + event: APIGatewayProxyEvent +): Promise => { + logger.appendKeys({ + "apigw-request-id": event.requestContext?.requestId + }) + + if (!event.body) { + throw new Error("Missing request body") + } + + // Parse username from the request (user will have to be authorized to reach this point) + const username = event.requestContext.authorizer?.claims["cognito:username"] + if (!username) { + throw new Error("username is required in the request body") + } + + logger.debug("Attempting to delete user token record", { username }) + + // Build and send DeleteCommand + await documentClient.send( + new DeleteCommand({ + TableName: TokenMappingTableName, + Key: { + username + } + }) + ) + + return { + statusCode: 200, + body: JSON.stringify({ + message: `Token record for user '${username}' has been deleted.`, + }), + } +} + +export const handler = middy(logoutHandler) + .use(injectLambdaContext(logger, { clearState: true })) + .use( + inputOutputLogger({ + logger: (request) => { + logger.info(request) + } + }) + ) + .use(middyErrorHandler.errorHandler({ logger })) diff --git a/packages/logoutLambda/tsconfig.json b/packages/logoutLambda/tsconfig.json new file mode 100644 index 0000000000..c9c555d96f --- /dev/null +++ b/packages/logoutLambda/tsconfig.json @@ -0,0 +1,111 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} From 393f422ba4ec58e76d51bd81751590130dea372d Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Fri, 3 Jan 2025 14:57:02 +0000 Subject: [PATCH 02/33] Create the modal. Also match behaviour to the spec. Logout page still needs checking to see if it works properly. --- .github/workflows/deploy_website_content.yml | 2 +- packages/cdk/resources/Cognito.ts | 4 +- packages/cpt-ui/app/logout/page.tsx | 43 ++++ packages/cpt-ui/app/selectyourrole/page.tsx | 4 +- .../cpt-ui/assets/styles/EpsLogoutModal.scss | 43 ++++ packages/cpt-ui/components/EpsHeader.tsx | 196 +++++++++++------- packages/cpt-ui/components/EpsLogoutModal.tsx | 67 ++++++ .../ui-strings/EpsLogoutModalStrings.ts | 6 + packages/cpt-ui/context/AuthProvider.tsx | 2 + packages/cpt-ui/tsconfig.json | 6 +- 10 files changed, 285 insertions(+), 88 deletions(-) create mode 100644 packages/cpt-ui/app/logout/page.tsx create mode 100644 packages/cpt-ui/assets/styles/EpsLogoutModal.scss create mode 100644 packages/cpt-ui/components/EpsLogoutModal.tsx create mode 100644 packages/cpt-ui/constants/ui-strings/EpsLogoutModalStrings.ts diff --git a/.github/workflows/deploy_website_content.yml b/.github/workflows/deploy_website_content.yml index 17afb18bfd..5b2dd98ae1 100644 --- a/.github/workflows/deploy_website_content.yml +++ b/.github/workflows/deploy_website_content.yml @@ -76,7 +76,7 @@ jobs: export NEXT_PUBLIC_userPoolClientId=${userPoolClientId} export NEXT_PUBLIC_userPoolId=${userPoolId} export NEXT_PUBLIC_redirectSignIn="https://${fullCloudfrontDomain}/site/auth_demo.html" - export NEXT_PUBLIC_redirectSignOut="https://${fullCloudfrontDomain}/site/" + export NEXT_PUBLIC_redirectSignOut="https://${fullCloudfrontDomain}/site/logout.html" export NEXT_PUBLIC_COMMIT_ID=${{ inputs.COMMIT_ID }} cd .build diff --git a/packages/cdk/resources/Cognito.ts b/packages/cdk/resources/Cognito.ts index 9de654e121..622ac6a08d 100644 --- a/packages/cdk/resources/Cognito.ts +++ b/packages/cdk/resources/Cognito.ts @@ -194,7 +194,7 @@ export class Cognito extends Construct { ] const logoutUrls = [ - `https://${props.fullCloudfrontDomain}/site/`, + `https://${props.fullCloudfrontDomain}/site/logout`, `https://${props.fullCloudfrontDomain}/site/auth_demo.html`, `https://${props.fullCloudfrontDomain}/auth_demo/` ] @@ -202,7 +202,7 @@ export class Cognito extends Construct { if (props.useLocalhostCallback) { callbackUrls.push( "http://localhost:3000/auth/") callbackUrls.push( "http://localhost:3000/auth_demo/") - logoutUrls.push( "http://localhost:3000/") + logoutUrls.push( "http://localhost:3000/logout/") } // add the web client const userPoolWebClient = userPool.addClient("WebClient", { diff --git a/packages/cpt-ui/app/logout/page.tsx b/packages/cpt-ui/app/logout/page.tsx new file mode 100644 index 0000000000..82e7498011 --- /dev/null +++ b/packages/cpt-ui/app/logout/page.tsx @@ -0,0 +1,43 @@ +'use client' +import React, { useContext, useEffect } from "react"; +import { Container, Button } from "nhsuk-react-components"; +import { AuthContext } from "@/context/AuthProvider"; + +export default function LogoutPage() { + + const auth = useContext(AuthContext); + + // Log out on page load + useEffect(() => { + const signOut = async () => { + console.log("Signing out", auth); + await auth?.cognitoSignOut(); + console.log("Signed out: ", auth); + } + + if (auth?.isSignedIn) { + signOut(); + } else { + console.log("Cannot sign out - not signed in"); + } + }, [auth]); + + return ( +
    + + {auth?.isSignedIn ? ( +

    Logging out

    + // FIXME: Spinner here + ) : ( + <> +

    Logout successful

    +
    You are now logged out of the service. To continue using the application, you must log in again.
    + + + )} +
    +
    + ); +} diff --git a/packages/cpt-ui/app/selectyourrole/page.tsx b/packages/cpt-ui/app/selectyourrole/page.tsx index 4b293a12f5..e835bbf000 100644 --- a/packages/cpt-ui/app/selectyourrole/page.tsx +++ b/packages/cpt-ui/app/selectyourrole/page.tsx @@ -69,7 +69,7 @@ export default function SelectYourRolePage() { if (!auth?.isSignedIn || !auth) { setLoading(false) - setError("Not signed in") + setError(null) return; } @@ -137,8 +137,6 @@ export default function SelectYourRolePage() { if (auth?.isSignedIn) { fetchTrackerUserInfo() - } else { - setError("No login session found") } }, [auth?.isSignedIn, fetchTrackerUserInfo]) diff --git a/packages/cpt-ui/assets/styles/EpsLogoutModal.scss b/packages/cpt-ui/assets/styles/EpsLogoutModal.scss new file mode 100644 index 0000000000..3bae269e8d --- /dev/null +++ b/packages/cpt-ui/assets/styles/EpsLogoutModal.scss @@ -0,0 +1,43 @@ +.EpsLogoutModalOverlay { + position: fixed; + top: 0; + left: 0; + height: 100vh; + width: 100vw; + background-color: rgba(0, 0, 0, 0.5); // darken the background + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + } + + .EpsLogoutModalContent { + background: #fff; + padding: 2rem; + border-radius: 4px; + position: relative; + max-width: 600px; + width: 90%; + } + + .EpsLogoutModalButton { + width: 40%; + } + + .EpsLogoutModalCloseButton { + position: absolute; + top: 1rem; + right: 1rem; + font-size: 1.5rem; + background: transparent; + border-radius: 4px; + cursor: pointer; // TODO: Does this want to be a normal cursor Or a pointer? + } + + .EpsLogoutModalButtonGroup { + display: flex; + justify-content: center; + gap: 20%; + margin-top: 2rem; + } + diff --git a/packages/cpt-ui/components/EpsHeader.tsx b/packages/cpt-ui/components/EpsHeader.tsx index a42a0b56aa..996988ebf9 100644 --- a/packages/cpt-ui/components/EpsHeader.tsx +++ b/packages/cpt-ui/components/EpsHeader.tsx @@ -1,91 +1,133 @@ -'use client' -import React, { useContext } from 'react' -import Link from 'next/link'; -import "@/assets/styles/header.scss" -import { useRouter, usePathname } from 'next/navigation'; +"use client" +import React, { useContext, useState } from "react"; +import Link from "next/link"; +import "@/assets/styles/header.scss"; +import { useRouter, usePathname } from "next/navigation"; import { Header } from "nhsuk-react-components"; import { - HEADER_SERVICE, - HEADER_CONFIRM_ROLE_BUTTON, - HEADER_CONFIRM_ROLE_TARGET, - HEADER_CHANGE_ROLE_BUTTON, - HEADER_CHANGE_ROLE_TARGET, - HEADER_SELECT_YOUR_ROLE_BUTTON, - HEADER_SELECT_YOUR_ROLE_TARGET -} from "@/constants/ui-strings/HeaderStrings" + HEADER_SERVICE, + HEADER_CONFIRM_ROLE_BUTTON, + HEADER_CONFIRM_ROLE_TARGET, + HEADER_CHANGE_ROLE_BUTTON, + HEADER_CHANGE_ROLE_TARGET, + HEADER_SELECT_YOUR_ROLE_BUTTON, + HEADER_SELECT_YOUR_ROLE_TARGET +} from "@/constants/ui-strings/HeaderStrings"; import { AuthContext } from "@/context/AuthProvider"; +import { EpsLogoutModal } from "@/components/EpsLogoutModal"; export default function EpsHeader() { - const router = useRouter() - const pathname = usePathname(); + const router = useRouter(); + const pathname = usePathname(); + const auth = useContext(AuthContext); - const auth = useContext(AuthContext); + const [showLogoutModal, setShowLogoutModal] = useState(false); - const handleLogoutClick = async (e: React.MouseEvent) => { - e.preventDefault(); - await auth?.cognitoSignOut(); - router.push("/"); - }; + const handleConfirmLogout = async () => { + setShowLogoutModal(false); + router.push("/logout"); + }; - console.log(router); // Query parameters + const handleLogoutClick = (e: React.MouseEvent) => { + e.preventDefault(); + setShowLogoutModal(true); + }; - return ( -
    - - + return ( + <> +
    + + - - {HEADER_SERVICE} - - - - -
  • - Placeholder 1 -
  • - -
  • - Placeholder 2 -
  • - - {pathname != '/' ? ( -
  • - {HEADER_CHANGE_ROLE_BUTTON} -
  • - ) : - ( -
  • - {HEADER_CONFIRM_ROLE_BUTTON} -
  • - ) - } - - {pathname === '/selectyourrole' ? ( -
  • - {HEADER_CONFIRM_ROLE_BUTTON} -
  • - ) : ( -
  • - {HEADER_SELECT_YOUR_ROLE_BUTTON} -
  • - )} + + {HEADER_SERVICE} + + + - {auth?.isSignedIn ? ( -
  • - - Log out - -
  • - ) : null} + + {/* Example placeholder links */} +
  • + + Placeholder 1 + +
  • - -
    -
    - ) +
  • + + Placeholder 2 + +
  • + + {/* Conditionally show "change role" or "confirm role" */} + {pathname !== "/" ? ( +
  • + + {HEADER_CHANGE_ROLE_BUTTON} + +
  • + ) : ( +
  • + + {HEADER_CONFIRM_ROLE_BUTTON} + +
  • + )} + + {pathname === "/selectyourrole" ? ( +
  • + + {HEADER_CONFIRM_ROLE_BUTTON} + +
  • + ) : ( +
  • + + {HEADER_SELECT_YOUR_ROLE_BUTTON} + +
  • + )} + + {/* FIXME: Only shows the Log out link if the user is signed in, but introduces a lag on page reload. Acceptable? */} + {auth?.isSignedIn && ( +
  • + + Log out + +
  • + )} + + + +
    + + setShowLogoutModal(false)} + onConfirm={handleConfirmLogout} + /> + + ); } diff --git a/packages/cpt-ui/components/EpsLogoutModal.tsx b/packages/cpt-ui/components/EpsLogoutModal.tsx new file mode 100644 index 0000000000..06a053df3d --- /dev/null +++ b/packages/cpt-ui/components/EpsLogoutModal.tsx @@ -0,0 +1,67 @@ +"use client"; +import React, { useEffect } from "react"; +import { Button } from "nhsuk-react-components" + +import "@/assets/styles/EpsLogoutModal.scss"; +import { EpsLogoutModalStrings } from "@/constants/ui-strings/EpsLogoutModalStrings"; + +interface EpsLogoutModalProps { + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; +} + +export function EpsLogoutModal({ isOpen, onClose, onConfirm }: EpsLogoutModalProps) { + // Close modal on `Escape` key + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + onClose(); + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [onClose]); + + // If the modal isn’t open, don’t render anything + if (!isOpen) return null; + + // Close if user clicks outside the modal content + const handleBackdropClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + onClose(); + } + }; + + + return ( +
    +
    + + +

    {EpsLogoutModalStrings.title}

    +

    {EpsLogoutModalStrings.caption}

    + + {/* TODO: style appropriately */} +
    + + + +
    +
    +
    + ); +} diff --git a/packages/cpt-ui/constants/ui-strings/EpsLogoutModalStrings.ts b/packages/cpt-ui/constants/ui-strings/EpsLogoutModalStrings.ts new file mode 100644 index 0000000000..dd5cd2e9ec --- /dev/null +++ b/packages/cpt-ui/constants/ui-strings/EpsLogoutModalStrings.ts @@ -0,0 +1,6 @@ +export const EpsLogoutModalStrings = { + title: "Are you sure you want to log out?", + caption: "Logging out will end your session.", + confirmButtonText: "Yes", + cancelButtonText: "No" +} diff --git a/packages/cpt-ui/context/AuthProvider.tsx b/packages/cpt-ui/context/AuthProvider.tsx index 4c2b2fab01..110b562eba 100644 --- a/packages/cpt-ui/context/AuthProvider.tsx +++ b/packages/cpt-ui/context/AuthProvider.tsx @@ -156,6 +156,8 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { const cognitoSignOut = async () => { console.log("Signing out..."); + // TODO: Also sign out of the CPT API, so it can delete the token? + try { await signOut({ global: true }); console.log("Signed out successfully!"); diff --git a/packages/cpt-ui/tsconfig.json b/packages/cpt-ui/tsconfig.json index 095fcbcc42..c29f3a6819 100644 --- a/packages/cpt-ui/tsconfig.json +++ b/packages/cpt-ui/tsconfig.json @@ -11,11 +11,7 @@ "compilerOptions": { "baseUrl": ".", "paths": { - "@/context/*": ["context/*"], - "@/app/*": ["app/*"], - "@/constants/*": ["constants/*"], - "@/assets/*": ["assets/*"], - "@/components/*": ["components/*"] + "@/*": ["*"], }, "lib": [ "dom", From 414e411eddb0539c2764497af6c34d50ccf5b9a4 Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Fri, 3 Jan 2025 15:55:07 +0000 Subject: [PATCH 03/33] Fix some minor things --- packages/cdk/resources/Cognito.ts | 8 ++++---- packages/cpt-ui/app/logout/page.tsx | 2 +- packages/cpt-ui/constants/ui-strings/HeaderStrings.ts | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/cdk/resources/Cognito.ts b/packages/cdk/resources/Cognito.ts index 622ac6a08d..d4112ce3c0 100644 --- a/packages/cdk/resources/Cognito.ts +++ b/packages/cdk/resources/Cognito.ts @@ -195,14 +195,14 @@ export class Cognito extends Construct { const logoutUrls = [ `https://${props.fullCloudfrontDomain}/site/logout`, - `https://${props.fullCloudfrontDomain}/site/auth_demo.html`, + `https://${props.fullCloudfrontDomain}/site/logout.html`, `https://${props.fullCloudfrontDomain}/auth_demo/` ] if (props.useLocalhostCallback) { - callbackUrls.push( "http://localhost:3000/auth/") - callbackUrls.push( "http://localhost:3000/auth_demo/") - logoutUrls.push( "http://localhost:3000/logout/") + callbackUrls.push("http://localhost:3000/site/auth/") + callbackUrls.push("http://localhost:3000/site/auth_demo/") + logoutUrls.push("http://localhost:3000/site/logout/") } // add the web client const userPoolWebClient = userPool.addClient("WebClient", { diff --git a/packages/cpt-ui/app/logout/page.tsx b/packages/cpt-ui/app/logout/page.tsx index 82e7498011..d3969ee5c7 100644 --- a/packages/cpt-ui/app/logout/page.tsx +++ b/packages/cpt-ui/app/logout/page.tsx @@ -32,7 +32,7 @@ export default function LogoutPage() { <>

    Logout successful

    You are now logged out of the service. To continue using the application, you must log in again.
    - diff --git a/packages/cpt-ui/constants/ui-strings/HeaderStrings.ts b/packages/cpt-ui/constants/ui-strings/HeaderStrings.ts index f9a48ae6f5..831bc8a4d9 100644 --- a/packages/cpt-ui/constants/ui-strings/HeaderStrings.ts +++ b/packages/cpt-ui/constants/ui-strings/HeaderStrings.ts @@ -1,8 +1,8 @@ // HEADER strings export const HEADER_SERVICE = "Clinical prescription tracking service" export const HEADER_CONFIRM_ROLE_BUTTON = "Confirm Role" -export const HEADER_CONFIRM_ROLE_TARGET = "confirmrole" +export const HEADER_CONFIRM_ROLE_TARGET = "/confirmrole" export const HEADER_CHANGE_ROLE_BUTTON = "Change Role" -export const HEADER_CHANGE_ROLE_TARGET = "changerole" +export const HEADER_CHANGE_ROLE_TARGET = "/changerole" export const HEADER_SELECT_YOUR_ROLE_BUTTON = "Select Your Role" -export const HEADER_SELECT_YOUR_ROLE_TARGET = "selectyourrole" +export const HEADER_SELECT_YOUR_ROLE_TARGET = "/selectyourrole" From 9209001ef425997807e29f85101dc070a975e02d Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Fri, 3 Jan 2025 16:27:10 +0000 Subject: [PATCH 04/33] Fix redirects --- packages/cdk/resources/Cognito.ts | 12 ++++++------ packages/cpt-ui/app/logout/page.tsx | 12 +++++++++--- packages/cpt-ui/components/EpsHeader.tsx | 15 +++++++++++++-- packages/cpt-ui/components/EpsLogoutModal.tsx | 10 +++++----- 4 files changed, 33 insertions(+), 16 deletions(-) diff --git a/packages/cdk/resources/Cognito.ts b/packages/cdk/resources/Cognito.ts index d4112ce3c0..7e813d468a 100644 --- a/packages/cdk/resources/Cognito.ts +++ b/packages/cdk/resources/Cognito.ts @@ -186,22 +186,22 @@ export class Cognito extends Construct { } const callbackUrls = [ - `https://${props.fullCloudfrontDomain}/site/`, + `https://${props.fullCloudfrontDomain}/site`, + `https://${props.fullCloudfrontDomain}/site/selectyourrole`, // FIXME: This is temporary, until we get routing fixed - `https://${props.fullCloudfrontDomain}/site/auth_demo.html`, - `https://${props.fullCloudfrontDomain}/auth_demo/`, + `https://${props.fullCloudfrontDomain}/site/selectyourrole.html`, + `https://${props.fullCloudfrontDomain}/auth_demo`, `https://${props.fullCloudfrontDomain}/oauth2/idpresponse` ] const logoutUrls = [ `https://${props.fullCloudfrontDomain}/site/logout`, `https://${props.fullCloudfrontDomain}/site/logout.html`, - `https://${props.fullCloudfrontDomain}/auth_demo/` + `https://${props.fullCloudfrontDomain}/auth_demo` ] if (props.useLocalhostCallback) { - callbackUrls.push("http://localhost:3000/site/auth/") - callbackUrls.push("http://localhost:3000/site/auth_demo/") + callbackUrls.push("http://localhost:3000/site/selectyourrole/") logoutUrls.push("http://localhost:3000/site/logout/") } // add the web client diff --git a/packages/cpt-ui/app/logout/page.tsx b/packages/cpt-ui/app/logout/page.tsx index d3969ee5c7..f36ea163a4 100644 --- a/packages/cpt-ui/app/logout/page.tsx +++ b/packages/cpt-ui/app/logout/page.tsx @@ -1,6 +1,8 @@ 'use client' import React, { useContext, useEffect } from "react"; -import { Container, Button } from "nhsuk-react-components"; +import { Container } from "nhsuk-react-components"; +import Link from "next/link"; + import { AuthContext } from "@/context/AuthProvider"; export default function LogoutPage() { @@ -32,9 +34,13 @@ export default function LogoutPage() { <>

    Logout successful

    You are now logged out of the service. To continue using the application, you must log in again.
    - + )} diff --git a/packages/cpt-ui/components/EpsHeader.tsx b/packages/cpt-ui/components/EpsHeader.tsx index 996988ebf9..9633d91a28 100644 --- a/packages/cpt-ui/components/EpsHeader.tsx +++ b/packages/cpt-ui/components/EpsHeader.tsx @@ -3,7 +3,7 @@ import React, { useContext, useState } from "react"; import Link from "next/link"; import "@/assets/styles/header.scss"; import { useRouter, usePathname } from "next/navigation"; -import { Header } from "nhsuk-react-components"; +import { Header, Button } from "nhsuk-react-components"; import { HEADER_SERVICE, HEADER_CONFIRM_ROLE_BUTTON, @@ -108,6 +108,18 @@ export default function EpsHeader() { {/* FIXME: Only shows the Log out link if the user is signed in, but introduces a lag on page reload. Acceptable? */} {auth?.isSignedIn && (
  • + +
  • + )} + +
  • - )} diff --git a/packages/cpt-ui/components/EpsLogoutModal.tsx b/packages/cpt-ui/components/EpsLogoutModal.tsx index 06a053df3d..6a37bea026 100644 --- a/packages/cpt-ui/components/EpsLogoutModal.tsx +++ b/packages/cpt-ui/components/EpsLogoutModal.tsx @@ -23,18 +23,18 @@ export function EpsLogoutModal({ isOpen, onClose, onConfirm }: EpsLogoutModalPro window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [onClose]); - - // If the modal isn’t open, don’t render anything - if (!isOpen) return null; - + // Close if user clicks outside the modal content const handleBackdropClick = (e: React.MouseEvent) => { if (e.target === e.currentTarget) { onClose(); } }; + - + // If the modal isn’t open, don’t render anything + if (!isOpen) return null; + return (
    From 7ae051b5adbad49b9130628b38dbcb0e9f33790c Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Fri, 3 Jan 2025 16:29:32 +0000 Subject: [PATCH 05/33] Fix CSS --- packages/cpt-ui/components/EpsLogoutModal.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/cpt-ui/components/EpsLogoutModal.tsx b/packages/cpt-ui/components/EpsLogoutModal.tsx index 6a37bea026..801896637e 100644 --- a/packages/cpt-ui/components/EpsLogoutModal.tsx +++ b/packages/cpt-ui/components/EpsLogoutModal.tsx @@ -36,9 +36,9 @@ export function EpsLogoutModal({ isOpen, onClose, onConfirm }: EpsLogoutModalPro if (!isOpen) return null; return ( -
    -
    - @@ -46,16 +46,16 @@ export function EpsLogoutModal({ isOpen, onClose, onConfirm }: EpsLogoutModalPro

    {EpsLogoutModalStrings.caption}

    {/* TODO: style appropriately */} -
    +
    + )} From d994d710c81da1f9d6b0cc5674a775772ed6870a Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Mon, 6 Jan 2025 12:00:13 +0000 Subject: [PATCH 09/33] Rename login page --- packages/cpt-ui/app/{auth_demo => login}/page.tsx | 0 packages/cpt-ui/app/logout/page.tsx | 2 +- packages/cpt-ui/components/EpsHeader.tsx | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/cpt-ui/app/{auth_demo => login}/page.tsx (100%) diff --git a/packages/cpt-ui/app/auth_demo/page.tsx b/packages/cpt-ui/app/login/page.tsx similarity index 100% rename from packages/cpt-ui/app/auth_demo/page.tsx rename to packages/cpt-ui/app/login/page.tsx diff --git a/packages/cpt-ui/app/logout/page.tsx b/packages/cpt-ui/app/logout/page.tsx index ee898bbba0..b05ce3ebb4 100644 --- a/packages/cpt-ui/app/logout/page.tsx +++ b/packages/cpt-ui/app/logout/page.tsx @@ -42,7 +42,7 @@ export default function LogoutPage() {
    You are now logged out of the service. To continue using the application, you must log in again.
    Log in diff --git a/packages/cpt-ui/components/EpsHeader.tsx b/packages/cpt-ui/components/EpsHeader.tsx index 996988ebf9..51f44a6a4d 100644 --- a/packages/cpt-ui/components/EpsHeader.tsx +++ b/packages/cpt-ui/components/EpsHeader.tsx @@ -55,7 +55,7 @@ export default function EpsHeader() {
  • - + Placeholder 2
  • From ee43978aba06fb4c32c2b72ab58d55a604700c75 Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Mon, 6 Jan 2025 13:23:38 +0000 Subject: [PATCH 10/33] Redirect login page to mock only if we are in dev mode --- .github/workflows/deploy_website_content.yml | 1 + .../cpt-ui/__tests__/AuthDemoPage.test.tsx | 2 +- packages/cpt-ui/app/login/page.tsx | 42 +++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy_website_content.yml b/.github/workflows/deploy_website_content.yml index 774b607a2c..5bbb0096cc 100644 --- a/.github/workflows/deploy_website_content.yml +++ b/.github/workflows/deploy_website_content.yml @@ -78,6 +78,7 @@ jobs: export NEXT_PUBLIC_redirectSignIn="https://${fullCloudfrontDomain}/site/selectyourrole.html" export NEXT_PUBLIC_redirectSignOut="https://${fullCloudfrontDomain}/site/logout.html" export NEXT_PUBLIC_COMMIT_ID=${{ inputs.COMMIT_ID }} + export NEXT_PUBLIC_TARGET_ENVIRONMENT=${{ inputs.TARGET_ENVIRONMENT }} cd .build make react-build diff --git a/packages/cpt-ui/__tests__/AuthDemoPage.test.tsx b/packages/cpt-ui/__tests__/AuthDemoPage.test.tsx index 11619cedf9..b3052f2913 100644 --- a/packages/cpt-ui/__tests__/AuthDemoPage.test.tsx +++ b/packages/cpt-ui/__tests__/AuthDemoPage.test.tsx @@ -75,7 +75,7 @@ const MockAuthProvider = ({ children }) => { // Since we've referenced AuthContext in the mock provider, we need to re-import it here // after the mock is set up. import { AuthContext } from "../context/AuthProvider"; -import AuthPage from "../app/auth_demo/page"; +import AuthPage from "../app/login/page"; describe("AuthPage", () => { it("renders the page and the main buttons", () => { diff --git a/packages/cpt-ui/app/login/page.tsx b/packages/cpt-ui/app/login/page.tsx index 232c73fcce..8bc7748562 100644 --- a/packages/cpt-ui/app/login/page.tsx +++ b/packages/cpt-ui/app/login/page.tsx @@ -4,9 +4,36 @@ import React, {useContext, useEffect} from "react"; import { Container, Col, Row, Button } from "nhsuk-react-components"; import { AuthContext } from "@/context/AuthProvider"; +const MOCK_AUTH_ALLOWED = [ + "dev", + "int", + "qa", + // "ref", + // "prod" +] + export default function AuthPage() { + const [allowMockAuth, setAllowMockAuth] = React.useState(false); const auth = useContext(AuthContext); + // On page load + useEffect(() => { + console.log("AuthPage loaded. What environment are we in?", process.env.NEXT_PUBLIC_TARGET_ENVIRONMENT) + + // Use secure login by default + const env: string = process.env.NEXT_PUBLIC_TARGET_ENVIRONMENT || "prod"; + + if (MOCK_AUTH_ALLOWED.includes(env)) { + console.log("Mock auth allowed in this environment"); + setAllowMockAuth(true); + } else { + console.log("Sign in with PTL auth"); + setAllowMockAuth(false); + signIn(); + } + + }, [auth]); + useEffect(() => { console.log(auth); }, [auth]) @@ -35,6 +62,21 @@ export default function AuthPage() { console.log("Signed out: ", auth); } + // TODO: This should show a spinner + if (!allowMockAuth) { + return ( +
    + + + +

    Redirecting to CIS2 login page...

    + +
    +
    +
    + ) + } + return (
    From b6367e8c54d33aec605adfd96198072a98802ebc Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Mon, 6 Jan 2025 14:26:04 +0000 Subject: [PATCH 11/33] Create reusable modal component --- packages/cpt-ui/app/login/page.tsx | 44 +++++++------- .../cpt-ui/assets/styles/EpsLogoutModal.scss | 43 ------------- packages/cpt-ui/assets/styles/EpsModal.scss | 42 +++++++++++++ packages/cpt-ui/components/EpsLogoutModal.tsx | 54 +++-------------- packages/cpt-ui/components/EpsModal.tsx | 60 +++++++++++++++++++ .../ui-strings/EpsLogoutModalStrings.ts | 4 +- 6 files changed, 133 insertions(+), 114 deletions(-) delete mode 100644 packages/cpt-ui/assets/styles/EpsLogoutModal.scss create mode 100644 packages/cpt-ui/assets/styles/EpsModal.scss create mode 100644 packages/cpt-ui/components/EpsModal.tsx diff --git a/packages/cpt-ui/app/login/page.tsx b/packages/cpt-ui/app/login/page.tsx index 8bc7748562..b3e78f0012 100644 --- a/packages/cpt-ui/app/login/page.tsx +++ b/packages/cpt-ui/app/login/page.tsx @@ -16,28 +16,6 @@ export default function AuthPage() { const [allowMockAuth, setAllowMockAuth] = React.useState(false); const auth = useContext(AuthContext); - // On page load - useEffect(() => { - console.log("AuthPage loaded. What environment are we in?", process.env.NEXT_PUBLIC_TARGET_ENVIRONMENT) - - // Use secure login by default - const env: string = process.env.NEXT_PUBLIC_TARGET_ENVIRONMENT || "prod"; - - if (MOCK_AUTH_ALLOWED.includes(env)) { - console.log("Mock auth allowed in this environment"); - setAllowMockAuth(true); - } else { - console.log("Sign in with PTL auth"); - setAllowMockAuth(false); - signIn(); - } - - }, [auth]); - - useEffect(() => { - console.log(auth); - }, [auth]) - const mockSignIn = async () => { console.log("Signing in (Mock)", auth); await auth?.cognitoSignIn({ @@ -62,6 +40,28 @@ export default function AuthPage() { console.log("Signed out: ", auth); } + // On page load + useEffect(() => { + console.log("AuthPage loaded. What environment are we in?", process.env.NEXT_PUBLIC_TARGET_ENVIRONMENT) + + // Use secure login by default + const env: string = process.env.NEXT_PUBLIC_TARGET_ENVIRONMENT || "prod"; + + if (MOCK_AUTH_ALLOWED.includes(env)) { + console.log("Mock auth allowed in this environment"); + setAllowMockAuth(true); + } else { + console.log("Sign in with PTL auth"); + setAllowMockAuth(false); + signIn(); + } + + }, [auth, signIn]); + + useEffect(() => { + console.log(auth); + }, [auth]) + // TODO: This should show a spinner if (!allowMockAuth) { return ( diff --git a/packages/cpt-ui/assets/styles/EpsLogoutModal.scss b/packages/cpt-ui/assets/styles/EpsLogoutModal.scss deleted file mode 100644 index 3bae269e8d..0000000000 --- a/packages/cpt-ui/assets/styles/EpsLogoutModal.scss +++ /dev/null @@ -1,43 +0,0 @@ -.EpsLogoutModalOverlay { - position: fixed; - top: 0; - left: 0; - height: 100vh; - width: 100vw; - background-color: rgba(0, 0, 0, 0.5); // darken the background - display: flex; - align-items: center; - justify-content: center; - z-index: 9999; - } - - .EpsLogoutModalContent { - background: #fff; - padding: 2rem; - border-radius: 4px; - position: relative; - max-width: 600px; - width: 90%; - } - - .EpsLogoutModalButton { - width: 40%; - } - - .EpsLogoutModalCloseButton { - position: absolute; - top: 1rem; - right: 1rem; - font-size: 1.5rem; - background: transparent; - border-radius: 4px; - cursor: pointer; // TODO: Does this want to be a normal cursor Or a pointer? - } - - .EpsLogoutModalButtonGroup { - display: flex; - justify-content: center; - gap: 20%; - margin-top: 2rem; - } - diff --git a/packages/cpt-ui/assets/styles/EpsModal.scss b/packages/cpt-ui/assets/styles/EpsModal.scss new file mode 100644 index 0000000000..1b5390e027 --- /dev/null +++ b/packages/cpt-ui/assets/styles/EpsModal.scss @@ -0,0 +1,42 @@ +.eps-modal-overlay { + position: fixed; + top: 0; + left: 0; + height: 100vh; + width: 100vw; + background-color: rgba(0, 0, 0, 0.5); // darken the background + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} + +.eps-modal-content { + background: #fff; + padding: 2rem; + border-radius: 4px; + position: relative; + max-width: 700px; + width: 90%; +} + +.eps-modal-button { + width: 40%; +} + +.eps-modal-close-button { + position: absolute; + top: 1rem; + right: 1rem; + font-size: 1.5rem; + background: transparent; + border-radius: 4px; + cursor: pointer; // TODO: Does this want to be a normal cursor Or a pointer? +} + +.eps-modal-button-group { + display: flex; + justify-content: center; + gap: 20%; + margin-top: 2rem; +} diff --git a/packages/cpt-ui/components/EpsLogoutModal.tsx b/packages/cpt-ui/components/EpsLogoutModal.tsx index 9f289b6c7a..96433163b8 100644 --- a/packages/cpt-ui/components/EpsLogoutModal.tsx +++ b/packages/cpt-ui/components/EpsLogoutModal.tsx @@ -1,8 +1,8 @@ "use client"; -import React, { useEffect } from "react"; +import React from "react"; import { Button } from "nhsuk-react-components" -import "@/assets/styles/EpsLogoutModal.scss"; +import { EpsModal } from "@/components/EpsModal"; import { EpsLogoutModalStrings } from "@/constants/ui-strings/EpsLogoutModalStrings"; interface EpsLogoutModalProps { @@ -12,68 +12,28 @@ interface EpsLogoutModalProps { } export function EpsLogoutModal({ isOpen, onClose, onConfirm }: EpsLogoutModalProps) { - // Close modal on `Escape` key - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Escape") { - onClose(); - } - }; - - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [onClose]); - - // Close if user clicks outside the modal content - const handleBackdropClick = (e: React.MouseEvent) => { - if (e.target === e.currentTarget) { - onClose(); - } - }; - - // Close if user activates on the background - const handleBackdropActivate = (e: React.KeyboardEvent) => { - if (e.key === "Enter" || e.key === " ") { - onClose(); - } - }; - - // If the modal isn’t open, don’t render anything - if (!isOpen) return null; return ( -
    -
    - - +

    {EpsLogoutModalStrings.title}

    {EpsLogoutModalStrings.caption}

    {/* TODO: style appropriately */} -
    +
    -
    -
    + ); } diff --git a/packages/cpt-ui/components/EpsModal.tsx b/packages/cpt-ui/components/EpsModal.tsx new file mode 100644 index 0000000000..869ea07d85 --- /dev/null +++ b/packages/cpt-ui/components/EpsModal.tsx @@ -0,0 +1,60 @@ +"use client"; +import React, { useEffect } from "react"; + +import "@/assets/styles/EpsModal.scss"; + +interface EpsModalProps { + children: React.ReactNode; + isOpen: boolean; + onClose: () => void; +} + +export function EpsModal({ children, isOpen, onClose }: EpsModalProps) { + // Close modal on `Escape` key + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + onClose(); + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [onClose]); + + // Close if user clicks outside the modal content + const handleBackdropClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + onClose(); + } + }; + + // Close if user activates on the background + const handleBackdropActivate = (e: React.KeyboardEvent) => { + if (e.key === "Enter" || e.key === " ") { + onClose(); + } + }; + + // If the modal isn’t open, don’t render anything + if (!isOpen) return null; + + return ( +
    +
    + + + {children} + +
    +
    + ) +} diff --git a/packages/cpt-ui/constants/ui-strings/EpsLogoutModalStrings.ts b/packages/cpt-ui/constants/ui-strings/EpsLogoutModalStrings.ts index dd5cd2e9ec..189b0ec753 100644 --- a/packages/cpt-ui/constants/ui-strings/EpsLogoutModalStrings.ts +++ b/packages/cpt-ui/constants/ui-strings/EpsLogoutModalStrings.ts @@ -1,6 +1,6 @@ export const EpsLogoutModalStrings = { title: "Are you sure you want to log out?", caption: "Logging out will end your session.", - confirmButtonText: "Yes", - cancelButtonText: "No" + confirmButtonText: "Log out", + cancelButtonText: "Cancel" } From 67606374ab9e1c73cccc5b00e98d9bd886588626 Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Mon, 6 Jan 2025 15:30:58 +0000 Subject: [PATCH 12/33] Remove unnecessary lambda --- ...eps-prescription-tracker-ui.code-workspace | 5 - packages/cpt-ui/app/logout/page.tsx | 1 - packages/cpt-ui/jest.config.ts | 6 +- packages/logoutLambda/.vscode/launch.json | 32 ----- packages/logoutLambda/.vscode/settings.json | 7 -- packages/logoutLambda/package-lock.json | 48 -------- packages/logoutLambda/package.json | 19 --- packages/logoutLambda/src/index.tsx | 76 ------------ packages/logoutLambda/tsconfig.json | 111 ------------------ 9 files changed, 1 insertion(+), 304 deletions(-) delete mode 100644 packages/logoutLambda/.vscode/launch.json delete mode 100644 packages/logoutLambda/.vscode/settings.json delete mode 100644 packages/logoutLambda/package-lock.json delete mode 100644 packages/logoutLambda/package.json delete mode 100644 packages/logoutLambda/src/index.tsx delete mode 100644 packages/logoutLambda/tsconfig.json diff --git a/.vscode/eps-prescription-tracker-ui.code-workspace b/.vscode/eps-prescription-tracker-ui.code-workspace index 5886f2eb00..e2eaccdc86 100644 --- a/.vscode/eps-prescription-tracker-ui.code-workspace +++ b/.vscode/eps-prescription-tracker-ui.code-workspace @@ -43,12 +43,7 @@ { "name": "packages/trackerUserInfoLambda", "path": "../packages/trackerUserInfoLambda" - }, - { - "name": "packages/logoutLambda", - "path": "../packages/logoutLambda" } - ], "settings": { "jest.disabledWorkspaceFolders": [ diff --git a/packages/cpt-ui/app/logout/page.tsx b/packages/cpt-ui/app/logout/page.tsx index b05ce3ebb4..89158520a5 100644 --- a/packages/cpt-ui/app/logout/page.tsx +++ b/packages/cpt-ui/app/logout/page.tsx @@ -41,7 +41,6 @@ export default function LogoutPage() {

    Logout successful

    You are now logged out of the service. To continue using the application, you must log in again.
    diff --git a/packages/cpt-ui/jest.config.ts b/packages/cpt-ui/jest.config.ts index 505a4a0be5..3c19569205 100644 --- a/packages/cpt-ui/jest.config.ts +++ b/packages/cpt-ui/jest.config.ts @@ -12,11 +12,7 @@ const customJestConfig = { moduleDirectories: ["node_modules", "/"], testEnvironment: "jest-environment-jsdom", moduleNameMapper: { - "^@/context/(.*)$": "/context/$1", - "^@/app/(.*)$": "/app/$1", - "^@/constants/(.*)$": "/constants/$1", - "^@/assets/(.*)$": "/assets/$1", - "^@/components/(.*)$": "/components/$1" + "^@/(.*)$": "/$1" } } diff --git a/packages/logoutLambda/.vscode/launch.json b/packages/logoutLambda/.vscode/launch.json deleted file mode 100644 index 5d1ee3c94c..0000000000 --- a/packages/logoutLambda/.vscode/launch.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "name": "vscode-jest-tests.v2", - "request": "launch", - "args": [ - "--runInBand", - "--watchAll=false", - "--testNamePattern", - "${jest.testNamePattern}", - "--runTestsByPath", - "${jest.testFile}", - "--config", - "${workspaceFolder}/jest.debug.config.ts" - ], - "cwd": "${workspaceFolder}", - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "disableOptimisticBPs": true, - "program": "${workspaceFolder}/../../node_modules/.bin/jest", - "windows": { - "program": "${workspaceFolder}/node_modules/jest/bin/jest" - }, - "env": { - "POWERTOOLS_DEV": true, - "NODE_OPTIONS": "--experimental-vm-modules" - } - } - ] -} diff --git a/packages/logoutLambda/.vscode/settings.json b/packages/logoutLambda/.vscode/settings.json deleted file mode 100644 index 657fd71293..0000000000 --- a/packages/logoutLambda/.vscode/settings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "jest.jestCommandLine": "/workspaces/eps-prescription-tracker-ui/node_modules/.bin/jest --no-cache", - "jest.nodeEnv": { - "POWERTOOLS_DEV": true, - "NODE_OPTIONS": "--experimental-vm-modules" - } - } diff --git a/packages/logoutLambda/package-lock.json b/packages/logoutLambda/package-lock.json deleted file mode 100644 index 3014086918..0000000000 --- a/packages/logoutLambda/package-lock.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "logoutlambda", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "logoutlambda", - "version": "1.0.0", - "license": "MIT", - "devDependencies": { - "@types/node": "^22.10.3", - "typescript": "^5.7.2" - } - }, - "node_modules/@types/node": { - "version": "22.10.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.3.tgz", - "integrity": "sha512-DifAyw4BkrufCILvD3ucnuN8eydUfc/C1GlyrnI+LK6543w5/L3VeVgf05o3B4fqSXP1dKYLOZsKfutpxPzZrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - } - } -} diff --git a/packages/logoutLambda/package.json b/packages/logoutLambda/package.json deleted file mode 100644 index 2f855f1596..0000000000 --- a/packages/logoutLambda/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "logoutlambda", - "version": "1.0.0", - "description": "Handles serverside token revokation for cpt-ui", - "main": "index.js", - "author": "NHS Digital", - "license": "MIT", - "scripts": { - "unit": "POWERTOOLS_DEV=true NODE_OPTIONS=--experimental-vm-modules jest --no-cache --coverage", - "lint": "eslint --max-warnings 0 --fix --config ../../eslint.config.mjs .", - "compile": "tsc", - "test": "npm run compile && npm run unit", - "check-licenses": "license-checker --failOn GPL --failOn LGPL --start ../.." - }, - "devDependencies": { - "@types/node": "^22.10.3", - "typescript": "^5.7.2" - } -} diff --git a/packages/logoutLambda/src/index.tsx b/packages/logoutLambda/src/index.tsx deleted file mode 100644 index a0ec588696..0000000000 --- a/packages/logoutLambda/src/index.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda" -import { Logger } from "@aws-lambda-powertools/logger" -import { injectLambdaContext } from "@aws-lambda-powertools/logger/middleware" -import middy from "@middy/core" -import inputOutputLogger from "@middy/input-output-logger" -import { MiddyErrorHandler } from "@cpt-ui-common/middyErrorHandler" - -import { DynamoDBClient } from "@aws-sdk/client-dynamodb" -import { DynamoDBDocumentClient, DeleteCommand } from "@aws-sdk/lib-dynamodb" - -const logger = new Logger({ serviceName: "logout" }) - -const TokenMappingTableName = process.env["TokenMappingTableName"] as string - -// Initialize DynamoDB clients -const dynamoClient = new DynamoDBClient({}) -const documentClient = DynamoDBDocumentClient.from(dynamoClient) - -const errorResponseBody = { - message: "A system error has occurred" -} - -const middyErrorHandler = new MiddyErrorHandler(errorResponseBody) - -/** - * The core Lambda handler (logoutHandler): - * Parses the username from the request. - * Deletes the record in DynamoDB where the partition key is 'username'. - */ -const logoutHandler = async ( - event: APIGatewayProxyEvent -): Promise => { - logger.appendKeys({ - "apigw-request-id": event.requestContext?.requestId - }) - - if (!event.body) { - throw new Error("Missing request body") - } - - // Parse username from the request (user will have to be authorized to reach this point) - const username = event.requestContext.authorizer?.claims["cognito:username"] - if (!username) { - throw new Error("username is required in the request body") - } - - logger.debug("Attempting to delete user token record", { username }) - - // Build and send DeleteCommand - await documentClient.send( - new DeleteCommand({ - TableName: TokenMappingTableName, - Key: { - username - } - }) - ) - - return { - statusCode: 200, - body: JSON.stringify({ - message: `Token record for user '${username}' has been deleted.`, - }), - } -} - -export const handler = middy(logoutHandler) - .use(injectLambdaContext(logger, { clearState: true })) - .use( - inputOutputLogger({ - logger: (request) => { - logger.info(request) - } - }) - ) - .use(middyErrorHandler.errorHandler({ logger })) diff --git a/packages/logoutLambda/tsconfig.json b/packages/logoutLambda/tsconfig.json deleted file mode 100644 index c9c555d96f..0000000000 --- a/packages/logoutLambda/tsconfig.json +++ /dev/null @@ -1,111 +0,0 @@ -{ - "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ - - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - - /* Language and Environment */ - "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - - /* Modules */ - "module": "commonjs", /* Specify what module code is generated. */ - // "rootDir": "./", /* Specify the root folder within your source files. */ - // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ - // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ - // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ - // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ - // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ - // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ - // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - // "outDir": "./", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ - // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ - - /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } -} From 9c329b66087d8be2019fc727ae983bd0be213b36 Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Mon, 6 Jan 2025 16:37:38 +0000 Subject: [PATCH 13/33] Make API backend work with non-standard base paths --- packages/cpt-ui/next.config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/cpt-ui/next.config.js b/packages/cpt-ui/next.config.js index e69e90425f..84458f223a 100644 --- a/packages/cpt-ui/next.config.js +++ b/packages/cpt-ui/next.config.js @@ -13,7 +13,8 @@ const nextConfig = { return [ { source: "/api/:path*", - destination: `${process.env.API_DOMAIN_OVERRIDE}/api/:path*` + destination: `${process.env.API_DOMAIN_OVERRIDE}/api/:path*`, + basePath: false } ] } From 85d31bbcfba3d2f89ea5854546bd36031eca6f4a Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Mon, 6 Jan 2025 17:15:50 +0000 Subject: [PATCH 14/33] Create a spinner component --- packages/cpt-ui/app/login/page.tsx | 23 ++-- packages/cpt-ui/app/logout/page.tsx | 7 +- packages/cpt-ui/app/selectyourrole/page.tsx | 9 +- packages/cpt-ui/components/EpsSpinner.tsx | 100 ++++++++++++++++++ .../constants/ui-strings/CardStrings.ts | 3 +- .../constants/ui-strings/EpsSpinnerStrings.ts | 3 + 6 files changed, 125 insertions(+), 20 deletions(-) create mode 100644 packages/cpt-ui/components/EpsSpinner.tsx create mode 100644 packages/cpt-ui/constants/ui-strings/EpsSpinnerStrings.ts diff --git a/packages/cpt-ui/app/login/page.tsx b/packages/cpt-ui/app/login/page.tsx index b3e78f0012..bb799e7aa7 100644 --- a/packages/cpt-ui/app/login/page.tsx +++ b/packages/cpt-ui/app/login/page.tsx @@ -1,8 +1,9 @@ 'use client' -import React, {useContext, useEffect} from "react"; +import React, { useContext, useEffect, useCallback } from "react"; import { Container, Col, Row, Button } from "nhsuk-react-components"; import { AuthContext } from "@/context/AuthProvider"; +import EpsSpinner from "@/components/EpsSpinner"; const MOCK_AUTH_ALLOWED = [ "dev", @@ -13,9 +14,11 @@ const MOCK_AUTH_ALLOWED = [ ] export default function AuthPage() { - const [allowMockAuth, setAllowMockAuth] = React.useState(false); const auth = useContext(AuthContext); + // Use secure login by default + const env: string = process.env.NEXT_PUBLIC_TARGET_ENVIRONMENT || "prod"; + const mockSignIn = async () => { console.log("Signing in (Mock)", auth); await auth?.cognitoSignIn({ @@ -25,14 +28,14 @@ export default function AuthPage() { }); } - const signIn = async () => { + const signIn = useCallback(async () => { console.log("Signing in (Primary)", auth); await auth?.cognitoSignIn({ provider: { custom: "Primary" } }); - } + }, [auth]); const signOut = async () => { console.log("Signing out", auth); @@ -44,15 +47,10 @@ export default function AuthPage() { useEffect(() => { console.log("AuthPage loaded. What environment are we in?", process.env.NEXT_PUBLIC_TARGET_ENVIRONMENT) - // Use secure login by default - const env: string = process.env.NEXT_PUBLIC_TARGET_ENVIRONMENT || "prod"; - if (MOCK_AUTH_ALLOWED.includes(env)) { - console.log("Mock auth allowed in this environment"); - setAllowMockAuth(true); + console.log("Mock auth is allowed in this environment"); } else { - console.log("Sign in with PTL auth"); - setAllowMockAuth(false); + console.log("User must sign in with Primary auth"); signIn(); } @@ -63,13 +61,14 @@ export default function AuthPage() { }, [auth]) // TODO: This should show a spinner - if (!allowMockAuth) { + if (!MOCK_AUTH_ALLOWED.includes(env)) { return (

    Redirecting to CIS2 login page...

    +
    diff --git a/packages/cpt-ui/app/logout/page.tsx b/packages/cpt-ui/app/logout/page.tsx index 89158520a5..cac9c046bb 100644 --- a/packages/cpt-ui/app/logout/page.tsx +++ b/packages/cpt-ui/app/logout/page.tsx @@ -1,9 +1,10 @@ 'use client' import React, { useContext, useEffect } from "react"; -import { Container } from "nhsuk-react-components"; +import { Container } from "nhsuk-react-components" import Link from "next/link"; import { AuthContext } from "@/context/AuthProvider"; +import EpsSpinner from "@/components/EpsSpinner"; export default function LogoutPage() { @@ -34,9 +35,9 @@ export default function LogoutPage() { {auth?.isSignedIn ? ( <>

    Logging out

    -
    Spinny Spinner...
    + - ) : ( + ) : ( <>

    Logout successful

    You are now logged out of the service. To continue using the application, you must log in again.
    diff --git a/packages/cpt-ui/app/selectyourrole/page.tsx b/packages/cpt-ui/app/selectyourrole/page.tsx index e835bbf000..75324527bc 100644 --- a/packages/cpt-ui/app/selectyourrole/page.tsx +++ b/packages/cpt-ui/app/selectyourrole/page.tsx @@ -1,8 +1,12 @@ 'use client' import React, {useState, useEffect, useContext, useCallback } from "react" import { Container, Col, Row, Details, Table, ErrorSummary, Button, InsetText } from "nhsuk-react-components" + import { AuthContext } from "@/context/AuthProvider"; + import EpsCard, { EpsCardProps } from "@/components/EpsCard"; +import EpsSpinner from "@/components/EpsSpinner"; + import {SELECT_YOUR_ROLE_PAGE_TEXT} from "@/constants/ui-strings/CardStrings"; export type RoleDetails = { @@ -49,8 +53,7 @@ const { noODSCode, noRoleName, noAddress, - errorDuringRoleSelection, - loadingMessage + errorDuringRoleSelection } = SELECT_YOUR_ROLE_PAGE_TEXT; export default function SelectYourRolePage() { @@ -156,7 +159,7 @@ export default function SelectYourRolePage() { - {loadingMessage} + diff --git a/packages/cpt-ui/components/EpsSpinner.tsx b/packages/cpt-ui/components/EpsSpinner.tsx new file mode 100644 index 0000000000..07a33037bd --- /dev/null +++ b/packages/cpt-ui/components/EpsSpinner.tsx @@ -0,0 +1,100 @@ +'use client'; +import React from 'react'; + +import { EpsSpinnerStrings } from '@/constants/ui-strings/EpsSpinnerStrings'; + + +function Spinner({ + radius = 100, + thickness = 12, + fraction = 0.2, // The fraction of the hoop that is green + speed = 1 // The speed (in seconds) for one full rotation +}) { + const circumference = 2 * Math.PI * radius; + + // The portion that should appear green is defined by "fraction" + // If fraction = 0.25, then 25% of the hoop is green and 75% is grey + const offset = circumference * (1 - fraction); + + return ( +
    +
    + + {/* Grey base circle (non-spinning) */} + + + {/* Spinning arc group */} + + + + + {/* "Loading..." text in the center */} + + {EpsSpinnerStrings.loading} + + + + {/* Inline keyframes for the spin animation */} + +
    +
    + ); +} + +export default Spinner; diff --git a/packages/cpt-ui/constants/ui-strings/CardStrings.ts b/packages/cpt-ui/constants/ui-strings/CardStrings.ts index 922e3a2307..02b9ee2236 100644 --- a/packages/cpt-ui/constants/ui-strings/CardStrings.ts +++ b/packages/cpt-ui/constants/ui-strings/CardStrings.ts @@ -19,6 +19,5 @@ export const SELECT_YOUR_ROLE_PAGE_TEXT = { noODSCode: "No ODS code", noRoleName: "No role name", noAddress: "Address not found", - errorDuringRoleSelection: "Error during role selection", - loadingMessage: "Loading..." + errorDuringRoleSelection: "Error during role selection" } diff --git a/packages/cpt-ui/constants/ui-strings/EpsSpinnerStrings.ts b/packages/cpt-ui/constants/ui-strings/EpsSpinnerStrings.ts new file mode 100644 index 0000000000..98e05ea588 --- /dev/null +++ b/packages/cpt-ui/constants/ui-strings/EpsSpinnerStrings.ts @@ -0,0 +1,3 @@ +export const EpsSpinnerStrings = { + loading: "Loading..." +} From 6ec8660426a5e0b3f16f515ed42a7635b1ca5474 Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Mon, 6 Jan 2025 17:21:56 +0000 Subject: [PATCH 15/33] Address linting problems --- packages/cpt-ui/__tests__/SelectYourRolePage.test.tsx | 3 ++- packages/cpt-ui/app/login/page.tsx | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/cpt-ui/__tests__/SelectYourRolePage.test.tsx b/packages/cpt-ui/__tests__/SelectYourRolePage.test.tsx index 9c264861b7..5f6ee97a5d 100644 --- a/packages/cpt-ui/__tests__/SelectYourRolePage.test.tsx +++ b/packages/cpt-ui/__tests__/SelectYourRolePage.test.tsx @@ -69,6 +69,7 @@ const renderWithAuth = (authOverrides = {}) => { }; import { SELECT_YOUR_ROLE_PAGE_TEXT } from "@/constants/ui-strings/CardStrings"; +import { EpsSpinnerStrings } from "../constants/ui-strings/EpsSpinnerStrings"; describe("SelectYourRolePage", () => { // Clear all mock calls before each test to avoid state leaks @@ -84,7 +85,7 @@ describe("SelectYourRolePage", () => { renderWithAuth({ isSignedIn: true, idToken: "mock-id-token" }); // Verify that the loading text appears - const loadingText = screen.getByText(SELECT_YOUR_ROLE_PAGE_TEXT.loadingMessage); + const loadingText = screen.getByText(EpsSpinnerStrings.loading); expect(loadingText).toBeInTheDocument(); }); diff --git a/packages/cpt-ui/app/login/page.tsx b/packages/cpt-ui/app/login/page.tsx index bb799e7aa7..686e7ad7b1 100644 --- a/packages/cpt-ui/app/login/page.tsx +++ b/packages/cpt-ui/app/login/page.tsx @@ -17,7 +17,7 @@ export default function AuthPage() { const auth = useContext(AuthContext); // Use secure login by default - const env: string = process.env.NEXT_PUBLIC_TARGET_ENVIRONMENT || "prod"; + const target_environment: string = process.env.NEXT_PUBLIC_TARGET_ENVIRONMENT || "prod"; const mockSignIn = async () => { console.log("Signing in (Mock)", auth); @@ -47,21 +47,21 @@ export default function AuthPage() { useEffect(() => { console.log("AuthPage loaded. What environment are we in?", process.env.NEXT_PUBLIC_TARGET_ENVIRONMENT) - if (MOCK_AUTH_ALLOWED.includes(env)) { + if (MOCK_AUTH_ALLOWED.includes(target_environment)) { console.log("Mock auth is allowed in this environment"); } else { console.log("User must sign in with Primary auth"); signIn(); } - }, [auth, signIn]); + }, [auth, signIn, target_environment]); useEffect(() => { console.log(auth); }, [auth]) // TODO: This should show a spinner - if (!MOCK_AUTH_ALLOWED.includes(env)) { + if (!MOCK_AUTH_ALLOWED.includes(target_environment)) { return (
    From 2373a3d7745fc6779c005cf63dc00a50039a2c33 Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Tue, 7 Jan 2025 00:05:22 +0000 Subject: [PATCH 16/33] Fix infinite loop. --- .../cpt-ui/__tests__/AuthDemoPage.test.tsx | 17 +++++++++++++++ packages/cpt-ui/app/login/page.tsx | 21 ++++++++++--------- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/packages/cpt-ui/__tests__/AuthDemoPage.test.tsx b/packages/cpt-ui/__tests__/AuthDemoPage.test.tsx index b3052f2913..a97d5467fb 100644 --- a/packages/cpt-ui/__tests__/AuthDemoPage.test.tsx +++ b/packages/cpt-ui/__tests__/AuthDemoPage.test.tsx @@ -78,6 +78,10 @@ import { AuthContext } from "../context/AuthProvider"; import AuthPage from "../app/login/page"; describe("AuthPage", () => { + beforeEach(() => { + process.env.NEXT_PUBLIC_TARGET_ENVIRONMENT = "dev"; + }); + it("renders the page and the main buttons", () => { const { container } = render( @@ -179,4 +183,17 @@ describe("AuthPage", () => { screen.getByText((content) => content.includes('"isSignedIn": false')) ).toBeInTheDocument(); }); + + it("shows a spinner when not in a mock auth environment", () => { + process.env.NEXT_PUBLIC_TARGET_ENVIRONMENT = "prod"; + + render( + + + + ); + + const spinner = screen.getByRole("heading", { name: /Redirecting to CIS2 login page.../i }); + expect(spinner).toBeInTheDocument(); + }); }); diff --git a/packages/cpt-ui/app/login/page.tsx b/packages/cpt-ui/app/login/page.tsx index 686e7ad7b1..75a35fd94b 100644 --- a/packages/cpt-ui/app/login/page.tsx +++ b/packages/cpt-ui/app/login/page.tsx @@ -35,6 +35,7 @@ export default function AuthPage() { custom: "Primary" } }); + console.log("Signed in: ", auth); }, [auth]); const signOut = async () => { @@ -43,18 +44,18 @@ export default function AuthPage() { console.log("Signed out: ", auth); } - // On page load useEffect(() => { - console.log("AuthPage loaded. What environment are we in?", process.env.NEXT_PUBLIC_TARGET_ENVIRONMENT) - - if (MOCK_AUTH_ALLOWED.includes(target_environment)) { - console.log("Mock auth is allowed in this environment"); - } else { - console.log("User must sign in with Primary auth"); - signIn(); + console.log( + "Login page loaded. What environment are we in?", + target_environment + ); + + // Only call signIn() if user is *not* in a mock environment AND *not* signed in yet. + if (!MOCK_AUTH_ALLOWED.includes(target_environment) && !auth?.isSignedIn) { + console.log("User must sign in with Primary auth"); + signIn(); } - - }, [auth, signIn, target_environment]); + }, [auth?.isSignedIn, signIn, target_environment]); useEffect(() => { console.log(auth); From a5fbbdd36f14092173f369f75ac4c82822489f4a Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Tue, 7 Jan 2025 09:41:03 +0000 Subject: [PATCH 17/33] Added a missing environment to allowed mock auth --- packages/cpt-ui/app/login/page.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/cpt-ui/app/login/page.tsx b/packages/cpt-ui/app/login/page.tsx index 75a35fd94b..87419b6f50 100644 --- a/packages/cpt-ui/app/login/page.tsx +++ b/packages/cpt-ui/app/login/page.tsx @@ -7,6 +7,7 @@ import EpsSpinner from "@/components/EpsSpinner"; const MOCK_AUTH_ALLOWED = [ "dev", + "dev-pr", "int", "qa", // "ref", From 99da5e7ab575ad8538002e88bd3b260cb63a027d Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Tue, 7 Jan 2025 09:48:25 +0000 Subject: [PATCH 18/33] Fix indentation --- packages/cpt-ui/components/EpsSpinner.tsx | 110 +++++++++++----------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/packages/cpt-ui/components/EpsSpinner.tsx b/packages/cpt-ui/components/EpsSpinner.tsx index 07a33037bd..5e3b323339 100644 --- a/packages/cpt-ui/components/EpsSpinner.tsx +++ b/packages/cpt-ui/components/EpsSpinner.tsx @@ -22,77 +22,77 @@ function Spinner({ alignItems: 'center', justifyContent: 'center' }}> -
    - - {/* Grey base circle (non-spinning) */} - - - {/* Spinning arc group */} - + {/* Grey base circle (non-spinning) */} - - {/* "Loading..." text in the center */} - - {EpsSpinnerStrings.loading} - - + {/* Spinning arc group */} + + + - {/* Inline keyframes for the spin animation */} - -
    + `} + +
    ); } From 1dcfe68f2eb4c6c411604714769103c81654f406 Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Tue, 7 Jan 2025 12:36:10 +0000 Subject: [PATCH 19/33] Create some unit tests --- ...thDemoPage.test.tsx => LoginPage.test.tsx} | 6 +- packages/cpt-ui/__tests__/LogoutPage.test.tsx | 172 ++++++++++++++++++ packages/cpt-ui/app/logout/page.tsx | 2 +- packages/cpt-ui/components/EpsSpinner.tsx | 7 +- 4 files changed, 181 insertions(+), 6 deletions(-) rename packages/cpt-ui/__tests__/{AuthDemoPage.test.tsx => LoginPage.test.tsx} (97%) create mode 100644 packages/cpt-ui/__tests__/LogoutPage.test.tsx diff --git a/packages/cpt-ui/__tests__/AuthDemoPage.test.tsx b/packages/cpt-ui/__tests__/LoginPage.test.tsx similarity index 97% rename from packages/cpt-ui/__tests__/AuthDemoPage.test.tsx rename to packages/cpt-ui/__tests__/LoginPage.test.tsx index a97d5467fb..6497f9b426 100644 --- a/packages/cpt-ui/__tests__/AuthDemoPage.test.tsx +++ b/packages/cpt-ui/__tests__/LoginPage.test.tsx @@ -5,7 +5,7 @@ import userEvent from "@testing-library/user-event"; import React, { useState } from "react"; // Mock the configureAmplify module -jest.mock("../context/configureAmplify", () => ({ +jest.mock("@/context/configureAmplify", () => ({ __esModule: true, authConfig: { Auth: { @@ -74,8 +74,8 @@ const MockAuthProvider = ({ children }) => { // Since we've referenced AuthContext in the mock provider, we need to re-import it here // after the mock is set up. -import { AuthContext } from "../context/AuthProvider"; -import AuthPage from "../app/login/page"; +import { AuthContext } from "@/context/AuthProvider"; +import AuthPage from "@/app/login/page"; describe("AuthPage", () => { beforeEach(() => { diff --git a/packages/cpt-ui/__tests__/LogoutPage.test.tsx b/packages/cpt-ui/__tests__/LogoutPage.test.tsx new file mode 100644 index 0000000000..ec7039b48a --- /dev/null +++ b/packages/cpt-ui/__tests__/LogoutPage.test.tsx @@ -0,0 +1,172 @@ +// @ts-nocheck +import "@testing-library/jest-dom"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import React, { useState } from "react"; + +// Mock the configureAmplify module +jest.mock("@/context/configureAmplify", () => ({ + __esModule: true, + authConfig: { + Auth: { + Cognito: { + userPoolClientId: "mockUserPoolClientId", + userPoolId: "mockUserPoolId", + loginWith: { + oauth: { + domain: "mockHostedLoginDomain", + scopes: ["openid", "email", "phone", "profile", "aws.cognito.signin.user.admin"], + redirectSignIn: ["mockRedirectSignIn"], + redirectSignOut: ["mockRedirectSignOut"], + responseType: "code", + }, + username: true, + email: false, + phone: false, + }, + }, + }, + }, +})); + +// Create a mock AuthContext provider that allows us to control the state +const mockCognitoSignIn = jest.fn(); +const mockCognitoSignOut = jest.fn(); + +interface MockAuthProviderProps { + children: React.ReactNode; + defaultIsSignedIn?: boolean; + defaultUser?: { username: string } | null; + } + + const MockAuthProvider: React.FC = ({ + children, + defaultIsSignedIn = true, + defaultUser = { username: "mockUser" }, + }) => { + // State to simulate auth changes + const [authState, setAuthState] = useState({ + isSignedIn: defaultIsSignedIn, + user: defaultUser, + error: null as string | null, + idToken: defaultIsSignedIn ? "mockIdToken" : null, + accessToken: defaultIsSignedIn ? "mockAccessToken" : null, + cognitoSignIn: async (options: { provider: { custom: any } }) => { + mockCognitoSignIn(options); + // Simulate a sign-in update + setAuthState((prev) => ({ + ...prev, + isSignedIn: true, + user: { username: options?.provider?.custom || "mockUser" }, + error: null, + idToken: "mockIdToken", + accessToken: "mockAccessToken", + })); + }, + cognitoSignOut: async () => { + mockCognitoSignOut(); + // Simulate a sign-out update + setAuthState((prev) => ({ + ...prev, + isSignedIn: false, + user: null, + error: null, + idToken: null, + accessToken: null, + })); + }, + }); + + return ( + {children} + ); + }; + +// Since we've referenced AuthContext in the mock provider, we need to re-import it here +// after the mock is set up. +import { AuthContext } from "@/context/AuthProvider"; +import LogoutPage from "@/app/logout/page"; + +describe("LogoutPage", () => { + // Use fake timers to control the setTimeout in LogoutPage + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + jest.clearAllMocks(); + }); + + it("renders 'Logout successful' immediately if the user is not signed in", () => { + render( + + + + ); + + // The user is not signed in, so we expect to see "Logout successful". + expect(screen.getByText(/Logout successful/i)).toBeInTheDocument(); + expect( + screen.getByText( + /You are now logged out of the service. To continue using the application, you must log in again/i + ) + ).toBeInTheDocument(); + + // We also expect to see the "Log in" link or button + expect(screen.getByRole("link", { name: /log in/i })).toBeInTheDocument(); + + // Because user is not signed in, we do NOT expect signOut to have been called + expect(mockCognitoSignOut).not.toHaveBeenCalled(); + }); + + it("shows a spinner and calls signOut when the user is signed in", async () => { + render( + + + + ); + + // Because the user is signed in, we expect "Logging out" and spinner + expect(screen.getByText(/Logging out/i)).toBeInTheDocument(); + + // The spinner is also expected to be in the document + // (assuming EpsSpinner might not have a particular role, but let's see if there's text or alt) + // If your EpsSpinner has a testable role/label, update the query accordingly + expect(screen.getByRole("progressbar")).toBeInTheDocument(); + + // signOut is delayed by 3s. Fast forward the timers so the logout can complete. + jest.advanceTimersByTime(3000); + + // Wait for re-render after signOut + await waitFor(() => { + expect(mockCognitoSignOut).toHaveBeenCalledTimes(1); + }); + + // After signOut, the user is no longer signed in, so we should see "Logout successful" + expect(screen.getByText(/Logout successful/i)).toBeInTheDocument(); + expect( + screen.getByText( + /You are now logged out of the service. To continue using the application, you must log in again/i + ) + ).toBeInTheDocument(); + }); + + it("does not call signOut if user is signed in, but we haven't advanced timers yet", () => { + render( + + + + ); + + // On initial render, user is signed in + // The call is triggered, but only after the 3s setTimeout. + // We haven't advanced timers, so the signOut shouldn't have completed yet. + expect(screen.getByText(/Logging out/i)).toBeInTheDocument(); + expect(mockCognitoSignOut).not.toHaveBeenCalled(); + }); + }); diff --git a/packages/cpt-ui/app/logout/page.tsx b/packages/cpt-ui/app/logout/page.tsx index cac9c046bb..bb720d45bc 100644 --- a/packages/cpt-ui/app/logout/page.tsx +++ b/packages/cpt-ui/app/logout/page.tsx @@ -35,7 +35,7 @@ export default function LogoutPage() { {auth?.isSignedIn ? ( <>

    Logging out

    - + ) : ( <> diff --git a/packages/cpt-ui/components/EpsSpinner.tsx b/packages/cpt-ui/components/EpsSpinner.tsx index 5e3b323339..968a0e4f11 100644 --- a/packages/cpt-ui/components/EpsSpinner.tsx +++ b/packages/cpt-ui/components/EpsSpinner.tsx @@ -17,11 +17,14 @@ function Spinner({ const offset = circumference * (1 - fraction); return ( -
    + }} + role='progressbar' + >
    Date: Tue, 7 Jan 2025 12:37:50 +0000 Subject: [PATCH 20/33] Fix indentation --- packages/cpt-ui/__tests__/LogoutPage.test.tsx | 154 +++++++++--------- 1 file changed, 74 insertions(+), 80 deletions(-) diff --git a/packages/cpt-ui/__tests__/LogoutPage.test.tsx b/packages/cpt-ui/__tests__/LogoutPage.test.tsx index ec7039b48a..24b0352f68 100644 --- a/packages/cpt-ui/__tests__/LogoutPage.test.tsx +++ b/packages/cpt-ui/__tests__/LogoutPage.test.tsx @@ -88,85 +88,79 @@ import { AuthContext } from "@/context/AuthProvider"; import LogoutPage from "@/app/logout/page"; describe("LogoutPage", () => { - // Use fake timers to control the setTimeout in LogoutPage - beforeAll(() => { - jest.useFakeTimers(); - }); - - afterAll(() => { - jest.useRealTimers(); - }); - - afterEach(() => { - jest.restoreAllMocks(); - jest.clearAllMocks(); - }); - - it("renders 'Logout successful' immediately if the user is not signed in", () => { - render( - - - - ); - - // The user is not signed in, so we expect to see "Logout successful". - expect(screen.getByText(/Logout successful/i)).toBeInTheDocument(); - expect( - screen.getByText( - /You are now logged out of the service. To continue using the application, you must log in again/i - ) - ).toBeInTheDocument(); - - // We also expect to see the "Log in" link or button - expect(screen.getByRole("link", { name: /log in/i })).toBeInTheDocument(); - - // Because user is not signed in, we do NOT expect signOut to have been called - expect(mockCognitoSignOut).not.toHaveBeenCalled(); - }); - - it("shows a spinner and calls signOut when the user is signed in", async () => { - render( - - - - ); - - // Because the user is signed in, we expect "Logging out" and spinner - expect(screen.getByText(/Logging out/i)).toBeInTheDocument(); - - // The spinner is also expected to be in the document - // (assuming EpsSpinner might not have a particular role, but let's see if there's text or alt) - // If your EpsSpinner has a testable role/label, update the query accordingly - expect(screen.getByRole("progressbar")).toBeInTheDocument(); - - // signOut is delayed by 3s. Fast forward the timers so the logout can complete. - jest.advanceTimersByTime(3000); - - // Wait for re-render after signOut - await waitFor(() => { - expect(mockCognitoSignOut).toHaveBeenCalledTimes(1); - }); - - // After signOut, the user is no longer signed in, so we should see "Logout successful" - expect(screen.getByText(/Logout successful/i)).toBeInTheDocument(); - expect( - screen.getByText( - /You are now logged out of the service. To continue using the application, you must log in again/i - ) - ).toBeInTheDocument(); - }); - - it("does not call signOut if user is signed in, but we haven't advanced timers yet", () => { - render( - - - - ); - - // On initial render, user is signed in - // The call is triggered, but only after the 3s setTimeout. - // We haven't advanced timers, so the signOut shouldn't have completed yet. - expect(screen.getByText(/Logging out/i)).toBeInTheDocument(); - expect(mockCognitoSignOut).not.toHaveBeenCalled(); + // Use fake timers to control the setTimeout in LogoutPage + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + jest.clearAllMocks(); + }); + + it("renders 'Logout successful' immediately if the user is not signed in", () => { + render( + + + + ); + + // The user is not signed in, so we expect to see "Logout successful". + expect(screen.getByText(/Logout successful/i)).toBeInTheDocument(); + expect( + screen.getByText( + /You are now logged out of the service. To continue using the application, you must log in again/i + ) + ).toBeInTheDocument(); + + // We also expect to see the "Log in" link or button + expect(screen.getByRole("link", { name: /log in/i })).toBeInTheDocument(); + + // Because user is not signed in, we do NOT expect signOut to have been called + expect(mockCognitoSignOut).not.toHaveBeenCalled(); + }); + + it("shows a spinner and calls signOut when the user is signed in", async () => { + render( + + + + ); + + // Because the user is signed in, we expect "Logging out" and spinner + expect(screen.getByText(/Logging out/i)).toBeInTheDocument(); + expect(screen.getByRole("progressbar")).toBeInTheDocument(); + // signOut is delayed by 3s (for now). Fast forward the timers so the logout can complete. + jest.advanceTimersByTime(3000); + // Wait for re-render after signOut + await waitFor(() => { + expect(mockCognitoSignOut).toHaveBeenCalledTimes(1); }); + + // After signOut, the user is no longer signed in, so we should see "Logout successful" + expect(screen.getByText(/Logout successful/i)).toBeInTheDocument(); + expect( + screen.getByText( + /You are now logged out of the service. To continue using the application, you must log in again/i + ) + ).toBeInTheDocument(); }); + + it("does not call signOut if user is signed in, but we haven't advanced timers yet", () => { + render( + + + + ); + + // On initial render, user is signed in + // The call is triggered, but only after the 3s setTimeout. + // We haven't advanced timers, so the signOut shouldn't have completed yet. + expect(screen.getByText(/Logging out/i)).toBeInTheDocument(); + expect(mockCognitoSignOut).not.toHaveBeenCalled(); + }); +}); From 66ee609faa08acb99cbbb08dbc05c5c3e78f5a35 Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Tue, 7 Jan 2025 13:19:34 +0000 Subject: [PATCH 21/33] modal unit tests --- packages/cpt-ui/__tests__/EpsModal.test.tsx | 96 +++++++++++++++++++++ packages/cpt-ui/components/EpsModal.tsx | 8 +- 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 packages/cpt-ui/__tests__/EpsModal.test.tsx diff --git a/packages/cpt-ui/__tests__/EpsModal.test.tsx b/packages/cpt-ui/__tests__/EpsModal.test.tsx new file mode 100644 index 0000000000..fb0c099498 --- /dev/null +++ b/packages/cpt-ui/__tests__/EpsModal.test.tsx @@ -0,0 +1,96 @@ +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import { EpsModal } from "@/components/EpsModal"; + +describe("EpsModal", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test("does not render the modal when isOpen is false", () => { + render( + +
    Modal Content
    +
    + ); + // The content should not be in the document + expect(screen.queryByText(/Modal Content/i)).not.toBeInTheDocument(); + }); + + test("renders the modal when isOpen is true", () => { + render( + +
    Modal Content
    +
    + ); + // The content should appear in the document + expect(screen.getByText(/Modal Content/i)).toBeInTheDocument(); + // The dialog container should be visible + expect(screen.getByRole("dialog")).toBeInTheDocument(); + }); + + test("calls onClose when user clicks outside modal content", () => { + const onCloseMock = jest.fn(); + render( + +
    Modal Content
    +
    + ); + + const overlay = screen.getByTestId("eps-modal-overlay"); + const modalContent = screen.getByTestId("modal-content"); + + // Clicking directly on the content should NOT trigger onClose + fireEvent.click(modalContent); + expect(onCloseMock).not.toHaveBeenCalled(); + + // Clicking on the overlay (outside the content) should trigger onClose + fireEvent.click(overlay); + expect(onCloseMock).toHaveBeenCalledTimes(1); + }); + + test("calls onClose when user clicks the close button", () => { + const onCloseMock = jest.fn(); + render( + +
    Modal Content
    +
    + ); + + const closeButton = screen.getByRole("button", { name: /Close modal/i }); + fireEvent.click(closeButton); + expect(onCloseMock).toHaveBeenCalledTimes(1); + }); + + test("calls onClose when user presses Escape", () => { + const onCloseMock = jest.fn(); + render( + +
    Modal Content
    +
    + ); + + // Fire 'Escape' keydown event on window + fireEvent.keyDown(window, { key: "Escape" }); + expect(onCloseMock).toHaveBeenCalledTimes(1); + }); + + test("calls onClose when user presses Enter or Space on the backdrop", () => { + const onCloseMock = jest.fn(); + render( + +
    Modal Content
    +
    + ); + + const overlay = screen.getByTestId("eps-modal-overlay"); + + fireEvent.keyDown(overlay, { key: "Enter" }); + expect(onCloseMock).toHaveBeenCalledTimes(1); + + // Fire again with ' ' + fireEvent.keyDown(overlay, { key: " " }); + expect(onCloseMock).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/cpt-ui/components/EpsModal.tsx b/packages/cpt-ui/components/EpsModal.tsx index 869ea07d85..de40349f10 100644 --- a/packages/cpt-ui/components/EpsModal.tsx +++ b/packages/cpt-ui/components/EpsModal.tsx @@ -46,8 +46,14 @@ export function EpsModal({ children, isOpen, onClose }: EpsModalProps) { role="button" tabIndex={0} onKeyDown={handleBackdropActivate} + data-testid="eps-modal-overlay" > -
    +
    From 4861028315a1d3377c17430501a868562c690df5 Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Tue, 7 Jan 2025 13:19:57 +0000 Subject: [PATCH 22/33] Forgot to save, commit --- packages/cpt-ui/__tests__/EpsModal.test.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/cpt-ui/__tests__/EpsModal.test.tsx b/packages/cpt-ui/__tests__/EpsModal.test.tsx index fb0c099498..06aba8d2a2 100644 --- a/packages/cpt-ui/__tests__/EpsModal.test.tsx +++ b/packages/cpt-ui/__tests__/EpsModal.test.tsx @@ -26,8 +26,6 @@ describe("EpsModal", () => { ); // The content should appear in the document expect(screen.getByText(/Modal Content/i)).toBeInTheDocument(); - // The dialog container should be visible - expect(screen.getByRole("dialog")).toBeInTheDocument(); }); test("calls onClose when user clicks outside modal content", () => { From 4a07fa973b0872fb5899eec77cde02a3eebd9ff4 Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Tue, 7 Jan 2025 14:19:04 +0000 Subject: [PATCH 23/33] Do some cleanup of various small issues --- .devcontainer/devcontainer.json | 37 ++++++++++--------- packages/cdk/resources/Cognito.ts | 2 +- packages/cpt-ui/__tests__/EpsModal.test.tsx | 3 +- packages/cpt-ui/__tests__/LogoutPage.test.tsx | 1 - packages/cpt-ui/app/login/page.tsx | 1 - packages/cpt-ui/app/logout/page.tsx | 5 ++- packages/cpt-ui/assets/styles/EpsModal.scss | 4 ++ packages/cpt-ui/components/EpsHeader.tsx | 4 ++ packages/cpt-ui/components/EpsLogoutModal.tsx | 17 +++++---- packages/cpt-ui/components/EpsModal.tsx | 26 +++++++------ packages/cpt-ui/components/EpsSpinner.tsx | 2 + packages/cpt-ui/context/AuthProvider.tsx | 3 +- 12 files changed, 61 insertions(+), 44 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 3ebed0e0e3..eb04bee686 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -27,24 +27,25 @@ "customizations": { "vscode": { "extensions": [ - "AmazonWebServices.aws-toolkit-vscode", - "redhat.vscode-yaml", - "ms-python.python", - "ms-python.flake8", - "eamodio.gitlens", - "github.vscode-pull-request-github", - "orta.vscode-jest", - "42crunch.vscode-openapi", - "mermade.openapi-lint", - "christian-kohler.npm-intellisense", - "dbaeumer.vscode-eslint", - "lfm.vscode-makefile-term", - "GrapeCity.gc-excelviewer", - "redhat.vscode-xml", - "streetsidesoftware.code-spell-checker", - "timonwong.shellcheck", - "mkhl.direnv", - "github.vscode-github-actions" + "AmazonWebServices.aws-toolkit-vscode", + "redhat.vscode-yaml", + "ms-python.python", + "ms-python.flake8", + "eamodio.gitlens", + "github.vscode-pull-request-github", + "orta.vscode-jest", + "42crunch.vscode-openapi", + "mermade.openapi-lint", + "christian-kohler.npm-intellisense", + "dbaeumer.vscode-eslint", + "lfm.vscode-makefile-term", + "GrapeCity.gc-excelviewer", + "redhat.vscode-xml", + "streetsidesoftware.code-spell-checker", + "timonwong.shellcheck", + "mkhl.direnv", + "github.vscode-github-actions", + "Gruntfuggly.todo-tree" ], "settings": { "python.defaultInterpreterPath": "/workspaces/eps-prescription-tracker-ui/.venv/bin/python", diff --git a/packages/cdk/resources/Cognito.ts b/packages/cdk/resources/Cognito.ts index 7e813d468a..4d44a3b209 100644 --- a/packages/cdk/resources/Cognito.ts +++ b/packages/cdk/resources/Cognito.ts @@ -186,10 +186,10 @@ export class Cognito extends Construct { } const callbackUrls = [ - `https://${props.fullCloudfrontDomain}/site`, `https://${props.fullCloudfrontDomain}/site/selectyourrole`, // FIXME: This is temporary, until we get routing fixed `https://${props.fullCloudfrontDomain}/site/selectyourrole.html`, + // TODO: This is for the proof-of-concept login page, and can probably be deleted soon. `https://${props.fullCloudfrontDomain}/auth_demo`, `https://${props.fullCloudfrontDomain}/oauth2/idpresponse` ] diff --git a/packages/cpt-ui/__tests__/EpsModal.test.tsx b/packages/cpt-ui/__tests__/EpsModal.test.tsx index 06aba8d2a2..5c002ce818 100644 --- a/packages/cpt-ui/__tests__/EpsModal.test.tsx +++ b/packages/cpt-ui/__tests__/EpsModal.test.tsx @@ -53,10 +53,11 @@ describe("EpsModal", () => { render(
    Modal Content
    +
    ); - const closeButton = screen.getByRole("button", { name: /Close modal/i }); + const closeButton = screen.getByText(/TEST CLOSE BUTTON/); fireEvent.click(closeButton); expect(onCloseMock).toHaveBeenCalledTimes(1); }); diff --git a/packages/cpt-ui/__tests__/LogoutPage.test.tsx b/packages/cpt-ui/__tests__/LogoutPage.test.tsx index 24b0352f68..ce84248f61 100644 --- a/packages/cpt-ui/__tests__/LogoutPage.test.tsx +++ b/packages/cpt-ui/__tests__/LogoutPage.test.tsx @@ -1,7 +1,6 @@ // @ts-nocheck import "@testing-library/jest-dom"; import { render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; import React, { useState } from "react"; // Mock the configureAmplify module diff --git a/packages/cpt-ui/app/login/page.tsx b/packages/cpt-ui/app/login/page.tsx index 87419b6f50..c4e0b6527b 100644 --- a/packages/cpt-ui/app/login/page.tsx +++ b/packages/cpt-ui/app/login/page.tsx @@ -62,7 +62,6 @@ export default function AuthPage() { console.log(auth); }, [auth]) - // TODO: This should show a spinner if (!MOCK_AUTH_ALLOWED.includes(target_environment)) { return (
    diff --git a/packages/cpt-ui/app/logout/page.tsx b/packages/cpt-ui/app/logout/page.tsx index bb720d45bc..99b15f9fc2 100644 --- a/packages/cpt-ui/app/logout/page.tsx +++ b/packages/cpt-ui/app/logout/page.tsx @@ -15,7 +15,7 @@ export default function LogoutPage() { const signOut = async () => { console.log("Signing out", auth); - // DELETEME: [DEV] Wait 3 seconds + // FIXME: [DEV] Wait 3 seconds await new Promise((resolve) => setTimeout(resolve, 3000)); await auth?.cognitoSignOut(); @@ -29,9 +29,10 @@ export default function LogoutPage() { } }, [auth]); + // TODO: Move strings to a constants file return (
    - + {auth?.isSignedIn ? ( <>

    Logging out

    diff --git a/packages/cpt-ui/assets/styles/EpsModal.scss b/packages/cpt-ui/assets/styles/EpsModal.scss index 1b5390e027..0f9e35bbdd 100644 --- a/packages/cpt-ui/assets/styles/EpsModal.scss +++ b/packages/cpt-ui/assets/styles/EpsModal.scss @@ -4,6 +4,7 @@ left: 0; height: 100vh; width: 100vw; + border: 0px; background-color: rgba(0, 0, 0, 0.5); // darken the background display: flex; align-items: center; @@ -15,9 +16,12 @@ background: #fff; padding: 2rem; border-radius: 4px; + border: 0px; + text-align: left; position: relative; max-width: 700px; width: 90%; + display: block; } .eps-modal-button { diff --git a/packages/cpt-ui/components/EpsHeader.tsx b/packages/cpt-ui/components/EpsHeader.tsx index 51f44a6a4d..9181015776 100644 --- a/packages/cpt-ui/components/EpsHeader.tsx +++ b/packages/cpt-ui/components/EpsHeader.tsx @@ -83,6 +83,10 @@ export default function EpsHeader() { )} + {/* + FIXME: Only the selectyourrole and changerole links get put in the + collapsible menu when on mobile + */} {pathname === "/selectyourrole" ? (
  • void; - onConfirm: () => void; + readonly isOpen: boolean; + readonly onClose: () => void; + readonly onConfirm: () => void; } export function EpsLogoutModal({ isOpen, onClose, onConfirm }: EpsLogoutModalProps) { return ( + +

    {EpsLogoutModalStrings.title}

    {EpsLogoutModalStrings.caption}

    @@ -23,17 +25,18 @@ export function EpsLogoutModal({ isOpen, onClose, onConfirm }: EpsLogoutModalPro
  • + ); } diff --git a/packages/cpt-ui/components/EpsModal.tsx b/packages/cpt-ui/components/EpsModal.tsx index de40349f10..ec29d24e93 100644 --- a/packages/cpt-ui/components/EpsModal.tsx +++ b/packages/cpt-ui/components/EpsModal.tsx @@ -4,9 +4,9 @@ import React, { useEffect } from "react"; import "@/assets/styles/EpsModal.scss"; interface EpsModalProps { - children: React.ReactNode; - isOpen: boolean; - onClose: () => void; + readonly children: React.ReactNode; + readonly isOpen: boolean; + readonly onClose: () => void; } export function EpsModal({ children, isOpen, onClose }: EpsModalProps) { @@ -40,17 +40,19 @@ export function EpsModal({ children, isOpen, onClose }: EpsModalProps) { if (!isOpen) return null; return ( + // This should be a button for accessibility, but we can't have buttons be descendants of buttons, + // and the modal children will have buttons in it. + // (making this a button does actually work, so this might be a FIXME to solve the hydration error)
    -
    @@ -60,7 +62,7 @@ export function EpsModal({ children, isOpen, onClose }: EpsModalProps) { {children} -
    +
    ) } diff --git a/packages/cpt-ui/components/EpsSpinner.tsx b/packages/cpt-ui/components/EpsSpinner.tsx index 968a0e4f11..e56cbee7c9 100644 --- a/packages/cpt-ui/components/EpsSpinner.tsx +++ b/packages/cpt-ui/components/EpsSpinner.tsx @@ -17,6 +17,8 @@ function Spinner({ const offset = circumference * (1 - fraction); return ( + // FIXME: In theory, this should be a , but doing that makes the spinner come out all funky. + // Someone better with CSS should fix that, since it would be better for accessibility.
    { const cognitoSignOut = async () => { console.log("Signing out..."); - // TODO: Also sign out of the CPT API, so it can delete the token? + // TODO: Also sign out of the CPT API, so it can delete the token + // This is blocked until we have a central Dynamo interaction lambda try { await signOut({ global: true }); From 65535915f7da0f1b07f062f81e2625c10abb0b19 Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Wed, 8 Jan 2025 09:39:11 +0000 Subject: [PATCH 24/33] misc. minor fixes. --- README.md | 10 +- packages/cpt-ui/__tests__/LogoutPage.test.tsx | 102 +++++++++--------- packages/cpt-ui/app/logout/page.tsx | 11 +- packages/cpt-ui/assets/styles/EpsModal.scss | 17 +-- packages/cpt-ui/components/EpsHeader.tsx | 7 +- packages/cpt-ui/components/EpsLogoutModal.tsx | 12 ++- packages/cpt-ui/components/EpsSpinner.tsx | 9 +- 7 files changed, 90 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index 8d415773b8..992fd3f432 100644 --- a/README.md +++ b/README.md @@ -98,11 +98,12 @@ export NEXT_PUBLIC_userPoolId="eu-west-2_deadbeef" export LOCAL_DEV=true # DON'T TOUCH! +export BASE_PATH="/site" export API_DOMAIN_OVERRIDE=https://${SERVICE_NAME}.dev.eps.national.nhs.uk/ export NEXT_PUBLIC_hostedLoginDomain=${SERVICE_NAME}.auth.eu-west-2.amazoncognito.com -export NEXT_PUBLIC_redirectSignIn=http://localhost:3000/auth_demo/ -export NEXT_PUBLIC_redirectSignOut=http://localhost:3000/ +export NEXT_PUBLIC_redirectSignIn=http://localhost:3000/site/login +export NEXT_PUBLIC_redirectSignOut=http://localhost:3000/site/logout export NEXT_PUBLIC_COMMIT_ID="Local Development Server" @@ -121,11 +122,12 @@ userPoolClientId=$(aws cloudformation list-exports --region eu-west-2 --query "E userPoolId=$(aws cloudformation list-exports --region eu-west-2 --query "Exports[?Name=='${SERVICE_NAME}-stateful-resources:userPool:Id'].Value" --output text) echo $userPoolClientId echo $userPoolId + ``` -For me, the aws terminal console installed in the dev container refuses to work. Another approach is to use the browser console, accessed by clicking the terminal icon next to the search bar on the AWS web dashboard. +For me, the aws terminal console installed in the dev container refuses to work without causing a headache. Another approach is to use the browser console, accessed by clicking the terminal icon next to the search bar on the AWS web dashboard. -n.b. Ensure you've properly sourced these variables! Direnv can sometimes miss changes. +n.b. Ensure you've properly sourced these variables! `direnv` can sometimes miss changes on my machine. ``` source .envrc ``` diff --git a/packages/cpt-ui/__tests__/LogoutPage.test.tsx b/packages/cpt-ui/__tests__/LogoutPage.test.tsx index ce84248f61..6a2e684e2b 100644 --- a/packages/cpt-ui/__tests__/LogoutPage.test.tsx +++ b/packages/cpt-ui/__tests__/LogoutPage.test.tsx @@ -33,53 +33,57 @@ const mockCognitoSignIn = jest.fn(); const mockCognitoSignOut = jest.fn(); interface MockAuthProviderProps { - children: React.ReactNode; - defaultIsSignedIn?: boolean; - defaultUser?: { username: string } | null; - } - - const MockAuthProvider: React.FC = ({ - children, - defaultIsSignedIn = true, - defaultUser = { username: "mockUser" }, - }) => { - // State to simulate auth changes - const [authState, setAuthState] = useState({ - isSignedIn: defaultIsSignedIn, - user: defaultUser, - error: null as string | null, - idToken: defaultIsSignedIn ? "mockIdToken" : null, - accessToken: defaultIsSignedIn ? "mockAccessToken" : null, - cognitoSignIn: async (options: { provider: { custom: any } }) => { - mockCognitoSignIn(options); - // Simulate a sign-in update - setAuthState((prev) => ({ - ...prev, - isSignedIn: true, - user: { username: options?.provider?.custom || "mockUser" }, - error: null, - idToken: "mockIdToken", - accessToken: "mockAccessToken", - })); - }, - cognitoSignOut: async () => { - mockCognitoSignOut(); - // Simulate a sign-out update - setAuthState((prev) => ({ - ...prev, - isSignedIn: false, - user: null, - error: null, - idToken: null, - accessToken: null, - })); - }, - }); - - return ( - {children} - ); - }; + children: React.ReactNode; + defaultIsSignedIn?: boolean; + defaultUser?: { username: string } | null; +} + +const MockAuthProvider: React.FC = ({ + children, + defaultIsSignedIn = true, + defaultUser = { username: "mockUser" }, +}) => { + // State to simulate auth changes + const [authState, setAuthState] = useState({ + isSignedIn: defaultIsSignedIn, + user: defaultUser, + error: null as string | null, + idToken: defaultIsSignedIn ? "mockIdToken" : null, + accessToken: defaultIsSignedIn ? "mockAccessToken" : null, + cognitoSignIn: async (options: { provider: { custom: any } }) => { + await new Promise((resolve) => setTimeout(resolve, 3000)); + + mockCognitoSignIn(options); + // Simulate a sign-in update + setAuthState((prev) => ({ + ...prev, + isSignedIn: true, + user: { username: options?.provider?.custom || "mockUser" }, + error: null, + idToken: "mockIdToken", + accessToken: "mockAccessToken", + })); + }, + cognitoSignOut: async () => { + await new Promise((resolve) => setTimeout(resolve, 3000)); + + mockCognitoSignOut(); + // Simulate a sign-out update + setAuthState((prev) => ({ + ...prev, + isSignedIn: false, + user: null, + error: null, + idToken: null, + accessToken: null, + })); + }, + }); + + return ( + {children} + ); +}; // Since we've referenced AuthContext in the mock provider, we need to re-import it here // after the mock is set up. @@ -112,7 +116,7 @@ describe("LogoutPage", () => { expect(screen.getByText(/Logout successful/i)).toBeInTheDocument(); expect( screen.getByText( - /You are now logged out of the service. To continue using the application, you must log in again/i + /You are now logged out of the service. To continue using the service, you must log in again/i ) ).toBeInTheDocument(); @@ -144,7 +148,7 @@ describe("LogoutPage", () => { expect(screen.getByText(/Logout successful/i)).toBeInTheDocument(); expect( screen.getByText( - /You are now logged out of the service. To continue using the application, you must log in again/i + /You are now logged out of the service. To continue using the service, you must log in again/i ) ).toBeInTheDocument(); }); diff --git a/packages/cpt-ui/app/logout/page.tsx b/packages/cpt-ui/app/logout/page.tsx index 99b15f9fc2..76fae18bbf 100644 --- a/packages/cpt-ui/app/logout/page.tsx +++ b/packages/cpt-ui/app/logout/page.tsx @@ -15,9 +15,6 @@ export default function LogoutPage() { const signOut = async () => { console.log("Signing out", auth); - // FIXME: [DEV] Wait 3 seconds - await new Promise((resolve) => setTimeout(resolve, 3000)); - await auth?.cognitoSignOut(); console.log("Signed out: ", auth); } @@ -41,11 +38,9 @@ export default function LogoutPage() { ) : ( <>

    Logout successful

    -
    You are now logged out of the service. To continue using the application, you must log in again.
    - +
    You are now logged out of the service. To continue using the service, you must log in again.
    +

    + Log in diff --git a/packages/cpt-ui/assets/styles/EpsModal.scss b/packages/cpt-ui/assets/styles/EpsModal.scss index 0f9e35bbdd..ab6edc2baa 100644 --- a/packages/cpt-ui/assets/styles/EpsModal.scss +++ b/packages/cpt-ui/assets/styles/EpsModal.scss @@ -25,22 +25,27 @@ } .eps-modal-button { - width: 40%; + width: 35%; } .eps-modal-close-button { position: absolute; - top: 1rem; + top: 0rem; right: 1rem; - font-size: 1.5rem; background: transparent; border-radius: 4px; - cursor: pointer; // TODO: Does this want to be a normal cursor Or a pointer? + border: 0px; + font-size: 3rem; + cursor: pointer; +} + +.eps-modal-close-button:hover { + color: red; } .eps-modal-button-group { display: flex; - justify-content: center; - gap: 20%; + justify-content: left; + gap: 5%; margin-top: 2rem; } diff --git a/packages/cpt-ui/components/EpsHeader.tsx b/packages/cpt-ui/components/EpsHeader.tsx index 9181015776..150018c876 100644 --- a/packages/cpt-ui/components/EpsHeader.tsx +++ b/packages/cpt-ui/components/EpsHeader.tsx @@ -83,10 +83,7 @@ export default function EpsHeader() { )} - {/* - FIXME: Only the selectyourrole and changerole links get put in the - collapsible menu when on mobile - */} + {/* FIXME: Only the selectyourrole and changerole links get put in the collapsible menu when on mobile */} {pathname === "/selectyourrole" ? (

  • )} - {/* FIXME: Only shows the Log out link if the user is signed in, but introduces a lag on page reload. Acceptable? */} + {/* FIXME: Only shows the Log out link if the user is signed in, but introduces a lag on page reload. */} {auth?.isSignedIn && (
  • -

    {EpsLogoutModalStrings.title}

    +

    + {EpsLogoutModalStrings.title}

    {EpsLogoutModalStrings.caption}

    - {/* TODO: style appropriately */}