diff --git a/.pipelines/3p-e2e.yml b/.pipelines/3p-e2e.yml index d169e57754..392bce6ed5 100644 --- a/.pipelines/3p-e2e.yml +++ b/.pipelines/3p-e2e.yml @@ -123,20 +123,12 @@ extends: - "react-router-sample" - "typescript-sample" - "b2c-sample" + - "react16-sample" + - "react17-sample" + - "react18-sample" debug: ${{ parameters.debug }} npmInstallTimeout: ${{ parameters.npmInstallTimeout }} enableScreenshots: ${{ parameters.enableScreenshots }} - - template: .pipelines/templates/msal-react-react18-e2e.yml@self - parameters: - poolType: ${{ parameters.poolType }} - sourceBranch: ${{ variables.sourceBranch }} - debug: ${{ parameters.debug }} - npmInstallTimeout: ${{ parameters.npmInstallTimeout }} - enableScreenshots: ${{ parameters.enableScreenshots }} - samples: - - "react-router-sample" - - "typescript-sample" - - "b2c-sample" - ${{ if eq(parameters.runAngularTests, true) }}: - template: .pipelines/templates/e2e-tests.yml@1P parameters: diff --git a/.pipelines/templates/msal-react-react18-e2e.yml b/.pipelines/templates/msal-react-react18-e2e.yml deleted file mode 100644 index 2a7a739176..0000000000 --- a/.pipelines/templates/msal-react-react18-e2e.yml +++ /dev/null @@ -1,184 +0,0 @@ -parameters: - - name: "poolType" - type: string - default: "linux" - values: - - "linux" - - "windows" - - name: "sourceBranch" - type: string - - name: "npmInstallTimeout" - type: number - default: 15 - - name: "debug" - type: boolean - default: false - - name: "enableScreenshots" - type: boolean - default: false - - name: "samples" - type: object - default: - - "react-router-sample" - - "typescript-sample" - - "b2c-sample" - -jobs: - - ${{ each sample in parameters.samples }}: - - job: validate_msal_react_r18_${{ replace(sample, '-', '_') }} - displayName: "[msal-react] - ${{ sample }} - React 18 - Node v22" - - pool: - type: ${{ parameters.poolType }} - isCustom: true - name: "Azure Pipelines" - ${{ if eq(parameters.poolType, 'linux') }}: - vmImage: "ubuntu-latest" - ${{ elseif eq(parameters.poolType, 'windows') }}: - vmImage: "windows-latest" - - variables: - - template: .pipelines/templates/variables.yml@1P - - name: "npm_config_cache" - value: "$(Pipeline.Workspace)/.npm" - - name: "samplePath" - ${{ if eq(parameters.poolType, 'windows') }}: - value: "samples/msal-react-samples/${{ sample }}" - ${{ else }}: - value: "samples/msal-react-samples/${{ sample }}" - - steps: - - template: .pipelines/templates/build-var-dump.yml@1P - parameters: - debug: ${{ parameters.debug }} - - - checkout: self - - - task: CmdLine@2 - displayName: "Checkout ${{ variables.sourceBranch }} branch" - inputs: - script: git switch $(sourceBranch) || git switch -c $(sourceBranch) origin/$(sourceBranch) || git switch -c $(sourceBranch) origin/dev - - - template: .pipelines/templates/node-install-wrapper.yml@1P - parameters: - nodeVersion: "22" - - - task: npmAuthenticate@0 - displayName: "Authenticate to npm package registry" - inputs: - workingFile: ./.npmrc - - - template: .pipelines/templates/npm-install.yml@1P - parameters: - clean: true - name: "msal-react" - workspace: "samples/msal-react-samples/${{ sample }}" - workingDir: "./" - useInternalFeed: false - timeout: ${{ parameters.npmInstallTimeout }} - - - task: CmdLine@2 - displayName: "Override sample to React 18 via npm overrides" - inputs: - targetType: inline - script: | - npm pkg set "overrides.react=^18" "overrides.react-dom=^18" "overrides.@types/react=^18" "overrides.@types/react-dom=^18" - npm install - workingDirectory: "./" - - - ${{ if eq(parameters.poolType, 'linux') }}: - - template: .pipelines/templates/npm-install.yml@1P - parameters: - name: "rollup linux platform deps" - workingDir: "./" - paramList: "--no-save" - packageList: "@rollup/rollup-linux-x64-gnu" - useInternalFeed: false - timeout: ${{ parameters.npmInstallTimeout }} - - - task: Npm@1 - displayName: "Build msal-react" - inputs: - command: "custom" - customCommand: "run build:all" - workingDir: "lib/msal-react" - - - task: Npm@1 - displayName: "Build msal-node" - inputs: - command: "custom" - customCommand: "run build:all" - workingDir: "lib/msal-node" - - - task: Npm@1 - displayName: "Build ${{ sample }}" - inputs: - command: "custom" - customCommand: "run build" - workingDir: "$(samplePath)" - - - task: Npm@1 - displayName: "Generate certificates" - inputs: - command: "custom" - customCommand: "run generate:certs --if-present" - workingDir: "$(samplePath)" - - - template: .pipelines/templates/install-keyvault-secrets.yml@1P - parameters: - keyVaultName: "msidlabs" - secretsFilter: "LabVaultAccessCert" - secretName: "LabVaultAccessCert" - pfxPath: "$(Build.SourcesDirectory)/LabCert.pfx" - pemPath: "$(Build.SourcesDirectory)/LabCert.pem" - - - ${{ if eq(parameters.enableScreenshots, true) }}: - - task: PowerShell@2 - displayName: "Create screenshot folder" - inputs: - targetType: "inline" - script: New-Item -ItemType Directory -Force -Path "${{ variables.samplePath }}/test/screenshots/react18" - pwsh: true - - - task: Npm@1 - displayName: "Test ${{ sample }}" - timeoutInMinutes: 30 - retryCountOnTaskFailure: 3 - inputs: - command: "custom" - customCommand: "run test:e2e -- --sample=${{ sample }} --detectOpenHandles --forceExit --reporters=default --reporters=jest-junit" - workingDir: "$(samplePath)" - env: - AZURE_CLIENT_CERTIFICATE_PATH: $(Build.SourcesDirectory)/LabCert.pem - AZURE_CLIENT_ID: $(AZURE_CLIENT_ID) - AZURE_CLIENT_SEND_CERTIFICATE_CHAIN: "true" - AZURE_TENANT_ID: $(AZURE_TENANT_ID) - JEST_JUNIT_OUTPUT_NAME: "${{ sample }}.xml" - SESSION_SECRET: $(EXPRESS_SESSION_SECRET) - ${{ if eq(parameters.enableScreenshots, true) }}: - ENABLE_E2E_SCREENSHOTS: "true" - - - task: PublishTestResults@2 - displayName: "Publish Test Results" - condition: succeededOrFailed() - inputs: - testResultsFiles: "$(samplePath)/${{ sample }}.xml" - testRunTitle: "${{ sample }}-react18" - - - ${{ if eq(parameters.enableScreenshots, true) }}: - - task: CopyFiles@2 - condition: failed() - inputs: - SourceFolder: "$(samplePath)/test/screenshots/react18" - Contents: "**" - TargetFolder: $(Build.ArtifactStagingDirectory) - - - task: PublishPipelineArtifact@1 - displayName: "Upload screenshots to artifacts" - condition: failed() - inputs: - targetPath: $(Build.ArtifactStagingDirectory) - artifact: "drop_e2e_test_validate_msal_react_r18_${{ replace(sample, '-', '_') }}" - publishLocation: "pipeline" - - - template: .pipelines/templates/remove-keyvault-secrets.yml@1P diff --git a/change/@azure-msal-react-fc1bd3a1-c1c1-43f6-b654-5934d367b063.json b/change/@azure-msal-react-fc1bd3a1-c1c1-43f6-b654-5934d367b063.json new file mode 100644 index 0000000000..7bc1b72702 --- /dev/null +++ b/change/@azure-msal-react-fc1bd3a1-c1c1-43f6-b654-5934d367b063.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Extend peer dependency range to support React 16 (16.8+) and React 17 [#8461](https://github.com/AzureAD/microsoft-authentication-library-for-js/pull/8461)", + "packageName": "@azure/msal-react", + "email": "joarroyo@microsoft.com", + "dependentChangeType": "patch" +} \ No newline at end of file diff --git a/lib/msal-react/AGENTS.md b/lib/msal-react/AGENTS.md index 1dd0b65b8e..859c0c998a 100644 --- a/lib/msal-react/AGENTS.md +++ b/lib/msal-react/AGENTS.md @@ -3,6 +3,7 @@ ## Supported Environments - msal-react supports all modern mainstream browsers (Chrome, Firefox, Safari, Edge) -- msal-react supports React versions 18 and 19 +- msal-react supports React versions 16 (16.8+), 17, 18, and 19 -Never use or suggest APIs or features that are not supported by the environments listed above. \ No newline at end of file +Never use or suggest APIs or features that are not supported by the environments listed above. +React hooks are required (available since React 16.8). Do not use React 18+ specific APIs (e.g., `useSyncExternalStore`, `useId`, `useTransition`, `startTransition`, `createRoot`) in the library source code. diff --git a/lib/msal-react/README.md b/lib/msal-react/README.md index 5098695941..6e5c325967 100644 --- a/lib/msal-react/README.md +++ b/lib/msal-react/README.md @@ -40,7 +40,7 @@ The `@azure/msal-react` package described by the code in this folder uses the [` | MSAL React version | MSAL support status | Supported React versions | | --------------------- | ------------------- | ------------------------ | -| MSAL React v5 | Active development | 18, 19 | +| MSAL React v5 | Active development | 16 (16.8+), 17, 18, 19 | | MSAL React v3 | In maintenance | 16, 17, 18, 19 | | MSAL React v1, v2 | In maintenance | 16, 17, 18 | @@ -134,6 +134,9 @@ Our [samples directory](https://github.com/AzureAD/microsoft-authentication-libr - [Next.js Sample](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/samples/msal-react-samples/nextjs-sample) - [Gatsby Sample](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/samples/msal-react-samples/gatsby-sample) - [B2C Sample](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/samples/msal-react-samples/b2c-sample) +- [React 16 Compat Sample (MUI v4)](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/samples/msal-react-samples/react16-sample) +- [React 17 Compat Sample](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/samples/msal-react-samples/react17-sample) +- [React 18 Compat Sample](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/samples/msal-react-samples/react18-sample) More advanced samples backed with a tutorial can be found in the [Azure Samples](https://github.com/Azure-Samples) space on GitHub: diff --git a/lib/msal-react/docs/getting-started.md b/lib/msal-react/docs/getting-started.md index 07ca4164a6..5ad860075a 100644 --- a/lib/msal-react/docs/getting-started.md +++ b/lib/msal-react/docs/getting-started.md @@ -10,7 +10,7 @@ `@azure/msal-react` is built on the [React context API](https://reactjs.org/docs/context.html) and all parts of your app that require authentication must be wrapped in the `MsalProvider` component. You will first need to [initialize](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/initialization.md) an instance of `PublicClientApplication` then pass this to `MsalProvider` as a prop. -> **React version support:** `@azure/msal-react` v5 supports both React 18 and React 19. Choose the rendering API that matches your React version. +> **React version support:** `@azure/msal-react` v5 supports React 16 (16.8+), 17, 18, and 19 (19.2.1+). React 19.0.0–19.2.0 are excluded due to [CVE-2025-55182](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2025-55182). All React hooks are required (available since React 16.8). Choose the rendering API that matches your React version. ```javascript import React from "react"; @@ -42,6 +42,15 @@ const root = createRoot(document.getElementById("root")); root.render(); ``` +For React 16 or 17, use `ReactDOM.render` instead: + +```javascript +import React from "react"; +import ReactDOM from "react-dom"; + +ReactDOM.render(, document.getElementById("root")); +``` + All components underneath `MsalProvider` will have access to the `PublicClientApplication` instance via context as well as all hooks and components provided by `@azure/msal-react`. ## Determining whether a user is authenticated diff --git a/lib/msal-react/docs/migration-guide-v4-v5.md b/lib/msal-react/docs/migration-guide-v4-v5.md index f9a8e22812..d76ab9676b 100644 --- a/lib/msal-react/docs/migration-guide-v4-v5.md +++ b/lib/msal-react/docs/migration-guide-v4-v5.md @@ -15,23 +15,6 @@ MSAL Browser v5 requires a dedicated redirect page/bridge for authentication flo Please see the [COOP section in the MSAL Browser v4-v5 migration guide](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/v4-migration.md#cross-origin-opener-policy-coop-support). -## Updated React version support -MSAL React v5 supports React 18.0.0 or greater and React 19.2.1 or greater. It no longer supports React 16 or 17. - -## Peer dependency ranges - -MSAL React v5 declares its `react` peer dependency as `"^18.0.0 || ^19.2.1"`. This means: - -- Applications using React 18.x will satisfy the peer dependency without errors or warnings. -- Applications using React 19.2.1 or newer (within the React 19.x line) will satisfy the peer dependency without errors or warnings. -- No `--legacy-peer-deps` flag is required for either version. - -### Known considerations - -- **`@types/react`**: If you use TypeScript, install the `@types/react` version matching your React major version (`@types/react@^18` for React 18, `@types/react@^19` for React 19). Both are compatible with the MSAL React v5 public API surface. -- **`@testing-library/react`**: v14+ supports React 18; v16+ supports both React 18 and 19. Choose the version that matches your React version. -- **`react-dom`**: Install the same major version of `react-dom` as `react` (e.g., `react-dom@^18` with `react@^18`). - ## Migrating from Create React App (react-scripts) Create React App is deprecated and `react-scripts` does not support React 19. If your app uses `react-scripts`, you should migrate to a different build tool before upgrading to `@azure/msal-react@^5`. While React 18 is supported by `@azure/msal-react@^5`, CRA is no longer maintained and Vite is recommended. diff --git a/lib/msal-react/package.json b/lib/msal-react/package.json index a124702116..b85832bd86 100644 --- a/lib/msal-react/package.json +++ b/lib/msal-react/package.json @@ -57,7 +57,7 @@ }, "peerDependencies": { "@azure/msal-browser": "^5.6.0", - "react": "^18.0.0 || ^19.2.1" + "react": "^16.8.0 || ^17 || ^18 || ^19.2.1" }, "devDependencies": { "@azure/msal-browser": "^5.6.0", @@ -83,4 +83,4 @@ "tslib": "^2.0.0", "typescript": "^4.9.5" } -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d7fd875a97..9a21eff36e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -314,7 +314,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -755,7 +754,6 @@ "integrity": "sha512-vdMJX9E7wePN41T+6BYRQBA+XiR9a5DBhs20dqtv8YVireQktH6mxLZIg1pVxkL/gnao2gpl/lOvp0xmC7UN/Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "7.26.9", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -829,7 +827,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "dev": true, - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -1018,8 +1015,7 @@ "version": "12.20.55", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "dev": true, - "peer": true + "dev": true }, "lib/msal-angular/node_modules/ajv": { "version": "8.18.0", @@ -1145,7 +1141,6 @@ "resolved": "https://registry.npmjs.org/ng-packagr/-/ng-packagr-19.2.2.tgz", "integrity": "sha512-dFuwFsDJMBSd1YtmLLcX5bNNUCQUlRqgf34aXA+79PmkOP+0eF8GP2949wq3+jMjmFTNm80Oo8IUYiSLwklKCQ==", "dev": true, - "peer": true, "dependencies": { "@rollup/plugin-json": "^6.1.0", "@rollup/wasm-node": "^4.24.0", @@ -1232,7 +1227,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", @@ -1372,7 +1366,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -1387,7 +1380,6 @@ "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -1532,7 +1524,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.57.tgz", "integrity": "sha512-f3T4y6VU4fVQDKVqJV4Uppy8c1p/sVvS3peyqxyWnzkqXFJLRU7Y1Bl7rMS1Qe9z0v4M6McY0Fp9yBsgHJUsWQ==", "dev": true, - "peer": true, "dependencies": { "undici-types": "~6.19.2" } @@ -1666,7 +1657,7 @@ }, "peerDependencies": { "@azure/msal-browser": "^5.6.0", - "react": "^18.0.0 || ^19.2.1" + "react": "^16.8.0 || ^17 || ^18 || ^19.2.1" } }, "lib/msal-react/node_modules/@types/node": { @@ -2400,7 +2391,6 @@ "integrity": "sha512-XGChk+26XZpcwzIQUVjgLxGVC//m5TaDrogseQNIGs2Chzv6KYbo91HftL69fTiM5udRYjg6IV7XEzDNF/GVUw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -2464,7 +2454,6 @@ "integrity": "sha512-/JYo8jJZ6BAgw3IVYJpinAfGb+RbaZubrElFvaq450BWxDPInv7Z99HKEQ3qEBRsBeIAQ/WrKXDxoJSjy7QMNQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -2482,7 +2471,6 @@ "integrity": "sha512-kWlqFW7ExvAqKv+X/6ZsWVW7YTmI1/3VUvADLC/6bkLTdKrHS8OtBHfsklXmHMNVbbFopTvoTeKkoqLGrW2lwg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -2496,7 +2484,6 @@ "integrity": "sha512-VZAzpxBoQgyy7AOlhxbAHxPQWo0nk8xsnrD36PLCZeTZA/5GNzO3lLVaX2N5BCUgpgiCBjNBKq9kVo6ZkAls9g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -2533,7 +2520,6 @@ "integrity": "sha512-bnQSmoJNI1LQxJnHnB01XQXqgOdgAtLAOsa24ZT6b2pWV3Vw0/7+V2dZsNZX/TJtejunvSgSDCEqgJhIQ5vBVg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -2594,6 +2580,7 @@ "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.2.tgz", "integrity": "sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA==", "dev": true, + "peer": true, "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", @@ -2611,6 +2598,7 @@ "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", "dev": true, + "peer": true, "engines": { "node": ">=10" } @@ -2619,13 +2607,15 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@apidevtools/swagger-parser": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.1.1.tgz", "integrity": "sha512-u/kozRnsPO/x8QtKYJOqoGtC4kH6yg1lfYkB9Au0WhYB0FNLpyFusttQtvhlwjtG3rOwiRz4D8DnnXa8iEpIKA==", "dev": true, + "peer": true, "dependencies": { "@apidevtools/json-schema-ref-parser": "11.7.2", "@apidevtools/openapi-schemas": "^2.1.0", @@ -2662,6 +2652,7 @@ "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", "dev": true, + "peer": true, "peerDependencies": { "ajv": "^8.5.0" }, @@ -2687,6 +2678,7 @@ "resolved": "https://registry.npmjs.org/@azure/arm-appservice/-/arm-appservice-13.0.3.tgz", "integrity": "sha512-Vu011o3/bikQNwtjouwmUJud+Z6Brcjij2D0omPWClRGg8i5gBfOYSpDkFGkHbhGlaky4fgvfkxD0uHGq34uYA==", "dev": true, + "peer": true, "dependencies": { "@azure/abort-controller": "^1.0.0", "@azure/core-auth": "^1.3.0", @@ -2705,6 +2697,7 @@ "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", "dev": true, + "peer": true, "dependencies": { "tslib": "^2.2.0" }, @@ -2717,6 +2710,7 @@ "resolved": "https://registry.npmjs.org/@azure/arm-resources/-/arm-resources-5.0.1.tgz", "integrity": "sha512-JbZtIqfEulsIA0rC3zM7jfF4KkOnye9aKcaO/jJqxJRm/gM6lAjEv7sL4njW8D+35l50P1f+UuH5OqN+UKJqNA==", "dev": true, + "peer": true, "dependencies": { "@azure/abort-controller": "^1.0.0", "@azure/core-auth": "^1.3.0", @@ -2735,6 +2729,7 @@ "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", "dev": true, + "peer": true, "dependencies": { "tslib": "^2.2.0" }, @@ -2747,6 +2742,7 @@ "resolved": "https://registry.npmjs.org/@azure/arm-storage/-/arm-storage-17.2.1.tgz", "integrity": "sha512-J2jmTPv8ZraSHDTz9l2Bx8gNL3ktfDDWo2mxWfzarn64O9Fjhb+l85YWyubGy2xUdeGuZPKzvQLltGv8bSu8eQ==", "dev": true, + "peer": true, "dependencies": { "@azure/abort-controller": "^1.0.0", "@azure/core-auth": "^1.3.0", @@ -2765,6 +2761,7 @@ "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", "dev": true, + "peer": true, "dependencies": { "tslib": "^2.2.0" }, @@ -2777,6 +2774,7 @@ "resolved": "https://registry.npmjs.org/@azure/arm-subscriptions/-/arm-subscriptions-5.1.0.tgz", "integrity": "sha512-6BeOF2eQWNLq22ch7xP9RxYnPjtGev54OUCGggKOWoOvmesK7jUZbIyLk8JeXDT21PEl7iyYnxw78gxJ7zBxQw==", "dev": true, + "peer": true, "dependencies": { "@azure/abort-controller": "^1.0.0", "@azure/core-auth": "^1.3.0", @@ -2795,6 +2793,7 @@ "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", "dev": true, + "peer": true, "dependencies": { "tslib": "^2.2.0" }, @@ -3118,7 +3117,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -7562,7 +7560,6 @@ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, - "peer": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -7611,13 +7608,15 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@feathersjs/hooks": { "version": "0.6.5", "resolved": "https://registry.npmjs.org/@feathersjs/hooks/-/hooks-0.6.5.tgz", "integrity": "sha512-WtcEoG/imdHRvC3vofGi/OcgH+cjHHhO0AfEeTlsnrKLjVKKBXV6aoIrB2nHZPpE7iW5sA7AZMR6bPD8ytxN+w==", "dev": true, + "peer": true, "engines": { "node": ">= 10" } @@ -8418,7 +8417,6 @@ "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.3.2.tgz", "integrity": "sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==", "dev": true, - "peer": true, "dependencies": { "@inquirer/checkbox": "^4.1.2", "@inquirer/confirm": "^5.1.6", @@ -9230,7 +9228,8 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@jsonjoy.com/base64": { "version": "1.1.2", @@ -9448,6 +9447,20 @@ "node": ">= 12.13.0" } }, + "node_modules/@material-ui/types": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz", + "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@microsoft/api-extractor": { "version": "7.57.6", "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.57.6.tgz", @@ -9595,6 +9608,7 @@ "resolved": "https://registry.npmjs.org/@microsoft/app-manifest/-/app-manifest-1.0.1.tgz", "integrity": "sha512-W4fw8JX/9CPATwNAi9dc25rCK/b3qSnoClVDzGfbYuy6ewY9FHgkwk/C1NzC8k/YwZAsKwMhHOvXUCt3u9ak3Q==", "dev": true, + "peer": true, "dependencies": { "@types/fs-extra": "^11.0.1", "@types/node-fetch": "^2.6.9", @@ -9609,6 +9623,7 @@ "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", "dev": true, + "peer": true, "dependencies": { "@types/jsonfile": "*", "@types/node": "*" @@ -9637,6 +9652,7 @@ "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", "dev": true, + "peer": true, "peerDependencies": { "ajv": "^8.5.0" }, @@ -9651,6 +9667,7 @@ "resolved": "https://registry.npmjs.org/@microsoft/dev-tunnels-contracts/-/dev-tunnels-contracts-1.1.9.tgz", "integrity": "sha512-OayhehwI+CnO0Wr53e29ZJZWGsNA5yVG7r54qmZSLc5HxA5Cozk4hP7EbYDCXkxh4NbQoT1dhTzC8bkRo+wWXw==", "dev": true, + "peer": true, "dependencies": { "buffer": "^5.2.1", "debug": "^4.1.1", @@ -9662,6 +9679,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dev": true, + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -9679,6 +9697,7 @@ "resolved": "https://registry.npmjs.org/@microsoft/dev-tunnels-management/-/dev-tunnels-management-1.1.9.tgz", "integrity": "sha512-wGuFEzvRiWZmDxQMGKEjOKhEIVnLiG6vRUuM9Hwqxpe/kbiyA2WiUyEVpniNPaaw8gDHTf9zJHnPNNj0JiL5mA==", "dev": true, + "peer": true, "dependencies": { "@microsoft/dev-tunnels-contracts": ">1.1.8", "axios": "^1.6.2", @@ -9692,6 +9711,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dev": true, + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -9709,6 +9729,7 @@ "resolved": "https://registry.npmjs.org/@microsoft/m365-spec-parser/-/m365-spec-parser-0.2.8.tgz", "integrity": "sha512-G26jdZo6TQNEwpsY95p96RWLtuqzGhrqPFacKJgtODlWmYGj63InMvSdwTDNlZ610SdA9nZUqvULFWmDE/VNqg==", "dev": true, + "peer": true, "dependencies": { "@apidevtools/swagger-parser": "^10.1.1", "@microsoft/app-manifest": "1.0.1", @@ -9726,6 +9747,7 @@ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", "dev": true, + "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -9766,7 +9788,6 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -9813,6 +9834,7 @@ "integrity": "sha512-AowuJwrrUxeF9Bq/frxuy9YZjK/ECk3pi0UBXl3CQLZ4XNWfgWatiFi/UWpyHDLccFs+0Za3nNYATFvgsxEFwQ==", "dev": true, "hasInstallScript": true, + "peer": true, "dependencies": { "@azure/arm-subscriptions": "^5.0.0", "@azure/core-auth": "^1.4.0", @@ -9855,6 +9877,7 @@ "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.16.0.tgz", "integrity": "sha512-1KOZj9IpcDSwpNiQNjt0jDYZpQvNZay7QAEi/5DLubay40iGYtLzya/jbjRPLyOTZhEKyL1MzPuw2HqBCjceYA==", "dev": true, + "peer": true, "engines": { "node": ">=0.8.0" } @@ -9864,6 +9887,7 @@ "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.16.2.tgz", "integrity": "sha512-An7l1hEr0w1HMMh1LU+rtDtqL7/jw74ORlc9Wnh06v7TU/xpG39/Zdr1ZJu3QpjUfKJ+E0/OXMW8DRSWTlh7qQ==", "dev": true, + "peer": true, "dependencies": { "@azure/msal-common": "14.16.0", "jsonwebtoken": "^9.0.0", @@ -9878,6 +9902,7 @@ "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-1.5.2.tgz", "integrity": "sha512-CifrkgQjDkUkWexmgYYNyB5603HhTHI91vLFeQXh6qrTKiCMVASol01Rs1cv6LP/A2WccZSRlJKZhbaBIs/9ZA==", "dev": true, + "peer": true, "dependencies": { "@inquirer/core": "^6.0.0", "@inquirer/type": "^1.1.6", @@ -9894,6 +9919,7 @@ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-6.0.0.tgz", "integrity": "sha512-fKi63Khkisgda3ohnskNf5uZJj+zXOaBvOllHsOkdsXRA/ubQLJQrZchFFi57NKbZzkTunXiBMdvWOv71alonw==", "dev": true, + "peer": true, "dependencies": { "@inquirer/type": "^1.1.6", "@types/mute-stream": "^0.0.4", @@ -9919,6 +9945,7 @@ "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-2.0.17.tgz", "integrity": "sha512-EqzhGryzmGpy2aJf6LxJVhndxYmFs+m8cxXzf8nejb1DE3sabf6mUgBcp4J0jAUEiAcYzqmkqRr7LPFh/WdnXA==", "dev": true, + "peer": true, "dependencies": { "@inquirer/core": "^6.0.0", "@inquirer/type": "^1.1.6", @@ -9933,6 +9960,7 @@ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-6.0.0.tgz", "integrity": "sha512-fKi63Khkisgda3ohnskNf5uZJj+zXOaBvOllHsOkdsXRA/ubQLJQrZchFFi57NKbZzkTunXiBMdvWOv71alonw==", "dev": true, + "peer": true, "dependencies": { "@inquirer/type": "^1.1.6", "@types/mute-stream": "^0.0.4", @@ -9958,6 +9986,7 @@ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-5.1.2.tgz", "integrity": "sha512-w3PMZH5rahrukn8/I7P9Ihil+twgLTUHDZtJlJyBbUKyPaOSSQjLZkb0PpncVhin1gCaMgOFXy6iNPgcZUoo2w==", "dev": true, + "peer": true, "dependencies": { "@inquirer/type": "^1.1.6", "@types/mute-stream": "^0.0.4", @@ -9983,6 +10012,7 @@ "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-1.2.15.tgz", "integrity": "sha512-gQ77Ls09x5vKLVNMH9q/7xvYPT6sIs5f7URksw+a2iJZ0j48tVS6crLqm2ugG33tgXHIwiEqkytY60Zyh5GkJQ==", "dev": true, + "peer": true, "dependencies": { "@inquirer/core": "^6.0.0", "@inquirer/type": "^1.1.6", @@ -9998,6 +10028,7 @@ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-6.0.0.tgz", "integrity": "sha512-fKi63Khkisgda3ohnskNf5uZJj+zXOaBvOllHsOkdsXRA/ubQLJQrZchFFi57NKbZzkTunXiBMdvWOv71alonw==", "dev": true, + "peer": true, "dependencies": { "@inquirer/type": "^1.1.6", "@types/mute-stream": "^0.0.4", @@ -10023,6 +10054,7 @@ "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-1.1.16.tgz", "integrity": "sha512-TGLU9egcuo+s7PxphKUCnJnpCIVY32/EwPCLLuu+gTvYiD8hZgx8Z2niNQD36sa6xcfpdLY6xXDBiL/+g1r2XQ==", "dev": true, + "peer": true, "dependencies": { "@inquirer/core": "^6.0.0", "@inquirer/type": "^1.1.6", @@ -10038,6 +10070,7 @@ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-6.0.0.tgz", "integrity": "sha512-fKi63Khkisgda3ohnskNf5uZJj+zXOaBvOllHsOkdsXRA/ubQLJQrZchFFi57NKbZzkTunXiBMdvWOv71alonw==", "dev": true, + "peer": true, "dependencies": { "@inquirer/type": "^1.1.6", "@types/mute-stream": "^0.0.4", @@ -10063,6 +10096,7 @@ "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-1.2.16.tgz", "integrity": "sha512-Ou0LaSWvj1ni+egnyQ+NBtfM1885UwhRCMtsRt2bBO47DoC1dwtCa+ZUNgrxlnCHHF0IXsbQHYtIIjFGAavI4g==", "dev": true, + "peer": true, "dependencies": { "@inquirer/core": "^6.0.0", "@inquirer/type": "^1.1.6", @@ -10077,6 +10111,7 @@ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-6.0.0.tgz", "integrity": "sha512-fKi63Khkisgda3ohnskNf5uZJj+zXOaBvOllHsOkdsXRA/ubQLJQrZchFFi57NKbZzkTunXiBMdvWOv71alonw==", "dev": true, + "peer": true, "dependencies": { "@inquirer/type": "^1.1.6", "@types/mute-stream": "^0.0.4", @@ -10102,6 +10137,7 @@ "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-1.1.16.tgz", "integrity": "sha512-aZYZVHLUXZ2gbBot+i+zOJrks1WaiI95lvZCn1sKfcw6MtSSlYC8uDX8sTzQvAsQ8epHoP84UNvAIT0KVGOGqw==", "dev": true, + "peer": true, "dependencies": { "@inquirer/core": "^6.0.0", "@inquirer/type": "^1.1.6", @@ -10117,6 +10153,7 @@ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-6.0.0.tgz", "integrity": "sha512-fKi63Khkisgda3ohnskNf5uZJj+zXOaBvOllHsOkdsXRA/ubQLJQrZchFFi57NKbZzkTunXiBMdvWOv71alonw==", "dev": true, + "peer": true, "dependencies": { "@inquirer/type": "^1.1.6", "@types/mute-stream": "^0.0.4", @@ -10142,6 +10179,7 @@ "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-3.3.2.tgz", "integrity": "sha512-k52mOMRvTUejrqyF1h8Z07chC+sbaoaUYzzr1KrJXyj7yaX7Nrh0a9vktv8TuocRwIJOQMaj5oZEmkspEcJFYQ==", "dev": true, + "peer": true, "dependencies": { "@inquirer/checkbox": "^1.5.2", "@inquirer/confirm": "^2.0.17", @@ -10162,6 +10200,7 @@ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-6.0.0.tgz", "integrity": "sha512-fKi63Khkisgda3ohnskNf5uZJj+zXOaBvOllHsOkdsXRA/ubQLJQrZchFFi57NKbZzkTunXiBMdvWOv71alonw==", "dev": true, + "peer": true, "dependencies": { "@inquirer/type": "^1.1.6", "@types/mute-stream": "^0.0.4", @@ -10187,6 +10226,7 @@ "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-1.2.16.tgz", "integrity": "sha512-pZ6TRg2qMwZAOZAV6TvghCtkr53dGnK29GMNQ3vMZXSNguvGqtOVc4j/h1T8kqGJFagjyfBZhUPGwNS55O5qPQ==", "dev": true, + "peer": true, "dependencies": { "@inquirer/core": "^6.0.0", "@inquirer/type": "^1.1.6", @@ -10201,6 +10241,7 @@ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-6.0.0.tgz", "integrity": "sha512-fKi63Khkisgda3ohnskNf5uZJj+zXOaBvOllHsOkdsXRA/ubQLJQrZchFFi57NKbZzkTunXiBMdvWOv71alonw==", "dev": true, + "peer": true, "dependencies": { "@inquirer/type": "^1.1.6", "@types/mute-stream": "^0.0.4", @@ -10226,6 +10267,7 @@ "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-1.3.3.tgz", "integrity": "sha512-RzlRISXWqIKEf83FDC9ZtJ3JvuK1l7aGpretf41BCWYrvla2wU8W8MTRNMiPrPJ+1SIqrRC1nZdZ60hD9hRXLg==", "dev": true, + "peer": true, "dependencies": { "@inquirer/core": "^6.0.0", "@inquirer/type": "^1.1.6", @@ -10242,6 +10284,7 @@ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-6.0.0.tgz", "integrity": "sha512-fKi63Khkisgda3ohnskNf5uZJj+zXOaBvOllHsOkdsXRA/ubQLJQrZchFFi57NKbZzkTunXiBMdvWOv71alonw==", "dev": true, + "peer": true, "dependencies": { "@inquirer/type": "^1.1.6", "@types/mute-stream": "^0.0.4", @@ -10267,6 +10310,7 @@ "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.5.tgz", "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==", "dev": true, + "peer": true, "dependencies": { "mute-stream": "^1.0.0" }, @@ -10279,6 +10323,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.57.tgz", "integrity": "sha512-f3T4y6VU4fVQDKVqJV4Uppy8c1p/sVvS3peyqxyWnzkqXFJLRU7Y1Bl7rMS1Qe9z0v4M6McY0Fp9yBsgHJUsWQ==", "dev": true, + "peer": true, "dependencies": { "undici-types": "~6.19.2" } @@ -10288,6 +10333,7 @@ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, + "peer": true, "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", @@ -10303,6 +10349,7 @@ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", "dev": true, + "peer": true, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -10312,6 +10359,7 @@ "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", "dev": true, + "peer": true, "engines": { "node": ">=0.12.0" } @@ -10321,6 +10369,7 @@ "resolved": "https://registry.npmjs.org/@microsoft/teamsfx-api/-/teamsfx-api-0.23.1.tgz", "integrity": "sha512-XmXX2dccOEU3lbYgOlijfwxmkXp6nO88JWx9P1al/1aMgbIeup2Y2H37Vmz2VwfIQC/i75FbbbbwqYjG2skQjQ==", "dev": true, + "peer": true, "dependencies": { "@azure/core-auth": "^1.4.0", "@microsoft/teams-manifest": "0.1.5", @@ -10336,6 +10385,7 @@ "resolved": "https://registry.npmjs.org/@microsoft/teamsfx-core/-/teamsfx-core-2.0.9.tgz", "integrity": "sha512-6zA/vvpHViROP6eDbnjS8PtPyyB4eZGH/cgTiOHeiRHznT9Pkd3rqFvaIHPEDhv2g76llHEk2gTFSqL7QFovAQ==", "dev": true, + "peer": true, "dependencies": { "@apidevtools/swagger-parser": "^10.1.0", "@azure/arm-appservice": "^13.0.0", @@ -10395,6 +10445,7 @@ "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.16.0.tgz", "integrity": "sha512-1KOZj9IpcDSwpNiQNjt0jDYZpQvNZay7QAEi/5DLubay40iGYtLzya/jbjRPLyOTZhEKyL1MzPuw2HqBCjceYA==", "dev": true, + "peer": true, "engines": { "node": ">=0.8.0" } @@ -10404,6 +10455,7 @@ "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.16.2.tgz", "integrity": "sha512-An7l1hEr0w1HMMh1LU+rtDtqL7/jw74ORlc9Wnh06v7TU/xpG39/Zdr1ZJu3QpjUfKJ+E0/OXMW8DRSWTlh7qQ==", "dev": true, + "peer": true, "dependencies": { "@azure/msal-common": "14.16.0", "jsonwebtoken": "^9.0.0", @@ -10419,6 +10471,7 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -10435,6 +10488,7 @@ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, + "peer": true, "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", @@ -10450,6 +10504,7 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, + "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -10461,7 +10516,8 @@ "version": "0.1.14", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@microsoft/tsdoc": { "version": "0.16.0", @@ -11027,7 +11083,6 @@ "version": "5.17.1", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.17.1.tgz", "integrity": "sha512-2B33kQf+GmPnrvXXweWAx+crbiUEsxCdCN979QDYnlH9ox4pd+0/IBriWLV+l6ORoBF60w39cWjFnJYGFdzXcw==", - "peer": true, "dependencies": { "@babel/runtime": "^7.23.9", "@mui/core-downloads-tracker": "^5.17.1", @@ -12065,7 +12120,6 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz", "integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==", "dev": true, - "peer": true, "dependencies": { "@octokit/auth-token": "^2.4.4", "@octokit/graphql": "^4.5.8", @@ -12251,6 +12305,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -12271,6 +12326,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -12291,6 +12347,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -12311,6 +12368,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -12331,6 +12389,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -12351,6 +12410,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -12371,6 +12431,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -12391,6 +12452,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -12411,6 +12473,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -12431,6 +12494,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -12451,6 +12515,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -12471,6 +12536,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -12491,6 +12557,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -12566,7 +12633,6 @@ "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -12576,6 +12642,7 @@ "version": "2.10.5", "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.5.tgz", "integrity": "sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==", + "peer": true, "dependencies": { "debug": "^4.4.1", "extract-zip": "^2.0.1", @@ -12596,6 +12663,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -12613,6 +12681,7 @@ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", "license": "MIT", + "peer": true, "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" @@ -12626,6 +12695,7 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "peer": true, "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", @@ -12658,7 +12728,6 @@ "version": "1.6.1", "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", - "peer": true, "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -13540,7 +13609,6 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -14224,7 +14292,8 @@ "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==" + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "peer": true }, "node_modules/@tsconfig/node10": { "version": "1.0.11", @@ -14314,7 +14383,8 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -14424,7 +14494,6 @@ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -14685,6 +14754,7 @@ "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", "dev": true, + "peer": true, "dependencies": { "@types/node": "*" } @@ -14693,7 +14763,6 @@ "version": "22.15.29", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz", "integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -14825,6 +14894,14 @@ "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", "dev": true }, + "node_modules/@types/scheduler": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/@types/semver": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", @@ -14903,7 +14980,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@types/ws": { "version": "8.18.1", @@ -14932,6 +15010,7 @@ "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", "optional": true, + "peer": true, "dependencies": { "@types/node": "*" } @@ -14992,7 +15071,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -15807,7 +15885,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -15863,6 +15940,7 @@ "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", "dev": true, + "peer": true, "engines": { "node": ">= 10.0.0" } @@ -15942,7 +16020,6 @@ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -16208,7 +16285,8 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/array-union": { "version": "2.1.0", @@ -16347,6 +16425,7 @@ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true, + "peer": true, "engines": { "node": "*" } @@ -16355,6 +16434,7 @@ "version": "0.13.4", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "peer": true, "dependencies": { "tslib": "^2.0.1" }, @@ -16427,6 +16507,7 @@ "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.3.2.tgz", "integrity": "sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA==", "dev": true, + "peer": true, "dependencies": { "tslib": "^2.3.1" } @@ -16511,6 +16592,7 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", "dev": true, + "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -16522,6 +16604,7 @@ "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-3.9.1.tgz", "integrity": "sha512-8PJDLJv7qTTMMwdnbMvrLYuvB47M81wRtxQmEdV5w4rgbTXTt+vtPkXwajOfOdSyv/wZICJOC+/UhXH4aQ/R+w==", "dev": true, + "peer": true, "dependencies": { "@babel/runtime": "^7.15.4", "is-retry-allowed": "^2.2.0" @@ -16534,7 +16617,8 @@ "node_modules/b4a": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", - "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==" + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "peer": true }, "node_modules/babel-jest": { "version": "29.7.0", @@ -16872,13 +16956,15 @@ "version": "2.5.4", "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", - "optional": true + "optional": true, + "peer": true }, "node_modules/bare-fs": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.5.tgz", "integrity": "sha512-1zccWBMypln0jEE05LzZt+V/8y8AQsQQqxtklqaIyg5nu6OAYFhZxPXinJTSG+kU5qyNmeLgcn9AW7eHiCHVLA==", "optional": true, + "peer": true, "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", @@ -16901,6 +16987,7 @@ "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz", "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==", "optional": true, + "peer": true, "engines": { "bare": ">=1.14.0" } @@ -16910,6 +16997,7 @@ "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", "optional": true, + "peer": true, "dependencies": { "bare-os": "^3.0.1" } @@ -16919,6 +17007,7 @@ "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", "optional": true, + "peer": true, "dependencies": { "streamx": "^2.21.0" }, @@ -17002,6 +17091,7 @@ "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" } @@ -17293,7 +17383,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -17586,7 +17675,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/callsites": { "version": "3.1.0", @@ -17639,6 +17729,7 @@ "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", "dev": true, + "peer": true, "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", @@ -17657,6 +17748,7 @@ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", "dev": true, + "peer": true, "engines": { "node": ">=4" } @@ -17706,6 +17798,7 @@ "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", "dev": true, + "peer": true, "engines": { "node": "*" } @@ -17715,6 +17808,7 @@ "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "dev": true, + "peer": true, "dependencies": { "get-func-name": "^2.0.2" }, @@ -17727,7 +17821,6 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "devOptional": true, - "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -17760,6 +17853,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-5.1.0.tgz", "integrity": "sha512-9MSRhWRVoRPDG0TgzkHrshFSJJNZzfY5UFqUMuksg7zL1yoZIZ3jLB0YAgHclbiAxPI86pBnwDX1tbzoiV8aFw==", + "peer": true, "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" @@ -17854,6 +17948,7 @@ "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", "dev": true, + "peer": true, "dependencies": { "string-width": "^4.2.0" }, @@ -17868,13 +17963,15 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "peer": true }, "node_modules/cli-table3/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, + "peer": true, "engines": { "node": ">=8" } @@ -17884,6 +17981,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "peer": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -18138,6 +18236,7 @@ "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.5.tgz", "integrity": "sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==", "dev": true, + "peer": true, "dependencies": { "array-timsort": "^1.0.3", "core-util-is": "^1.0.3", @@ -18755,6 +18854,7 @@ "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", "dev": true, + "peer": true, "engines": { "node": "*" } @@ -18763,7 +18863,8 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/cryptr/-/cryptr-6.3.0.tgz", "integrity": "sha512-TA4byAuorT8qooU9H8YJhBwnqD151i1rcauHfJ3Divg6HmukHB2AYMp0hmjv2873J2alr4t15QqC7zAnWFrtfQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/css-loader": { "version": "6.11.0", @@ -18868,6 +18969,16 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/css-vendor": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz", + "integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.3", + "is-in-browser": "^1.0.2" + } + }, "node_modules/css-what": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", @@ -18942,6 +19053,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "peer": true, "engines": { "node": ">= 14" } @@ -19088,6 +19200,7 @@ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", "dev": true, + "peer": true, "dependencies": { "type-detect": "^4.0.0" }, @@ -19225,6 +19338,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "peer": true, "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", @@ -19309,6 +19423,7 @@ "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.6.1.tgz", "integrity": "sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q==", "dev": true, + "peer": true, "dependencies": { "address": "^1.0.1", "debug": "4" @@ -19326,6 +19441,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dev": true, + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -20406,7 +20522,6 @@ "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", "dev": true, - "peer": true, "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" @@ -20512,6 +20627,7 @@ "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", "dev": true, "optional": true, + "peer": true, "dependencies": { "prr": "~1.0.1" }, @@ -20710,7 +20826,8 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", - "dev": true + "dev": true, + "peer": true }, "node_modules/esbuild": { "version": "0.25.4", @@ -20831,7 +20948,6 @@ "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, - "peer": true, "dependencies": { "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.4.3", @@ -21687,7 +21803,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -21801,7 +21916,6 @@ "version": "1.18.2", "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==", - "peer": true, "dependencies": { "cookie": "0.7.2", "cookie-signature": "1.0.7", @@ -21952,7 +22066,8 @@ "node_modules/fast-fifo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "peer": true }, "node_modules/fast-glob": { "version": "3.3.3", @@ -21997,7 +22112,8 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true + "dev": true, + "peer": true }, "node_modules/fast-uri": { "version": "3.0.6", @@ -22825,6 +22941,7 @@ "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, + "peer": true, "engines": { "node": "*" } @@ -22988,6 +23105,7 @@ "version": "6.0.4", "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==", + "peer": true, "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", @@ -23001,6 +23119,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -23382,6 +23501,7 @@ "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz", "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==", "dev": true, + "peer": true, "engines": { "node": ">=8" } @@ -23530,7 +23650,6 @@ "integrity": "sha512-ooiZW1Xy8rQ4oELQ++otI2T9DsKpV0M6c6cO6JGx4RTfav9poFFLlet9UMXHZnoM1yG0HWGlQLswBGX3RZmHtg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -23944,7 +24063,8 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.5.tgz", "integrity": "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==", - "dev": true + "dev": true, + "peer": true }, "node_modules/http2-wrapper": { "version": "1.0.3", @@ -24017,6 +24137,12 @@ "node": ">=10.18" } }, + "node_modules/hyphenate-style-name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", + "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==", + "license": "BSD-3-Clause" + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -24117,6 +24243,7 @@ "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", "dev": true, "optional": true, + "peer": true, "bin": { "image-size": "bin/image-size.js" }, @@ -24490,7 +24617,8 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true + "dev": true, + "peer": true }, "node_modules/is-bun-module": { "version": "2.0.0", @@ -24648,6 +24776,12 @@ "node": ">=0.10.0" } }, + "node_modules/is-in-browser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", + "integrity": "sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g==", + "license": "MIT" + }, "node_modules/is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", @@ -24810,6 +24944,7 @@ "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", "dev": true, + "peer": true, "engines": { "node": ">=10" }, @@ -25174,8 +25309,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.0.1.tgz", "integrity": "sha512-w+JDABxQCkxbGGxg+a2hUVZyqUS2JKngvIyLGu/xiw2ZwgsoSB0iiecLQsQORSeaKQ6iGrCyWG86RfNDuoA7Lg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/jasmine-spec-reporter": { "version": "5.0.2", @@ -25190,7 +25324,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -25492,7 +25625,6 @@ "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", "dev": true, - "peer": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", @@ -26037,7 +26169,6 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -26060,7 +26191,8 @@ "version": "3.7.7", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/js-tokens": { "version": "4.0.0", @@ -26308,6 +26440,7 @@ "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.5.0.tgz", "integrity": "sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==", "dev": true, + "peer": true, "engines": { "node": "*" } @@ -26333,6 +26466,96 @@ "npm": ">=6" } }, + "node_modules/jss": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss/-/jss-10.10.0.tgz", + "integrity": "sha512-cqsOTS7jqPsPMjtKYDUpdFC0AbhYFLTcuGRqymgmdJIeQ8cH7+AgX7YSgQy79wXloZq2VvATYxUOUQEvS1V/Zw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "csstype": "^3.0.2", + "is-in-browser": "^1.1.3", + "tiny-warning": "^1.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/jss" + } + }, + "node_modules/jss-plugin-camel-case": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.10.0.tgz", + "integrity": "sha512-z+HETfj5IYgFxh1wJnUAU8jByI48ED+v0fuTuhKrPR+pRBYS2EDwbusU8aFOpCdYhtRc9zhN+PJ7iNE8pAWyPw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "hyphenate-style-name": "^1.0.3", + "jss": "10.10.0" + } + }, + "node_modules/jss-plugin-default-unit": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.10.0.tgz", + "integrity": "sha512-SvpajxIECi4JDUbGLefvNckmI+c2VWmP43qnEy/0eiwzRUsafg5DVSIWSzZe4d2vFX1u9nRDP46WCFV/PXVBGQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0" + } + }, + "node_modules/jss-plugin-global": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.10.0.tgz", + "integrity": "sha512-icXEYbMufiNuWfuazLeN+BNJO16Ge88OcXU5ZDC2vLqElmMybA31Wi7lZ3lf+vgufRocvPj8443irhYRgWxP+A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0" + } + }, + "node_modules/jss-plugin-nested": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.10.0.tgz", + "integrity": "sha512-9R4JHxxGgiZhurDo3q7LdIiDEgtA1bTGzAbhSPyIOWb7ZubrjQe8acwhEQ6OEKydzpl8XHMtTnEwHXCARLYqYA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0", + "tiny-warning": "^1.0.2" + } + }, + "node_modules/jss-plugin-props-sort": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.10.0.tgz", + "integrity": "sha512-5VNJvQJbnq/vRfje6uZLe/FyaOpzP/IH1LP+0fr88QamVrGJa0hpRRyAa0ea4U/3LcorJfBFVyC4yN2QC73lJg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0" + } + }, + "node_modules/jss-plugin-rule-value-function": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.10.0.tgz", + "integrity": "sha512-uEFJFgaCtkXeIPgki8ICw3Y7VMkL9GEan6SqmT9tqpwM+/t+hxfMUdU4wQ0MtOiMNWhwnckBV0IebrKcZM9C0g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0", + "tiny-warning": "^1.0.2" + } + }, + "node_modules/jss-plugin-vendor-prefixer": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.10.0.tgz", + "integrity": "sha512-UY/41WumgjW8r1qMCO8l1ARg7NHnfRVWRhZ2E2m0DMYsr2DD91qIXLyNhiX83hHswR7Wm4D+oDYNC1zWCJWtqg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "css-vendor": "^2.0.8", + "jss": "10.10.0" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -26415,7 +26638,6 @@ "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@colors/colors": "1.5.0", "body-parser": "^1.19.0", @@ -26845,6 +27067,7 @@ "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", "dev": true, + "peer": true, "dependencies": { "graceful-fs": "^4.1.9" } @@ -26881,7 +27104,6 @@ "resolved": "https://registry.npmjs.org/less/-/less-4.2.2.tgz", "integrity": "sha512-tkuLHQlvWUTeQ3doAqnHbNn8T6WX1KA8yvbKG9x4VtKtIjHsVKQZCH11zRgAfbDAXC2UNIg/K9BYAAcEzUIrNg==", "dev": true, - "peer": true, "dependencies": { "copy-anything": "^2.0.1", "parse-node-version": "^1.0.1", @@ -26935,6 +27157,7 @@ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, "optional": true, + "peer": true, "bin": { "mime": "cli.js" }, @@ -26948,6 +27171,7 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -27478,6 +27702,7 @@ "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", "dev": true, + "peer": true, "dependencies": { "get-func-name": "^2.0.1" } @@ -27544,6 +27769,7 @@ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -27747,6 +27973,7 @@ "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", "dev": true, + "peer": true, "dependencies": { "charenc": "0.0.2", "crypt": "0.0.2", @@ -28069,7 +28296,8 @@ "node_modules/mitt": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", - "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "peer": true }, "node_modules/mkcert": { "version": "3.2.0", @@ -28300,6 +28528,7 @@ "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", "dev": true, + "peer": true, "bin": { "mustache": "bin/mustache" } @@ -28367,6 +28596,7 @@ "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", "dev": true, "optional": true, + "peer": true, "dependencies": { "iconv-lite": "^0.6.3", "sax": "^1.2.4" @@ -28384,6 +28614,7 @@ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "optional": true, + "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -28408,6 +28639,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "peer": true, "engines": { "node": ">= 0.4.0" } @@ -28416,7 +28648,8 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/neverthrow/-/neverthrow-3.2.0.tgz", "integrity": "sha512-AINA32QbYO83L+3CBI6I5lH4LpBSlLwWteJ+uI25s4AQy6g/xz3RZuedmuNo91lLw2rY+AbPEPQdxd7mg1rXoQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/next": { "version": "15.5.12", @@ -28582,6 +28815,7 @@ "resolved": "https://registry.npmjs.org/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz", "integrity": "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==", "dev": true, + "peer": true, "dependencies": { "http2-client": "^1.2.5" }, @@ -29024,13 +29258,15 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz", "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/node-readfiles": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/node-readfiles/-/node-readfiles-0.2.0.tgz", "integrity": "sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==", "dev": true, + "peer": true, "dependencies": { "es6-promise": "^3.2.1" } @@ -29814,6 +30050,7 @@ "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz", "integrity": "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==", "dev": true, + "peer": true, "dependencies": { "fast-safe-stringify": "^2.0.7" } @@ -29823,6 +30060,7 @@ "resolved": "https://registry.npmjs.org/oas-linter/-/oas-linter-3.2.2.tgz", "integrity": "sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ==", "dev": true, + "peer": true, "dependencies": { "@exodus/schemasafe": "^1.0.0-rc.2", "should": "^13.2.1", @@ -29837,6 +30075,7 @@ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "dev": true, + "peer": true, "engines": { "node": ">= 6" } @@ -29846,6 +30085,7 @@ "resolved": "https://registry.npmjs.org/oas-resolver/-/oas-resolver-2.5.6.tgz", "integrity": "sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ==", "dev": true, + "peer": true, "dependencies": { "node-fetch-h2": "^2.3.0", "oas-kit-common": "^1.0.8", @@ -29865,6 +30105,7 @@ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "dev": true, + "peer": true, "engines": { "node": ">= 6" } @@ -29874,6 +30115,7 @@ "resolved": "https://registry.npmjs.org/oas-schema-walker/-/oas-schema-walker-1.1.5.tgz", "integrity": "sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ==", "dev": true, + "peer": true, "funding": { "url": "https://github.com/Mermade/oas-kit?sponsor=1" } @@ -29883,6 +30125,7 @@ "resolved": "https://registry.npmjs.org/oas-validator/-/oas-validator-5.0.8.tgz", "integrity": "sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw==", "dev": true, + "peer": true, "dependencies": { "call-me-maybe": "^1.0.1", "oas-kit-common": "^1.0.8", @@ -29902,6 +30145,7 @@ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "dev": true, + "peer": true, "engines": { "node": ">= 6" } @@ -30282,7 +30526,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.1.tgz", "integrity": "sha512-qwxv6dq682yVvgKKp2qWwLgRbscDAYktPptK4JPojCwwi3R9cwrvIxS4lvBpzmcqzR4bdn54Z0IG1uHFskW4dA==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.33.1", "@typescript-eslint/types": "8.33.1", @@ -30492,7 +30735,6 @@ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -30615,7 +30857,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", @@ -30889,7 +31130,6 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -30905,7 +31145,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -30962,6 +31201,7 @@ "resolved": "https://registry.npmjs.org/office-addin-manifest-converter/-/office-addin-manifest-converter-0.4.1.tgz", "integrity": "sha512-2eOdCCYJ5bhCe2p9KKETdg1UNshsKaT0lDU/jNopAg3t7zC1WxwvofTSO/+4Log5L4Re+wUdV8MqrQikZBa7+Q==", "dev": true, + "peer": true, "dependencies": { "@xmldom/xmldom": "^0.8.5", "commander": "^9.0.0", @@ -30976,6 +31216,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", "dev": true, + "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -31127,6 +31368,7 @@ "resolved": "https://registry.npmjs.org/office-addin-project/-/office-addin-project-0.8.6.tgz", "integrity": "sha512-S93brHVDaMRZIIEibK12m6eTItoKqdy2Ep51qNhU7RaZTjH7n/C66AzdDai06UJb3I3AXfXHnKv/AJL2voGGWA==", "dev": true, + "peer": true, "dependencies": { "adm-zip": "0.5.12", "commander": "^6.2.1", @@ -31146,6 +31388,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", "dev": true, + "peer": true, "engines": { "node": ">= 6" } @@ -31155,6 +31398,7 @@ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, + "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -31169,6 +31413,7 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, + "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -31178,6 +31423,7 @@ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, + "peer": true, "engines": { "node": ">= 4.0.0" } @@ -31550,6 +31796,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "peer": true, "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", @@ -31568,6 +31815,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -31584,6 +31832,7 @@ "version": "8.0.5", "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "peer": true, "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", @@ -31597,6 +31846,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "peer": true, "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" @@ -32114,6 +32364,7 @@ "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true, + "peer": true, "engines": { "node": "*" } @@ -32297,7 +32548,6 @@ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", "license": "Apache-2.0", - "peer": true, "bin": { "playwright-core": "cli.js" }, @@ -32334,6 +32584,12 @@ "node": ">=10.4.0" } }, + "node_modules/popper.js": { + "version": "1.16.1-lts", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz", + "integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==", + "license": "MIT" + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -32363,7 +32619,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -32630,7 +32885,6 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==", "dev": true, - "peer": true, "bin": { "prettier": "bin-prettier.js" }, @@ -32668,6 +32922,7 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -32682,6 +32937,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "peer": true, "engines": { "node": ">=10" }, @@ -32693,7 +32949,8 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true + "dev": true, + "peer": true }, "node_modules/proc-log": { "version": "2.0.1", @@ -32794,6 +33051,7 @@ "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", "dev": true, + "peer": true, "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", @@ -32804,7 +33062,8 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/protocols": { "version": "2.0.2", @@ -32829,6 +33088,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "peer": true, "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", @@ -32847,6 +33107,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -32863,6 +33124,7 @@ "version": "7.18.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "peer": true, "engines": { "node": ">=12" } @@ -32871,6 +33133,7 @@ "version": "8.0.5", "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "peer": true, "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", @@ -32890,7 +33153,8 @@ "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", "dev": true, - "optional": true + "optional": true, + "peer": true }, "node_modules/psl": { "version": "1.15.0", @@ -32958,6 +33222,7 @@ "version": "24.10.0", "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.10.0.tgz", "integrity": "sha512-xX0QJRc8t19iAwRDsAOR38Q/Zx/W6WVzJCEhKCAwp2XMsaWqfNtQ+rBfQW9PlF+Op24d7c8Zlgq9YNmbnA7hdQ==", + "peer": true, "dependencies": { "@puppeteer/browsers": "2.10.5", "chromium-bidi": "5.1.0", @@ -32974,6 +33239,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -32990,6 +33256,7 @@ "version": "8.18.2", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -33195,7 +33462,6 @@ "version": "19.2.1", "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -33234,7 +33500,6 @@ "version": "19.2.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -33303,6 +33568,18 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react16-sample": { + "resolved": "samples/msal-react-samples/react16-sample", + "link": true + }, + "node_modules/react17-sample": { + "resolved": "samples/msal-react-samples/react17-sample", + "link": true + }, + "node_modules/react18-sample": { + "resolved": "samples/msal-react-samples/react18-sample", + "link": true + }, "node_modules/read-binary-file-arch": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", @@ -33671,6 +33948,7 @@ "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.9.tgz", "integrity": "sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==", "dev": true, + "peer": true, "funding": { "url": "https://github.com/Mermade/oas-kit?sponsor=1" } @@ -33890,6 +34168,7 @@ "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", "dev": true, + "peer": true, "engines": { "node": ">=0.10" } @@ -34397,7 +34676,6 @@ "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -34626,7 +34904,6 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -35031,6 +35308,7 @@ "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==", "dev": true, + "peer": true, "dependencies": { "should-equal": "^2.0.0", "should-format": "^3.0.3", @@ -35044,6 +35322,7 @@ "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz", "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", "dev": true, + "peer": true, "dependencies": { "should-type": "^1.4.0" } @@ -35053,6 +35332,7 @@ "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", "integrity": "sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q==", "dev": true, + "peer": true, "dependencies": { "should-type": "^1.3.0", "should-type-adaptors": "^1.0.1" @@ -35062,13 +35342,15 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", "integrity": "sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/should-type-adaptors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", "dev": true, + "peer": true, "dependencies": { "should-type": "^1.3.0", "should-util": "^1.0.0" @@ -35078,7 +35360,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz", "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==", - "dev": true + "dev": true, + "peer": true }, "node_modules/shx": { "version": "0.3.4", @@ -35820,6 +36103,7 @@ "version": "2.22.1", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", + "peer": true, "dependencies": { "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" @@ -36254,6 +36538,7 @@ "resolved": "https://registry.npmjs.org/swagger2openapi/-/swagger2openapi-7.0.8.tgz", "integrity": "sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==", "dev": true, + "peer": true, "dependencies": { "call-me-maybe": "^1.0.1", "node-fetch": "^2.6.1", @@ -36281,6 +36566,7 @@ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "dev": true, + "peer": true, "engines": { "node": ">= 6" } @@ -36602,6 +36888,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "peer": true, "dependencies": { "b4a": "^1.6.4" } @@ -36672,6 +36959,12 @@ "dev": true, "optional": true }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -36898,7 +37191,6 @@ "integrity": "sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "bs-logger": "^0.2.6", "fast-json-stable-stringify": "^2.1.0", @@ -37001,7 +37293,6 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz", "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==", "devOptional": true, - "peer": true, "dependencies": { "arg": "^4.1.0", "diff": "^4.0.1", @@ -37058,8 +37349,7 @@ "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "peer": true + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -37524,13 +37814,15 @@ "node_modules/typed-query-selector": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", - "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==" + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "peer": true }, "node_modules/typedi": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/typedi/-/typedi-0.10.0.tgz", "integrity": "sha512-v3UJF8xm68BBj6AF4oQML3ikrfK2c9EmZUyLOfShpJuItAqVBHWP/KtpGinkSsIiP6EZyyb6Z3NXyW9dgS9X1w==", - "dev": true + "dev": true, + "peer": true }, "node_modules/typedoc": { "version": "0.24.8", @@ -37582,7 +37874,6 @@ "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -37754,7 +38045,8 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/undici": { "version": "7.22.0", @@ -38206,6 +38498,7 @@ "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.10" } @@ -38228,7 +38521,6 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -38703,6 +38995,7 @@ "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-4.0.0.tgz", "integrity": "sha512-perEnXQdQOJMTDFNv+UF3h1Y0z4iSiaN9jIlb0OqIYgosPCZGYh/MCUlkFtV2668PL69lRDO32hmvL2yiidUYg==", "dev": true, + "peer": true, "engines": { "node": ">=8.0.0 || >=10.0.0" } @@ -38809,7 +39102,6 @@ "integrity": "sha512-dRXm0a2qcHPUBEzVk8uph0xWSjV/xZxenQQbLwnwP7caQCYpqG1qddwlyEkIDkYn0K8tvmcrZ+bOrzoQ3HxCDw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -39027,7 +39319,6 @@ "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.2.tgz", "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", "dev": true, - "peer": true, "dependencies": { "@types/bonjour": "^3.5.13", "@types/connect-history-api-fallback": "^1.5.4", @@ -39689,8 +39980,7 @@ "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.19.0.tgz", "integrity": "sha512-c3Cp4eOVsYY5Q839dR5IejghRPpxciGmLWWaP9g+ppfMeBChMeLa1DCA+pmX/jyDZ+zxFOmlJL/82qVdayVoGQ==", "deprecated": "This package is now deprecated. Move to @xterm/xterm instead.", - "dev": true, - "peer": true + "dev": true }, "node_modules/xterm-addon-fit": { "version": "0.5.0", @@ -39730,6 +40020,7 @@ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", "dev": true, + "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -39930,7 +40221,6 @@ "version": "3.25.75", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.75.tgz", "integrity": "sha512-OhpzAmVzabPOL6C3A3gpAifqr9MqihV/Msx3gor2b2kviCgcb+HM9SEOpMWwwNp9MRunWnhtAKUoo0AHhjyPPg==", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -39948,8 +40238,7 @@ "node_modules/zone.js": { "version": "0.15.1", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", - "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", - "peer": true + "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==" }, "regression-tests/msal-node/client-credential": { "version": "1.0.0", @@ -40129,7 +40418,6 @@ "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.2.0.tgz", "integrity": "sha512-SzlLoMT/r5wKqPicx5okCAiN5UD5+VE7x/F1G6gSJCcnBfbK5PqHPUmDnMW4jw9Ode06KZDT7ntstn6fG+Ld8w==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -40259,7 +40547,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -40334,7 +40621,6 @@ "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.2.0.tgz", "integrity": "sha512-1P0TNL1F51NC7JAaXabaAHY7Y1zBloLSZXfml1POa4a116V+y/QZfPGsxM0LwD1qSSXhSb2LNl7duTtJAP39bA==", "license": "MIT", - "peer": true, "dependencies": { "parse5": "^8.0.0", "tslib": "^2.3.0" @@ -40386,7 +40672,6 @@ "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.0.tgz", "integrity": "sha512-6zJMPi0i/XDniEgv3/t2BjuDHiOG44lgIR5PYyxqGpgJ0kqB5hku/0TuentNEi1VnBYgthnfhjek7c+lakXmhw==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -40403,7 +40688,6 @@ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.0.tgz", "integrity": "sha512-0RPkma8UVNpse/VJcXT9w6SKzTMz4J/uMGj0l9enM1frg9xrx1fwi/lLmaVV9Nr9LfqPjQdxNFFlvaBB7g/2zg==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -40417,7 +40701,6 @@ "integrity": "sha512-gZd58p0/JjgdxMX3v+LjCB6e3dBIfNVr/YzXoh55TfffdBCUQY94hl1+DFQkJ72K5EX+1zbaz03dIm30kw1bGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "7.29.0", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -40450,7 +40733,6 @@ "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.0.tgz", "integrity": "sha512-VnTbmZq3g3Q+s3nCZ8VUDMLjMezOg/bqUxAJ/DrRWCrEcTP5JO3mrNPs3FHj+qlB0T+BQP7uQv6QTzPVKybwoA==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -40476,7 +40758,6 @@ "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.0.tgz", "integrity": "sha512-NduUtPWLauH/FLayEDkLyaKAGqKzXbcfO7468LOWCXN3crhNVQyIWRQPOUcdpoJwDAGLpN85m3DhJhXNnA9c5w==", "license": "MIT", - "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "tslib": "^2.3.0" @@ -40513,7 +40794,6 @@ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.0.tgz", "integrity": "sha512-IUGukpvvT2B5Dl76qzk6rY7UIHUT9u4BhT2AwVz+5JqcX9KwQtYD17Gt7wj6bvIgCXKWG+CfN8Zd9DECOCYWjg==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -41011,7 +41291,6 @@ "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@inquirer/checkbox": "^4.3.2", "@inquirer/confirm": "^5.1.21", @@ -41395,7 +41674,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.57.tgz", "integrity": "sha512-f3T4y6VU4fVQDKVqJV4Uppy8c1p/sVvS3peyqxyWnzkqXFJLRU7Y1Bl7rMS1Qe9z0v4M6McY0Fp9yBsgHJUsWQ==", "dev": true, - "peer": true, "dependencies": { "undici-types": "~6.19.2" } @@ -41572,7 +41850,6 @@ "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "readdirp": "^5.0.0" }, @@ -41958,7 +42235,6 @@ "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cli-truncate": "^5.0.0", "colorette": "^2.0.20", @@ -42669,7 +42945,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -42906,7 +43181,6 @@ "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.2.0.tgz", "integrity": "sha512-SzlLoMT/r5wKqPicx5okCAiN5UD5+VE7x/F1G6gSJCcnBfbK5PqHPUmDnMW4jw9Ode06KZDT7ntstn6fG+Ld8w==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -43074,7 +43348,6 @@ "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.0.tgz", "integrity": "sha512-6zJMPi0i/XDniEgv3/t2BjuDHiOG44lgIR5PYyxqGpgJ0kqB5hku/0TuentNEi1VnBYgthnfhjek7c+lakXmhw==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -43091,7 +43364,6 @@ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.0.tgz", "integrity": "sha512-0RPkma8UVNpse/VJcXT9w6SKzTMz4J/uMGj0l9enM1frg9xrx1fwi/lLmaVV9Nr9LfqPjQdxNFFlvaBB7g/2zg==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -43105,7 +43377,6 @@ "integrity": "sha512-gZd58p0/JjgdxMX3v+LjCB6e3dBIfNVr/YzXoh55TfffdBCUQY94hl1+DFQkJ72K5EX+1zbaz03dIm30kw1bGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "7.29.0", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -43138,7 +43409,6 @@ "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.0.tgz", "integrity": "sha512-VnTbmZq3g3Q+s3nCZ8VUDMLjMezOg/bqUxAJ/DrRWCrEcTP5JO3mrNPs3FHj+qlB0T+BQP7uQv6QTzPVKybwoA==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -43164,7 +43434,6 @@ "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.0.tgz", "integrity": "sha512-NduUtPWLauH/FLayEDkLyaKAGqKzXbcfO7468LOWCXN3crhNVQyIWRQPOUcdpoJwDAGLpN85m3DhJhXNnA9c5w==", "license": "MIT", - "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "tslib": "^2.3.0" @@ -43201,7 +43470,6 @@ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.0.tgz", "integrity": "sha512-IUGukpvvT2B5Dl76qzk6rY7UIHUT9u4BhT2AwVz+5JqcX9KwQtYD17Gt7wj6bvIgCXKWG+CfN8Zd9DECOCYWjg==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -43697,7 +43965,6 @@ "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", "dev": true, - "peer": true, "dependencies": { "@inquirer/checkbox": "^4.3.2", "@inquirer/confirm": "^5.1.21", @@ -44257,7 +44524,6 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "dev": true, - "peer": true, "dependencies": { "readdirp": "^5.0.0" }, @@ -44584,8 +44850,7 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.5.0.tgz", "integrity": "sha512-9PMzyvhtocxb3aXJVOPqBDswdgyAeSB81QnLop4npOpbqnheaTEwPc9ZloQeVswugPManznQBjD8kWDTjlnHuw==", - "dev": true, - "peer": true + "dev": true }, "samples/msal-angular-samples/angular-modules-sample/node_modules/json-parse-even-better-errors": { "version": "5.0.0", @@ -44602,7 +44867,6 @@ "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==", "dev": true, - "peer": true, "dependencies": { "jasmine-core": "^4.1.0" }, @@ -44629,7 +44893,6 @@ "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", "dev": true, - "peer": true, "dependencies": { "cli-truncate": "^5.0.0", "colorette": "^2.0.20", @@ -45328,7 +45591,6 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -45379,7 +45641,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -45636,7 +45897,6 @@ "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.2.0.tgz", "integrity": "sha512-SzlLoMT/r5wKqPicx5okCAiN5UD5+VE7x/F1G6gSJCcnBfbK5PqHPUmDnMW4jw9Ode06KZDT7ntstn6fG+Ld8w==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -45804,7 +46064,6 @@ "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.0.tgz", "integrity": "sha512-6zJMPi0i/XDniEgv3/t2BjuDHiOG44lgIR5PYyxqGpgJ0kqB5hku/0TuentNEi1VnBYgthnfhjek7c+lakXmhw==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -45821,7 +46080,6 @@ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.0.tgz", "integrity": "sha512-0RPkma8UVNpse/VJcXT9w6SKzTMz4J/uMGj0l9enM1frg9xrx1fwi/lLmaVV9Nr9LfqPjQdxNFFlvaBB7g/2zg==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -45835,7 +46093,6 @@ "integrity": "sha512-gZd58p0/JjgdxMX3v+LjCB6e3dBIfNVr/YzXoh55TfffdBCUQY94hl1+DFQkJ72K5EX+1zbaz03dIm30kw1bGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "7.29.0", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -45868,7 +46125,6 @@ "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.0.tgz", "integrity": "sha512-VnTbmZq3g3Q+s3nCZ8VUDMLjMezOg/bqUxAJ/DrRWCrEcTP5JO3mrNPs3FHj+qlB0T+BQP7uQv6QTzPVKybwoA==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -45894,7 +46150,6 @@ "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.0.tgz", "integrity": "sha512-NduUtPWLauH/FLayEDkLyaKAGqKzXbcfO7468LOWCXN3crhNVQyIWRQPOUcdpoJwDAGLpN85m3DhJhXNnA9c5w==", "license": "MIT", - "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "tslib": "^2.3.0" @@ -45931,7 +46186,6 @@ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.0.tgz", "integrity": "sha512-IUGukpvvT2B5Dl76qzk6rY7UIHUT9u4BhT2AwVz+5JqcX9KwQtYD17Gt7wj6bvIgCXKWG+CfN8Zd9DECOCYWjg==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -46429,7 +46683,6 @@ "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@inquirer/checkbox": "^4.3.2", "@inquirer/confirm": "^5.1.21", @@ -47012,7 +47265,6 @@ "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "readdirp": "^5.0.0" }, @@ -47389,7 +47641,6 @@ "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cli-truncate": "^5.0.0", "colorette": "^2.0.20", @@ -48108,7 +48359,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -48159,7 +48409,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -48364,7 +48613,6 @@ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -48545,7 +48793,6 @@ "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -50299,7 +50546,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.110.tgz", "integrity": "sha512-WW2o4gTmREtSnqKty9nhqF/vA0GKd0V/rbC0OyjSk9Bz6bzlsXKT+i7WDdS/a0z74rfT2PO4dArVCSnapNLA5Q==", "dev": true, - "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -50807,7 +51053,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.110.tgz", "integrity": "sha512-WW2o4gTmREtSnqKty9nhqF/vA0GKd0V/rbC0OyjSk9Bz6bzlsXKT+i7WDdS/a0z74rfT2PO4dArVCSnapNLA5Q==", "dev": true, - "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -51413,7 +51658,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -51560,7 +51804,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.110.tgz", "integrity": "sha512-WW2o4gTmREtSnqKty9nhqF/vA0GKd0V/rbC0OyjSk9Bz6bzlsXKT+i7WDdS/a0z74rfT2PO4dArVCSnapNLA5Q==", "dev": true, - "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -51966,6 +52209,382 @@ "vite": "^5.4.21" } }, + "samples/msal-react-samples/react16-sample": { + "version": "0.1.0", + "dependencies": { + "@azure/msal-browser": "^5.0.0", + "@azure/msal-react": "^5.0.0", + "@material-ui/core": "^4.12.4", + "@material-ui/icons": "^4.11.3", + "react": "^16.8.0", + "react-dom": "^16.8.0", + "react-router-dom": "^6.7.0" + }, + "devDependencies": { + "@types/jest": "^29.5.0", + "@vitejs/plugin-react": "^4.3.3", + "e2e-test-utils": "file:../../e2eTestUtils", + "env-cmd": "^10.1.0", + "jest": "^29.5.0", + "jest-junit": "^16.0.0", + "ts-jest": "^29.1.0", + "vite": "^5.4.21" + } + }, + "samples/msal-react-samples/react16-sample/node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "license": "MIT" + }, + "samples/msal-react-samples/react16-sample/node_modules/@material-ui/core": { + "version": "4.12.4", + "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.12.4.tgz", + "integrity": "sha512-tr7xekNlM9LjA6pagJmL8QCgZXaubWUwkJnoYcMKd4gw/t4XiyvnTkjdGrUVicyB2BsdaAv1tvow45bPM4sSwQ==", + "deprecated": "Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5.", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.4.4", + "@material-ui/styles": "^4.11.5", + "@material-ui/system": "^4.12.2", + "@material-ui/types": "5.1.0", + "@material-ui/utils": "^4.11.3", + "@types/react-transition-group": "^4.2.0", + "clsx": "^1.0.4", + "hoist-non-react-statics": "^3.3.2", + "popper.js": "1.16.1-lts", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0", + "react-transition-group": "^4.4.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/material-ui" + }, + "peerDependencies": { + "@types/react": "^16.8.6 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "samples/msal-react-samples/react16-sample/node_modules/@material-ui/icons": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.3.tgz", + "integrity": "sha512-IKHlyx6LDh8n19vzwH5RtHIOHl9Tu90aAAxcbWME6kp4dmvODM3UvOHJeMIDzUbd4muuJKHmlNoBN+mDY4XkBA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.4.4" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "@material-ui/core": "^4.0.0", + "@types/react": "^16.8.6 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "samples/msal-react-samples/react16-sample/node_modules/@material-ui/styles": { + "version": "4.11.5", + "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.5.tgz", + "integrity": "sha512-o/41ot5JJiUsIETME9wVLAJrmIWL3j0R0Bj2kCOLbSfqEkKf0fmaPt+5vtblUh5eXr2S+J/8J3DaCb10+CzPGA==", + "deprecated": "Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5.", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.4.4", + "@emotion/hash": "^0.8.0", + "@material-ui/types": "5.1.0", + "@material-ui/utils": "^4.11.3", + "clsx": "^1.0.4", + "csstype": "^2.5.2", + "hoist-non-react-statics": "^3.3.2", + "jss": "^10.5.1", + "jss-plugin-camel-case": "^10.5.1", + "jss-plugin-default-unit": "^10.5.1", + "jss-plugin-global": "^10.5.1", + "jss-plugin-nested": "^10.5.1", + "jss-plugin-props-sort": "^10.5.1", + "jss-plugin-rule-value-function": "^10.5.1", + "jss-plugin-vendor-prefixer": "^10.5.1", + "prop-types": "^15.7.2" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/material-ui" + }, + "peerDependencies": { + "@types/react": "^16.8.6 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "samples/msal-react-samples/react16-sample/node_modules/@material-ui/styles/node_modules/csstype": { + "version": "2.6.21", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", + "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==", + "license": "MIT" + }, + "samples/msal-react-samples/react16-sample/node_modules/@material-ui/system": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.12.2.tgz", + "integrity": "sha512-6CSKu2MtmiJgcCGf6nBQpM8fLkuB9F55EKfbdTC80NND5wpTmKzwdhLYLH3zL4cLlK0gVaaltW7/wMuyTnN0Lw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.4.4", + "@material-ui/utils": "^4.11.3", + "csstype": "^2.5.2", + "prop-types": "^15.7.2" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/material-ui" + }, + "peerDependencies": { + "@types/react": "^16.8.6 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "samples/msal-react-samples/react16-sample/node_modules/@material-ui/system/node_modules/csstype": { + "version": "2.6.21", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", + "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==", + "license": "MIT" + }, + "samples/msal-react-samples/react16-sample/node_modules/@material-ui/utils": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.3.tgz", + "integrity": "sha512-ZuQPV4rBK/V1j2dIkSSEcH5uT6AaHuKWFfotADHsC0wVL1NLd2WkFCm4ZZbX33iO4ydl6V0GPngKm8HZQ2oujg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.4.4", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + } + }, + "samples/msal-react-samples/react16-sample/node_modules/@types/react": { + "version": "17.0.91", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.91.tgz", + "integrity": "sha512-xauZca6qMeCU3Moy0KxCM9jtf1vyk6qRYK39Ryf3afUqwgNUjRIGoDdS9BcGWgAMGSg1hvP4XcmlYrM66PtqeA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "^0.16", + "csstype": "^3.2.2" + } + }, + "samples/msal-react-samples/react16-sample/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "samples/msal-react-samples/react16-sample/node_modules/react": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", + "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "samples/msal-react-samples/react16-sample/node_modules/react-dom": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", + "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.19.1" + }, + "peerDependencies": { + "react": "^16.14.0" + } + }, + "samples/msal-react-samples/react16-sample/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "license": "MIT" + }, + "samples/msal-react-samples/react16-sample/node_modules/scheduler": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", + "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "samples/msal-react-samples/react17-sample": { + "version": "0.1.0", + "dependencies": { + "@azure/msal-browser": "^5.0.0", + "@azure/msal-react": "^5.0.0", + "@emotion/react": "^11.10.5", + "@emotion/styled": "^11.10.5", + "@mui/icons-material": "^5.10.16", + "@mui/material": "^5.10.17", + "react": "^17.0.0", + "react-dom": "^17.0.0", + "react-router-dom": "^6.7.0" + }, + "devDependencies": { + "@types/jest": "^29.5.0", + "@vitejs/plugin-react": "^4.3.3", + "e2e-test-utils": "file:../../e2eTestUtils", + "env-cmd": "^10.1.0", + "jest": "^29.5.0", + "jest-junit": "^16.0.0", + "ts-jest": "^29.1.0", + "vite": "^5.4.21" + } + }, + "samples/msal-react-samples/react17-sample/node_modules/react": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "samples/msal-react-samples/react17-sample/node_modules/react-dom": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", + "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "scheduler": "^0.20.2" + }, + "peerDependencies": { + "react": "17.0.2" + } + }, + "samples/msal-react-samples/react17-sample/node_modules/scheduler": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", + "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "samples/msal-react-samples/react18-sample": { + "version": "0.1.0", + "dependencies": { + "@azure/msal-browser": "^5.0.0", + "@azure/msal-react": "^5.0.0", + "@emotion/react": "^11.10.5", + "@emotion/styled": "^11.10.5", + "@mui/icons-material": "^5.10.16", + "@mui/material": "^5.10.17", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-router-dom": "^6.7.0" + }, + "devDependencies": { + "@types/jest": "^29.5.0", + "@vitejs/plugin-react": "^4.3.3", + "e2e-test-utils": "file:../../e2eTestUtils", + "env-cmd": "^10.1.0", + "jest": "^29.5.0", + "jest-junit": "^16.0.0", + "ts-jest": "^29.1.0", + "vite": "^5.4.21" + } + }, + "samples/msal-react-samples/react18-sample/node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "samples/msal-react-samples/react18-sample/node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "samples/msal-react-samples/react18-sample/node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "samples/msal-react-samples/typescript-sample": { "version": "0.1.0", "dependencies": { diff --git a/samples/msal-react-samples/react16-sample/.env.development b/samples/msal-react-samples/react16-sample/.env.development new file mode 100644 index 0000000000..b8e1347305 --- /dev/null +++ b/samples/msal-react-samples/react16-sample/.env.development @@ -0,0 +1,5 @@ +BROWSER=none + +VITE_CLIENT_ID=ENTER_CLIENT_ID_HERE +VITE_AUTHORITY=https://login.microsoftonline.com/ENTER_TENANT_ID_HERE +VITE_REDIRECT_URI=/redirect diff --git a/samples/msal-react-samples/react16-sample/.env.e2e b/samples/msal-react-samples/react16-sample/.env.e2e new file mode 100644 index 0000000000..7b1d8d02ec --- /dev/null +++ b/samples/msal-react-samples/react16-sample/.env.e2e @@ -0,0 +1,5 @@ +BROWSER=none + +VITE_CLIENT_ID=b5c2e510-4a17-4feb-b219-e55aa5b74144 +VITE_AUTHORITY=https://login.microsoftonline.com/common +VITE_REDIRECT_URI=/redirect diff --git a/samples/msal-react-samples/react16-sample/.gitignore b/samples/msal-react-samples/react16-sample/.gitignore new file mode 100644 index 0000000000..227a007b62 --- /dev/null +++ b/samples/msal-react-samples/react16-sample/.gitignore @@ -0,0 +1,20 @@ +# dependencies +/node_modules + +# testing +/coverage + +# production +/build +/dist + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/samples/msal-react-samples/react16-sample/.npmrc b/samples/msal-react-samples/react16-sample/.npmrc new file mode 100644 index 0000000000..43c97e719a --- /dev/null +++ b/samples/msal-react-samples/react16-sample/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/samples/msal-react-samples/react16-sample/README.md b/samples/msal-react-samples/react16-sample/README.md new file mode 100644 index 0000000000..81879d16f4 --- /dev/null +++ b/samples/msal-react-samples/react16-sample/README.md @@ -0,0 +1,87 @@ +# MSAL.js for React Sample - React 16 Compatibility + +## About this sample + +This sample is derived from the [react-router-sample](../react-router-sample) and demonstrates MSAL React running with **React 16** (16.8+). It uses `ReactDOM.render` (React 16 API) for rendering and MUI v4 (`@material-ui/core`) for UI components, since MUI v5 (`@mui/material`) does not support React 16. + +## Notable files and what they demonstrate + +1. `./src/App.jsx` - Shows implementation of `MsalProvider`, all children will have access to `@azure/msal-react` context, hooks and components. +1. `./src/index.jsx` - Shows initialization of the `PublicClientApplication` that is passed to `App.jsx` +1. `./src/pages/Home.jsx` - Homepage, shows how to conditionally render content using `AuthenticatedTemplate` and `UnauthenticatedTemplate` depending on whether or not a user is signed in. +1. `./src/pages/Profile.jsx` - Example of a protected route using `MsalAuthenticationTemplate`. If a user is not yet signed in, signin will be invoked automatically. If a user is signed in it will acquire an access token and make a call to MS Graph to fetch user profile data. +1. `./src/authConfig.js` - Configuration options for `PublicClientApplication` and token requests. +1. `./src/ui-components/SignInSignOutButton.jsx` - Example of how to conditionally render a Sign In or Sign Out button using the `useIsAuthenticated` hook. +1. `./src/ui-components/SignInButton.jsx` - Example of how to get the `PublicClientApplication` instance using the `useMsal` hook and invoking a login function. +1. `./src/ui-components/SignOutButton.jsx` - Example of how to get the `PublicClientApplication` instance using the `useMsal` hook and invoking a logout function. +1. `./src/utils/MsGraphApiCall.js` - Example of how to call the MS Graph API with an access token. +1. `./src/utils/NavigationClient.js` - Example implementation of `INavigationClient` which can be used to override the default navigation functions MSAL.js uses + +### (Optional) MSAL React and class components + +For a demonstration of how to use MSAL React with class components, see: `./src/pages/ProfileWithMsal.jsx` and `./src/pages/ProfileRawContext.jsx`. + +*After* you initialize `MsalProvider`, there are 3 approaches you can take to protect your class components with MSAL React: + +1. Wrap each component that you want to protect with `withMsal` higher-order component (HOC) (e.g. [Profile](./src/pages/ProfileWithMsal.jsx#Profile)). +1. Consume the raw context directly (e.g. [ProfileContent](./src/pages/ProfileRawContext.jsx#ProfileContent)). +1. Pass context down from a parent component that has access to the `msalContext` via one of the other means above (e.g. [ProfileContent](./src/pages/ProfileWithMsal.jsx#ProfileContent)). + +For more information, visit: + +- [Docs: Class Components](../../../lib/msal-react/docs/class-components.md) +- [MSAL React FAQ](../../../lib/msal-react/FAQ.md) + +## How to run the sample + +### Pre-requisites + +- Ensure [all pre-requisites](../../../lib/msal-react/README.md#prerequisites) have been completed to run `@azure/msal-react`. +- Install node.js if needed (). + +### Configure the application + +- Open `./.env.development` in an editor. +- Replace `ENTER_CLIENT_ID_HERE` with the Application (client) ID from the portal registration, or use the currently configured lab registration. +- Replace `ENTER_TENANT_ID_HERE` with the tenant ID from the portal registration, or use the currently configured lab registration. + - Optionally, you may replace any of the other parameters, or you can remove them and use the default values. + +These parameters are used in `./src/authConfig.js` to configure MSAL. + +#### Install npm dependencies for sample + +```bash +# Install dev dependencies for msal-react and msal-browser from root of repo +npm install + +# Change directory to sample directory +cd samples/msal-react-samples/react16-sample + +# Build packages locally +npm run build:package +``` + +#### Running the sample development server + +1. In a command prompt, run `npm start`. +1. Open [http://localhost:3000](http://localhost:3000) to view it in the browser. +1. Open [http://localhost:3000/profile](http://localhost:3000/profile) to see an example of a protected route. If you are not yet signed in, signin will be invoked automatically. + +The page will reload if you make edits. +You will also see any lint errors in the console. + +- In the web page, click on the "Login" button and select either `Sign in using Popup` or `Sign in using Redirect` to begin the auth flow. + +#### Running the sample production server + +1. In a command prompt, run `npm run build`. +1. Next run `npx vite preview --port 3000 --strictPort`. +1. Open [http://localhost:3000](http://localhost:3000) to view it in the browser. +1. Open [http://localhost:3000/profile](http://localhost:3000/profile) to see an example of a protected route. If you are not yet signed in, signin will be invoked automatically. + +#### Learn more about the 3rd-party libraries used to create this sample + +- [React documentation](https://reactjs.org/). +- [Vite documentation](https://vite.dev/guide/) +- [React Router documentation](https://reactrouter.com/web/guides/quick-start) +- [Material-UI documentation](https://material-ui.com/getting-started/installation/) diff --git a/samples/msal-react-samples/react16-sample/index.html b/samples/msal-react-samples/react16-sample/index.html new file mode 100644 index 0000000000..884d5dc1bd --- /dev/null +++ b/samples/msal-react-samples/react16-sample/index.html @@ -0,0 +1,19 @@ + + + + + + + + + MSAL-React Sample + + + +
+ + + diff --git a/samples/msal-react-samples/react16-sample/jest.config.cjs b/samples/msal-react-samples/react16-sample/jest.config.cjs new file mode 100644 index 0000000000..b01f168f49 --- /dev/null +++ b/samples/msal-react-samples/react16-sample/jest.config.cjs @@ -0,0 +1,8 @@ +module.exports = { + displayName: "React 16 Compat", + globals: { + __PORT__: 3000, + __STARTCMD__: "env-cmd -f .env.e2e npm start", + }, + preset: "../../e2eTestUtils/jest-puppeteer-utils/jest-preset.js", +}; diff --git a/samples/msal-react-samples/react16-sample/package.json b/samples/msal-react-samples/react16-sample/package.json new file mode 100644 index 0000000000..a95d57c86c --- /dev/null +++ b/samples/msal-react-samples/react16-sample/package.json @@ -0,0 +1,48 @@ +{ + "name": "react16-sample", + "version": "0.1.0", + "private": true, + "type": "module", + "dependencies": { + "@azure/msal-browser": "^5.0.0", + "@azure/msal-react": "^5.0.0", + "@material-ui/core": "^4.12.4", + "@material-ui/icons": "^4.11.3", + "react": "^16.8.0", + "react-dom": "^16.8.0", + "react-router-dom": "^6.7.0" + }, + "scripts": { + "start": "vite", + "test:e2e": "jest", + "build": "vite build", + "build:package": "cd ../../../ && npm run build:all --workspace=lib/msal-react" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@types/jest": "^29.5.0", + "@vitejs/plugin-react": "^4.3.3", + "e2e-test-utils": "file:../../e2eTestUtils", + "env-cmd": "^10.1.0", + "jest": "^29.5.0", + "jest-junit": "^16.0.0", + "ts-jest": "^29.1.0", + "vite": "^5.4.21" + }, + "jest-junit": { + "suiteNameTemplate": "React 16 Compat Tests", + "outputDirectory": ".", + "outputName": "test-results.xml" + } +} \ No newline at end of file diff --git a/samples/msal-react-samples/react16-sample/public/favicon.ico b/samples/msal-react-samples/react16-sample/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/samples/msal-react-samples/react16-sample/public/favicon.ico differ diff --git a/samples/msal-react-samples/react16-sample/src/App.jsx b/samples/msal-react-samples/react16-sample/src/App.jsx new file mode 100644 index 0000000000..5e2b021ce3 --- /dev/null +++ b/samples/msal-react-samples/react16-sample/src/App.jsx @@ -0,0 +1,62 @@ +import { Routes, Route, useNavigate, useLocation } from "react-router-dom"; +// Material-UI imports +import Grid from "@material-ui/core/Grid"; + +// MSAL imports +import { MsalProvider } from "@azure/msal-react"; +import { CustomNavigationClient } from "./utils/NavigationClient"; + +// Sample app imports +import { PageLayout } from "./ui-components/PageLayout"; +import { Home } from "./pages/Home"; +import { Profile } from "./pages/Profile"; +import { Logout } from "./pages/Logout"; +import { Redirect } from "./pages/Redirect"; + +// Class-based equivalents of "Profile" component +import { ProfileWithMsal } from "./pages/ProfileWithMsal"; +import { ProfileRawContext } from "./pages/ProfileRawContext"; +import { ProfileUseMsalAuthenticationHook } from "./pages/ProfileUseMsalAuthenticationHook"; + +function App({ pca }) { + // The next 3 lines are optional. This is how you configure MSAL to take advantage of the router's navigate functions when MSAL redirects between pages in your app + const navigate = useNavigate(); + const location = useLocation(); + const navigationClient = new CustomNavigationClient(navigate); + pca.setNavigationClient(navigationClient); + + // Don't wrap redirect page in MsalProvider to prevent MSAL from consuming the auth response + const isRedirectPage = location.pathname === '/redirect'; + + if (isRedirectPage) { + return ; + } + + return ( + + + + + + + + ); +} + +function Pages() { + return ( + + } /> + } /> + } /> + } + /> + } /> + } /> + + ); +} + +export default App; diff --git a/samples/msal-react-samples/react16-sample/src/authConfig.js b/samples/msal-react-samples/react16-sample/src/authConfig.js new file mode 100644 index 0000000000..73ea34a7f7 --- /dev/null +++ b/samples/msal-react-samples/react16-sample/src/authConfig.js @@ -0,0 +1,51 @@ +import { LogLevel, BrowserUtils } from "@azure/msal-browser"; + +// Config object to be passed to Msal on creation +export const msalConfig = { + auth: { + clientId: import.meta.env.VITE_CLIENT_ID, + authority: import.meta.env.VITE_AUTHORITY, + redirectUri: import.meta.env.VITE_REDIRECT_URI, + postLogoutRedirectUri: "/", + onRedirectNavigate: () => !BrowserUtils.isInIframe() + }, + cache: { + cacheLocation: "localStorage", + }, + system: { + allowPlatformBroker: false, // Disables WAM Broker + loggerOptions: { + loggerCallback: (level, message, containsPii) => { + if (containsPii) { + return; + } + switch (level) { + case LogLevel.Error: + console.error(message); + return; + case LogLevel.Info: + console.info(message); + return; + case LogLevel.Verbose: + console.debug(message); + return; + case LogLevel.Warning: + console.warn(message); + return; + default: + return; + } + }, + }, + }, +}; + +// Add here scopes for id token to be used at MS Identity Platform endpoints. +export const loginRequest = { + scopes: ["User.Read"] +}; + +// Add here the endpoints for MS Graph API services you would like to use. +export const graphConfig = { + graphMeEndpoint: "https://graph.microsoft.com/v1.0/me" +}; diff --git a/samples/msal-react-samples/react16-sample/src/index.jsx b/samples/msal-react-samples/react16-sample/src/index.jsx new file mode 100644 index 0000000000..8232c1dc94 --- /dev/null +++ b/samples/msal-react-samples/react16-sample/src/index.jsx @@ -0,0 +1,37 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { BrowserRouter as Router } from "react-router-dom"; +import { ThemeProvider } from "@material-ui/core/styles"; +import { theme } from "./styles/theme"; +import App from './App'; + +// MSAL imports +import { PublicClientApplication, EventType } from "@azure/msal-browser"; +import { msalConfig } from "./authConfig"; + +export const msalInstance = new PublicClientApplication(msalConfig); + +msalInstance.initialize().then(() => { + // Default to using the first account if no account is active on page load + if (!msalInstance.getActiveAccount() && msalInstance.getAllAccounts().length > 0) { + // Account selection logic is app dependent. Adjust as needed for different use cases. + msalInstance.setActiveAccount(msalInstance.getAllAccounts()[0]); + } + + msalInstance.addEventCallback((event) => { + if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) { + const account = event.payload; + msalInstance.setActiveAccount(account); + } + }); + + // React 16 uses ReactDOM.render instead of createRoot + ReactDOM.render( + + + + + , + document.getElementById("root") + ); +}); diff --git a/samples/msal-react-samples/react16-sample/src/pages/Home.jsx b/samples/msal-react-samples/react16-sample/src/pages/Home.jsx new file mode 100644 index 0000000000..ec233b1d21 --- /dev/null +++ b/samples/msal-react-samples/react16-sample/src/pages/Home.jsx @@ -0,0 +1,26 @@ +import { AuthenticatedTemplate, UnauthenticatedTemplate } from "@azure/msal-react"; +import Button from "@material-ui/core/Button"; +import ButtonGroup from "@material-ui/core/ButtonGroup"; +import Typography from "@material-ui/core/Typography"; +import { Link as RouterLink } from "react-router-dom"; + +export function Home() { + return ( + <> + + + + + + + + + + + +
Please sign-in to see your profile information.
+
+
+ + ); +} \ No newline at end of file diff --git a/samples/msal-react-samples/react16-sample/src/pages/Logout.jsx b/samples/msal-react-samples/react16-sample/src/pages/Logout.jsx new file mode 100644 index 0000000000..21d765b948 --- /dev/null +++ b/samples/msal-react-samples/react16-sample/src/pages/Logout.jsx @@ -0,0 +1,16 @@ +import React, { useEffect } from "react"; +import { useMsal } from "@azure/msal-react"; + +export function Logout() { + const { instance } = useMsal(); + + useEffect(() => { + instance.logoutRedirect({ + account: instance.getActiveAccount(), + }) + }, [ instance ]); + + return ( +
Logout
+ ) +} diff --git a/samples/msal-react-samples/react16-sample/src/pages/Profile.jsx b/samples/msal-react-samples/react16-sample/src/pages/Profile.jsx new file mode 100644 index 0000000000..38075bf6a1 --- /dev/null +++ b/samples/msal-react-samples/react16-sample/src/pages/Profile.jsx @@ -0,0 +1,79 @@ +import { useEffect, useState, useCallback } from "react"; + +// Msal imports +import { MsalAuthenticationTemplate, useMsal } from "@azure/msal-react"; +import { EventType, InteractionType, InteractionRequiredAuthError } from "@azure/msal-browser"; +import { loginRequest } from "../authConfig"; + +// Sample app imports +import { ProfileData } from "../ui-components/ProfileData"; +import { Loading } from "../ui-components/Loading"; +import { ErrorComponent } from "../ui-components/ErrorComponent"; +import { callMsGraph } from "../utils/MsGraphApiCall"; + +// Material-ui imports +import Paper from "@material-ui/core/Paper"; + +const ProfileContent = () => { + const { instance } = useMsal(); + const [graphData, setGraphData] = useState(null); + + const fetchProfile = useCallback(() => { + if (!instance.getActiveAccount()) { + return; + } + callMsGraph().then(response => setGraphData(response)).catch((e) => { + if (e instanceof InteractionRequiredAuthError) { + instance.acquireTokenRedirect({ + ...loginRequest, + account: instance.getActiveAccount() + }); + } + }); + }, [instance]); + + useEffect(() => { + // Attempt to fetch profile data immediately + fetchProfile(); + + // Subscribe to active account changes so the Graph call is retried + // once setActiveAccount has been called. In React 16/17 the render + // triggered by ACQUIRE_TOKEN_SUCCESS fires before the LOGIN_SUCCESS + // handler sets the active account, so getActiveAccount() returns null + // on the first attempt. + const callbackId = instance.addEventCallback((event) => { + if (event.eventType === EventType.ACTIVE_ACCOUNT_CHANGED) { + fetchProfile(); + } + }); + + return () => { + if (callbackId) { + instance.removeEventCallback(callbackId); + } + }; + }, [instance, fetchProfile]); + + return ( + + { graphData ? : null } + + ); +}; + +export function Profile() { + const authRequest = { + ...loginRequest + }; + + return ( + + + + ) +}; \ No newline at end of file diff --git a/samples/msal-react-samples/react16-sample/src/pages/ProfileRawContext.jsx b/samples/msal-react-samples/react16-sample/src/pages/ProfileRawContext.jsx new file mode 100644 index 0000000000..50f4ce257c --- /dev/null +++ b/samples/msal-react-samples/react16-sample/src/pages/ProfileRawContext.jsx @@ -0,0 +1,112 @@ +import { Component } from "react"; + +// Msal imports +import { MsalAuthenticationTemplate, MsalContext } from "@azure/msal-react"; +import { InteractionType, EventType, InteractionRequiredAuthError } from "@azure/msal-browser"; +import { loginRequest } from "../authConfig"; + +// Sample app imports +import { ProfileData } from "../ui-components/ProfileData"; +import { Loading } from "../ui-components/Loading"; +import { ErrorComponent } from "../ui-components/ErrorComponent"; +import { callMsGraph } from "../utils/MsGraphApiCall"; + +// Material-ui imports +import Paper from "@material-ui/core/Paper"; + + +/** + * This class is using the raw context directly. The available + * objects and methods are the same as in "withMsal" HOC usage. + */ +class ProfileContent extends Component { + + static contextType = MsalContext; + + constructor(props) { + super(props) + + this.state = { + graphData: null, + } + + this.callbackId = null; + } + + fetchGraphData() { + if (this.state.graphData) { + return; + } + + const instance = this.context.instance; + if (!instance.getActiveAccount()) { + return; + } + + callMsGraph().then(response => this.setState({graphData: response})).catch((e) => { + if (e instanceof InteractionRequiredAuthError) { + instance.acquireTokenRedirect({ + ...loginRequest, + account: instance.getActiveAccount() + }); + } + }); + } + + componentDidMount() { + // Attempt to fetch profile data immediately + this.fetchGraphData(); + + // Subscribe to active account changes so the Graph call is retried + // once setActiveAccount has been called. In React 16/17 the render + // triggered by ACQUIRE_TOKEN_SUCCESS fires before the LOGIN_SUCCESS + // handler sets the active account, so getActiveAccount() returns null + // on the first attempt. + this.callbackId = this.context.instance.addEventCallback((event) => { + if (event.eventType === EventType.ACTIVE_ACCOUNT_CHANGED) { + this.fetchGraphData(); + } + }); + } + + componentWillUnmount() { + if (this.callbackId) { + this.context.instance.removeEventCallback(this.callbackId); + } + } + + render() { + return ( + + { this.state.graphData ? : null } + + ); + } +} + +/** + * This class is using "withMsal" HOC. It passes down the msalContext + * as a prop to its children. + */ +class Profile extends Component { + + render() { + + const authRequest = { + ...loginRequest + }; + + return ( + + + + ); + } +} + +export const ProfileRawContext = Profile diff --git a/samples/msal-react-samples/react16-sample/src/pages/ProfileUseMsalAuthenticationHook.jsx b/samples/msal-react-samples/react16-sample/src/pages/ProfileUseMsalAuthenticationHook.jsx new file mode 100644 index 0000000000..e88289b69d --- /dev/null +++ b/samples/msal-react-samples/react16-sample/src/pages/ProfileUseMsalAuthenticationHook.jsx @@ -0,0 +1,51 @@ +import { useEffect, useState } from "react"; + +// Msal imports +import { useMsalAuthentication } from "@azure/msal-react"; +import { InteractionType } from "@azure/msal-browser"; +import { loginRequest } from "../authConfig"; + +// Sample app imports +import { ProfileData } from "../ui-components/ProfileData"; +import { ErrorComponent } from "../ui-components/ErrorComponent"; +import { callMsGraph } from "../utils/MsGraphApiCall"; + +// Material-ui imports +import Paper from "@material-ui/core/Paper"; + +const ProfileContent = () => { + const [graphData, setGraphData] = useState(null); + const { result, error } = useMsalAuthentication(InteractionType.Popup, { + ...loginRequest, + }); + + useEffect(() => { + if (!!graphData) { + // We already have the data, no need to call the API + return; + } + + if (!!error) { + // Error occurred attempting to acquire a token, either handle the error or do nothing + return; + } + + if (result) { + callMsGraph().then(response => setGraphData(response)); + } + }, [error, result, graphData]); + + if (error) { + return ; + } + + return ( + + { graphData ? : null } + + ); +}; + +export function ProfileUseMsalAuthenticationHook() { + return +}; diff --git a/samples/msal-react-samples/react16-sample/src/pages/ProfileWithMsal.jsx b/samples/msal-react-samples/react16-sample/src/pages/ProfileWithMsal.jsx new file mode 100644 index 0000000000..8c4f124684 --- /dev/null +++ b/samples/msal-react-samples/react16-sample/src/pages/ProfileWithMsal.jsx @@ -0,0 +1,87 @@ +import { Component } from "react"; + +// Msal imports +import { MsalAuthenticationTemplate, withMsal } from "@azure/msal-react"; +import { InteractionType, InteractionStatus, InteractionRequiredAuthError } from "@azure/msal-browser"; +import { loginRequest } from "../authConfig"; + +// Sample app imports +import { ProfileData } from "../ui-components/ProfileData"; +import { Loading } from "../ui-components/Loading"; +import { ErrorComponent } from "../ui-components/ErrorComponent"; +import { callMsGraph } from "../utils/MsGraphApiCall"; + +// Material-ui imports +import Paper from "@material-ui/core/Paper"; + +/** + * This class is a child component of "Profile". MsalContext is passed + * down from the parent and available as a prop here. + */ +class ProfileContent extends Component { + + constructor(props) { + super(props) + + this.state = { + graphData: null, + } + } + + setGraphData() { + if (!this.state.graphData && this.props.msalContext.inProgress === InteractionStatus.None) { + callMsGraph().then(response => this.setState({graphData: response})).catch((e) => { + if (e instanceof InteractionRequiredAuthError) { + this.props.msalContext.instance.acquireTokenRedirect({ + ...loginRequest, + account: this.props.msalContext.instance.getActiveAccount() + }); + } + }); + } + } + + componentDidMount() { + this.setGraphData(); + } + + componentDidUpdate() { + this.setGraphData(); + } + + render() { + return ( + + { this.state.graphData ? : null } + + ); + } +} + +/** + * This class is using "withMsal" HOC and has access to authentication + * state. It passes down the msalContext as a prop to its children. + */ +class Profile extends Component { + + render() { + + const authRequest = { + ...loginRequest + }; + + return ( + + + + ); + } +} + +// Wrap your class component to access authentication state as props +export const ProfileWithMsal = withMsal(Profile); \ No newline at end of file diff --git a/samples/msal-react-samples/react16-sample/src/pages/Redirect.jsx b/samples/msal-react-samples/react16-sample/src/pages/Redirect.jsx new file mode 100644 index 0000000000..dd4529aaf3 --- /dev/null +++ b/samples/msal-react-samples/react16-sample/src/pages/Redirect.jsx @@ -0,0 +1,20 @@ +// This page serves the redirect bridge for MSAL authentication +// It's excluded from MsalProvider wrapper in App.js to prevent MSAL from processing the hash +import { useEffect } from "react"; +import { broadcastResponseToMainFrame } from "@azure/msal-browser/redirect-bridge"; + +export function Redirect() { + useEffect(() => { + // Call broadcastResponseToMainFrame when component mounts + broadcastResponseToMainFrame().catch((error) => { + console.error("Error broadcasting response to main frame:", error); + }); + }, []); + + return ( +
+

Processing authentication...

+
+ ); +} + diff --git a/samples/msal-react-samples/react16-sample/src/styles/theme.js b/samples/msal-react-samples/react16-sample/src/styles/theme.js new file mode 100644 index 0000000000..2b13a6ea37 --- /dev/null +++ b/samples/msal-react-samples/react16-sample/src/styles/theme.js @@ -0,0 +1,20 @@ +import { createTheme } from '@material-ui/core/styles'; +import { red } from '@material-ui/core/colors'; + +// Create a theme instance. +export const theme = createTheme({ + palette: { + primary: { + main: '#556cd6', + }, + secondary: { + main: '#19857b', + }, + error: { + main: red.A400, + }, + background: { + default: '#fff', + }, + }, +}); diff --git a/samples/msal-react-samples/react16-sample/src/ui-components/AccountPicker.jsx b/samples/msal-react-samples/react16-sample/src/ui-components/AccountPicker.jsx new file mode 100644 index 0000000000..bd56ac8d92 --- /dev/null +++ b/samples/msal-react-samples/react16-sample/src/ui-components/AccountPicker.jsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { useMsal } from "@azure/msal-react"; +import Avatar from '@material-ui/core/Avatar'; +import List from '@material-ui/core/List'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemAvatar from '@material-ui/core/ListItemAvatar'; +import ListItemText from '@material-ui/core/ListItemText'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import Dialog from '@material-ui/core/Dialog'; +import PersonIcon from '@material-ui/icons/Person'; +import AddIcon from '@material-ui/icons/Add'; +import { loginRequest } from "../authConfig"; + +export const AccountPicker = (props) => { + const { instance, accounts } = useMsal(); + const { onClose, open } = props; + + const handleListItemClick = (account) => { + instance.setActiveAccount(account); + if (!account) { + instance.loginRedirect({ + ...loginRequest, + prompt: "login" + }) + } else { + // To ensure account related page attributes update after the account is changed + window.location.reload(); + } + + onClose(account); + }; + + return ( + + Set active account + + {accounts.map((account) => ( + handleListItemClick(account)} key={account.homeAccountId}> + + + + + + + + ))} + + handleListItemClick(null)}> + + + + + + + + + + ); +}; \ No newline at end of file diff --git a/samples/msal-react-samples/react16-sample/src/ui-components/ErrorComponent.jsx b/samples/msal-react-samples/react16-sample/src/ui-components/ErrorComponent.jsx new file mode 100644 index 0000000000..93050b04dc --- /dev/null +++ b/samples/msal-react-samples/react16-sample/src/ui-components/ErrorComponent.jsx @@ -0,0 +1,5 @@ +import Typography from "@material-ui/core/Typography"; + +export const ErrorComponent = ({error}) => { + return An Error Occurred: {error.errorCode}; +} \ No newline at end of file diff --git a/samples/msal-react-samples/react16-sample/src/ui-components/Loading.jsx b/samples/msal-react-samples/react16-sample/src/ui-components/Loading.jsx new file mode 100644 index 0000000000..e500ff1f28 --- /dev/null +++ b/samples/msal-react-samples/react16-sample/src/ui-components/Loading.jsx @@ -0,0 +1,5 @@ +import Typography from "@material-ui/core/Typography"; + +export const Loading = () => { + return Authentication in progress... +} \ No newline at end of file diff --git a/samples/msal-react-samples/react16-sample/src/ui-components/NavBar.jsx b/samples/msal-react-samples/react16-sample/src/ui-components/NavBar.jsx new file mode 100644 index 0000000000..9d2fa829b9 --- /dev/null +++ b/samples/msal-react-samples/react16-sample/src/ui-components/NavBar.jsx @@ -0,0 +1,25 @@ +import AppBar from "@material-ui/core/AppBar"; +import Toolbar from "@material-ui/core/Toolbar"; +import Link from "@material-ui/core/Link"; +import Typography from "@material-ui/core/Typography"; +import WelcomeName from "./WelcomeName"; +import SignInSignOutButton from "./SignInSignOutButton"; +import { Link as RouterLink } from "react-router-dom"; + +const NavBar = () => { + return ( +
+ + + + MS Identity Platform + + + + + +
+ ); +}; + +export default NavBar; \ No newline at end of file diff --git a/samples/msal-react-samples/react16-sample/src/ui-components/PageLayout.jsx b/samples/msal-react-samples/react16-sample/src/ui-components/PageLayout.jsx new file mode 100644 index 0000000000..7f4706818a --- /dev/null +++ b/samples/msal-react-samples/react16-sample/src/ui-components/PageLayout.jsx @@ -0,0 +1,16 @@ +import Typography from "@material-ui/core/Typography"; +import NavBar from "./NavBar"; + +export const PageLayout = (props) => { + return ( + <> + + +
Welcome to the Microsoft Authentication Library For React Quickstart
+
+
+
+ {props.children} + + ); +}; \ No newline at end of file diff --git a/samples/msal-react-samples/react16-sample/src/ui-components/ProfileData.jsx b/samples/msal-react-samples/react16-sample/src/ui-components/ProfileData.jsx new file mode 100644 index 0000000000..33b42e50f4 --- /dev/null +++ b/samples/msal-react-samples/react16-sample/src/ui-components/ProfileData.jsx @@ -0,0 +1,78 @@ +import React from "react"; +import List from "@material-ui/core/List"; +import ListItem from "@material-ui/core/ListItem"; +import ListItemText from "@material-ui/core/ListItemText"; +import ListItemAvatar from "@material-ui/core/ListItemAvatar"; +import Avatar from "@material-ui/core/Avatar"; +import PersonIcon from '@material-ui/icons/Person'; +import WorkIcon from "@material-ui/icons/Work"; +import MailIcon from '@material-ui/icons/Mail'; +import PhoneIcon from '@material-ui/icons/Phone'; +import LocationOnIcon from '@material-ui/icons/LocationOn'; + +export const ProfileData = ({graphData}) => { + return ( + + + + + + + + ); +}; + +const NameListItem = ({name}) => ( + + + + + + + + +); + +const JobTitleListItem = ({jobTitle}) => ( + + + + + + + + +); + +const MailListItem = ({mail}) => ( + + + + + + + + +); + +const PhoneListItem = ({phone}) => ( + + + + + + + + +); + +const LocationListItem = ({location}) => ( + + + + + + + + +); diff --git a/samples/msal-react-samples/react16-sample/src/ui-components/SignInButton.jsx b/samples/msal-react-samples/react16-sample/src/ui-components/SignInButton.jsx new file mode 100644 index 0000000000..6d1c81fb52 --- /dev/null +++ b/samples/msal-react-samples/react16-sample/src/ui-components/SignInButton.jsx @@ -0,0 +1,162 @@ +import { useState, useRef, useEffect } from "react"; +import { useMsal } from "@azure/msal-react"; +import Button from "@material-ui/core/Button"; +import MenuItem from '@material-ui/core/MenuItem'; +import Menu from '@material-ui/core/Menu'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogContentText from '@material-ui/core/DialogContentText'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import { loginRequest } from "../authConfig"; + +export const SignInButton = () => { + const { instance } = useMsal(); + + const [anchorEl, setAnchorEl] = useState(null); + const [showRetryDialog, setShowRetryDialog] = useState(false); + const [retryRequested, setRetryRequested] = useState(false); + const [showPopupWarning, setShowPopupWarning] = useState(false); + const open = Boolean(anchorEl); + + // Track mounted state to avoid setting state after unmount (React 16 does not batch async state updates) + const isMountedRef = useRef(true); + useEffect(() => { + return () => { + isMountedRef.current = false; + }; + }, []); + + const handleLogin = async (loginType) => { + setAnchorEl(null); + + if (loginType === "popup") { + // Show warning when popup is about to open + setShowPopupWarning(true); + + try { + await instance.loginPopup({ + ...loginRequest, + // Only override if user explicitly clicked retry + overrideInteractionInProgress: retryRequested + }); + + // Hide warning on success — guard against unmounted component + if (isMountedRef.current) { + setShowPopupWarning(false); + setRetryRequested(false); + } + } catch (error) { + // Hide warning on error — guard against unmounted component + if (isMountedRef.current) { + setShowPopupWarning(false); + + if (error.errorCode === 'interaction_in_progress') { + // Show retry dialog - let user decide whether to retry + setShowRetryDialog(true); + } else { + // Reset retry flag for other errors + setRetryRequested(false); + console.error(error); + } + } + } + } else if (loginType === "redirect") { + instance.loginRedirect(loginRequest); + } + } + + const handleRetry = () => { + setShowRetryDialog(false); + setRetryRequested(true); // User explicitly requested retry + handleLogin("popup"); + } + + const handleCancelRetry = () => { + setShowRetryDialog(false); + setRetryRequested(false); + } + + return ( +
+ + setAnchorEl(null)} + > + handleLogin("popup")} key="loginPopup">Sign in using Popup + handleLogin("redirect")} key="loginRedirect">Sign in using Redirect + + + {/* Warning message during popup authentication */} + {showPopupWarning && ( +
+ Authentication in Progress +

Please complete authentication in the popup window. Do not close the popup until authentication is complete.

+
+ )} + + {/* Retry dialog for interaction_in_progress errors */} + + Authentication Already in Progress + + + An authentication request is already in progress. This may happen if: + + +
    +
  • You closed the popup window before completing authentication
  • +
  • The previous authentication attempt is still pending
  • +
+
+ + Would you like to cancel the pending authentication and try again? + +
+ Warning +

Retrying will cancel the pending authentication request.

+
+
+ + + + +
+
+ ) +}; diff --git a/samples/msal-react-samples/react16-sample/src/ui-components/SignInSignOutButton.jsx b/samples/msal-react-samples/react16-sample/src/ui-components/SignInSignOutButton.jsx new file mode 100644 index 0000000000..61633e1459 --- /dev/null +++ b/samples/msal-react-samples/react16-sample/src/ui-components/SignInSignOutButton.jsx @@ -0,0 +1,20 @@ +import { useIsAuthenticated, useMsal } from "@azure/msal-react"; +import { SignInButton } from "./SignInButton"; +import { SignOutButton } from "./SignOutButton"; +import { InteractionStatus } from "@azure/msal-browser"; + +const SignInSignOutButton = () => { + const { inProgress } = useMsal(); + const isAuthenticated = useIsAuthenticated(); + + if (isAuthenticated) { + return ; + } else if (inProgress !== InteractionStatus.Startup && inProgress !== InteractionStatus.HandleRedirect) { + // inProgress check prevents sign-in button from being displayed briefly after returning from a redirect sign-in. Processing the server response takes a render cycle or two + return ; + } else { + return null; + } +} + +export default SignInSignOutButton; \ No newline at end of file diff --git a/samples/msal-react-samples/react16-sample/src/ui-components/SignOutButton.jsx b/samples/msal-react-samples/react16-sample/src/ui-components/SignOutButton.jsx new file mode 100644 index 0000000000..0a99f910d8 --- /dev/null +++ b/samples/msal-react-samples/react16-sample/src/ui-components/SignOutButton.jsx @@ -0,0 +1,65 @@ +import { useState } from "react"; +import { useMsal } from "@azure/msal-react"; +import IconButton from '@material-ui/core/IconButton'; +import AccountCircle from "@material-ui/icons/AccountCircle"; +import MenuItem from '@material-ui/core/MenuItem'; +import Menu from '@material-ui/core/Menu'; +import { AccountPicker } from "./AccountPicker"; + +export const SignOutButton = () => { + const { instance } = useMsal(); + const [accountSelectorOpen, setOpen] = useState(false); + + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + const handleLogout = (logoutType) => { + setAnchorEl(null); + + if (logoutType === "popup") { + instance.logoutPopup(); + } else if (logoutType === "redirect") { + instance.logoutRedirect(); + } + } + + const handleAccountSelection = () => { + setAnchorEl(null); + setOpen(true); + } + + const handleClose = () => { + setOpen(false); + }; + + return ( +
+ setAnchorEl(event.currentTarget)} + color="inherit" + > + + + setAnchorEl(null)} + > + handleAccountSelection()} key="switchAccount">Switch Account + handleLogout("popup")} key="logoutPopup">Logout using Popup + handleLogout("redirect")} key="logoutRedirect">Logout using Redirect + + +
+ ) +}; \ No newline at end of file diff --git a/samples/msal-react-samples/react16-sample/src/ui-components/WelcomeName.jsx b/samples/msal-react-samples/react16-sample/src/ui-components/WelcomeName.jsx new file mode 100644 index 0000000000..4f135587a0 --- /dev/null +++ b/samples/msal-react-samples/react16-sample/src/ui-components/WelcomeName.jsx @@ -0,0 +1,47 @@ +import { useEffect, useState, useCallback } from "react"; +import { useMsal } from "@azure/msal-react"; +import { EventType } from "@azure/msal-browser"; +import Typography from "@material-ui/core/Typography"; + +const WelcomeName = () => { + const { instance } = useMsal(); + const [name, setName] = useState(null); + + const updateName = useCallback(() => { + const activeAccount = instance.getActiveAccount(); + if (activeAccount) { + setName(activeAccount.name.split(' ')[0]); + } else { + setName(null); + } + }, [instance]); + + useEffect(() => { + // Set the name from the current active account on mount + updateName(); + + // Subscribe to active account changes so the component updates when + // setActiveAccount is called. This avoids the React 16/17 batching issue where the + // render triggered by ACQUIRE_TOKEN_SUCCESS runs before setActiveAccount + // has been called, causing getActiveAccount() to return null. + const callbackId = instance.addEventCallback((event) => { + if (event.eventType === EventType.ACTIVE_ACCOUNT_CHANGED) { + updateName(); + } + }); + + return () => { + if (callbackId) { + instance.removeEventCallback(callbackId); + } + }; + }, [instance, updateName]); + + if (name) { + return Welcome, {name}; + } else { + return null; + } +}; + +export default WelcomeName; \ No newline at end of file diff --git a/samples/msal-react-samples/react16-sample/src/utils/MsGraphApiCall.js b/samples/msal-react-samples/react16-sample/src/utils/MsGraphApiCall.js new file mode 100644 index 0000000000..360493a86b --- /dev/null +++ b/samples/msal-react-samples/react16-sample/src/utils/MsGraphApiCall.js @@ -0,0 +1,33 @@ +import { loginRequest, graphConfig } from "../authConfig"; +import { msalInstance } from "../index"; + +export async function callMsGraph(accessToken) { + if (!accessToken) { + const account = msalInstance.getActiveAccount(); + if (!account) { + throw Error("No active account! Verify a user has been signed in and setActiveAccount has been called."); + } + + const response = await msalInstance.acquireTokenSilent({ + ...loginRequest, + account: account + }); + accessToken = response.accessToken; + } + + const headers = new Headers(); + const bearer = `Bearer ${accessToken}`; + + headers.append("Authorization", bearer); + + const options = { + method: "GET", + headers: headers + }; + + const response = await fetch(graphConfig.graphMeEndpoint, options); + if (!response.ok) { + throw new Error(`MS Graph request failed: ${response.status} ${response.statusText}`); + } + return response.json(); +} diff --git a/samples/msal-react-samples/react16-sample/src/utils/NavigationClient.js b/samples/msal-react-samples/react16-sample/src/utils/NavigationClient.js new file mode 100644 index 0000000000..70eab8c6d7 --- /dev/null +++ b/samples/msal-react-samples/react16-sample/src/utils/NavigationClient.js @@ -0,0 +1,28 @@ +import { NavigationClient } from "@azure/msal-browser"; + +/** + * This is an example for overriding the default function MSAL uses to navigate to other urls in your webpage + */ +export class CustomNavigationClient extends NavigationClient { + constructor(navigate) { + super(); + this.navigate = navigate; + } + + /** + * Navigates to other pages within the same web application + * You can use the useNavigate hook provided by react-router-dom to take advantage of client-side routing + * @param url + * @param options + */ + async navigateInternal(url, options) { + const relativePath = url.replace(window.location.origin, ""); + if (options.noHistory) { + this.navigate(relativePath, { replace: true }); + } else { + this.navigate(relativePath); + } + + return false; + } +} diff --git a/samples/msal-react-samples/react16-sample/test/home.spec.ts b/samples/msal-react-samples/react16-sample/test/home.spec.ts new file mode 100644 index 0000000000..eb2c005709 --- /dev/null +++ b/samples/msal-react-samples/react16-sample/test/home.spec.ts @@ -0,0 +1,148 @@ +import * as puppeteer from "puppeteer"; +import { + Screenshot, + setupCredentials, + enterCredentials, + RETRY_TIMES, + LabClient, + LabApiQueryParams, + AzureEnvironments, + AppTypes, + BrowserCacheUtils, +} from "e2e-test-utils"; + +const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/home-tests`; + +describe("/ (Home Page)", () => { + jest.retryTimes(RETRY_TIMES); + let browser: puppeteer.Browser; + let context: puppeteer.BrowserContext; + let page: puppeteer.Page; + let port: number; + let username: string; + let accountPwd: string; + let BrowserCache: BrowserCacheUtils; + + beforeAll(async () => { + // @ts-ignore + browser = await global.__BROWSER__; + // @ts-ignore + port = global.__PORT__; + + const labApiParams: LabApiQueryParams = { + azureEnvironment: AzureEnvironments.CLOUD, + appType: AppTypes.CLOUD, + }; + + const labClient = new LabClient(); + const envResponse = await labClient.getVarsByCloudEnvironment( + labApiParams + ); + + [username, accountPwd] = await setupCredentials( + envResponse[0], + labClient + ); + }); + + beforeEach(async () => { + context = await browser.createBrowserContext(); + page = await context.newPage(); + page.setDefaultTimeout(5000); + BrowserCache = new BrowserCacheUtils(page, "localStorage"); + await page.goto(`http://localhost:${port}`); + }); + + afterEach(async () => { + await page.close(); + await context.close(); + }); + + it("AuthenticatedTemplate - children are rendered after logging in with loginRedirect", async () => { + const testName = "redirectBaseCase"; + const screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + await screenshot.takeScreenshot(page, "Page loaded"); + + // Initiate Login + const signInButton = await page.waitForSelector( + "xpath=//button[contains(., 'Login')]" + ); + await signInButton.click(); + await screenshot.takeScreenshot(page, "Login button clicked"); + const loginRedirectButton = await page.waitForSelector( + "xpath=//li[contains(., 'Sign in using Redirect')]" + ); + await loginRedirectButton.click(); + + await enterCredentials(page, screenshot, username, accountPwd); + await screenshot.takeScreenshot(page, "Returned to app"); + + // Verify UI now displays logged in content + await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]"); + const profileButton = await page.waitForSelector( + "xpath=//header//button" + ); + await profileButton.click(); + const logoutButtons = await page.$$( + "xpath/.//li[contains(., 'Logout using')]" + ); + expect(logoutButtons.length).toBe(2); + await screenshot.takeScreenshot(page, "App signed in"); + + // Verify tokens are in cache + await BrowserCache.verifyTokenStore({ + scopes: ['User.Read'], + }); + }); + + it("AuthenticatedTemplate - children are rendered after logging in with loginPopup", async () => { + const testName = "popupBaseCase"; + const screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + await screenshot.takeScreenshot(page, "Page loaded"); + + // Initiate Login + const signInButton = await page.waitForSelector( + "xpath=//button[contains(., 'Login')]" + ); + await signInButton.click(); + await screenshot.takeScreenshot(page, "Login button clicked"); + const loginPopupButton = await page.waitForSelector( + "xpath=//li[contains(., 'Sign in using Popup')]" + ); + const newPopupWindowPromise = new Promise((resolve) => + page.once("popup", resolve) + ); + await loginPopupButton.click(); + const popupPage = await newPopupWindowPromise; + if (!popupPage) { + throw new Error('Popup window was not opened'); + } + + await enterCredentials(popupPage, screenshot, username, accountPwd); + await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]", { + timeout: 3000, + }); + await screenshot.takeScreenshot(page, "Popup closed"); + + // Verify UI now displays logged in content + await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]"); + const profileButton = await page.waitForSelector( + "xpath=//header//button" + ); + await profileButton.click(); + const logoutButtons = await page.$$( + "xpath/.//li[contains(., 'Logout using')]" + ); + expect(logoutButtons.length).toBe(2); + await screenshot.takeScreenshot(page, "App signed in"); + + // Verify tokens are in cache + await BrowserCache.verifyTokenStore({ + scopes: ['User.Read'], + }); + }); +}); diff --git a/samples/msal-react-samples/react16-sample/test/profile.spec.ts b/samples/msal-react-samples/react16-sample/test/profile.spec.ts new file mode 100644 index 0000000000..e3b383cf66 --- /dev/null +++ b/samples/msal-react-samples/react16-sample/test/profile.spec.ts @@ -0,0 +1,170 @@ +import * as puppeteer from "puppeteer"; +import { + Screenshot, + setupCredentials, + enterCredentials, + RETRY_TIMES, + LabClient, + LabApiQueryParams, + AzureEnvironments, + AppTypes, + BrowserCacheUtils, +} from "e2e-test-utils"; + +const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/profile-tests`; + +async function verifyTokenStore( + BrowserCache: BrowserCacheUtils, + scopes: string[] +): Promise { + await BrowserCache.verifyTokenStore({ + scopes, + }); + const telemetryCacheEntry = await BrowserCache.getTelemetryCacheEntry( + "b5c2e510-4a17-4feb-b219-e55aa5b74144" + ); + expect(telemetryCacheEntry).not.toBeNull(); + expect(telemetryCacheEntry["cacheHits"]).toBeGreaterThanOrEqual(1); +} + +describe("/profile", () => { + jest.retryTimes(RETRY_TIMES); + let browser: puppeteer.Browser; + let context: puppeteer.BrowserContext; + let page: puppeteer.Page; + let port: number; + let username: string; + let accountPwd: string; + let BrowserCache: BrowserCacheUtils; + + beforeAll(async () => { + // @ts-ignore + browser = await global.__BROWSER__; + // @ts-ignore + port = global.__PORT__; + + const labApiParams: LabApiQueryParams = { + azureEnvironment: AzureEnvironments.CLOUD, + appType: AppTypes.CLOUD, + }; + + const labClient = new LabClient(); + const envResponse = await labClient.getVarsByCloudEnvironment( + labApiParams + ); + + [username, accountPwd] = await setupCredentials( + envResponse[0], + labClient + ); + }); + + beforeEach(async () => { + context = await browser.createBrowserContext(); + page = await context.newPage(); + page.setDefaultTimeout(5000); + BrowserCache = new BrowserCacheUtils(page, "localStorage"); + await page.goto(`http://localhost:${port}`); + }); + + afterEach(async () => { + await page.close(); + await context.close(); + }); + + it("MsalAuthenticationTemplate - invokes loginPopup if user is not signed in", async () => { + const testName = "MsalAuthenticationTemplateBaseCase"; + const screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + await screenshot.takeScreenshot(page, "Home page loaded"); + + // Navigate to /profile and expect popup to be opened without interaction + const newPopupWindowPromise = new Promise((resolve) => + page.once("popup", resolve) + ); + await page.goto(`http://localhost:${port}/profile`); + await screenshot.takeScreenshot(page, "Profile page loaded"); + const popupPage = await newPopupWindowPromise; + if (!popupPage) { + throw new Error('Popup window was not opened'); + } + + await enterCredentials(popupPage, screenshot, username, accountPwd); + + // Wait for Graph data to display + await page.waitForSelector("xpath/.//div/ul/li[contains(., 'Name')]", { + timeout: 5000, + }); + await screenshot.takeScreenshot(page, "Graph data acquired"); + + // Verify UI now displays logged in content + await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]"); + const profileButton = await page.waitForSelector( + "xpath=//header//button" + ); + await profileButton.click(); + const logoutButtons = await page.$$( + "xpath/.//li[contains(., 'Logout using')]" + ); + expect(logoutButtons.length).toBe(2); + await screenshot.takeScreenshot(page, "App signed in"); + + // Verify tokens are in cache + await verifyTokenStore(BrowserCache, ["User.Read"]); + }); + + it("MsalAuthenticationTemplate - renders children without invoking login if user is already signed in", async () => { + const testName = "MsalAuthenticationTemplateSignedInCase"; + const screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + await screenshot.takeScreenshot(page, "Page loaded"); + + // Initiate Login + const signInButton = await page.waitForSelector( + "xpath=//button[contains(., 'Login')]" + ); + await signInButton.click(); + await screenshot.takeScreenshot(page, "Login button clicked"); + const loginPopupButton = await page.waitForSelector( + "xpath=//li[contains(., 'Sign in using Popup')]" + ); + const newPopupWindowPromise = new Promise((resolve) => + page.once("popup", resolve) + ); + await loginPopupButton.click(); + const popupPage = await newPopupWindowPromise; + if (!popupPage) { + throw new Error('Popup window was not opened'); + } + + await enterCredentials(popupPage, screenshot, username, accountPwd); + await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]", { + timeout: 3000, + }); + await screenshot.takeScreenshot(page, "Popup closed"); + + // Verify UI now displays logged in content + await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]"); + const profileButton = await page.waitForSelector( + "xpath=//header//button" + ); + await profileButton.click(); + const logoutButtons = await page.$$( + "xpath/.//li[contains(., 'Logout using')]" + ); + expect(logoutButtons.length).toBe(2); + await screenshot.takeScreenshot(page, "App signed in"); + + // Go to protected page + await page.goto(`http://localhost:${port}/profile`); + // Wait for Graph data to display + await page.waitForSelector("xpath/.//div/ul/li[contains(., 'Name')]", { + timeout: 5000, + }); + await screenshot.takeScreenshot(page, "Graph data acquired"); + // Verify tokens are in cache + await verifyTokenStore(BrowserCache, ["User.Read"]); + }); +}); diff --git a/samples/msal-react-samples/react16-sample/test/profileRawContext.spec.ts b/samples/msal-react-samples/react16-sample/test/profileRawContext.spec.ts new file mode 100644 index 0000000000..ff4df3c2a2 --- /dev/null +++ b/samples/msal-react-samples/react16-sample/test/profileRawContext.spec.ts @@ -0,0 +1,104 @@ +import * as puppeteer from "puppeteer"; +import { + Screenshot, + setupCredentials, + enterCredentials, + RETRY_TIMES, + LabClient, + LabApiQueryParams, + AzureEnvironments, + AppTypes, + BrowserCacheUtils, +} from "e2e-test-utils"; + +const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/profileRawContext-tests`; + +async function verifyTokenStore( + BrowserCache: BrowserCacheUtils, + scopes: string[] +): Promise { + await BrowserCache.verifyTokenStore({ + scopes, + }); + const telemetryCacheEntry = await BrowserCache.getTelemetryCacheEntry( + "b5c2e510-4a17-4feb-b219-e55aa5b74144" + ); + expect(telemetryCacheEntry).not.toBeNull(); + expect(telemetryCacheEntry["cacheHits"]).toBeGreaterThanOrEqual(1); +} + +describe("/profileRawContext", () => { + jest.retryTimes(RETRY_TIMES); + let browser: puppeteer.Browser; + let context: puppeteer.BrowserContext; + let page: puppeteer.Page; + let port: number; + let username: string; + let accountPwd: string; + let BrowserCache: BrowserCacheUtils; + + beforeAll(async () => { + // @ts-ignore + browser = await global.__BROWSER__; + // @ts-ignore + port = global.__PORT__; + + const labApiParams: LabApiQueryParams = { + azureEnvironment: AzureEnvironments.CLOUD, + appType: AppTypes.CLOUD, + }; + + const labClient = new LabClient(); + const envResponse = await labClient.getVarsByCloudEnvironment( + labApiParams + ); + + [username, accountPwd] = await setupCredentials( + envResponse[0], + labClient + ); + }); + + beforeEach(async () => { + context = await browser.createBrowserContext(); + page = await context.newPage(); + page.setDefaultTimeout(5000); + BrowserCache = new BrowserCacheUtils(page, "localStorage"); + await page.goto(`http://localhost:${port}`); + }); + + afterEach(async () => { + await page.close(); + await context.close(); + }); + + it("MsalAuthenticationTemplate - invokes loginPopup if user is not signed in (class component w/ raw context)", async () => { + const testName = "MsalAuthenticationTemplatePopupCase"; + const screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + await screenshot.takeScreenshot(page, "Home page loaded"); + + // Navigate to /profile and expect popup to be opened without interaction + const newPopupWindowPromise = new Promise((resolve) => + page.once("popup", resolve) + ); + await page.goto(`http://localhost:${port}/profileRawContext`); + await screenshot.takeScreenshot(page, "Profile page loaded"); + const popupPage = await newPopupWindowPromise; + if (!popupPage) { + throw new Error('Popup window was not opened'); + } + + await enterCredentials(popupPage, screenshot, username, accountPwd); + + // Wait for Graph data to display + await page.waitForSelector("xpath/.//div/ul/li[contains(., 'Name')]", { + timeout: 5000, + }); + await screenshot.takeScreenshot(page, "Graph data acquired"); + + // Verify tokens are in cache + await verifyTokenStore(BrowserCache, ["User.Read"]); + }); +}); diff --git a/samples/msal-react-samples/react16-sample/test/profileWithMsal.spec.ts b/samples/msal-react-samples/react16-sample/test/profileWithMsal.spec.ts new file mode 100644 index 0000000000..1106546587 --- /dev/null +++ b/samples/msal-react-samples/react16-sample/test/profileWithMsal.spec.ts @@ -0,0 +1,97 @@ +import * as puppeteer from "puppeteer"; +import { + Screenshot, + setupCredentials, + enterCredentials, + RETRY_TIMES, + LabClient, + LabApiQueryParams, + AzureEnvironments, + AppTypes, + BrowserCacheUtils, +} from "e2e-test-utils"; + +const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/profileWithMsal-tests`; + +async function verifyTokenStore( + BrowserCache: BrowserCacheUtils, + scopes: string[] +): Promise { + await BrowserCache.verifyTokenStore({ + scopes, + }); + const telemetryCacheEntry = await BrowserCache.getTelemetryCacheEntry( + "b5c2e510-4a17-4feb-b219-e55aa5b74144" + ); + expect(telemetryCacheEntry).not.toBeNull(); + expect(telemetryCacheEntry["cacheHits"]).toBeGreaterThanOrEqual(1); +} + +describe("/profileWithMsal", () => { + jest.retryTimes(RETRY_TIMES); + let browser: puppeteer.Browser; + let context: puppeteer.BrowserContext; + let page: puppeteer.Page; + let port: number; + let username: string; + let accountPwd: string; + let BrowserCache: BrowserCacheUtils; + + beforeAll(async () => { + // @ts-ignore + browser = await global.__BROWSER__; + // @ts-ignore + port = global.__PORT__; + + const labApiParams: LabApiQueryParams = { + azureEnvironment: AzureEnvironments.CLOUD, + appType: AppTypes.CLOUD, + }; + + const labClient = new LabClient(); + const envResponse = await labClient.getVarsByCloudEnvironment( + labApiParams + ); + + [username, accountPwd] = await setupCredentials( + envResponse[0], + labClient + ); + }); + + beforeEach(async () => { + context = await browser.createBrowserContext(); + page = await context.newPage(); + page.setDefaultTimeout(5000); + BrowserCache = new BrowserCacheUtils(page, "localStorage"); + await page.goto(`http://localhost:${port}`); + }); + + afterEach(async () => { + await page.close(); + await context.close(); + }); + + it("MsalAuthenticationTemplate - invokes loginRedirect if user is not signed in (class component w/ withMsal HOC)", async () => { + const testName = "MsalAuthenticationTemplateRedirectCase"; + const screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + await screenshot.takeScreenshot(page, "Home page loaded"); + + // Navigate to /profileWithMsal and expect redirect to be initiated without interaction + await page.goto(`http://localhost:${port}/profileWithMsal`); + await screenshot.takeScreenshot(page, "Profile page loaded"); + + await enterCredentials(page, screenshot, username, accountPwd); + + // Wait for Graph data to display + await page.waitForSelector("xpath/.//div/ul/li[contains(., 'Name')]", { + timeout: 5000, + }); + await screenshot.takeScreenshot(page, "Graph data acquired"); + + // Verify tokens are in cache + await verifyTokenStore(BrowserCache, ["User.Read"]); + }); +}); diff --git a/samples/msal-react-samples/react16-sample/tsconfig.json b/samples/msal-react-samples/react16-sample/tsconfig.json new file mode 100644 index 0000000000..a8df22f95c --- /dev/null +++ b/samples/msal-react-samples/react16-sample/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../tsconfig.json" +} \ No newline at end of file diff --git a/samples/msal-react-samples/react16-sample/vite.config.js b/samples/msal-react-samples/react16-sample/vite.config.js new file mode 100644 index 0000000000..9be5c7392c --- /dev/null +++ b/samples/msal-react-samples/react16-sample/vite.config.js @@ -0,0 +1,10 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + server: { + port: 3000, + strictPort: true, + }, +}); diff --git a/samples/msal-react-samples/react17-sample/.env.development b/samples/msal-react-samples/react17-sample/.env.development new file mode 100644 index 0000000000..b8e1347305 --- /dev/null +++ b/samples/msal-react-samples/react17-sample/.env.development @@ -0,0 +1,5 @@ +BROWSER=none + +VITE_CLIENT_ID=ENTER_CLIENT_ID_HERE +VITE_AUTHORITY=https://login.microsoftonline.com/ENTER_TENANT_ID_HERE +VITE_REDIRECT_URI=/redirect diff --git a/samples/msal-react-samples/react17-sample/.env.e2e b/samples/msal-react-samples/react17-sample/.env.e2e new file mode 100644 index 0000000000..7b1d8d02ec --- /dev/null +++ b/samples/msal-react-samples/react17-sample/.env.e2e @@ -0,0 +1,5 @@ +BROWSER=none + +VITE_CLIENT_ID=b5c2e510-4a17-4feb-b219-e55aa5b74144 +VITE_AUTHORITY=https://login.microsoftonline.com/common +VITE_REDIRECT_URI=/redirect diff --git a/samples/msal-react-samples/react17-sample/.gitignore b/samples/msal-react-samples/react17-sample/.gitignore new file mode 100644 index 0000000000..227a007b62 --- /dev/null +++ b/samples/msal-react-samples/react17-sample/.gitignore @@ -0,0 +1,20 @@ +# dependencies +/node_modules + +# testing +/coverage + +# production +/build +/dist + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/samples/msal-react-samples/react17-sample/.npmrc b/samples/msal-react-samples/react17-sample/.npmrc new file mode 100644 index 0000000000..43c97e719a --- /dev/null +++ b/samples/msal-react-samples/react17-sample/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/samples/msal-react-samples/react17-sample/README.md b/samples/msal-react-samples/react17-sample/README.md new file mode 100644 index 0000000000..112839a3ab --- /dev/null +++ b/samples/msal-react-samples/react17-sample/README.md @@ -0,0 +1,87 @@ +# MSAL.js for React Sample - React 17 Compatibility + +## About this sample + +This sample is derived from the [react-router-sample](../react-router-sample) and demonstrates MSAL React running with **React 17**. It uses `ReactDOM.render` (React 16/17 API) for rendering and MUI v5 (`@mui/material`) for UI components. + +## Notable files and what they demonstrate + +1. `./src/App.jsx` - Shows implementation of `MsalProvider`, all children will have access to `@azure/msal-react` context, hooks and components. +1. `./src/index.jsx` - Shows initialization of the `PublicClientApplication` that is passed to `App.jsx` +1. `./src/pages/Home.jsx` - Homepage, shows how to conditionally render content using `AuthenticatedTemplate` and `UnauthenticatedTemplate` depending on whether or not a user is signed in. +1. `./src/pages/Profile.jsx` - Example of a protected route using `MsalAuthenticationTemplate`. If a user is not yet signed in, signin will be invoked automatically. If a user is signed in it will acquire an access token and make a call to MS Graph to fetch user profile data. +1. `./src/authConfig.js` - Configuration options for `PublicClientApplication` and token requests. +1. `./src/ui-components/SignInSignOutButton.jsx` - Example of how to conditionally render a Sign In or Sign Out button using the `useIsAuthenticated` hook. +1. `./src/ui-components/SignInButton.jsx` - Example of how to get the `PublicClientApplication` instance using the `useMsal` hook and invoking a login function. +1. `./src/ui-components/SignOutButton.jsx` - Example of how to get the `PublicClientApplication` instance using the `useMsal` hook and invoking a logout function. +1. `./src/utils/MsGraphApiCall.js` - Example of how to call the MS Graph API with an access token. +1. `./src/utils/NavigationClient.js` - Example implementation of `INavigationClient` which can be used to override the default navigation functions MSAL.js uses + +### (Optional) MSAL React and class components + +For a demonstration of how to use MSAL React with class components, see: `./src/pages/ProfileWithMsal.jsx` and `./src/pages/ProfileRawContext.jsx`. + +*After* you initialize `MsalProvider`, there are 3 approaches you can take to protect your class components with MSAL React: + +1. Wrap each component that you want to protect with `withMsal` higher-order component (HOC) (e.g. [Profile](./src/pages/ProfileWithMsal.jsx#Profile)). +1. Consume the raw context directly (e.g. [ProfileContent](./src/pages/ProfileRawContext.jsx#ProfileContent)). +1. Pass context down from a parent component that has access to the `msalContext` via one of the other means above (e.g. [ProfileContent](./src/pages/ProfileWithMsal.jsx#ProfileContent)). + +For more information, visit: + +- [Docs: Class Components](../../../lib/msal-react/docs/class-components.md) +- [MSAL React FAQ](../../../lib/msal-react/FAQ.md) + +## How to run the sample + +### Pre-requisites + +- Ensure [all pre-requisites](../../../lib/msal-react/README.md#prerequisites) have been completed to run `@azure/msal-react`. +- Install node.js if needed (). + +### Configure the application + +- Open `./.env.development` in an editor. +- Replace `ENTER_CLIENT_ID_HERE` with the Application (client) ID from the portal registration, or use the currently configured lab registration. +- Replace `ENTER_TENANT_ID_HERE` with the tenant ID from the portal registration, or use the currently configured lab registration. + - Optionally, you may replace any of the other parameters, or you can remove them and use the default values. + +These parameters are used in `./src/authConfig.js` to configure MSAL. + +#### Install npm dependencies for sample + +```bash +# Install dev dependencies for msal-react and msal-browser from root of repo +npm install + +# Change directory to sample directory +cd samples/msal-react-samples/react17-sample + +# Build packages locally +npm run build:package +``` + +#### Running the sample development server + +1. In a command prompt, run `npm start`. +1. Open [http://localhost:3000](http://localhost:3000) to view it in the browser. +1. Open [http://localhost:3000/profile](http://localhost:3000/profile) to see an example of a protected route. If you are not yet signed in, signin will be invoked automatically. + +The page will reload if you make edits. +You will also see any lint errors in the console. + +- In the web page, click on the "Login" button and select either `Sign in using Popup` or `Sign in using Redirect` to begin the auth flow. + +#### Running the sample production server + +1. In a command prompt, run `npm run build`. +1. Next run `npx vite preview --port 3000 --strictPort`. +1. Open [http://localhost:3000](http://localhost:3000) to view it in the browser. +1. Open [http://localhost:3000/profile](http://localhost:3000/profile) to see an example of a protected route. If you are not yet signed in, signin will be invoked automatically. + +#### Learn more about the 3rd-party libraries used to create this sample + +- [React documentation](https://reactjs.org/). +- [Vite documentation](https://vite.dev/guide/) +- [React Router documentation](https://reactrouter.com/web/guides/quick-start) +- [Material-UI documentation](https://material-ui.com/getting-started/installation/) diff --git a/samples/msal-react-samples/react17-sample/index.html b/samples/msal-react-samples/react17-sample/index.html new file mode 100644 index 0000000000..884d5dc1bd --- /dev/null +++ b/samples/msal-react-samples/react17-sample/index.html @@ -0,0 +1,19 @@ + + + + + + + + + MSAL-React Sample + + + +
+ + + diff --git a/samples/msal-react-samples/react17-sample/jest.config.cjs b/samples/msal-react-samples/react17-sample/jest.config.cjs new file mode 100644 index 0000000000..51aa2c62ba --- /dev/null +++ b/samples/msal-react-samples/react17-sample/jest.config.cjs @@ -0,0 +1,8 @@ +module.exports = { + displayName: "React 17 Compat", + globals: { + __PORT__: 3000, + __STARTCMD__: "env-cmd -f .env.e2e npm start", + }, + preset: "../../e2eTestUtils/jest-puppeteer-utils/jest-preset.js", +}; diff --git a/samples/msal-react-samples/react17-sample/package.json b/samples/msal-react-samples/react17-sample/package.json new file mode 100644 index 0000000000..66af55284b --- /dev/null +++ b/samples/msal-react-samples/react17-sample/package.json @@ -0,0 +1,50 @@ +{ + "name": "react17-sample", + "version": "0.1.0", + "private": true, + "type": "module", + "dependencies": { + "@azure/msal-browser": "^5.0.0", + "@azure/msal-react": "^5.0.0", + "@emotion/react": "^11.10.5", + "@emotion/styled": "^11.10.5", + "@mui/icons-material": "^5.10.16", + "@mui/material": "^5.10.17", + "react": "^17.0.0", + "react-dom": "^17.0.0", + "react-router-dom": "^6.7.0" + }, + "scripts": { + "start": "vite", + "test:e2e": "jest", + "build": "vite build", + "build:package": "cd ../../../ && npm run build:all --workspace=lib/msal-react" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@types/jest": "^29.5.0", + "@vitejs/plugin-react": "^4.3.3", + "e2e-test-utils": "file:../../e2eTestUtils", + "env-cmd": "^10.1.0", + "jest": "^29.5.0", + "jest-junit": "^16.0.0", + "ts-jest": "^29.1.0", + "vite": "^5.4.21" + }, + "jest-junit": { + "suiteNameTemplate": "React 17 Compat Tests", + "outputDirectory": ".", + "outputName": "test-results.xml" + } +} \ No newline at end of file diff --git a/samples/msal-react-samples/react17-sample/public/favicon.ico b/samples/msal-react-samples/react17-sample/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/samples/msal-react-samples/react17-sample/public/favicon.ico differ diff --git a/samples/msal-react-samples/react17-sample/src/App.jsx b/samples/msal-react-samples/react17-sample/src/App.jsx new file mode 100644 index 0000000000..6756321e28 --- /dev/null +++ b/samples/msal-react-samples/react17-sample/src/App.jsx @@ -0,0 +1,62 @@ +import { Routes, Route, useNavigate, useLocation } from "react-router-dom"; +// Material-UI imports +import Grid from "@mui/material/Grid"; + +// MSAL imports +import { MsalProvider } from "@azure/msal-react"; +import { CustomNavigationClient } from "./utils/NavigationClient"; + +// Sample app imports +import { PageLayout } from "./ui-components/PageLayout"; +import { Home } from "./pages/Home"; +import { Profile } from "./pages/Profile"; +import { Logout } from "./pages/Logout"; +import { Redirect } from "./pages/Redirect"; + +// Class-based equivalents of "Profile" component +import { ProfileWithMsal } from "./pages/ProfileWithMsal"; +import { ProfileRawContext } from "./pages/ProfileRawContext"; +import { ProfileUseMsalAuthenticationHook } from "./pages/ProfileUseMsalAuthenticationHook"; + +function App({ pca }) { + // The next 3 lines are optional. This is how you configure MSAL to take advantage of the router's navigate functions when MSAL redirects between pages in your app + const navigate = useNavigate(); + const location = useLocation(); + const navigationClient = new CustomNavigationClient(navigate); + pca.setNavigationClient(navigationClient); + + // Don't wrap redirect page in MsalProvider to prevent MSAL from consuming the auth response + const isRedirectPage = location.pathname === '/redirect'; + + if (isRedirectPage) { + return ; + } + + return ( + + + + + + + + ); +} + +function Pages() { + return ( + + } /> + } /> + } /> + } + /> + } /> + } /> + + ); +} + +export default App; diff --git a/samples/msal-react-samples/react17-sample/src/authConfig.js b/samples/msal-react-samples/react17-sample/src/authConfig.js new file mode 100644 index 0000000000..73ea34a7f7 --- /dev/null +++ b/samples/msal-react-samples/react17-sample/src/authConfig.js @@ -0,0 +1,51 @@ +import { LogLevel, BrowserUtils } from "@azure/msal-browser"; + +// Config object to be passed to Msal on creation +export const msalConfig = { + auth: { + clientId: import.meta.env.VITE_CLIENT_ID, + authority: import.meta.env.VITE_AUTHORITY, + redirectUri: import.meta.env.VITE_REDIRECT_URI, + postLogoutRedirectUri: "/", + onRedirectNavigate: () => !BrowserUtils.isInIframe() + }, + cache: { + cacheLocation: "localStorage", + }, + system: { + allowPlatformBroker: false, // Disables WAM Broker + loggerOptions: { + loggerCallback: (level, message, containsPii) => { + if (containsPii) { + return; + } + switch (level) { + case LogLevel.Error: + console.error(message); + return; + case LogLevel.Info: + console.info(message); + return; + case LogLevel.Verbose: + console.debug(message); + return; + case LogLevel.Warning: + console.warn(message); + return; + default: + return; + } + }, + }, + }, +}; + +// Add here scopes for id token to be used at MS Identity Platform endpoints. +export const loginRequest = { + scopes: ["User.Read"] +}; + +// Add here the endpoints for MS Graph API services you would like to use. +export const graphConfig = { + graphMeEndpoint: "https://graph.microsoft.com/v1.0/me" +}; diff --git a/samples/msal-react-samples/react17-sample/src/index.jsx b/samples/msal-react-samples/react17-sample/src/index.jsx new file mode 100644 index 0000000000..6c560955be --- /dev/null +++ b/samples/msal-react-samples/react17-sample/src/index.jsx @@ -0,0 +1,37 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { BrowserRouter as Router } from "react-router-dom"; +import { ThemeProvider } from "@mui/material/styles"; +import { theme } from "./styles/theme"; +import App from './App'; + +// MSAL imports +import { PublicClientApplication, EventType } from "@azure/msal-browser"; +import { msalConfig } from "./authConfig"; + +export const msalInstance = new PublicClientApplication(msalConfig); + +msalInstance.initialize().then(() => { + // Default to using the first account if no account is active on page load + if (!msalInstance.getActiveAccount() && msalInstance.getAllAccounts().length > 0) { + // Account selection logic is app dependent. Adjust as needed for different use cases. + msalInstance.setActiveAccount(msalInstance.getAllAccounts()[0]); + } + + msalInstance.addEventCallback((event) => { + if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) { + const account = event.payload; + msalInstance.setActiveAccount(account); + } + }); + + // React 17 uses ReactDOM.render instead of createRoot + ReactDOM.render( + + + + + , + document.getElementById("root") + ); +}); diff --git a/samples/msal-react-samples/react17-sample/src/pages/Home.jsx b/samples/msal-react-samples/react17-sample/src/pages/Home.jsx new file mode 100644 index 0000000000..288c3eb8bb --- /dev/null +++ b/samples/msal-react-samples/react17-sample/src/pages/Home.jsx @@ -0,0 +1,26 @@ +import { AuthenticatedTemplate, UnauthenticatedTemplate } from "@azure/msal-react"; +import Button from "@mui/material/Button"; +import ButtonGroup from "@mui/material/ButtonGroup"; +import Typography from "@mui/material/Typography"; +import { Link as RouterLink } from "react-router-dom"; + +export function Home() { + return ( + <> + + + + + + + + + + + +
Please sign-in to see your profile information.
+
+
+ + ); +} \ No newline at end of file diff --git a/samples/msal-react-samples/react17-sample/src/pages/Logout.jsx b/samples/msal-react-samples/react17-sample/src/pages/Logout.jsx new file mode 100644 index 0000000000..21d765b948 --- /dev/null +++ b/samples/msal-react-samples/react17-sample/src/pages/Logout.jsx @@ -0,0 +1,16 @@ +import React, { useEffect } from "react"; +import { useMsal } from "@azure/msal-react"; + +export function Logout() { + const { instance } = useMsal(); + + useEffect(() => { + instance.logoutRedirect({ + account: instance.getActiveAccount(), + }) + }, [ instance ]); + + return ( +
Logout
+ ) +} diff --git a/samples/msal-react-samples/react17-sample/src/pages/Profile.jsx b/samples/msal-react-samples/react17-sample/src/pages/Profile.jsx new file mode 100644 index 0000000000..c015c779f1 --- /dev/null +++ b/samples/msal-react-samples/react17-sample/src/pages/Profile.jsx @@ -0,0 +1,79 @@ +import { useEffect, useState, useCallback } from "react"; + +// Msal imports +import { MsalAuthenticationTemplate, useMsal } from "@azure/msal-react"; +import { EventType, InteractionType, InteractionRequiredAuthError } from "@azure/msal-browser"; +import { loginRequest } from "../authConfig"; + +// Sample app imports +import { ProfileData } from "../ui-components/ProfileData"; +import { Loading } from "../ui-components/Loading"; +import { ErrorComponent } from "../ui-components/ErrorComponent"; +import { callMsGraph } from "../utils/MsGraphApiCall"; + +// Material-ui imports +import Paper from "@mui/material/Paper"; + +const ProfileContent = () => { + const { instance } = useMsal(); + const [graphData, setGraphData] = useState(null); + + const fetchProfile = useCallback(() => { + if (!instance.getActiveAccount()) { + return; + } + callMsGraph().then(response => setGraphData(response)).catch((e) => { + if (e instanceof InteractionRequiredAuthError) { + instance.acquireTokenRedirect({ + ...loginRequest, + account: instance.getActiveAccount() + }); + } + }); + }, [instance]); + + useEffect(() => { + // Attempt to fetch profile data immediately + fetchProfile(); + + // Subscribe to active account changes so the Graph call is retried + // once setActiveAccount has been called. In React 16/17 the render + // triggered by ACQUIRE_TOKEN_SUCCESS fires before the LOGIN_SUCCESS + // handler sets the active account, so getActiveAccount() returns null + // on the first attempt. + const callbackId = instance.addEventCallback((event) => { + if (event.eventType === EventType.ACTIVE_ACCOUNT_CHANGED) { + fetchProfile(); + } + }); + + return () => { + if (callbackId) { + instance.removeEventCallback(callbackId); + } + }; + }, [instance, fetchProfile]); + + return ( + + { graphData ? : null } + + ); +}; + +export function Profile() { + const authRequest = { + ...loginRequest + }; + + return ( + + + + ) +}; \ No newline at end of file diff --git a/samples/msal-react-samples/react17-sample/src/pages/ProfileRawContext.jsx b/samples/msal-react-samples/react17-sample/src/pages/ProfileRawContext.jsx new file mode 100644 index 0000000000..38ed1622d5 --- /dev/null +++ b/samples/msal-react-samples/react17-sample/src/pages/ProfileRawContext.jsx @@ -0,0 +1,112 @@ +import { Component } from "react"; + +// Msal imports +import { MsalAuthenticationTemplate, MsalContext } from "@azure/msal-react"; +import { InteractionType, EventType, InteractionRequiredAuthError } from "@azure/msal-browser"; +import { loginRequest } from "../authConfig"; + +// Sample app imports +import { ProfileData } from "../ui-components/ProfileData"; +import { Loading } from "../ui-components/Loading"; +import { ErrorComponent } from "../ui-components/ErrorComponent"; +import { callMsGraph } from "../utils/MsGraphApiCall"; + +// Material-ui imports +import Paper from "@mui/material/Paper"; + + +/** + * This class is using the raw context directly. The available + * objects and methods are the same as in "withMsal" HOC usage. + */ +class ProfileContent extends Component { + + static contextType = MsalContext; + + constructor(props) { + super(props) + + this.state = { + graphData: null, + } + + this.callbackId = null; + } + + fetchGraphData() { + if (this.state.graphData) { + return; + } + + const instance = this.context.instance; + if (!instance.getActiveAccount()) { + return; + } + + callMsGraph().then(response => this.setState({graphData: response})).catch((e) => { + if (e instanceof InteractionRequiredAuthError) { + instance.acquireTokenRedirect({ + ...loginRequest, + account: instance.getActiveAccount() + }); + } + }); + } + + componentDidMount() { + // Attempt to fetch profile data immediately + this.fetchGraphData(); + + // Subscribe to active account changes so the Graph call is retried + // once setActiveAccount has been called. In React 16/17 the render + // triggered by ACQUIRE_TOKEN_SUCCESS fires before the LOGIN_SUCCESS + // handler sets the active account, so getActiveAccount() returns null + // on the first attempt. + this.callbackId = this.context.instance.addEventCallback((event) => { + if (event.eventType === EventType.ACTIVE_ACCOUNT_CHANGED) { + this.fetchGraphData(); + } + }); + } + + componentWillUnmount() { + if (this.callbackId) { + this.context.instance.removeEventCallback(this.callbackId); + } + } + + render() { + return ( + + { this.state.graphData ? : null } + + ); + } +} + +/** + * This class is using "withMsal" HOC. It passes down the msalContext + * as a prop to its children. + */ +class Profile extends Component { + + render() { + + const authRequest = { + ...loginRequest + }; + + return ( + + + + ); + } +} + +export const ProfileRawContext = Profile diff --git a/samples/msal-react-samples/react17-sample/src/pages/ProfileUseMsalAuthenticationHook.jsx b/samples/msal-react-samples/react17-sample/src/pages/ProfileUseMsalAuthenticationHook.jsx new file mode 100644 index 0000000000..43bdf3fe11 --- /dev/null +++ b/samples/msal-react-samples/react17-sample/src/pages/ProfileUseMsalAuthenticationHook.jsx @@ -0,0 +1,51 @@ +import { useEffect, useState } from "react"; + +// Msal imports +import { useMsalAuthentication } from "@azure/msal-react"; +import { InteractionType } from "@azure/msal-browser"; +import { loginRequest } from "../authConfig"; + +// Sample app imports +import { ProfileData } from "../ui-components/ProfileData"; +import { ErrorComponent } from "../ui-components/ErrorComponent"; +import { callMsGraph } from "../utils/MsGraphApiCall"; + +// Material-ui imports +import Paper from "@mui/material/Paper"; + +const ProfileContent = () => { + const [graphData, setGraphData] = useState(null); + const { result, error } = useMsalAuthentication(InteractionType.Popup, { + ...loginRequest, + }); + + useEffect(() => { + if (!!graphData) { + // We already have the data, no need to call the API + return; + } + + if (!!error) { + // Error occurred attempting to acquire a token, either handle the error or do nothing + return; + } + + if (result) { + callMsGraph().then(response => setGraphData(response)); + } + }, [error, result, graphData]); + + if (error) { + return ; + } + + return ( + + { graphData ? : null } + + ); +}; + +export function ProfileUseMsalAuthenticationHook() { + return +}; diff --git a/samples/msal-react-samples/react17-sample/src/pages/ProfileWithMsal.jsx b/samples/msal-react-samples/react17-sample/src/pages/ProfileWithMsal.jsx new file mode 100644 index 0000000000..1a2b0b0335 --- /dev/null +++ b/samples/msal-react-samples/react17-sample/src/pages/ProfileWithMsal.jsx @@ -0,0 +1,87 @@ +import { Component } from "react"; + +// Msal imports +import { MsalAuthenticationTemplate, withMsal } from "@azure/msal-react"; +import { InteractionType, InteractionStatus, InteractionRequiredAuthError } from "@azure/msal-browser"; +import { loginRequest } from "../authConfig"; + +// Sample app imports +import { ProfileData } from "../ui-components/ProfileData"; +import { Loading } from "../ui-components/Loading"; +import { ErrorComponent } from "../ui-components/ErrorComponent"; +import { callMsGraph } from "../utils/MsGraphApiCall"; + +// Material-ui imports +import Paper from "@mui/material/Paper"; + +/** + * This class is a child component of "Profile". MsalContext is passed + * down from the parent and available as a prop here. + */ +class ProfileContent extends Component { + + constructor(props) { + super(props) + + this.state = { + graphData: null, + } + } + + setGraphData() { + if (!this.state.graphData && this.props.msalContext.inProgress === InteractionStatus.None) { + callMsGraph().then(response => this.setState({graphData: response})).catch((e) => { + if (e instanceof InteractionRequiredAuthError) { + this.props.msalContext.instance.acquireTokenRedirect({ + ...loginRequest, + account: this.props.msalContext.instance.getActiveAccount() + }); + } + }); + } + } + + componentDidMount() { + this.setGraphData(); + } + + componentDidUpdate() { + this.setGraphData(); + } + + render() { + return ( + + { this.state.graphData ? : null } + + ); + } +} + +/** + * This class is using "withMsal" HOC and has access to authentication + * state. It passes down the msalContext as a prop to its children. + */ +class Profile extends Component { + + render() { + + const authRequest = { + ...loginRequest + }; + + return ( + + + + ); + } +} + +// Wrap your class component to access authentication state as props +export const ProfileWithMsal = withMsal(Profile); \ No newline at end of file diff --git a/samples/msal-react-samples/react17-sample/src/pages/Redirect.jsx b/samples/msal-react-samples/react17-sample/src/pages/Redirect.jsx new file mode 100644 index 0000000000..dd4529aaf3 --- /dev/null +++ b/samples/msal-react-samples/react17-sample/src/pages/Redirect.jsx @@ -0,0 +1,20 @@ +// This page serves the redirect bridge for MSAL authentication +// It's excluded from MsalProvider wrapper in App.js to prevent MSAL from processing the hash +import { useEffect } from "react"; +import { broadcastResponseToMainFrame } from "@azure/msal-browser/redirect-bridge"; + +export function Redirect() { + useEffect(() => { + // Call broadcastResponseToMainFrame when component mounts + broadcastResponseToMainFrame().catch((error) => { + console.error("Error broadcasting response to main frame:", error); + }); + }, []); + + return ( +
+

Processing authentication...

+
+ ); +} + diff --git a/samples/msal-react-samples/react17-sample/src/styles/theme.js b/samples/msal-react-samples/react17-sample/src/styles/theme.js new file mode 100644 index 0000000000..442acc8121 --- /dev/null +++ b/samples/msal-react-samples/react17-sample/src/styles/theme.js @@ -0,0 +1,20 @@ +import { unstable_createMuiStrictModeTheme as createMuiTheme } from '@mui/material/styles'; +import { red } from '@mui/material/colors'; + +// Create a theme instance. +export const theme = createMuiTheme({ + palette: { + primary: { + main: '#556cd6', + }, + secondary: { + main: '#19857b', + }, + error: { + main: red.A400, + }, + background: { + default: '#fff', + }, + }, +}); diff --git a/samples/msal-react-samples/react17-sample/src/ui-components/AccountPicker.jsx b/samples/msal-react-samples/react17-sample/src/ui-components/AccountPicker.jsx new file mode 100644 index 0000000000..6b1f855fe3 --- /dev/null +++ b/samples/msal-react-samples/react17-sample/src/ui-components/AccountPicker.jsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { useMsal } from "@azure/msal-react"; +import Avatar from '@mui/material/Avatar'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemAvatar from '@mui/material/ListItemAvatar'; +import ListItemText from '@mui/material/ListItemText'; +import DialogTitle from '@mui/material/DialogTitle'; +import Dialog from '@mui/material/Dialog'; +import PersonIcon from '@mui/icons-material/Person'; +import AddIcon from '@mui/icons-material/Add'; +import { loginRequest } from "../authConfig"; + +export const AccountPicker = (props) => { + const { instance, accounts } = useMsal(); + const { onClose, open } = props; + + const handleListItemClick = (account) => { + instance.setActiveAccount(account); + if (!account) { + instance.loginRedirect({ + ...loginRequest, + prompt: "login" + }) + } else { + // To ensure account related page attributes update after the account is changed + window.location.reload(); + } + + onClose(account); + }; + + return ( + + Set active account + + {accounts.map((account) => ( + handleListItemClick(account)} key={account.homeAccountId}> + + + + + + + + ))} + + handleListItemClick(null)}> + + + + + + + + + + ); +}; \ No newline at end of file diff --git a/samples/msal-react-samples/react17-sample/src/ui-components/ErrorComponent.jsx b/samples/msal-react-samples/react17-sample/src/ui-components/ErrorComponent.jsx new file mode 100644 index 0000000000..de8ddd2607 --- /dev/null +++ b/samples/msal-react-samples/react17-sample/src/ui-components/ErrorComponent.jsx @@ -0,0 +1,5 @@ +import { Typography } from "@mui/material"; + +export const ErrorComponent = ({error}) => { + return An Error Occurred: {error.errorCode}; +} \ No newline at end of file diff --git a/samples/msal-react-samples/react17-sample/src/ui-components/Loading.jsx b/samples/msal-react-samples/react17-sample/src/ui-components/Loading.jsx new file mode 100644 index 0000000000..c2ae494c80 --- /dev/null +++ b/samples/msal-react-samples/react17-sample/src/ui-components/Loading.jsx @@ -0,0 +1,5 @@ +import { Typography } from "@mui/material"; + +export const Loading = () => { + return Authentication in progress... +} \ No newline at end of file diff --git a/samples/msal-react-samples/react17-sample/src/ui-components/NavBar.jsx b/samples/msal-react-samples/react17-sample/src/ui-components/NavBar.jsx new file mode 100644 index 0000000000..bea9940fbc --- /dev/null +++ b/samples/msal-react-samples/react17-sample/src/ui-components/NavBar.jsx @@ -0,0 +1,25 @@ +import AppBar from "@mui/material/AppBar"; +import Toolbar from "@mui/material/Toolbar"; +import Link from "@mui/material/Link"; +import Typography from "@mui/material/Typography"; +import WelcomeName from "./WelcomeName"; +import SignInSignOutButton from "./SignInSignOutButton"; +import { Link as RouterLink } from "react-router-dom"; + +const NavBar = () => { + return ( +
+ + + + MS Identity Platform + + + + + +
+ ); +}; + +export default NavBar; \ No newline at end of file diff --git a/samples/msal-react-samples/react17-sample/src/ui-components/PageLayout.jsx b/samples/msal-react-samples/react17-sample/src/ui-components/PageLayout.jsx new file mode 100644 index 0000000000..f4e5672879 --- /dev/null +++ b/samples/msal-react-samples/react17-sample/src/ui-components/PageLayout.jsx @@ -0,0 +1,16 @@ +import Typography from "@mui/material/Typography"; +import NavBar from "./NavBar"; + +export const PageLayout = (props) => { + return ( + <> + + +
Welcome to the Microsoft Authentication Library For React Quickstart
+
+
+
+ {props.children} + + ); +}; \ No newline at end of file diff --git a/samples/msal-react-samples/react17-sample/src/ui-components/ProfileData.jsx b/samples/msal-react-samples/react17-sample/src/ui-components/ProfileData.jsx new file mode 100644 index 0000000000..f7aa6223c7 --- /dev/null +++ b/samples/msal-react-samples/react17-sample/src/ui-components/ProfileData.jsx @@ -0,0 +1,78 @@ +import React from "react"; +import List from "@mui/material/List"; +import ListItem from "@mui/material/ListItem"; +import ListItemText from "@mui/material/ListItemText"; +import ListItemAvatar from "@mui/material/ListItemAvatar"; +import Avatar from "@mui/material/Avatar"; +import PersonIcon from '@mui/icons-material/Person'; +import WorkIcon from "@mui/icons-material/Work"; +import MailIcon from '@mui/icons-material/Mail'; +import PhoneIcon from '@mui/icons-material/Phone'; +import LocationOnIcon from '@mui/icons-material/LocationOn'; + +export const ProfileData = ({graphData}) => { + return ( + + + + + + + + ); +}; + +const NameListItem = ({name}) => ( + + + + + + + + +); + +const JobTitleListItem = ({jobTitle}) => ( + + + + + + + + +); + +const MailListItem = ({mail}) => ( + + + + + + + + +); + +const PhoneListItem = ({phone}) => ( + + + + + + + + +); + +const LocationListItem = ({location}) => ( + + + + + + + + +); diff --git a/samples/msal-react-samples/react17-sample/src/ui-components/SignInButton.jsx b/samples/msal-react-samples/react17-sample/src/ui-components/SignInButton.jsx new file mode 100644 index 0000000000..9ffc1751ef --- /dev/null +++ b/samples/msal-react-samples/react17-sample/src/ui-components/SignInButton.jsx @@ -0,0 +1,161 @@ +import { useState, useRef, useEffect } from "react"; +import { useMsal } from "@azure/msal-react"; +import Button from "@mui/material/Button"; +import MenuItem from '@mui/material/MenuItem'; +import Menu from '@mui/material/Menu'; +import Alert from '@mui/material/Alert'; +import AlertTitle from '@mui/material/AlertTitle'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; +import { loginRequest } from "../authConfig"; + +export const SignInButton = () => { + const { instance } = useMsal(); + + const [anchorEl, setAnchorEl] = useState(null); + const [showRetryDialog, setShowRetryDialog] = useState(false); + const [retryRequested, setRetryRequested] = useState(false); + const [showPopupWarning, setShowPopupWarning] = useState(false); + const open = Boolean(anchorEl); + + // Track mounted state to avoid setting state after unmount (React 17 does not batch async state updates) + const isMountedRef = useRef(true); + useEffect(() => { + return () => { + isMountedRef.current = false; + }; + }, []); + + const handleLogin = async (loginType) => { + setAnchorEl(null); + + if (loginType === "popup") { + // Show warning when popup is about to open + setShowPopupWarning(true); + + try { + await instance.loginPopup({ + ...loginRequest, + // Only override if user explicitly clicked retry + overrideInteractionInProgress: retryRequested + }); + + // Hide warning on success — guard against unmounted component + if (isMountedRef.current) { + setShowPopupWarning(false); + setRetryRequested(false); + } + } catch (error) { + // Hide warning on error — guard against unmounted component + if (isMountedRef.current) { + setShowPopupWarning(false); + + if (error.errorCode === 'interaction_in_progress') { + // Show retry dialog - let user decide whether to retry + setShowRetryDialog(true); + } else { + // Reset retry flag for other errors + setRetryRequested(false); + console.error(error); + } + } + } + } else if (loginType === "redirect") { + instance.loginRedirect(loginRequest); + } + } + + const handleRetry = () => { + setShowRetryDialog(false); + setRetryRequested(true); // User explicitly requested retry + handleLogin("popup"); + } + + const handleCancelRetry = () => { + setShowRetryDialog(false); + setRetryRequested(false); + } + + return ( +
+ + setAnchorEl(null)} + > + handleLogin("popup")} key="loginPopup">Sign in using Popup + handleLogin("redirect")} key="loginRedirect">Sign in using Redirect + + + {/* Warning message during popup authentication */} + {showPopupWarning && ( + + Authentication in Progress + Please complete authentication in the popup window. Do not close the popup until authentication is complete. + + )} + + {/* Retry dialog for interaction_in_progress errors */} + + Authentication Already in Progress + + + An authentication request is already in progress. This may happen if: + + +
    +
  • You closed the popup window before completing authentication
  • +
  • The previous authentication attempt is still pending
  • +
+
+ + Would you like to cancel the pending authentication and try again? + + + Warning + Retrying will cancel the pending authentication request. + +
+ + + + +
+
+ ) +}; diff --git a/samples/msal-react-samples/react17-sample/src/ui-components/SignInSignOutButton.jsx b/samples/msal-react-samples/react17-sample/src/ui-components/SignInSignOutButton.jsx new file mode 100644 index 0000000000..61633e1459 --- /dev/null +++ b/samples/msal-react-samples/react17-sample/src/ui-components/SignInSignOutButton.jsx @@ -0,0 +1,20 @@ +import { useIsAuthenticated, useMsal } from "@azure/msal-react"; +import { SignInButton } from "./SignInButton"; +import { SignOutButton } from "./SignOutButton"; +import { InteractionStatus } from "@azure/msal-browser"; + +const SignInSignOutButton = () => { + const { inProgress } = useMsal(); + const isAuthenticated = useIsAuthenticated(); + + if (isAuthenticated) { + return ; + } else if (inProgress !== InteractionStatus.Startup && inProgress !== InteractionStatus.HandleRedirect) { + // inProgress check prevents sign-in button from being displayed briefly after returning from a redirect sign-in. Processing the server response takes a render cycle or two + return ; + } else { + return null; + } +} + +export default SignInSignOutButton; \ No newline at end of file diff --git a/samples/msal-react-samples/react17-sample/src/ui-components/SignOutButton.jsx b/samples/msal-react-samples/react17-sample/src/ui-components/SignOutButton.jsx new file mode 100644 index 0000000000..2a9fbd451e --- /dev/null +++ b/samples/msal-react-samples/react17-sample/src/ui-components/SignOutButton.jsx @@ -0,0 +1,65 @@ +import { useState } from "react"; +import { useMsal } from "@azure/msal-react"; +import IconButton from '@mui/material/IconButton'; +import AccountCircle from "@mui/icons-material/AccountCircle"; +import MenuItem from '@mui/material/MenuItem'; +import Menu from '@mui/material/Menu'; +import { AccountPicker } from "./AccountPicker"; + +export const SignOutButton = () => { + const { instance } = useMsal(); + const [accountSelectorOpen, setOpen] = useState(false); + + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + const handleLogout = (logoutType) => { + setAnchorEl(null); + + if (logoutType === "popup") { + instance.logoutPopup(); + } else if (logoutType === "redirect") { + instance.logoutRedirect(); + } + } + + const handleAccountSelection = () => { + setAnchorEl(null); + setOpen(true); + } + + const handleClose = () => { + setOpen(false); + }; + + return ( +
+ setAnchorEl(event.currentTarget)} + color="inherit" + > + + + setAnchorEl(null)} + > + handleAccountSelection()} key="switchAccount">Switch Account + handleLogout("popup")} key="logoutPopup">Logout using Popup + handleLogout("redirect")} key="logoutRedirect">Logout using Redirect + + +
+ ) +}; \ No newline at end of file diff --git a/samples/msal-react-samples/react17-sample/src/ui-components/WelcomeName.jsx b/samples/msal-react-samples/react17-sample/src/ui-components/WelcomeName.jsx new file mode 100644 index 0000000000..ac4ad1754c --- /dev/null +++ b/samples/msal-react-samples/react17-sample/src/ui-components/WelcomeName.jsx @@ -0,0 +1,47 @@ +import { useEffect, useState, useCallback } from "react"; +import { useMsal } from "@azure/msal-react"; +import { EventType } from "@azure/msal-browser"; +import Typography from "@mui/material/Typography"; + +const WelcomeName = () => { + const { instance } = useMsal(); + const [name, setName] = useState(null); + + const updateName = useCallback(() => { + const activeAccount = instance.getActiveAccount(); + if (activeAccount) { + setName(activeAccount.name.split(' ')[0]); + } else { + setName(null); + } + }, [instance]); + + useEffect(() => { + // Set the name from the current active account on mount + updateName(); + + // Subscribe to active account changes so the component updates when + // setActiveAccount is called. This avoids the React 16/17 batching issue where the + // render triggered by ACQUIRE_TOKEN_SUCCESS runs before setActiveAccount + // has been called, causing getActiveAccount() to return null. + const callbackId = instance.addEventCallback((event) => { + if (event.eventType === EventType.ACTIVE_ACCOUNT_CHANGED) { + updateName(); + } + }); + + return () => { + if (callbackId) { + instance.removeEventCallback(callbackId); + } + }; + }, [instance, updateName]); + + if (name) { + return Welcome, {name}; + } else { + return null; + } +}; + +export default WelcomeName; \ No newline at end of file diff --git a/samples/msal-react-samples/react17-sample/src/utils/MsGraphApiCall.js b/samples/msal-react-samples/react17-sample/src/utils/MsGraphApiCall.js new file mode 100644 index 0000000000..360493a86b --- /dev/null +++ b/samples/msal-react-samples/react17-sample/src/utils/MsGraphApiCall.js @@ -0,0 +1,33 @@ +import { loginRequest, graphConfig } from "../authConfig"; +import { msalInstance } from "../index"; + +export async function callMsGraph(accessToken) { + if (!accessToken) { + const account = msalInstance.getActiveAccount(); + if (!account) { + throw Error("No active account! Verify a user has been signed in and setActiveAccount has been called."); + } + + const response = await msalInstance.acquireTokenSilent({ + ...loginRequest, + account: account + }); + accessToken = response.accessToken; + } + + const headers = new Headers(); + const bearer = `Bearer ${accessToken}`; + + headers.append("Authorization", bearer); + + const options = { + method: "GET", + headers: headers + }; + + const response = await fetch(graphConfig.graphMeEndpoint, options); + if (!response.ok) { + throw new Error(`MS Graph request failed: ${response.status} ${response.statusText}`); + } + return response.json(); +} diff --git a/samples/msal-react-samples/react17-sample/src/utils/NavigationClient.js b/samples/msal-react-samples/react17-sample/src/utils/NavigationClient.js new file mode 100644 index 0000000000..70eab8c6d7 --- /dev/null +++ b/samples/msal-react-samples/react17-sample/src/utils/NavigationClient.js @@ -0,0 +1,28 @@ +import { NavigationClient } from "@azure/msal-browser"; + +/** + * This is an example for overriding the default function MSAL uses to navigate to other urls in your webpage + */ +export class CustomNavigationClient extends NavigationClient { + constructor(navigate) { + super(); + this.navigate = navigate; + } + + /** + * Navigates to other pages within the same web application + * You can use the useNavigate hook provided by react-router-dom to take advantage of client-side routing + * @param url + * @param options + */ + async navigateInternal(url, options) { + const relativePath = url.replace(window.location.origin, ""); + if (options.noHistory) { + this.navigate(relativePath, { replace: true }); + } else { + this.navigate(relativePath); + } + + return false; + } +} diff --git a/samples/msal-react-samples/react17-sample/test/home.spec.ts b/samples/msal-react-samples/react17-sample/test/home.spec.ts new file mode 100644 index 0000000000..eb2c005709 --- /dev/null +++ b/samples/msal-react-samples/react17-sample/test/home.spec.ts @@ -0,0 +1,148 @@ +import * as puppeteer from "puppeteer"; +import { + Screenshot, + setupCredentials, + enterCredentials, + RETRY_TIMES, + LabClient, + LabApiQueryParams, + AzureEnvironments, + AppTypes, + BrowserCacheUtils, +} from "e2e-test-utils"; + +const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/home-tests`; + +describe("/ (Home Page)", () => { + jest.retryTimes(RETRY_TIMES); + let browser: puppeteer.Browser; + let context: puppeteer.BrowserContext; + let page: puppeteer.Page; + let port: number; + let username: string; + let accountPwd: string; + let BrowserCache: BrowserCacheUtils; + + beforeAll(async () => { + // @ts-ignore + browser = await global.__BROWSER__; + // @ts-ignore + port = global.__PORT__; + + const labApiParams: LabApiQueryParams = { + azureEnvironment: AzureEnvironments.CLOUD, + appType: AppTypes.CLOUD, + }; + + const labClient = new LabClient(); + const envResponse = await labClient.getVarsByCloudEnvironment( + labApiParams + ); + + [username, accountPwd] = await setupCredentials( + envResponse[0], + labClient + ); + }); + + beforeEach(async () => { + context = await browser.createBrowserContext(); + page = await context.newPage(); + page.setDefaultTimeout(5000); + BrowserCache = new BrowserCacheUtils(page, "localStorage"); + await page.goto(`http://localhost:${port}`); + }); + + afterEach(async () => { + await page.close(); + await context.close(); + }); + + it("AuthenticatedTemplate - children are rendered after logging in with loginRedirect", async () => { + const testName = "redirectBaseCase"; + const screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + await screenshot.takeScreenshot(page, "Page loaded"); + + // Initiate Login + const signInButton = await page.waitForSelector( + "xpath=//button[contains(., 'Login')]" + ); + await signInButton.click(); + await screenshot.takeScreenshot(page, "Login button clicked"); + const loginRedirectButton = await page.waitForSelector( + "xpath=//li[contains(., 'Sign in using Redirect')]" + ); + await loginRedirectButton.click(); + + await enterCredentials(page, screenshot, username, accountPwd); + await screenshot.takeScreenshot(page, "Returned to app"); + + // Verify UI now displays logged in content + await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]"); + const profileButton = await page.waitForSelector( + "xpath=//header//button" + ); + await profileButton.click(); + const logoutButtons = await page.$$( + "xpath/.//li[contains(., 'Logout using')]" + ); + expect(logoutButtons.length).toBe(2); + await screenshot.takeScreenshot(page, "App signed in"); + + // Verify tokens are in cache + await BrowserCache.verifyTokenStore({ + scopes: ['User.Read'], + }); + }); + + it("AuthenticatedTemplate - children are rendered after logging in with loginPopup", async () => { + const testName = "popupBaseCase"; + const screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + await screenshot.takeScreenshot(page, "Page loaded"); + + // Initiate Login + const signInButton = await page.waitForSelector( + "xpath=//button[contains(., 'Login')]" + ); + await signInButton.click(); + await screenshot.takeScreenshot(page, "Login button clicked"); + const loginPopupButton = await page.waitForSelector( + "xpath=//li[contains(., 'Sign in using Popup')]" + ); + const newPopupWindowPromise = new Promise((resolve) => + page.once("popup", resolve) + ); + await loginPopupButton.click(); + const popupPage = await newPopupWindowPromise; + if (!popupPage) { + throw new Error('Popup window was not opened'); + } + + await enterCredentials(popupPage, screenshot, username, accountPwd); + await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]", { + timeout: 3000, + }); + await screenshot.takeScreenshot(page, "Popup closed"); + + // Verify UI now displays logged in content + await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]"); + const profileButton = await page.waitForSelector( + "xpath=//header//button" + ); + await profileButton.click(); + const logoutButtons = await page.$$( + "xpath/.//li[contains(., 'Logout using')]" + ); + expect(logoutButtons.length).toBe(2); + await screenshot.takeScreenshot(page, "App signed in"); + + // Verify tokens are in cache + await BrowserCache.verifyTokenStore({ + scopes: ['User.Read'], + }); + }); +}); diff --git a/samples/msal-react-samples/react17-sample/test/profile.spec.ts b/samples/msal-react-samples/react17-sample/test/profile.spec.ts new file mode 100644 index 0000000000..e3b383cf66 --- /dev/null +++ b/samples/msal-react-samples/react17-sample/test/profile.spec.ts @@ -0,0 +1,170 @@ +import * as puppeteer from "puppeteer"; +import { + Screenshot, + setupCredentials, + enterCredentials, + RETRY_TIMES, + LabClient, + LabApiQueryParams, + AzureEnvironments, + AppTypes, + BrowserCacheUtils, +} from "e2e-test-utils"; + +const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/profile-tests`; + +async function verifyTokenStore( + BrowserCache: BrowserCacheUtils, + scopes: string[] +): Promise { + await BrowserCache.verifyTokenStore({ + scopes, + }); + const telemetryCacheEntry = await BrowserCache.getTelemetryCacheEntry( + "b5c2e510-4a17-4feb-b219-e55aa5b74144" + ); + expect(telemetryCacheEntry).not.toBeNull(); + expect(telemetryCacheEntry["cacheHits"]).toBeGreaterThanOrEqual(1); +} + +describe("/profile", () => { + jest.retryTimes(RETRY_TIMES); + let browser: puppeteer.Browser; + let context: puppeteer.BrowserContext; + let page: puppeteer.Page; + let port: number; + let username: string; + let accountPwd: string; + let BrowserCache: BrowserCacheUtils; + + beforeAll(async () => { + // @ts-ignore + browser = await global.__BROWSER__; + // @ts-ignore + port = global.__PORT__; + + const labApiParams: LabApiQueryParams = { + azureEnvironment: AzureEnvironments.CLOUD, + appType: AppTypes.CLOUD, + }; + + const labClient = new LabClient(); + const envResponse = await labClient.getVarsByCloudEnvironment( + labApiParams + ); + + [username, accountPwd] = await setupCredentials( + envResponse[0], + labClient + ); + }); + + beforeEach(async () => { + context = await browser.createBrowserContext(); + page = await context.newPage(); + page.setDefaultTimeout(5000); + BrowserCache = new BrowserCacheUtils(page, "localStorage"); + await page.goto(`http://localhost:${port}`); + }); + + afterEach(async () => { + await page.close(); + await context.close(); + }); + + it("MsalAuthenticationTemplate - invokes loginPopup if user is not signed in", async () => { + const testName = "MsalAuthenticationTemplateBaseCase"; + const screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + await screenshot.takeScreenshot(page, "Home page loaded"); + + // Navigate to /profile and expect popup to be opened without interaction + const newPopupWindowPromise = new Promise((resolve) => + page.once("popup", resolve) + ); + await page.goto(`http://localhost:${port}/profile`); + await screenshot.takeScreenshot(page, "Profile page loaded"); + const popupPage = await newPopupWindowPromise; + if (!popupPage) { + throw new Error('Popup window was not opened'); + } + + await enterCredentials(popupPage, screenshot, username, accountPwd); + + // Wait for Graph data to display + await page.waitForSelector("xpath/.//div/ul/li[contains(., 'Name')]", { + timeout: 5000, + }); + await screenshot.takeScreenshot(page, "Graph data acquired"); + + // Verify UI now displays logged in content + await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]"); + const profileButton = await page.waitForSelector( + "xpath=//header//button" + ); + await profileButton.click(); + const logoutButtons = await page.$$( + "xpath/.//li[contains(., 'Logout using')]" + ); + expect(logoutButtons.length).toBe(2); + await screenshot.takeScreenshot(page, "App signed in"); + + // Verify tokens are in cache + await verifyTokenStore(BrowserCache, ["User.Read"]); + }); + + it("MsalAuthenticationTemplate - renders children without invoking login if user is already signed in", async () => { + const testName = "MsalAuthenticationTemplateSignedInCase"; + const screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + await screenshot.takeScreenshot(page, "Page loaded"); + + // Initiate Login + const signInButton = await page.waitForSelector( + "xpath=//button[contains(., 'Login')]" + ); + await signInButton.click(); + await screenshot.takeScreenshot(page, "Login button clicked"); + const loginPopupButton = await page.waitForSelector( + "xpath=//li[contains(., 'Sign in using Popup')]" + ); + const newPopupWindowPromise = new Promise((resolve) => + page.once("popup", resolve) + ); + await loginPopupButton.click(); + const popupPage = await newPopupWindowPromise; + if (!popupPage) { + throw new Error('Popup window was not opened'); + } + + await enterCredentials(popupPage, screenshot, username, accountPwd); + await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]", { + timeout: 3000, + }); + await screenshot.takeScreenshot(page, "Popup closed"); + + // Verify UI now displays logged in content + await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]"); + const profileButton = await page.waitForSelector( + "xpath=//header//button" + ); + await profileButton.click(); + const logoutButtons = await page.$$( + "xpath/.//li[contains(., 'Logout using')]" + ); + expect(logoutButtons.length).toBe(2); + await screenshot.takeScreenshot(page, "App signed in"); + + // Go to protected page + await page.goto(`http://localhost:${port}/profile`); + // Wait for Graph data to display + await page.waitForSelector("xpath/.//div/ul/li[contains(., 'Name')]", { + timeout: 5000, + }); + await screenshot.takeScreenshot(page, "Graph data acquired"); + // Verify tokens are in cache + await verifyTokenStore(BrowserCache, ["User.Read"]); + }); +}); diff --git a/samples/msal-react-samples/react17-sample/test/profileRawContext.spec.ts b/samples/msal-react-samples/react17-sample/test/profileRawContext.spec.ts new file mode 100644 index 0000000000..ff4df3c2a2 --- /dev/null +++ b/samples/msal-react-samples/react17-sample/test/profileRawContext.spec.ts @@ -0,0 +1,104 @@ +import * as puppeteer from "puppeteer"; +import { + Screenshot, + setupCredentials, + enterCredentials, + RETRY_TIMES, + LabClient, + LabApiQueryParams, + AzureEnvironments, + AppTypes, + BrowserCacheUtils, +} from "e2e-test-utils"; + +const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/profileRawContext-tests`; + +async function verifyTokenStore( + BrowserCache: BrowserCacheUtils, + scopes: string[] +): Promise { + await BrowserCache.verifyTokenStore({ + scopes, + }); + const telemetryCacheEntry = await BrowserCache.getTelemetryCacheEntry( + "b5c2e510-4a17-4feb-b219-e55aa5b74144" + ); + expect(telemetryCacheEntry).not.toBeNull(); + expect(telemetryCacheEntry["cacheHits"]).toBeGreaterThanOrEqual(1); +} + +describe("/profileRawContext", () => { + jest.retryTimes(RETRY_TIMES); + let browser: puppeteer.Browser; + let context: puppeteer.BrowserContext; + let page: puppeteer.Page; + let port: number; + let username: string; + let accountPwd: string; + let BrowserCache: BrowserCacheUtils; + + beforeAll(async () => { + // @ts-ignore + browser = await global.__BROWSER__; + // @ts-ignore + port = global.__PORT__; + + const labApiParams: LabApiQueryParams = { + azureEnvironment: AzureEnvironments.CLOUD, + appType: AppTypes.CLOUD, + }; + + const labClient = new LabClient(); + const envResponse = await labClient.getVarsByCloudEnvironment( + labApiParams + ); + + [username, accountPwd] = await setupCredentials( + envResponse[0], + labClient + ); + }); + + beforeEach(async () => { + context = await browser.createBrowserContext(); + page = await context.newPage(); + page.setDefaultTimeout(5000); + BrowserCache = new BrowserCacheUtils(page, "localStorage"); + await page.goto(`http://localhost:${port}`); + }); + + afterEach(async () => { + await page.close(); + await context.close(); + }); + + it("MsalAuthenticationTemplate - invokes loginPopup if user is not signed in (class component w/ raw context)", async () => { + const testName = "MsalAuthenticationTemplatePopupCase"; + const screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + await screenshot.takeScreenshot(page, "Home page loaded"); + + // Navigate to /profile and expect popup to be opened without interaction + const newPopupWindowPromise = new Promise((resolve) => + page.once("popup", resolve) + ); + await page.goto(`http://localhost:${port}/profileRawContext`); + await screenshot.takeScreenshot(page, "Profile page loaded"); + const popupPage = await newPopupWindowPromise; + if (!popupPage) { + throw new Error('Popup window was not opened'); + } + + await enterCredentials(popupPage, screenshot, username, accountPwd); + + // Wait for Graph data to display + await page.waitForSelector("xpath/.//div/ul/li[contains(., 'Name')]", { + timeout: 5000, + }); + await screenshot.takeScreenshot(page, "Graph data acquired"); + + // Verify tokens are in cache + await verifyTokenStore(BrowserCache, ["User.Read"]); + }); +}); diff --git a/samples/msal-react-samples/react17-sample/test/profileWithMsal.spec.ts b/samples/msal-react-samples/react17-sample/test/profileWithMsal.spec.ts new file mode 100644 index 0000000000..1106546587 --- /dev/null +++ b/samples/msal-react-samples/react17-sample/test/profileWithMsal.spec.ts @@ -0,0 +1,97 @@ +import * as puppeteer from "puppeteer"; +import { + Screenshot, + setupCredentials, + enterCredentials, + RETRY_TIMES, + LabClient, + LabApiQueryParams, + AzureEnvironments, + AppTypes, + BrowserCacheUtils, +} from "e2e-test-utils"; + +const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/profileWithMsal-tests`; + +async function verifyTokenStore( + BrowserCache: BrowserCacheUtils, + scopes: string[] +): Promise { + await BrowserCache.verifyTokenStore({ + scopes, + }); + const telemetryCacheEntry = await BrowserCache.getTelemetryCacheEntry( + "b5c2e510-4a17-4feb-b219-e55aa5b74144" + ); + expect(telemetryCacheEntry).not.toBeNull(); + expect(telemetryCacheEntry["cacheHits"]).toBeGreaterThanOrEqual(1); +} + +describe("/profileWithMsal", () => { + jest.retryTimes(RETRY_TIMES); + let browser: puppeteer.Browser; + let context: puppeteer.BrowserContext; + let page: puppeteer.Page; + let port: number; + let username: string; + let accountPwd: string; + let BrowserCache: BrowserCacheUtils; + + beforeAll(async () => { + // @ts-ignore + browser = await global.__BROWSER__; + // @ts-ignore + port = global.__PORT__; + + const labApiParams: LabApiQueryParams = { + azureEnvironment: AzureEnvironments.CLOUD, + appType: AppTypes.CLOUD, + }; + + const labClient = new LabClient(); + const envResponse = await labClient.getVarsByCloudEnvironment( + labApiParams + ); + + [username, accountPwd] = await setupCredentials( + envResponse[0], + labClient + ); + }); + + beforeEach(async () => { + context = await browser.createBrowserContext(); + page = await context.newPage(); + page.setDefaultTimeout(5000); + BrowserCache = new BrowserCacheUtils(page, "localStorage"); + await page.goto(`http://localhost:${port}`); + }); + + afterEach(async () => { + await page.close(); + await context.close(); + }); + + it("MsalAuthenticationTemplate - invokes loginRedirect if user is not signed in (class component w/ withMsal HOC)", async () => { + const testName = "MsalAuthenticationTemplateRedirectCase"; + const screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + await screenshot.takeScreenshot(page, "Home page loaded"); + + // Navigate to /profileWithMsal and expect redirect to be initiated without interaction + await page.goto(`http://localhost:${port}/profileWithMsal`); + await screenshot.takeScreenshot(page, "Profile page loaded"); + + await enterCredentials(page, screenshot, username, accountPwd); + + // Wait for Graph data to display + await page.waitForSelector("xpath/.//div/ul/li[contains(., 'Name')]", { + timeout: 5000, + }); + await screenshot.takeScreenshot(page, "Graph data acquired"); + + // Verify tokens are in cache + await verifyTokenStore(BrowserCache, ["User.Read"]); + }); +}); diff --git a/samples/msal-react-samples/react17-sample/tsconfig.json b/samples/msal-react-samples/react17-sample/tsconfig.json new file mode 100644 index 0000000000..a8df22f95c --- /dev/null +++ b/samples/msal-react-samples/react17-sample/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../tsconfig.json" +} \ No newline at end of file diff --git a/samples/msal-react-samples/react17-sample/vite.config.js b/samples/msal-react-samples/react17-sample/vite.config.js new file mode 100644 index 0000000000..9be5c7392c --- /dev/null +++ b/samples/msal-react-samples/react17-sample/vite.config.js @@ -0,0 +1,10 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + server: { + port: 3000, + strictPort: true, + }, +}); diff --git a/samples/msal-react-samples/react18-sample/.env.development b/samples/msal-react-samples/react18-sample/.env.development new file mode 100644 index 0000000000..b8e1347305 --- /dev/null +++ b/samples/msal-react-samples/react18-sample/.env.development @@ -0,0 +1,5 @@ +BROWSER=none + +VITE_CLIENT_ID=ENTER_CLIENT_ID_HERE +VITE_AUTHORITY=https://login.microsoftonline.com/ENTER_TENANT_ID_HERE +VITE_REDIRECT_URI=/redirect diff --git a/samples/msal-react-samples/react18-sample/.env.e2e b/samples/msal-react-samples/react18-sample/.env.e2e new file mode 100644 index 0000000000..7b1d8d02ec --- /dev/null +++ b/samples/msal-react-samples/react18-sample/.env.e2e @@ -0,0 +1,5 @@ +BROWSER=none + +VITE_CLIENT_ID=b5c2e510-4a17-4feb-b219-e55aa5b74144 +VITE_AUTHORITY=https://login.microsoftonline.com/common +VITE_REDIRECT_URI=/redirect diff --git a/samples/msal-react-samples/react18-sample/.gitignore b/samples/msal-react-samples/react18-sample/.gitignore new file mode 100644 index 0000000000..227a007b62 --- /dev/null +++ b/samples/msal-react-samples/react18-sample/.gitignore @@ -0,0 +1,20 @@ +# dependencies +/node_modules + +# testing +/coverage + +# production +/build +/dist + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/samples/msal-react-samples/react18-sample/.npmrc b/samples/msal-react-samples/react18-sample/.npmrc new file mode 100644 index 0000000000..43c97e719a --- /dev/null +++ b/samples/msal-react-samples/react18-sample/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/samples/msal-react-samples/react18-sample/README.md b/samples/msal-react-samples/react18-sample/README.md new file mode 100644 index 0000000000..22a2850b06 --- /dev/null +++ b/samples/msal-react-samples/react18-sample/README.md @@ -0,0 +1,87 @@ +# MSAL.js for React Sample - React 18 Compatibility + +## About this sample + +This sample is derived from the [react-router-sample](../react-router-sample) and demonstrates MSAL React running with **React 18**. It uses `ReactDOM.createRoot` (React 18+) for rendering and MUI v5 (`@mui/material`) for UI components. + +## Notable files and what they demonstrate + +1. `./src/App.jsx` - Shows implementation of `MsalProvider`, all children will have access to `@azure/msal-react` context, hooks and components. +1. `./src/index.jsx` - Shows initialization of the `PublicClientApplication` that is passed to `App.jsx` +1. `./src/pages/Home.jsx` - Homepage, shows how to conditionally render content using `AuthenticatedTemplate` and `UnauthenticatedTemplate` depending on whether or not a user is signed in. +1. `./src/pages/Profile.jsx` - Example of a protected route using `MsalAuthenticationTemplate`. If a user is not yet signed in, signin will be invoked automatically. If a user is signed in it will acquire an access token and make a call to MS Graph to fetch user profile data. +1. `./src/authConfig.js` - Configuration options for `PublicClientApplication` and token requests. +1. `./src/ui-components/SignInSignOutButton.jsx` - Example of how to conditionally render a Sign In or Sign Out button using the `useIsAuthenticated` hook. +1. `./src/ui-components/SignInButton.jsx` - Example of how to get the `PublicClientApplication` instance using the `useMsal` hook and invoking a login function. +1. `./src/ui-components/SignOutButton.jsx` - Example of how to get the `PublicClientApplication` instance using the `useMsal` hook and invoking a logout function. +1. `./src/utils/MsGraphApiCall.js` - Example of how to call the MS Graph API with an access token. +1. `./src/utils/NavigationClient.js` - Example implementation of `INavigationClient` which can be used to override the default navigation functions MSAL.js uses + +### (Optional) MSAL React and class components + +For a demonstration of how to use MSAL React with class components, see: `./src/pages/ProfileWithMsal.jsx` and `./src/pages/ProfileRawContext.jsx`. + +*After* you initialize `MsalProvider`, there are 3 approaches you can take to protect your class components with MSAL React: + +1. Wrap each component that you want to protect with `withMsal` higher-order component (HOC) (e.g. [Profile](./src/pages/ProfileWithMsal.jsx#Profile)). +1. Consume the raw context directly (e.g. [ProfileContent](./src/pages/ProfileRawContext.jsx#ProfileContent)). +1. Pass context down from a parent component that has access to the `msalContext` via one of the other means above (e.g. [ProfileContent](./src/pages/ProfileWithMsal.jsx#ProfileContent)). + +For more information, visit: + +- [Docs: Class Components](../../../lib/msal-react/docs/class-components.md) +- [MSAL React FAQ](../../../lib/msal-react/FAQ.md) + +## How to run the sample + +### Pre-requisites + +- Ensure [all pre-requisites](../../../lib/msal-react/README.md#prerequisites) have been completed to run `@azure/msal-react`. +- Install node.js if needed (). + +### Configure the application + +- Open `./.env.development` in an editor. +- Replace `ENTER_CLIENT_ID_HERE` with the Application (client) ID from the portal registration, or use the currently configured lab registration. +- Replace `ENTER_TENANT_ID_HERE` with the tenant ID from the portal registration, or use the currently configured lab registration. + - Optionally, you may replace any of the other parameters, or you can remove them and use the default values. + +These parameters are used in `./src/authConfig.js` to configure MSAL. + +#### Install npm dependencies for sample + +```bash +# Install dev dependencies for msal-react and msal-browser from root of repo +npm install + +# Change directory to sample directory +cd samples/msal-react-samples/react18-sample + +# Build packages locally +npm run build:package +``` + +#### Running the sample development server + +1. In a command prompt, run `npm start`. +1. Open [http://localhost:3000](http://localhost:3000) to view it in the browser. +1. Open [http://localhost:3000/profile](http://localhost:3000/profile) to see an example of a protected route. If you are not yet signed in, signin will be invoked automatically. + +The page will reload if you make edits. +You will also see any lint errors in the console. + +- In the web page, click on the "Login" button and select either `Sign in using Popup` or `Sign in using Redirect` to begin the auth flow. + +#### Running the sample production server + +1. In a command prompt, run `npm run build`. +1. Next run `npx vite preview --port 3000 --strictPort`. +1. Open [http://localhost:3000](http://localhost:3000) to view it in the browser. +1. Open [http://localhost:3000/profile](http://localhost:3000/profile) to see an example of a protected route. If you are not yet signed in, signin will be invoked automatically. + +#### Learn more about the 3rd-party libraries used to create this sample + +- [React documentation](https://reactjs.org/). +- [Vite documentation](https://vite.dev/guide/) +- [React Router documentation](https://reactrouter.com/web/guides/quick-start) +- [Material-UI documentation](https://material-ui.com/getting-started/installation/) diff --git a/samples/msal-react-samples/react18-sample/index.html b/samples/msal-react-samples/react18-sample/index.html new file mode 100644 index 0000000000..884d5dc1bd --- /dev/null +++ b/samples/msal-react-samples/react18-sample/index.html @@ -0,0 +1,19 @@ + + + + + + + + + MSAL-React Sample + + + +
+ + + diff --git a/samples/msal-react-samples/react18-sample/jest.config.cjs b/samples/msal-react-samples/react18-sample/jest.config.cjs new file mode 100644 index 0000000000..c6dedc82ed --- /dev/null +++ b/samples/msal-react-samples/react18-sample/jest.config.cjs @@ -0,0 +1,8 @@ +module.exports = { + displayName: "React 18 Compat", + globals: { + __PORT__: 3000, + __STARTCMD__: "env-cmd -f .env.e2e npm start", + }, + preset: "../../e2eTestUtils/jest-puppeteer-utils/jest-preset.js", +}; diff --git a/samples/msal-react-samples/react18-sample/package.json b/samples/msal-react-samples/react18-sample/package.json new file mode 100644 index 0000000000..fc8895633c --- /dev/null +++ b/samples/msal-react-samples/react18-sample/package.json @@ -0,0 +1,50 @@ +{ + "name": "react18-sample", + "version": "0.1.0", + "private": true, + "type": "module", + "dependencies": { + "@azure/msal-browser": "^5.0.0", + "@azure/msal-react": "^5.0.0", + "@emotion/react": "^11.10.5", + "@emotion/styled": "^11.10.5", + "@mui/icons-material": "^5.10.16", + "@mui/material": "^5.10.17", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-router-dom": "^6.7.0" + }, + "scripts": { + "start": "vite", + "test:e2e": "jest", + "build": "vite build", + "build:package": "cd ../../../ && npm run build:all --workspace=lib/msal-react" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@types/jest": "^29.5.0", + "@vitejs/plugin-react": "^4.3.3", + "e2e-test-utils": "file:../../e2eTestUtils", + "env-cmd": "^10.1.0", + "jest": "^29.5.0", + "jest-junit": "^16.0.0", + "ts-jest": "^29.1.0", + "vite": "^5.4.21" + }, + "jest-junit": { + "suiteNameTemplate": "React 18 Compat Tests", + "outputDirectory": ".", + "outputName": "test-results.xml" + } +} \ No newline at end of file diff --git a/samples/msal-react-samples/react18-sample/public/favicon.ico b/samples/msal-react-samples/react18-sample/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/samples/msal-react-samples/react18-sample/public/favicon.ico differ diff --git a/samples/msal-react-samples/react18-sample/src/App.jsx b/samples/msal-react-samples/react18-sample/src/App.jsx new file mode 100644 index 0000000000..6756321e28 --- /dev/null +++ b/samples/msal-react-samples/react18-sample/src/App.jsx @@ -0,0 +1,62 @@ +import { Routes, Route, useNavigate, useLocation } from "react-router-dom"; +// Material-UI imports +import Grid from "@mui/material/Grid"; + +// MSAL imports +import { MsalProvider } from "@azure/msal-react"; +import { CustomNavigationClient } from "./utils/NavigationClient"; + +// Sample app imports +import { PageLayout } from "./ui-components/PageLayout"; +import { Home } from "./pages/Home"; +import { Profile } from "./pages/Profile"; +import { Logout } from "./pages/Logout"; +import { Redirect } from "./pages/Redirect"; + +// Class-based equivalents of "Profile" component +import { ProfileWithMsal } from "./pages/ProfileWithMsal"; +import { ProfileRawContext } from "./pages/ProfileRawContext"; +import { ProfileUseMsalAuthenticationHook } from "./pages/ProfileUseMsalAuthenticationHook"; + +function App({ pca }) { + // The next 3 lines are optional. This is how you configure MSAL to take advantage of the router's navigate functions when MSAL redirects between pages in your app + const navigate = useNavigate(); + const location = useLocation(); + const navigationClient = new CustomNavigationClient(navigate); + pca.setNavigationClient(navigationClient); + + // Don't wrap redirect page in MsalProvider to prevent MSAL from consuming the auth response + const isRedirectPage = location.pathname === '/redirect'; + + if (isRedirectPage) { + return ; + } + + return ( + + + + + + + + ); +} + +function Pages() { + return ( + + } /> + } /> + } /> + } + /> + } /> + } /> + + ); +} + +export default App; diff --git a/samples/msal-react-samples/react18-sample/src/authConfig.js b/samples/msal-react-samples/react18-sample/src/authConfig.js new file mode 100644 index 0000000000..73ea34a7f7 --- /dev/null +++ b/samples/msal-react-samples/react18-sample/src/authConfig.js @@ -0,0 +1,51 @@ +import { LogLevel, BrowserUtils } from "@azure/msal-browser"; + +// Config object to be passed to Msal on creation +export const msalConfig = { + auth: { + clientId: import.meta.env.VITE_CLIENT_ID, + authority: import.meta.env.VITE_AUTHORITY, + redirectUri: import.meta.env.VITE_REDIRECT_URI, + postLogoutRedirectUri: "/", + onRedirectNavigate: () => !BrowserUtils.isInIframe() + }, + cache: { + cacheLocation: "localStorage", + }, + system: { + allowPlatformBroker: false, // Disables WAM Broker + loggerOptions: { + loggerCallback: (level, message, containsPii) => { + if (containsPii) { + return; + } + switch (level) { + case LogLevel.Error: + console.error(message); + return; + case LogLevel.Info: + console.info(message); + return; + case LogLevel.Verbose: + console.debug(message); + return; + case LogLevel.Warning: + console.warn(message); + return; + default: + return; + } + }, + }, + }, +}; + +// Add here scopes for id token to be used at MS Identity Platform endpoints. +export const loginRequest = { + scopes: ["User.Read"] +}; + +// Add here the endpoints for MS Graph API services you would like to use. +export const graphConfig = { + graphMeEndpoint: "https://graph.microsoft.com/v1.0/me" +}; diff --git a/samples/msal-react-samples/react18-sample/src/index.jsx b/samples/msal-react-samples/react18-sample/src/index.jsx new file mode 100644 index 0000000000..f75f9c6eac --- /dev/null +++ b/samples/msal-react-samples/react18-sample/src/index.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { BrowserRouter as Router } from "react-router-dom"; +import { ThemeProvider } from "@mui/material/styles"; +import { theme } from "./styles/theme"; +import App from './App'; + +// MSAL imports +import { PublicClientApplication, EventType } from "@azure/msal-browser"; +import { msalConfig } from "./authConfig"; + +export const msalInstance = new PublicClientApplication(msalConfig); + +msalInstance.initialize().then(() => { + // Default to using the first account if no account is active on page load + if (!msalInstance.getActiveAccount() && msalInstance.getAllAccounts().length > 0) { + // Account selection logic is app dependent. Adjust as needed for different use cases. + msalInstance.setActiveAccount(msalInstance.getAllAccounts()[0]); + } + + msalInstance.addEventCallback((event) => { + if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) { + const account = event.payload; + msalInstance.setActiveAccount(account); + } + }); + + const container = document.getElementById("root"); + const root = ReactDOM.createRoot(container); + + root.render( + + + + + + ); +}); diff --git a/samples/msal-react-samples/react18-sample/src/pages/Home.jsx b/samples/msal-react-samples/react18-sample/src/pages/Home.jsx new file mode 100644 index 0000000000..288c3eb8bb --- /dev/null +++ b/samples/msal-react-samples/react18-sample/src/pages/Home.jsx @@ -0,0 +1,26 @@ +import { AuthenticatedTemplate, UnauthenticatedTemplate } from "@azure/msal-react"; +import Button from "@mui/material/Button"; +import ButtonGroup from "@mui/material/ButtonGroup"; +import Typography from "@mui/material/Typography"; +import { Link as RouterLink } from "react-router-dom"; + +export function Home() { + return ( + <> + + + + + + + + + + + +
Please sign-in to see your profile information.
+
+
+ + ); +} \ No newline at end of file diff --git a/samples/msal-react-samples/react18-sample/src/pages/Logout.jsx b/samples/msal-react-samples/react18-sample/src/pages/Logout.jsx new file mode 100644 index 0000000000..21d765b948 --- /dev/null +++ b/samples/msal-react-samples/react18-sample/src/pages/Logout.jsx @@ -0,0 +1,16 @@ +import React, { useEffect } from "react"; +import { useMsal } from "@azure/msal-react"; + +export function Logout() { + const { instance } = useMsal(); + + useEffect(() => { + instance.logoutRedirect({ + account: instance.getActiveAccount(), + }) + }, [ instance ]); + + return ( +
Logout
+ ) +} diff --git a/samples/msal-react-samples/react18-sample/src/pages/Profile.jsx b/samples/msal-react-samples/react18-sample/src/pages/Profile.jsx new file mode 100644 index 0000000000..73965b9448 --- /dev/null +++ b/samples/msal-react-samples/react18-sample/src/pages/Profile.jsx @@ -0,0 +1,76 @@ +import { useEffect, useState, useCallback } from "react"; + +// Msal imports +import { MsalAuthenticationTemplate, useMsal } from "@azure/msal-react"; +import { EventType, InteractionType, InteractionRequiredAuthError } from "@azure/msal-browser"; +import { loginRequest } from "../authConfig"; + +// Sample app imports +import { ProfileData } from "../ui-components/ProfileData"; +import { Loading } from "../ui-components/Loading"; +import { ErrorComponent } from "../ui-components/ErrorComponent"; +import { callMsGraph } from "../utils/MsGraphApiCall"; + +// Material-ui imports +import Paper from "@mui/material/Paper"; + +const ProfileContent = () => { + const { instance } = useMsal(); + const [graphData, setGraphData] = useState(null); + + const fetchProfile = useCallback(() => { + if (!instance.getActiveAccount()) { + return; + } + callMsGraph().then(response => setGraphData(response)).catch((e) => { + if (e instanceof InteractionRequiredAuthError) { + instance.acquireTokenRedirect({ + ...loginRequest, + account: instance.getActiveAccount() + }); + } + }); + }, [instance]); + + useEffect(() => { + // Attempt to fetch profile data immediately + fetchProfile(); + + // Subscribe to active account changes so the Graph call is retried + // once setActiveAccount has been called. + const callbackId = instance.addEventCallback((event) => { + if (event.eventType === EventType.ACTIVE_ACCOUNT_CHANGED) { + fetchProfile(); + } + }); + + return () => { + if (callbackId) { + instance.removeEventCallback(callbackId); + } + }; + }, [instance, fetchProfile]); + + return ( + + { graphData ? : null } + + ); +}; + +export function Profile() { + const authRequest = { + ...loginRequest + }; + + return ( + + + + ) +}; \ No newline at end of file diff --git a/samples/msal-react-samples/react18-sample/src/pages/ProfileRawContext.jsx b/samples/msal-react-samples/react18-sample/src/pages/ProfileRawContext.jsx new file mode 100644 index 0000000000..a6db93a520 --- /dev/null +++ b/samples/msal-react-samples/react18-sample/src/pages/ProfileRawContext.jsx @@ -0,0 +1,109 @@ +import { Component } from "react"; + +// Msal imports +import { MsalAuthenticationTemplate, MsalContext } from "@azure/msal-react"; +import { InteractionType, EventType, InteractionRequiredAuthError } from "@azure/msal-browser"; +import { loginRequest } from "../authConfig"; + +// Sample app imports +import { ProfileData } from "../ui-components/ProfileData"; +import { Loading } from "../ui-components/Loading"; +import { ErrorComponent } from "../ui-components/ErrorComponent"; +import { callMsGraph } from "../utils/MsGraphApiCall"; + +// Material-ui imports +import Paper from "@mui/material/Paper"; + + +/** + * This class is using the raw context directly. The available + * objects and methods are the same as in "withMsal" HOC usage. + */ +class ProfileContent extends Component { + + static contextType = MsalContext; + + constructor(props) { + super(props) + + this.state = { + graphData: null, + } + + this.callbackId = null; + } + + fetchGraphData() { + if (this.state.graphData) { + return; + } + + const instance = this.context.instance; + if (!instance.getActiveAccount()) { + return; + } + + callMsGraph().then(response => this.setState({graphData: response})).catch((e) => { + if (e instanceof InteractionRequiredAuthError) { + instance.acquireTokenRedirect({ + ...loginRequest, + account: instance.getActiveAccount() + }); + } + }); + } + + componentDidMount() { + // Attempt to fetch profile data immediately + this.fetchGraphData(); + + // Subscribe to active account changes so the Graph call is retried + // once setActiveAccount has been called. + this.callbackId = this.context.instance.addEventCallback((event) => { + if (event.eventType === EventType.ACTIVE_ACCOUNT_CHANGED) { + this.fetchGraphData(); + } + }); + } + + componentWillUnmount() { + if (this.callbackId) { + this.context.instance.removeEventCallback(this.callbackId); + } + } + + render() { + return ( + + { this.state.graphData ? : null } + + ); + } +} + +/** + * This class is using "withMsal" HOC. It passes down the msalContext + * as a prop to its children. + */ +class Profile extends Component { + + render() { + + const authRequest = { + ...loginRequest + }; + + return ( + + + + ); + } +} + +export const ProfileRawContext = Profile diff --git a/samples/msal-react-samples/react18-sample/src/pages/ProfileUseMsalAuthenticationHook.jsx b/samples/msal-react-samples/react18-sample/src/pages/ProfileUseMsalAuthenticationHook.jsx new file mode 100644 index 0000000000..43bdf3fe11 --- /dev/null +++ b/samples/msal-react-samples/react18-sample/src/pages/ProfileUseMsalAuthenticationHook.jsx @@ -0,0 +1,51 @@ +import { useEffect, useState } from "react"; + +// Msal imports +import { useMsalAuthentication } from "@azure/msal-react"; +import { InteractionType } from "@azure/msal-browser"; +import { loginRequest } from "../authConfig"; + +// Sample app imports +import { ProfileData } from "../ui-components/ProfileData"; +import { ErrorComponent } from "../ui-components/ErrorComponent"; +import { callMsGraph } from "../utils/MsGraphApiCall"; + +// Material-ui imports +import Paper from "@mui/material/Paper"; + +const ProfileContent = () => { + const [graphData, setGraphData] = useState(null); + const { result, error } = useMsalAuthentication(InteractionType.Popup, { + ...loginRequest, + }); + + useEffect(() => { + if (!!graphData) { + // We already have the data, no need to call the API + return; + } + + if (!!error) { + // Error occurred attempting to acquire a token, either handle the error or do nothing + return; + } + + if (result) { + callMsGraph().then(response => setGraphData(response)); + } + }, [error, result, graphData]); + + if (error) { + return ; + } + + return ( + + { graphData ? : null } + + ); +}; + +export function ProfileUseMsalAuthenticationHook() { + return +}; diff --git a/samples/msal-react-samples/react18-sample/src/pages/ProfileWithMsal.jsx b/samples/msal-react-samples/react18-sample/src/pages/ProfileWithMsal.jsx new file mode 100644 index 0000000000..1a2b0b0335 --- /dev/null +++ b/samples/msal-react-samples/react18-sample/src/pages/ProfileWithMsal.jsx @@ -0,0 +1,87 @@ +import { Component } from "react"; + +// Msal imports +import { MsalAuthenticationTemplate, withMsal } from "@azure/msal-react"; +import { InteractionType, InteractionStatus, InteractionRequiredAuthError } from "@azure/msal-browser"; +import { loginRequest } from "../authConfig"; + +// Sample app imports +import { ProfileData } from "../ui-components/ProfileData"; +import { Loading } from "../ui-components/Loading"; +import { ErrorComponent } from "../ui-components/ErrorComponent"; +import { callMsGraph } from "../utils/MsGraphApiCall"; + +// Material-ui imports +import Paper from "@mui/material/Paper"; + +/** + * This class is a child component of "Profile". MsalContext is passed + * down from the parent and available as a prop here. + */ +class ProfileContent extends Component { + + constructor(props) { + super(props) + + this.state = { + graphData: null, + } + } + + setGraphData() { + if (!this.state.graphData && this.props.msalContext.inProgress === InteractionStatus.None) { + callMsGraph().then(response => this.setState({graphData: response})).catch((e) => { + if (e instanceof InteractionRequiredAuthError) { + this.props.msalContext.instance.acquireTokenRedirect({ + ...loginRequest, + account: this.props.msalContext.instance.getActiveAccount() + }); + } + }); + } + } + + componentDidMount() { + this.setGraphData(); + } + + componentDidUpdate() { + this.setGraphData(); + } + + render() { + return ( + + { this.state.graphData ? : null } + + ); + } +} + +/** + * This class is using "withMsal" HOC and has access to authentication + * state. It passes down the msalContext as a prop to its children. + */ +class Profile extends Component { + + render() { + + const authRequest = { + ...loginRequest + }; + + return ( + + + + ); + } +} + +// Wrap your class component to access authentication state as props +export const ProfileWithMsal = withMsal(Profile); \ No newline at end of file diff --git a/samples/msal-react-samples/react18-sample/src/pages/Redirect.jsx b/samples/msal-react-samples/react18-sample/src/pages/Redirect.jsx new file mode 100644 index 0000000000..dd4529aaf3 --- /dev/null +++ b/samples/msal-react-samples/react18-sample/src/pages/Redirect.jsx @@ -0,0 +1,20 @@ +// This page serves the redirect bridge for MSAL authentication +// It's excluded from MsalProvider wrapper in App.js to prevent MSAL from processing the hash +import { useEffect } from "react"; +import { broadcastResponseToMainFrame } from "@azure/msal-browser/redirect-bridge"; + +export function Redirect() { + useEffect(() => { + // Call broadcastResponseToMainFrame when component mounts + broadcastResponseToMainFrame().catch((error) => { + console.error("Error broadcasting response to main frame:", error); + }); + }, []); + + return ( +
+

Processing authentication...

+
+ ); +} + diff --git a/samples/msal-react-samples/react18-sample/src/styles/theme.js b/samples/msal-react-samples/react18-sample/src/styles/theme.js new file mode 100644 index 0000000000..442acc8121 --- /dev/null +++ b/samples/msal-react-samples/react18-sample/src/styles/theme.js @@ -0,0 +1,20 @@ +import { unstable_createMuiStrictModeTheme as createMuiTheme } from '@mui/material/styles'; +import { red } from '@mui/material/colors'; + +// Create a theme instance. +export const theme = createMuiTheme({ + palette: { + primary: { + main: '#556cd6', + }, + secondary: { + main: '#19857b', + }, + error: { + main: red.A400, + }, + background: { + default: '#fff', + }, + }, +}); diff --git a/samples/msal-react-samples/react18-sample/src/ui-components/AccountPicker.jsx b/samples/msal-react-samples/react18-sample/src/ui-components/AccountPicker.jsx new file mode 100644 index 0000000000..6b1f855fe3 --- /dev/null +++ b/samples/msal-react-samples/react18-sample/src/ui-components/AccountPicker.jsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { useMsal } from "@azure/msal-react"; +import Avatar from '@mui/material/Avatar'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemAvatar from '@mui/material/ListItemAvatar'; +import ListItemText from '@mui/material/ListItemText'; +import DialogTitle from '@mui/material/DialogTitle'; +import Dialog from '@mui/material/Dialog'; +import PersonIcon from '@mui/icons-material/Person'; +import AddIcon from '@mui/icons-material/Add'; +import { loginRequest } from "../authConfig"; + +export const AccountPicker = (props) => { + const { instance, accounts } = useMsal(); + const { onClose, open } = props; + + const handleListItemClick = (account) => { + instance.setActiveAccount(account); + if (!account) { + instance.loginRedirect({ + ...loginRequest, + prompt: "login" + }) + } else { + // To ensure account related page attributes update after the account is changed + window.location.reload(); + } + + onClose(account); + }; + + return ( + + Set active account + + {accounts.map((account) => ( + handleListItemClick(account)} key={account.homeAccountId}> + + + + + + + + ))} + + handleListItemClick(null)}> + + + + + + + + + + ); +}; \ No newline at end of file diff --git a/samples/msal-react-samples/react18-sample/src/ui-components/ErrorComponent.jsx b/samples/msal-react-samples/react18-sample/src/ui-components/ErrorComponent.jsx new file mode 100644 index 0000000000..de8ddd2607 --- /dev/null +++ b/samples/msal-react-samples/react18-sample/src/ui-components/ErrorComponent.jsx @@ -0,0 +1,5 @@ +import { Typography } from "@mui/material"; + +export const ErrorComponent = ({error}) => { + return An Error Occurred: {error.errorCode}; +} \ No newline at end of file diff --git a/samples/msal-react-samples/react18-sample/src/ui-components/Loading.jsx b/samples/msal-react-samples/react18-sample/src/ui-components/Loading.jsx new file mode 100644 index 0000000000..c2ae494c80 --- /dev/null +++ b/samples/msal-react-samples/react18-sample/src/ui-components/Loading.jsx @@ -0,0 +1,5 @@ +import { Typography } from "@mui/material"; + +export const Loading = () => { + return Authentication in progress... +} \ No newline at end of file diff --git a/samples/msal-react-samples/react18-sample/src/ui-components/NavBar.jsx b/samples/msal-react-samples/react18-sample/src/ui-components/NavBar.jsx new file mode 100644 index 0000000000..bea9940fbc --- /dev/null +++ b/samples/msal-react-samples/react18-sample/src/ui-components/NavBar.jsx @@ -0,0 +1,25 @@ +import AppBar from "@mui/material/AppBar"; +import Toolbar from "@mui/material/Toolbar"; +import Link from "@mui/material/Link"; +import Typography from "@mui/material/Typography"; +import WelcomeName from "./WelcomeName"; +import SignInSignOutButton from "./SignInSignOutButton"; +import { Link as RouterLink } from "react-router-dom"; + +const NavBar = () => { + return ( +
+ + + + MS Identity Platform + + + + + +
+ ); +}; + +export default NavBar; \ No newline at end of file diff --git a/samples/msal-react-samples/react18-sample/src/ui-components/PageLayout.jsx b/samples/msal-react-samples/react18-sample/src/ui-components/PageLayout.jsx new file mode 100644 index 0000000000..f4e5672879 --- /dev/null +++ b/samples/msal-react-samples/react18-sample/src/ui-components/PageLayout.jsx @@ -0,0 +1,16 @@ +import Typography from "@mui/material/Typography"; +import NavBar from "./NavBar"; + +export const PageLayout = (props) => { + return ( + <> + + +
Welcome to the Microsoft Authentication Library For React Quickstart
+
+
+
+ {props.children} + + ); +}; \ No newline at end of file diff --git a/samples/msal-react-samples/react18-sample/src/ui-components/ProfileData.jsx b/samples/msal-react-samples/react18-sample/src/ui-components/ProfileData.jsx new file mode 100644 index 0000000000..f7aa6223c7 --- /dev/null +++ b/samples/msal-react-samples/react18-sample/src/ui-components/ProfileData.jsx @@ -0,0 +1,78 @@ +import React from "react"; +import List from "@mui/material/List"; +import ListItem from "@mui/material/ListItem"; +import ListItemText from "@mui/material/ListItemText"; +import ListItemAvatar from "@mui/material/ListItemAvatar"; +import Avatar from "@mui/material/Avatar"; +import PersonIcon from '@mui/icons-material/Person'; +import WorkIcon from "@mui/icons-material/Work"; +import MailIcon from '@mui/icons-material/Mail'; +import PhoneIcon from '@mui/icons-material/Phone'; +import LocationOnIcon from '@mui/icons-material/LocationOn'; + +export const ProfileData = ({graphData}) => { + return ( + + + + + + + + ); +}; + +const NameListItem = ({name}) => ( + + + + + + + + +); + +const JobTitleListItem = ({jobTitle}) => ( + + + + + + + + +); + +const MailListItem = ({mail}) => ( + + + + + + + + +); + +const PhoneListItem = ({phone}) => ( + + + + + + + + +); + +const LocationListItem = ({location}) => ( + + + + + + + + +); diff --git a/samples/msal-react-samples/react18-sample/src/ui-components/SignInButton.jsx b/samples/msal-react-samples/react18-sample/src/ui-components/SignInButton.jsx new file mode 100644 index 0000000000..521b50ca7d --- /dev/null +++ b/samples/msal-react-samples/react18-sample/src/ui-components/SignInButton.jsx @@ -0,0 +1,149 @@ +import { useState } from "react"; +import { useMsal } from "@azure/msal-react"; +import Button from "@mui/material/Button"; +import MenuItem from '@mui/material/MenuItem'; +import Menu from '@mui/material/Menu'; +import Alert from '@mui/material/Alert'; +import AlertTitle from '@mui/material/AlertTitle'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; +import { loginRequest } from "../authConfig"; + +export const SignInButton = () => { + const { instance } = useMsal(); + + const [anchorEl, setAnchorEl] = useState(null); + const [showRetryDialog, setShowRetryDialog] = useState(false); + const [retryRequested, setRetryRequested] = useState(false); + const [showPopupWarning, setShowPopupWarning] = useState(false); + const open = Boolean(anchorEl); + + const handleLogin = async (loginType) => { + setAnchorEl(null); + + if (loginType === "popup") { + // Show warning when popup is about to open + setShowPopupWarning(true); + + try { + await instance.loginPopup({ + ...loginRequest, + // Only override if user explicitly clicked retry + overrideInteractionInProgress: retryRequested + }); + + // Hide warning on success + setShowPopupWarning(false); + setRetryRequested(false); + } catch (error) { + // Hide warning on error + setShowPopupWarning(false); + + if (error.errorCode === 'interaction_in_progress') { + // Show retry dialog - let user decide whether to retry + setShowRetryDialog(true); + } else { + // Reset retry flag for other errors + setRetryRequested(false); + console.error(error); + } + } + } else if (loginType === "redirect") { + instance.loginRedirect(loginRequest); + } + } + + const handleRetry = () => { + setShowRetryDialog(false); + setRetryRequested(true); // User explicitly requested retry + handleLogin("popup"); + } + + const handleCancelRetry = () => { + setShowRetryDialog(false); + setRetryRequested(false); + } + + return ( +
+ + setAnchorEl(null)} + > + handleLogin("popup")} key="loginPopup">Sign in using Popup + handleLogin("redirect")} key="loginRedirect">Sign in using Redirect + + + {/* Warning message during popup authentication */} + {showPopupWarning && ( + + Authentication in Progress + Please complete authentication in the popup window. Do not close the popup until authentication is complete. + + )} + + {/* Retry dialog for interaction_in_progress errors */} + + Authentication Already in Progress + + + An authentication request is already in progress. This may happen if: + + +
    +
  • You closed the popup window before completing authentication
  • +
  • The previous authentication attempt is still pending
  • +
+
+ + Would you like to cancel the pending authentication and try again? + + + Warning + Retrying will cancel the pending authentication request. + +
+ + + + +
+
+ ) +}; diff --git a/samples/msal-react-samples/react18-sample/src/ui-components/SignInSignOutButton.jsx b/samples/msal-react-samples/react18-sample/src/ui-components/SignInSignOutButton.jsx new file mode 100644 index 0000000000..61633e1459 --- /dev/null +++ b/samples/msal-react-samples/react18-sample/src/ui-components/SignInSignOutButton.jsx @@ -0,0 +1,20 @@ +import { useIsAuthenticated, useMsal } from "@azure/msal-react"; +import { SignInButton } from "./SignInButton"; +import { SignOutButton } from "./SignOutButton"; +import { InteractionStatus } from "@azure/msal-browser"; + +const SignInSignOutButton = () => { + const { inProgress } = useMsal(); + const isAuthenticated = useIsAuthenticated(); + + if (isAuthenticated) { + return ; + } else if (inProgress !== InteractionStatus.Startup && inProgress !== InteractionStatus.HandleRedirect) { + // inProgress check prevents sign-in button from being displayed briefly after returning from a redirect sign-in. Processing the server response takes a render cycle or two + return ; + } else { + return null; + } +} + +export default SignInSignOutButton; \ No newline at end of file diff --git a/samples/msal-react-samples/react18-sample/src/ui-components/SignOutButton.jsx b/samples/msal-react-samples/react18-sample/src/ui-components/SignOutButton.jsx new file mode 100644 index 0000000000..2a9fbd451e --- /dev/null +++ b/samples/msal-react-samples/react18-sample/src/ui-components/SignOutButton.jsx @@ -0,0 +1,65 @@ +import { useState } from "react"; +import { useMsal } from "@azure/msal-react"; +import IconButton from '@mui/material/IconButton'; +import AccountCircle from "@mui/icons-material/AccountCircle"; +import MenuItem from '@mui/material/MenuItem'; +import Menu from '@mui/material/Menu'; +import { AccountPicker } from "./AccountPicker"; + +export const SignOutButton = () => { + const { instance } = useMsal(); + const [accountSelectorOpen, setOpen] = useState(false); + + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + const handleLogout = (logoutType) => { + setAnchorEl(null); + + if (logoutType === "popup") { + instance.logoutPopup(); + } else if (logoutType === "redirect") { + instance.logoutRedirect(); + } + } + + const handleAccountSelection = () => { + setAnchorEl(null); + setOpen(true); + } + + const handleClose = () => { + setOpen(false); + }; + + return ( +
+ setAnchorEl(event.currentTarget)} + color="inherit" + > + + + setAnchorEl(null)} + > + handleAccountSelection()} key="switchAccount">Switch Account + handleLogout("popup")} key="logoutPopup">Logout using Popup + handleLogout("redirect")} key="logoutRedirect">Logout using Redirect + + +
+ ) +}; \ No newline at end of file diff --git a/samples/msal-react-samples/react18-sample/src/ui-components/WelcomeName.jsx b/samples/msal-react-samples/react18-sample/src/ui-components/WelcomeName.jsx new file mode 100644 index 0000000000..18b60ee4a9 --- /dev/null +++ b/samples/msal-react-samples/react18-sample/src/ui-components/WelcomeName.jsx @@ -0,0 +1,45 @@ +import { useEffect, useState, useCallback } from "react"; +import { useMsal } from "@azure/msal-react"; +import { EventType } from "@azure/msal-browser"; +import Typography from "@mui/material/Typography"; + +const WelcomeName = () => { + const { instance } = useMsal(); + const [name, setName] = useState(null); + + const updateName = useCallback(() => { + const activeAccount = instance.getActiveAccount(); + if (activeAccount) { + setName(activeAccount.name.split(' ')[0]); + } else { + setName(null); + } + }, [instance]); + + useEffect(() => { + // Set the name from the current active account on mount + updateName(); + + // Subscribe to active account changes so the component updates when + // setActiveAccount is called + const callbackId = instance.addEventCallback((event) => { + if (event.eventType === EventType.ACTIVE_ACCOUNT_CHANGED) { + updateName(); + } + }); + + return () => { + if (callbackId) { + instance.removeEventCallback(callbackId); + } + }; + }, [instance, updateName]); + + if (name) { + return Welcome, {name}; + } else { + return null; + } +}; + +export default WelcomeName; \ No newline at end of file diff --git a/samples/msal-react-samples/react18-sample/src/utils/MsGraphApiCall.js b/samples/msal-react-samples/react18-sample/src/utils/MsGraphApiCall.js new file mode 100644 index 0000000000..360493a86b --- /dev/null +++ b/samples/msal-react-samples/react18-sample/src/utils/MsGraphApiCall.js @@ -0,0 +1,33 @@ +import { loginRequest, graphConfig } from "../authConfig"; +import { msalInstance } from "../index"; + +export async function callMsGraph(accessToken) { + if (!accessToken) { + const account = msalInstance.getActiveAccount(); + if (!account) { + throw Error("No active account! Verify a user has been signed in and setActiveAccount has been called."); + } + + const response = await msalInstance.acquireTokenSilent({ + ...loginRequest, + account: account + }); + accessToken = response.accessToken; + } + + const headers = new Headers(); + const bearer = `Bearer ${accessToken}`; + + headers.append("Authorization", bearer); + + const options = { + method: "GET", + headers: headers + }; + + const response = await fetch(graphConfig.graphMeEndpoint, options); + if (!response.ok) { + throw new Error(`MS Graph request failed: ${response.status} ${response.statusText}`); + } + return response.json(); +} diff --git a/samples/msal-react-samples/react18-sample/src/utils/NavigationClient.js b/samples/msal-react-samples/react18-sample/src/utils/NavigationClient.js new file mode 100644 index 0000000000..70eab8c6d7 --- /dev/null +++ b/samples/msal-react-samples/react18-sample/src/utils/NavigationClient.js @@ -0,0 +1,28 @@ +import { NavigationClient } from "@azure/msal-browser"; + +/** + * This is an example for overriding the default function MSAL uses to navigate to other urls in your webpage + */ +export class CustomNavigationClient extends NavigationClient { + constructor(navigate) { + super(); + this.navigate = navigate; + } + + /** + * Navigates to other pages within the same web application + * You can use the useNavigate hook provided by react-router-dom to take advantage of client-side routing + * @param url + * @param options + */ + async navigateInternal(url, options) { + const relativePath = url.replace(window.location.origin, ""); + if (options.noHistory) { + this.navigate(relativePath, { replace: true }); + } else { + this.navigate(relativePath); + } + + return false; + } +} diff --git a/samples/msal-react-samples/react18-sample/test/home.spec.ts b/samples/msal-react-samples/react18-sample/test/home.spec.ts new file mode 100644 index 0000000000..eb2c005709 --- /dev/null +++ b/samples/msal-react-samples/react18-sample/test/home.spec.ts @@ -0,0 +1,148 @@ +import * as puppeteer from "puppeteer"; +import { + Screenshot, + setupCredentials, + enterCredentials, + RETRY_TIMES, + LabClient, + LabApiQueryParams, + AzureEnvironments, + AppTypes, + BrowserCacheUtils, +} from "e2e-test-utils"; + +const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/home-tests`; + +describe("/ (Home Page)", () => { + jest.retryTimes(RETRY_TIMES); + let browser: puppeteer.Browser; + let context: puppeteer.BrowserContext; + let page: puppeteer.Page; + let port: number; + let username: string; + let accountPwd: string; + let BrowserCache: BrowserCacheUtils; + + beforeAll(async () => { + // @ts-ignore + browser = await global.__BROWSER__; + // @ts-ignore + port = global.__PORT__; + + const labApiParams: LabApiQueryParams = { + azureEnvironment: AzureEnvironments.CLOUD, + appType: AppTypes.CLOUD, + }; + + const labClient = new LabClient(); + const envResponse = await labClient.getVarsByCloudEnvironment( + labApiParams + ); + + [username, accountPwd] = await setupCredentials( + envResponse[0], + labClient + ); + }); + + beforeEach(async () => { + context = await browser.createBrowserContext(); + page = await context.newPage(); + page.setDefaultTimeout(5000); + BrowserCache = new BrowserCacheUtils(page, "localStorage"); + await page.goto(`http://localhost:${port}`); + }); + + afterEach(async () => { + await page.close(); + await context.close(); + }); + + it("AuthenticatedTemplate - children are rendered after logging in with loginRedirect", async () => { + const testName = "redirectBaseCase"; + const screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + await screenshot.takeScreenshot(page, "Page loaded"); + + // Initiate Login + const signInButton = await page.waitForSelector( + "xpath=//button[contains(., 'Login')]" + ); + await signInButton.click(); + await screenshot.takeScreenshot(page, "Login button clicked"); + const loginRedirectButton = await page.waitForSelector( + "xpath=//li[contains(., 'Sign in using Redirect')]" + ); + await loginRedirectButton.click(); + + await enterCredentials(page, screenshot, username, accountPwd); + await screenshot.takeScreenshot(page, "Returned to app"); + + // Verify UI now displays logged in content + await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]"); + const profileButton = await page.waitForSelector( + "xpath=//header//button" + ); + await profileButton.click(); + const logoutButtons = await page.$$( + "xpath/.//li[contains(., 'Logout using')]" + ); + expect(logoutButtons.length).toBe(2); + await screenshot.takeScreenshot(page, "App signed in"); + + // Verify tokens are in cache + await BrowserCache.verifyTokenStore({ + scopes: ['User.Read'], + }); + }); + + it("AuthenticatedTemplate - children are rendered after logging in with loginPopup", async () => { + const testName = "popupBaseCase"; + const screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + await screenshot.takeScreenshot(page, "Page loaded"); + + // Initiate Login + const signInButton = await page.waitForSelector( + "xpath=//button[contains(., 'Login')]" + ); + await signInButton.click(); + await screenshot.takeScreenshot(page, "Login button clicked"); + const loginPopupButton = await page.waitForSelector( + "xpath=//li[contains(., 'Sign in using Popup')]" + ); + const newPopupWindowPromise = new Promise((resolve) => + page.once("popup", resolve) + ); + await loginPopupButton.click(); + const popupPage = await newPopupWindowPromise; + if (!popupPage) { + throw new Error('Popup window was not opened'); + } + + await enterCredentials(popupPage, screenshot, username, accountPwd); + await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]", { + timeout: 3000, + }); + await screenshot.takeScreenshot(page, "Popup closed"); + + // Verify UI now displays logged in content + await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]"); + const profileButton = await page.waitForSelector( + "xpath=//header//button" + ); + await profileButton.click(); + const logoutButtons = await page.$$( + "xpath/.//li[contains(., 'Logout using')]" + ); + expect(logoutButtons.length).toBe(2); + await screenshot.takeScreenshot(page, "App signed in"); + + // Verify tokens are in cache + await BrowserCache.verifyTokenStore({ + scopes: ['User.Read'], + }); + }); +}); diff --git a/samples/msal-react-samples/react18-sample/test/profile.spec.ts b/samples/msal-react-samples/react18-sample/test/profile.spec.ts new file mode 100644 index 0000000000..e3b383cf66 --- /dev/null +++ b/samples/msal-react-samples/react18-sample/test/profile.spec.ts @@ -0,0 +1,170 @@ +import * as puppeteer from "puppeteer"; +import { + Screenshot, + setupCredentials, + enterCredentials, + RETRY_TIMES, + LabClient, + LabApiQueryParams, + AzureEnvironments, + AppTypes, + BrowserCacheUtils, +} from "e2e-test-utils"; + +const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/profile-tests`; + +async function verifyTokenStore( + BrowserCache: BrowserCacheUtils, + scopes: string[] +): Promise { + await BrowserCache.verifyTokenStore({ + scopes, + }); + const telemetryCacheEntry = await BrowserCache.getTelemetryCacheEntry( + "b5c2e510-4a17-4feb-b219-e55aa5b74144" + ); + expect(telemetryCacheEntry).not.toBeNull(); + expect(telemetryCacheEntry["cacheHits"]).toBeGreaterThanOrEqual(1); +} + +describe("/profile", () => { + jest.retryTimes(RETRY_TIMES); + let browser: puppeteer.Browser; + let context: puppeteer.BrowserContext; + let page: puppeteer.Page; + let port: number; + let username: string; + let accountPwd: string; + let BrowserCache: BrowserCacheUtils; + + beforeAll(async () => { + // @ts-ignore + browser = await global.__BROWSER__; + // @ts-ignore + port = global.__PORT__; + + const labApiParams: LabApiQueryParams = { + azureEnvironment: AzureEnvironments.CLOUD, + appType: AppTypes.CLOUD, + }; + + const labClient = new LabClient(); + const envResponse = await labClient.getVarsByCloudEnvironment( + labApiParams + ); + + [username, accountPwd] = await setupCredentials( + envResponse[0], + labClient + ); + }); + + beforeEach(async () => { + context = await browser.createBrowserContext(); + page = await context.newPage(); + page.setDefaultTimeout(5000); + BrowserCache = new BrowserCacheUtils(page, "localStorage"); + await page.goto(`http://localhost:${port}`); + }); + + afterEach(async () => { + await page.close(); + await context.close(); + }); + + it("MsalAuthenticationTemplate - invokes loginPopup if user is not signed in", async () => { + const testName = "MsalAuthenticationTemplateBaseCase"; + const screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + await screenshot.takeScreenshot(page, "Home page loaded"); + + // Navigate to /profile and expect popup to be opened without interaction + const newPopupWindowPromise = new Promise((resolve) => + page.once("popup", resolve) + ); + await page.goto(`http://localhost:${port}/profile`); + await screenshot.takeScreenshot(page, "Profile page loaded"); + const popupPage = await newPopupWindowPromise; + if (!popupPage) { + throw new Error('Popup window was not opened'); + } + + await enterCredentials(popupPage, screenshot, username, accountPwd); + + // Wait for Graph data to display + await page.waitForSelector("xpath/.//div/ul/li[contains(., 'Name')]", { + timeout: 5000, + }); + await screenshot.takeScreenshot(page, "Graph data acquired"); + + // Verify UI now displays logged in content + await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]"); + const profileButton = await page.waitForSelector( + "xpath=//header//button" + ); + await profileButton.click(); + const logoutButtons = await page.$$( + "xpath/.//li[contains(., 'Logout using')]" + ); + expect(logoutButtons.length).toBe(2); + await screenshot.takeScreenshot(page, "App signed in"); + + // Verify tokens are in cache + await verifyTokenStore(BrowserCache, ["User.Read"]); + }); + + it("MsalAuthenticationTemplate - renders children without invoking login if user is already signed in", async () => { + const testName = "MsalAuthenticationTemplateSignedInCase"; + const screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + await screenshot.takeScreenshot(page, "Page loaded"); + + // Initiate Login + const signInButton = await page.waitForSelector( + "xpath=//button[contains(., 'Login')]" + ); + await signInButton.click(); + await screenshot.takeScreenshot(page, "Login button clicked"); + const loginPopupButton = await page.waitForSelector( + "xpath=//li[contains(., 'Sign in using Popup')]" + ); + const newPopupWindowPromise = new Promise((resolve) => + page.once("popup", resolve) + ); + await loginPopupButton.click(); + const popupPage = await newPopupWindowPromise; + if (!popupPage) { + throw new Error('Popup window was not opened'); + } + + await enterCredentials(popupPage, screenshot, username, accountPwd); + await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]", { + timeout: 3000, + }); + await screenshot.takeScreenshot(page, "Popup closed"); + + // Verify UI now displays logged in content + await page.waitForSelector("xpath/.//header[contains(., 'Welcome,')]"); + const profileButton = await page.waitForSelector( + "xpath=//header//button" + ); + await profileButton.click(); + const logoutButtons = await page.$$( + "xpath/.//li[contains(., 'Logout using')]" + ); + expect(logoutButtons.length).toBe(2); + await screenshot.takeScreenshot(page, "App signed in"); + + // Go to protected page + await page.goto(`http://localhost:${port}/profile`); + // Wait for Graph data to display + await page.waitForSelector("xpath/.//div/ul/li[contains(., 'Name')]", { + timeout: 5000, + }); + await screenshot.takeScreenshot(page, "Graph data acquired"); + // Verify tokens are in cache + await verifyTokenStore(BrowserCache, ["User.Read"]); + }); +}); diff --git a/samples/msal-react-samples/react18-sample/test/profileRawContext.spec.ts b/samples/msal-react-samples/react18-sample/test/profileRawContext.spec.ts new file mode 100644 index 0000000000..ff4df3c2a2 --- /dev/null +++ b/samples/msal-react-samples/react18-sample/test/profileRawContext.spec.ts @@ -0,0 +1,104 @@ +import * as puppeteer from "puppeteer"; +import { + Screenshot, + setupCredentials, + enterCredentials, + RETRY_TIMES, + LabClient, + LabApiQueryParams, + AzureEnvironments, + AppTypes, + BrowserCacheUtils, +} from "e2e-test-utils"; + +const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/profileRawContext-tests`; + +async function verifyTokenStore( + BrowserCache: BrowserCacheUtils, + scopes: string[] +): Promise { + await BrowserCache.verifyTokenStore({ + scopes, + }); + const telemetryCacheEntry = await BrowserCache.getTelemetryCacheEntry( + "b5c2e510-4a17-4feb-b219-e55aa5b74144" + ); + expect(telemetryCacheEntry).not.toBeNull(); + expect(telemetryCacheEntry["cacheHits"]).toBeGreaterThanOrEqual(1); +} + +describe("/profileRawContext", () => { + jest.retryTimes(RETRY_TIMES); + let browser: puppeteer.Browser; + let context: puppeteer.BrowserContext; + let page: puppeteer.Page; + let port: number; + let username: string; + let accountPwd: string; + let BrowserCache: BrowserCacheUtils; + + beforeAll(async () => { + // @ts-ignore + browser = await global.__BROWSER__; + // @ts-ignore + port = global.__PORT__; + + const labApiParams: LabApiQueryParams = { + azureEnvironment: AzureEnvironments.CLOUD, + appType: AppTypes.CLOUD, + }; + + const labClient = new LabClient(); + const envResponse = await labClient.getVarsByCloudEnvironment( + labApiParams + ); + + [username, accountPwd] = await setupCredentials( + envResponse[0], + labClient + ); + }); + + beforeEach(async () => { + context = await browser.createBrowserContext(); + page = await context.newPage(); + page.setDefaultTimeout(5000); + BrowserCache = new BrowserCacheUtils(page, "localStorage"); + await page.goto(`http://localhost:${port}`); + }); + + afterEach(async () => { + await page.close(); + await context.close(); + }); + + it("MsalAuthenticationTemplate - invokes loginPopup if user is not signed in (class component w/ raw context)", async () => { + const testName = "MsalAuthenticationTemplatePopupCase"; + const screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + await screenshot.takeScreenshot(page, "Home page loaded"); + + // Navigate to /profile and expect popup to be opened without interaction + const newPopupWindowPromise = new Promise((resolve) => + page.once("popup", resolve) + ); + await page.goto(`http://localhost:${port}/profileRawContext`); + await screenshot.takeScreenshot(page, "Profile page loaded"); + const popupPage = await newPopupWindowPromise; + if (!popupPage) { + throw new Error('Popup window was not opened'); + } + + await enterCredentials(popupPage, screenshot, username, accountPwd); + + // Wait for Graph data to display + await page.waitForSelector("xpath/.//div/ul/li[contains(., 'Name')]", { + timeout: 5000, + }); + await screenshot.takeScreenshot(page, "Graph data acquired"); + + // Verify tokens are in cache + await verifyTokenStore(BrowserCache, ["User.Read"]); + }); +}); diff --git a/samples/msal-react-samples/react18-sample/test/profileWithMsal.spec.ts b/samples/msal-react-samples/react18-sample/test/profileWithMsal.spec.ts new file mode 100644 index 0000000000..1106546587 --- /dev/null +++ b/samples/msal-react-samples/react18-sample/test/profileWithMsal.spec.ts @@ -0,0 +1,97 @@ +import * as puppeteer from "puppeteer"; +import { + Screenshot, + setupCredentials, + enterCredentials, + RETRY_TIMES, + LabClient, + LabApiQueryParams, + AzureEnvironments, + AppTypes, + BrowserCacheUtils, +} from "e2e-test-utils"; + +const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/profileWithMsal-tests`; + +async function verifyTokenStore( + BrowserCache: BrowserCacheUtils, + scopes: string[] +): Promise { + await BrowserCache.verifyTokenStore({ + scopes, + }); + const telemetryCacheEntry = await BrowserCache.getTelemetryCacheEntry( + "b5c2e510-4a17-4feb-b219-e55aa5b74144" + ); + expect(telemetryCacheEntry).not.toBeNull(); + expect(telemetryCacheEntry["cacheHits"]).toBeGreaterThanOrEqual(1); +} + +describe("/profileWithMsal", () => { + jest.retryTimes(RETRY_TIMES); + let browser: puppeteer.Browser; + let context: puppeteer.BrowserContext; + let page: puppeteer.Page; + let port: number; + let username: string; + let accountPwd: string; + let BrowserCache: BrowserCacheUtils; + + beforeAll(async () => { + // @ts-ignore + browser = await global.__BROWSER__; + // @ts-ignore + port = global.__PORT__; + + const labApiParams: LabApiQueryParams = { + azureEnvironment: AzureEnvironments.CLOUD, + appType: AppTypes.CLOUD, + }; + + const labClient = new LabClient(); + const envResponse = await labClient.getVarsByCloudEnvironment( + labApiParams + ); + + [username, accountPwd] = await setupCredentials( + envResponse[0], + labClient + ); + }); + + beforeEach(async () => { + context = await browser.createBrowserContext(); + page = await context.newPage(); + page.setDefaultTimeout(5000); + BrowserCache = new BrowserCacheUtils(page, "localStorage"); + await page.goto(`http://localhost:${port}`); + }); + + afterEach(async () => { + await page.close(); + await context.close(); + }); + + it("MsalAuthenticationTemplate - invokes loginRedirect if user is not signed in (class component w/ withMsal HOC)", async () => { + const testName = "MsalAuthenticationTemplateRedirectCase"; + const screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + await screenshot.takeScreenshot(page, "Home page loaded"); + + // Navigate to /profileWithMsal and expect redirect to be initiated without interaction + await page.goto(`http://localhost:${port}/profileWithMsal`); + await screenshot.takeScreenshot(page, "Profile page loaded"); + + await enterCredentials(page, screenshot, username, accountPwd); + + // Wait for Graph data to display + await page.waitForSelector("xpath/.//div/ul/li[contains(., 'Name')]", { + timeout: 5000, + }); + await screenshot.takeScreenshot(page, "Graph data acquired"); + + // Verify tokens are in cache + await verifyTokenStore(BrowserCache, ["User.Read"]); + }); +}); diff --git a/samples/msal-react-samples/react18-sample/tsconfig.json b/samples/msal-react-samples/react18-sample/tsconfig.json new file mode 100644 index 0000000000..a8df22f95c --- /dev/null +++ b/samples/msal-react-samples/react18-sample/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../tsconfig.json" +} \ No newline at end of file diff --git a/samples/msal-react-samples/react18-sample/vite.config.js b/samples/msal-react-samples/react18-sample/vite.config.js new file mode 100644 index 0000000000..9be5c7392c --- /dev/null +++ b/samples/msal-react-samples/react18-sample/vite.config.js @@ -0,0 +1,10 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + server: { + port: 3000, + strictPort: true, + }, +});