diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000000..3c44241cc4f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.eslintignore b/.eslintignore index 2c9a1fa5a85..b73884b7805 100644 --- a/.eslintignore +++ b/.eslintignore @@ -6,4 +6,3 @@ **/libs/ **/js/ **/plugins/ -apps/myaccount/src/main/* diff --git a/.eslintrc.js b/.eslintrc.js index 17fcd88adb7..7663cf94b94 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -133,7 +133,7 @@ module.exports = { ecmaVersion: 9, sourceType: "module" }, - plugins: [ "import" ], + plugins: [ "import", "eslint-plugin-tsdoc" ], root: true, rules: { "array-bracket-spacing": [ 1, "always" ], @@ -256,7 +256,8 @@ module.exports = { minKeys: 2, natural: false } - ] + ], + "tsdoc/syntax": "warn" }, settings: { react: { diff --git a/.github/workflows/pr-builder.yml b/.github/workflows/pr-builder.yml index 217ec2a815a..159b860d79b 100644 --- a/.github/workflows/pr-builder.yml +++ b/.github/workflows/pr-builder.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [ 12.x ] + node-version: [ lts/* ] steps: - name: ⬇️ Checkout id: checkout @@ -34,30 +34,41 @@ jobs: with: node-version: ${{ matrix.node-version }} - - name: 🛠️ Bootstrap Dependencies - id: bootstrap-npm-and-lerna-dependencies - run: npm run bootstrap + - name: 🥡 Setup pnpm + id: setup-pnpm + uses: pnpm/action-setup@v2.1.0 + with: + version: latest + run_install: false - - name: ⏳ Lint - run: npm run lint:ci + - name: 🎈 Get pnpm store directory + id: get-pnpm-cache-dir + run: | + echo "::set-output name=pnpm_cache_dir::$(pnpm store path)" - - name: 🔆 Cache NPM directory - id: cache-npm-modules - uses: actions/cache@v2 + - name: 🔆 Cache pnpm modules + uses: actions/cache@v3 + id: pnpm-cache with: - path: ~/.npm - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + path: ${{ steps.get-pnpm-cache-dir.outputs.pnpm_cache_dir }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- - ${{ runner.os }}-build- - ${{ runner.os }}- + ${{ runner.os }}-pnpm-store- + + - name: 🧩 Install Dependencies + id: install-dependencies + run: pnpm install --no-frozen-lockfile + + - name: ⏳ Lint + id: lint-with-eslint + run: pnpm lint typecheck: name: ʦ Typecheck (STATIC ANALYSIS) runs-on: ubuntu-latest strategy: matrix: - node-version: [ 12.x ] + node-version: [ lts/* ] steps: - name: ⬇️ Checkout id: checkout @@ -69,23 +80,36 @@ jobs: with: node-version: ${{ matrix.node-version }} - - name: 🛠️ Bootstrap Dependencies - id: bootstrap-npm-and-lerna-dependencies - run: npm run bootstrap + - name: 🥡 Setup pnpm + uses: pnpm/action-setup@v2.1.0 + with: + version: latest + run_install: false - - name: ☄️ Check Type Errors - run: npm run typecheck + - name: 🎈 Get pnpm store directory + id: get-pnpm-cache-dir + run: | + echo "::set-output name=pnpm_cache_dir::$(pnpm store path)" - - name: 🔆 Cache NPM directory - id: cache-npm-modules - uses: actions/cache@v2 + - name: 🔆 Cache pnpm modules + uses: actions/cache@v3 + id: pnpm-cache with: - path: ~/.npm - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + path: ${{ steps.get-pnpm-cache-dir.outputs.pnpm_cache_dir }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- - ${{ runner.os }}-build- - ${{ runner.os }}- + ${{ runner.os }}-pnpm-store- + + - name: 🧩 Install Dependencies + id: install-dependencies + run: pnpm install --no-frozen-lockfile + + - name: 👷 Build Re-usable Modules + id: build-reusable-modules + run: pnpm build:modules + + - name: ☄️ Check Type Errors + run: pnpm typecheck test: name: 👾 Unit Test (TESTING) @@ -93,7 +117,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [ 12.x ] + node-version: [ lts/* ] steps: - name: ⬇️ Checkout id: checkout @@ -105,35 +129,44 @@ jobs: with: node-version: ${{ matrix.node-version }} - - name: 🛠️ Bootstrap Dependencies - id: bootstrap-npm-and-lerna-dependencies - run: npm run bootstrap + - name: 🥡 Setup pnpm + uses: pnpm/action-setup@v2.1.0 + with: + version: latest + run_install: false + + - name: 🎈 Get pnpm store directory + id: get-pnpm-cache-dir + run: | + echo "::set-output name=pnpm_cache_dir::$(pnpm store path)" + + - name: 🔆 Cache pnpm modules + uses: actions/cache@v3 + id: pnpm-cache + with: + path: ${{ steps.get-pnpm-cache-dir.outputs.pnpm_cache_dir }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: 🧩 Install Dependencies + id: install-dependencies + run: pnpm install --no-frozen-lockfile - name: 👷 Build Re-usable Modules id: build-reusable-modules - run: npm run build:modules + run: pnpm build:modules - name: 🃏 Run Jest & Collect Coverage id: run-jest-test-and-coverage - run: npm run test:unit:coverage + run: pnpm test:unit:coverage - name: 🤖 Aggregate Test Coverage id: aggregate-coverage-reports run: | - npm run test:unit:coverage:aggregate - npm run nyc:text-summary-report - npm run nyc:text-report - - - name: 🔆 Cache NPM directory - id: cache-npm-modules - uses: actions/cache@v2 - with: - path: ~/.npm - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- - ${{ runner.os }}-build- - ${{ runner.os }}- + pnpm test:unit:coverage:aggregate + pnpm nyc:text-summary-report + pnpm nyc:text-report build: name: 🚧 Build @@ -141,7 +174,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [ 12.x ] + node-version: [ lts/* ] maven-version: [ 3.6.3 ] java-version: [ 1.8 ] steps: @@ -167,24 +200,33 @@ jobs: with: maven-version: ${{ matrix.maven-version }} - - name: 🛠️ Bootstrap Dependencies - id: bootstrap-npm-and-lerna-dependencies - run: npm run bootstrap + - name: 🥡 Setup pnpm + uses: pnpm/action-setup@v2.1.0 + with: + version: latest + run_install: false - - name: 🏗️ Maven Build - id: build-with-maven - run: mvn clean install -U -Dlint.exec.skip=true -Dbootstrap.exec.skip=true + - name: 🎈 Get pnpm store directory + id: get-pnpm-cache-dir + run: | + echo "::set-output name=pnpm_cache_dir::$(pnpm store path)" - - name: 🔆 Cache NPM directory - id: cache-npm-modules - uses: actions/cache@v2 + - name: 🔆 Cache pnpm modules + uses: actions/cache@v3 + id: pnpm-cache with: - path: ~/.npm - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + path: ${{ steps.get-pnpm-cache-dir.outputs.pnpm_cache_dir }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- - ${{ runner.os }}-build- - ${{ runner.os }}- + ${{ runner.os }}-pnpm-store- + + - name: 🧩 Install Dependencies + id: install-dependencies + run: pnpm install --no-frozen-lockfile + + - name: 🏗️ Maven Build + id: build-with-maven + run: mvn clean install -U -Dlint.exec.skip=true -Dbootstrap.exec.skip=true - name: 💾 Cache local Maven repository id: cache-maven-m2 diff --git a/.gitignore b/.gitignore index 5f5fcda5850..295ddb854ba 100644 --- a/.gitignore +++ b/.gitignore @@ -57,8 +57,9 @@ build/Release node_modules/ jspm_packages/ -# TypeScript v1 declaration files +# TypeScript typings/ +tsconfig.tsbuildinfo # Optional npm cache directory .npm @@ -88,6 +89,7 @@ typings/ .next # project build folders & files +dist target/ npm/ modules/*/dist @@ -96,6 +98,7 @@ modules/theme/src/semantic-ui-core/definitions modules/theme/src/themes/sample modules/react-components/storybook-static modules/react-components/.cache +modules/react-components/public/themes apps/*/build/ apps/*/dist/ apps/*/src/main/webapp/libs/themes @@ -105,6 +108,9 @@ apps/*/cache apps/myaccount/src/themes/ apps/console/src/themes/ apps/console/src/extensions/i18n/dist +apps/*/src/main/webapp/extensions/layouts +apps/*/src/main/webapp/includes/layouts +apps/console/src/login-portal-layouts # Integration test module. tests/output/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000000..4c2f52b3be7 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +auto-install-peers=true +strict-peer-dependencies=false diff --git a/README.md b/README.md index bb125d51e86..7a67e30aa68 100644 --- a/README.md +++ b/README.md @@ -2,151 +2,77 @@ End-user apps in WSO2 Identity Server +=============================== + | Branch | Build Status | Travis CI Status | | :------------ |:------------- |:------------- | master | [![Build Status](https://wso2.org/jenkins/view/Dashboard/job/platform-builds/job/identity-apps/badge/icon)](https://wso2.org/jenkins/view/Dashboard/job/platform-builds/job/identity-apps/) | [![Build Status](https://travis-ci.org/wso2/identity-apps.svg?branch=master)](https://travis-ci.org/wso2/identity-apps) | -## Setup build environment - -1. Install NodeJS from [https://nodejs.org/en/download/](https://nodejs.org/en/download/). -2. Install Maven from [https://maven.apache.org/download.cgi](https://maven.apache.org/download.cgi). * For Maven 3.8 and up, please check the Troubleshoot section. -3. Install JDK 1.8 [https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html](https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html). - -## Build & Run - -#### Build - -1. Download or clone the project source code from [https://github.com/wso2/identity-apps](https://github.com/wso2/identity-apps) -2. Run `mvn clean install` from the command line in the project root directory (where the root `pom.xml` is located). - -If you are building [product-is](https://github.com/wso2/product-is), the built identity apps dependencies will install to your local `.m2` repository during the build above. - -3. Then you just need to build [WSO2 Identity Server](https://github.com/wso2/product-is) after. _(Follow the guide there)_ +[![Stackoverflow](https://img.shields.io/badge/Ask%20for%20help%20on-Stackoverflow-orange)](https://stackoverflow.com/questions/tagged/wso2is) +[![Slack](https://img.shields.io/badge/Join%20us%20on-Slack-%23e01563.svg)](https://join.slack.com/t/wso2is/shared_invite/zt-19lsbfvhc-T6t0p_J4tXcMvnuHX8605w) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/wso2/product-is/blob/master/LICENSE) +[![Twitter](https://img.shields.io/twitter/follow/wso2.svg?style=social&label=Follow)](https://twitter.com/intent/follow?screen_name=wso2) -#### Run +## Prerequisite -4. Execute `wso2server.sh` (For unix environment) or `wso2server.bat` (For windows environment) file from the `bin` directory to run the WSO2 Identity Server. -5. Navigate to `https://localhost:9443/myaccount` or `https://localhost:9443/console` from the browser. (Add certificate exception if required) +### Setup Development Environment -## Run in dev mode +1. Install NodeJS LTS(Latest Stable Version) from [https://nodejs.org/en/download/](https://nodejs.org/en/download/). +2. Instal the latest version of [pnpm](https://pnpm.io/). -1. **Do only if you skip WSO2 Identity Server build step above:** Download the built distribution of WSO2 Identity Server from [https://wso2.com/identity-and-access-management/](https://wso2.com/identity-and-access-management/). - -2. Add the following code to `repository/conf/deployment.toml` in `WSO2 Identity Server` distribution pack to allow CORS. - - ```toml - [cors] - allowed_origins = [ - "https://localhost:9000", - "https://localhost:9001" - ] - supported_methods = [ - "GET", - "POST", - "HEAD", - "OPTIONS", - "PUT", - "PATCH", - "HEAD", - "DELETE", - "PATCH" - ] - exposed_headers = [ "Location" ] + ```shell + npm install -g pnpm@latest ``` -3. Add your hostname and port as a trusted FIDO2 origin to the `deployment.toml` file as given below. - ```toml - [fido.trusted] - origins=["https://localhost:9000"] - ``` -4. Currently, `Console` & `My Account` are considered as system applications hence they are readonly by default. So in order to configure the `Callback Urls` as specified in **step 7**, you need to add the following config to the `deployment.toml` file to override the default behaviour. + Or, follow the other [recommended installation options](https://pnpm.io/installation). - ```toml - [system_applications] - read_only_apps = [] - ``` -5. Execute `wso2server.sh` (For unix environment) or `wso2server.bat` (For windows environment) file from the `bin` directory to run WSO2 Identity Server. -6. Navigate to `https://localhost:9443/carbon/` from the browser, and login to the system by entering an admin password. -> **Hint!** Can find out the default password details here: [https://docs.wso2.com/display/ADMIN44x/Configuring+the+System+Administrator](https://docs.wso2.com/display/ADMIN44x/Configuring+the+System+Administrator) -7. In the system, navigate to `Service Providers -> List` from left side panel. And then go to `Edit` option in the application that you want to configure in dev mode (ex: `MY_ACCOUNT`). Then click on `Inbound Authentication Configuration -> OAuth/OpenID Connect Configuration -> Edit`. And then update the `Callback Url` field with below corresponding values. +3. Install Maven from [https://maven.apache.org/download.cgi](https://maven.apache.org/download.cgi). +4. Install JDK 1.8 [https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html](https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html). - **Console** +### Download WSO2 Identity Server - ``` - regexp=(https://localhost:9443/console|https://localhost:9443/t/(.*)/console|https://localhost:9443/console/login|https://localhost:9443/t/(.*)/console/login|https://localhost:9001/console|https://localhost:9001/t/(.*)/console|https://localhost:9001/console/login|https://localhost:9001/t/(.*)/console/login) - ``` +In order to setup this repository locally, you need to have [WSO2 Identity Server](https://wso2.com/identity-server/) installed on your local environment. - **My Account** +We recommend you to download the [latest release](https://github.com/wso2/product-is/releases) or build the [product-is](https://github.com/wso2/product-is) from [source](https://github.com/wso2/product-is#building-the-distribution-from-source). - ``` - regexp=(https://localhost:9443/myaccount|https://localhost:9443/t/(.*)/myaccount|https://localhost:9443/myaccount/login|https://localhost:9443/t/(.*)/myaccount/login|https://localhost:9000/myaccount|https://localhost:9000/t/(.*)/myaccount|https://localhost:9000/myaccount/login|https://localhost:9000/t/(.*)/myaccount/login) - ``` - -8. Open cloned or downloaded Identity Apps repo and run the following commands from the command line in the project root directory (where the `package.json` is located) to build all the packages with dependencies. _(Note:- Not necessary if you have already done above identity apps build steps)_ - - ```bash - # `npm run bootstrap` will install npm dependencies and bootstrap lerna modules. - npm run bootstrap && npm run build - ``` - - or - - ```bash - # This will run `npm run bootstrap && npm run build` in the background. - npm run build:dev - ``` - - > **_Note:-_** - > - > To build a single package/app, you can use this command: `npx lerna bootstrap --scope && npx lerna run --scope build`. - > - > E.g. `npx lerna bootstrap --scope @wso2is/myaccount && npx lerna run --scope @wso2is/myaccount build` +### Setup WSO2 Identity Server -9. Start the apps in development mode, Execute `cd apps/ && npm start` command. E.g. `cd apps/myaccount && npm start`. -10. Once the app is successfully started, you can access the via the URLs `https://localhost:9000/myaccount` or `https://localhost:9001/console`. +Follow the [Identity Server setup guide](./docs/prerequisite/SETUP_IDENTITY_SERVER.md) and configure the WSO2 Identity Server for running the apps in the development mode. -## Running Tests - -### Unit Tests +## Build & Run -Product Unit tests have been implemented using [Jest](https://jestjs.io/) along with [React Testing Library](https://testing-library.com/docs/react-testing-library/intro) -and you can run the unit test suites using the following commands. +### Build -#### Run Tests for all modules +Clone or download the Identity Apps repository and run the following commands from the command line in the project root directory (where the `package.json` is located) to build all the packages with dependencies. -```bash -npm run test +```shell +# From project root. +mvn clean install ``` -#### Run Tests for individual module - -```bash -npx lerna run test --scope @wso2is/forms -``` +### Run -### Integration Tests +To start the apps in development mode, execute the following commands accordingly. -Product integration tests have been written using [Cypress Testing Framework](https://www.cypress.io/) and you can run the test suites using the following command. +#### Console -#### Headless mode - -```bash -npm run test:integration +```shell +# To start Console +cd apps/console +pnpm start ``` -#### Interactive mode - -```bash -npm run test:integration:interactive -``` +Once the development server is live, you can access the application via [https://localhost:9001/console](https://localhost:9001/console). -#### Only Smoke Tests +#### My Account -```bash -npm run test:integration:smoke +```shell +# To start My Account +cd apps/myaccount +pnpm start ``` -For more information regarding the test module, checkout the [README](./tests/README.md) in the `tests` module. +Once the development server is live, you can access the application via [https://localhost:9000/myaccount](https://localhost:9000/myaccount). ## Configuration @@ -155,96 +81,17 @@ Read through our [configurations guidelines](./docs/CONFIGURATION.md) to learn a ## Deployment -#### Deploying the apps on an external server +Go through our [deployment guide](./docs/DEPLOYMENT.md) to learn the different supported app deployment options. -It is possible to deploy the Console and My Account applications on an external server. To do so, the following steps has to be followed in order to build the applications. +## Connectors -##### Method 1 - Build using Maven - -Follow the steps in listed [here](#build) in-order to build the project with maven. - -Once the build is complete, execute the following commands in-order to build the Console & My Account applications for external deployment. - -**Console** - -###### Deploy on a Java EE server (ex: Tomcat) - -```bash -npx lerna run build:external --scope @wso2is/console -``` - -###### Deploy on a static server. - -```bash -npx lerna run build:external:static --scope @wso2is/console -``` - -Once the build is completed, you can find the build artifacts inside the build folder i.e `apps/console/build`. - -**My Account** - -###### Deploy on a Java EE server (ex: Tomcat) - -```bash -npx lerna run build:external --scope @wso2is/myaccount -``` - -###### Deploy on a static server. - -```bash -npx lerna run build:external:static --scope @wso2is/myaccount -``` - -Once the build is completed, you can find the build artifacts inside the build folder i.e `apps/myaccount/build`. - -##### Method 2 - Build using npm - -You can simply use npm to build the Console and My Account applications for external deployment by just executing the following script. - -###### Deploy on a Java EE server (ex: Tomcat) - -```bash -# From project root -npm run build:external -``` - -###### Deploy on a static server. - -```bash -# From project root -npm run build:external:static -``` - -The respective build artifacts could be found inside the build folder. (`apps/(myaccount|console)/build`) +Go through our [connectors guide](./docs/CONNECTORS.md) to learn how to handle connectors in the Identity Server Console. ## Troubleshoot -- If you face any out of memory build failures, make sure that you have set maven options to `set MAVEN_OPTS=-Xmx384M` -- For Maven v3.8 up, add below configuration to the `~/.m2/settings.xml` (Create a new file if the file exist) - ```xml - - - - wso2-nexus-public - external:http:* - http://maven.wso2.org/nexus/content/groups/wso2-public/ - false - - - wso2-nexus-release - external:http:* - http://maven.wso2.org/nexus/content/repositories/releases/ - false - - - wso2-nexus-snapshots - external:http:* - http://maven.wso2.org/nexus/content/repositories/snapshots/ - false - - - - ``` +Go through our [troubleshooting guide](./docs/TROUBLESHOOTING.md) to clarify and issues you encounter. + +If the issue you are facing is not on the existing guide, consider reaching out to us on slack, stackoverflow threads or by creating an issue as described in [Reporting Issues](#reporting-issues). ## Contributing @@ -259,3 +106,6 @@ We encourage you to report issues, improvements and feature requests regarding t ## License Licenses this source under the Apache License, Version 2.0 ([LICENSE](LICENSE)), You may not use this file except in compliance with the License. + +--------------------------------------------------------------------------- +(c) Copyright 2022 WSO2 LLC. diff --git a/apps/authentication-portal/package.json b/apps/authentication-portal/package.json index 5092e17c72a..79602396707 100644 --- a/apps/authentication-portal/package.json +++ b/apps/authentication-portal/package.json @@ -1,13 +1,18 @@ { "name": "@wso2is/authentication-portal", - "version": "1.2.893", + "version": "1.4.67", "description": "WSO2 Identity Server Authentication Portal", "author": "WSO2", "license": "Apache-2.0", "scripts": { - "build": "node scripts/build.js" + "build": "node scripts/build.js", + "clean": "pnpm clean:lock-files && pnpm clean:maven-folders && pnpm clean:node-modules", + "clean:lock-files": "pnpm rimraf package-lock.json && pnpm rimraf pnpm-lock.yaml && pnpm rimraf yarn.lock", + "clean:maven-folders": "pnpm rimraf target", + "clean:node-modules": "pnpm rimraf node_modules" }, "dependencies": { - "@wso2is/theme": "^1.2.893" + "@wso2is/theme": "*", + "rimraf": "^3.0.2" } } diff --git a/apps/authentication-portal/pom.xml b/apps/authentication-portal/pom.xml index b6c3e834334..d4716fcd8ef 100644 --- a/apps/authentication-portal/pom.xml +++ b/apps/authentication-portal/pom.xml @@ -19,7 +19,7 @@ org.wso2.identity.apps identity-apps - 1.2.893-SNAPSHOT + 1.4.67-SNAPSHOT ../../pom.xml @@ -238,12 +238,23 @@ org.wso2.carbon.extension.identity.authenticator.outbound.totp org.wso2.carbon.extension.identity.authenticator.totp.connector provided + + + opensaml + opensaml + + org.wso2.carbon.identity.governance org.wso2.carbon.identity.captcha provided + + org.wso2.identity.apps + org.wso2.identity.apps.taglibs.layout.controller + 1.4.34 + diff --git a/apps/authentication-portal/project.json b/apps/authentication-portal/project.json new file mode 100644 index 00000000000..8f257aaf345 --- /dev/null +++ b/apps/authentication-portal/project.json @@ -0,0 +1,32 @@ +{ + "root": "apps/authentication-portal", + "projectType": "application", + "targets": { + "build": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "pnpm build", + "description": "Building Authentication Portal" + } + ], + "cwd": "apps/authentication-portal", + "parallel": false + } + }, + "clean": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "pnpm clean", + "description": "Cleaning Authentication Portal" + } + ], + "cwd": "apps/authentication-portal", + "parallel": false + } + } + } +} diff --git a/apps/authentication-portal/scripts/build.js b/apps/authentication-portal/scripts/build.js index b80897b6417..9fef1499121 100644 --- a/apps/authentication-portal/scripts/build.js +++ b/apps/authentication-portal/scripts/build.js @@ -21,7 +21,7 @@ const path = require("path"); const fs = require("fs-extra"); const srcDir = path.join(__dirname, "..", "src", "main", "webapp"); -const themeModuleDir = path.join(__dirname, "../", "node_modules", "@wso2is", "theme"); +const themeModuleDir = path.join(__dirname, "..", "node_modules", "@wso2is", "theme"); fs.copy(path.join(themeModuleDir, "dist", "lib"), path.join(srcDir, "libs")) .then(() => { diff --git a/apps/authentication-portal/src/main/resources/org/wso2/carbon/identity/application/authentication/endpoint/i18n/Resources.properties b/apps/authentication-portal/src/main/resources/org/wso2/carbon/identity/application/authentication/endpoint/i18n/Resources.properties index fe7c0ef088f..e00fbea4c28 100644 --- a/apps/authentication-portal/src/main/resources/org/wso2/carbon/identity/application/authentication/endpoint/i18n/Resources.properties +++ b/apps/authentication-portal/src/main/resources/org/wso2/carbon/identity/application/authentication/endpoint/i18n/Resources.properties @@ -29,7 +29,8 @@ login.fail.message=Login failed! Please check your username and password and try emailusername.fail.message=Invalid username! Username should be an E-Mail address. username.fail.message=Could not find any account with this user name! Please recheck the username and try again. password.fail.message=Login failed! Please recheck the password and try again. -recaptcha.fail.message=reCaptcha validation is required for user. +query.params.contains.user.credentials=Request URL contains user credentials. Cannot send user credentials as query parameters with the URL. +recaptcha.fail.message=reCAPTCHA verification failed. Please try again. account.confirmation.pending=Account is unverified. An account activation link has been sent to your registered email address, please check your inbox. password.reset.pending=Password reset is required. A password reset link has been sent to your registered email address, please check your inbox. account.resend.email.success=Email sent successfully. @@ -192,6 +193,8 @@ resend.confirmation.page.message=Please complete the captcha below. error.retry=Authentication Failed! Please Retry error.retry.code.invalid=Authentication failed. The code you entered is invalid or expired. Please retry. authenticate=Continue +no.valid.client.in.tenant=A valid client with the given client_id cannot be found in the tenant domain. +not.authorized.for.requested.grant.type=The authenticated client is not authorized to use the requested grant type. # TOTP authentication error.fail=Authentication Failed! @@ -206,6 +209,12 @@ enable.totp=Set up an Authenticator App error.totp.not.enabled.please.enable=Scan the QR code below using an authenticator app to verify your identity using codes generated by the app. show.qr.code=Show QR code to scan and enrol the user confirm.you.have.scanned.the.qr.code=Have you scanned the QR code? You need to enter the verification code in the next step. +cannot.access.totp=Can't access TOTP authenticator? + +# Backup Code authentication +error.backup.code.not.enabled=Enable the Backup codes in your Profile. Cannot proceed further without Backup code authentication. +auth.backup.code=Enter the Backup Code +use.backup.code=Use Backup Codes # Email OTP authentication enter.email=Enter Your Email Address @@ -265,6 +274,9 @@ update.local.user.claims.error=Error occurred while updating claims for local us retrieving.realm.to.handle.claims.error=Error occurred while retrieving the Realm for the tenant domain to handle local claims. post.auth.cookie.not.found=Your authentication flow is ended or invalid. Post authentication sequence tracking cookie not found in the request. Please initiate again. +# FIDO authentication +authentication.failed=Authentication Failed! +no.registered.device.found=No registered device found, Please register your device before sign in. fido.failed.instruction=Click on proceed and follow the instructions given by your browser to authenticate yourself using a security key or biometrics in your device. fido.error=Sign In with security key/biometrics interrupted! fido.registration.info=Haven’t registered your security key? Register in @@ -273,3 +285,23 @@ fido.retry=Retry fido.proceed=Proceed fido.cancel=Cancel fido.authenticator=Security Key + +# Magic Link +magic.link.heading=Check your inbox! +magic.link.content=Click the link we sent to your email to sign in. +magic.link=Magic Link +magic.link.info=Use the same browser to open the link. + +# SMS OTP +enter.code.sent.smsotp=Enter the code sent to your mobile phone: +auth.with.smsotp=Authenticating with SMSOTP +error.code.expired.resend=The code entered is expired. Click Resend Code to continue. +error.send.smsotp=Unable to send code to your phone number +error.wrong.code=The code entered is incorrect. Authentication Failed! +error.failed.with.smsotp=Failed Authentication with SMSOTP +error.smsotp.disabled=Enable the SMS OTP in your Profile. Cannot proceed further without SMS OTP authentication. +error.token.expired=The code entered is expired. Authentication Failed! +error.user.not.found.smsotp=User not found in the directory. Cannot proceed further without SMS OTP authentication. +authenticate.button=Authenticate +please.enter.code=Please enter the code! +enter.phone.number=Enter Your Mobile Phone Number diff --git a/apps/authentication-portal/src/main/resources/org/wso2/carbon/identity/application/authentication/endpoint/i18n/Resources_fr_FR.properties b/apps/authentication-portal/src/main/resources/org/wso2/carbon/identity/application/authentication/endpoint/i18n/Resources_fr_FR.properties index 2f62cea262e..1bd0b61b27f 100644 --- a/apps/authentication-portal/src/main/resources/org/wso2/carbon/identity/application/authentication/endpoint/i18n/Resources_fr_FR.properties +++ b/apps/authentication-portal/src/main/resources/org/wso2/carbon/identity/application/authentication/endpoint/i18n/Resources_fr_FR.properties @@ -29,7 +29,8 @@ login.fail.message=Échec de la connexion! Veuillez vérifier vos identifiant et emailusername.fail.message=Nom d'utilisateur invalide ! Le nom d'utilisateur doit être une adresse e-mail. username.fail.message=Impossible de trouver un compte avec ce nom d'utilisateur ! Veuillez revérifier le nom d'utilisateur et réessayer. password.fail.message=Connexion échouée ! Veuillez revérifier le mot de passe et réessayer. -recaptcha.fail.message=La validation par reCaptcha est requise pour l'utilisateur. +query.params.contains.user.credentials=L'URL de la demande contient les informations d'identification de l'utilisateur. Impossible d'envoyer les informations d'identification de l'utilisateur en tant que paramètres de requête avec l'URL. +recaptcha.fail.message=La vrification reCAPTCHA a chou. Veuillez ressayer. account.confirmation.pending=Le compte n'est pas vérifié. Un lien d'activation de compte a été envoyé à votre adresse électronique enregistrée, veuillez vérifier votre boîte de réception. password.reset.pending=La réinitialisation du mot de passe est requise. Un lien de réinitialisation du mot de passe a été envoyé à votre adresse électronique account.resend.email.success=L'e-mail a été envoyé avec succès. @@ -189,6 +190,8 @@ resend.confirmation.page.message=Veuillez compléter le captcha ci-dessous. error.retry=Authentification échouée ! Veuillez réessayer error.retry.code.invalid=Authentification échouée. Le code que vous avez entré est invalide ou a expiré. Veuillez réessayer authenticate=Continuer +no.valid.client.in.tenant=Un client valide avec le client_id donn est introuvable dans le domaine du locataire. +not.authorized.for.requested.grant.type=Le client authentifi n'est pas autoris utiliser le type d'octroi demand. # TOTP authentication error.fail=Echec de l'authentification ! @@ -203,6 +206,12 @@ enable.totp=Configurer une application Authenticator error.totp.not.enabled.please.enable=Scannez le code QR ci-dessous à l'aide d'une application d'authentification pour vérifier votre identité à l'aide des codes générés par l'application. show.qr.code=Afficher le code QR pour le scanner et vous enroler confirm.you.have.scanned.the.qr.code=Avez-vous scanné le code QR? Vous devez entrer le code de vérification à l'étape suivante. +cannot.access.totp=Impossible d'acc?der ? l'authentificateur TOTP ? + +# Backup Code authentication +error.backup.code.not.enabled=Activez les codes de sauvegarde dans votre profil. Impossible de continuer sans authentification par code de secours. +auth.backup.code=Entrez le code de secours +use.backup.code=Utiliser des codes de secours # Email OTP authentication enter.email=Saisissez votre adresse électronique @@ -238,3 +247,23 @@ fido.retry=Retenter fido.proceed=Procéder fido.cancel=Annuler fido.authenticator=Connectez-vous avec la clé de sécurité + +#Magic Link +magic.link.heading=Vérifiez votre boîte de réception ! +magic.link.content=Cliquez sur le lien que nous vous avons envoyé par e-mail pour vous connecter. +magic.link=Lien magique +magic.link.info=Utilisez le même navigateur pour ouvrir le lien. + +# SMS OTP +enter.code.sent.smsotp=Entrez le code envoyé à votre téléphone portable : +auth.with.smsotp=S'authentifier avec SMSOTP +error.code.expired.resend=Le code saisi est expiré. Cliquez sur Renvoyer le code pour continuer. +error.send.smsotp=Impossible d'envoyer le code à votre numéro de téléphone +error.wrong.code=Le code saisi est incorrect. L'authentification a échoué ! +error.failed.with.smsotp=Echec de l'authentification par SMSOTP +error.smsotp.disabled=Activez l'OTP SMS dans votre profil. Impossible de continuer sans celui-ci. +error.token.expired=Le code saisi est expiré. L'authentification a échoué ! +error.user.not.found.smsotp=Utilisateur introuvable dans l'annuaire. Impossible de continuer. +authenticate.button=S'uthentifier +please.enter.code=Veuillez entrer le code ! +enter.phone.number=Entrez votre numéro de téléphone portable diff --git a/apps/authentication-portal/src/main/webapp/WEB-INF/web.xml b/apps/authentication-portal/src/main/webapp/WEB-INF/web.xml index 5e4c48bd111..e5c35ff837c 100644 --- a/apps/authentication-portal/src/main/webapp/WEB-INF/web.xml +++ b/apps/authentication-portal/src/main/webapp/WEB-INF/web.xml @@ -312,6 +312,16 @@ /totpError.jsp + + backup_code.do + /backup-code.jsp + + + + backup_code_error.do + /backup-code-error.jsp + + email_capture.do /emailAddressCapture.jsp @@ -327,6 +337,41 @@ /emailOtpError.jsp + + mobile.do + /mobile.jsp + + + + sms_otp.do + /smsOtp.jsp + + + + sms_otp_error.do + /smsOtpError.jsp + + + + magic_link_notification.do + /magic_link_notification.jsp + + + + magic_link_notification.do + /magic_link_notification.do + + + + org_name.do + /org_name.jsp + + + + select_org.do + /select_org.jsp + + totp_enroll.do /totp_enroll.do @@ -342,6 +387,16 @@ /totp_error.do + + backup_code.do + /backup_code.do + + + + backup_code_error.do + /backup_code_error.do + + email_capture.do /email_capture.do @@ -357,6 +412,31 @@ /email_otp_error.do + + mobile.do + /mobile.do + + + + sms_otp.do + /sms_otp.do + + + + sms_otp_error.do + /sms_otp_error.do + + + + org_name.do + /org_name.do + + + + select_org.do + /select_org.do + + device.do /device.do diff --git a/apps/authentication-portal/src/main/webapp/add-security-questions.jsp b/apps/authentication-portal/src/main/webapp/add-security-questions.jsp index f0c340841c5..646c3faa8f8 100644 --- a/apps/authentication-portal/src/main/webapp/add-security-questions.jsp +++ b/apps/authentication-portal/src/main/webapp/add-security-questions.jsp @@ -34,6 +34,9 @@ <%@ page import="java.util.List" %> <%@ page import="java.util.Map" %> <%@ page import="java.io.File" %> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> + + <% String BUNDLE = "org.wso2.carbon.identity.application.authentication.endpoint.i18n.Resources"; @@ -71,6 +74,11 @@ } %> +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "medium"); +%> + @@ -85,9 +93,8 @@ <% } %> -
-
- + + <% File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); @@ -97,7 +104,8 @@ <% } else { %> <% } %> - + +

<%=AuthenticationEndpointUtil.i18n(resourceBundle, "answer.following.questions")%>

@@ -154,18 +162,19 @@
- -
- - - <% - File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); - if (productFooterFile.exists()) { - %> - - <% } else { %> - - <% } %> + + + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + + <% diff --git a/apps/authentication-portal/src/main/webapp/authenticate.jsp b/apps/authentication-portal/src/main/webapp/authenticate.jsp index 5ff332f0822..3bc944968d4 100644 --- a/apps/authentication-portal/src/main/webapp/authenticate.jsp +++ b/apps/authentication-portal/src/main/webapp/authenticate.jsp @@ -34,10 +34,13 @@ <%@ page import="static org.wso2.carbon.identity.application.authentication.endpoint.util.Constants.SESSION_DATA_KEY" %> <%@ page import="static org.wso2.carbon.identity.application.authentication.endpoint.util.Constants.AUTHENTICATION_REST_ENDPOINT_URL" %> <%@ page import="java.io.File" %> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> <%@include file="includes/localize.jsp" %> + + <% String sessionDataKey = request.getParameter(SESSION_DATA_KEY); String token = ""; @@ -102,6 +105,11 @@ } %> +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "large"); +%> + @@ -116,9 +124,8 @@ <% } %> -
-
- + + <% File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); @@ -128,7 +135,8 @@ <% } else { %> <% } %> - + +

<%=AuthenticationEndpointUtil.i18n(resourceBundle, "you.are.redirected.back.to")%> <%=commonauthURL%> <%=AuthenticationEndpointUtil.i18n(resourceBundle, "if.the.redirection.fails.please.click")%>.

@@ -139,18 +147,19 @@
-
-
- - - <% - File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); - if (productFooterFile.exists()) { - %> - - <% } else { %> - - <% } %> + + + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + + <% diff --git a/apps/authentication-portal/src/main/webapp/backup-code-error.jsp b/apps/authentication-portal/src/main/webapp/backup-code-error.jsp new file mode 100755 index 00000000000..1a4a23ef6ca --- /dev/null +++ b/apps/authentication-portal/src/main/webapp/backup-code-error.jsp @@ -0,0 +1,127 @@ +<%-- + ~ Copyright (c) 2022, WSO2 Inc. (http://www.wso2.com) All Rights Reserved. + ~ + ~ WSO2 Inc. licenses this file to you under the Apache License, + ~ Version 2.0 (the "License"); you may not use this file except + ~ in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, + ~ software distributed under the License is distributed on an + ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + ~ KIND, either express or implied. See the License for the + ~ specific language governing permissions and limitations + ~ under the License. +--%> + +<%@ page import="org.wso2.carbon.identity.application.authentication.endpoint.util.Constants" %> +<%@ page import="java.io.File" %> +<%@ page import="java.util.Map" %> +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ include file="includes/localize.jsp" %> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> + + + +<% + request.getSession().invalidate(); String queryString=request.getQueryString(); + Map idpAuthenticatorMapping = null; + if (request.getAttribute(Constants.IDP_AUTHENTICATOR_MAP) != null) { + idpAuthenticatorMapping = (Map)request.getAttribute(Constants.IDP_AUTHENTICATOR_MAP); + } + + String errorMessage = AuthenticationEndpointUtil.i18n(resourceBundle,"error.fail"); + String authenticationFailed = "false"; + + if (Boolean.parseBoolean(request.getParameter(Constants.AUTH_FAILURE))) { + authenticationFailed = "true"; + + if (request.getParameter(Constants.AUTH_FAILURE_MSG) != null) { + errorMessage = request.getParameter(Constants.AUTH_FAILURE_MSG); + + if (errorMessage.equalsIgnoreCase("authentication.fail.message")) { + errorMessage = AuthenticationEndpointUtil.i18n(resourceBundle,"error.fail"); + } + } + } +%> + +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "medium"); +%> + + + + + + <% + File headerFile = new File(getServletContext().getRealPath("extensions/header.jsp")); + if (headerFile.exists()) { + %> + + <% } else { %> + + <% } %> + + + + + + + + + + + <% + File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); + if (productTitleFile.exists()) { + %> + + <% } else { %> + + <% } %> + + +
+ +

<%=AuthenticationEndpointUtil.i18n(resourceBundle, "error.fail")%>

+
+
+
+
+ <%=AuthenticationEndpointUtil.i18n(resourceBundle, "error.backup.code.not.enabled")%> +
+
+
+
+
+ + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + +
+ + + <% File footerFile = new File(getServletContext().getRealPath("extensions/footer.jsp")); + if (footerFile.exists()) { %> + + <% } else { %> + + <% } %> + + + diff --git a/apps/authentication-portal/src/main/webapp/backup-code.jsp b/apps/authentication-portal/src/main/webapp/backup-code.jsp new file mode 100755 index 00000000000..0564f841466 --- /dev/null +++ b/apps/authentication-portal/src/main/webapp/backup-code.jsp @@ -0,0 +1,176 @@ +<%-- + ~ Copyright (c) 2022, WSO2 Inc. (http://www.wso2.com) All Rights Reserved. + ~ + ~ WSO2 Inc. licenses this file to you under the Apache License, + ~ Version 2.0 (the "License"); you may not use this file except + ~ in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, + ~ software distributed under the License is distributed on an + ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + ~ KIND, either express or implied. See the License for the + ~ specific language governing permissions and limitations + ~ under the License. +--%> + +<%@ page import="org.owasp.encoder.Encode" %> +<%@ page import="org.wso2.carbon.identity.application.authentication.endpoint.util.Constants" %> +<%@ page import="org.wso2.carbon.identity.application.authentication.endpoint.util.AuthenticationEndpointUtil" %> +<%@ page import="java.io.File" %> +<%@ page import="java.util.Map" %> +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ include file="includes/localize.jsp" %> +<%@ include file="includes/init-url.jsp" %> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> + + + +<% request.getSession().invalidate(); String queryString=request.getQueryString(); + Map idpAuthenticatorMapping = null; + if (request.getAttribute(Constants.IDP_AUTHENTICATOR_MAP) != null) { + idpAuthenticatorMapping = (Map) request.getAttribute(Constants.IDP_AUTHENTICATOR_MAP); + } + String errorMessage = AuthenticationEndpointUtil.i18n(resourceBundle,"error.retry"); + String authenticationFailed = "false"; + + if (Boolean.parseBoolean(request.getParameter(Constants.AUTH_FAILURE))) { + authenticationFailed = "true"; + + if (request.getParameter(Constants.AUTH_FAILURE_MSG) != null) { + errorMessage = Encode.forHtmlAttribute(request.getParameter(Constants.AUTH_FAILURE_MSG)); + if (errorMessage.equalsIgnoreCase("authentication.fail.message") || errorMessage.equalsIgnoreCase("login.fail.message")) { + errorMessage = AuthenticationEndpointUtil.i18n(resourceBundle,"error.retry"); + } + } + } +%> + +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "medium"); +%> + + + + + <% File headerFile=new File(getServletContext().getRealPath("extensions/header.jsp")); + if (headerFile.exists()) { + %> + + <% } else { %> + + <% } %> + + + + + + + + + <% if (new File(getServletContext().getRealPath("extensions/timeout.jsp")).exists()) { %> + + <% } else { %> + + <% } %> + + + + + <% + File productTitleFile = new File(getServletContext() + .getRealPath("extensions/product-title.jsp")); + if (productTitleFile.exists()) { + %> + + <% } else { %> + + <% } %> + + +
+ +

<%=AuthenticationEndpointUtil.i18n(resourceBundle, "auth.backup.code")%>

+ + <% if ("true".equals(authenticationFailed)) { %> +
<%=errorMessage%>
+ + <% } %> + + +
+
+

+
+ "> +
+ + +
+
+ " class="ui primary button"> +
+
+
+
+ + <% + String multiOptionURI=request.getParameter("multiOptionURI"); + if (multiOptionURI != null && AuthenticationEndpointUtil.isValidURL(multiOptionURI)) { %> + + <%=AuthenticationEndpointUtil.i18n(resourceBundle, "choose.other.option")%> + + <% } %> +
+
+ + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + +
+ + + <% + File footerFile = new File(getServletContext().getRealPath("extensions/footer.jsp")); + if (footerFile.exists()) { + %> + + <% } else { %> + + <% } %> + + diff --git a/apps/authentication-portal/src/main/webapp/basicauth.jsp b/apps/authentication-portal/src/main/webapp/basicauth.jsp index 452448827e2..0d578a7c0de 100644 --- a/apps/authentication-portal/src/main/webapp/basicauth.jsp +++ b/apps/authentication-portal/src/main/webapp/basicauth.jsp @@ -63,6 +63,10 @@ document.getElementById("restartFlowForm").submit(); } + function onCompleted() { + $('#loginForm').submit(); + } + // Handle form submission preventing double submission. $(document).ready(function(){ $.fn.preventDoubleSubmission = function() { @@ -74,7 +78,16 @@ console.warn("Prevented a possible double submit event"); } else { e.preventDefault(); - + <% + if (reCaptchaEnabled) { + %> + if (!grecaptcha.getResponse()) { + grecaptcha.execute(); + return; + } + <% + } + %> var userName = document.getElementById("username"); userName.value = userName.value.trim(); @@ -110,15 +123,6 @@ $('#loginForm').preventDoubleSubmission(); }); - function showResendReCaptcha() { - <% if (StringUtils.isNotBlank(request.getParameter("failedUsername"))){ %> - <% if (reCaptchaResendEnabled) { %> - window.location.href="resend-confirmation-captcha.jsp?<%=AuthenticationEndpointUtil.cleanErrorMessages(Encode.forJava(request.getQueryString()))%>"; - <% } else { %> - window.location.href="login.do?resend_username=<%=Encode.forHtml(URLEncoder.encode(request.getParameter("failedUsername"), UTF_8))%>&<%=AuthenticationEndpointUtil.cleanErrorMessages(Encode.forJava(request.getQueryString()))%>"; - <% } %> - <% } %> - } <%! @@ -257,18 +261,29 @@ <% if (Boolean.parseBoolean(loginFailed) && errorCode.equals(IdentityCoreConstants.USER_ACCOUNT_NOT_CONFIRMED_ERROR_CODE) && request.getParameter("resend_username") == null) { %>
- <%= AuthenticationEndpointUtil.i18n(resourceBundle, errorMessage) %> +
&<%=AuthenticationEndpointUtil.cleanErrorMessages(Encode.forJava(request.getQueryString()))%>" method="post" id="resendForm"> + <%= AuthenticationEndpointUtil.i18n(resourceBundle, errorMessage) %> - + - <%=AuthenticationEndpointUtil.i18n(resourceBundle, "no.confirmation.mail")%> + <%=AuthenticationEndpointUtil.i18n(resourceBundle, "no.confirmation.mail")%> - - <%=StringEscapeUtils.escapeHtml4(AuthenticationEndpointUtil.i18n(resourceBundle, "resend.mail"))%> - + +
<% } %> @@ -325,15 +340,16 @@ style="margin: 0 auto; right: 0; pointer-events: auto; cursor: pointer;"> + <% - if (reCaptchaEnabled) { + if (reCaptchaEnabled) { %> -
-
-
+
">
<% } @@ -479,13 +495,22 @@
-
-
+
+
+ +
+
<% if (isSelfSignUpEPAvailable && !isIdentifierFirstLogin(inputType) && isSelfSignUpEnabledInTenant) { %>
-
- -
<%! @@ -558,6 +572,10 @@ } }); + function onSubmitResend(token) { + $("#resendForm").submit(); + } + diff --git a/apps/authentication-portal/src/main/webapp/consent.jsp b/apps/authentication-portal/src/main/webapp/consent.jsp index c45f4a9444d..a6df0bbd6a2 100644 --- a/apps/authentication-portal/src/main/webapp/consent.jsp +++ b/apps/authentication-portal/src/main/webapp/consent.jsp @@ -20,6 +20,9 @@ <%@ page import="org.wso2.carbon.identity.application.authentication.endpoint.util.Constants" %> <%@ page import="java.io.File" %> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> + + <%@include file="includes/localize.jsp" %> <%@include file="includes/init-url.jsp" %> @@ -37,6 +40,11 @@ } %> +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "medium"); +%> + @@ -58,9 +66,8 @@ <% } %> -
-
- + + <% File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); @@ -70,8 +77,8 @@ <% } else { %> <% } %> - - + +

@@ -175,18 +182,19 @@

-
- - - - <% - File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); - if (productFooterFile.exists()) { - %> - - <% } else { %> - - <% } %> + + + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + + <% diff --git a/apps/authentication-portal/src/main/webapp/cookie_policy.jsp b/apps/authentication-portal/src/main/webapp/cookie_policy.jsp index 9c7b534013b..de51ef1b2c2 100644 --- a/apps/authentication-portal/src/main/webapp/cookie_policy.jsp +++ b/apps/authentication-portal/src/main/webapp/cookie_policy.jsp @@ -18,8 +18,16 @@ <%@ page import="java.io.File" %> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> <%@ include file="includes/localize.jsp" %> + + +<%-- Data for the layout from the page --%> +<% + layoutData.put("isPolicyPage", true); + layoutData.put("containerSize", "medium"); +%> @@ -35,47 +43,41 @@ <% } %> -
- - -
-
- - <% - File cookiePolicyFile = new File(getServletContext().getRealPath("extensions/cookie-policy-content.jsp")); - if (cookiePolicyFile.exists()) { - %> - - <% } else { %> - - <% } %> -
-
-
- - - <% - File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); - if (productFooterFile.exists()) { - %> - - <% } else { %> - - <% } %> + + + + <% + File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); + if (productTitleFile.exists()) { + %> + + <% } else { %> + + <% } %> + + + + <% + File cookiePolicyFile = new File(getServletContext().getRealPath("extensions/cookie-policy-content.jsp")); + if (cookiePolicyFile.exists()) { + %> + + <% } else { %> + + <% } %> + + + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + + <% diff --git a/apps/authentication-portal/src/main/webapp/device-success.jsp b/apps/authentication-portal/src/main/webapp/device-success.jsp index 9c3f255c887..da1e52a4549 100644 --- a/apps/authentication-portal/src/main/webapp/device-success.jsp +++ b/apps/authentication-portal/src/main/webapp/device-success.jsp @@ -19,8 +19,15 @@ <%@ page import="java.io.File" %> <%@ page import="org.owasp.encoder.Encode" %> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> <%@include file="includes/localize.jsp" %> + + +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "medium"); +%> @@ -36,9 +43,8 @@ <% } %> -
-
- + + <% File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); @@ -48,7 +54,8 @@ <% } else { %> <% } %> - + +
<%if(Encode.forHtmlAttribute(request.getParameter("app_name")) != null) { %> @@ -75,18 +82,19 @@ <% } %>
-
-
- - - <% - File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); - if (productFooterFile.exists()) { - %> - - <% } else { %> - - <% } %> + + + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + + <% diff --git a/apps/authentication-portal/src/main/webapp/domain.jsp b/apps/authentication-portal/src/main/webapp/domain.jsp index 83d368ed349..d4ac9ccb7f2 100644 --- a/apps/authentication-portal/src/main/webapp/domain.jsp +++ b/apps/authentication-portal/src/main/webapp/domain.jsp @@ -19,9 +19,11 @@ <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ page import="org.owasp.encoder.Encode" %> <%@ page import="java.io.File" %> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> <%@include file="includes/localize.jsp" %> <%@include file="includes/init-url.jsp" %> + <% String domainUnknown = AuthenticationEndpointUtil.i18n(resourceBundle, "domain.unknown"); @@ -39,6 +41,11 @@ } %> +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "large"); +%> + @@ -53,9 +60,8 @@ <% } %> -
-
- + + <% File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); @@ -65,19 +71,20 @@ <% } else { %> <% } %> - + +

<%=AuthenticationEndpointUtil.i18n(resourceBundle, "federated.login")%>

-
+ <% if (loginFailed) { %>
<%=Encode.forHtml(errorMessage)%>
<% } %> - +
- ">
@@ -90,18 +97,19 @@
-
- - - - <% - File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); - if (productFooterFile.exists()) { - %> - - <% } else { %> - - <% } %> + + + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + + <% diff --git a/apps/authentication-portal/src/main/webapp/dynamic_prompt.jsp b/apps/authentication-portal/src/main/webapp/dynamic_prompt.jsp index a8c1ca6e2ea..f08858c4bc3 100644 --- a/apps/authentication-portal/src/main/webapp/dynamic_prompt.jsp +++ b/apps/authentication-portal/src/main/webapp/dynamic_prompt.jsp @@ -25,10 +25,12 @@ <%@ page import="java.net.URLEncoder" %> <%@ page import="java.io.File" %> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> <%@include file="includes/localize.jsp" %> + <%@ taglib prefix="e" uri="https://www.owasp.org/index.php/OWASP_Java_Encoder_Project" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> @@ -53,6 +55,11 @@ String templatePath = templateMap.get(templateId); %> +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "medium"); +%> + @@ -72,9 +79,8 @@ -
-
- + + <% File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); @@ -84,7 +90,8 @@ <% } else { %> <% } %> - + +
<% if (templatePath != null) { @@ -106,18 +113,19 @@
<% } %>
- -
- - - <% - File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); - if (productFooterFile.exists()) { - %> - - <% } else { %> - - <% } %> + + + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + + <% diff --git a/apps/authentication-portal/src/main/webapp/emailAddressCapture.jsp b/apps/authentication-portal/src/main/webapp/emailAddressCapture.jsp index ffbd36938ef..9a120ff959b 100644 --- a/apps/authentication-portal/src/main/webapp/emailAddressCapture.jsp +++ b/apps/authentication-portal/src/main/webapp/emailAddressCapture.jsp @@ -24,6 +24,9 @@ <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <%@ include file="includes/localize.jsp" %> <%@ include file="includes/init-url.jsp" %> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> + + <% request.getSession().invalidate(); @@ -50,6 +53,11 @@ } %> +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "medium"); +%> + @@ -73,127 +81,129 @@ -
-
- - <% - File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); - if (productTitleFile.exists()) { - %> - - <% - } else { - %> - - <% - } - %> - -
- -

<%=AuthenticationEndpointUtil.i18n(resourceBundle, "enter.email")%> -

- + + + <% - if ("true".equals(authenticationFailed)) { + File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); + if (productTitleFile.exists()) { %> -
<%=Encode.forHtmlContent(errorMessage)%> -
- + <% - } + } else { %> + <% - if ("true".equals(authenticationFailed)) { + } %> -
<%=Encode.forHtmlContent(errorMessage)%> + + +
+ +

<%=AuthenticationEndpointUtil.i18n(resourceBundle, "enter.email")%> +

+ + <% + if ("true".equals(authenticationFailed)) { + %> +
<%=Encode.forHtmlContent(errorMessage)%> +
+ + <% + } + %> + <% + if ("true".equals(authenticationFailed)) { + %> +
<%=Encode.forHtmlContent(errorMessage)%> +
+ + <% + } + %> +
+
+
+ + <% + String loginFailed = request.getParameter("authFailure"); + if (loginFailed != null && "true".equals(loginFailed)) { + String authFailureMsg = request.getParameter("authFailureMsg"); + if (authFailureMsg != null && "login.fail.message".equals(authFailureMsg)) { + %> +
<%=AuthenticationEndpointUtil.i18n(resourceBundle, "error.retry")%> +
+ + <% + } + } + %> + +
+ +
+ + +
+ " + class="ui primary button"> +
+
+
- +
+ + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% + } else { + %> + <% } %> -
-
-
- - <% - String loginFailed = request.getParameter("authFailure"); - if (loginFailed != null && "true".equals(loginFailed)) { - String authFailureMsg = request.getParameter("authFailureMsg"); - if (authFailureMsg != null && "login.fail.message".equals(authFailureMsg)) { - %> -
<%=AuthenticationEndpointUtil.i18n(resourceBundle, "error.retry")%> -
- - <% - } - } - %> - -
- -
- - -
- " - class="ui primary button"> -
-
-
-
-
-
- - -<% - File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); - if (productFooterFile.exists()) { -%> - -<% -} else { -%> - -<% - } -%> + + - -<% - File footerFile = new File(getServletContext().getRealPath("extensions/footer.jsp")); - if (footerFile.exists()) { -%> - -<% -} else { -%> - -<% - } -%> + + <% + File footerFile = new File(getServletContext().getRealPath("extensions/footer.jsp")); + if (footerFile.exists()) { + %> + + <% + } else { + %> + + <% + } + %> - + diff --git a/apps/authentication-portal/src/main/webapp/emailOtp.jsp b/apps/authentication-portal/src/main/webapp/emailOtp.jsp index 60048737396..87ff08a1de5 100644 --- a/apps/authentication-portal/src/main/webapp/emailOtp.jsp +++ b/apps/authentication-portal/src/main/webapp/emailOtp.jsp @@ -24,6 +24,10 @@ <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <%@ include file="includes/localize.jsp" %> <%@ include file="includes/init-url.jsp" %> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> + + + <% request.getSession().invalidate(); String queryString = request.getQueryString(); @@ -47,6 +51,12 @@ } } %> + +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "medium"); +%> + @@ -73,144 +83,146 @@ <% } %> -
-
- - <% - File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); - if (productTitleFile.exists()) { - %> - - <% } else { %> - - <% } %> - -
- -

<%=AuthenticationEndpointUtil.i18n(resourceBundle, "otp.verification")%> -

- + + + <% - if ("true".equals(authenticationFailed)) { + File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); + if (productTitleFile.exists()) { %> -
<%=Encode.forHtmlContent(errorMessage)%> -
- + + <% } else { %> + <% } %> -
-
-
- <% - String loginFailed = request.getParameter("authFailure"); - if (loginFailed != null && "true".equals(loginFailed)) { - String authFailureMsg = request.getParameter("authFailureMsg"); - if (authFailureMsg != null && "login.fail.message".equals(authFailureMsg)) { - %> -
<%=AuthenticationEndpointUtil.i18n(resourceBundle, "error.retry")%> -
- - <% } - } %> - <% if (request.getParameter("screenValue") != null) { %> -
- -
- - + + +
+ +

<%=AuthenticationEndpointUtil.i18n(resourceBundle, "otp.verification")%> +

+ + <% + if ("true".equals(authenticationFailed)) { + %> +
<%=Encode.forHtmlContent(errorMessage)%> +
+ + <% } %> +
+
+ + <% + String loginFailed = request.getParameter("authFailure"); + if (loginFailed != null && "true".equals(loginFailed)) { + String authFailureMsg = request.getParameter("authFailureMsg"); + if (authFailureMsg != null && "login.fail.message".equals(authFailureMsg)) { + %> +
<%=AuthenticationEndpointUtil.i18n(resourceBundle, "error.retry")%>
- <% } else { %> + + <% } + } %> + <% if (request.getParameter("screenValue") != null) { %>
+ (<%=Encode.forHtmlContent(request.getParameter("screenValue"))%>) +
- +
- <% } %> -
- - - - -
- <% - if ("true".equals(authenticationFailed)) { - %> - <%=AuthenticationEndpointUtil.i18n(resourceBundle, "resend.code")%> - - <% } %> - " - class="ui primary button"/> -
- + <% } else { %> +
+ +
+ + +
+ <% } %> +
+ + + + +
+ <% + if ("true".equals(authenticationFailed)) { + %> + <%=AuthenticationEndpointUtil.i18n(resourceBundle, "resend.code")%> + + <% } %> + " + class="ui primary button"/> +
+ +
-
-
-
+ + + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + + - -<% - File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); - if (productFooterFile.exists()) { -%> - -<% } else { %> - -<% } %> + + <% + File footerFile = new File(getServletContext().getRealPath("extensions/footer.jsp")); + if (footerFile.exists()) { + %> + + <% } else { %> + + <% } %> - -<% - File footerFile = new File(getServletContext().getRealPath("extensions/footer.jsp")); - if (footerFile.exists()) { -%> - -<% } else { %> - -<% } %> - - + diff --git a/apps/authentication-portal/src/main/webapp/emailOtpError.jsp b/apps/authentication-portal/src/main/webapp/emailOtpError.jsp index 8d7699dead6..537fafc5c68 100644 --- a/apps/authentication-portal/src/main/webapp/emailOtpError.jsp +++ b/apps/authentication-portal/src/main/webapp/emailOtpError.jsp @@ -23,6 +23,10 @@ <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <%@ include file="includes/localize.jsp" %> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> + + + <% request.getSession().invalidate(); String queryString = request.getQueryString(); @@ -61,6 +65,11 @@ } %> +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "medium"); +%> + @@ -80,51 +89,53 @@ -
-
- - <% - File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); - if (productTitleFile.exists()) { - %> - - <% } else { %> - - <% } %> - -
- -

<%=AuthenticationEndpointUtil.i18n(resourceBundle, "error.emailOTP.title")%> -

- + + + <% - if ("true".equals(authenticationFailed)) { + File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); + if (productTitleFile.exists()) { %> -
<%=Encode.forHtmlContent(errorMessage)%> + + <% } else { %> + + <% } %> + + +
+ +

<%=AuthenticationEndpointUtil.i18n(resourceBundle, "error.emailOTP.title")%> +

+ + <% + if ("true".equals(authenticationFailed)) { + %> +
<%=Encode.forHtmlContent(errorMessage)%> +
+ <% } %>
+
+ + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + <% } %> -
-
-
+ + - -<% - File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); - if (productFooterFile.exists()) { -%> - -<% } else { %> - -<% } %> - - -<% - File footerFile = new File(getServletContext().getRealPath("extensions/footer.jsp")); - if (footerFile.exists()) { -%> - -<% } else { %> - -<% } %> + + <% + File footerFile = new File(getServletContext().getRealPath("extensions/footer.jsp")); + if (footerFile.exists()) { + %> + + <% } else { %> + + <% } %> diff --git a/apps/authentication-portal/src/main/webapp/enableTOTP.jsp b/apps/authentication-portal/src/main/webapp/enableTOTP.jsp index 58d3c6eb769..d83ca53d5f9 100644 --- a/apps/authentication-portal/src/main/webapp/enableTOTP.jsp +++ b/apps/authentication-portal/src/main/webapp/enableTOTP.jsp @@ -25,165 +25,175 @@ <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <%@ include file="includes/localize.jsp" %> <%@ include file="includes/init-url.jsp" %> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> - <% - request.getSession().invalidate(); - String queryString = request.getQueryString(); - Map idpAuthenticatorMapping = null; - if (request.getAttribute(Constants.IDP_AUTHENTICATOR_MAP) != null) { - idpAuthenticatorMapping = (Map) request.getAttribute(Constants.IDP_AUTHENTICATOR_MAP); - } + - String errorMessage = AuthenticationEndpointUtil.i18n(resourceBundle,"error.retry"); - String authenticationFailed = "false"; +<% + request.getSession().invalidate(); + String queryString = request.getQueryString(); + Map idpAuthenticatorMapping = null; + if (request.getAttribute(Constants.IDP_AUTHENTICATOR_MAP) != null) { + idpAuthenticatorMapping = (Map) request.getAttribute(Constants.IDP_AUTHENTICATOR_MAP); + } - if (Boolean.parseBoolean(request.getParameter(Constants.AUTH_FAILURE))) { - authenticationFailed = "true"; + String errorMessage = AuthenticationEndpointUtil.i18n(resourceBundle,"error.retry"); + String authenticationFailed = "false"; - if (request.getParameter(Constants.AUTH_FAILURE_MSG) != null) { - errorMessage = Encode.forHtmlAttribute(request.getParameter(Constants.AUTH_FAILURE_MSG)); + if (Boolean.parseBoolean(request.getParameter(Constants.AUTH_FAILURE))) { + authenticationFailed = "true"; - if (errorMessage.equalsIgnoreCase("authentication.fail.message")) { - errorMessage = AuthenticationEndpointUtil.i18n(resourceBundle,"error.retry"); - } + if (request.getParameter(Constants.AUTH_FAILURE_MSG) != null) { + errorMessage = Encode.forHtmlAttribute(request.getParameter(Constants.AUTH_FAILURE_MSG)); + + if (errorMessage.equalsIgnoreCase("authentication.fail.message")) { + errorMessage = AuthenticationEndpointUtil.i18n(resourceBundle,"error.retry"); } } - %> - - - - - <% - File headerFile = new File(getServletContext().getRealPath("extensions/header.jsp")); - if (headerFile.exists()) { - %> - - <% } else { %> - - <% } %> - - - - - - -
-
- + } +%> + +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "medium"); +%> + + + + + <% + File headerFile = new File(getServletContext().getRealPath("extensions/header.jsp")); + if (headerFile.exists()) { + %> + + <% } else { %> + + <% } %> + + + + + + + + + + <% + File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); + if (productTitleFile.exists()) { + %> + + <% } else { %> + + <% } %> + + +
+ +

<%=AuthenticationEndpointUtil.i18n(resourceBundle, "enable.totp")%>

+ <% - File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); - if (productTitleFile.exists()) { + if ("true".equals(authenticationFailed)) { %> - - <% } else { %> - +
<%=errorMessage%>
+ <% } %> - -
- -

<%=AuthenticationEndpointUtil.i18n(resourceBundle, "enable.totp")%>

- - <% - if ("true".equals(authenticationFailed)) { - %> -
<%=errorMessage%>
- - <% } %> -
-
- <% - String loginFailed = request.getParameter("authFailure"); - if (loginFailed != null && "true".equals(loginFailed)) { - String authFailureMsg = request.getParameter("authFailureMsg"); - if (authFailureMsg != null && "login.fail.message".equals(authFailureMsg)) { - %> -
<%=AuthenticationEndpointUtil.i18n(resourceBundle, "error.retry")%>
- - <% } } %> - -

<%=AuthenticationEndpointUtil.i18n(resourceBundle, "error.totp.not.enabled.please.enable")%>

- - - - - -
- - - - -
- -
- " class="ui button secondary"> - " class="ui primary button"> -
- -
+
+
+ <% + String loginFailed = request.getParameter("authFailure"); + if (loginFailed != null && "true".equals(loginFailed)) { + String authFailureMsg = request.getParameter("authFailureMsg"); + if (authFailureMsg != null && "login.fail.message".equals(authFailureMsg)) { + %> +
<%=AuthenticationEndpointUtil.i18n(resourceBundle, "error.retry")%>
+ + <% } } %> + +

<%=AuthenticationEndpointUtil.i18n(resourceBundle, "error.totp.not.enabled.please.enable")%>

+ + + + + +
+ + + + +
+ +
+ " class="ui button secondary"> + " class="ui primary button"> +
+
-
- - - - - - - <% - File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); - if (productFooterFile.exists()) { - %> - - <% } else { %> - - <% } %> + + + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + + <% @@ -494,25 +542,6 @@ $(this).hide(); $('.main-link').next().hide(); }); - - <% - if(reCaptchaEnabled) { - %> - var error_msg = $("#error-msg"); - - $("#loginForm").submit(function (e) { - var resp = $("[name='g-recaptcha-response']")[0].value; - if (resp.trim() == '') { - error_msg.text("<%=AuthenticationEndpointUtil.i18n(resourceBundle,"please.select.recaptcha")%>"); - error_msg.show(); - $("html, body").animate({scrollTop: error_msg.offset().top}, 'slow'); - return false; - } - return true; - }); - <% - } - %> }); function myFunction(key, value, name) { diff --git a/apps/authentication-portal/src/main/webapp/logout.jsp b/apps/authentication-portal/src/main/webapp/logout.jsp index f2580f69410..fd119f6778f 100644 --- a/apps/authentication-portal/src/main/webapp/logout.jsp +++ b/apps/authentication-portal/src/main/webapp/logout.jsp @@ -16,10 +16,17 @@ ~ under the License. --%> -<%@ page import="java.io.File" %> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> +<%@ page import="java.io.File" %> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> <%@ include file="includes/localize.jsp" %> + + +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "medium"); +%> @@ -35,9 +42,8 @@ <% } %> -
-
- + + <% File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); @@ -47,22 +53,24 @@ <% } else { %> <% } %> - + +

<%=AuthenticationEndpointUtil.i18n(resourceBundle, "logged.out")%>

-
-
- - - <% - File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); - if (productFooterFile.exists()) { - %> - - <% } else { %> - - <% } %> + + + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + + <% diff --git a/apps/authentication-portal/src/main/webapp/long-wait.jsp b/apps/authentication-portal/src/main/webapp/long-wait.jsp index 05c85bd4809..b88dd6bf9d0 100644 --- a/apps/authentication-portal/src/main/webapp/long-wait.jsp +++ b/apps/authentication-portal/src/main/webapp/long-wait.jsp @@ -20,14 +20,21 @@ <%@ page import="java.io.File" %> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> <%@ page import="org.owasp.encoder.Encode" %> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> <%@include file="includes/localize.jsp" %> + <% String sessionDataKey = request.getParameter("sessionDataKey"); %> +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "medium"); +%> + @@ -44,9 +51,8 @@ -
-
- + + <% File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); @@ -56,25 +62,27 @@ <% } else { %> <% } %> - + +
-
-
- - - <% - File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); - if (productFooterFile.exists()) { - %> - - <% } else { %> - - <% } %> + + + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + + <% diff --git a/apps/authentication-portal/src/main/webapp/magic_link_notification.jsp b/apps/authentication-portal/src/main/webapp/magic_link_notification.jsp new file mode 100644 index 00000000000..eec8d306787 --- /dev/null +++ b/apps/authentication-portal/src/main/webapp/magic_link_notification.jsp @@ -0,0 +1,100 @@ +<%-- + ~ Copyright (c) 2022, WSO2 Inc. (http://www.wso2.com) All Rights Reserved. + ~ + ~ WSO2 Inc. licenses this file to you under the Apache License, + ~ Version 2.0 (the "License"); you may not use this file except + ~ in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, + ~ software distributed under the License is distributed on an + ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + ~ KIND, either express or implied. See the License for the + ~ specific language governing permissions and limitations + ~ under the License. + --%> + +<%@ page import="org.apache.commons.collections.map.HashedMap" %> +<%@ page import="org.apache.commons.lang.StringUtils" %> +<%@ page import="org.owasp.encoder.Encode" %> +<%@ page import="org.wso2.carbon.identity.mgt.endpoint.util.IdentityManagementEndpointUtil" %> +<%@ page import="org.wso2.carbon.identity.mgt.endpoint.util.client.model.Property" %> +<%@ page import="org.wso2.carbon.identity.core.util.IdentityTenantUtil" %> +<%@ page import="java.io.File" %> +<%@ page import="java.net.URISyntaxException" %> +<%@ page import="java.net.URLEncoder" %> +<%@ page import="java.util.ArrayList" %> +<%@ page import="java.util.List" %> +<%@ page import="java.util.Map" %> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> + + + + +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "medium"); +%> + + + + + <% + File headerFile = new File(getServletContext().getRealPath("extensions/header.jsp")); + if (headerFile.exists()) { + %> + + <% } else { %> + + <% } %> + + + + + + + + + + + + + + + + + + <% + File footerFile = new File(getServletContext().getRealPath("extensions/footer.jsp")); + if (footerFile.exists()) { + %> + + <% } else { %> + + <% } %> + + + + diff --git a/apps/sms-otp-authentication-portal/src/main/webapp/mobile.jsp b/apps/authentication-portal/src/main/webapp/mobile.jsp similarity index 85% rename from apps/sms-otp-authentication-portal/src/main/webapp/mobile.jsp rename to apps/authentication-portal/src/main/webapp/mobile.jsp index dc5d1ca5cfd..73cd9c2a8e8 100644 --- a/apps/sms-otp-authentication-portal/src/main/webapp/mobile.jsp +++ b/apps/authentication-portal/src/main/webapp/mobile.jsp @@ -27,6 +27,9 @@ <%@ page import="static java.util.Base64.getDecoder" %> <%@ page import="org.wso2.carbon.identity.authenticator.smsotp.SMSOTPConstants" %> <%@ page import="org.apache.commons.lang.StringUtils" %> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> + + <% request.getSession().invalidate(); @@ -46,7 +49,7 @@ mobileRegexPolicyValidationErrorMessage = new String(getDecoder().decode(request.getParameter(SMSOTPConstants.MOBILE_NUMBER_PATTERN_POLICY_FAILURE_ERROR_MESSAGE))); } - String errorMessage = IdentityManagementEndpointUtil.i18n(recoveryResourceBundle,"error.retry"); + String errorMessage = IdentityManagementEndpointUtil.i18n(resourceBundle,"error.retry"); boolean authenticationFailed = false; if (Boolean.parseBoolean(request.getParameter(Constants.AUTH_FAILURE))) { @@ -66,6 +69,11 @@ } %> +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "medium"); +%> + @@ -84,8 +92,8 @@ -
-
+ + <% File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); @@ -95,10 +103,11 @@ <% } else { %> <% } %> - + +
-

<%=IdentityManagementEndpointUtil.i18n(recoveryResourceBundle, "enter.phone.number")%>

+

<%=IdentityManagementEndpointUtil.i18n(resourceBundle, "enter.phone.number")%>

<% if (authenticationFailed) { @@ -116,7 +125,7 @@ String authFailureMsg = request.getParameter("authFailureMsg"); if (authFailureMsg != null && "login.fail.message".equals(authFailureMsg)) { %> -
<%=IdentityManagementEndpointUtil.i18n(recoveryResourceBundle, "error.retry")%>
+
<%=IdentityManagementEndpointUtil.i18n(resourceBundle, "error.retry")%>
<% } } %>
@@ -128,24 +137,25 @@ value='<%=Encode.forHtmlAttribute(request.getParameter("sessionDataKey"))%>'/>
- " + " class="ui primary button"/>
-
-
- - - <% - File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); - if (productFooterFile.exists()) { - %> - - <% } else { %> - - <% } %> + + + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + + <% diff --git a/apps/authentication-portal/src/main/webapp/oauth2_authz.jsp b/apps/authentication-portal/src/main/webapp/oauth2_authz.jsp index 3e39498ad83..fb14597be20 100644 --- a/apps/authentication-portal/src/main/webapp/oauth2_authz.jsp +++ b/apps/authentication-portal/src/main/webapp/oauth2_authz.jsp @@ -31,9 +31,11 @@ <%@ page import="java.util.stream.Stream" %> <%@ page import="java.io.File" %> <%@ page import="java.util.Set" %> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> <%@include file="includes/localize.jsp" %> <%@include file="includes/init-url.jsp" %> + <% String app = request.getParameter("application"); @@ -41,6 +43,11 @@ boolean displayScopes = Boolean.parseBoolean(getServletContext().getInitParameter("displayScopes")); %> +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "medium"); +%> + @@ -62,9 +69,8 @@ <% } %> -
-
- + + <% File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); @@ -74,7 +80,8 @@ <% } else { %> <% } %> - + +
@@ -183,8 +190,19 @@
- -
+ + + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + + - - <% - File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); - if (productFooterFile.exists()) { - %> - - <% } else { %> - - <% } %> - <% File footerFile = new File(getServletContext().getRealPath("extensions/footer.jsp")); diff --git a/apps/authentication-portal/src/main/webapp/oauth2_consent.jsp b/apps/authentication-portal/src/main/webapp/oauth2_consent.jsp index 4617baafd6f..7502fb186d1 100644 --- a/apps/authentication-portal/src/main/webapp/oauth2_consent.jsp +++ b/apps/authentication-portal/src/main/webapp/oauth2_consent.jsp @@ -16,6 +16,7 @@ ~ under the License. --%> +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ page import="org.apache.commons.collections.CollectionUtils" %> <%@ page import="org.apache.commons.lang.ArrayUtils" %> <%@ page import="org.apache.commons.lang.StringUtils" %> @@ -35,11 +36,11 @@ <%@ page import="java.util.stream.Stream" %> <%@ page import="java.util.Set" %> <%@ page import="java.util.StringTokenizer" %> - -<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> <%@ include file="includes/localize.jsp" %> + <% String app = request.getParameter("application"); @@ -83,6 +84,11 @@ } %> +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "medium"); +%> + @@ -104,9 +110,8 @@ <% } %> -
-
- + + <% File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); @@ -130,7 +135,8 @@ <% } %> - + +

<%=Encode.forHtml(request.getParameter("application"))%> @@ -338,18 +344,19 @@

- -
- - - <% - File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); - if (productFooterFile.exists()) { - %> - - <% } else { %> - - <% } %> + + + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + + <% diff --git a/apps/authentication-portal/src/main/webapp/oauth2_error.jsp b/apps/authentication-portal/src/main/webapp/oauth2_error.jsp index 81e16c405c2..412c72b7e26 100644 --- a/apps/authentication-portal/src/main/webapp/oauth2_error.jsp +++ b/apps/authentication-portal/src/main/webapp/oauth2_error.jsp @@ -19,9 +19,11 @@ <%@ page import="org.owasp.encoder.Encode" %> <%@ page import="java.io.File" %> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> <%@ include file="includes/localize.jsp" %> + <% String errorCode = request.getParameter("oauthErrorCode"); @@ -36,6 +38,11 @@ } %> +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "medium"); +%> + @@ -50,9 +57,8 @@ <% } %> -
-
- + + <% File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); @@ -62,7 +68,8 @@ <% } else { %> <% } %> - + +
<% @@ -79,18 +86,19 @@ <% } %>
-
-
- - - <% - File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); - if (productFooterFile.exists()) { - %> - - <% } else { %> - - <% } %> + + + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + + <% diff --git a/apps/authentication-portal/src/main/webapp/oauth2_logout_consent.jsp b/apps/authentication-portal/src/main/webapp/oauth2_logout_consent.jsp index 8f2adfd648d..780fc4bbfcf 100644 --- a/apps/authentication-portal/src/main/webapp/oauth2_logout_consent.jsp +++ b/apps/authentication-portal/src/main/webapp/oauth2_logout_consent.jsp @@ -18,9 +18,16 @@ <%@ page import="java.io.File" %> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> <%@ include file="includes/localize.jsp" %> + + +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "medium"); +%> @@ -36,9 +43,8 @@ <% } %> -
-
- + + <% File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); @@ -48,7 +54,8 @@ <% } else { %> <% } %> - + +

<%=AuthenticationEndpointUtil.i18n(resourceBundle, "do.you.want.to.logout")%>

@@ -72,18 +79,19 @@
-
-
- - - <% - File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); - if (productFooterFile.exists()) { - %> - - <% } else { %> - - <% } %> + + + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + + <% diff --git a/apps/authentication-portal/src/main/webapp/openid_profile.jsp b/apps/authentication-portal/src/main/webapp/openid_profile.jsp index 080e48b81cb..a97698e5f4f 100644 --- a/apps/authentication-portal/src/main/webapp/openid_profile.jsp +++ b/apps/authentication-portal/src/main/webapp/openid_profile.jsp @@ -18,9 +18,12 @@ <%@ page import="org.owasp.encoder.Encode" %> <%@ page import="java.io.File" %> +<%@ page import="org.wso2.carbon.identity.application.authentication.endpoint.util.AuthenticationEndpointUtil" %> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> <%@include file="includes/localize.jsp" %> <%@include file="includes/init-url.jsp" %> + <% String[] profiles = request.getParameterValues("profile"); @@ -33,6 +36,11 @@ } %> +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "large"); +%> + @@ -47,9 +55,8 @@ <% } %> -
-
- + + <% File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); @@ -59,7 +66,8 @@ <% } else { %> <% } %> - + +

<%=AuthenticationEndpointUtil.i18n(resourceBundle, "openid.user.claims")%> @@ -100,24 +108,31 @@ "approve.always")%>"/> - " - onclick="javascript:document.location.href='<%=Encode.forJavaScript(openidreturnto)%>'"/> + <% + if (AuthenticationEndpointUtil.isValidURL(openidreturnto)) { + %> + " + onclick="javascript:document.location.href='<%=Encode.forJavaScript(openidreturnto)%>'"/> + <% + } + %>

- -
- - - <% - File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); - if (productFooterFile.exists()) { - %> - - <% } else { %> - - <% } %> + + + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + + <% diff --git a/apps/authentication-portal/src/main/webapp/org_name.jsp b/apps/authentication-portal/src/main/webapp/org_name.jsp new file mode 100644 index 00000000000..c2271ede513 --- /dev/null +++ b/apps/authentication-portal/src/main/webapp/org_name.jsp @@ -0,0 +1,160 @@ +<%-- + ~ Copyright (c) 2022, WSO2 Inc. (http://www.wso2.com). + ~ + ~ WSO2 Inc. licenses this file to you under the Apache License, + ~ Version 2.0 (the "License"); you may not use this file except + ~ in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, + ~ software distributed under the License is distributed on an + ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + ~ KIND, either express or implied. See the License for the + ~ specific language governing permissions and limitations + ~ under the License. + --%> + +<%@ page import="org.owasp.encoder.Encode" %> +<%@ page import="org.wso2.carbon.identity.application.authentication.endpoint.util.Constants" %> +<%@ page import="java.io.File" %> +<%@ page import="java.util.Map" %> +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> +<%@ include file="includes/localize.jsp" %> +<%@ include file="includes/init-url.jsp" %> + + +<% + String idp = request.getParameter("idp"); + String authenticator = request.getParameter("authenticator"); + String sessionDataKey = request.getParameter(Constants.SESSION_DATA_KEY); + + String errorMessage = AuthenticationEndpointUtil.i18n(resourceBundle, "error.retry"); + String authenticationFailed = "false"; + + if (Boolean.parseBoolean(request.getParameter(Constants.AUTH_FAILURE))) { + authenticationFailed = "true"; + + if (request.getParameter(Constants.AUTH_FAILURE_MSG) != null) { + errorMessage = request.getParameter(Constants.AUTH_FAILURE_MSG); + + if (errorMessage.equalsIgnoreCase("authentication.fail.message")) { + errorMessage = AuthenticationEndpointUtil.i18n(resourceBundle, "error.retry"); + } + } + } +%> + +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "medium"); +%> + + + + + <% + File headerFile = new File(getServletContext().getRealPath("extensions/header.jsp")); + if (headerFile.exists()) { + %> + + <% + } else { + %> + + <% + } + %> + + + + + + + + + <% + File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); + if (productTitleFile.exists()) { + %> + + <% + } else { + %> + + <% + } + %> + + +
+ +

Sign In with <%= StringUtils.isNotBlank(idp) ? idp : "Organization Login" %>

+ + + <% + if ("true".equals(authenticationFailed)) { + %> +
<%=Encode.forHtmlContent(errorMessage)%>
+ + <% + } + %> + +
+ + +
+

Name of the Organization:

+ + + + + +
+ +
+
+ +
+
+ + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% + } else { + %> + + <% + } + %> + +
+ + + <% + File footerFile = new File(getServletContext().getRealPath("extensions/footer.jsp")); + if (footerFile.exists()) { + %> + + <% + } else { + %> + + <% + } + %> + + diff --git a/apps/authentication-portal/src/main/webapp/privacy_policy.jsp b/apps/authentication-portal/src/main/webapp/privacy_policy.jsp index e57468e877e..c7f69cc6348 100644 --- a/apps/authentication-portal/src/main/webapp/privacy_policy.jsp +++ b/apps/authentication-portal/src/main/webapp/privacy_policy.jsp @@ -18,8 +18,15 @@ <%@ page import="java.io.File" %> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> <%@ include file="includes/localize.jsp" %> + + +<%-- Data for the layout from the page --%> +<% + layoutData.put("isPolicyPage", true); +%> @@ -35,47 +42,41 @@ <% } %> -
- - -
-
- - <% - File privacyPolicyFile = new File(getServletContext().getRealPath("extensions/privacy-policy-content.jsp")); - if (privacyPolicyFile.exists()) { - %> - - <% } else { %> - - <% } %> -
-
-
- - - <% - File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); - if (productFooterFile.exists()) { - %> - - <% } else { %> - - <% } %> + + + + <% + File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); + if (productTitleFile.exists()) { + %> + + <% } else { %> + + <% } %> + + + + <% + File privacyPolicyFile = new File(getServletContext().getRealPath("extensions/privacy-policy-content.jsp")); + if (privacyPolicyFile.exists()) { + %> + + <% } else { %> + + <% } %> + + + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + + <% diff --git a/apps/authentication-portal/src/main/webapp/requested-claims.jsp b/apps/authentication-portal/src/main/webapp/requested-claims.jsp index af2db043a72..354b868b57e 100644 --- a/apps/authentication-portal/src/main/webapp/requested-claims.jsp +++ b/apps/authentication-portal/src/main/webapp/requested-claims.jsp @@ -20,9 +20,11 @@ <%@ page import="org.owasp.encoder.Encode" %> <%@ page import="java.io.File" %> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> <%@ include file="includes/localize.jsp" %> <%@ include file="includes/init-url.jsp" %> + <% String[] missingClaimList = null; @@ -36,6 +38,11 @@ } %> +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "medium"); +%> + @@ -61,9 +68,8 @@ <% } %> -
-
- + + <% File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); @@ -73,7 +79,8 @@ <% } else { %> <% } %> - + +

<%=AuthenticationEndpointUtil.i18n(resourceBundle, "provide.mandatory.details")%> @@ -165,18 +172,19 @@

- -
- - - <% - File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); - if (productFooterFile.exists()) { - %> - - <% } else { %> - - <% } %> + + + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + + <% diff --git a/apps/authentication-portal/src/main/webapp/resend-confirmation-captcha.jsp b/apps/authentication-portal/src/main/webapp/resend-confirmation-captcha.jsp index 62e8d6cff14..7effaef8c6f 100644 --- a/apps/authentication-portal/src/main/webapp/resend-confirmation-captcha.jsp +++ b/apps/authentication-portal/src/main/webapp/resend-confirmation-captcha.jsp @@ -26,9 +26,11 @@ <%@ page import="java.util.ArrayList" %> <%@ page import="java.util.Arrays" %> <%@ page import="org.wso2.carbon.identity.captcha.util.CaptchaUtil" %> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> <%@ include file="includes/localize.jsp" %> + <% String UTF_8 = "UTF-8"; @@ -38,6 +40,11 @@ } %> +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "medium"); +%> + @@ -62,9 +69,8 @@ -
-
- + + <% File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); @@ -74,7 +80,8 @@ <% } else { %> <% } %> - + +

@@ -116,18 +123,19 @@

- -
- - - <% - File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); - if (productFooterFile.exists()) { - %> - - <% } else { %> - - <% } %> + + + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + + <% @@ -146,6 +154,18 @@ $(document).ready(function () { <% if (reCaptchaResendEnabled) { %> + var errorMessage = $("#error-msg"); + errorMessage.hide(); + + $( "#recoverySubmit" ).click(function() { + var reCaptchaResponse = $("[name='g-recaptcha-response']")[0].value; + + if (reCaptchaResponse.trim() == '') { + errorMessage.text("Please select reCaptcha."); + errorMessage.show(); + return false; + } + }); $("#resend-captcha-container").show(); <% } else { %> $("#resendForm").submit(); diff --git a/apps/authentication-portal/src/main/webapp/retry.jsp b/apps/authentication-portal/src/main/webapp/retry.jsp index bd635bb412d..7f16065c48d 100644 --- a/apps/authentication-portal/src/main/webapp/retry.jsp +++ b/apps/authentication-portal/src/main/webapp/retry.jsp @@ -28,9 +28,11 @@ <%@ page import="java.util.Map" %> <%@ page import="org.wso2.carbon.identity.mgt.endpoint.util.IdentityManagementEndpointUtil" %> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> <%@ include file="includes/localize.jsp" %> <%@include file="includes/init-url.jsp" %> + <%! private static final String SERVER_AUTH_URL = "/api/identity/auth/v1.1/"; @@ -100,6 +102,11 @@ } %> +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "medium"); +%> + @@ -114,9 +121,8 @@ <% } %> -
-
- + + <% File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); @@ -126,7 +132,8 @@ <% } else { %> <% } %> - + +
@@ -138,18 +145,19 @@
-
-
- - - <% - File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); - if (productFooterFile.exists()) { - %> - - <% } else { %> - - <% } %> + + + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + + <% diff --git a/apps/authentication-portal/src/main/webapp/samlsso_notification.jsp b/apps/authentication-portal/src/main/webapp/samlsso_notification.jsp index ddb77d966ed..dbe2457cbe5 100644 --- a/apps/authentication-portal/src/main/webapp/samlsso_notification.jsp +++ b/apps/authentication-portal/src/main/webapp/samlsso_notification.jsp @@ -20,8 +20,10 @@ <%@ page import="java.io.File" %> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> <%@include file="includes/localize.jsp" %> + <%! private static final String INVALID_MESSAGE_MESSAGE = "The message was not recognized by the SAML 2.0 SSO Provider. Please check the logs for more details"; @@ -55,6 +57,11 @@ session.invalidate(); %> +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "large"); +%> + @@ -69,9 +76,8 @@ <% } %> -
-
- + + <% File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); @@ -81,24 +87,32 @@ <% } else { %> <% } %> - + +

<%=AuthenticationEndpointUtil.i18n(resourceBundle, "saml.sso")%>

<%=AuthenticationEndpointUtil.i18n(resourceBundle, errorStat)%>

<%=AuthenticationEndpointUtil.i18n(resourceBundle, errorMsg)%>

+ <% if (new File(getServletContext().getRealPath("includes/error-tracking-reference.jsp")).exists()) { %> + + + + + <% } %>
-
-
- - - <% - File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); - if (productFooterFile.exists()) { - %> - - <% } else { %> - - <% } %> + + + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + + <% diff --git a/apps/authentication-portal/src/main/webapp/samlsso_redirect.jsp b/apps/authentication-portal/src/main/webapp/samlsso_redirect.jsp index 8e53327230c..974c8bb4278 100644 --- a/apps/authentication-portal/src/main/webapp/samlsso_redirect.jsp +++ b/apps/authentication-portal/src/main/webapp/samlsso_redirect.jsp @@ -22,8 +22,10 @@ <%@ page import="java.net.URLDecoder" %> <%@ page import="org.owasp.encoder.Encode" %> <%@ page import="java.io.File" %> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> <%@include file="includes/localize.jsp" %> + <% String assertionConsumerURL = (String) request.getAttribute(Constants.SAML2SSO.ASSERTION_CONSUMER_URL); @@ -37,6 +39,11 @@ } %> +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "large"); +%> + @@ -51,9 +58,8 @@ <% } %> -
-
- + + <% File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); @@ -63,7 +69,8 @@ <% } else { %> <% } %> - + +

<%=AuthenticationEndpointUtil.i18n(resourceBundle, "you.are.redirected.back.to")%> <%=Encode.forHtmlContent(assertionConsumerURL)%>. @@ -77,18 +84,19 @@

- -
- - - <% - File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); - if (productFooterFile.exists()) { - %> - - <% } else { %> - - <% } %> + + + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + + <% diff --git a/apps/authentication-portal/src/main/webapp/select_org.jsp b/apps/authentication-portal/src/main/webapp/select_org.jsp new file mode 100644 index 00000000000..d4c041e33b1 --- /dev/null +++ b/apps/authentication-portal/src/main/webapp/select_org.jsp @@ -0,0 +1,167 @@ +<%-- + ~ Copyright (c) 2022, WSO2 LLC. (http://www.wso2.com). + ~ + ~ WSO2 LLC. licenses this file to you under the Apache License, + ~ Version 2.0 (the "License"); you may not use this file except + ~ in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, + ~ software distributed under the License is distributed on an + ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + ~ KIND, either express or implied. See the License for the + ~ specific language governing permissions and limitations + ~ under the License. + --%> + +<%@ page import="org.owasp.encoder.Encode" %> +<%@ page import="org.wso2.carbon.identity.application.authentication.endpoint.util.Constants" %> +<%@ page import="java.io.File" %> +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> +<%@ include file="includes/localize.jsp" %> +<%@ include file="includes/init-url.jsp" %> + + +<% + + String idp = request.getParameter("idp"); + String authenticator = request.getParameter("authenticator"); + String sessionDataKey = request.getParameter(Constants.SESSION_DATA_KEY); + int orgCount = Integer.parseInt(request.getParameter("orgCount")); + + String errorMessage = AuthenticationEndpointUtil.i18n(resourceBundle, "error.retry"); + String authenticationFailed = "false"; + + if (Boolean.parseBoolean(request.getParameter(Constants.AUTH_FAILURE))) { + authenticationFailed = "true"; + + if (request.getParameter(Constants.AUTH_FAILURE_MSG) != null) { + errorMessage = request.getParameter(Constants.AUTH_FAILURE_MSG); + + if (errorMessage.equalsIgnoreCase("authentication.fail.message")) { + errorMessage = AuthenticationEndpointUtil.i18n(resourceBundle, "error.retry"); + } + } + } +%> + +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "medium"); +%> + + + + + <% + File headerFile = new File(getServletContext().getRealPath("extensions/header.jsp")); + if (headerFile.exists()) { + %> + + <% + } else { + %> + + <% + } + %> + + + + + + + + + <% + File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); + if (productTitleFile.exists()) { + %> + + <% + } else { + %> + + <% + } + %> + + +
+ +

Select Your Organization

+ + + <% + if ("true".equals(authenticationFailed)) { + %> +
<%=Encode.forHtmlContent(errorMessage)%>
+ + <% + } + %> + +
+ +
+
+ <% + for (int i=1; i <= orgCount; i++) { %> + " required> +
+ <% + }%> + + + + +
+ +
+ +
+
+ +
+
+ + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% + } else { + %> + + <% + } + %> + +
+ + + <% + File footerFile = new File(getServletContext().getRealPath("extensions/footer.jsp")); + if (footerFile.exists()) { + %> + + <% + } else { + %> + + <% + } + %> + + diff --git a/apps/sms-otp-authentication-portal/src/main/webapp/smsotp.jsp b/apps/authentication-portal/src/main/webapp/smsOtp.jsp similarity index 81% rename from apps/sms-otp-authentication-portal/src/main/webapp/smsotp.jsp rename to apps/authentication-portal/src/main/webapp/smsOtp.jsp index 6cd94656f1e..ea0b902a969 100644 --- a/apps/sms-otp-authentication-portal/src/main/webapp/smsotp.jsp +++ b/apps/authentication-portal/src/main/webapp/smsOtp.jsp @@ -24,6 +24,9 @@ <%@ page import="java.util.Map" %> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> <%@ include file="includes/localize.jsp" %> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> + + <% request.getSession().invalidate(); @@ -33,7 +36,7 @@ idpAuthenticatorMapping = (Map) request.getAttribute(Constants.IDP_AUTHENTICATOR_MAP); } - String errorMessage = IdentityManagementEndpointUtil.i18n(recoveryResourceBundle,"error.retry"); + String errorMessage = IdentityManagementEndpointUtil.i18n(resourceBundle,"error.retry"); String authenticationFailed = "false"; if (Boolean.parseBoolean(request.getParameter(Constants.AUTH_FAILURE))) { @@ -43,15 +46,20 @@ errorMessage = request.getParameter(Constants.AUTH_FAILURE_MSG); if (errorMessage.equalsIgnoreCase("authentication.fail.message")) { - errorMessage = IdentityManagementEndpointUtil.i18n(recoveryResourceBundle,"error.retry"); + errorMessage = IdentityManagementEndpointUtil.i18n(resourceBundle,"error.retry"); } if (errorMessage.equalsIgnoreCase(SMSOTPConstants.TOKEN_EXPIRED_VALUE)) { - errorMessage = IdentityManagementEndpointUtil.i18n(recoveryResourceBundle,"error.code.expired.resend"); + errorMessage = IdentityManagementEndpointUtil.i18n(resourceBundle,"error.code.expired.resend"); } } } %> +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "medium"); +%> + @@ -70,8 +78,8 @@ -
-
+ + <% File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); @@ -81,10 +89,11 @@ <% } else { %> <% } %> - + +
-

<%=IdentityManagementEndpointUtil.i18n(recoveryResourceBundle, "auth.with.smsotp")%>

+

<%=IdentityManagementEndpointUtil.i18n(resourceBundle, "auth.with.smsotp")%>

<% if ("true".equals(authenticationFailed)) { @@ -104,7 +113,7 @@ if (authFailureMsg != null && "login.fail.message".equals(authFailureMsg)) { %>
- <%=IdentityManagementEndpointUtil.i18n(recoveryResourceBundle, "error.retry")%> + <%=IdentityManagementEndpointUtil.i18n(resourceBundle, "error.retry")%>
<% } } %> @@ -112,13 +121,13 @@ <% if (request.getParameter("screenvalue") != null) { %>
<% } else { %>
- + <% } %> @@ -132,29 +141,30 @@ if ("true".equals(reSendCode)) { %> <% } } %> " class="ui primary button"/> + value="<%=IdentityManagementEndpointUtil.i18n(resourceBundle, "authenticate.button")%>" class="ui primary button"/>
-
-
- - - <% - File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); - if (productFooterFile.exists()) { - %> - - <% } else { %> - - <% } %> + + + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + + <% @@ -175,7 +185,7 @@ var OTPcode = document.getElementById("OTPcode").value; if (OTPcode == "") { document.getElementById('alertDiv').innerHTML - = '
<%=IdentityManagementEndpointUtil.i18n(recoveryResourceBundle, "please.enter.code")%>
'; + = '
<%=IdentityManagementEndpointUtil.i18n(resourceBundle, "please.enter.code")%>
'; } else { $('#pin_form').data("submitted", true); $('#pin_form').submit(); diff --git a/apps/sms-otp-authentication-portal/src/main/webapp/smsotpError.jsp b/apps/authentication-portal/src/main/webapp/smsOtpError.jsp similarity index 79% rename from apps/sms-otp-authentication-portal/src/main/webapp/smsotpError.jsp rename to apps/authentication-portal/src/main/webapp/smsOtpError.jsp index a0fcbd691d6..6cefe7b7cb9 100644 --- a/apps/sms-otp-authentication-portal/src/main/webapp/smsotpError.jsp +++ b/apps/authentication-portal/src/main/webapp/smsOtpError.jsp @@ -28,6 +28,9 @@ <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> <%@ page import="static java.util.Base64.getDecoder" %> <%@ include file="includes/localize.jsp" %> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> + + <% request.getSession().invalidate(); @@ -37,7 +40,7 @@ idpAuthenticatorMapping = (Map) request.getAttribute(Constants.IDP_AUTHENTICATOR_MAP); } - String errorMessage = IdentityManagementEndpointUtil.i18n(recoveryResourceBundle,"error.retry"); + String errorMessage = IdentityManagementEndpointUtil.i18n(resourceBundle,"error.retry"); String authenticationFailed = "false"; String errorInfo = null; @@ -48,22 +51,22 @@ errorMessage = request.getParameter(Constants.AUTH_FAILURE_MSG); if (errorMessage.equalsIgnoreCase("authentication.fail.message")) { - errorMessage = IdentityManagementEndpointUtil.i18n(recoveryResourceBundle,"error.retry"); + errorMessage = IdentityManagementEndpointUtil.i18n(resourceBundle,"error.retry"); } else if (errorMessage.equalsIgnoreCase(SMSOTPConstants.UNABLE_SEND_CODE_VALUE)) { - errorMessage = IdentityManagementEndpointUtil.i18n(recoveryResourceBundle,"error.send"); + errorMessage = IdentityManagementEndpointUtil.i18n(resourceBundle,"error.send"); } else if (errorMessage.equalsIgnoreCase(SMSOTPConstants.ERROR_CODE_MISMATCH)) { - errorMessage = IdentityManagementEndpointUtil.i18n(recoveryResourceBundle,"error.code"); + errorMessage = IdentityManagementEndpointUtil.i18n(resourceBundle,"error.code"); } else if (errorMessage.equalsIgnoreCase(SMSOTPConstants.ERROR_SMSOTP_DISABLE_MSG)) { - errorMessage = IdentityManagementEndpointUtil.i18n(recoveryResourceBundle,"error.smsotp.disabled"); + errorMessage = IdentityManagementEndpointUtil.i18n(resourceBundle,"error.smsotp.disabled"); } else if (errorMessage.equalsIgnoreCase(SMSOTPConstants.TOKEN_EXPIRED_VALUE)) { - errorMessage = IdentityManagementEndpointUtil.i18n(recoveryResourceBundle,"error.token.expired"); + errorMessage = IdentityManagementEndpointUtil.i18n(resourceBundle,"error.token.expired"); } else if (errorMessage.equalsIgnoreCase(SMSOTPConstants.SEND_OTP_DIRECTLY_DISABLE_MSG)) { - errorMessage = IdentityManagementEndpointUtil.i18n(recoveryResourceBundle,"error.user.not.found"); + errorMessage = IdentityManagementEndpointUtil.i18n(resourceBundle,"error.user.not.found.smsotp"); } else if (errorMessage.equalsIgnoreCase("user.account.locked")) { - errorMessage = IdentityManagementEndpointUtil.i18n(recoveryResourceBundle,"error.user.locked"); + errorMessage = IdentityManagementEndpointUtil.i18n(resourceBundle,"error.user.account.locked"); String unlockTime = request.getParameter("unlockTime"); if (unlockTime != null) { - errorMessage = String.format(IdentityManagementEndpointUtil.i18n(recoveryResourceBundle,"error.user.locked.temporarly"), unlockTime); + errorMessage = String.format(IdentityManagementEndpointUtil.i18n(resourceBundle,"error.user.locked.temporarly"), unlockTime); } } else if (SMSOTPUtils.useInternalErrorCodes()) { String httpCode = URLDecoder.decode(errorMessage, SMSOTPConstants.CHAR_SET_UTF_8); @@ -77,6 +80,12 @@ } } %> + +<%-- Data for the layout from the page --%> +<% + layoutData.put("containerSize", "medium"); +%> + @@ -95,8 +104,8 @@ -
-
+ + <% File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); @@ -106,10 +115,11 @@ <% } else { %> <% } %> - + +
-

<%=IdentityManagementEndpointUtil.i18n(recoveryResourceBundle, "error.failed.with.smsotp")%>

+

<%=IdentityManagementEndpointUtil.i18n(resourceBundle, "error.failed.with.smsotp")%>

<% if ("true".equals(authenticationFailed)) { %> @@ -122,18 +132,19 @@ } %>
-
-
- - - <% - File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); - if (productFooterFile.exists()) { - %> - - <% } else { %> - - <% } %> + + + + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + + <% diff --git a/apps/authentication-portal/src/main/webapp/templates/genericForm.jsp b/apps/authentication-portal/src/main/webapp/templates/genericForm.jsp index 6f4f7e089ba..d64201f0bbd 100644 --- a/apps/authentication-portal/src/main/webapp/templates/genericForm.jsp +++ b/apps/authentication-portal/src/main/webapp/templates/genericForm.jsp @@ -21,32 +21,24 @@ <%@taglib prefix="e" uri="https://www.owasp.org/index.php/OWASP_Java_Encoder_Project" %> -
-

- Welcome -

-
- -
-
- - { !isUserAttributesLoading? ( + { isUserAttributesLoading === false ? ( diff --git a/apps/console/src/features/applications/components/settings/attribute-management/role-mapping.tsx b/apps/console/src/features/applications/components/settings/attribute-management/role-mapping.tsx index aeb63d92e3c..651e1e1b83f 100644 --- a/apps/console/src/features/applications/components/settings/attribute-management/role-mapping.tsx +++ b/apps/console/src/features/applications/components/settings/attribute-management/role-mapping.tsx @@ -16,7 +16,6 @@ * under the License. */ -import { getRolesList } from "@wso2is/core/api"; import { AlertLevels, RoleListInterface, RolesInterface, TestableComponentInterface } from "@wso2is/core/models"; import { addAlert } from "@wso2is/core/store"; import { DynamicField, Heading, KeyValue } from "@wso2is/react-components"; @@ -24,6 +23,7 @@ import React, { FunctionComponent, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useDispatch } from "react-redux"; import { Grid } from "semantic-ui-react"; +import { getRolesList } from "../../../../roles/api"; import { RoleMappingInterface } from "../../../models"; interface RoleMappingPropsInterface extends TestableComponentInterface { diff --git a/apps/console/src/features/applications/components/settings/certificate/application-certificate-wrapper.tsx b/apps/console/src/features/applications/components/settings/certificate/application-certificate-wrapper.tsx index 06e64529b2a..4164f51d609 100644 --- a/apps/console/src/features/applications/components/settings/certificate/application-certificate-wrapper.tsx +++ b/apps/console/src/features/applications/components/settings/certificate/application-certificate-wrapper.tsx @@ -20,13 +20,13 @@ import { AlertLevels, TestableComponentInterface } from "@wso2is/core/models"; import { addAlert } from "@wso2is/core/store"; import { URLUtils } from "@wso2is/core/utils"; import { Field, Forms, Validation } from "@wso2is/forms"; -import { Code, Heading, Hint } from "@wso2is/react-components"; +import { Code, ConfirmationModal, Heading, Hint, Message } from "@wso2is/react-components"; import { FormValidation } from "@wso2is/validation"; import isEmpty from "lodash-es/isEmpty"; import React, { FunctionComponent, ReactElement, ReactNode, useEffect, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { useDispatch } from "react-redux"; -import { Divider, Grid, Message } from "semantic-ui-react"; +import { Divider, Grid } from "semantic-ui-react"; import { ApplicationCertificatesListComponent } from "./application-certificate-list"; import { commonConfig } from "../../../../../extensions"; import { @@ -77,6 +77,7 @@ interface ApplicationWrapperCertificatesPropsInterface extends TestableComponent hidden: boolean; readOnly: boolean; triggerSubmit?: boolean; + canDiscardCertificate?: () => boolean; } /** @@ -103,6 +104,7 @@ export const ApplicationCertificateWrapper: FunctionComponent(CertificateTypeInterface.NONE); const [ PEMValue, setPEMValue ] = useState(undefined); const [ JWKSValue, setJWKSValue ] = useState(undefined); + const [ showInvalidOperationModal, setShowInvalidOperationModal ] = useState(false); /** * Set the certificate type @@ -163,9 +166,7 @@ export const ApplicationCertificateWrapper: FunctionComponent ( + setShowInvalidOperationModal(false) } + type="negative" + open={ showInvalidOperationModal } + primaryAction={ t("common:okay") } + onPrimaryActionClick={ (): void => { + setShowInvalidOperationModal(false); + } } + closeOnDimmerClick={ false } + > + + { + t("console:develop.features.applications.forms." + + "advancedConfig.sections.certificate.invalidOperationModal.header") } + + + { t("console:develop.features.applications.forms." + + "advancedConfig.sections.certificate.invalidOperationModal.message") } + + + ); + return ( !hidden ? ( @@ -255,6 +287,19 @@ export const ApplicationCertificateWrapper: FunctionComponent { + const certType = value as CertificateTypeInterface; + + if(CertificateTypeInterface.NONE === certType && !canDiscardCertificate()){ + setShowInvalidOperationModal(true); + + return false; + } + + return true; + } + } type="radio" value={ certificate?.type } children={ !hideJWKS ? [ @@ -322,7 +367,9 @@ export const ApplicationCertificateWrapper: FunctionComponent { + setPEMValue(val); + } } applicationCertificate={ PEMValue } /> ) @@ -384,7 +431,7 @@ export const ApplicationCertificateWrapper: FunctionComponent @@ -393,6 +440,7 @@ export const ApplicationCertificateWrapper: FunctionComponent{ resolveHintContent(protocol) } } + { showInvalidOperationModal && renderInvalidOperationModal() } ) : null diff --git a/apps/console/src/features/applications/components/settings/general-application-settings.tsx b/apps/console/src/features/applications/components/settings/general-application-settings.tsx index c3d29375b65..afd794147e6 100644 --- a/apps/console/src/features/applications/components/settings/general-application-settings.tsx +++ b/apps/console/src/features/applications/components/settings/general-application-settings.tsx @@ -30,6 +30,7 @@ import React, { FunctionComponent, ReactElement, useState } from "react"; import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; import { Divider } from "semantic-ui-react"; +import { applicationConfig } from "../../../../extensions"; import { AppState, FeatureConfigInterface, UIConfigInterface } from "../../../core"; import { deleteApplication, updateApplicationDetails } from "../../api"; import { @@ -96,6 +97,10 @@ interface GeneralApplicationSettingsInterface extends SBACInterface setShowDeleteConfirmationModal(false) } - type="warning" + type="negative" open={ showDeleteConfirmationModal } assertionHint={ t("console:develop.features.applications.confirmations.deleteApplication." + "assertionHint") } @@ -319,7 +333,7 @@ export const GeneralApplicationSettings: FunctionComponent { t("console:develop.features.applications.confirmations.deleteApplication.message") } diff --git a/apps/console/src/features/applications/components/settings/provisioning/inbound-provisioning-configuration.tsx b/apps/console/src/features/applications/components/settings/provisioning/inbound-provisioning-configuration.tsx index 4618a640d71..98d137222be 100644 --- a/apps/console/src/features/applications/components/settings/provisioning/inbound-provisioning-configuration.tsx +++ b/apps/console/src/features/applications/components/settings/provisioning/inbound-provisioning-configuration.tsx @@ -16,7 +16,6 @@ * under the License. */ -import { getUserStoreList } from "@wso2is/core/api"; import { hasRequiredScopes } from "@wso2is/core/helpers"; import { AlertLevels, SBACInterface, TestableComponentInterface } from "@wso2is/core/models"; import { addAlert } from "@wso2is/core/store"; @@ -25,7 +24,9 @@ import React, { FunctionComponent, MouseEvent, ReactElement, useEffect, useState import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; import { AccordionTitleProps, Divider, Grid } from "semantic-ui-react"; -import { AppState, AuthenticatorAccordion, FeatureConfigInterface } from "../../../../core"; +import { AppState, AuthenticatorAccordion, FeatureConfigInterface, store } from "../../../../core"; +import { OrganizationUtils } from "../../../../organizations/utils"; +import { getUserStoreList } from "../../../../userstores/api"; import { updateApplicationConfigurations } from "../../../api"; import { ProvisioningConfigurationInterface, SimpleUserStoreListItemInterface } from "../../../models"; import { ProvisioningConfigurationsForm } from "../../forms"; @@ -154,12 +155,16 @@ export const InboundProvisioningConfigurations: FunctionComponent { - userstore.push(...response.data); - setUserStore(userstore); - }).catch(() => { + if (OrganizationUtils.isCurrentOrganizationRoot()) { + getUserStoreList().then((response) => { + userstore.push(...response.data); + setUserStore(userstore); + }).catch(() => { + setUserStore(userstore); + }); + } else { setUserStore(userstore); - }); + } }, []); return ( diff --git a/apps/console/src/features/applications/components/settings/provisioning/outbound-provisioning-configuration.tsx b/apps/console/src/features/applications/components/settings/provisioning/outbound-provisioning-configuration.tsx index 9194e0708a2..7a03b776b96 100644 --- a/apps/console/src/features/applications/components/settings/provisioning/outbound-provisioning-configuration.tsx +++ b/apps/console/src/features/applications/components/settings/provisioning/outbound-provisioning-configuration.tsx @@ -364,7 +364,7 @@ export const OutboundProvisioningConfiguration: FunctionComponent setShowDeleteConfirmationModal(false) } - type="warning" + type="negative" open={ showDeleteConfirmationModal } assertion={ deletingIdp?.idp } assertionHint={ ( @@ -401,7 +401,7 @@ export const OutboundProvisioningConfiguration: FunctionComponent { t("console:develop.features.applications.confirmations.deleteOutboundProvisioningIDP" + diff --git a/apps/console/src/features/applications/components/settings/sign-on-methods/script-based-flow/script-based-flow.tsx b/apps/console/src/features/applications/components/settings/sign-on-methods/script-based-flow/script-based-flow.tsx index 6c2394d0eeb..e76bc8d3162 100644 --- a/apps/console/src/features/applications/components/settings/sign-on-methods/script-based-flow/script-based-flow.tsx +++ b/apps/console/src/features/applications/components/settings/sign-on-methods/script-based-flow/script-based-flow.tsx @@ -49,6 +49,7 @@ import { Checkbox, Dropdown, Header, Icon, Input, Menu, Popup, Sidebar } from "s import { stripSlashes } from "slashes"; import { ScriptTemplatesSidePanel } from "./script-templates-side-panel"; import { AppUtils, EventPublisher, getOperationIcons } from "../../../../../core"; +import { OrganizationUtils } from "../../../../../organizations/utils"; import { deleteSecret, getSecretList } from "../../../../../secrets/api/secret"; import AddSecretWizard from "../../../../../secrets/components/add-secret-wizard"; import { ADAPTIVE_SCRIPT_SECRETS } from "../../../../../secrets/constants/secrets.common"; @@ -186,31 +187,33 @@ export const ScriptBasedFlow: FunctionComponent = */ const loadSecretListForSecretType = (): void => { - setIsSecretListLoading(true); + if (OrganizationUtils.isCurrentOrganizationRoot()) { + setIsSecretListLoading(true); - getSecretList({ - params: { secretType: ADAPTIVE_SCRIPT_SECRETS } - }).then((axiosResponse: AxiosResponse) => { - setSecretList(axiosResponse.data); - setFilteredSecretList(axiosResponse.data); - }).catch((error) => { - if (error.response && error.response.data && error.response.data.description) { + getSecretList({ + params: { secretType: ADAPTIVE_SCRIPT_SECRETS } + }).then((axiosResponse: AxiosResponse) => { + setSecretList(axiosResponse.data); + setFilteredSecretList(axiosResponse.data); + }).catch((error) => { + if (error.response && error.response.data && error.response.data.description) { + dispatch(addAlert({ + description: error.response.data?.description, + level: AlertLevels.ERROR, + message: error.response.data?.message + })); + + return; + } dispatch(addAlert({ - description: error.response.data?.description, + description: t("console:develop.features.secrets.errors.generic.description"), level: AlertLevels.ERROR, - message: error.response.data?.message + message: t("console:develop.features.secrets.errors.generic.message") })); - - return; - } - dispatch(addAlert({ - description: t("console:develop.features.secrets.errors.generic.description"), - level: AlertLevels.ERROR, - message: t("console:develop.features.secrets.errors.generic.message") - })); - }).finally(() => { - setIsSecretListLoading(false); - }); + }).finally(() => { + setIsSecretListLoading(false); + }); + } }; @@ -346,9 +349,10 @@ export const ScriptBasedFlow: FunctionComponent = // If there is a script and if the script is not a default script, // assume the user has modified the script and show the editor. - if (authenticationSequence?.script - && !AdaptiveScriptUtils.isDefaultScript(authenticationSequence.script, - authenticationSequence?.steps?.length)) { + if (authenticationSequence?.script && !AdaptiveScriptUtils.isDefaultScript( + authenticationSequence.script, + authenticationSequence?.steps?.length + )) { setShowConditionalAuthContent(true); @@ -489,7 +493,7 @@ export const ScriptBasedFlow: FunctionComponent = * Handles conditional authentication on/off swicth. */ const handleConditionalAuthToggleChange = (): void => { - + if (showConditionalAuthContent) { setShowScriptResetWarning(true); @@ -608,7 +612,7 @@ export const ScriptBasedFlow: FunctionComponent = } > Click here if you need to add more steps to the flow. - Once you add a new step,executeStep(STEP_NUMBER); will appear on + Once you add a new step,executeStep(STEP_NUMBER); will appear on the script editor. @@ -1097,7 +1101,7 @@ export const ScriptBasedFlow: FunctionComponent = } } onPrimaryActionClick={ onSecretDeleteClick } open={ showDeleteConfirmationModal } - type="warning" + type="negative" assertionHint={ t("console:develop.features.secrets.modals.deleteSecret.assertionHint") } assertionType="checkbox" primaryAction={ t("console:develop.features.secrets.modals.deleteSecret.primaryActionButtonText") } @@ -1109,7 +1113,7 @@ export const ScriptBasedFlow: FunctionComponent = { t("console:develop.features.secrets.modals.deleteSecret.warningMessage") } @@ -1204,8 +1208,8 @@ export const ScriptBasedFlow: FunctionComponent = { resolveApiDocumentationLink() } - +
{ renderSecretListDropdown() }
diff --git a/apps/console/src/features/applications/components/settings/sign-on-methods/script-based-flow/template-description.tsx b/apps/console/src/features/applications/components/settings/sign-on-methods/script-based-flow/template-description.tsx index 7ed0096d7cd..3715eac8bf5 100644 --- a/apps/console/src/features/applications/components/settings/sign-on-methods/script-based-flow/template-description.tsx +++ b/apps/console/src/features/applications/components/settings/sign-on-methods/script-based-flow/template-description.tsx @@ -16,14 +16,20 @@ * under the License. */ -import { CodeEditor, DocumentationLink, LinkButton, useDocumentation } from "@wso2is/react-components"; +import { + CodeEditor, + DocumentationLink, + LinkButton, + Message, + useDocumentation +} from "@wso2is/react-components"; import isObject from "lodash-es/isObject"; import React, { FunctionComponent, ReactElement } from "react"; import { useTranslation } from "react-i18next"; -import { Icon, List, Message, Modal, Table } from "semantic-ui-react"; +import { List, Modal, Table } from "semantic-ui-react"; import { AdaptiveAuthTemplateInterface, AdaptiveAuthTemplateTypes } from "../../../../models"; /** @@ -208,15 +214,14 @@ export const TemplateDescription: FunctionComponent - - - - { template.helpLink } - - + + { template.helpLink } + + } + /> ) } diff --git a/apps/console/src/features/applications/components/settings/sign-on-methods/sign-in-method-customization.tsx b/apps/console/src/features/applications/components/settings/sign-on-methods/sign-in-method-customization.tsx index df286d7622c..85083a90865 100644 --- a/apps/console/src/features/applications/components/settings/sign-on-methods/sign-in-method-customization.tsx +++ b/apps/console/src/features/applications/components/settings/sign-on-methods/sign-in-method-customization.tsx @@ -25,18 +25,19 @@ import { Heading, Hint, LinkButton, + Message, PrimaryButton, - Text, useDocumentation } from "@wso2is/react-components"; import kebabCase from "lodash-es/kebabCase"; -import React, { Fragment, FunctionComponent, ReactElement, useEffect, useState } from "react"; +import React, { FunctionComponent, ReactElement, useEffect, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; -import { Divider, Grid, Icon, Message } from "semantic-ui-react"; +import { Divider, Grid, Icon } from "semantic-ui-react"; import { ScriptBasedFlow } from "./script-based-flow"; import { StepBasedFlow } from "./step-based-flow"; import DefaultFlowConfigurationSequenceTemplate from "./templates/default-sequence.json"; +import { applicationConfig } from "../../../../../extensions"; import { AppState, ConfigReducerStateInterface, EventPublisher, FeatureConfigInterface } from "../../../../core"; import { GenericAuthenticatorInterface, IdentityProviderManagementConstants } from "../../../../identity-providers"; import { getRequestPathAuthenticators, updateAuthenticationSequence } from "../../../api"; @@ -65,6 +66,10 @@ interface SignInMethodCustomizationPropsInterface extends SBACInterface by default. // Overriding the behaviour here to make sure it renders properly. - className="warning visible" - header={ ( - - - Warning - - ) } + header="Warning" content={ ( -
+ <> { moreThan1IdP ? ( - + <> Currently, Just-in-Time (JIT) user provisioning is disabled for the following connections:
    @@ -513,38 +513,36 @@ export const SignInMethodCustomization: FunctionComponent )) }
-
+ ) : ( - + <> Currently, Just-in-Time(JIT) user provisioning is disabled for the { idpList[FIRST_ENTRY].name } connection. - + ) } { moreThan1IdP ? ( - + <> To use MFA with these connections, enable JIT provisioning or use an authentication script to skip the MFA options for these connections during user login. - + ) : ( - + <> To use MFA with this connection, enable JIT provisioning or use an authentication script to skip the MFA options for this connection during user login. - + ) } - - - Learn More - - -
+ + Learn More + + ) } /> ); @@ -568,6 +566,9 @@ export const SignInMethodCustomization: FunctionComponent { + eventPublisher.publish("application-revert-sign-in-method-default", { + "client-id": clientId + }); handleSequenceUpdate(null, true); onReset(); } } @@ -595,24 +596,48 @@ export const SignInMethodCustomization: FunctionComponent authenticator.authenticator === IdentityProviderManagementConstants.FIDO_AUTHENTICATOR) + && ( + + + To sign in with passwordless login, your users + should have their FIDO2 security keys or biometrics + registered via My Account. + + + { t("common:learnMore") } + + ) + } + /> + ) + } + { + authenticationSequence.steps[ 0 ].options.find(authenticator => + authenticator.authenticator === IdentityProviderManagementConstants.IDENTIFIER_FIRST_AUTHENTICATOR + && applicationConfig.signInMethod.identifierFirstWarning) && ( - To sign in with passwordless login, your users - should have their FIDO2 security keys or biometrics - registered via My Account. + You can only use Identifier First authenticator with the Magic Link authenticator. + Using it with any other authenticator can lead to unexpected behavior. - - { t("common:learnMore") } - ) } @@ -638,17 +663,22 @@ export const SignInMethodCustomization: FunctionComponent - setIsDefaultScript(true) } - /> + { + isAdaptiveAuthenticationAvailable + && ( + setIsDefaultScript(true) } + /> + ) + } { (config?.ui?.isRequestPathAuthenticationEnabled === false) ? null diff --git a/apps/console/src/features/applications/components/settings/sign-on-methods/sign-in-method-landing.tsx b/apps/console/src/features/applications/components/settings/sign-on-methods/sign-in-method-landing.tsx index 6bad3f60374..bb36b1b166b 100644 --- a/apps/console/src/features/applications/components/settings/sign-on-methods/sign-in-method-landing.tsx +++ b/apps/console/src/features/applications/components/settings/sign-on-methods/sign-in-method-landing.tsx @@ -17,14 +17,14 @@ */ import { SBACInterface, TestableComponentInterface } from "@wso2is/core/models"; -import { Code, GenericIcon, Heading, InfoCard, Text } from "@wso2is/react-components"; +import { Heading, InfoCard } from "@wso2is/react-components"; import React, { FunctionComponent, ReactElement } from "react"; -import { Trans, useTranslation } from "react-i18next"; +import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; -import { Divider, Grid, Responsive, Segment } from "semantic-ui-react"; -import { AppState, Config, ConfigReducerStateInterface, FeatureConfigInterface } from "../../../../core"; +import { Grid, Responsive, Segment } from "semantic-ui-react"; +import { AppState, ConfigReducerStateInterface, EventPublisher, FeatureConfigInterface } from "../../../../core"; import { IdentityProviderManagementConstants } from "../../../../identity-providers"; -import { getAuthenticatorIcons, getSignInMethodIllustrations } from "../../../configs"; +import { getAuthenticatorIcons } from "../../../configs"; import { LoginFlowTypes } from "../../../models"; /** @@ -35,6 +35,10 @@ interface SignInMethodLandingPropsInterface extends SBACInterface = ( props: SignInMethodLandingPropsInterface ): ReactElement => { - - const { - isLoading, - onLoginFlowSelect, - hiddenOptions, - [ "data-testid" ]: testId - } = props; + const { isLoading, onLoginFlowSelect, hiddenOptions, [ "data-testid" ]: testId, clientId } = props; const { t } = useTranslation(); const config: ConfigReducerStateInterface = useSelector((state: AppState) => state.config); + const eventPublisher = EventPublisher.getInstance(); + return ( - + - -
- -
+ + + { t( + "console:develop.features.applications.edit.sections.signOnMethod.sections." + + "landing.flowBuilder.heading" + ) } + - +
+ +
- - { - t("console:develop.features.applications.edit.sections.signOnMethod.sections." + - "landing.flowBuilder.heading") - } - - -
+
+ +
+ + { t( + "console:develop.features.applications.edit." + + "sections.signOnMethod.sections." + + "landing.flowBuilder.headings.passwordlessLogin" + ) } + + { !hiddenOptions?.includes(LoginFlowTypes.FIDO_LOGIN) && + !config.ui?.hiddenAuthenticators.includes( + IdentityProviderManagementConstants.FIDO_AUTHENTICATOR + ) && ( + { + eventPublisher.publish("application-begin-sign-in-biometrics-password-less", { + "client-id": clientId + }); + onLoginFlowSelect(LoginFlowTypes.FIDO_LOGIN); + } } + /> + ) } + { !hiddenOptions?.includes(LoginFlowTypes.MAGIC_LINK) && + !config.ui?.hiddenAuthenticators.includes( + IdentityProviderManagementConstants.MAGIC_LINK_AUTHENTICATOR + ) && ( + { + eventPublisher.publish("application-begin-sign-in-magiclink-password-less", { + "client-id": clientId + }); + onLoginFlowSelect(LoginFlowTypes.MAGIC_LINK); + } } + /> + ) } + { (!hiddenOptions.includes(LoginFlowTypes.GOOGLE_LOGIN) || + !hiddenOptions.includes(LoginFlowTypes.FACEBOOK_LOGIN) || + !hiddenOptions.includes(LoginFlowTypes.GITHUB_LOGIN)) && ( + <> + + { t( + "console:develop.features.applications.edit." + + "sections.signOnMethod.sections." + + "landing.flowBuilder.headings.socialLogin" + ) } + + { !hiddenOptions.includes(LoginFlowTypes.GOOGLE_LOGIN) && ( + { + eventPublisher.publish("application-begin-sign-in-google-social-login", { + type: clientId + }); + onLoginFlowSelect(LoginFlowTypes.GOOGLE_LOGIN); + } } + /> + ) } + + { !hiddenOptions.includes(LoginFlowTypes.GITHUB_LOGIN) && ( + onLoginFlowSelect(LoginFlowTypes.PASSWORDLESS_LOGIN) } - /> - ) - } - { - !hiddenOptions.includes(LoginFlowTypes.DEFAULT) && ( - <> - Or + "types.github.description" + ) } + onClick={ () => onLoginFlowSelect(LoginFlowTypes.GITHUB_LOGIN) } + /> + ) } + { !hiddenOptions.includes(LoginFlowTypes.FACEBOOK_LOGIN) && ( onLoginFlowSelect(LoginFlowTypes.DEFAULT) } + image={ getAuthenticatorIcons().facebook } + header={ t( + "console:develop.features.applications.edit.sections" + + ".signOnMethod.sections.landing.flowBuilder.types.facebook.heading" + ) } + description={ t( + "console:develop.features.applications.edit.sections" + + ".signOnMethod.sections.landing.flowBuilder." + + "types.facebook.description" + ) } + onClick={ () => onLoginFlowSelect(LoginFlowTypes.FACEBOOK_LOGIN) } /> - - ) - } + ) } + + ) }
diff --git a/apps/console/src/features/applications/components/settings/sign-on-methods/sign-on-methods.tsx b/apps/console/src/features/applications/components/settings/sign-on-methods/sign-on-methods.tsx index 72d52c4aafa..a705fa1cf04 100644 --- a/apps/console/src/features/applications/components/settings/sign-on-methods/sign-on-methods.tsx +++ b/apps/console/src/features/applications/components/settings/sign-on-methods/sign-on-methods.tsx @@ -30,6 +30,8 @@ import DefaultFlowConfigurationSequenceTemplate from "./templates/default-sequen import FacebookLoginSequenceTemplate from "./templates/facebook-login-sequence.json"; import GitHubLoginSequenceTemplate from "./templates/github-login-sequence.json"; import GoogleLoginSequenceTemplate from "./templates/google-login-sequence.json"; +import MagicLinkSequenceTemplate from "./templates/magic-link-sequence.json"; +import SecondFactorEMAILOTPSequenceTemplate from "./templates/second-factor-email-otp-sequence.json"; import SecondFactorTOTPSequenceTemplate from "./templates/second-factor-totp-sequence.json"; import UsernamelessSequenceTemplate from "./templates/usernameless-login-sequence.json"; import { AppConstants, EventPublisher, FeatureConfigInterface, history } from "../../../../core"; @@ -42,7 +44,12 @@ import { IdentityProviderTemplateInterface } from "../../../../identity-providers"; import { ApplicationManagementConstants } from "../../../constants"; -import { ApplicationInterface, AuthenticationSequenceInterface, LoginFlowTypes } from "../../../models"; +import { + ApplicationInterface, + AuthenticationSequenceInterface, + AuthenticationSequenceType, + LoginFlowTypes +} from "../../../models"; import { AdaptiveScriptUtils } from "../../../utils"; /** @@ -62,6 +69,10 @@ interface SignOnMethodsPropsInterface extends SBACInterface = ( const { appId, authenticationSequence, + clientId, isLoading, onUpdate, readOnly, - [ "data-testid" ]: testId + ["data-testid"]: testId } = props; const { t } = useTranslation(); @@ -131,7 +143,7 @@ export const SignOnMethods: FunctionComponent = ( const [ idpCreateWizardTriggerOrigin, setIDPCreateWizardTriggerOrigin - ] = useState<"INTERNAL"|"EXTERNAL">(undefined); + ] = useState<"INTERNAL" | "EXTERNAL">(undefined); const eventPublisher: EventPublisher = EventPublisher.getInstance(); @@ -177,9 +189,9 @@ export const SignOnMethods: FunctionComponent = ( * google: GenericAuthenticatorInterface[]) => void} onSuccess - On Success callback. */ const fetchAndCategorizeAuthenticators = (onSuccess?: (all: GenericAuthenticatorInterface[][], - google: GenericAuthenticatorInterface[], - github: GenericAuthenticatorInterface[], - facebook: GenericAuthenticatorInterface[] + google: GenericAuthenticatorInterface[], + github: GenericAuthenticatorInterface[], + facebook: GenericAuthenticatorInterface[] ) => void): Promise => { setIsAuthenticatorsFetchRequestLoading(true); @@ -191,17 +203,17 @@ export const SignOnMethods: FunctionComponent = ( const gitHub: GenericAuthenticatorInterface[] = []; const facebook: GenericAuthenticatorInterface[] = []; - response[ 1 ].filter((authenticator: GenericAuthenticatorInterface) => { + response[1].filter((authenticator: GenericAuthenticatorInterface) => { if (authenticator.defaultAuthenticator.authenticatorId === IdentityProviderManagementConstants.GOOGLE_OIDC_AUTHENTICATOR_ID) { google.push(authenticator); } else if (authenticator.defaultAuthenticator.authenticatorId - === IdentityProviderManagementConstants.GITHUB_AUTHENTICATOR_ID) { + === IdentityProviderManagementConstants.GITHUB_AUTHENTICATOR_ID) { gitHub.push(authenticator); } else if (authenticator.defaultAuthenticator.authenticatorId - === IdentityProviderManagementConstants.FACEBOOK_AUTHENTICATOR_ID) { + === IdentityProviderManagementConstants.FACEBOOK_AUTHENTICATOR_ID) { facebook.push(authenticator); } @@ -231,16 +243,16 @@ export const SignOnMethods: FunctionComponent = ( */ const isDefaultFlowConfiguration = (): boolean => { - if (authenticationSequence?.steps?.length !== 1 || authenticationSequence.steps[ 0 ].options?.length !== 1) { + if (authenticationSequence?.steps?.length !== 1 || authenticationSequence.steps[0].options?.length !== 1) { return false; } - const isBasicStep: boolean = authenticationSequence.steps[ 0 ].options[ 0 ].authenticator + const isBasicStep: boolean = authenticationSequence.steps[0].options[0].authenticator === IdentityProviderManagementConstants.BASIC_AUTHENTICATOR; const isBasicScript: boolean = !authenticationSequence.script || AdaptiveScriptUtils.isDefaultScript(authenticationSequence.script, authenticationSequence.steps?.length); - return isBasicStep && isBasicScript; + return isBasicStep && isBasicScript && authenticationSequence.type === AuthenticationSequenceType.DEFAULT; }; /** @@ -274,7 +286,15 @@ export const SignOnMethods: FunctionComponent = ( ...authenticationSequence, ...cloneDeep(SecondFactorTOTPSequenceTemplate) }); - } else if (loginFlow === LoginFlowTypes.PASSWORDLESS_LOGIN) { + } else if(loginFlow === LoginFlowTypes.SECOND_FACTOR_EMAIL_OTP){ + eventPublisher.publish("application-sign-in-method-click-add", { + type: "second-factor-email-otp" + }); + setModeratedAuthenticationSequence({ + ...authenticationSequence, + ...cloneDeep(SecondFactorEMAILOTPSequenceTemplate) + }); + }else if (loginFlow === LoginFlowTypes.FIDO_LOGIN) { eventPublisher.publish("application-sign-in-method-click-add", { type: "first-factor-fido" }); @@ -374,6 +394,15 @@ export const SignOnMethods: FunctionComponent = ( LoginFlowTypes.FACEBOOK_LOGIN) }); } + } else if (loginFlow === LoginFlowTypes.MAGIC_LINK) { + eventPublisher.publish("application-sign-in-method-click-add", { + type: "magic-link-login" + }); + + setModeratedAuthenticationSequence({ + ...authenticationSequence, + ...cloneDeep(MagicLinkSequenceTemplate) + }); } setLoginFlow(loginFlow); @@ -457,7 +486,7 @@ export const SignOnMethods: FunctionComponent = ( // Since the wizard was triggered from landing page, set the origin as `INTERNAL`. setIDPCreateWizardTriggerOrigin("INTERNAL"); } } - data-testid={ `${ testId }-add-missing-authenticator-modal` } + data-testid={ `${testId}-add-missing-authenticator-modal` } closeOnDimmerClick={ false } > @@ -483,7 +512,7 @@ export const SignOnMethods: FunctionComponent = ( tOptions={ { authenticator: authenticatorName } } > You do not have an active Social Connection configured with { authenticatorName } - Authenticator. Click on the Configure button to register a new + Authenticator. Click on the Configure button to register a new { authenticatorName } Social Connection or navigate to the { history.push(AppConstants.getPaths().get("IDP")); @@ -547,7 +576,7 @@ export const SignOnMethods: FunctionComponent = ( setLoginFlow(socialDisclaimerModalType); setShowDuplicateSocialAuthenticatorSelectionModal(false); } } - data-testid={ `${ testId }-duplicate-authenticator-selection-modal` } + data-testid={ `${testId}-duplicate-authenticator-selection-modal` } closeOnDimmerClick={ false } > @@ -574,31 +603,36 @@ export const SignOnMethods: FunctionComponent = ( tOptions={ { authenticator: authenticatorName } } > You have multiple Social Connections configured with { authenticatorName } - Authenticator. Select the desired one from the selection below to proceed. + Authenticator. Select the desired one from the selection below to proceed. -
@@ -368,7 +372,7 @@ export const AuthenticationStep: FunctionComponent 0)) - && !isContainsOTPAuthenticator && ( + && showSubjectIdentifierCheckBox && (
= ( props: AuthenticatorsPropsInterface ): ReactElement => { - const { authenticators, authenticationSteps, @@ -118,7 +116,6 @@ export const Authenticators: FunctionComponent = ( * Updates the internal selected authenticators state when the prop changes. */ useEffect(() => { - if (!selected) { return; } @@ -127,9 +124,7 @@ export const Authenticators: FunctionComponent = ( }, [ selected ]); const isFactorEnabled = (authenticator: GenericAuthenticatorInterface): boolean => { - if (authenticator.category === AuthenticatorCategories.SECOND_FACTOR) { - // If there is only one step in the flow, second factor authenticators shouldn't be allowed. if (currentStep === 0) { return false; @@ -142,6 +137,17 @@ export const Authenticators: FunctionComponent = ( ); } + // Check if the authenticator is a magic link authenticator + if (authenticator.id === IdentityProviderManagementConstants.MAGIC_LINK_AUTHENTICATOR_ID) { + return SignInMethodUtils.isMagicLinkAuthenticatorValid(currentStep, authenticationSteps); + } + + if ([ + IdentityProviderManagementConstants.IDENTIFIER_FIRST_AUTHENTICATOR_ID, + IdentityProviderManagementConstants.BASIC_AUTHENTICATOR_ID ].includes(authenticator.id)) { + return SignInMethodUtils.isFirstFactorValid(currentStep, authenticationSteps); + } + return true; }; @@ -153,72 +159,95 @@ export const Authenticators: FunctionComponent = ( * @return {React.ReactElement} */ const resolvePopupContent = (authenticator: GenericAuthenticatorInterface): ReactElement => { - const InfoLabel = ( ); if (authenticator.category === AuthenticatorCategories.SECOND_FACTOR) { - return ( <> - { - (currentStep === 0) - ? ( - - { InfoLabel } - - { - applicationConfig.signInMethod.authenticatorSelection.messages - .secondFactorDisabledInFirstStep - ?? t("console:develop.features.applications.edit.sections" + - ".signOnMethod.sections.authenticationFlow.sections.stepBased" + - ".secondFactorDisabledInFirstStep") - } - - - ) - : ( - - { InfoLabel } - - { - applicationConfig.signInMethod.authenticatorSelection.messages - .secondFactorDisabled - ?? ( - - The second-factor authenticators can only be used if Username & Password, - Social Login, - Security Key/Biometrics - or any other handlers such as - Identifier First that can handle these - factors are present in a previous step. - - ) + { currentStep === 0 ? ( + + { InfoLabel } + + { applicationConfig.signInMethod.authenticatorSelection.messages + .secondFactorDisabledInFirstStep ?? + t( + "console:develop.features.applications.edit.sections" + + ".signOnMethod.sections.authenticationFlow.sections.stepBased" + + ".secondFactorDisabledInFirstStep" + ) } + + + ) : ( + + { InfoLabel } + + { applicationConfig.signInMethod.authenticatorSelection.messages + .secondFactorDisabled ?? ( + - - ) - } + > + The second-factor authenticators can only be used if{ " " } + Username & Password,{ " " } + Social Login, + Security Key/Biometrics + or any other handlers that can handle these factors are + present in a previous step. + + ) } + + + ) } ); } else if (authenticator.category === AuthenticatorCategories.SOCIAL) { + return ( + + { InfoLabel } + + { t( + "console:develop.features.applications.edit.sections.signOnMethod.sections." + + "authenticationFlow.sections.stepBased.authenticatorDisabled" + ) } + + + ); + } else if (authenticator.id === IdentityProviderManagementConstants.MAGIC_LINK_AUTHENTICATOR_ID + && !SignInMethodUtils.isMagicLinkAuthenticatorValid(currentStep, authenticationSteps)) { + return ( + + { InfoLabel } + + { + t( + "console:develop.features.applications.edit.sections" + + ".signOnMethod.sections.authenticationFlow.sections.stepBased" + + ".magicLinkDisabled" + ) + } + + + ); + } else if ([ + IdentityProviderManagementConstants.IDENTIFIER_FIRST_AUTHENTICATOR_ID, + IdentityProviderManagementConstants.BASIC_AUTHENTICATOR_ID ].includes(authenticator.id)) { return ( { InfoLabel } { - t("console:develop.features.applications.edit.sections.signOnMethod.sections." + - "authenticationFlow.sections.stepBased.authenticatorDisabled") + t( + "console:develop.features.applications.edit.sections" + + ".signOnMethod.sections.authenticationFlow.sections.stepBased" + + ".firstFactorDisabled" + ) } @@ -232,13 +261,11 @@ export const Authenticators: FunctionComponent = ( * @param {GenericAuthenticatorInterface} selectedAuthenticator - Selected Authenticator. */ const handleAuthenticatorSelect = (selectedAuthenticator: GenericAuthenticatorInterface): void => { - if (!selectedAuthenticator.isEnabled) { return; } if (selectedAuthenticators.some((authenticator) => authenticator.id === selectedAuthenticator.id)) { - const filtered = selectedAuthenticators.filter((authenticator) => { return authenticator.id !== selectedAuthenticator.id; }); @@ -261,7 +288,6 @@ export const Authenticators: FunctionComponent = ( * @return {any[] | string[]} */ const resolveAuthenticatorLabels = (authenticator: FederatedAuthenticatorInterface): string[] => { - if (!authenticator) { return []; } @@ -272,51 +298,58 @@ export const Authenticators: FunctionComponent = ( return ( { heading && { heading } } - { - authenticators.map((authenticator: GenericAuthenticatorInterface, index) => ( - { - return evalAuthenticator.id === authenticator.id; - }) - } - subHeader={ authenticator.categoryDisplayName } - description={ authenticator.description } - image={ authenticator.image } - tags={ showLabels && resolveAuthenticatorLabels((authenticator?.defaultAuthenticator)) } - onClick={ () => { - isFactorEnabled(authenticator) && handleAuthenticatorSelect(authenticator); - } } - imageOptions={ { - floated: false, - inline: true - } } - data-testid={ `${ testId }-authenticator-${ authenticator.name }` } - /> - ) } - /> - )) - } + { authenticators.filter(authenticator => { + if (authenticator?.name + .includes(IdentityProviderManagementConstants.SMS_OTP_AUTHENTICATOR)) { + return false; + } + + return true; + }).map((authenticator: GenericAuthenticatorInterface, index) => ( + { + return evalAuthenticator.id === authenticator.id; + }) + } + subHeader={ authenticator.categoryDisplayName } + description={ authenticator.description } + image={ authenticator.image } + tags={ showLabels && resolveAuthenticatorLabels(authenticator?.defaultAuthenticator) } + onClick={ () => { + isFactorEnabled(authenticator) && handleAuthenticatorSelect(authenticator); + } } + imageOptions={ { + floated: false, + inline: true + } } + data-testid={ `${ testId }-authenticator-${ authenticator.name }` } + />) + } + /> + )) } ); }; diff --git a/apps/console/src/features/applications/components/settings/sign-on-methods/step-based-flow/step-based-flow.tsx b/apps/console/src/features/applications/components/settings/sign-on-methods/step-based-flow/step-based-flow.tsx index 40d94b0b426..c15cba1aec5 100644 --- a/apps/console/src/features/applications/components/settings/sign-on-methods/step-based-flow/step-based-flow.tsx +++ b/apps/console/src/features/applications/components/settings/sign-on-methods/step-based-flow/step-based-flow.tsx @@ -167,7 +167,10 @@ export const StepBasedFlow: FunctionComponent const secondFactorAuth: GenericAuthenticatorInterface[] = []; localAuthenticators.forEach((authenticator: GenericAuthenticatorInterface) => { - if (ApplicationManagementConstants.SECOND_FACTOR_AUTHENTICATORS.includes(authenticator.id)) { + if (authenticator.name === IdentityProviderManagementConstants.BACKUP_CODE_AUTHENTICATOR) { + // Backup code authenticator is not available for customer users at the moment. + return; + } else if (ApplicationManagementConstants.SECOND_FACTOR_AUTHENTICATORS.includes(authenticator.id)) { secondFactorAuth.push(authenticator); } else { moderatedLocalAuthenticators.push(authenticator); @@ -202,7 +205,7 @@ export const StepBasedFlow: FunctionComponent setAuthenticationSteps(authenticationSequence?.steps); setSubjectStepId(authenticationSequence?.subjectStepId); setAttributeStepId(authenticationSequence?.attributeStepId); - }, [ authenticationSequence ]); + }, []); /** * Called when update is triggered. @@ -394,7 +397,10 @@ export const StepBasedFlow: FunctionComponent (item) => item.authenticatorId === authenticator.defaultAuthenticator.authenticatorId ); - steps[ stepIndex ].options.push({ authenticator: defaultAuthenticator.name, idp: authenticator.idp }); + steps[ stepIndex ].options.push({ + authenticator: defaultAuthenticator.name, + idp: authenticator.idp + }); setAuthenticationSteps(steps); }; @@ -406,7 +412,6 @@ export const StepBasedFlow: FunctionComponent * @param {number} optionIndex - Index of the option. */ const handleStepOptionDelete = (stepIndex: number, optionIndex: number): void => { - const steps: AuthenticationStepInterface[] = [ ...authenticationSteps ]; const [ @@ -414,14 +419,28 @@ export const StepBasedFlow: FunctionComponent rightSideSteps ]: AuthenticationStepInterface[][] = SignInMethodUtils.getLeftAndRightSideSteps(stepIndex, steps); + // Checks if identifier first can be deleted. + if ( + stepIndex === 0 && + steps[0].options[optionIndex].authenticator === + IdentityProviderManagementConstants.IDENTIFIER_FIRST_AUTHENTICATOR && + steps[1]?.options?.find( + (option) => option.authenticator === IdentityProviderManagementConstants.MAGIC_LINK_AUTHENTICATOR + ) + ) { + dispatchDeleteErrorNotification(); + + return; + } + const containSecondFactorOnRight: boolean = SignInMethodUtils.hasSpecificFactorsInSteps( - ApplicationManagementConstants.SECOND_FACTOR_AUTHENTICATORS, rightSideSteps); + [ ...ApplicationManagementConstants.SECOND_FACTOR_AUTHENTICATORS ], rightSideSteps); // If there are second factor authenticators on the right, evaluate further. if (containSecondFactorOnRight) { const deletingOption: AuthenticatorInterface = steps[ stepIndex ].options[ optionIndex ]; const noOfSecondFactorsOnRight: number = SignInMethodUtils.countSpecificFactorInSteps( - ApplicationManagementConstants.SECOND_FACTOR_AUTHENTICATORS, rightSideSteps); + [ ...ApplicationManagementConstants.SECOND_FACTOR_AUTHENTICATORS ], rightSideSteps); const noOfSecondFactorsOnRightRequiringHandlers: number = SignInMethodUtils.countSpecificFactorInSteps( [ IdentityProviderManagementConstants.TOTP_AUTHENTICATOR, @@ -429,7 +448,10 @@ export const StepBasedFlow: FunctionComponent ], rightSideSteps); const onlySecondFactorsRequiringHandlersOnRight: boolean = noOfSecondFactorsOnRight === noOfSecondFactorsOnRightRequiringHandlers; - const isDeletingOptionFirstFactor: boolean = ApplicationManagementConstants.FIRST_FACTOR_AUTHENTICATORS + const isDeletingOptionFirstFactor: boolean = [ + ...ApplicationManagementConstants.FIRST_FACTOR_AUTHENTICATORS, + IdentityProviderManagementConstants.IDENTIFIER_FIRST_AUTHENTICATOR + ] .includes(deletingOption.authenticator); const isDeletingOptionSecondFactorHandler: boolean = [ ...ApplicationManagementConstants.TOTP_HANDLERS, @@ -490,19 +512,7 @@ export const StepBasedFlow: FunctionComponent // If there are no other handlers, Show a warning and abort option delete. if (noOfProperHandlersOnLeft <= 1) { - dispatch( - addAlert({ - description: t( - "console:develop.features.applications.notifications." + - "deleteOptionErrorDueToSecondFactorsOnRight.genericError.description" - ), - level: AlertLevels.WARNING, - message: t( - "console:develop.features.applications.notifications." + - "deleteOptionErrorDueToSecondFactorsOnRight.genericError.message" - ) - }) - ); + dispatchDeleteErrorNotification(); return; } @@ -515,6 +525,25 @@ export const StepBasedFlow: FunctionComponent setAuthenticationSteps(steps); }; + /** + * This method dispatches a notification when there is an error during validating a delete action. + */ + const dispatchDeleteErrorNotification = (): void => { + dispatch( + addAlert({ + description: t( + "console:develop.features.applications.notifications." + + "deleteOptionErrorDueToSecondFactorsOnRight.genericError.description" + ), + level: AlertLevels.WARNING, + message: t( + "console:develop.features.applications.notifications." + + "deleteOptionErrorDueToSecondFactorsOnRight.genericError.message" + ) + }) + ); + }; + /** * Handles step option authenticator change. * @@ -678,6 +707,28 @@ export const StepBasedFlow: FunctionComponent return false; } + // Don't allow identifier first being the only authenticator in the flow. + if ( steps.length === 1 + && steps[ 0 ].options.length === 1 + && steps[ 0 ].options[ 0 ].authenticator + === IdentityProviderManagementConstants.IDENTIFIER_FIRST_AUTHENTICATOR ) { + dispatch( + addAlert({ + description: t( + "console:develop.features.applications.notifications.updateOnlyIdentifierFirstError" + + ".description" + ), + level: AlertLevels.WARNING, + message: t( + "console:develop.features.applications.notifications.updateOnlyIdentifierFirstError" + + ".message" + ) + }) + ); + + return false; + } + return true; }; diff --git a/apps/console/src/features/applications/components/settings/sign-on-methods/templates/magic-link-sequence.json b/apps/console/src/features/applications/components/settings/sign-on-methods/templates/magic-link-sequence.json new file mode 100644 index 00000000000..23c520805e0 --- /dev/null +++ b/apps/console/src/features/applications/components/settings/sign-on-methods/templates/magic-link-sequence.json @@ -0,0 +1,24 @@ +{ + "steps": [ + { + "id": 1, + "options": [ + { + "authenticator": "IdentifierExecutor", + "idp": "LOCAL" + } + ] + }, + { + "id": 2, + "options": [ + { + "authenticator": "MagicLinkAuthenticator", + "idp": "LOCAL" + } + ] + } + ], + "attributeStepId": 2, + "subjectStepId": 2 +} diff --git a/apps/console/src/features/applications/components/settings/sign-on-methods/templates/second-factor-email-otp-sequence.json b/apps/console/src/features/applications/components/settings/sign-on-methods/templates/second-factor-email-otp-sequence.json new file mode 100644 index 00000000000..02911102b22 --- /dev/null +++ b/apps/console/src/features/applications/components/settings/sign-on-methods/templates/second-factor-email-otp-sequence.json @@ -0,0 +1,22 @@ +{ + "steps": [ + { + "id": 1, + "options": [ + { + "authenticator": "BasicAuthenticator", + "idp": "LOCAL" + } + ] + }, + { + "id": 2, + "options": [ + { + "authenticator": "email-otp-authenticator", + "idp": "LOCAL" + } + ] + } + ] +} diff --git a/apps/console/src/features/applications/components/wizard/help/generic-minimal-wizard-form-help.tsx b/apps/console/src/features/applications/components/wizard/help/generic-minimal-wizard-form-help.tsx index 6969a5a0b90..992d878e96e 100644 --- a/apps/console/src/features/applications/components/wizard/help/generic-minimal-wizard-form-help.tsx +++ b/apps/console/src/features/applications/components/wizard/help/generic-minimal-wizard-form-help.tsx @@ -17,9 +17,9 @@ */ import { TestableComponentInterface } from "@wso2is/core/models"; -import { Heading } from "@wso2is/react-components"; +import { Heading, Message } from "@wso2is/react-components"; import React, { FunctionComponent, ReactElement } from "react"; -import { Divider, Message } from "semantic-ui-react"; +import { Divider } from "semantic-ui-react"; import { ApplicationManagementConstants } from "../../../constants"; import { ApplicationTemplateListItemInterface, @@ -89,12 +89,17 @@ export const GenericMinimalWizardFormHelp: FunctionComponent - - - Click here - { " " } - to learn more about supported protocols for agent-based single sign-on. - + + + Click here + { " " } + to learn more about supported protocols for agent-based single sign-on. + + } + /> ) } diff --git a/apps/console/src/features/applications/components/wizard/minimal-application-create-wizard.tsx b/apps/console/src/features/applications/components/wizard/minimal-application-create-wizard.tsx index 02e0595d847..a8e5beedc7d 100644 --- a/apps/console/src/features/applications/components/wizard/minimal-application-create-wizard.tsx +++ b/apps/console/src/features/applications/components/wizard/minimal-application-create-wizard.tsx @@ -54,6 +54,8 @@ import { history, store } from "../../../core"; +import { TierLimitReachErrorModal } from "../../../core/components/tier-limit-reach-error-modal"; +import { OrganizationUtils } from "../../../organizations/utils"; import { createApplication, getApplicationList, getApplicationTemplateData } from "../../api"; import { getInboundProtocolLogos } from "../../configs"; import { ApplicationManagementConstants } from "../../constants"; @@ -137,7 +139,7 @@ export const MinimalAppCreateWizard: FunctionComponent>(undefined); const [ customApplicationProtocol, - setCustomApplicationProtocol + setCustomApplicationProtocol ] = useState(SupportedAuthProtocolTypes.OAUTH2_OIDC); const [ isSubmitting, setIsSubmitting ] = useState(false); const [ generalFormValues, setGeneralFormValues ] = useState>(undefined); @@ -146,6 +148,7 @@ export const MinimalAppCreateWizard: FunctionComponent(false); const [ metaUrlError, setMetaUrlError ] = useState(false); const [ protocolValuesChange, setProtocolValuesChange ] = useState(false); + const [ openLimitReachedModal, setOpenLimitReachedModal ] = useState(false); const nameRef = useRef(); const issuerRef = useRef(); const metaUrlRef = useRef(); @@ -159,7 +162,9 @@ export const MinimalAppCreateWizard: FunctionComponent { // Stop fetching CORS origins if the selected template is `Expert Mode`. - if (!selectedTemplate || selectedTemplate.id === CustomApplicationTemplate.id) { + if (!selectedTemplate + || selectedTemplate.id === CustomApplicationTemplate.id + || !OrganizationUtils.isCurrentOrganizationRoot()) { return; } @@ -284,7 +289,21 @@ export const MinimalAppCreateWizard: FunctionComponent { + setOpenLimitReachedModal(false); + handleWizardClose(); + }; + /** * Load application template data. */ @@ -593,7 +611,7 @@ export const MinimalAppCreateWizard: FunctionComponent { - // If `previewOnly`, avoid click actions. + // If `previewOnly`, avoid click actions. if ((subTemplate as ApplicationTemplateInterface).previewOnly) { return; } @@ -909,7 +931,7 @@ export const MinimalAppCreateWizard: FunctionComponent - { + { // The Management App checkbox is only present in OIDC Standard-Based apps (customApplicationProtocol === SupportedAuthProtocolTypes.OAUTH2_OIDC && selectedTemplate?.templateId === "custom-application") && ( @@ -1008,58 +1030,84 @@ export const MinimalAppCreateWizard: FunctionComponent - - - { title } - { subTitle && ( - - { subTitle } - - { t("common:learnMore") } - - + <> + { openLimitReachedModal && ( + - { resolveContent() } - - - - - - { t("common:cancel") } - - - - { - setIssuerError(false); - setSubmit(); - } } - data-testid={ `${ testId }-next-button` } - loading={ isSubmitting } - disabled={ isSubmitting } + handleModalClose={ handleLimitReachedModalClose } + header={ t( + "console:develop.features.applications.notifications.tierLimitReachedError.heading" + ) } + description={ t( + "console:develop.features.applications.notifications." + + "tierLimitReachedError.emptyPlaceholder.subtitles" + ) } + message={ t( + "console:develop.features.applications.notifications." + + "tierLimitReachedError.emptyPlaceholder.title" + ) } + openModal={ openLimitReachedModal } + /> + ) } + + + + { title } + { subTitle && ( + + { subTitle } + - { t("common:register") } - - - - - - - { renderHelpPanel() } - + { t("common:learnMore") } + + + ) } + + { resolveContent() } + + + + + + { t("common:cancel") } + + + + { + setIssuerError(false); + setSubmit(); + } } + data-testid={ `${ testId }-next-button` } + loading={ isSubmitting } + disabled={ isSubmitting } + > + { t("common:register") } + + + + + + + { renderHelpPanel() } + + ); }; diff --git a/apps/console/src/features/applications/components/wizard/oauth-protocol-settings-wizard-form.tsx b/apps/console/src/features/applications/components/wizard/oauth-protocol-settings-wizard-form.tsx index 484a95ac5c6..083f993874a 100644 --- a/apps/console/src/features/applications/components/wizard/oauth-protocol-settings-wizard-form.tsx +++ b/apps/console/src/features/applications/components/wizard/oauth-protocol-settings-wizard-form.tsx @@ -19,13 +19,13 @@ import { TestableComponentInterface } from "@wso2is/core/models"; import { URLUtils } from "@wso2is/core/utils"; import { Field, FormValue, Forms } from "@wso2is/forms"; -import { ContentLoader, Hint, LinkButton, URLInput } from "@wso2is/react-components"; +import { ContentLoader, Hint, LinkButton, Message, URLInput } from "@wso2is/react-components"; import intersection from "lodash-es/intersection"; import isEmpty from "lodash-es/isEmpty"; import React, { FunctionComponent, ReactElement, useEffect, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; -import { Grid, Icon, Message } from "semantic-ui-react"; +import { Grid } from "semantic-ui-react"; import { AppState, ConfigReducerStateInterface } from "../../../../features/core"; import { getAuthProtocolMetadata } from "../../api"; import SinglePageApplicationTemplate @@ -548,44 +548,46 @@ export const OauthProtocolSettingsWizardForm: FunctionComponent - { - (callBackURLFromTemplate) && ( - - - { - - Don’t have an app? Try out a sample app - using { callBackURLFromTemplate } - as the Authorized URL. - - } - { - (callBackUrls === undefined || callBackUrls === "") && ( - { - e.preventDefault(); - const host = new URL(callBackURLFromTemplate); - - handleAddAllowOrigin(host.origin); - setCallBackUrls(callBackURLFromTemplate); + { (callBackURLFromTemplate) && ( + + { + - Add Now - - ) - } - - - ) - } + Don’t have an app? Try out a sample app + using { callBackURLFromTemplate } + as the Authorized URL. + + } + { + (callBackUrls === undefined || callBackUrls === "") && ( + { + e.preventDefault(); + const host = new URL(callBackURLFromTemplate); + + handleAddAllowOrigin(host.origin); + setCallBackUrls(callBackURLFromTemplate); + } } + data-testid={ `${ testId }-add-now-button` } + > + Add Now + + ) + } + ) + } + /> + ) } ) } diff --git a/apps/console/src/features/applications/components/wizard/saml-protocol-settings-all-option-wizard-form.tsx b/apps/console/src/features/applications/components/wizard/saml-protocol-settings-all-option-wizard-form.tsx index 8f86b21b76d..a81fb5e9ff8 100644 --- a/apps/console/src/features/applications/components/wizard/saml-protocol-settings-all-option-wizard-form.tsx +++ b/apps/console/src/features/applications/components/wizard/saml-protocol-settings-all-option-wizard-form.tsx @@ -19,13 +19,21 @@ import { TestableComponentInterface } from "@wso2is/core/models"; import { URLUtils } from "@wso2is/core/utils"; import { Field, FormValue, Forms, Validation } from "@wso2is/forms"; -import { ContentLoader, FilePicker, Hint, LinkButton, URLInput, XMLFileStrategy } from "@wso2is/react-components"; +import { + ContentLoader, + FilePicker, + Hint, + LinkButton, + Message, + URLInput, + XMLFileStrategy +} from "@wso2is/react-components"; import { FormValidation } from "@wso2is/validation"; import isEmpty from "lodash-es/isEmpty"; import React, { FunctionComponent, ReactElement, useEffect, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; -import { Button, Grid, Icon, Message } from "semantic-ui-react"; +import { Button, Grid, Icon } from "semantic-ui-react"; import { commonConfig } from "../../../../extensions"; import { AppState, ConfigReducerStateInterface, getCertificateIllustrations } from "../../../core"; import { SAMLConfigModes } from "../../models"; @@ -113,7 +121,7 @@ export const SAMLProtocolAllSettingsWizardForm: FunctionComponent(null); const [ configureMode, setConfigureMode ] = useState(undefined); const [ hasAssertionConsumerUrls, setHasAssertionConsumerUrls ] = useState(false); - + // State related to file picker const [ xmlBase64String, setXmlBase64String ] = useState(); const [ selectedMetadataFile, setSelectedMetadataFile ] = useState(null); @@ -191,9 +199,9 @@ export const SAMLProtocolAllSettingsWizardForm: FunctionComponent { @@ -345,9 +353,8 @@ export const SAMLProtocolAllSettingsWizardForm: FunctionComponent @@ -465,43 +472,47 @@ export const SAMLProtocolAllSettingsWizardForm: FunctionComponent { (assertionConsumerURLFromTemplate) && ( - - - { - - Don’t have an app? Try out a sample app - using { assertionConsumerURLFromTemplate } as - the assertion Response URL. (You can download and run a sample - at a later step.) - - } - { - (assertionConsumerUrls === undefined || - assertionConsumerUrls === "") && ( - { - e.preventDefault(); - setAssertionConsumerUrls( - assertionConsumerURLFromTemplate); - setIssuer(issuerFromTemplate); - setHasAssertionConsumerUrls(true) - } } - data-testid={ `${testId}-add-now-button` } - > - Add Now - - ) + + { + + Don’t have an app? Try out a sample app + using { assertionConsumerURLFromTemplate } as + the assertion Response URL. (You can download and run a sample + at a later step.) + + } + { + (assertionConsumerUrls === undefined || + assertionConsumerUrls === "") && ( + { + e.preventDefault(); + setAssertionConsumerUrls( + assertionConsumerURLFromTemplate); + setIssuer(issuerFromTemplate); + setHasAssertionConsumerUrls(true); + } } + data-testid={ `${testId}-add-now-button` } + > + Add Now + + ) + } + } - - + /> ) } diff --git a/apps/console/src/features/applications/components/wizard/saml-protocol-settings-wizard-form.tsx b/apps/console/src/features/applications/components/wizard/saml-protocol-settings-wizard-form.tsx index 1a3a95013fe..a42794e2482 100644 --- a/apps/console/src/features/applications/components/wizard/saml-protocol-settings-wizard-form.tsx +++ b/apps/console/src/features/applications/components/wizard/saml-protocol-settings-wizard-form.tsx @@ -19,13 +19,13 @@ import { TestableComponentInterface } from "@wso2is/core/models"; import { URLUtils } from "@wso2is/core/utils"; import { Field, FormValue, Forms } from "@wso2is/forms"; -import { ContentLoader, Hint, LinkButton, URLInput } from "@wso2is/react-components"; +import { ContentLoader, Hint, LinkButton, Message, URLInput } from "@wso2is/react-components"; import isEmpty from "lodash-es/isEmpty"; import React, { FunctionComponent, ReactElement, useEffect, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; -import { Grid, Icon, Message } from "semantic-ui-react"; import { AppState, ConfigReducerStateInterface } from "../../../core"; +import { Grid } from "semantic-ui-react"; /** * Proptypes for the oauth protocol settings wizard form component. @@ -331,40 +331,46 @@ export const SAMLProtocolSettingsWizardForm: FunctionComponent { (assertionConsumerURLFromTemplate) && ( - - - { - - Don’t have an app? Try out a sample app - using { assertionConsumerURLFromTemplate } as - the assertion Response URL. (You can download and run a sample - at a later step.) - + + { + + Don’t have an app? Try out a sample app + using + { assertionConsumerURLFromTemplate } + as the assertion Response URL. (You can download and + run a sample at a later step.) + + } + { + (assertionConsumerUrls === undefined || + assertionConsumerUrls === "") && ( + { + e.preventDefault(); + setAssertionConsumerUrls( + assertionConsumerURLFromTemplate); + setIssuer(issuerFromTemplate); + } } + data-testid={ `${ testId }-add-now-button` } + > + + Add Now + + + ) + } + } - { - (assertionConsumerUrls === undefined || - assertionConsumerUrls === "") && ( - { - e.preventDefault(); - setAssertionConsumerUrls( - assertionConsumerURLFromTemplate); - setIssuer(issuerFromTemplate); - } } - data-testid={ `${ testId }-add-now-button` } - > - Add Now - - ) - } - - + /> ) } diff --git a/apps/console/src/features/applications/configs/ui.ts b/apps/console/src/features/applications/configs/ui.ts index 11201fc7d27..ec6b9bd36fa 100644 --- a/apps/console/src/features/applications/configs/ui.ts +++ b/apps/console/src/features/applications/configs/ui.ts @@ -40,6 +40,7 @@ import { ReactComponent as IntrospectIcon } from "../../../themes/default/assets import { ReactComponent as IssuerIcon } from "../../../themes/default/assets/images/icons/issuer.svg"; import { ReactComponent as JWKSIcon } from "../../../themes/default/assets/images/icons/jwks.svg"; import { ReactComponent as LockShieldIcon } from "../../../themes/default/assets/images/icons/lock-shield.svg"; +import { ReactComponent as MagicLinkLogo } from "../../../themes/default/assets/images/icons/magic-link-icon.svg"; import { ReactComponent as MagnifierColoredIcon } from "../../../themes/default/assets/images/icons/magnifier-colored-icon.svg"; @@ -63,7 +64,18 @@ import { ReactComponent as StartButtonIcon } from "../../../themes/default/asset import { ReactComponent as TokenIcon } from "../../../themes/default/assets/images/icons/token.svg"; import { ReactComponent as UserInfoIcon } from "../../../themes/default/assets/images/icons/userInfo.svg"; import { ReactComponent as WarningIcon } from "../../../themes/default/assets/images/icons/warning-icon.svg"; +import { + ReactComponent as FacebookLogo +} from "../../../themes/default/assets/images/identity-providers/facebook-idp-illustration.svg"; import GithubIdPIcon from "../../../themes/default/assets/images/identity-providers/github-idp-illustration.svg"; +import { + ReactComponent as GoogleLogo +} from "../../../themes/default/assets/images/identity-providers/google-idp-illustration.svg"; +import { + ReactComponent as Office365Logo +} from "../../../themes/default/assets/images/identity-providers/office-365.svg"; +import { ReactComponent as TwitterLogo } from "../../../themes/default/assets/images/identity-providers/twitter.svg"; +import { ReactComponent as YahooLogo } from "../../../themes/default/assets/images/identity-providers/yahoo.svg"; import { ReactComponent as ProtocolPredefined } from "../../../themes/default/assets/images/illustrations/application-predefined.svg"; @@ -123,13 +135,8 @@ import OpenIDLogo from "../../../themes/default/assets/images/protocols/openid.p import SamlLogo from "../../../themes/default/assets/images/protocols/saml.png"; import WSFedLogo from "../../../themes/default/assets/images/protocols/ws-fed.png"; import WSTrustLogo from "../../../themes/default/assets/images/protocols/ws-trust.png"; -import { ReactComponent as FacebookLogo } from "../../../themes/default/assets/images/social/facebook.svg"; -import { ReactComponent as GoogleLogo } from "../../../themes/default/assets/images/social/google.svg"; -import { ReactComponent as TwitterLogo } from "../../../themes/default/assets/images/social/twitter.svg"; import { ReactComponent as JWTLogo } from "../../../themes/default/assets/images/technologies/jwt-logo.svg"; import { ReactComponent as MicrosoftLogo } from "../../../themes/default/assets/images/third-party/microsoft-logo.svg"; -import { ReactComponent as Office365Logo } from "../../../themes/default/assets/images/third-party/office-365-logo.svg"; -import { ReactComponent as YahooLogo } from "../../../themes/default/assets/images/third-party/yahoo-logo.svg"; import { SupportedAuthProtocolTypes } from "../models"; export const getInboundProtocolLogos = (): { @@ -213,6 +220,7 @@ export const getAuthenticatorIcons = (): { google: FunctionComponent>; identifierFirst: FunctionComponent>; jwtBasic: FunctionComponent>; + magicLink: FunctionComponent>; microsoft: FunctionComponent>; office365: FunctionComponent>; sessionExecutor: FunctionComponent>; @@ -233,6 +241,7 @@ export const getAuthenticatorIcons = (): { google: GoogleLogo, identifierFirst: MagnifierColoredIcon, jwtBasic: JWTLogo, + magicLink: MagicLinkLogo, microsoft: MicrosoftLogo, office365: Office365Logo, sessionExecutor: ClockColoredIcon, diff --git a/apps/console/src/features/applications/constants/application-management.ts b/apps/console/src/features/applications/constants/application-management.ts index f9ace716cfe..8e398fb6ded 100644 --- a/apps/console/src/features/applications/constants/application-management.ts +++ b/apps/console/src/features/applications/constants/application-management.ts @@ -195,12 +195,14 @@ export class ApplicationManagementConstants { public static readonly AUTHORIZATION_CODE_GRANT: string = "authorization_code"; public static readonly CLIENT_CREDENTIALS_GRANT: string = "client_credentials"; public static readonly REFRESH_TOKEN_GRANT: string = "refresh_token"; + public static readonly ORGANIZATION_SWITCH_GRANT: string = "organization_switch"; public static readonly IMPLICIT_GRANT: string = "implicit"; public static readonly PASSWORD: string = "password"; public static readonly SAML2_BEARER: string = "urn:ietf:params:oauth:grant-type:saml2-bearer"; public static readonly JWT_BEARER: string = "urn:ietf:params:oauth:grant-type:jwt-bearer"; public static readonly IWA_NTLM: string = "iwa:ntlm"; public static readonly UMA_TICKET: string = "urn:ietf:params:oauth:grant-type:uma-ticket"; + public static readonly DEVICE_GRANT: string = "urn:ietf:params:oauth:grant-type:device_code"; /** * Currently refresh grant type is recommended to use at least one of below. @@ -238,7 +240,9 @@ export class ApplicationManagementConstants { ApplicationManagementConstants.IMPLICIT_GRANT, ApplicationManagementConstants.PASSWORD, ApplicationManagementConstants.CLIENT_CREDENTIALS_GRANT, - ApplicationManagementConstants.REFRESH_TOKEN_GRANT + ApplicationManagementConstants.REFRESH_TOKEN_GRANT, + ApplicationManagementConstants.ORGANIZATION_SWITCH_GRANT, + ApplicationManagementConstants.DEVICE_GRANT ] }; @@ -352,7 +356,6 @@ export class ApplicationManagementConstants { // First factor authenticators. public static readonly FIRST_FACTOR_AUTHENTICATORS = [ IdentityProviderManagementConstants.BASIC_AUTHENTICATOR, - IdentityProviderManagementConstants.IDENTIFIER_FIRST_AUTHENTICATOR, IdentityProviderManagementConstants.FIDO_AUTHENTICATOR ]; @@ -421,6 +424,8 @@ export class ApplicationManagementConstants { public static readonly CUSTOM_APPLICATION_PASSIVE_STS = "custom-application-passive-sts"; + public static readonly CUSTOM_APPLICATION = "custom-application"; + public static readonly CUSTOM_APPLICATION_PROTOCOL_ORDER: Map = new Map([ [ "oidc", 0 ], diff --git a/apps/console/src/features/applications/data/application-templates/templates/android-mobile-application/android-mobile-application.json b/apps/console/src/features/applications/data/application-templates/templates/android-mobile-application/android-mobile-application.json index 37a75f83954..96dc6bc4987 100644 --- a/apps/console/src/features/applications/data/application-templates/templates/android-mobile-application/android-mobile-application.json +++ b/apps/console/src/features/applications/data/application-templates/templates/android-mobile-application/android-mobile-application.json @@ -38,17 +38,6 @@ ], "publicClient": true } - }, - "claimConfiguration": { - "dialect": "LOCAL", - "requestedClaims": [ - { - "claim": { - "uri": "http://wso2.org/claims/username" - }, - "mandatory": true - } - ] } } } diff --git a/apps/console/src/features/applications/data/application-templates/templates/oidc-web-application/oidc-web-application.json b/apps/console/src/features/applications/data/application-templates/templates/oidc-web-application/oidc-web-application.json index 0e51ea83766..7025744521f 100644 --- a/apps/console/src/features/applications/data/application-templates/templates/oidc-web-application/oidc-web-application.json +++ b/apps/console/src/features/applications/data/application-templates/templates/oidc-web-application/oidc-web-application.json @@ -39,17 +39,6 @@ ], "publicClient": false } - }, - "claimConfiguration": { - "dialect": "LOCAL", - "requestedClaims": [ - { - "claim": { - "uri": "http://wso2.org/claims/username" - }, - "mandatory": true - } - ] } } } diff --git a/apps/console/src/features/applications/data/application-templates/templates/saml-web-application/saml-web-application.json b/apps/console/src/features/applications/data/application-templates/templates/saml-web-application/saml-web-application.json index 64b6d5f20f2..a84cccc164a 100644 --- a/apps/console/src/features/applications/data/application-templates/templates/saml-web-application/saml-web-application.json +++ b/apps/console/src/features/applications/data/application-templates/templates/saml-web-application/saml-web-application.json @@ -48,17 +48,6 @@ } } } - }, - "claimConfiguration": { - "dialect": "LOCAL", - "requestedClaims": [ - { - "claim": { - "uri": "http://wso2.org/claims/username" - }, - "mandatory": true - } - ] } } } diff --git a/apps/console/src/features/applications/data/application-templates/templates/single-page-application/single-page-application.json b/apps/console/src/features/applications/data/application-templates/templates/single-page-application/single-page-application.json index 698984a3e05..437d4c6febd 100644 --- a/apps/console/src/features/applications/data/application-templates/templates/single-page-application/single-page-application.json +++ b/apps/console/src/features/applications/data/application-templates/templates/single-page-application/single-page-application.json @@ -57,17 +57,6 @@ "renewRefreshToken": true } } - }, - "claimConfiguration": { - "dialect": "LOCAL", - "requestedClaims": [ - { - "claim": { - "uri": "http://wso2.org/claims/username" - }, - "mandatory": true - } - ] } } } diff --git a/apps/console/src/features/applications/data/application-templates/templates/windows-desktop-application/windows-desktop-application.json b/apps/console/src/features/applications/data/application-templates/templates/windows-desktop-application/windows-desktop-application.json index eb25082ab83..8c3c8c97d8b 100644 --- a/apps/console/src/features/applications/data/application-templates/templates/windows-desktop-application/windows-desktop-application.json +++ b/apps/console/src/features/applications/data/application-templates/templates/windows-desktop-application/windows-desktop-application.json @@ -42,17 +42,6 @@ "supportPlainTransformAlgorithm": true } } - }, - "claimConfiguration": { - "dialect": "LOCAL", - "requestedClaims": [ - { - "claim": { - "uri": "http://wso2.org/claims/username" - }, - "mandatory": true - } - ] } } } diff --git a/apps/console/src/features/applications/models/application.ts b/apps/console/src/features/applications/models/application.ts index 96035caf1de..d7ec54cdfd5 100644 --- a/apps/console/src/features/applications/models/application.ts +++ b/apps/console/src/features/applications/models/application.ts @@ -1,7 +1,7 @@ /** - * Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2022, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. * - * WSO2 Inc. licenses this file to you under the Apache License, + * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the License); you may not use this file except * in compliance with the License. * You may obtain a copy of the License at @@ -36,6 +36,7 @@ export interface ApplicationBasicInterface { accessUrl?: string; templateId?: string; isManagementApp?: boolean; + advancedConfigurations?: AdvancedConfigurationsInterface; } export enum ApplicationAccessTypes { @@ -206,6 +207,16 @@ export interface AdvancedConfigurationsInterface { skipLogoutConsent?: boolean; returnAuthenticatedIdpList?: boolean; enableAuthorization?: boolean; + fragment?: boolean; + additionalSpProperties?: additionalSpProperty[] +} +/** + * Interface for the additional sp properties. + */ +export interface additionalSpProperty { + name: string; + value: string; + displayName?: string; } export enum AuthenticationSequenceType { @@ -683,7 +694,9 @@ export enum LoginFlowTypes { GOOGLE_LOGIN = "GOOGLE_LOGIN", GITHUB_LOGIN = "GITHUB_LOGIN", SECOND_FACTOR_TOTP = "SECOND_FACTOR_TOTP", - PASSWORDLESS_LOGIN = "PASSWORDLESS_LOGIN", + SECOND_FACTOR_EMAIL_OTP = "SECOND_FACTOR_EMAIL_OTP", + FIDO_LOGIN = "FIDO_LOGIN", + MAGIC_LINK = "MAGIC_LINK", DEFAULT = "DEFAULT" } @@ -697,3 +710,16 @@ export enum URLFragmentTypes { TAB_INDEX = "tab=", VIEW = "view=", } + +/** + * Enum for customized tab types + */ +export enum ApplicationTabTypes { + GENERAL = "General", + PROTOCOL ="protocol", + USER_ATTRIBUTES = "user-attributes", + SIGN_IN_METHOD = "sign-in-method", + PROVISIONING = "provisioning", + ADVANCED = "advanced", + INFO = "info" +} diff --git a/apps/console/src/features/applications/pages/application-edit.tsx b/apps/console/src/features/applications/pages/application-edit.tsx index 6f5689c1ca4..45d216b2f59 100755 --- a/apps/console/src/features/applications/pages/application-edit.tsx +++ b/apps/console/src/features/applications/pages/application-edit.tsx @@ -1,7 +1,7 @@ /** - * Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2022, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. * - * WSO2 Inc. licenses this file to you under the Apache License, + * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except * in compliance with the License. * You may obtain a copy of the License at @@ -19,20 +19,16 @@ import { hasRequiredScopes, isFeatureEnabled } from "@wso2is/core/helpers"; import { AlertLevels, StorageIdentityAppsSettingsInterface, TestableComponentInterface } from "@wso2is/core/models"; import { addAlert } from "@wso2is/core/store"; -import { - AnimatedAvatar, - AppAvatar, - LabelWithPopup, - PageLayout -} from "@wso2is/react-components"; +import { AnimatedAvatar, AppAvatar, LabelWithPopup, PageLayout, PrimaryButton } from "@wso2is/react-components"; import cloneDeep from "lodash-es/cloneDeep"; import get from "lodash-es/get"; import isEmpty from "lodash-es/isEmpty"; -import React, { FunctionComponent, ReactElement, useEffect, useState } from "react"; +import React, { FunctionComponent, ReactElement, useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; import { RouteComponentProps } from "react-router"; -import { Label } from "semantic-ui-react"; +import { Label, Popup } from "semantic-ui-react"; +import { applicationConfig } from "../../../extensions/configs/application"; import { AppConstants, AppState, @@ -43,8 +39,11 @@ import { setHelpPanelDocsContentURL, toggleHelpPanelVisibility } from "../../core"; +import { getOrganizations, getSharedOrganizations } from "../../organizations/api"; +import { OrganizationInterface } from "../../organizations/models"; import { getApplicationDetails } from "../api"; -import { EditApplication } from "../components"; +import { EditApplication, InboundProtocolDefaultFallbackTemplates } from "../components"; +import { ApplicationShareModal } from "../components/modals/application-share-modal"; import { ApplicationManagementConstants } from "../constants"; import CustomApplicationTemplate from "../data/application-templates/templates/custom-application/custom-application.json"; @@ -61,7 +60,8 @@ import { ApplicationTemplateManagementUtils } from "../utils"; /** * Proptypes for the applications edit page component. */ -interface ApplicationEditPageInterface extends TestableComponentInterface, RouteComponentProps { } +interface ApplicationEditPageInterface extends TestableComponentInterface, RouteComponentProps { +} /** * Application Edit page component. @@ -86,18 +86,74 @@ const ApplicationEditPage: FunctionComponent = ( const dispatch = useDispatch(); + const appDescElement = useRef(null); + const allowedScopes: string = useSelector((state: AppState) => state?.auth?.allowedScopes); const helpPanelDocStructure: PortalDocumentationStructureInterface = useSelector( (state: AppState) => state.helpPanel.docStructure); const applicationTemplates: ApplicationTemplateListItemInterface[] = useSelector( (state: AppState) => state.application.templates); const featureConfig: FeatureConfigInterface = useSelector((state: AppState) => state.config.ui.features); + const tenantDomain: string = useSelector((state: AppState) => state.auth.tenantDomain); + const currentOrganization = useSelector((state: AppState) => state.organization.organization); const [ application, setApplication ] = useState(emptyApplication); const [ applicationTemplate, setApplicationTemplate ] = useState(undefined); const [ isApplicationRequestLoading, setApplicationRequestLoading ] = useState(false); const [ inboundProtocolList, setInboundProtocolList ] = useState(undefined); const [ inboundProtocolConfigs, setInboundProtocolConfigs ] = useState>(undefined); + const [ isDescTruncated, setIsDescTruncated ] = useState(false); + const [ showAppShareModal, setShowAppShareModal ] = useState(false); + const [ subOrganizationList, setSubOrganizationList ] = useState>([]); + const [ sharedOrganizationList, setSharedOrganizationList ] = useState>([]); + + useEffect(() => { + /** + * What's the goal of this effect? + * To figure out the application's description is truncated or not. + * + * Even though {@link useRef} calls twice, the PageLayout component doesn't render + * the passed children immediately (it will use a placeholder when it's loading), + * when that happens the relative element always returns 0 as the offset height + * and width. So, I'm relying on this boolean variable {@link isApplicationRequestLoading} + * to re-render it for the third time, so it returns correct values for figuring out + * whether the element's content is truncated or not. + * + * Please refer implementation details of {@link PageHeader}, if you check its + * heading content, you can see that it conditionally renders first. So, for us + * to correctly figure out the offset width and scroll width of the target + * element we need it to be persistently mounted inside the {@link Header} + * element. + * + * What exactly happens inside this effect? + * + * 1st Call - + * React calls this with a {@code null} value for {@link appDescElement} + * (This is expected in useRef()) + * + * 2nd Call - + * React updates the {@link appDescElement} with the target element. + * But {@link PageHeader} will immediately unmount it (because there's a request is ongoing). + * When that happens, for "some reason" we always get { offsetWidth, scrollWidth + * and all the related attributes } as zero or null. + * + * 3rd Call - + * So, whenever there's some changes to {@link isApplicationRequestLoading} + * we want React to re-update to reference so that we can accurately read the + * element's measurements (once after a successful load the {@link PageHeader} + * will try to render the component we actually pass down the tree) + * + * For more additional context please refer comment: + * {@see https://github.com/wso2/identity-apps/pull/3028#issuecomment-1123847668} + */ + if (appDescElement || isApplicationRequestLoading) { + const nativeElement = appDescElement.current; + + if (nativeElement && (nativeElement.offsetWidth < nativeElement.scrollWidth)) { + setIsDescTruncated(true); + } + } + }, [ appDescElement, isApplicationRequestLoading ]); /** * Get whether to show the help panel @@ -148,8 +204,8 @@ const ApplicationEditPage: FunctionComponent = ( if (!application || !(applicationTemplates - && applicationTemplates instanceof Array - && applicationTemplates.length > 0)) { + && applicationTemplates instanceof Array + && applicationTemplates.length > 0)) { /** * What's this? @@ -178,16 +234,40 @@ const ApplicationEditPage: FunctionComponent = ( return; } - let template = applicationTemplates.find((template) => template.id === application.templateId); + determineApplicationTemplate(); - if (application.templateId === ApplicationManagementConstants.CUSTOM_APPLICATION_OIDC - || application.templateId === ApplicationManagementConstants.CUSTOM_APPLICATION_SAML - || application.templateId === ApplicationManagementConstants.CUSTOM_APPLICATION_PASSIVE_STS) { - template = applicationTemplates.find((template) => template.id === CustomApplicationTemplate.id ); + }, [ applicationTemplates, application ]); + + useEffect(() => { + + /** + * If there's no application {@link ApplicationInterface.templateId} + * in the application instance, then we manually bind a templateId. You + * may ask why templateId is null at this point? Well, one reason + * is that, if you create an application via the API, the templateId + * is an optional property in the model instance. + * + * So, if someone creates one without it, we don't have a template + * to bootstrap the model. When that happens the edit view will not + * work properly. + * + * We have added a mapping for application's inbound protocol + * {@link InboundProtocolDefaultFallbackTemplates} to pick a default + * template if none is present. One caveat is that, if we couldn't + * find any template from the fallback mapping, we always assign + * {@link ApplicationManagementConstants.CUSTOM_APPLICATION_OIDC} to it. + * Additionally {@see InboundFormFactory}. + */ + if (!application?.templateId) { + if (application.inboundProtocols?.length > 0) { + application.templateId = InboundProtocolDefaultFallbackTemplates.get( + application.inboundProtocols[ 0 /*We pick the first*/ ].type + ) ?? ApplicationManagementConstants.CUSTOM_APPLICATION_OIDC; + determineApplicationTemplate(); + } } - setApplicationTemplate(template); - }, [ applicationTemplates, application ]); + }, [ isApplicationRequestLoading, application ]); /** * Push to 404 if application edit feature is disabled. @@ -197,7 +277,7 @@ const ApplicationEditPage: FunctionComponent = ( return; } - if(!isFeatureEnabled(featureConfig.applications, + if (!isFeatureEnabled(featureConfig.applications, ApplicationManagementConstants.FEATURE_DICTIONARY.get("APPLICATION_EDIT"))) { history.push(AppConstants.getPaths().get("PAGE_NOT_FOUND")); @@ -222,10 +302,105 @@ const ApplicationEditPage: FunctionComponent = ( dispatch( setHelpPanelDocsContentURL(editApplicationDocs[ ApplicationManagementConstants.APPLICATION_TEMPLATE_DOC_MAPPING - .get(applicationTemplate.id) ]?.[ApplicationManagementConstants.APPLICATION_DOCS_OVERVIEW]) + .get(applicationTemplate.id)]?.[ApplicationManagementConstants.APPLICATION_DOCS_OVERVIEW]) ); }, [ applicationTemplate, helpPanelDocStructure ]); + /** + * Load the list of sub organizations under the current organization & list of already shared organizations of the + * application for application sharing. + */ + useEffect(() => { + if (!showAppShareModal || !isOrganizationManagementEnabled) { + return; + } + + getOrganizations( + null, + null, + null, + null, + true, + false + ).then((response) => { + setSubOrganizationList(response.organizations); + }).catch((error) => { + if (error?.description) { + dispatch( + addAlert({ + description: error.description, + level: AlertLevels.ERROR, + message: t( + "console:manage.features.organizations.notifications." + + "getOrganizationList.error.message" + ) + }) + ); + + return; + } + + dispatch( + addAlert({ + description: t( + "console:manage.features.organizations.notifications.getOrganizationList" + + ".genericError.description" + ), + level: AlertLevels.ERROR, + message: t( + "console:manage.features.organizations.notifications." + + "getOrganizationList.genericError.message" + ) + }) + ); + }); + + getSharedOrganizations( + currentOrganization.id, + application.id + ).then((response) => { + setSharedOrganizationList(response.data.organizations); + }).catch((error) => { + if (error.response.data.description) { + dispatch( + addAlert({ + description: error.response.data.description, + level: AlertLevels.ERROR, + message: t("console:develop.features.applications.edit.sections.shareApplication" + + ".getSharedOrganizations.genericError.message") + }) + ); + + return; + } + + dispatch( + addAlert({ + description: t("console:develop.features.applications.edit.sections.shareApplication" + + ".getSharedOrganizations.genericError.description"), + level: AlertLevels.ERROR, + message: t("console:develop.features.applications.edit.sections.shareApplication" + + ".getSharedOrganizations.genericError.message") + }) + ); + } + ); + }, [ getOrganizations, showAppShareModal ]); + + const determineApplicationTemplate = () => { + + let template = applicationTemplates.find((template) => template.id === application.templateId); + + if (application.templateId === ApplicationManagementConstants.CUSTOM_APPLICATION_OIDC + || application.templateId === ApplicationManagementConstants.CUSTOM_APPLICATION_SAML + || application.templateId === ApplicationManagementConstants.CUSTOM_APPLICATION_PASSIVE_STS) { + template = applicationTemplates.find((template) => template.id === CustomApplicationTemplate.id); + } + + setApplicationTemplate(template); + + }; + /** * Retrieves application details from the API. * @@ -308,9 +483,9 @@ const ApplicationEditPage: FunctionComponent = ( } if (inboundProtocolList.length === 1 - && inboundProtocolList.includes(SupportedAuthProtocolTypes.OIDC) - && inboundProtocolConfigs - && inboundProtocolConfigs[ SupportedAuthProtocolTypes.OIDC ]) { + && inboundProtocolList.includes(SupportedAuthProtocolTypes.OIDC) + && inboundProtocolConfigs + && inboundProtocolConfigs[ SupportedAuthProtocolTypes.OIDC ]) { if (inboundProtocolConfigs[ SupportedAuthProtocolTypes.OIDC ].state === State.REVOKED) { @@ -333,6 +508,10 @@ const ApplicationEditPage: FunctionComponent = ( ); }; + const onApplicationSharingCompleted = useCallback(() => { + getApplication(application.id); + }, [ getApplication, application ]); + /** * Returns if the application is readonly or not by evaluating the `readOnly` attribute in * URL, the `access` attribute in application info response && the scope validation. @@ -349,6 +528,7 @@ const ApplicationEditPage: FunctionComponent = ( return ( { application.name } @@ -359,30 +539,46 @@ const ApplicationEditPage: FunctionComponent = ( ) } contentTopMargin={ true } description={ ( -
- { applicationTemplate?.name && } - { application.description } -
+ applicationConfig.editApplication.getOverriddenDescription(inboundProtocolConfigs?.oidc?.clientId, + tenantDomain, applicationTemplate?.name) + ?? ( +
+ { applicationTemplate?.name && ( + + ) } + { application?.description } + ) } + /> +
+ ) ) } image={ - application.imageUrl - ? ( - - ) - : ( - - ) + applicationConfig.editApplication.getOverriddenImage(inboundProtocolConfigs?.oidc?.clientId, + tenantDomain) + ?? ( + application.imageUrl + ? ( + + ) + : ( + + ) + ) } backButton={ { - "data-testid": `${ testId }-page-back-button`, + "data-testid": `${testId}-page-back-button`, onClick: handleBackButtonClick, text: t("console:develop.pages.applicationsEdit.backButton") } } @@ -391,6 +587,28 @@ const ApplicationEditPage: FunctionComponent = ( pageHeaderMaxWidth={ true } data-testid={ `${ testId }-page-layout` } truncateContent={ true } + action={ ( + <> + { + applicationConfig.editApplication.getActions(inboundProtocolConfigs?.oidc?.clientId, + tenantDomain, testId) + } + + { + (isOrganizationManagementEnabled + && applicationConfig.editApplication.showApplicationShare + && !application.advancedConfigurations?.fragment + && application.access === ApplicationAccessTypes.WRITE + && hasRequiredScopes(featureConfig?.applications, + featureConfig?.applications?.scopes?.update, allowedScopes)) && ( + setShowAppShareModal(true) }> + { t("console:develop.features.applications.edit.sections" + + ".shareApplication.shareApplication") } + + ) + } + + ) } > = ( } } readOnly={ resolveReadOnlyState() } /> + + { (showAppShareModal && application) && ( + setShowAppShareModal(false) } + onApplicationSharingCompleted={ onApplicationSharingCompleted } + /> + ) }
); }; diff --git a/apps/console/src/features/applications/pages/application-template.tsx b/apps/console/src/features/applications/pages/application-template.tsx index 377ba55787e..eaa9dd606fb 100755 --- a/apps/console/src/features/applications/pages/application-template.tsx +++ b/apps/console/src/features/applications/pages/application-template.tsx @@ -404,6 +404,7 @@ const ApplicationTemplateSelectPage: FunctionComponent = ( const [ listSortingStrategy, setListSortingStrategy ] = useState( APPLICATIONS_LIST_SORTING_OPTIONS[ 0 ] ); - const [ appList, setAppList ] = useState({}); const [ listOffset, setListOffset ] = useState(0); const [ listItemLimit, setListItemLimit ] = useState(UIConstants.DEFAULT_RESOURCE_LIST_ITEM_LIMIT); - const [ isApplicationListRequestLoading, setApplicationListRequestLoading ] = useState(false); - const [ isLoading, setLoading ] = useState(true); const [ triggerClearQuery, setTriggerClearQuery ] = useState(false); const [ showWizard, setShowWizard ] = useState(false); - const [ isApplicationsNextPageAvailable, setIsApplicationsNextPageAvailable ] = useState(undefined); const config: ConfigReducerStateInterface = useSelector((state: AppState) => state.config); - const consumerAccountURL: string = useSelector((state: AppState) => + const consumerAccountURL: string = useSelector((state: AppState) => state?.config?.deployment?.accountApp?.tenantQualifiedPath); + const [ isLoadingForTheFirstTime, setIsLoadingForTheFirstTime ] = useState(true); const eventPublisher: EventPublisher = EventPublisher.getInstance(); + const { + data: applicationList, + isLoading: isApplicationListFetchRequestLoading, + error: applicationListFetchRequestError, + mutate: mutateApplicationListFetchRequest + } = useApplicationList("advancedConfigurations,templateId", listItemLimit, listOffset, searchQuery); + /** - * Called on every `listOffset` & `listItemLimit` change. + * Sets the initial spinner. + * TODO: Remove this once the loaders are finalized. */ useEffect(() => { - if(searchQuery) { - getAppLists(listItemLimit, listOffset, searchQuery); - } else { - getAppLists(listItemLimit, listOffset, null); - } - }, [ listOffset, listItemLimit ]); + if (isApplicationListFetchRequestLoading === false && isLoadingForTheFirstTime === true) { + setIsLoadingForTheFirstTime(false); + } + }, [ isApplicationListFetchRequestLoading, isLoadingForTheFirstTime ]); /** - * Retrieves the list of applications. - * - * @param {number} limit - List limit. - * @param {number} offset - List offset. - * @param {string} filter - Search query. + * Handles the application list fetch request error. */ - const getAppLists = (limit: number, offset: number, filter: string): void => { - setApplicationListRequestLoading(true); - - getApplicationList(limit, offset, filter) - .then((response) => { - handleNextButtonVisibility(response); - setAppList(response); - }) - .catch((error) => { - if (error.response && error.response.data && error.response.data.description) { - dispatch(addAlert({ - description: error.response.data.description, - level: AlertLevels.ERROR, - message: t("console:develop.features.applications.notifications." + - "fetchApplications.error.message") - })); - - return; - } - - dispatch(addAlert({ - description: t("console:develop.features.applications.notifications.fetchApplications" + - ".genericError.description"), - level: AlertLevels.ERROR, - message: t("console:develop.features.applications.notifications." + - "fetchApplications.genericError.message") - })); - }) - .finally(() => { - setApplicationListRequestLoading(false); - setLoading(false); - }); - }; + useEffect(() => { + + if (!applicationListFetchRequestError) { + return; + } + + if (applicationListFetchRequestError.response + && applicationListFetchRequestError.response.data + && applicationListFetchRequestError.response.data.description) { + dispatch(addAlert({ + description: applicationListFetchRequestError.response.data.description, + level: AlertLevels.ERROR, + message: t("console:develop.features.applications.notifications." + + "fetchApplications.error.message") + })); + + return; + } + + dispatch(addAlert({ + description: t("console:develop.features.applications.notifications.fetchApplications" + + ".genericError.description"), + level: AlertLevels.ERROR, + message: t("console:develop.features.applications.notifications." + + "fetchApplications.genericError.message") + })); + }, [ applicationListFetchRequestError ]); /** * Sets the list sorting strategy. @@ -206,18 +201,18 @@ const ApplicationsPage: FunctionComponent = ( }; /** - * - * Sets the Next button visibility. + * Checks if `Next` page nav button should be shown. * * @param appList - List of applications. + * @returns {boolean} - `true` if `Next` page nav button should be shown. */ - const handleNextButtonVisibility = (appList: ApplicationListInterface): void => { + const shouldShowNextPageNavigation = (appList: ApplicationListInterface): boolean => { - if (appList.startIndex + appList.count === appList.totalResults + 1) { - setIsApplicationsNextPageAvailable(false); - } else { - setIsApplicationsNextPageAvailable(true); + if (appList?.startIndex + appList?.count === appList?.totalResults + 1) { + return false; } + + return true; }; /** @@ -228,7 +223,6 @@ const ApplicationsPage: FunctionComponent = ( */ const handleApplicationFilter = (query: string): void => { setSearchQuery(query); - getAppLists(listItemLimit, listOffset, query); }; /** @@ -256,7 +250,7 @@ const ApplicationsPage: FunctionComponent = ( * Handles application delete action. */ const handleApplicationDelete = (): void => { - getAppLists(listItemLimit, listOffset, null); + mutateApplicationListFetchRequest(); }; /** @@ -264,7 +258,6 @@ const ApplicationsPage: FunctionComponent = ( */ const handleSearchQueryClear = (): void => { setSearchQuery(""); - getAppLists(listItemLimit, listOffset, null); setTriggerClearQuery(!triggerClearQuery); }; @@ -291,7 +284,8 @@ const ApplicationsPage: FunctionComponent = ( * @return {React.ReactElement} */ const renderTenantedMyAccountLink = (): ReactElement => { - if (AppConstants.getTenant() === AppConstants.getSuperTenant()) { + if (AppConstants.getTenant() === AppConstants.getSuperTenant() || + !applicationConfig.advancedConfigurations.showMyAccount) { return null; } @@ -303,12 +297,13 @@ const ApplicationsPage: FunctionComponent = ( - = ( transparent /> { t("console:develop.features.applications.myaccount.title") } + { t("console:develop.features.applications.myaccount.description") } + + { t("common:learnMore") } + = ( return ( { eventPublisher.publish("application-click-new-application-button"); history.push(AppConstants.getPaths().get("APPLICATION_TEMPLATES")); @@ -389,10 +393,10 @@ const ApplicationsPage: FunctionComponent = ( contentTopMargin={ (AppConstants.getTenant() === AppConstants.getSuperTenant()) } data-testid={ `${ testId }-page-layout` } > - { !isLoading? ( + { !isLoadingForTheFirstTime? ( <> { renderTenantedMyAccountLink() } - { renderRemoteFetchStatus() } + { /* renderRemoteFetchStatus() */ } = ( key: 0, text: t("common:name"), value: "name" + }, + { + key: 1, + text: "ClientId", + value: "clientId" } ] } filterAttributePlaceholder={ @@ -419,25 +428,26 @@ const ApplicationsPage: FunctionComponent = ( placeholder={ t("console:develop.features.applications.advancedSearch.placeholder") } defaultSearchAttribute="name" defaultSearchOperator="co" + predefinedDefaultSearchStrategy="name co %search-value% or clientId co %search-value%" triggerClearQuery={ triggerClearQuery } data-testid={ `${ testId }-list-advanced-search` } /> ) } - currentListSize={ appList.count } + currentListSize={ applicationList?.count } listItemLimit={ listItemLimit } onItemsPerPageDropdownChange={ handleItemsPerPageDropdownChange } onPageChange={ handlePaginationChange } onSortStrategyChange={ handleListSortingStrategyOnChange } showPagination={ true } - showTopActionPanel={ - isApplicationListRequestLoading - || !(!searchQuery && appList?.totalResults <= 0) } + showTopActionPanel={ + isApplicationListFetchRequestLoading + || !(!searchQuery && applicationList?.totalResults <= 0) } sortOptions={ APPLICATIONS_LIST_SORTING_OPTIONS } sortStrategy={ listSortingStrategy } - totalPages={ Math.ceil(appList.totalResults / listItemLimit) } - totalListSize={ appList.totalResults } + totalPages={ Math.ceil(applicationList?.totalResults / listItemLimit) } + totalListSize={ applicationList?.totalResults } paginationOptions={ { - disableNextButton: !isApplicationsNextPageAvailable + disableNextButton: !shouldShowNextPageNavigation(applicationList) } } data-testid={ `${ testId }-list-layout` } > @@ -450,6 +460,11 @@ const ApplicationsPage: FunctionComponent = ( key: 0, text: t("common:name"), value: "name" + }, + { + key: 1, + text: "ClientId", + value: "clientId" } ] } filterAttributePlaceholder={ @@ -469,13 +484,16 @@ const ApplicationsPage: FunctionComponent = ( } defaultSearchAttribute="name" defaultSearchOperator="co" + predefinedDefaultSearchStrategy={ + "name co %search-value% or clientId co %search-value%" + } triggerClearQuery={ triggerClearQuery } data-testid={ `${ testId }-list-advanced-search` } /> ) } featureConfig={ featureConfig } - isLoading={ isApplicationListRequestLoading } - list={ appList } + isLoading={ isApplicationListFetchRequestLoading } + list={ applicationList } onApplicationDelete={ handleApplicationDelete } onEmptyListPlaceholderActionClick={ () => { @@ -507,7 +525,7 @@ const ApplicationsPage: FunctionComponent = ( ) : ( ) } diff --git a/apps/console/src/features/applications/utils/adaptive-script-utils.ts b/apps/console/src/features/applications/utils/adaptive-script-utils.ts index 41967695283..6de35da39d1 100644 --- a/apps/console/src/features/applications/utils/adaptive-script-utils.ts +++ b/apps/console/src/features/applications/utils/adaptive-script-utils.ts @@ -84,16 +84,23 @@ export class AdaptiveScriptUtils { + scriptBody + ApplicationManagementConstants.DEFAULT_ADAPTIVE_AUTH_SCRIPT_FOOTER; - return AdaptiveScriptUtils.minifyScript(moderatedScript) === AdaptiveScriptUtils.minifyScript(scriptComposed); + const userDefined: string = AdaptiveScriptUtils.minifyScript(moderatedScript, false); + const defaultScript: string = AdaptiveScriptUtils.minifyScript(scriptComposed, false); + + return userDefined === defaultScript; } /** * Strips spaces and new lines in the script. * * @param {string | string[]} originalScript - Original script. - * @return {string} + * @param {boolean} ignoreComments Whether to ignore code comments. + * @return {string} Minified string. */ - public static minifyScript(originalScript: string | string[]): string { + public static minifyScript( + originalScript: string | string[], + ignoreComments: boolean = true + ): string { if (!originalScript) return ApplicationManagementConstants.EMPTY_STRING; @@ -122,11 +129,21 @@ export class AdaptiveScriptUtils { */ const comments = /\/\*[\s\S]*?\*\/|\/\/.*/gm; - return script - .replace(comments, ApplicationManagementConstants.EMPTY_STRING) + let minimized = script; + + if (ignoreComments) + minimized = minimized.replace( + comments, + ApplicationManagementConstants.EMPTY_STRING + ); + + minimized = minimized .replace(/(?:\r\n|\r|\n)/g, ApplicationManagementConstants.EMPTY_STRING) .replace(/\s/g, ApplicationManagementConstants.EMPTY_STRING) .trim(); + + return minimized; + } public static isEmptyScript(script: string | string[]): boolean { diff --git a/apps/console/src/features/applications/utils/application-management-utils.ts b/apps/console/src/features/applications/utils/application-management-utils.ts index 8dfc6f6e29a..675afaced88 100644 --- a/apps/console/src/features/applications/utils/application-management-utils.ts +++ b/apps/console/src/features/applications/utils/application-management-utils.ts @@ -23,6 +23,7 @@ import { I18n } from "@wso2is/i18n"; import camelCase from "lodash-es/camelCase"; import intersectionBy from "lodash-es/intersectionBy"; import unionBy from "lodash-es/unionBy"; +import { FunctionComponent, SVGProps } from "react"; import { DocPanelUICardInterface, store } from "../../core"; import { getAvailableInboundProtocols, @@ -49,7 +50,6 @@ import { setOIDCApplicationConfigs, setSAMLApplicationConfigs } from "../store"; -import { FunctionComponent, SVGProps } from "react"; /** * Utility class for application(service provider) operations. @@ -399,4 +399,20 @@ export class ApplicationManagementUtils { return SAMLConfigurationDisplayNames[mode]; } + + public static mapProtocolTypeToName(type: string): string { + let protocolName = type; + + if (protocolName === "oauth2") { + protocolName = SupportedAuthProtocolTypes.OIDC; + } else if (protocolName === "passivests") { + protocolName = SupportedAuthProtocolTypes.WS_FEDERATION; + } else if (protocolName === "wstrust") { + protocolName = SupportedAuthProtocolTypes.WS_TRUST; + } else if (protocolName === "samlsso") { + protocolName = SupportedAuthProtocolTypes.SAML; + } + + return protocolName; + } } diff --git a/apps/console/src/features/applications/utils/application-template-management-utils.ts b/apps/console/src/features/applications/utils/application-template-management-utils.ts index b2e8fca1159..4379091a1ea 100644 --- a/apps/console/src/features/applications/utils/application-template-management-utils.ts +++ b/apps/console/src/features/applications/utils/application-template-management-utils.ts @@ -22,6 +22,7 @@ import { addAlert } from "@wso2is/core/store"; import { I18n } from "@wso2is/i18n"; import { TemplateCardTagInterface } from "@wso2is/react-components"; import groupBy from "lodash-es/groupBy"; +import isObject from "lodash-es/isObject"; import startCase from "lodash-es/startCase"; import { getTechnologyLogos } from "../../core/configs"; import { store } from "../../core/store"; @@ -189,7 +190,22 @@ export class ApplicationTemplateManagementUtils { * @return {TemplateCardTagInterface[]} Set of Technologies compatible for `TemplateCard`. */ public static buildSupportedTechnologies(technologies: string[]): TemplateCardTagInterface[] { - return technologies?.map((technology: string) => { + + const _technologies = technologies?.map((technology: string) => { + + // If the technology is already resolved, return that istead of trying to resolve again. + if (typeof technology !== "string") { + if (isObject(technology) + && Object.prototype.hasOwnProperty.call(technology, "displayName") + && Object.prototype.hasOwnProperty.call(technology, "logo") + && Object.prototype.hasOwnProperty.call(technology, "name")) { + + return technology; + } + + return null; + } + let logo = null; for (const [ key, value ] of Object.entries(getTechnologyLogos())) { @@ -206,6 +222,8 @@ export class ApplicationTemplateManagementUtils { name: technology }; }); + + return _technologies.filter(Boolean); } /** diff --git a/apps/console/src/features/applications/utils/sign-in-method-utils.ts b/apps/console/src/features/applications/utils/sign-in-method-utils.ts index 5cb84f056b5..e4d998eb8da 100644 --- a/apps/console/src/features/applications/utils/sign-in-method-utils.ts +++ b/apps/console/src/features/applications/utils/sign-in-method-utils.ts @@ -19,7 +19,6 @@ import flatten from "lodash-es/flatten"; import { - AuthenticatorCategories, GenericAuthenticatorInterface, IdentityProviderManagementConstants, ProvisioningInterface @@ -209,6 +208,51 @@ export class SignInMethodUtils { leftSideSteps); } + /** + * This method decides if the magic-link authenticator can be added to the current step. + * + * @param {number} currentStep The current step. + * @param {AuthenticationStepInterface} authenticationSteps The authentication steps. + * + * @returns {boolean} + */ + public static isMagicLinkAuthenticatorValid(currentStep: number, + authenticationSteps: AuthenticationStepInterface[]): boolean { + // The magic link authenticator can only be added to the second step. + if (currentStep !== 1) { + return false; + } + + const identifierFirst = authenticationSteps[ 0 ].options.find( + authenticator => + authenticator.authenticator === IdentityProviderManagementConstants.IDENTIFIER_FIRST_AUTHENTICATOR); + + // The first step should have the identifier first authenticator. + if (authenticationSteps.length > 1 && !identifierFirst) { + return false; + } + + return true; + } + + /** + * Checks if a identifier first or basic auth already exists. Returns false if it does or tru otherwise. + * + * @param {number} currentStep The current step. + * @param {AuthenticationStepInterface} authenticationSteps The authentication steps. + * + * @returns {boolean} + */ + public static isFirstFactorValid(currentStep: number, authenticationSteps: AuthenticationStepInterface[]): boolean { + const firstFactor = authenticationSteps[currentStep].options.find( + (authenticator) => + authenticator.authenticator === IdentityProviderManagementConstants.IDENTIFIER_FIRST_AUTHENTICATOR || + authenticator.authenticator === IdentityProviderManagementConstants.BASIC_AUTHENTICATOR + ); + + return !firstFactor; + } + public static isConnectionsJITUPConflictWithMFA( args: ConnectionsJITUPConflictWithMFAArgs ): ConnectionsJITUPConflictWithMFAReturnValue { diff --git a/apps/console/src/features/authentication/store/actions/authenticate.ts b/apps/console/src/features/authentication/store/actions/authenticate.ts index f030bc668e6..715478a739b 100644 --- a/apps/console/src/features/authentication/store/actions/authenticate.ts +++ b/apps/console/src/features/authentication/store/actions/authenticate.ts @@ -16,7 +16,7 @@ * under the License. */ -import { getProfileInfo, getProfileSchemas } from "@wso2is/core/api"; +import { AsgardeoSPAClient, DecodedIDTokenPayload } from "@asgardeo/auth-react"; import { IdentityAppsApiException } from "@wso2is/core/exceptions"; import { AlertInterface, @@ -34,9 +34,10 @@ import { import { I18n } from "@wso2is/i18n"; import isEmpty from "lodash-es/isEmpty"; import { Dispatch } from "redux"; +import { commonConfig } from "../../../../extensions"; import { Config } from "../../../core/configs"; import { store } from "../../../core/store"; -import { commonConfig } from "../../../../extensions"; +import { getProfileInfo, getProfileSchemas } from "../../../users/api"; /** * Gets profile information by making an API call @@ -48,92 +49,116 @@ export const getProfileInformation = ( dispatch(setProfileInfoRequestLoadingStatus(true)); - // Get the profile info. - // TODO: Add the function to handle SCIM disabled error. - getProfileInfo(meEndpoint, clientOrigin, null) - .then((infoResponse: ProfileInfoInterface) => { - if (infoResponse.responseStatus !== 200) { - dispatch( - addAlert({ - description: I18n.instance.t( - "console:manage.notifications.getProfileInfo.genericError.description" - ), - level: AlertLevels.ERROR, - message: I18n.instance.t("console:manage.notifications.getProfileInfo.genericError.message") - }) - ); + const getProfileInfoFromToken: boolean = store.getState().auth.isPrivilegedUser || + (window[ "AppUtils" ].getConfig().getProfileInfoFromIDToken ?? false); - return; - } - - dispatch(setProfileInfo(infoResponse)); - - commonConfig.hotjarTracking.tagAttributes(); - - // If the schemas in the redux store is empty, fetch the SCIM schemas from the API. - if (isEmpty(store.getState().profile.profileSchemas)) { - dispatch(setProfileSchemaRequestLoadingStatus(true)); - - getProfileSchemas() - .then((response: ProfileSchemaInterface[]) => { - dispatch(setSCIMSchemas(response)); - }) - .catch((error: IdentityAppsApiException) => { - if (error?.response?.data?.description) { - dispatch( - addAlert({ - description: error.response.data.description, - level: AlertLevels.ERROR, - message: I18n.instance.t("console:manage.notifications.getProfileSchema." + - "error.message") - }) - ); - } + const getProfileSchema = (): void => { + // If the schemas in the redux store is empty, fetch the SCIM schemas from the API. + if (isEmpty(store.getState().profile.profileSchemas)) { + dispatch(setProfileSchemaRequestLoadingStatus(true)); + getProfileSchemas() + .then((response: ProfileSchemaInterface[]) => { + dispatch(setSCIMSchemas(response)); + }) + .catch((error: IdentityAppsApiException) => { + if (error?.response?.data?.description) { dispatch( addAlert({ - description: I18n.instance.t( - "console:manage.notifications.getProfileSchema.genericError.description" - ), + description: error.response.data.description, level: AlertLevels.ERROR, - message: I18n.instance.t( - "console:manage.notifications.getProfileSchema.genericError.message" - ) + message: I18n.instance.t("console:manage.notifications.getProfileSchema." + + "error.message") }) ); - }) - .finally(() => { - dispatch(setProfileSchemaRequestLoadingStatus(false)); - }); - } + } + + dispatch( + addAlert({ + description: I18n.instance.t( + "console:manage.notifications.getProfileSchema.genericError.description" + ), + level: AlertLevels.ERROR, + message: I18n.instance.t( + "console:manage.notifications.getProfileSchema.genericError.message" + ) + }) + ); + }) + .finally(() => { + dispatch(setProfileSchemaRequestLoadingStatus(false)); + }); + } + }; + + if (getProfileInfoFromToken && meEndpoint.includes("scim2/Me")) { + AsgardeoSPAClient.getInstance().getDecodedIDToken().then((decodedToken: DecodedIDTokenPayload) => { + const profileInfo: ProfileInfoInterface = { + emails: [ decodedToken.email ] ?? [], + id: decodedToken.sub, + name: { + familyName: decodedToken.family_name ?? "", + givenName: decodedToken.given_name ?? "" + }, + profileUrl: "", + userName: decodedToken.username + }; + + dispatch(setProfileInfo(profileInfo)); + dispatch(setProfileInfoRequestLoadingStatus(false)); + getProfileSchema(); + }); + } else { + // Get the profile info. + // TODO: Add the function to handle SCIM disabled error. + getProfileInfo(meEndpoint, clientOrigin, null) + .then((infoResponse: ProfileInfoInterface) => { + if (infoResponse.responseStatus !== 200) { + dispatch( + addAlert({ + description: I18n.instance.t( + "console:manage.notifications.getProfileInfo.genericError.description" + ), + level: AlertLevels.ERROR, + message: I18n.instance.t("console:manage.notifications.getProfileInfo.genericError.message") + }) + ); + + return; + } + dispatch(setProfileInfo(infoResponse)); + commonConfig.hotjarTracking.tagAttributes(); + getProfileSchema(); + + return; + }) + .catch((error: IdentityAppsApiException) => { + if (error.response && error.response.data && error.response.data.detail) { + dispatch( + addAlert({ + description: I18n.instance.t( + "console:manage.notifications.getProfileInfo.error.description", { + description: error.response.data.detail + } ), + level: AlertLevels.ERROR, + message: I18n.instance.t("console:manage.notifications.getProfileInfo.error.message") + }) + ); + + return; + } - return; - }) - .catch((error: IdentityAppsApiException) => { - if (error.response && error.response.data && error.response.data.detail) { dispatch( addAlert({ - description: I18n.instance.t("console:manage.notifications.getProfileInfo.error.description", { - description: error.response.data.detail - }), + description: I18n.instance.t("console:manage.notifications.getProfileInfo.genericError." + + "description"), level: AlertLevels.ERROR, - message: I18n.instance.t("console:manage.notifications.getProfileInfo.error.message") + message: I18n.instance.t("console:manage.notifications.getProfileInfo.genericError.message") }) ); - - return; - } - - dispatch( - addAlert({ - description: I18n.instance.t("console:manage.notifications.getProfileInfo.genericError." + - "description"), - level: AlertLevels.ERROR, - message: I18n.instance.t("console:manage.notifications.getProfileInfo.genericError.message") - }) - ); - }) - .finally(() => { - dispatch(setProfileInfoRequestLoadingStatus(false)); - }); + }) + .finally(() => { + dispatch(setProfileInfoRequestLoadingStatus(false)); + }); + } }; diff --git a/apps/console/src/features/authentication/utils/authenticate-utils.ts b/apps/console/src/features/authentication/utils/authenticate-utils.ts index e9669292052..5dda1ba81f6 100644 --- a/apps/console/src/features/authentication/utils/authenticate-utils.ts +++ b/apps/console/src/features/authentication/utils/authenticate-utils.ts @@ -17,7 +17,7 @@ */ import { AuthReactConfig, ResponseMode, Storage } from "@asgardeo/auth-react"; -import { TokenConstants } from "@wso2is/core/constants"; +import { AppConstants, TokenConstants } from "@wso2is/core/constants"; import UAParser from "ua-parser-js"; /** @@ -39,33 +39,36 @@ export class AuthenticateUtils { // eslint-disable-next-line @typescript-eslint/no-empty-function private constructor() {} - public static initializeConfig: AuthReactConfig = { - baseUrl: - window["AppUtils"].getConfig().idpConfigs?.serverOrigin ?? - window[ "AppUtils" ].getConfig().idpConfigs.serverOrigin, - checkSessionInterval: window[ "AppUtils" ].getConfig()?.session?.checkSessionInterval, - clientHost: window["AppUtils"].getConfig().clientOriginWithTenant, - clientID: window["AppUtils"].getConfig().clientID, - clockTolerance: window[ "AppUtils" ].getConfig().idpConfigs?.clockTolerance, - disableTrySignInSilently: new URL(location.href).searchParams.get("disable_silent_sign_in") === "true", - enableOIDCSessionManagement: true, - enablePKCE: window["AppUtils"].getConfig().idpConfigs?.enablePKCE ?? true, - endpoints: { - authorizationEndpoint: window["AppUtils"].getConfig().idpConfigs?.authorizeEndpointURL, - checkSessionIframe: window["AppUtils"].getConfig().idpConfigs?.oidcSessionIFrameEndpointURL, - endSessionEndpoint: window["AppUtils"].getConfig().idpConfigs?.logoutEndpointURL, - jwksUri: window["AppUtils"].getConfig().idpConfigs?.jwksEndpointURL, - revocationEndpoint: window["AppUtils"].getConfig().idpConfigs?.tokenRevocationEndpointURL, - tokenEndpoint: window["AppUtils"].getConfig().idpConfigs?.tokenEndpointURL - }, - resourceServerURLs: AuthenticateUtils.resolveBaseUrls(), - responseMode: window["AppUtils"].getConfig().idpConfigs?.responseMode ?? responseModeFallback, - scope: window["AppUtils"].getConfig().idpConfigs?.scope ?? [TokenConstants.SYSTEM_SCOPE], - sendCookiesInRequests: true, - sessionRefreshInterval: window[ "AppUtils" ].getConfig()?.session?.sessionRefreshTimeOut, - signInRedirectURL: window["AppUtils"].getConfig().loginCallbackURL, - signOutRedirectURL: window["AppUtils"].getConfig().loginCallbackURL, - storage: AuthenticateUtils.resolveStorage() as Storage.WebWorker + public static getInitializeConfig = (): AuthReactConfig => { + + return { + baseUrl: + window["AppUtils"]?.getConfig()?.idpConfigs?.serverOrigin ?? + window[ "AppUtils" ]?.getConfig()?.idpConfigs.serverOrigin, + checkSessionInterval: window[ "AppUtils" ]?.getConfig()?.session?.checkSessionInterval, + clientHost: window["AppUtils"]?.getConfig()?.clientOriginWithTenant, + clientID: window["AppUtils"]?.getConfig()?.clientID, + clockTolerance: window[ "AppUtils" ]?.getConfig().idpConfigs?.clockTolerance, + disableTrySignInSilently: new URL(location.href).searchParams.get("disable_silent_sign_in") === "true", + enableOIDCSessionManagement: true, + enablePKCE: window["AppUtils"]?.getConfig()?.idpConfigs?.enablePKCE ?? true, + endpoints: { + authorizationEndpoint: window["AppUtils"]?.getConfig()?.idpConfigs?.authorizeEndpointURL, + checkSessionIframe: window["AppUtils"]?.getConfig()?.idpConfigs?.oidcSessionIFrameEndpointURL, + endSessionEndpoint: window["AppUtils"]?.getConfig()?.idpConfigs?.logoutEndpointURL, + jwksUri: window["AppUtils"]?.getConfig()?.idpConfigs?.jwksEndpointURL, + revocationEndpoint: window["AppUtils"]?.getConfig()?.idpConfigs?.tokenRevocationEndpointURL, + tokenEndpoint: window["AppUtils"]?.getConfig()?.idpConfigs?.tokenEndpointURL + }, + resourceServerURLs: AuthenticateUtils.resolveBaseUrls(), + responseMode: window["AppUtils"]?.getConfig()?.idpConfigs?.responseMode ?? responseModeFallback, + scope: window["AppUtils"]?.getConfig()?.idpConfigs?.scope ?? [ TokenConstants.SYSTEM_SCOPE ], + sendCookiesInRequests: true, + sessionRefreshInterval: window[ "AppUtils" ]?.getConfig()?.session?.sessionRefreshTimeOut, + signInRedirectURL: window["AppUtils"]?.getConfig()?.loginCallbackURL, + signOutRedirectURL: window["AppUtils"]?.getConfig()?.loginCallbackURL, + storage: AuthenticateUtils.resolveStorage() as Storage.WebWorker + }; }; /** @@ -74,13 +77,17 @@ export class AuthenticateUtils { * @returns {Storage} */ public static resolveStorage(): Storage { - const storageFallback: Storage = - new UAParser().getBrowser().name === "IE" ? Storage.SessionStorage : Storage.WebWorker; - if (window["AppUtils"].getConfig().idpConfigs?.storage) { + const activeBrowser: string = new UAParser()?.getBrowser()?.name; + + const storageFallback: Storage = AppConstants.WEB_WORKER_UNSUPPORTED_AGENTS.includes(activeBrowser) + ? Storage.SessionStorage + : Storage.WebWorker; + + if (window["AppUtils"]?.getConfig()?.idpConfigs?.storage) { if ( - window["AppUtils"].getConfig().idpConfigs?.storage === Storage.WebWorker && - new UAParser().getBrowser().name === "IE" + window["AppUtils"].getConfig().idpConfigs.storage === Storage.WebWorker && + storageFallback !== Storage.WebWorker ) { return Storage.SessionStorage; } @@ -99,8 +106,8 @@ export class AuthenticateUtils { * @return {string[]} */ public static resolveBaseUrls(): string[] { - let baseUrls = window["AppUtils"].getConfig().idpConfigs?.baseUrls; - const serverOrigin = window["AppUtils"].getConfig().serverOrigin; + let baseUrls = window["AppUtils"]?.getConfig()?.idpConfigs?.baseUrls; + const serverOrigin = window["AppUtils"]?.getConfig()?.serverOrigin; if (baseUrls) { // If the server origin is not specified in the overridden config, append it. @@ -111,7 +118,7 @@ export class AuthenticateUtils { return baseUrls; } - return [serverOrigin]; + return [ serverOrigin ]; } /** @@ -127,7 +134,7 @@ export class AuthenticateUtils { // If the override URL & original URL has search params, try to moderate the URL. if (parsedOverrideURL.search && parsedOriginalURL.search) { - for (const [key, value] of parsedOriginalURL.searchParams.entries()) { + for (const [ key, value ] of parsedOriginalURL.searchParams.entries()) { if (!parsedOverrideURL.searchParams.has(key)) { parsedOverrideURL.searchParams.append(key, value); } diff --git a/apps/console/src/features/certificates/components/certificates-list.tsx b/apps/console/src/features/certificates/components/certificates-list.tsx index 41bd75a1666..901ec589c63 100644 --- a/apps/console/src/features/certificates/components/certificates-list.tsx +++ b/apps/console/src/features/certificates/components/certificates-list.tsx @@ -247,18 +247,19 @@ export const CertificatesList: FunctionComponent const showDeleteConfirm = (): ReactElement => { const isTenantCertificate: boolean = decodeCertificate(deleteCertificatePem) .serialNumber === tenantCertificate; + return ( + (

Please type { { id:deleteID } } to confirm. -

+

) } assertionType={ isTenantCertificate ? "input" : null } primaryAction={ t("console:manage.features.certificates.keystore.confirmation.primaryAction") } @@ -301,7 +302,7 @@ export const CertificatesList: FunctionComponent <> { t("console:manage.features.certificates.keystore.confirmation.message") } @@ -336,6 +337,7 @@ export const CertificatesList: FunctionComponent const hex = certificate.hex; const byteArray = new Uint8Array(hex.length / 2); + for (let x = 0; x < byteArray.length; x++) { byteArray[ x ] = parseInt(hex.substr(x * 2, 2), 16); } @@ -438,12 +440,12 @@ export const CertificatesList: FunctionComponent />
View Certificate - { - certificateDisplay?.alias - ? certificateDisplay?.alias - : certificateDisplay?.issuerDN && ( - CertificateManagementUtils.searchIssuerDNAlias(certificateDisplay?.issuerDN) - ) - } + certificateDisplay?.alias + ? certificateDisplay?.alias + : certificateDisplay?.issuerDN && ( + CertificateManagementUtils.searchIssuerDNAlias(certificateDisplay?.issuerDN) + ) + }

Serial Number: { certificateDisplay?.serialNumber }
@@ -592,7 +594,7 @@ export const CertificatesList: FunctionComponent ?? t("console:manage.features.certificates.keystore.notifications.getAlias." + "genericError.message") })); - }); + }); return; } @@ -695,6 +697,7 @@ export const CertificatesList: FunctionComponent // Checks whether the alias of this certificate matches the tenant domain // or the authenticated user's tenant domain. const isTenant = tenantDomain === alias || authTenantDomain === alias; + return !(type === KEYSTORE && hasScopes) || isSuper || isTenant; }, icon: (): SemanticICONS => "trash alternate", diff --git a/apps/console/src/features/certificates/configs/ui.ts b/apps/console/src/features/certificates/configs/ui.ts index 1d032afbb18..3f63f1384f1 100644 --- a/apps/console/src/features/certificates/configs/ui.ts +++ b/apps/console/src/features/certificates/configs/ui.ts @@ -16,6 +16,7 @@ * under the License. */ +import { FunctionComponent, SVGProps } from "react"; import { ReactComponent as CertificateAvatar } from "../../../themes/default/assets/images/icons/certificate-avatar.svg"; @@ -27,7 +28,13 @@ import { } from "../../../themes/default/assets/images/illustrations/certificate.svg"; import { ReactComponent as CertificateRibbon } from "../../../themes/default/assets/images/illustrations/ribbon.svg"; -export const getCertificateIllustrations = () => { +export const getCertificateIllustrations = (): { + avatar: FunctionComponent>; + badge: FunctionComponent>; + file: FunctionComponent>; + ribbon: FunctionComponent>; + uploadPlaceholder: FunctionComponent>; +} => { return { avatar: CertificateAvatar, @@ -38,7 +45,9 @@ export const getCertificateIllustrations = () => { }; }; -export const getImportCertificateWizardStepIcons = () => { +export const getImportCertificateWizardStepIcons = (): { + general: FunctionComponent> , +} => { return { general: DocumentIcon diff --git a/apps/console/src/features/certificates/pages/certificates-keystore.tsx b/apps/console/src/features/certificates/pages/certificates-keystore.tsx index e93bb8aface..ee2caab21bf 100644 --- a/apps/console/src/features/certificates/pages/certificates-keystore.tsx +++ b/apps/console/src/features/certificates/pages/certificates-keystore.tsx @@ -241,12 +241,13 @@ const CertificatesKeystore: FunctionComponent } isLoading={ isLoading } title={ t("console:manage.features.certificates.keystore.pageLayout.title") } + pageTitle={ t("console:manage.features.certificates.keystore.pageLayout.title") } description={ t("console:manage.features.certificates.keystore.pageLayout.description") } data-testid={ `${ testId }-page-layout` } > defaultSearchOperator="co" triggerClearQuery={ triggerClearQuery } data-testid={ `${ testId }-advanced-search` } - /> + />) } currentListSize={ listItemLimit } listItemLimit={ listItemLimit } @@ -294,7 +295,7 @@ const CertificatesKeystore: FunctionComponent > defaultSearchOperator="co" triggerClearQuery={ triggerClearQuery } data-testid={ `${ testId }-advanced-search` } - /> + />) } isLoading={ isLoading } list={ paginate(filteredCertificatesKeystore, listItemLimit, offset) } diff --git a/apps/console/src/features/certificates/pages/certificates-truststore.tsx b/apps/console/src/features/certificates/pages/certificates-truststore.tsx index f1d9b7f97f4..dbae3e342d1 100644 --- a/apps/console/src/features/certificates/pages/certificates-truststore.tsx +++ b/apps/console/src/features/certificates/pages/certificates-truststore.tsx @@ -177,12 +177,13 @@ const CertificatesTruststore: FunctionComponent + />) } currentListSize={ listItemLimit } listItemLimit={ listItemLimit } @@ -227,7 +228,7 @@ const CertificatesTruststore: FunctionComponent + />) } isLoading={ isLoading } list={ paginate(filteredCertificatesTruststore, listItemLimit, offset) } diff --git a/apps/console/src/features/claims/api/claims.ts b/apps/console/src/features/claims/api/claims.ts index 298f3491913..f14e9deb941 100644 --- a/apps/console/src/features/claims/api/claims.ts +++ b/apps/console/src/features/claims/api/claims.ts @@ -17,8 +17,9 @@ */ import { AsgardeoSPAClient } from "@asgardeo/auth-react"; +import { ClaimConstants } from "@wso2is/core/constants"; import { IdentityAppsApiException } from "@wso2is/core/exceptions"; -import { Claim, HttpMethods } from "@wso2is/core/models"; +import { Claim, ClaimDialect, ClaimsGetParams, ExternalClaim, HttpMethods } from "@wso2is/core/models"; import { AxiosError, AxiosResponse } from "axios"; import { store } from "../../core"; import { ClaimManagementConstants } from "../constants"; @@ -509,3 +510,140 @@ export const getServerSupportedClaimsForSchema = (id: string): Promise} response. + * @throws {IdentityAppsApiException} + */ +export const getAllLocalClaims = (params: ClaimsGetParams): Promise => { + + const requestConfig = { + headers: { + Accept: "application/json", + "Content-Type": "application/json" + }, + method: HttpMethods.GET, + params, + url: store.getState().config.endpoints.localClaims + }; + + return httpClient(requestConfig) + .then((response: AxiosResponse) => { + if (response.status !== 200) { + throw new IdentityAppsApiException( + ClaimConstants.ALL_LOCAL_CLAIMS_FETCH_REQUEST_INVALID_RESPONSE_CODE_ERROR, + null, + response.status, + response.request, + response, + response.config); + } + + return Promise.resolve(response.data); + }) + .catch((error: AxiosError) => { + throw new IdentityAppsApiException( + ClaimConstants.ALL_LOCAL_CLAIMS_FETCH_REQUEST_ERROR, + error.stack, + error.code, + error.request, + error.response, + error.config); + }); +}; + +/** + * Get all the claim dialects. + * + * @param {ClaimsGetParams} params - sort, filter, offset, attributes, limit. + * @return {Promise} response. + * @throws {IdentityAppsApiException} + */ +export const getDialects = (params: ClaimsGetParams): Promise => { + + const requestConfig = { + headers: { + Accept: "application/json", + "Content-Type": "application/json" + }, + method: HttpMethods.GET, + params, + url: store.getState().config.endpoints.claims + }; + + return httpClient(requestConfig) + .then((response: AxiosResponse) => { + if (response.status !== 200) { + throw new IdentityAppsApiException( + ClaimConstants.DIALECTS_FETCH_REQUEST_INVALID_RESPONSE_CODE_ERROR, + null, + response.status, + response.request, + response, + response.config); + } + + return Promise.resolve(response.data); + }) + .catch((error: AxiosError) => { + throw new IdentityAppsApiException( + ClaimConstants.DIALECTS_FETCH_REQUEST_ERROR, + error.stack, + error.code, + error.request, + error.response, + error.config); + }); +}; + +/** + * Get all the external claims. + * + * @param {string } dialectID - Claim Dialect ID. + * @param {ClaimsGetParams} params - limit, offset, filter, attributes, sort. + * @return {Promise} response. + * @throws {IdentityAppsApiException} + */ +export const getAllExternalClaims = (dialectID: string, params: ClaimsGetParams): Promise => { + + const requestConfig = { + headers: { + Accept: "application/json", + "Content-Type": "application/json" + }, + method: HttpMethods.GET, + params, + url: `${store.getState().config.endpoints.externalClaims.replace("{}", dialectID)}` + }; + + return httpClient(requestConfig) + .then((response: AxiosResponse) => { + if (response.status !== 200) { + throw new IdentityAppsApiException( + ClaimConstants.ALL_EXTERNAL_CLAIMS_FETCH_REQUEST_INVALID_RESPONSE_CODE_ERROR, + null, + response.status, + response.request, + response, + response.config); + } + + return Promise.resolve(response.data); + }) + .catch((error: AxiosError) => { + if (error?.response?.data?.code !== ClaimManagementConstants.RESOURCE_NOT_FOUND_ERROR_CODE) { + throw new IdentityAppsApiException( + ClaimConstants.ALL_EXTERNAL_CLAIMS_FETCH_REQUEST_ERROR, + error.stack, + error.code, + error.request, + error.response, + error.config); + } + + return Promise.resolve([]); + }); +}; diff --git a/apps/console/src/features/claims/components/add/add-external-claim.tsx b/apps/console/src/features/claims/components/add/add-external-claim.tsx index 32945f0a3ab..8f123eba1ee 100644 --- a/apps/console/src/features/claims/components/add/add-external-claim.tsx +++ b/apps/console/src/features/claims/components/add/add-external-claim.tsx @@ -16,16 +16,16 @@ * under the License. */ -import { getAllLocalClaims } from "@wso2is/core/api"; import { AlertLevels, Claim, ClaimsGetParams, ExternalClaim, TestableComponentInterface } from "@wso2is/core/models"; import { addAlert } from "@wso2is/core/store"; import { Field, FormValue, Forms, Validation, useTrigger } from "@wso2is/forms"; -import { Code, ContentLoader, Hint, Link, PrimaryButton } from "@wso2is/react-components"; +import { Code, ContentLoader, Hint, Link, Message, PrimaryButton } from "@wso2is/react-components"; import React, { FunctionComponent, ReactElement, SyntheticEvent, useEffect, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { useDispatch } from "react-redux"; -import { DropdownItemProps, DropdownOnSearchChangeData, Grid, Label, Message } from "semantic-ui-react"; -import { attributeConfig } from "../../../../extensions"; +import { DropdownItemProps, DropdownOnSearchChangeData, Grid, Label } from "semantic-ui-react"; +import { SCIMConfigs, attributeConfig } from "../../../../extensions"; +import { getAllLocalClaims } from "../../../claims/api"; import { AppConstants, history } from "../../../core"; import { addExternalClaim, getServerSupportedClaimsForSchema } from "../../api"; import { ClaimManagementConstants } from "../../constants"; @@ -141,13 +141,17 @@ export const AddExternalClaims: FunctionComponent { + if (SCIMConfigs.serverSupportedClaimsAvailable.includes(claimDialectUri) && + serverSupportedClaims?.length === 0) { + setEmptyServerSupportedClaims(true); + } else { + setEmptyServerSupportedClaims(false); + } if (attributeType !== "oidc" && claimDialectUri !== attributeConfig.localAttributes.customDialectURI) { if (!serverSupportedClaims || serverSupportedClaims.length === 0) { - setEmptyServerSupportedClaims(true); setEmptyClaims(false); } else { - setEmptyServerSupportedClaims(false); if (!filteredLocalClaims || filteredLocalClaims.length === 0) { setEmptyClaims(true); } else { @@ -164,10 +168,7 @@ export const AddExternalClaims: FunctionComponent { - if ( - claimDialectUri === attributeConfig.localAttributes.customDialectURI || - claimDialectUri === attributeConfig.localAttributes.oidcDialectURI - ) { + if (!SCIMConfigs.serverSupportedClaimsAvailable.includes(claimDialectUri)) { setServerSideClaimsLoading(false); return; @@ -249,7 +250,7 @@ export const AddExternalClaims: FunctionComponent { + mappedLocalClaims?.forEach((externalClaim: string) => { tempLocalClaims = [ ...removeMappedLocalClaim(externalClaim, tempLocalClaims) ]; }); setLocalClaimsSearchResults(tempLocalClaims); @@ -333,9 +334,8 @@ export const AddExternalClaims: FunctionComponent - { attributeType !== "oidc" - && claimDialectUri !== attributeConfig.localAttributes.customDialectURI - ? ( + { SCIMConfigs.serverSupportedClaimsAvailable.includes(claimDialectUri) + ? ( - ) : ( + ) + : + ( { - attributeType !== ClaimManagementConstants.OIDC && + (attributeType !== ClaimManagementConstants.OIDC && + attributeType !== ClaimManagementConstants.OTHERS) && ( { - (isEmptyClaims || isEmptyServerSupportedClaims) && ( + ((!isLocalClaimsLoading && !serverSideClaimsLoading) + && (isEmptyServerSupportedClaims || isEmptyClaims)) + && ( - - { - !isEmptyServerSupportedClaims - ? ( - - - There are no local attributes available for mapping. - Add new local attributes from - - - history.push(AppConstants.getPaths() - .get("LOCAL_CLAIMS")) - } - > here - . - - ) : ( - - - All the SCIM attributes are mapped to local claims. - - - ) + + { + !isEmptyServerSupportedClaims + ? ( + <> + + There are no local attributes available for + mapping. Add new local attributes from + + + history.push(AppConstants.getPaths() + .get("LOCAL_CLAIMS")) + } + > here + . + + ) : ( + + All the SCIM attributes are mapped to local + claims. + + ) + } + ) } - + /> ) @@ -573,5 +583,6 @@ export const AddExternalClaims: FunctionComponent = ( /** * Conditionally disable map attribute step - * if there are no secondary user stores. + * if there are no secondary user stores and + * if the user stores are disabled */ useEffect(() => { - if ( hiddenUserStores && hiddenUserStores.length > 0 ) { + + let userStoresEnabled: boolean = false; + + if ( hiddenUserStores && hiddenUserStores.length > 0) { attributeConfig.localAttributes.isUserStoresHidden(hiddenUserStores).then(state => { - setShowMapAttributes(state.length > 0); + state.map((store: UserStoreListItem) => { + if(store.enabled){ + userStoresEnabled = true; + } + }); + + setShowMapAttributes(state.length > 0 && userStoresEnabled); }); } else { setShowMapAttributes(true); } - }, [ hiddenUserStores ]); /** diff --git a/apps/console/src/features/claims/components/claims-list.tsx b/apps/console/src/features/claims/components/claims-list.tsx index ea58056a1e4..f8cdbfad6e7 100644 --- a/apps/console/src/features/claims/components/claims-list.tsx +++ b/apps/console/src/features/claims/components/claims-list.tsx @@ -17,7 +17,6 @@ */ import { AccessControlConstants, Show } from "@wso2is/access-control"; -import { getProfileSchemas } from "@wso2is/core/api"; import { IdentityAppsApiException } from "@wso2is/core/exceptions"; import { hasRequiredScopes } from "@wso2is/core/helpers"; import { @@ -46,15 +45,15 @@ import { } from "@wso2is/react-components"; import isEqual from "lodash-es/isEqual"; import React,{ - Dispatch, - FunctionComponent, - ReactElement, - ReactNode, - SetStateAction, - SyntheticEvent, - useEffect, - useRef, - useState + Dispatch, + FunctionComponent, + ReactElement, + ReactNode, + SetStateAction, + SyntheticEvent, + useEffect, + useRef, + useState } from "react"; import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; @@ -67,8 +66,10 @@ import { FeatureConfigInterface, UIConstants, getEmptyPlaceholderIllustrations, - history + history, + store } from "../../core"; +import { getProfileSchemas } from "../../users/api"; import { UserStoreListItem, getUserStores } from "../../userstores"; import { deleteAClaim, deleteADialect, deleteAnExternalClaim } from "../api"; import { ClaimManagementConstants } from "../constants"; @@ -506,7 +507,7 @@ export const ClaimsList: FunctionComponent = ( return ( = ( { t("console:manage.features.claims.list.confirmation.message", { @@ -1045,13 +1046,13 @@ export const ClaimsList: FunctionComponent = ( }, attributeConfig.externalAttributes.showDeleteIcon(dialectID, list) && { hidden: (claim: ExternalClaim): boolean => { - if (!hasRequiredScopes(featureConfig?.attributeDialects, - featureConfig?.attributeDialects?.scopes?.delete, allowedScopes) + if (!hasRequiredScopes(featureConfig?.attributeDialects, + featureConfig?.attributeDialects?.scopes?.delete, allowedScopes) || attributeConfig.externalAttributes.hideDeleteIcon(claim)) { return true; } - - if (attributeConfig.defaultScimMapping + + if (attributeConfig.defaultScimMapping && Object.keys(attributeConfig.defaultScimMapping).length > 0) { const defaultSCIMClaims: Map = attributeConfig .defaultScimMapping[claim.claimDialectURI]; diff --git a/apps/console/src/features/claims/components/edit/external-dialect/edit-external-claim.tsx b/apps/console/src/features/claims/components/edit/external-dialect/edit-external-claim.tsx index 3ed07a228b2..b285c976828 100644 --- a/apps/console/src/features/claims/components/edit/external-dialect/edit-external-claim.tsx +++ b/apps/console/src/features/claims/components/edit/external-dialect/edit-external-claim.tsx @@ -16,7 +16,6 @@ * under the License. */ -import { getAllLocalClaims } from "@wso2is/core/api"; import { AlertLevels, Claim, ClaimsGetParams, ExternalClaim, TestableComponentInterface } from "@wso2is/core/models"; import { addAlert } from "@wso2is/core/store"; import { Field, FormValue, Forms, Validation } from "@wso2is/forms"; @@ -25,6 +24,8 @@ import React, { FunctionComponent, ReactElement, useEffect, useState } from "rea import { useTranslation } from "react-i18next"; import { useDispatch } from "react-redux"; import { Grid } from "semantic-ui-react"; +import { applicationConfig } from "../../../../../extensions"; +import { getAllLocalClaims } from "../../../../claims/api"; import { sortList } from "../../../../core"; import { getAnExternalClaim, updateAnExternalClaim } from "../../../api"; import { ClaimManagementConstants } from "../../../constants"; @@ -114,7 +115,7 @@ export const EditExternalClaim: FunctionComponent { setIsClaimsLoading(true); const params: ClaimsGetParams = { - "exclude-identity-claims": true, + "exclude-identity-claims": applicationConfig.excludeIdentityClaims, filter: null, limit: null, offset: null, @@ -324,13 +325,15 @@ export const EditExternalClaim: FunctionComponent - { claim?.displayName } - - { claim.claimURI } - -
), + { claim?.displayName } + + { claim?.claimURI } + +
+ ), value: claim?.claimURI }; }) diff --git a/apps/console/src/features/claims/components/edit/local-claim/edit-basic-details-local-claims.tsx b/apps/console/src/features/claims/components/edit/local-claim/edit-basic-details-local-claims.tsx index 11e45b7353e..76b8e2e5828 100644 --- a/apps/console/src/features/claims/components/edit/local-claim/edit-basic-details-local-claims.tsx +++ b/apps/console/src/features/claims/components/edit/local-claim/edit-basic-details-local-claims.tsx @@ -17,7 +17,6 @@ */ import { AccessControlConstants, Show } from "@wso2is/access-control"; -import { getProfileSchemas } from "@wso2is/core/api"; import { IdentityAppsApiException } from "@wso2is/core/exceptions"; import { hasRequiredScopes } from "@wso2is/core/helpers"; import { @@ -36,16 +35,18 @@ import { DangerZoneGroup, EmphasizedSegment, Hint, - Link + Link, + Message } from "@wso2is/react-components"; import Axios from "axios"; import React, { FunctionComponent, ReactElement, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; -import { Divider, Grid, Message, Form as SemanticForm } from "semantic-ui-react"; +import { Divider, Grid, Form as SemanticForm } from "semantic-ui-react"; import { attributeConfig } from "../../../../../extensions"; import { SCIMConfigs } from "../../../../../extensions/configs/scim"; import { AppConstants, AppState, FeatureConfigInterface, history } from "../../../../core"; +import { getProfileSchemas } from "../../../../users/api"; import { deleteAClaim, getExternalClaims, updateAClaim } from "../../../api"; import { ClaimManagementConstants } from "../../../constants"; @@ -166,7 +167,7 @@ export const EditBasicDetailsLocalClaims: FunctionComponent ( setConfirmDelete(false) } - type="warning" + type="negative" open={ confirmDelete } assertionHint={ t("console:manage.features.claims.local.confirmation.hint") } assertionType="checkbox" @@ -180,7 +181,7 @@ export const EditBasicDetailsLocalClaims: FunctionComponent { t("console:manage.features.claims.local.confirmation.header") } - + { t("console:manage.features.claims.local.confirmation.message") } @@ -398,34 +399,32 @@ export const EditBasicDetailsLocalClaims: FunctionComponent - - - { - !hasMapping ? ( - <> - { t("console:manage.features.claims.local.forms.infoMessages." + - "disabledConfigInfo") } -
- Add SCIM mapping from - { - history.push( - AppConstants.getPaths().get("SCIM_MAPPING") - ); - } - } - > - . -
- - ):( - t("console:manage.features.claims.local.forms.infoMessages." + + + { t("console:manage.features.claims.local.forms.infoMessages." + + "disabledConfigInfo") } +
+ Add SCIM mapping from + + history.push( + AppConstants.getPaths().get("SCIM_MAPPING") + ) + } + > here + . +
+ + ):( + t("console:manage.features.claims.local.forms.infoMessages." + "configApplicabilityInfo") - ) - } -
-
+ ) + } + />
) ) diff --git a/apps/console/src/features/claims/components/edit/local-claim/edit-mapped-attributes-local-claims.tsx b/apps/console/src/features/claims/components/edit/local-claim/edit-mapped-attributes-local-claims.tsx index 767e53a4db0..41082046e9a 100644 --- a/apps/console/src/features/claims/components/edit/local-claim/edit-mapped-attributes-local-claims.tsx +++ b/apps/console/src/features/claims/components/edit/local-claim/edit-mapped-attributes-local-claims.tsx @@ -17,7 +17,6 @@ */ import { AccessControlConstants, Show } from "@wso2is/access-control"; -import { getUserStoreList } from "@wso2is/core/api"; import { hasRequiredScopes } from "@wso2is/core/helpers"; import { AlertLevels, Claim, TestableComponentInterface } from "@wso2is/core/models"; import { addAlert } from "@wso2is/core/store"; @@ -27,8 +26,9 @@ import React, { FunctionComponent, ReactElement, useEffect, useMemo, useState } import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; import { Divider, Grid } from "semantic-ui-react"; -import { AppState, FeatureConfigInterface } from "../../../../core"; +import { AppState, FeatureConfigInterface, store } from "../../../../core"; import { UserStoreListItem } from "../../../../userstores"; +import { getUserStoreList } from "../../../../userstores/api"; import { updateAClaim } from "../../../api"; /** diff --git a/apps/console/src/features/claims/components/wizard/external-dialect/external-claims-add-dialect.tsx b/apps/console/src/features/claims/components/wizard/external-dialect/external-claims-add-dialect.tsx index 65348d5fc93..43f4581c315 100644 --- a/apps/console/src/features/claims/components/wizard/external-dialect/external-claims-add-dialect.tsx +++ b/apps/console/src/features/claims/components/wizard/external-dialect/external-claims-add-dialect.tsx @@ -114,7 +114,7 @@ export const ExternalClaims: FunctionComponent = ( const [ claims, setClaims ] = useState([]); const [ initialList, setInitialList ] = useState([]); - const [ tempMappedLocalClaims, setTempMappedLocalClaims ] = useState(); + const [ tempMappedLocalClaims, setTempMappedLocalClaims ] = useState([]); const ref = useRef(true); const firstTimeValueChanges = useRef(true); @@ -291,6 +291,7 @@ export const ExternalClaims: FunctionComponent = ( ExternalClaims.defaultProps = { attributeType: ClaimManagementConstants.OTHERS, "data-testid": "external-claims", + mappedLocalClaims: [], shouldShowInitialValues: true, wizard: true }; diff --git a/apps/console/src/features/claims/components/wizard/external-dialect/summary-add-dialect.tsx b/apps/console/src/features/claims/components/wizard/external-dialect/summary-add-dialect.tsx index 02fb9bd0c13..6b5e81fdc8e 100644 --- a/apps/console/src/features/claims/components/wizard/external-dialect/summary-add-dialect.tsx +++ b/apps/console/src/features/claims/components/wizard/external-dialect/summary-add-dialect.tsx @@ -17,10 +17,10 @@ */ import { TestableComponentInterface } from "@wso2is/core/models"; -import { AnimatedAvatar } from "@wso2is/react-components"; +import { AnimatedAvatar, Message } from "@wso2is/react-components"; import React, { FunctionComponent, ReactElement } from "react"; import { useTranslation } from "react-i18next"; -import { Grid, Image, Message, Table } from "semantic-ui-react"; +import { Grid, Image, Table } from "semantic-ui-react"; import { ClaimManagementConstants } from "../../../constants"; import { AddExternalClaim } from "../../../models"; import { resolveType } from "../../../utils"; @@ -130,9 +130,13 @@ export const SummaryAddDialect: FunctionComponent - - { t("console:manage.features.claims.dialects.wizard.summary.notFound") } - + ) diff --git a/apps/console/src/features/claims/components/wizard/local-claim/basic-details-local-claims.tsx b/apps/console/src/features/claims/components/wizard/local-claim/basic-details-local-claims.tsx index f3d70df3dca..30e24b5f13d 100644 --- a/apps/console/src/features/claims/components/wizard/local-claim/basic-details-local-claims.tsx +++ b/apps/console/src/features/claims/components/wizard/local-claim/basic-details-local-claims.tsx @@ -18,10 +18,10 @@ import { TestableComponentInterface } from "@wso2is/core/models"; import { Field, FormValue, Forms, Validation } from "@wso2is/forms"; -import { GenericIcon, Hint, InlineEditInput } from "@wso2is/react-components"; +import { GenericIcon, Hint, InlineEditInput, Message } from "@wso2is/react-components"; import React, { ReactElement, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Card, Grid, Icon, Label, Message, Popup } from "semantic-ui-react"; +import { Card, Grid, Icon, Label, Popup } from "semantic-ui-react"; import { attributeConfig } from "../../../../../extensions"; import { getTechnologyLogos } from "../../../../core"; @@ -304,24 +304,26 @@ export const BasicDetailsLocalClaims = (props: BasicDetailsLocalClaimsPropsInter { // TODO : Need to move ti i18n files showSCIMMappingError && ( - -

- The SCIM mapping value entered contains illegal - characters. Only alphabets, numbers, `_` are allowed. -

-
+ ) } { // TODO : Need to move ti i18n files showOIDCMappingError && ( - -

- The OpenID Connect mapping value entered contains + ) } diff --git a/apps/console/src/features/claims/components/wizard/local-claim/mapped-attributes.tsx b/apps/console/src/features/claims/components/wizard/local-claim/mapped-attributes.tsx index 7a2de8535b4..c5afd175025 100644 --- a/apps/console/src/features/claims/components/wizard/local-claim/mapped-attributes.tsx +++ b/apps/console/src/features/claims/components/wizard/local-claim/mapped-attributes.tsx @@ -16,7 +16,6 @@ * under the License. */ -import { getUserStoreList } from "@wso2is/core/api"; import { TestableComponentInterface } from "@wso2is/core/models"; import { Field, FormValue, Forms } from "@wso2is/forms"; import React, { FunctionComponent, ReactElement, useEffect, useState } from "react"; @@ -24,8 +23,9 @@ import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; import { Divider, Grid } from "semantic-ui-react"; import { attributeConfig } from "../../../../../extensions"; -import { AppState } from "../../../../core"; +import { AppState, store } from "../../../../core"; import { UserStoreListItem } from "../../../../userstores"; +import { getUserStoreList } from "../../../../userstores/api"; /** * Prop types of `MappedAttributes` component @@ -125,24 +125,28 @@ export const MappedAttributes: FunctionComponent { userStore.map((store: UserStoreListItem, index: number) => { return ( - - - { store.name } - - - - - + <> + { store?.enabled && ( + + + { store.name } + + + + + + ) } + ); }) } diff --git a/apps/console/src/features/claims/configs/endpoints.ts b/apps/console/src/features/claims/configs/endpoints.ts index 1a1104c831b..79825e3013d 100644 --- a/apps/console/src/features/claims/configs/endpoints.ts +++ b/apps/console/src/features/claims/configs/endpoints.ts @@ -22,12 +22,16 @@ import { ClaimResourceEndpointsInterface } from "../models"; * Get the resource endpoints for the Claim Management feature. * * @param {string} serverHost - Server Host. + * @param {string }serverHostWithOrgPath - Server Host with the Organization Path. * @return {ClaimResourceEndpointsInterface} */ -export const getClaimResourceEndpoints = (serverHost: string): ClaimResourceEndpointsInterface => { +export const getClaimResourceEndpoints = ( + serverHost: string, + serverHostWithOrgPath: string +): ClaimResourceEndpointsInterface => { return { claims: `${ serverHost }/api/server/v1/claim-dialects`, externalClaims:`${ serverHost }/api/server/v1/claim-dialects/{}/claims`, - localClaims: `${ serverHost }/api/server/v1/claim-dialects/local/claims` + localClaims: `${ serverHostWithOrgPath }/api/server/v1/claim-dialects/local/claims` }; }; diff --git a/apps/console/src/features/claims/constants/claim-management-constants.ts b/apps/console/src/features/claims/constants/claim-management-constants.ts index f59ec348719..81d37115572 100644 --- a/apps/console/src/features/claims/constants/claim-management-constants.ts +++ b/apps/console/src/features/claims/constants/claim-management-constants.ts @@ -113,17 +113,12 @@ export class ClaimManagementConstants { ClaimManagementConstants.ATTRIBUTE_DIALECT_IDS.get("SCIM_SCHEMAS_CORE") ] + public static readonly CUSTOM_MAPPING: string = SCIMConfigs.custom; + public static readonly OIDC_MAPPING: string[] = [ SCIMConfigs.oidc ]; - public static readonly SCIM_MAPPING: string[] = [ - "urn:ietf:params:scim:schemas:core:2.0:User", - "urn:scim:schemas:core:1.0", - "urn:ietf:params:scim:schemas:core:2.0", - "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" - ] - public static readonly OIDC: string = "oidc"; public static readonly SCIM: string = "scim"; public static readonly OTHERS: string = "others"; @@ -133,7 +128,7 @@ export class ClaimManagementConstants { { name: "User Schema", uri: "urn:ietf:params:scim:schemas:core:2.0:User" }, { name: "Enterprise Schema", uri: "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" }, { name: "Core 1.0 Schema", uri: "urn:scim:schemas:core:1.0" } - ] + ]; /** * Display names of User Id & Username to @@ -146,4 +141,8 @@ export class ClaimManagementConstants { public static readonly EMPTY_STRING = ""; + /** + * The error code that is returned when there is no item in the list + */ + public static readonly RESOURCE_NOT_FOUND_ERROR_CODE: string = "CMT-50017"; } diff --git a/apps/console/src/features/claims/pages/attribute-mappings.tsx b/apps/console/src/features/claims/pages/attribute-mappings.tsx index e054f23d12c..857981d8ee8 100644 --- a/apps/console/src/features/claims/pages/attribute-mappings.tsx +++ b/apps/console/src/features/claims/pages/attribute-mappings.tsx @@ -16,7 +16,6 @@ * under the License. */ -import { getAllExternalClaims, getDialects } from "@wso2is/core/api"; import { AlertLevels, ClaimDialect, ExternalClaim, TestableComponentInterface } from "@wso2is/core/models"; import { addAlert } from "@wso2is/core/store"; import { @@ -34,7 +33,8 @@ import { useDispatch, useSelector } from "react-redux"; import { RouteChildrenProps } from "react-router"; import { Image, StrictTabProps } from "semantic-ui-react"; import ExternalDialectEditPage from "./external-dialect-edit"; -import { attributeConfig } from "../../../extensions"; +import { SCIMConfigs, attributeConfig } from "../../../extensions"; +import { getAllExternalClaims, getDialects } from "../../claims/api"; import { AppConstants, AppState, getTechnologyLogos, history } from "../../core"; import { } from "../components"; import { ClaimManagementConstants } from "../constants"; @@ -286,7 +286,8 @@ export const AttributeMappings: FunctionComponent { if (ClaimManagementConstants.OIDC_MAPPING.includes(attributeMapping.dialectURI)) { type === ClaimManagementConstants.OIDC && attributeMappings.push(attributeMapping); - } else if (ClaimManagementConstants.SCIM_MAPPING.includes(attributeMapping.dialectURI)) { + } else if (Object.values(ClaimManagementConstants.SCIM_TABS).map( + (tab: { name: string; uri: string }) => tab.uri).includes(attributeMapping.dialectURI)) { type === ClaimManagementConstants.SCIM && attributeMappings.push(attributeMapping); } else if (type === ClaimManagementConstants.OTHERS) { attributeMappings.push(attributeMapping); @@ -333,23 +334,25 @@ export const AttributeMappings: FunctionComponent { - const dialect = dialects?.find((dialect: ClaimDialect) => dialect.dialectURI === tab.uri); - - dialect && - panes.push({ - menuItem: tab.name, - render: () => ( - - - - ) - }); + if (!SCIMConfigs.hideCore1Schema || SCIMConfigs.scim.core1Schema !== tab.uri) { + const dialect = dialects?.find((dialect: ClaimDialect) => dialect.dialectURI === tab.uri); + + dialect && + panes.push({ + menuItem: tab.name, + render: () => ( + + + + ) + }); + } }); if (attributeConfig.showCustomDialectInSCIM) { @@ -400,6 +403,7 @@ export const AttributeMappings: FunctionComponent = ( filteredDialect.forEach((attributeMapping: ClaimDialect) => { if (ClaimManagementConstants.OIDC_MAPPING.includes(attributeMapping.dialectURI)) { oidc.push(attributeMapping); - } else if (ClaimManagementConstants.SCIM_MAPPING.includes(attributeMapping.dialectURI)) { + } else if (Object.values(ClaimManagementConstants.SCIM_TABS).map( + (tab: { name: string; uri: string }) => tab.uri).includes(attributeMapping.dialectURI)) { scim.push(attributeMapping); } else { - if (attributeConfig.showCustomDialectInSCIM - && attributeMapping.dialectURI !== attributeConfig.localAttributes.customDialectURI ){ + if (attributeConfig.showCustomDialectInSCIM) { + if (attributeMapping.dialectURI !== attributeConfig.localAttributes.customDialectURI) { + others.push(attributeMapping); + } + } else { others.push(attributeMapping); } } @@ -173,6 +177,7 @@ const ClaimDialectsPage: FunctionComponent = ( /> ) } @@ -429,78 +434,80 @@ const ClaimDialectsPage: FunctionComponent = ( ) } { - otherAttributeMappings?.length > 0 && ( - { - history.push( - AppConstants.getPaths() - .get("ATTRIBUTE_MAPPINGS") - .replace(":type", ClaimManagementConstants.OTHERS) - ); - } } - className="clickable" - data-testid={ `${ testId }-other-dialect-container` } - > - - - - - - - - C - - - { t( - "console:manage.features.claims.dialects.sections." + - "manageAttributeMappings.custom.heading" - ) } - - - { t( - "console:manage.features.claims.attributeMappings." + - "custom.description" - ) } - - - - ) - } - inverted - /> - - - - - - - ) + !attributeConfig.showCustomDialectInSCIM && + otherAttributeMappings?.length > 0 && + ( + { + history.push( + AppConstants.getPaths() + .get("ATTRIBUTE_MAPPINGS") + .replace(":type", ClaimManagementConstants.OTHERS) + ); + } } + className="clickable" + data-testid={ `${ testId }-other-dialect-container` } + > + + + + + + + + C + + + { t( + "console:manage.features.claims.dialects.sections." + + "manageAttributeMappings.custom.heading" + ) } + + + { t( + "console:manage.features.claims.attributeMappings." + + "custom.description" + ) } + + + + ) + } + inverted + /> + + + + + + + ) } diff --git a/apps/console/src/features/claims/pages/external-dialect-edit.tsx b/apps/console/src/features/claims/pages/external-dialect-edit.tsx index c97f4ce2f41..dc958bc40af 100644 --- a/apps/console/src/features/claims/pages/external-dialect-edit.tsx +++ b/apps/console/src/features/claims/pages/external-dialect-edit.tsx @@ -17,7 +17,6 @@ */ import { AccessControlConstants, Show } from "@wso2is/access-control"; -import { getAllExternalClaims } from "@wso2is/core/api"; import { AlertLevels, ClaimDialect, ExternalClaim, TestableComponentInterface } from "@wso2is/core/models"; import { addAlert } from "@wso2is/core/store"; import { ConfirmationModal, DangerZone, DangerZoneGroup, EmphasizedSegment } from "@wso2is/react-components"; @@ -26,6 +25,7 @@ import { Trans, useTranslation } from "react-i18next"; import { useDispatch } from "react-redux"; import { Divider, Grid, Header, Placeholder } from "semantic-ui-react"; import { attributeConfig } from "../../../extensions"; +import { getAllExternalClaims } from "../../claims/api"; import { AppConstants, history, sortList } from "../../core"; import { deleteADialect, getADialect } from "../api"; import { EditDialectDetails, EditExternalClaims } from "../components"; @@ -89,7 +89,7 @@ const ExternalDialectEditPage: FunctionComponent ( setConfirmDelete(false) } - type="warning" + type="negative" open={ confirmDelete } assertion={ dialect.dialectURI } assertionHint={ ( @@ -110,7 +110,7 @@ const ExternalDialectEditPage: FunctionComponent { t("console:manage.features.claims.dialects.confirmations.header") } - + { t("console:manage.features.claims.dialects.confirmations.message") } diff --git a/apps/console/src/features/claims/pages/local-claims-edit.tsx b/apps/console/src/features/claims/pages/local-claims-edit.tsx index b82d2432e3d..17894a089b6 100644 --- a/apps/console/src/features/claims/pages/local-claims-edit.tsx +++ b/apps/console/src/features/claims/pages/local-claims-edit.tsx @@ -170,6 +170,7 @@ const LocalClaimsEditPage: FunctionComponent = ( ) } title={ claim?.displayName } + pageTitle="Edit Attributes" description={ t("console:manage.features.claims.local.pageLayout.edit.description") } backButton={ { onClick: () => { diff --git a/apps/console/src/features/claims/pages/local-claims.tsx b/apps/console/src/features/claims/pages/local-claims.tsx index 25fa30a6f0f..3f6018d28ec 100644 --- a/apps/console/src/features/claims/pages/local-claims.tsx +++ b/apps/console/src/features/claims/pages/local-claims.tsx @@ -16,7 +16,6 @@ * under the License. */ -import { getAllLocalClaims } from "@wso2is/core/api"; import { hasRequiredScopes } from "@wso2is/core/helpers"; import { AlertLevels, Claim, ClaimsGetParams, TestableComponentInterface } from "@wso2is/core/models"; import { addAlert } from "@wso2is/core/store"; @@ -27,6 +26,7 @@ import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; import { DropdownItemProps, DropdownProps, Icon, PaginationProps } from "semantic-ui-react"; import { attributeConfig } from "../../../extensions"; +import { getAllLocalClaims } from "../../claims/api"; import { AdvancedSearchWithBasicFilters, AppConstants, @@ -310,6 +310,7 @@ const LocalClaimsPage: FunctionComponent = ( } isLoading={ isLoading } title={ t("console:manage.features.claims.local.pageLayout.local.title") } + pageTitle={ t("console:manage.features.claims.local.pageLayout.local.title") } description={ ( <> { t(attributeConfig.attributes.description) } diff --git a/apps/console/src/features/core/components/advanced-search-with-basic-filters.tsx b/apps/console/src/features/core/components/advanced-search-with-basic-filters.tsx index 87d8dccaba3..6e9b861afa7 100644 --- a/apps/console/src/features/core/components/advanced-search-with-basic-filters.tsx +++ b/apps/console/src/features/core/components/advanced-search-with-basic-filters.tsx @@ -19,7 +19,13 @@ import { TestableComponentInterface } from "@wso2is/core/models"; import { SearchUtils } from "@wso2is/core/utils"; import { DropdownChild, Field, Forms } from "@wso2is/forms"; -import { AdvancedSearch, AdvancedSearchPropsInterface, LinkButton, PrimaryButton } from "@wso2is/react-components"; +import { + AdvancedSearch, + AdvancedSearchPropsInterface, + LinkButton, + PrimaryButton, + SessionTimedOutContext +} from "@wso2is/react-components"; import React, { FunctionComponent, ReactElement, useState } from "react"; import { useTranslation } from "react-i18next"; import { Divider, Form, Grid } from "semantic-ui-react"; @@ -92,6 +98,10 @@ export interface AdvancedSearchWithBasicFiltersPropsInterface extends TestableCo * Search input placeholder. */ placeholder: string; + /** + * Predefined Default Search strategy. ex: "name co %search-value% or clientId co %search-value%". + */ + predefinedDefaultSearchStrategy?: string; /** * Submit button text. */ @@ -137,6 +147,7 @@ export const AdvancedSearchWithBasicFilters: FunctionComponent(false); const [ isFiltersReset, setIsFiltersReset ] = useState(false); const [ externalSearchQuery, setExternalSearchQuery ] = useState(""); + const sessionTimedOut = React.useContext(SessionTimedOutContext); /** * Handles the form submit. @@ -204,6 +216,17 @@ export const AdvancedSearchWithBasicFilters: FunctionComponent { + if (predefinedDefaultSearchStrategy) { + return predefinedDefaultSearchStrategy; + } else { + return `${defaultSearchAttribute} ${defaultSearchOperator} %search-value%`; + } + } + /** * Default filter condition options. * @@ -233,7 +256,7 @@ export const AdvancedSearchWithBasicFilters: FunctionComponent @@ -288,6 +313,7 @@ export const AdvancedSearchWithBasicFilters: FunctionComponent ) } ); diff --git a/apps/console/src/features/groups/components/edit-group/edit-group-roles.tsx b/apps/console/src/features/groups/components/edit-group/edit-group-roles.tsx index 84ee9ab1559..ba54f244656 100644 --- a/apps/console/src/features/groups/components/edit-group/edit-group-roles.tsx +++ b/apps/console/src/features/groups/components/edit-group/edit-group-roles.tsx @@ -16,7 +16,6 @@ * under the License. */ -import { getRolesList } from "@wso2is/core/api"; import { AlertLevels, RolesMemberInterface, @@ -38,9 +37,9 @@ import escapeRegExp from "lodash-es/escapeRegExp"; import forEach from "lodash-es/forEach"; import forEachRight from "lodash-es/forEachRight"; import isEmpty from "lodash-es/isEmpty"; -import React, { FunctionComponent, ReactElement, useEffect, useState } from "react"; +import React, { FunctionComponent, ReactElement, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useDispatch } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { Button, Divider, @@ -52,10 +51,12 @@ import { Popup, Table } from "semantic-ui-react"; -import { getEmptyPlaceholderIllustrations, updateResources } from "../../../core"; -import { APPLICATION_DOMAIN, INTERNAL_DOMAIN } from "../../../roles/constants"; +import { AppState, getEmptyPlaceholderIllustrations, updateResources } from "../../../core"; +import { getOrganizationRoles } from "../../../organizations/api"; +import { OrganizationUtils } from "../../../organizations/utils"; +import { APPLICATION_DOMAIN, INTERNAL_DOMAIN, getRolesList } from "../../../roles"; +import { RolePermissions } from "../../../users/components"; import { UserRolePermissions } from "../../../users/components/user-role-permissions"; -import { RolePermissions } from "../../../users/components/wizard"; import { GroupsInterface } from "../../models"; interface GroupRolesPropsInterface extends TestableComponentInterface { @@ -112,6 +113,10 @@ export const GroupRolesList: FunctionComponent = ( const [ assignedRoles, setAssignedRoles ] = useState([]); + const currentOrganization = useSelector((state: AppState) => state.organization.organization); + const isRootOrganization = useMemo(() => + OrganizationUtils.isRootOrganization(currentOrganization), [ currentOrganization ]); + const [ alert, setAlert, alertComponent ] = useWizardAlert(); useEffect(() => { @@ -170,10 +175,17 @@ export const GroupRolesList: FunctionComponent = ( }, [ group ]); useEffect(() => { - getRolesList(null) - .then((response) => { - setPrimaryRoles(response.data.Resources); - }); + if (isRootOrganization) { + getRolesList(null) + .then((response) => { + setPrimaryRoles(response.data.Resources); + }); + } else { + getOrganizationRoles(currentOrganization.id, null, 100, null) + .then((response) => { + setPrimaryRoles(response.Resources); + }); + } }, []); /** @@ -286,6 +298,7 @@ export const GroupRolesList: FunctionComponent = ( method: "PATCH", path: "/Roles/" + id }; + removeOperations.push(operation); }); @@ -442,6 +455,7 @@ export const GroupRolesList: FunctionComponent = ( const addRoles = () => { const addedRoles = [ ...tempRoleList ]; + if (checkedUnassignedListItems?.length > 0) { checkedUnassignedListItems.map((role) => { if (!(tempRoleList?.includes(role))) { @@ -458,6 +472,7 @@ export const GroupRolesList: FunctionComponent = ( const removeRoles = () => { const removedRoles = [ ...roleList ]; + if (checkedAssignedListItems?.length > 0) { checkedAssignedListItems.map((role) => { if (!(roleList?.includes(role))) { @@ -490,6 +505,7 @@ export const GroupRolesList: FunctionComponent = ( */ const createItemLabel = (roleName: string) => { const role = roleName.split("/"); + if (role.length > 0) { if (role[0] == "Application") { return { @@ -574,6 +590,7 @@ export const GroupRolesList: FunctionComponent = ( { roleList?.map((role, index) => { const roleName = role.displayName?.split("/"); + if (roleName.length >= 1) { return ( = ( { tempRoleList?.map((role, index) => { const roleName = role.displayName.split("/"); + if (roleName.length >= 1) { return ( = ( assignedRoles && assignedRoles?.map((role) => { const groupName = role.display.split("/"); + if (groupName.length > 1) { isMatch = re.test(role.display); if (isMatch) { @@ -782,11 +801,12 @@ export const GroupRolesList: FunctionComponent = ( { - assignedRoles?.map((group) => { + assignedRoles?.map((group, index: number) => { const groupRole = group.display.split("/"); + if (groupRole.length >= 1) { return ( - + { groupRole[ 0 ] == APPLICATION_DOMAIN ? ( @@ -795,12 +815,12 @@ export const GroupRolesList: FunctionComponent = ( ) : ( - - - - ) + + + + ) } { @@ -812,7 +832,7 @@ export const GroupRolesList: FunctionComponent = ( = ( group.value ) } - /> + />) } /> diff --git a/apps/console/src/features/groups/components/edit-group/edit-group.tsx b/apps/console/src/features/groups/components/edit-group/edit-group.tsx index 354acf5af18..4ccaf0f9f63 100644 --- a/apps/console/src/features/groups/components/edit-group/edit-group.tsx +++ b/apps/console/src/features/groups/components/edit-group/edit-group.tsx @@ -19,7 +19,7 @@ import { hasRequiredScopes, isFeatureEnabled } from "@wso2is/core/helpers"; import { SBACInterface } from "@wso2is/core/models"; import { ResourceTab } from "@wso2is/react-components"; -import React, { FunctionComponent, ReactElement, useEffect, useState } from "react"; +import React, { FunctionComponent, ReactElement, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; // TODO: Move to shared components. import { useSelector } from "react-redux"; @@ -28,6 +28,7 @@ import { GroupRolesList } from "./edit-group-roles"; import { GroupUsersList } from "./edit-group-users"; import { FeatureConfigInterface } from "../../../core/models"; import { AppState } from "../../../core/store"; +import { OrganizationUtils } from "../../../organizations/utils"; import { getUsersList } from "../../../users/api"; import { UserBasicInterface } from "../../../users/models"; import { GroupConstants } from "../../constants"; @@ -79,6 +80,10 @@ export const EditGroup: FunctionComponent = (props: EditGroupPro const [ selectedUsersList, setSelectedUsersList ] = useState([]); const [ isReadOnly, setReadOnly ] = useState(false); + const currentOrganization = useSelector((state: AppState) => state.organization.organization); + const isRootOrganization = useMemo(() => + OrganizationUtils.isRootOrganization(currentOrganization), [ currentOrganization ]); + useEffect(() => { getUserList(); @@ -185,7 +190,9 @@ export const EditGroup: FunctionComponent = (props: EditGroupPro /> ) - },{ + }, + // ToDo - Enabled only for root organizations as BE doesn't have full SCIM support for organizations yet + isRootOrganization ? { menuItem: t("console:manage.features.roles.edit.menuItems.roles"), render: () => ( @@ -197,7 +204,7 @@ export const EditGroup: FunctionComponent = (props: EditGroupPro /> ) - } + } : null ]; return panes; diff --git a/apps/console/src/features/groups/components/group-list.tsx b/apps/console/src/features/groups/components/group-list.tsx index 517a677c8f9..501ad2f7b40 100644 --- a/apps/console/src/features/groups/components/group-list.tsx +++ b/apps/console/src/features/groups/components/group-list.tsx @@ -16,9 +16,9 @@ * under the License. */ +import { AccessControlConstants, Show } from "@wso2is/access-control"; import { hasRequiredScopes, isFeatureEnabled } from "@wso2is/core/helpers"; import { LoadableComponentInterface, SBACInterface, TestableComponentInterface } from "@wso2is/core/models"; -import { CommonUtils } from "@wso2is/core/utils"; import { AnimatedAvatar, AppAvatar, @@ -30,8 +30,9 @@ import { TableActionsInterface, TableColumnInterface } from "@wso2is/react-components"; +import moment from "moment"; import React, { ReactElement, ReactNode, SyntheticEvent, useState } from "react"; -import { Trans, useTranslation } from "react-i18next"; +import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; import { Header, Icon, Label, SemanticICONS } from "semantic-ui-react"; import { @@ -44,7 +45,6 @@ import { } from "../../core"; import { GroupConstants } from "../constants"; import { GroupsInterface } from "../models"; -import { AccessControlConstants, Show } from "@wso2is/access-control"; interface GroupListProps extends SBACInterface, LoadableComponentInterface, TestableComponentInterface { @@ -283,7 +283,14 @@ export const GroupList: React.FunctionComponent = (props: GroupL hidden: !showMetaContent, id: "lastModified", key: "lastModified", - render: (group: GroupsInterface): ReactNode => CommonUtils.humanizeDateDifference(group.meta.created), + render: (group: GroupsInterface): ReactNode => { + const now = moment(new Date()); + const receivedDate = moment(group.meta.created); + + return t("console:common.dateTime.humanizedDateString", { + date: moment.duration(now.diff(receivedDate)).humanize() + }); + }, title: t("console:manage.features.groups.list.columns.lastModified") }, { @@ -316,12 +323,12 @@ export const GroupList: React.FunctionComponent = (props: GroupL ? group?.displayName?.split("/")[0] : "PRIMARY"; - return !isFeatureEnabled(featureConfig?.groups, + return !isFeatureEnabled(featureConfig?.groups, GroupConstants.FEATURE_DICTIONARY.get("GROUP_UPDATE")) - || hasRequiredScopes(featureConfig?.groups, featureConfig?.groups?.scopes?.read, allowedScopes) - || readOnlyUserStores?.includes(userStore.toString()) - ? "eye" - : "pencil alternate"; + || !hasRequiredScopes(featureConfig?.groups, featureConfig?.groups?.scopes?.update, allowedScopes) + || readOnlyUserStores?.includes(userStore.toString()) + ? "eye" + : "pencil alternate"; }, onClick: (e: SyntheticEvent, group: GroupsInterface): void => handleGroupEdit(group.id), @@ -332,10 +339,10 @@ export const GroupList: React.FunctionComponent = (props: GroupL return !isFeatureEnabled(featureConfig?.groups, GroupConstants.FEATURE_DICTIONARY.get("GROUP_UPDATE")) - || hasRequiredScopes(featureConfig?.groups, featureConfig?.groups?.scopes?.read, allowedScopes) - || readOnlyUserStores?.includes(userStore.toString()) - ? t("common:view") - : t("common:edit"); + || !hasRequiredScopes(featureConfig?.groups, featureConfig?.groups?.scopes?.update, allowedScopes) + || readOnlyUserStores?.includes(userStore.toString()) + ? t("common:view") + : t("common:edit"); }, renderer: "semantic-icon" } @@ -389,34 +396,34 @@ export const GroupList: React.FunctionComponent = (props: GroupL /> { showGroupDeleteConfirmation && - setShowDeleteConfirmationModal(false) } - type="warning" - open={ showGroupDeleteConfirmation } - assertionHint={ t("console:manage.features.roles.list.confirmations.deleteItem.assertionHint") } - assertionType="checkbox" - primaryAction="Confirm" - secondaryAction="Cancel" - onSecondaryActionClick={ (): void => setShowDeleteConfirmationModal(false) } - onPrimaryActionClick={ (): void => { - handleGroupDelete(currentDeletedGroup); - setShowDeleteConfirmationModal(false); - } } - closeOnDimmerClick={ false } - > - - { t("console:manage.features.roles.list.confirmations.deleteItem.header") } - - - { t("console:manage.features.roles.list.confirmations.deleteItem.message", - { type: "group" }) } - - - { t("console:manage.features.roles.list.confirmations.deleteItem.content", - { type: "group" }) } - - + ( setShowDeleteConfirmationModal(false) } + type="negative" + open={ showGroupDeleteConfirmation } + assertionHint={ t("console:manage.features.roles.list.confirmations.deleteItem.assertionHint") } + assertionType="checkbox" + primaryAction="Confirm" + secondaryAction="Cancel" + onSecondaryActionClick={ (): void => setShowDeleteConfirmationModal(false) } + onPrimaryActionClick={ (): void => { + handleGroupDelete(currentDeletedGroup); + setShowDeleteConfirmationModal(false); + } } + closeOnDimmerClick={ false } + > + + { t("console:manage.features.roles.list.confirmations.deleteItem.header") } + + + { t("console:manage.features.roles.list.confirmations.deleteItem.message", + { type: "group" }) } + + + { t("console:manage.features.roles.list.confirmations.deleteItem.content", + { type: "group" }) } + + ) } ); diff --git a/apps/console/src/features/groups/components/wizard/create-group-wizard.tsx b/apps/console/src/features/groups/components/wizard/create-group-wizard.tsx index be3dca9110c..182ba54d317 100644 --- a/apps/console/src/features/groups/components/wizard/create-group-wizard.tsx +++ b/apps/console/src/features/groups/components/wizard/create-group-wizard.tsx @@ -16,25 +16,25 @@ * under the License. */ -import { getRolesList } from "@wso2is/core/api"; import { AlertLevels, RolesInterface, TestableComponentInterface } from "@wso2is/core/models"; import { addAlert } from "@wso2is/core/store"; import { useTrigger } from "@wso2is/forms"; import { Heading, LinkButton, PrimaryButton, Steps, useWizardAlert } from "@wso2is/react-components"; -import React, { FunctionComponent, ReactElement, useEffect, useState } from "react"; +import React, { FunctionComponent, ReactElement, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useDispatch } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { Button, Grid, Icon, Modal } from "semantic-ui-react"; import { GroupBasics } from "./group-basics"; import { CreateGroupSummary } from "./group-summary"; -import { AppConstants, AssignRoles, RolePermissions, history } from "../../../core"; -import { updateRole } from "../../../roles/api"; +import { AppConstants, AppState, AssignRoles, RolePermissions, history } from "../../../core"; +import { getOrganizationRoles } from "../../../organizations/api"; +import { OrganizationRoleManagementConstants } from "../../../organizations/constants"; +import { OrganizationRoleListItemInterface } from "../../../organizations/models"; +import { OrganizationUtils } from "../../../organizations/utils"; +import { getRolesList, updateRole } from "../../../roles/api"; import { createGroup } from "../../api"; import { getGroupsWizardStepIcons } from "../../configs"; -import { - CreateGroupInterface, - CreateGroupMemberInterface -} from "../../models"; +import { CreateGroupInterface, CreateGroupMemberInterface } from "../../models"; /** * Interface which captures create group props. @@ -94,12 +94,18 @@ export const CreateGroupWizard: FunctionComponent = (props: Cr const [ isRoleSelected, setRoleSelection ] = useState(false); const [ selectedRoleId, setSelectedRoleId ] = useState(); - const [ roleList, setRoleList ] = useState([]); - const [ tempRoleList, setTempRoleList ] = useState([]); - const [ initialRoleList, setInitialRoleList ] = useState([]); - const [ initialTempRoleList, setInitialTempRoleList ] = useState([]); + const [ roleList, setRoleList ] = useState([]); + const [ tempRoleList, setTempRoleList ] = useState([]); + const [ initialRoleList, setInitialRoleList ] = useState([]); + const [ initialTempRoleList, setInitialTempRoleList ] = useState([]); const [ isEnded, setEnded ] = useState(false); + const currentOrganization = useSelector((state: AppState) => state.organization.organization); + const isRootOrganization = useMemo(() => + OrganizationUtils.isRootOrganization(currentOrganization), [ currentOrganization ]); + const [ alert, setAlert, alertComponent ] = useWizardAlert(); /** @@ -127,10 +133,24 @@ export const CreateGroupWizard: FunctionComponent = (props: Cr useEffect(() => { if (roleList.length < 1) { - getRolesList(null) - .then((response) => { - setRoleList(response.data.Resources); - }); + if (isRootOrganization) { + getRolesList(null) + .then((response) => { + setRoleList(response.data.Resources); + }); + } else { + getOrganizationRoles(currentOrganization.id, null, 100, null) + .then((response) => { + if (!response.Resources) { + return; + } + + const roles = response.Resources.filter((role) => + role.displayName !== OrganizationRoleManagementConstants.ORG_CREATOR_ROLE_NAME); + + setRoleList(roles); + }); + } } }, []); @@ -183,6 +203,7 @@ export const CreateGroupWizard: FunctionComponent = (props: Cr const members: CreateGroupMemberInterface[] = []; const users = groupDetails?.UserList; + if (users?.length > 0) { users?.forEach(user => { members?.push({ @@ -219,16 +240,16 @@ export const CreateGroupWizard: FunctionComponent = (props: Cr } const roleData = { - "Operations": [{ + "Operations": [ { "op": "add", "value": { - "groups": [{ + "groups": [ { "display": createdGroup.displayName, "value": createdGroup.id - }] + } ] } - }], - "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"] + } ], + "schemas": [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ] }; if (rolesList && rolesList.length > 0) { @@ -249,7 +270,7 @@ export const CreateGroupWizard: FunctionComponent = (props: Cr description: t("console:manage.features.groups.notifications." + "createPermission." + "error.description", - { description: error.response.data.detail }), + { description: error.response.data.detail }), level: AlertLevels.ERROR, message: t("console:manage.features.groups.notifications.createPermission." + "error.message") @@ -297,7 +318,7 @@ export const CreateGroupWizard: FunctionComponent = (props: Cr dispatch( addAlert({ description: t("console:manage.features.groups.notifications.createGroup.error.description", - { description: error.response.data.detail }), + { description: error.response.data.detail }), level: AlertLevels.ERROR, message: t("console:manage.features.groups.notifications.createGroup.error.message") }) @@ -369,15 +390,15 @@ export const CreateGroupWizard: FunctionComponent = (props: Cr }; // Create group wizard steps - const WIZARD_STEPS = [{ + const ALL_WIZARD_STEPS = [ { content: ( handleWizardSubmit(values, WizardStepsFormTypes.BASIC_DETAILS) } @@ -388,12 +409,12 @@ export const CreateGroupWizard: FunctionComponent = (props: Cr },{ content: ( viewRolePermissions - ? - : ) + : ( handleWizardSubmit(values, WizardStepsFormTypes.ROLE_LIST) } initialValues={ @@ -409,7 +430,7 @@ export const CreateGroupWizard: FunctionComponent = (props: Cr handleInitialTempListChange={ (roles) => handleAddedRoleInitialListChange(roles) } handleInitialRoleListChange={ (roles) => handleInitialRoleListChange(roles) } handleSetRoleId={ (roleId) => handleRoleIdSet(roleId) } - /> + />) ), icon: getGroupsWizardStepIcons().roles, title: t("console:manage.features.roles.addRoleWizard.wizardSteps.5") @@ -424,7 +445,11 @@ export const CreateGroupWizard: FunctionComponent = (props: Cr ), icon: getGroupsWizardStepIcons().summary, title: t("console:manage.features.roles.addRoleWizard.wizardSteps.3") - }]; + } ]; + + const WIZARD_STEPS = OrganizationUtils.isCurrentOrganizationRoot() + ? [ ...ALL_WIZARD_STEPS ] + : [ ...ALL_WIZARD_STEPS.slice(0, 1), ...ALL_WIZARD_STEPS.slice(2) ]; /** * Function to change the current wizard step to next. @@ -433,12 +458,17 @@ export const CreateGroupWizard: FunctionComponent = (props: Cr switch(currentStep) { case 0: setSubmitGeneralSettings(); + break; case 1: - setSubmitRoleList(); + OrganizationUtils.isCurrentOrganizationRoot() + ? setSubmitRoleList() + : setFinishSubmit(); + break; case 2: setFinishSubmit(); + break; } @@ -520,15 +550,15 @@ export const CreateGroupWizard: FunctionComponent = (props: Cr ) } { currentStep === 0 && ( - + ) } { currentStep === WIZARD_STEPS.length - 1 && ( = (props: GroupBasi * @param roleName - User input role name */ const validateGroupNamePattern = async (): Promise => { + if (!OrganizationUtils.isCurrentOrganizationRoot()) { + return Promise.resolve(".*"); + } + let userStoreRegEx = ""; - if (userStore && userStore !== SharedUserStoreConstants.PRIMARY_USER_STORE) { + if (userStore && userStore !== SharedUserStoreConstants.PRIMARY_USER_STORE.toLocaleLowerCase()) { await SharedUserStoreUtils.getUserStoreRegEx(userStore, SharedUserStoreConstants.USERSTORE_REGEX_PROPERTIES.RolenameRegEx) .then((response) => { @@ -180,22 +187,26 @@ export const GroupBasics: FunctionComponent = (props: GroupBasi value: "" }; - getUserStoreList() - .then((response) => { - if (storeOptions === []) { - storeOptions.push(storeOption); - } - response.data.map((store, index) => { - storeOption = { - key: index, - text: store.name, - value: store.name - }; - storeOptions.push(storeOption); - } - ); - setUserStoresList(storeOptions); - }); + setUserStore(storeOptions[ 0 ].value); + + if (OrganizationUtils.isCurrentOrganizationRoot()) { + getUserStoreList() + .then((response) => { + if (storeOptions.length === 0) { + storeOptions.push(storeOption); + } + response.data.map((store, index) => { + storeOption = { + key: index, + text: store.name, + value: store.name + }; + storeOptions.push(storeOption); + } + ); + setUserStoresList(storeOptions); + }); + } setUserStoresList(storeOptions); }; @@ -223,24 +234,26 @@ export const GroupBasics: FunctionComponent = (props: GroupBasi > - - + + } - listen={ handleDomainChange } - value={ initialValues?.basicDetails?.domain ?? userStoreOptions[ 0 ]?.value } - /> - + required={ true } + element={

} + listen={ handleDomainChange } + value={ initialValues?.basicDetails?.domain ?? userStoreOptions[ 0 ]?.value } + /> + + = (props: GroupBasi .validateInputAgainstRegEx(value, regex); }); + if (!isGroupNameValid) { validation.isValid = false; validation.errorMessages.push( @@ -273,7 +287,7 @@ export const GroupBasics: FunctionComponent = (props: GroupBasi } const searchData: SearchGroupInterface = { - filter: `displayName eq ${ + filter: `displayName eq ${ userStore ?? SharedUserStoreConstants.PRIMARY_USER_STORE }/${ value }`, schemas: [ "urn:ietf:params:scim:api:messages:2.0:SearchRequest" diff --git a/apps/console/src/features/groups/configs/ui.ts b/apps/console/src/features/groups/configs/ui.ts index 85a2c123021..3cd28f3b022 100644 --- a/apps/console/src/features/groups/configs/ui.ts +++ b/apps/console/src/features/groups/configs/ui.ts @@ -16,12 +16,18 @@ * under the License. */ +import { FunctionComponent, SVGProps } from "react"; import { ReactComponent as DocumentIcon } from "../../../themes/default/assets/images/icons/document-icon.svg"; import { ReactComponent as GearsIcon } from "../../../themes/default/assets/images/icons/gears-icon.svg"; import { ReactComponent as ReportIcon } from "../../../themes/default/assets/images/icons/report-icon.svg"; import { ReactComponent as UserIcon } from "../../../themes/default/assets/images/icons/user-icon.svg"; -export const getGroupsWizardStepIcons = () => { +export const getGroupsWizardStepIcons = (): { + general: FunctionComponent>; + roles: FunctionComponent>; + summary: FunctionComponent>; + users: FunctionComponent>; +} => { return { general: DocumentIcon, diff --git a/apps/console/src/features/groups/constants/group-constants.ts b/apps/console/src/features/groups/constants/group-constants.ts index bd0c216e227..03be0c7032b 100644 --- a/apps/console/src/features/groups/constants/group-constants.ts +++ b/apps/console/src/features/groups/constants/group-constants.ts @@ -54,5 +54,5 @@ export class GroupConstants { * @constant * @type {string} */ - public static ALL_GROUPS: string = "all"; + public static ALL_GROUPS: string = "All user stores"; } diff --git a/apps/console/src/features/groups/models/groups.ts b/apps/console/src/features/groups/models/groups.ts index 19a97d35a6f..b82097c9323 100644 --- a/apps/console/src/features/groups/models/groups.ts +++ b/apps/console/src/features/groups/models/groups.ts @@ -108,6 +108,7 @@ export interface GroupSCIMOperationsInterface { */ export interface SearchGroupInterface { schemas: string[]; + domain?: string; startIndex: number; filter: string; } diff --git a/apps/console/src/features/groups/pages/group-edit.tsx b/apps/console/src/features/groups/pages/group-edit.tsx index ff4378a7fe6..d94367590c2 100644 --- a/apps/console/src/features/groups/pages/group-edit.tsx +++ b/apps/console/src/features/groups/pages/group-edit.tsx @@ -21,6 +21,7 @@ import React, { FunctionComponent, ReactElement, useEffect, useState } from "rea import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; import { AppConstants, AppState, FeatureConfigInterface, SharedUserStoreUtils, history } from "../../core"; +import { OrganizationUtils } from "../../organizations/utils"; import { getGroupById } from "../api"; import { EditGroup } from "../components"; import { GroupsInterface } from "../models"; @@ -40,6 +41,10 @@ const GroupEditPage: FunctionComponent = (): ReactElement => { * Get the readOnly user stores list. */ useEffect(() => { + if (!OrganizationUtils.isCurrentOrganizationRoot()) { + return; + } + SharedUserStoreUtils.getReadOnlyUserStores().then((response) => { setReadOnlyUserStoresList(response); }); @@ -66,7 +71,7 @@ const GroupEditPage: FunctionComponent = (): ReactElement => { } }).catch(() => { // TODO: handle error - }) + }) .finally(() => { setIsGroupDetailsRequestLoading(false); }); @@ -88,6 +93,7 @@ const GroupEditPage: FunctionComponent = (): ReactElement => { group.displayName : t("console:manage.pages.rolesEdit.title") } + pageTitle={ t("console:manage.pages.rolesEdit.title") } backButton={ { onClick: handleBackButtonClick, text: t("console:manage.pages.rolesEdit.backButton", { type: "groups" }) diff --git a/apps/console/src/features/groups/pages/groups.tsx b/apps/console/src/features/groups/pages/groups.tsx index c35087969e0..ee6f2399fd5 100644 --- a/apps/console/src/features/groups/pages/groups.tsx +++ b/apps/console/src/features/groups/pages/groups.tsx @@ -17,7 +17,6 @@ */ import { AccessControlConstants, Show } from "@wso2is/access-control"; -import { getUserStoreList } from "@wso2is/core/api"; import { AlertInterface, AlertLevels, RolesInterface } from "@wso2is/core/models"; import { addAlert } from "@wso2is/core/store"; import { @@ -39,9 +38,12 @@ import { UIConstants, UserStoreProperty, getAUserStore, - getEmptyPlaceholderIllustrations + getEmptyPlaceholderIllustrations, + store } from "../../core"; +import { OrganizationUtils } from "../../organizations/utils"; import { UserStorePostData } from "../../userstores"; +import { getUserStoreList } from "../../userstores/api"; import { deleteGroupById, getGroupList, searchGroupList } from "../api"; import { GroupList } from "../components"; import { CreateGroupWizard } from "../components/wizard"; @@ -115,6 +117,10 @@ const GroupsPage: FunctionComponent = (): ReactElement => { }, [ userStore ]); useEffect(() => { + if (!OrganizationUtils.isCurrentOrganizationRoot()) { + return; + } + SharedUserStoreUtils.getReadOnlyUserStores().then((response) => { setReadOnlyUserStoresList(response); }); @@ -127,11 +133,13 @@ const GroupsPage: FunctionComponent = (): ReactElement => { .then((response) => { if (response.status === 200) { const groupResources = response.data.Resources; + if (groupResources && groupResources instanceof Array && groupResources.length !== 0) { const updatedResources = groupResources.filter((role: GroupsInterface) => { return !role.displayName.includes("Application/") && !role.displayName.includes("Internal/"); }); + response.data.Resources = updatedResources; setGroupsList(updatedResources); setGroupsPage(0, listItemLimit, updatedResources); @@ -191,31 +199,35 @@ const GroupsPage: FunctionComponent = (): ReactElement => { value: "" }; - getUserStoreList() - .then((response) => { - if (storeOptions === []) { - storeOptions.push(storeOption); - } + setUserStore(storeOptions[ 0 ].value); - response.data.map((store, index) => { - getAUserStore(store.id).then((response: UserStorePostData) => { - const isDisabled = response.properties.find( - (property: UserStoreProperty) => property.name === "Disabled")?.value === "true"; - - if (!isDisabled) { - storeOption = { - key: index, - text: store.name, - value: store.name - }; - storeOptions.push(storeOption); - } - }); - } - ); + if (OrganizationUtils.isCurrentOrganizationRoot()) { + getUserStoreList() + .then((response) => { + if (storeOptions.length === 0) { + storeOptions.push(storeOption); + } - setUserStoresList(storeOptions); - }); + response.data.map((store, index) => { + getAUserStore(store.id).then((response: UserStorePostData) => { + const isDisabled = response.properties.find( + (property: UserStoreProperty) => property.name === "Disabled")?.value === "true"; + + if (!isDisabled) { + storeOption = { + key: index, + text: store.name, + value: store.name + }; + storeOptions.push(storeOption); + } + }); + } + ); + + setUserStoresList(storeOptions); + }); + } setUserStoresList(storeOptions); }; @@ -233,7 +245,7 @@ const GroupsPage: FunctionComponent = (): ReactElement => { }; const searchRoleListHandler = (searchQuery: string) => { - const searchData: SearchGroupInterface = { + let searchData: SearchGroupInterface = { filter: searchQuery, schemas: [ "urn:ietf:params:scim:api:messages:2.0:SearchRequest" @@ -241,12 +253,17 @@ const GroupsPage: FunctionComponent = (): ReactElement => { startIndex: 1 }; + if (userStore) { + searchData = { ...searchData, domain: userStore }; + } + setSearchQuery(searchQuery); searchGroupList(searchData).then(response => { if (response.status === 200) { const results = response.data.Resources; let updatedResults = []; + if (results) { updatedResults = results.filter((role: RolesInterface) => { return !role.displayName.includes("Application/") && !role.displayName.includes("Internal/"); @@ -275,6 +292,7 @@ const GroupsPage: FunctionComponent = (): ReactElement => { const handlePaginationChange = (event: React.MouseEvent, data: PaginationProps) => { const offsetValue = (data.activePage as number - 1) * listItemLimit; + setListOffset(offsetValue); setGroupsPage(offsetValue, listItemLimit, groupList); }; @@ -332,6 +350,7 @@ const GroupsPage: FunctionComponent = (): ReactElement => { const handleUserFilter = (query: string): void => { if (query === null || query === "displayName sw ") { getGroups(); + return; } @@ -364,6 +383,7 @@ const GroupsPage: FunctionComponent = (): ReactElement => { ) } title={ t("console:manage.pages.groups.title") } + pageTitle={ t("console:manage.pages.groups.title") } description={ t("console:manage.pages.groups.subTitle") } > = (): ReactElement => { onSortStrategyChange={ handleListSortingStrategyOnChange } sortStrategy={ listSortingStrategy } rightActionPanel={ - + />) } showPagination={ paginatedGroups.length > 0 } showTopActionPanel={ isGroupsListRequestLoading @@ -421,14 +441,14 @@ const GroupsPage: FunctionComponent = (): ReactElement => { totalListSize={ groupList?.length } > { groupsError - ? : - ) : + ( = (): ReactElement => { searchQuery={ searchQuery } readOnlyUserStores={ readOnlyUserStoresList } featureConfig={ featureConfig } - /> + />) } { diff --git a/apps/console/src/features/identity-providers/api/identity-provider.ts b/apps/console/src/features/identity-providers/api/identity-provider.ts index 5dd0843b698..8b14d6d4f0c 100644 --- a/apps/console/src/features/identity-providers/api/identity-provider.ts +++ b/apps/console/src/features/identity-providers/api/identity-provider.ts @@ -22,6 +22,11 @@ import { HttpMethods } from "@wso2is/core/models"; import { AxiosError, AxiosResponse } from "axios"; import { identityProviderConfig } from "../../../extensions/configs"; import { store } from "../../core"; +import useRequest, { + RequestConfigInterface, + RequestErrorInterface, + RequestResultInterface +} from "../../core/hooks/use-request"; import { IdentityProviderManagementConstants } from "../constants"; import { AuthenticatorInterface, @@ -74,6 +79,7 @@ export const createIdentityProvider = (identityProvider: object): Promise = if ((response.status !== 201)) { return Promise.reject(new Error("Failed to create the application.")); } + return Promise.resolve(response); }).catch((error) => { return Promise.reject(error); @@ -83,6 +89,7 @@ export const createIdentityProvider = (identityProvider: object): Promise = /** * Gets the IdP list with limit and offset. * + * @deprecated Use `useIdentityProviderList` hook instead. * @param {number} limit - Maximum Limit of the IdP List. * @param {number} offset - Offset for get to start. * @param {string} filter - Search filter. @@ -118,12 +125,56 @@ export const getIdentityProviderList = ( if (response.status !== 200) { return Promise.reject(new Error("Failed to get IdP list from: ")); } + return Promise.resolve(response.data as IdentityProviderListResponseInterface); }).catch((error) => { return Promise.reject(error); }); }; +/** + * Hook to get the IDP list with limit and offset. + * + * @param {number} limit - Maximum Limit of the IdP List. + * @param {number} offset - Offset for get to start. + * @param {string} filter - Search filter. + * @param {string} requiredAttributes - Extra attribute to be included in the list response. ex:`isFederationHub` + * + * @returns {RequestResultInterface} + */ +export const useIdentityProviderList = ( + limit?: number, + offset?: number, + filter?: string, + requiredAttributes?: string +): RequestResultInterface => { + + const requestConfig: RequestConfigInterface = { + headers: { + "Accept": "application/json", + "Content-Type": "application/json" + }, + method: HttpMethods.GET, + params: { + filter, + limit, + offset, + requiredAttributes + }, + url: store.getState().config.endpoints.identityProviders + }; + + const { data, error, isValidating, mutate } = useRequest(requestConfig); + + return { + data, + error: error, + isLoading: !error && !data, + isValidating, + mutate + }; +}; + /** * Gets detail about the Identity Provider. * @@ -146,6 +197,7 @@ export const getIdentityProviderDetail = (id: string): Promise => { if (response.status !== 200) { return Promise.reject(new Error("Failed to get idp details from: ")); } + return Promise.resolve(response.data as IdentityProviderResponseInterface); }).catch((error) => { return Promise.reject(error); @@ -181,6 +233,7 @@ export const getAllIdentityProvidersDetail = ( response, response.config); } + return Promise.resolve(response.data as IdentityProviderResponseInterface[]); }).catch((error: AxiosError) => { throw new IdentityAppsApiException( @@ -216,6 +269,7 @@ export const deleteIdentityProvider = (id: string): Promise => { if (response.status !== 204) { return Promise.reject(new Error("Failed to delete the identity provider.")); } + return Promise.resolve(response); }).catch((error) => { return Promise.reject(error); @@ -260,6 +314,7 @@ export const updateIdentityProviderDetails = (idp: IdentityProviderInterface): P if (response.status !== 200) { return Promise.reject(new Error("Failed to update identity provider: " + id)); } + return Promise.resolve(response.data as IdentityProviderInterface); }).catch((error) => { return Promise.reject(error); @@ -297,6 +352,7 @@ export const updateFederatedAuthenticator = ( if (response.status !== 200) { return Promise.reject(new Error("Failed to update identity provider: " + idpId)); } + return Promise.resolve(response.data as IdentityProviderInterface); }).catch((error) => { return Promise.reject(error); @@ -330,6 +386,7 @@ export const getFederatedAuthenticatorDetails = (idpId: string, authenticatorId: new Error("Failed to get federated authenticator details for: " + authenticatorId) ); } + return Promise.resolve(response.data as FederatedAuthenticatorListItemInterface); }).catch((error) => { return Promise.reject(error); @@ -359,6 +416,7 @@ export const getFederatedAuthenticatorMeta = (id: string): Promise => { if (response.status !== 200) { return Promise.reject(new Error("Failed to get federated authenticator meta details for: " + id)); } + return Promise.resolve(response.data as FederatedAuthenticatorMetaInterface); }).catch((error) => { return Promise.reject(error); @@ -387,6 +445,7 @@ export const getFederatedAuthenticatorsList = (): Promise => { if (response.status !== 200) { return Promise.reject(new Error("Failed to get federated authenticators list")); } + return Promise.resolve(response.data as FederatedAuthenticatorMetaInterface); }).catch((error) => { return Promise.reject(error); @@ -522,6 +581,7 @@ export const updateOutboundProvisioningConnector = ( if (response.status !== 200) { return Promise.reject(new Error("Failed to update identity provider: " + idpId)); } + return Promise.resolve(response.data as IdentityProviderInterface); }).catch((error) => { return Promise.reject(error); @@ -557,6 +617,7 @@ export const updateJITProvisioningConfigs = ( if (response.status !== 200) { return Promise.reject(new Error("Failed to update jit configuration: " + idpId)); } + return Promise.resolve(response.data as IdentityProviderInterface); }).catch((error: AxiosError) => { throw new IdentityAppsApiException( @@ -589,6 +650,7 @@ export const getJITProvisioningConfigs = ( if (response.status !== 200) { return Promise.reject(new Error("Failed to get jit configuration: " + idpId)); } + return Promise.resolve(response.data as IdentityProviderInterface); }).catch((error: AxiosError) => { throw new IdentityAppsApiException( @@ -631,6 +693,7 @@ export const updateClaimsConfigs = ( if (response.status !== 200) { return Promise.reject(new Error("Failed to update identity provider: " + idpId)); } + return Promise.resolve(response.data as IdentityProviderInterface); }).catch((error: AxiosError) => { throw new IdentityAppsApiException( @@ -653,7 +716,7 @@ export const updateClaimsConfigs = ( * @return {Promise} A promise containing the response. */ export const getIdentityProviderTemplateList = (limit?: number, offset?: number, - filter?: string): Promise => { + filter?: string): Promise => { const requestConfig = { headers: { "Accept": "application/json", @@ -764,6 +827,7 @@ export const updateIDPRoleMappings = ( if (response.status !== 200) { return Promise.reject(new Error("Failed to update identity provider: " + idpId)); } + return Promise.resolve(response.data as IdentityProviderInterface); }).catch((error) => { return Promise.reject(error); @@ -1072,6 +1136,7 @@ export const getOutboundProvisioningConnectorsList = (): Promise { return Promise.reject(error); @@ -1106,6 +1171,7 @@ export const updateIDPCertificate = ( if (response.status !== 200) { return Promise.reject(new Error("Failed to update identity provider: " + idpId)); } + return Promise.resolve(response.data as IdentityProviderInterface); }).catch((error: AxiosError) => { throw new IdentityAppsApiException( @@ -1146,6 +1212,7 @@ export const updateOutboundProvisioningConnectors = ( if (response.status !== 200) { return Promise.reject(new Error("Failed to update identity provider: " + idpId)); } + return Promise.resolve(response.data as IdentityProviderInterface); }).catch((error) => { return Promise.reject(error); @@ -1180,6 +1247,7 @@ export const updateFederatedAuthenticators = ( if (response.status !== 200) { return Promise.reject(new Error("Failed to update identity provider: " + idpId)); } + return Promise.resolve(response.data as IdentityProviderInterface); }).catch((error) => { return Promise.reject(error); @@ -1211,6 +1279,7 @@ export const getIDPConnectedApps = (idpId: string): Promise => { new Error("Failed to get connected apps for the IDP: " + idpId) ); } + return Promise.resolve(response.data as ConnectedAppsInterface); }).catch((error) => { return Promise.reject(error); diff --git a/apps/console/src/features/identity-providers/components/authenticator-grid.tsx b/apps/console/src/features/identity-providers/components/authenticator-grid.tsx index 2a8daedc7c2..b11be55c8d2 100644 --- a/apps/console/src/features/identity-providers/components/authenticator-grid.tsx +++ b/apps/console/src/features/identity-providers/components/authenticator-grid.tsx @@ -277,7 +277,7 @@ export const AuthenticatorGrid: FunctionComponent setShowDeleteConfirmationModal(false) } - type="warning" + type="negative" open={ showDeleteConfirmationModal } assertion={ deletingIDP?.name } assertionHint={ t("console:develop.features.authenticationProvider."+ @@ -449,7 +449,7 @@ export const AuthenticatorGrid: FunctionComponent { t("console:develop.features.authenticationProvider.confirmations.deleteIDP.message") } @@ -464,7 +464,7 @@ export const AuthenticatorGrid: FunctionComponent setShowDeleteErrorDueToConnectedAppsModal(false) } - type="warning" + type="negative" open={ showDeleteErrorDueToConnectedAppsModal } secondaryAction={ t("common:close") } onSecondaryActionClick={ (): void => setShowDeleteErrorDueToConnectedAppsModal(false) } @@ -475,8 +475,10 @@ export const AuthenticatorGrid: FunctionComponent - + { t("console:develop.features.authenticationProvider." + "confirmations.deleteIDPWithConnectedApps.message") } @@ -487,8 +489,8 @@ export const AuthenticatorGrid: FunctionComponent { isAppsLoading ? ( - - ) : + + ) : connectedApps?.map((app, index) => { return ( { app } diff --git a/apps/console/src/features/identity-providers/components/edit-multi-factor-authenticator.tsx b/apps/console/src/features/identity-providers/components/edit-multi-factor-authenticator.tsx index 6eaaed89584..30e6c55f4e2 100644 --- a/apps/console/src/features/identity-providers/components/edit-multi-factor-authenticator.tsx +++ b/apps/console/src/features/identity-providers/components/edit-multi-factor-authenticator.tsx @@ -32,7 +32,7 @@ import { } from "../../../extensions"; import { updateMultiFactorAuthenticatorDetails } from "../api"; import { IdentityProviderManagementConstants } from "../constants"; -import { AuthenticatorInterface, MultiFactorAuthenticatorInterface } from "../models"; +import { AuthenticatorInterface, AuthenticatorSettingsFormModes, MultiFactorAuthenticatorInterface } from "../models"; /** * Proptypes for the Multi-factor Authenticator edit component. @@ -193,6 +193,7 @@ export const EditMultiFactorAuthenticator: FunctionComponent (undefined); + const [ , setFormFields ] = useState(undefined); const [ initialValues, setInitialValues ] = useState(undefined); + // SMS OTP length unit is set to digits or characters according to the state of this variable + const [ isOTPNumeric, setIsOTPNumeric ] = useState(); + /** * Flattens and resolved form initial values and field metadata. */ @@ -174,24 +183,42 @@ export const EmailOTPAuthenticatorForm: FunctionComponent IdentityProviderManagementConstants - .EMAIL_OTP_AUTHENTICATOR_SETTINGS_FORM_FIELD_CONSTRAINTS.EXPIRY_TIME_MAX_VALUE)) { + } else if (( values.EmailOTP_ExpiryTime < IdentityProviderManagementConstants + .EMAIL_OTP_AUTHENTICATOR_SETTINGS_FORM_FIELD_CONSTRAINTS.EXPIRY_TIME_MIN_VALUE ) + || ( values.EmailOTP_ExpiryTime > IdentityProviderManagementConstants + .EMAIL_OTP_AUTHENTICATOR_SETTINGS_FORM_FIELD_CONSTRAINTS.EXPIRY_TIME_MAX_VALUE )) { // Check for invalid range. errors.EmailOTP_ExpiryTime = t("console:develop.features.authenticationProvider.forms" + ".authenticatorSettings.emailOTP.expiryTime.validations.range"); @@ -268,12 +306,12 @@ export const EmailOTPAuthenticatorForm: FunctionComponent IdentityProviderManagementConstants .EMAIL_OTP_AUTHENTICATOR_SETTINGS_FORM_FIELD_CONSTRAINTS.OTP_LENGTH_MAX_VALUE)) { // Check for invalid range. errors.EmailOTP_OTPLength = t("console:develop.features.authenticationProvider.forms" + - ".authenticatorSettings.emailOTP.tokenLength.validations.range"); + `.authenticatorSettings.emailOTP.tokenLength.validations.range.${isOTPNumeric ? "digits" : "characters"}`); } return errors; @@ -302,15 +340,14 @@ export const EmailOTPAuthenticatorForm: FunctionComponent - The generated passcode will be expired after this defined time period. Please pick a - value between 1 second & 86400 seconds(1 day). - + Please pick a value between 1 minute & 1440 minutes (1 day). + ) } required={ true } readOnly={ readOnly } @@ -326,7 +363,7 @@ export const EmailOTPAuthenticatorForm: FunctionComponent @@ -337,28 +374,51 @@ export const EmailOTPAuthenticatorForm: FunctionComponent + + Please clear this checkbox to enable alphanumeric characters. + ) + } + readOnly={ readOnly } + width={ 12 } + data-testid={ `${ testId }-otp-regex-use-numeric` } + listen={ (e:boolean) => {setIsOTPNumeric(e);} } + /> - The number of allowed characters in the OTP token. Please pick a value between + The number of allowed characters in the OTP. Please pick a value between 4-10. - + ) } required={ true } readOnly={ readOnly } @@ -370,31 +430,17 @@ export const EmailOTPAuthenticatorForm: FunctionComponent - - Only numeric characters (0-9) are used for the OTP token. - Please clear this checkbox to enable alphanumeric characters. - - } - readOnly={ readOnly } - width={ 16 } - data-testid={ `${ testId }-otp-regex-use-numeric` } - /> + > + + + } required={ formFields?.ClientSecret?.meta?.isMandatory } - readOnly={ readOnly || formFields?.ClientSecret?.meta?.readOnly } + readOnly={ + readOnly || ( + mode === AuthenticatorSettingsFormModes.CREATE + ? false + : formFields?.ClientSecret?.meta?.readOnly + ) + } value={ formFields?.ClientSecret?.value } maxLength={ formFields?.ClientSecret?.meta?.maxLength } minLength={ @@ -352,7 +372,13 @@ export const FacebookAuthenticatorForm: FunctionComponent } required={ formFields?.ClientId?.meta?.isMandatory } - readOnly={ readOnly || formFields?.ClientId?.meta?.readOnly } + readOnly={ + readOnly || ( + mode === AuthenticatorSettingsFormModes.CREATE + ? false + : formFields?.ClientId?.meta?.readOnly + ) + } value={ formFields?.ClientId?.value } maxLength={ formFields?.ClientId?.meta?.maxLength } minLength={ @@ -332,7 +346,13 @@ export const GithubAuthenticatorForm: FunctionComponent } required={ formFields?.ClientSecret?.meta?.isMandatory } - readOnly={ readOnly || formFields?.ClientSecret?.meta?.readOnly } + readOnly={ + readOnly || ( + mode === AuthenticatorSettingsFormModes.CREATE + ? false + : formFields?.ClientSecret?.meta?.readOnly + ) + } value={ formFields?.ClientSecret?.value } maxLength={ formFields?.ClientSecret?.meta?.maxLength } minLength={ @@ -360,7 +380,13 @@ export const GithubAuthenticatorForm: FunctionComponent ) } required={ formFields?.ClientId?.meta?.isMandatory } - readOnly={ readOnly || formFields?.ClientId?.meta?.readOnly } + readOnly={ + readOnly || ( + mode === AuthenticatorSettingsFormModes.CREATE + ? false + : formFields?.ClientId?.meta?.readOnly + ) + } value={ formFields?.ClientId?.value } maxLength={ IdentityProviderManagementConstants .AUTHENTICATOR_SETTINGS_FORM_FIELD_CONSTRAINTS.CLIENT_ID_MAX_LENGTH as number } @@ -365,7 +379,13 @@ export const GoogleAuthenticatorForm: FunctionComponent } required={ formFields?.ClientSecret?.meta?.isMandatory } - readOnly={ readOnly || formFields?.ClientSecret?.meta?.readOnly } + readOnly={ + readOnly || ( + mode === AuthenticatorSettingsFormModes.CREATE + ? false + : formFields?.ClientSecret?.meta?.readOnly + ) + } value={ formFields?.ClientSecret?.value } maxLength={ formFields?.ClientSecret?.meta?.maxLength } minLength={ @@ -393,7 +413,13 @@ export const GoogleAuthenticatorForm: FunctionComponent + { (formFields?.AdditionalQueryParameters?.value && !isEmpty(extractScopes(formFields.AdditionalQueryParameters.value))) && ( diff --git a/apps/console/src/features/identity-providers/components/forms/authenticators/saml-authenticator-form.tsx b/apps/console/src/features/identity-providers/components/forms/authenticators/saml-authenticator-form.tsx index c8adbd6ab4b..eaefcb96d8c 100644 --- a/apps/console/src/features/identity-providers/components/forms/authenticators/saml-authenticator-form.tsx +++ b/apps/console/src/features/identity-providers/components/forms/authenticators/saml-authenticator-form.tsx @@ -25,26 +25,27 @@ import { useSelector } from "react-redux"; import { Divider, Grid, SemanticWIDTHS } from "semantic-ui-react"; import { AppState, ConfigReducerStateInterface } from "../../../../core"; import { + AuthenticatorSettingsFormModes, CommonAuthenticatorFormInitialValuesInterface, FederatedAuthenticatorWithMetaInterface } from "../../../models"; import { - composeValidators, DEFAULT_NAME_ID_FORMAT, DEFAULT_PROTOCOL_BINDING, + IDENTITY_PROVIDER_AUTHORIZED_REDIRECT_URL_LENGTH, + IDENTITY_PROVIDER_ENTITY_ID_LENGTH, + LOGOUT_URL_LENGTH, + SERVICE_PROVIDER_ENTITY_ID_LENGTH, + SSO_URL_LENGTH, + composeValidators, fastSearch, getAvailableNameIDFormats, getAvailableProtocolBindingTypes, getDigestAlgorithmOptionsMapped, getSignatureAlgorithmOptionsMapped, hasLength, - IDENTITY_PROVIDER_AUTHORIZED_REDIRECT_URL_LENGTH, - IDENTITY_PROVIDER_ENTITY_ID_LENGTH, isUrl, - LOGOUT_URL_LENGTH, - required, - SERVICE_PROVIDER_ENTITY_ID_LENGTH, - SSO_URL_LENGTH + required } from "../../utils/saml-idp-utils"; /** @@ -59,6 +60,12 @@ const I18N_TARGET_KEY = "console:develop.features.authenticationProvider.forms.a * {@link SamlAuthenticatorSettingsForm.defaultProps}. */ interface SamlSettingsFormPropsInterface extends TestableComponentInterface { + /** + * The intended mode of the authenticator form. + * If the mode is "EDIT", the form will be used in the edit view and will rely on metadata for readonly states, etc. + * If the mode is "CREATE", the form will be used in the add wizards and will all the fields will be editable. + */ + mode: AuthenticatorSettingsFormModes; authenticator: FederatedAuthenticatorWithMetaInterface; onSubmit: (values: CommonAuthenticatorFormInitialValuesInterface) => void; readOnly?: boolean; @@ -116,7 +123,7 @@ export const SamlAuthenticatorSettingsForm: FunctionComponent(false); const [ isLogoutEnabled, setIsLogoutEnabled ] = useState(false); - const authorizedRedirectURL: string = config?.deployment?.serverHost + "/commonauth" ; + const authorizedRedirectURL: string = config?.deployment?.customServerHost + "/commonauth" ; /** * ISAuthnReqSigned, IsLogoutReqSigned these two fields states will be used by other @@ -181,13 +188,14 @@ export const SamlAuthenticatorSettingsForm: FunctionComponent { const ifEitherOneOfThemIsChecked = isLogoutReqSigned || isAuthnReqSigned; + setIsAlgorithmsEnabled(ifEitherOneOfThemIsChecked); }, [ isLogoutReqSigned, isAuthnReqSigned ]); const onFormSubmit = (values: { [ key: string ]: any }): void => { const manualOverride = { - "IncludeProtocolBinding": includeProtocolBinding, "ISAuthnReqSigned": isAuthnReqSigned, + "IncludeProtocolBinding": includeProtocolBinding, "IsLogoutEnabled": isLogoutEnabled, "IsLogoutReqSigned": isLogoutReqSigned, "IsSLORequestAccepted": isSLORequestAccepted, @@ -204,6 +212,7 @@ export const SamlAuthenticatorSettingsForm: FunctionComponent ({ key, value: manualOverride[key] })) as any ] }); + onSubmit(authn); }; @@ -211,13 +220,15 @@ export const SamlAuthenticatorSettingsForm: FunctionComponent void 0; }; return ( -
+ { - return propertyMetadata?.subProperties?.length > 0 && getFieldType(propertyMetadata) === FieldType.CHECKBOX; + return propertyMetadata?.subProperties?.length > 0 + && getFieldType(propertyMetadata, mode) === FieldType.CHECKBOX; }; /** @@ -139,7 +142,7 @@ export const CommonPluggableComponentForm: FunctionComponent { - return propertyMetadata?.subProperties?.length > 0 && getFieldType(propertyMetadata) === FieldType.RADIO; + return propertyMetadata?.subProperties?.length > 0 && getFieldType(propertyMetadata, mode) === FieldType.RADIO; }; const getField = (property: CommonPluggableComponentPropertyInterface, @@ -155,7 +158,18 @@ export const CommonPluggableComponentForm: FunctionComponent - { getPropertyField(property, { ...eachPropertyMeta, readOnly }, listen, testId) } + { + getPropertyField( + property, + { + ...eachPropertyMeta, + readOnly: mode === AuthenticatorSettingsFormModes.CREATE ? false : readOnly + }, + mode, + listen, + testId + ) + } ); @@ -163,7 +177,18 @@ export const CommonPluggableComponentForm: FunctionComponent - { getPropertyField(property, { ...eachPropertyMeta, readOnly }, listen, testId) } + { + getPropertyField( + property, + { + ...eachPropertyMeta, + readOnly: mode === AuthenticatorSettingsFormModes.CREATE ? false : readOnly + }, + mode, + listen, + testId + ) + } ); diff --git a/apps/console/src/features/identity-providers/components/forms/factories/authenticator-form-factory.tsx b/apps/console/src/features/identity-providers/components/forms/factories/authenticator-form-factory.tsx index df39c4aeeb4..989c30e520d 100644 --- a/apps/console/src/features/identity-providers/components/forms/factories/authenticator-form-factory.tsx +++ b/apps/console/src/features/identity-providers/components/forms/factories/authenticator-form-factory.tsx @@ -18,8 +18,10 @@ import { TestableComponentInterface } from "@wso2is/core/models"; import React, { FunctionComponent, ReactElement } from "react"; +import { identityProviderConfig } from "../../../../../extensions/configs/identity-provider"; import { IdentityProviderManagementConstants } from "../../../constants"; import { + AuthenticatorSettingsFormModes, FederatedAuthenticatorListItemInterface, FederatedAuthenticatorMetaInterface, FederatedAuthenticatorWithMetaInterface @@ -37,6 +39,12 @@ import { SamlAuthenticatorSettingsForm } from "../authenticators/saml-authentica * Proptypes for the authenticator form factory component. */ interface AuthenticatorFormFactoryInterface extends TestableComponentInterface { + /** + * The intended mode of the authenticator form. + * If the mode is "EDIT", the form will be used in the edit view and will rely on metadata for readonly states, etc. + * If the mode is "CREATE", the form will be used in the add wizards and will all the fields will be editable. + */ + mode: AuthenticatorSettingsFormModes; metadata?: FederatedAuthenticatorMetaInterface; initialValues: FederatedAuthenticatorListItemInterface; onSubmit: (values: FederatedAuthenticatorListItemInterface) => void; @@ -56,6 +64,10 @@ interface AuthenticatorFormFactoryInterface extends TestableComponentInterface { * Specifies if the form is submitting. */ isSubmitting?: boolean; + /** + * Created template Id. + */ + templateId?: string; } /** @@ -71,6 +83,7 @@ export const AuthenticatorFormFactory: FunctionComponent void; @@ -38,11 +48,14 @@ interface OutboundProvisioningConnectorFormFactoryInterface extends TestableComp * @return {ReactElement} */ export const OutboundProvisioningConnectorFormFactory: FunctionComponent< - OutboundProvisioningConnectorFormFactoryInterface> = (props: OutboundProvisioningConnectorFormFactoryInterface): - ReactElement => { + OutboundProvisioningConnectorFormFactoryInterface +> = ( + props: OutboundProvisioningConnectorFormFactoryInterface +): ReactElement => { const { metadata, + mode, initialValues, onSubmit, type, @@ -55,15 +68,18 @@ export const OutboundProvisioningConnectorFormFactory: FunctionComponent< const generateConnector = (): ReactElement => { switch (type) { default: - return ; + return ( + + ); } }; diff --git a/apps/console/src/features/identity-providers/components/forms/general-details-form.tsx b/apps/console/src/features/identity-providers/components/forms/general-details-form.tsx index 0c776c25a92..cabf8aed4de 100644 --- a/apps/console/src/features/identity-providers/components/forms/general-details-form.tsx +++ b/apps/console/src/features/identity-providers/components/forms/general-details-form.tsx @@ -19,13 +19,14 @@ import { TestableComponentInterface } from "@wso2is/core/models"; import { Field, Form } from "@wso2is/form"; import { EmphasizedSegment, Heading } from "@wso2is/react-components"; -import React, { FunctionComponent, ReactElement } from "react"; +import { FormValidation } from "@wso2is/validation"; +import React, { FunctionComponent, ReactElement, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { Divider, Grid } from "semantic-ui-react"; import { identityProviderConfig } from "../../../../extensions"; +import { IdentityProviderManagementConstants } from "../../constants"; import { IdentityProviderInterface, IdentityProviderListResponseInterface } from "../../models"; import { IdpCertificates } from "../settings"; -import { FormValidation } from "@wso2is/validation"; /** * Proptypes for the identity provider general details form component. @@ -100,7 +101,6 @@ interface GeneralDetailsFormPopsInterface extends TestableComponentInterface { const IDP_NAME_MAX_LENGTH: number = 50; const IDP_DESCRIPTION_MAX_LENGTH: number = 300; -const IDP_IMAGE_URL_MAX_LENGTH: number = 2000; /** * Form to edit general details of the identity provider. @@ -127,6 +127,13 @@ export const GeneralDetailsForm: FunctionComponent { + return identityProviderConfig.editIdentityProvider.getCertificateOptionsForTemplate(editingIDP?.templateId); + }, []); + // const [ modifiedName, setModifiedName ] = useState(name); const { t } = useTranslation(); @@ -142,6 +149,7 @@ export const GeneralDetailsForm: FunctionComponent 0) { idpList?.identityProviders.map((idp) => { if (idp?.name === value && name !== value) { @@ -196,6 +204,23 @@ export const GeneralDetailsForm: FunctionComponent { + + let showCertificate: boolean = identityProviderConfig.generalDetailsForm.showCertificate; + + if (certificateOptionsForTemplate !== undefined + && !certificateOptionsForTemplate.JWKS + && !certificateOptionsForTemplate.PEM) { + showCertificate = false; + } + + return showCertificate; + }; + return ( @@ -254,8 +279,14 @@ export const GeneralDetailsForm: FunctionComponent - { identityProviderConfig.generalDetailsForm.showCertificate && ( + { shouldShowCertificates() && ( ) } @@ -299,6 +339,6 @@ export const GeneralDetailsForm: FunctionComponent { +export const getFieldType = ( + propertyMetadata: CommonPluggableComponentMetaPropertyInterface, + mode: AuthenticatorSettingsFormModes +): FieldType => { if (propertyMetadata?.type?.toUpperCase() === CommonConstants.BOOLEAN) { return FieldType.CHECKBOX; } else if (propertyMetadata?.isConfidential) { @@ -513,7 +517,7 @@ export const getFieldType = (propertyMetadata: CommonPluggableComponentMetaPrope } else if (propertyMetadata?.key === CommonConstants.SCOPE_KEY) { return FieldType.TABLE; } else if (propertyMetadata?.key.toUpperCase().includes(CommonConstants.FIELD_COMPONENT_KEYWORD_URL)) { - if (propertyMetadata?.key === AUTHORIZATION_REDIRECT_URL) { + if (propertyMetadata?.key === AUTHORIZATION_REDIRECT_URL && mode !== AuthenticatorSettingsFormModes.CREATE) { return FieldType.COPY_INPUT; } else { // TODO: Need proper backend support to identity URL fields-https://github.com/wso2/product-is/issues/12501. @@ -540,13 +544,15 @@ export const getFieldType = (propertyMetadata: CommonPluggableComponentMetaPrope * @param listen Listener method for the on change events of a checkbox field * @return Corresponding property field. */ -export const getPropertyField = (property: CommonPluggableComponentPropertyInterface, - propertyMetadata: CommonPluggableComponentMetaPropertyInterface, - listen?: (key: string, values: Map) => void, - testId?: string): - ReactElement => { +export const getPropertyField = ( + property: CommonPluggableComponentPropertyInterface, + propertyMetadata: CommonPluggableComponentMetaPropertyInterface, + mode: AuthenticatorSettingsFormModes, + listen?: (key: string, values: Map) => void, + testId?: string +): ReactElement => { - switch (getFieldType(propertyMetadata)) { + switch (getFieldType(propertyMetadata, mode)) { case FieldType.CHECKBOX : { if (listen) { return getCheckboxFieldWithListener(property, propertyMetadata, listen, testId); diff --git a/apps/console/src/features/identity-providers/components/forms/jit-provisioning-configuration-form.tsx b/apps/console/src/features/identity-providers/components/forms/jit-provisioning-configuration-form.tsx index 7d5da8d0c81..9e63deb7fa1 100644 --- a/apps/console/src/features/identity-providers/components/forms/jit-provisioning-configuration-form.tsx +++ b/apps/console/src/features/identity-providers/components/forms/jit-provisioning-configuration-form.tsx @@ -19,14 +19,14 @@ import { AccessControlConstants, Show } from "@wso2is/access-control"; import { TestableComponentInterface } from "@wso2is/core/models"; import { Field, Forms } from "@wso2is/forms"; -import { Code, DocumentationLink, Hint, Text, useDocumentation } from "@wso2is/react-components"; +import { Code, DocumentationLink, Hint, Message, useDocumentation } from "@wso2is/react-components"; import classNames from "classnames"; import React, { Fragment, FunctionComponent, ReactElement, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; -import { Button, Grid, Icon, Message } from "semantic-ui-react"; +import { Button, Grid } from "semantic-ui-react"; import { identityProviderConfig } from "../../../../extensions"; -import { SimpleUserStoreListItemInterface } from "../../../applications"; +import { SimpleUserStoreListItemInterface } from "../../../applications/models"; import { AppState, ConfigReducerStateInterface } from "../../../core"; import { IdentityProviderInterface, @@ -154,33 +154,21 @@ export const JITProvisioningConfigurationsForm: FunctionComponent by default // Overriding the behaviour here to make sure it renders properly. - className="warning visible" - header={ - ( - - - Warning - - ) - } + header="Warning" content={ ( -
- - JIT user provisioning should be enabled for external identity providers - (connections) when there are MFA mechanisms - such as TOTP and Email OTP configured - in an application's login flow. - - - - Learn More - - -
+ <> + JIT user provisioning should be enabled for external identity providers + (connections) when there are MFA mechanisms + such as TOTP and Email OTP configured + in an application's login flow. + + Learn More + + ) } /> diff --git a/apps/console/src/features/identity-providers/components/forms/outbound-provisioning-connectors/common-outbound-provisioning-connector-form.tsx b/apps/console/src/features/identity-providers/components/forms/outbound-provisioning-connectors/common-outbound-provisioning-connector-form.tsx index 2b55bd6f4be..59ce9274a1f 100644 --- a/apps/console/src/features/identity-providers/components/forms/outbound-provisioning-connectors/common-outbound-provisioning-connector-form.tsx +++ b/apps/console/src/features/identity-providers/components/forms/outbound-provisioning-connectors/common-outbound-provisioning-connector-form.tsx @@ -27,11 +27,12 @@ import { CommonPluggableComponentForm } from "../components"; * @return { ReactElement } */ export const CommonOutboundProvisioningConnectorForm: FunctionComponent< - CommonPluggableComponentFormPropsInterface> = (props: CommonPluggableComponentFormPropsInterface -): ReactElement => { + CommonPluggableComponentFormPropsInterface +> = (props: CommonPluggableComponentFormPropsInterface): ReactElement => { const { metadata, + mode, initialValues, onSubmit, triggerSubmit, @@ -42,6 +43,7 @@ export const CommonOutboundProvisioningConnectorForm: FunctionComponent< return ( (undefined); const [ defaultActiveIndex, setDefaultActiveIndex ] = useState(0); + const isOrganizationEnterpriseAuthenticator = identityProvider.federatedAuthenticators + .defaultAuthenticatorId === IdentityProviderManagementConstants.ORGANIZATION_ENTERPRISE_AUTHENTICATOR_ID; + const urlSearchParams: URLSearchParams = new URLSearchParams(location.search); const idpAdvanceConfig: IdentityProviderAdvanceInterface = { @@ -127,6 +131,12 @@ export const EditIdentityProvider: FunctionComponent ( + + + + ); + const GeneralIdentityProviderSettingsTabPane = (): ReactElement => ( ); @@ -168,17 +179,19 @@ export const EditIdentityProvider: FunctionComponent ); @@ -191,6 +204,7 @@ export const EditIdentityProvider: FunctionComponent ); @@ -204,6 +218,7 @@ export const EditIdentityProvider: FunctionComponent ); @@ -217,6 +232,7 @@ export const EditIdentityProvider: FunctionComponent ); @@ -229,6 +245,8 @@ export const EditIdentityProvider: FunctionComponent ); @@ -279,7 +297,7 @@ export const EditIdentityProvider: FunctionComponent { - if (!type) { - return false; - } - - if (type === IdentityProviderManagementConstants.IDP_TEMPLATE_IDS.FACEBOOK) { - return false; - } else if (type === IdentityProviderManagementConstants.IDP_TEMPLATE_IDS.GOOGLE) { - return false; - } else if (type === IdentityProviderManagementConstants.IDP_TEMPLATE_IDS.GITHUB) { - return false; - } else if (type === IdentityProviderManagementConstants.IDP_TEMPLATE_IDS.OIDC) { - return false; - } + const isTabEnabledInExtensions: boolean | undefined = identityProviderConfig + .editIdentityProvider + .isTabEnabledForIdP(type, IdentityProviderTabTypes.USER_ATTRIBUTES); - return true; + return isTabEnabledInExtensions !== undefined + ? isTabEnabledInExtensions + : true; }; + if (!identityProvider || isLoading) { + return ; + } + return ( - identityProvider && !isLoading - ? ( - - ) - : + { + setDefaultActiveIndex(data.activeIndex); + } } + /> ); }; diff --git a/apps/console/src/features/identity-providers/components/identity-provider-list.tsx b/apps/console/src/features/identity-providers/components/identity-provider-list.tsx index f1835a1c130..6460af275f5 100644 --- a/apps/console/src/features/identity-providers/components/identity-provider-list.tsx +++ b/apps/console/src/features/identity-providers/components/identity-provider-list.tsx @@ -33,9 +33,9 @@ import { TableColumnInterface } from "@wso2is/react-components"; import React, { FunctionComponent, ReactElement, ReactNode, SyntheticEvent, useState } from "react"; -import { Trans, useTranslation } from "react-i18next"; +import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; -import { Divider, Header, Icon, List, SemanticICONS } from "semantic-ui-react"; +import { Divider, Header, Icon, Label, List, SemanticICONS } from "semantic-ui-react"; import { handleIDPDeleteError } from "./utils"; import { getApplicationDetails } from "../../applications/api"; import { ApplicationBasicInterface } from "../../applications/models"; @@ -184,6 +184,7 @@ export const IdentityProviderList: FunctionComponent { appNames.push(app.name); }); @@ -292,47 +293,61 @@ export const IdentityProviderList: FunctionComponent ( -
- { - idp.image - ? ( - - ) - : ( - - ) } - size="mini" - spaced="right" - data-testid={ `${ testId }-item-image` } - /> - ) - } - - { idp.name } - - { idp.description } - - -
- ), + render: (idp: IdentityProviderInterface): ReactNode => { + const isOrgIdp = (idp.federatedAuthenticators.defaultAuthenticatorId === + IdentityProviderManagementConstants.ORGANIZATION_ENTERPRISE_AUTHENTICATOR_ID); + + return ( +
+ { + idp.image + ? ( + + ) + : ( + + ) } + size="mini" + spaced="right" + data-testid={ `${testId}-item-image` } + /> + ) + } + + { idp.name } + { + isOrgIdp && ( + + ) + } + + { idp.description } + + +
+ ); + }, title: t("console:develop.features.idp.list.name") }, { @@ -363,8 +378,8 @@ export const IdentityProviderList: FunctionComponent hasRequiredScopes(featureConfig?.identityProviders, featureConfig?.identityProviders?.scopes?.update, allowedScopes) - ? "pencil alternate" - : "eye", + ? "pencil alternate" + : "eye", onClick: (e: SyntheticEvent, idp: IdentityProviderInterface): void => handleIdentityProviderEdit(idp.id), popupText: (): string => @@ -416,7 +431,7 @@ export const IdentityProviderList: FunctionComponent setShowDeleteConfirmationModal(false) } - type="warning" + type="negative" open={ showDeleteConfirmationModal } assertion={ deletingIDP?.name } assertionHint={ t("console:develop.features.authenticationProvider."+ @@ -436,7 +451,7 @@ export const IdentityProviderList: FunctionComponent { t("console:develop.features.idp.confirmations.deleteIDP.message") } @@ -451,7 +466,7 @@ export const IdentityProviderList: FunctionComponent setShowDeleteErrorDueToConnectedAppsModal(false) } - type="warning" + type="negative" open={ showDeleteErrorDueToConnectedAppsModal } secondaryAction={ t("common:close") } onSecondaryActionClick={ (): void => setShowDeleteErrorDueToConnectedAppsModal(false) } @@ -463,7 +478,7 @@ export const IdentityProviderList: FunctionComponent { t("console:develop.features.idp.confirmations.deleteIDPWithConnectedApps.message") } @@ -474,8 +489,8 @@ export const IdentityProviderList: FunctionComponent { isAppsLoading ? ( - - ) : + + ) : connectedApps?.map((app, index) => { return ( { app } diff --git a/apps/console/src/features/identity-providers/components/meta/authenticators.ts b/apps/console/src/features/identity-providers/components/meta/authenticators.ts index 7a556beb065..1e72b18a759 100644 --- a/apps/console/src/features/identity-providers/components/meta/authenticators.ts +++ b/apps/console/src/features/identity-providers/components/meta/authenticators.ts @@ -16,132 +16,142 @@ * under the License. */ -import { getAuthenticatorIcons, getIdPIcons } from "../../configs"; -import { FederatedAuthenticatorMetaDataInterface, StrictGenericAuthenticatorInterface } from "../../models"; +import { getIdPIcons } from "../../configs"; +import { IdentityProviderManagementConstants } from "../../constants"; +import { AuthenticatorMeta } from "../../meta"; +import { FederatedAuthenticatorMetaDataInterface } from "../../models"; -export const getFederatedAuthenticators = (): FederatedAuthenticatorMetaDataInterface[] => { +/** + * The metadata set of connectors shipped OOTB by Identity Server. + * TODO: Remove this mapping once there's an API to get the connector icons, etc. + * @returns {FederatedAuthenticatorMetaDataInterface[]} + */ +const getKnownConnectorMetadata = (): FederatedAuthenticatorMetaDataInterface[] => { return [ { - authenticatorId: "T2ZmaWNlMzY1QXV0aGVudGljYXRvcg", + authenticatorId: IdentityProviderManagementConstants.OFFICE_365_AUTHENTICATOR_ID, + description: "Seamless integration with Office 365.", displayName: "Office 365", icon: getIdPIcons().office365, - name: "Office365Authenticator" + name: IdentityProviderManagementConstants.OFFICE_365_AUTHENTICATOR_NAME }, { - authenticatorId: "VHdpdHRlckF1dGhlbnRpY2F0b3I", + authenticatorId: IdentityProviderManagementConstants.TWITTER_AUTHENTICATOR_ID, + description: "Login users with existing Twitter accounts.", displayName: "Twitter", icon: getIdPIcons().twitter, - name: "TwitterAuthenticator" + name: IdentityProviderManagementConstants.TWITTER_AUTHENTICATOR_NAME }, { - authenticatorId: "RmFjZWJvb2tBdXRoZW50aWNhdG9y", + authenticatorId: IdentityProviderManagementConstants.FACEBOOK_AUTHENTICATOR_ID, + description: "Login users with existing Facebook accounts.", displayName: "Facebook", icon: getIdPIcons().facebook, - name: "FacebookAuthenticator" + name: IdentityProviderManagementConstants.FACEBOOK_AUTHENTICATOR_NAME }, { - authenticatorId: "R29vZ2xlT0lEQ0F1dGhlbnRpY2F0b3I", - displayName: "Google OIDC", + authenticatorId: IdentityProviderManagementConstants.GOOGLE_OIDC_AUTHENTICATOR_ID, + description: "Login users with existing Google accounts.", + displayName: "Google", icon: getIdPIcons().google, - name: "GoogleOIDCAuthenticator" + name: IdentityProviderManagementConstants.GOOGLE_OIDC_AUTHENTICATOR_NAME }, { - authenticatorId: "TWljcm9zb2Z0V2luZG93c0xpdmVBdXRoZW50aWNhdG9y", + authenticatorId: IdentityProviderManagementConstants.MS_LIVE_AUTHENTICATOR_ID, + description: "Login users with their Microsoft Live accounts.", displayName: "Microsoft Windows Live", icon: getIdPIcons().microsoft, - name: "MicrosoftWindowsLiveAuthenticator" + name: IdentityProviderManagementConstants.MS_LIVE_AUTHENTICATOR_NAME }, { - authenticatorId: "UGFzc2l2ZVNUU0F1dGhlbnRpY2F0b3I", + authenticatorId: IdentityProviderManagementConstants.PASSIVE_STS_AUTHENTICATOR_ID, + description: "Login users with WS Federation.", displayName: "Passive STS", icon: getIdPIcons().wsFed, - name: "PassiveSTSAuthenticator" - }, - { - authenticatorId: "WWFob29PQXV0aDJBdXRoZW50aWNhdG9y", - displayName: "Yahoo OAuth 2", - icon: getIdPIcons().yahoo, - name: "YahooOAuth2Authenticator" + name: IdentityProviderManagementConstants.PASSIVE_STS_AUTHENTICATOR_NAME }, { - authenticatorId: "SVdBS2VyYmVyb3NBdXRoZW50aWNhdG9y", + authenticatorId: IdentityProviderManagementConstants.IWA_KERBEROS_AUTHENTICATOR_ID, + description: "Login users to Microsoft Windows servers.", displayName: "IWA Kerberos", icon: getIdPIcons().iwaKerberos, - name: "IWAKerberosAuthenticator" + name: IdentityProviderManagementConstants.IWA_KERBEROS_AUTHENTICATOR_NAME }, { - authenticatorId: "U0FNTFNTT0F1dGhlbnRpY2F0b3I", - displayName: "SAML SSO", + authenticatorId: IdentityProviderManagementConstants.SAML_AUTHENTICATOR_ID, + description: "Login users with their accounts using SAML protocol.", + displayName: "SAML", icon: getIdPIcons().saml, - name: "SAMLSSOAuthenticator" + name: IdentityProviderManagementConstants.SAML_AUTHENTICATOR_NAME }, { - authenticatorId: "T3BlbklEQ29ubmVjdEF1dGhlbnRpY2F0b3I", + authenticatorId: IdentityProviderManagementConstants.OIDC_AUTHENTICATOR_ID, + description: "Login users with their accounts using OpenID Connect protocol.", displayName: "OpenID Connect", icon: getIdPIcons().oidc, - name: "OpenIDConnectAuthenticator" + name: IdentityProviderManagementConstants.OIDC_AUTHENTICATOR_NAME }, { - authenticatorId: "RW1haWxPVFA", + authenticatorId: IdentityProviderManagementConstants.LEGACY_EMAIL_OTP_AUTHENTICATOR_ID, + description: AuthenticatorMeta + .getAuthenticatorDescription(IdentityProviderManagementConstants.LEGACY_EMAIL_OTP_AUTHENTICATOR_ID), displayName: "Email OTP", icon: getIdPIcons().emailOTP, - name: "EmailOTP" + name: IdentityProviderManagementConstants.LEGACY_EMAIL_OTP_AUTHENTICATOR_NAME }, { - authenticatorId: "U01TT1RQ", + authenticatorId: IdentityProviderManagementConstants.SMS_OTP_AUTHENTICATOR_ID, + description: AuthenticatorMeta.getAuthenticatorDescription("U01TT1RQ"), displayName: "SMS OTP", icon: getIdPIcons().smsOTP, - name: "SMSOTP" + name: IdentityProviderManagementConstants.SMS_OTP_AUTHENTICATOR_NAME } ]; }; -export const getSelectedFederatedAuthenticators = (): StrictGenericAuthenticatorInterface[] => { +/** + * The metadata set of connectors that we know are supported by Identity Server. + * TODO: Remove this mapping once there's an API to get the connector icons, etc. + * @returns {FederatedAuthenticatorMetaDataInterface[]} + */ +const getKnownExternalConnectorMetadata = (): FederatedAuthenticatorMetaDataInterface[] => { return [ { - id: "TWljcm9zb2Z0V2luZG93c0xpdmVBdXRoZW50aWNhdG9y", - image: getAuthenticatorIcons()?.microsoft, - name: "MicrosoftWindowsLiveAuthenticator" - }, - { - id: "R29vZ2xlT0lEQ0F1dGhlbnRpY2F0b3I", - image: getAuthenticatorIcons()?.google, - name: "GoogleOIDCAuthenticator" - }, - { - id: "U01TT1RQ", - image: getAuthenticatorIcons()?.smsOTP, - name: "SMSOTP" - }, - { - id: "VHdpdHRlckF1dGhlbnRpY2F0b3I", - image: getAuthenticatorIcons()?.twitter, - name: "TwitterAuthenticator" - }, - { - id: "RW1haWxPVFA", - image: getAuthenticatorIcons()?.emailOTP, - name: "EmailOTP" - }, - { - id: "WWFob29PQXV0aDJBdXRoZW50aWNhdG9y", - image: getAuthenticatorIcons()?.yahoo, - name: "YahooOAuth2Authenticator" - }, - { - id: "SVdBS2VyYmVyb3NBdXRoZW50aWNhdG9y", - image: undefined, - name: "IWAKerberosAuthenticator" - }, - { - id: "RmFjZWJvb2tBdXRoZW50aWNhdG9y", - image: getAuthenticatorIcons()?.facebook, - name: "FacebookAuthenticator" + authenticatorId: IdentityProviderManagementConstants.YAHOO_AUTHENTICATOR_ID, + description: "Login users with their Yahoo accounts.", + displayName: "Yahoo", + icon: getIdPIcons().yahoo, + name: IdentityProviderManagementConstants.YAHOO_AUTHENTICATOR_NAME }, { - id: "T2ZmaWNlMzY1QXV0aGVudGljYXRvcg", - image: getAuthenticatorIcons()?.office365, - name: "Office365Authenticator" + authenticatorId: IdentityProviderManagementConstants.GITHUB_AUTHENTICATOR_ID, + description: "Login users with existing GitHub accounts", + displayName: "GitHub", + icon: getIdPIcons().github, + name: IdentityProviderManagementConstants.GITHUB_AUTHENTICATOR_NAME } ]; }; + +/** + * The metadata set of connectors that are added by user. + * TODO: Remove this mapping once there's an API to get the connector icons, etc. + * @returns {FederatedAuthenticatorMetaDataInterface[]} + */ +const getExternalConnectorMetadataExtensions = (): FederatedAuthenticatorMetaDataInterface[] => { + return window[ "AppUtils" ]?.getConfig()?.extensions?.connectors ?? []; +}; + +/** + * The metadata set of all the connectors that are available. + * TODO: Remove this mapping once there's an API to get the connector icons, etc. + * @returns {FederatedAuthenticatorMetaDataInterface[]} + */ +export const getConnectorMetadata = (): FederatedAuthenticatorMetaDataInterface[] => { + + return [ + ...getKnownConnectorMetadata(), + ...getKnownExternalConnectorMetadata(), + ...getExternalConnectorMetadataExtensions() + ]; +}; diff --git a/apps/console/src/features/identity-providers/components/meta/index.ts b/apps/console/src/features/identity-providers/components/meta/index.ts index 5f129dd94e1..2dd5ff818c4 100644 --- a/apps/console/src/features/identity-providers/components/meta/index.ts +++ b/apps/console/src/features/identity-providers/components/meta/index.ts @@ -17,5 +17,4 @@ */ export * from "./authenticators"; -export * from "./templates"; export * from "./connectors"; diff --git a/apps/console/src/features/identity-providers/components/meta/templates.ts b/apps/console/src/features/identity-providers/components/meta/templates.ts deleted file mode 100644 index 96d50f39aca..00000000000 --- a/apps/console/src/features/identity-providers/components/meta/templates.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. - * - * WSO2 Inc. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export const ExpertModeTemplate = { - category: "DEFAULT", - description: "Create a new Identity Provider with minimum configurations", - displayOrder: 1, - id: "expert-mode", - idp: { - certificate: {}, - claims: { - provisioningClaims: [], - roleClaim: { - uri: "" - }, - userIdClaim: { - uri: "" - } - }, - description: "", - federatedAuthenticators: { - authenticators: [], - defaultAuthenticatorId: "" - }, - homeRealmIdentifier: "", - image: "", - isFederationHub: false, - isPrimary: false, - name: "", - provisioning: {}, - roles: { - mappings: [], - outboundProvisioningRoles: [] - } - }, - image: "expert", - name: "Expert Mode", - services: [] -}; diff --git a/apps/console/src/features/identity-providers/components/settings/advance-settings.tsx b/apps/console/src/features/identity-providers/components/settings/advance-settings.tsx index ba93e7c1d66..b7a1f70416e 100644 --- a/apps/console/src/features/identity-providers/components/settings/advance-settings.tsx +++ b/apps/console/src/features/identity-providers/components/settings/advance-settings.tsx @@ -43,10 +43,18 @@ interface AdvanceSettingsPropsInterface extends TestableComponentInterface { * Callback to update the idp details. */ onUpdate: (id: string) => void; + /** + * Is the idp info request loading. + */ + isLoading?: boolean; /** * Specifies if the component should only be read-only. */ isReadOnly: boolean; + /** + * Loading Component. + */ + loader: () => ReactElement; } /** @@ -64,6 +72,8 @@ export const AdvanceSettings: FunctionComponent = advancedConfigurations, onUpdate, isReadOnly, + isLoading, + loader: Loader, [ "data-testid" ]: testId } = props; @@ -100,6 +110,10 @@ export const AdvanceSettings: FunctionComponent = }); }; + if (isLoading) { + return ; + } + return ( ( - + diff --git a/apps/console/src/features/identity-providers/components/settings/attribute-management/role-mapping-settings.tsx b/apps/console/src/features/identity-providers/components/settings/attribute-management/role-mapping-settings.tsx index 597b788d367..063e7a36183 100644 --- a/apps/console/src/features/identity-providers/components/settings/attribute-management/role-mapping-settings.tsx +++ b/apps/console/src/features/identity-providers/components/settings/attribute-management/role-mapping-settings.tsx @@ -20,8 +20,14 @@ import { RoleListInterface, RolesInterface, TestableComponentInterface } from "@ import { DynamicField, Heading, Hint } from "@wso2is/react-components"; import React, { FunctionComponent, ReactElement, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; +import { useSelector } from "react-redux"; import { Grid } from "semantic-ui-react"; -import { getGroupList } from "../../../../groups/api"; +import { AppState } from "../../../../core"; +import { getOrganizationRoles } from "../../../../organizations/api"; +import { OrganizationRoleListItemInterface } from "../../../../organizations/models"; +import { OrganizationUtils } from "../../../../organizations/utils"; +import { getRolesList } from "../../../../roles"; +import { IdentityProviderConstants } from "../../../constants"; import { IdentityProviderRoleMappingInterface } from "../../../models"; import { handleGetRoleListError } from "../../utils"; @@ -60,7 +66,8 @@ export const RoleMappingSettings: FunctionComponent { - const [roleList, setRoleList] = useState(); + const currentOrganization = useSelector((state: AppState) => state.organization.organization); + const [ roleList, setRoleList ] = useState(); const { onSubmit, @@ -76,7 +83,7 @@ export const RoleMappingSettings: FunctionComponent { - const filterRole: RolesInterface[] = roleList.filter( + const filterRole: RolesInterface[] | OrganizationRoleListItemInterface[] = roleList.filter( (role) => { return !(role.displayName.includes("Application/") || role.displayName.includes("Internal/")); }); @@ -90,17 +97,60 @@ export const RoleMappingSettings: FunctionComponent { - getGroupList(null) - .then((response) => { - if (response.status === 200) { - const allRole: RoleListInterface = response.data; - setRoleList(allRole.Resources); - } - }) - .catch((error) => { - handleGetRoleListError(error); - }); - }, [initialRoleMappings]); + if (OrganizationUtils.isCurrentOrganizationRoot()) { + getRolesList(null) + .then((response) => { + if (response.status === 200) { + const allRole: RoleListInterface = response.data; + + setRoleList(allRole.Resources); + } + }) + .catch((error) => { + handleGetRoleListError(error); + }); + } else { + getOrganizationRoles(currentOrganization.id, null, null, null) + .then((response) => { + setRoleList(response.Resources); + }); + } + }, [ initialRoleMappings ]); + + + /** + * Prepends `Internal/` to the role name if it does not have a domain prepended already. + * + * @param {string} role The role name as received from the API response. + * + * @returns {string} + */ + const resolveRoleName = (role: string): string => { + if (role.split("/").length === 1) { + return `${ IdentityProviderConstants.INTERNAL_DOMAIN }${ role }`; + } + + return role; + }; + + /** + * Removes `Internal/` part from the role name if it is present. + * + * @param {string} role The role name as received from the API response. + * + * @returns {string} + */ + const resolveRoleDisplayName = (role: string): string => { + const roleParts: string[] = role.split("/"); + + if (roleParts.length > 1) { + if (roleParts[ 0 ] === IdentityProviderConstants.INTERNAL_DOMAIN.slice(0, -1)) { + return roleParts[ 1 ]; + } + } + + return role; + }; return ( <> @@ -115,7 +165,7 @@ export const RoleMappingSettings: FunctionComponent { return { - key: mapping.localRole, + key: resolveRoleDisplayName(mapping.localRole), value: mapping.idpRole }; }) : [] @@ -131,14 +181,15 @@ export const RoleMappingSettings: FunctionComponent { + listen={ (data) => { if (data.length > 0) { const finalData: IdentityProviderRoleMappingInterface[] = data.map(mapping => { return { idpRole: mapping.value, - localRole: mapping.key + localRole: resolveRoleName(mapping.key) }; }); + onSubmit(finalData); } else { onSubmit([]); diff --git a/apps/console/src/features/identity-providers/components/settings/attribute-management/uri-attributes-settings.tsx b/apps/console/src/features/identity-providers/components/settings/attribute-management/uri-attributes-settings.tsx index f62fe96cdb0..140abce86d6 100644 --- a/apps/console/src/features/identity-providers/components/settings/attribute-management/uri-attributes-settings.tsx +++ b/apps/console/src/features/identity-providers/components/settings/attribute-management/uri-attributes-settings.tsx @@ -46,6 +46,10 @@ interface AdvanceAttributeSettingsPropsInterface extends TestableComponentInterf * Specifies if the component should only be read-only. */ isReadOnly: boolean; + /** + * Is the IdP type SAML + */ + isSaml: boolean; } export const UriAttributesSettings: FunctionComponent = ( @@ -63,6 +67,7 @@ export const UriAttributesSettings: FunctionComponent - - The attribute that identifies the user at the enterprise identity provider. - When attributes are configured based on the authentication response of this IdP connection, - you can use one of them as the subject. Otherwise, the - default saml2:Subject in the SAML response is used as the subject attribute. - + { isSaml + ? ( + + The attribute that identifies the user at the enterprise identity provider. + When attributes are configured based on the authentication response of this + IdP connection, you can use one of them as the subject. Otherwise, the + default saml2:Subject in the SAML response is used as the + subject attribute. + + ) + : ( + + Specifies the attribute that identifies the user at the identity provider. + + ) + } diff --git a/apps/console/src/features/identity-providers/components/settings/attribute-settings.tsx b/apps/console/src/features/identity-providers/components/settings/attribute-settings.tsx index d980937aabb..76d1eb0c1be 100644 --- a/apps/console/src/features/identity-providers/components/settings/attribute-settings.tsx +++ b/apps/console/src/features/identity-providers/components/settings/attribute-settings.tsx @@ -17,11 +17,10 @@ */ import { AccessControlConstants, Show } from "@wso2is/access-control"; -import { getAllLocalClaims } from "@wso2is/core/api"; import { AlertLevels, Claim, TestableComponentInterface } from "@wso2is/core/models"; import { addAlert } from "@wso2is/core/store"; import { useTrigger } from "@wso2is/forms"; -import { ContentLoader, EmphasizedSegment } from "@wso2is/react-components"; +import { EmphasizedSegment } from "@wso2is/react-components"; import isEmpty from "lodash-es/isEmpty"; import React, { FunctionComponent, ReactElement, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -29,6 +28,7 @@ import { useDispatch } from "react-redux"; import { Button, Divider, Grid } from "semantic-ui-react"; import { AttributeSelection, RoleMappingSettings, UriAttributesSettings } from "./attribute-management"; import { AttributesSelectionV2 } from "./attribute-management/attribute-selection-v2"; +import { getAllLocalClaims } from "../../../claims/api"; import { IdentityProviderClaimInterface, IdentityProviderClaimMappingInterface, @@ -108,6 +108,14 @@ interface AttributeSelectionPropsInterface extends TestableComponentInterface { * Specifies if the component should only be read-only. */ isReadOnly: boolean; + /** + * Loading Component. + */ + loader: () => ReactElement; + /** + * Is the IdP type SAML + */ + isSaml: boolean; } export const LocalDialectURI = "http://wso2.org/claims"; @@ -126,6 +134,8 @@ export const AttributeSettings: FunctionComponent([]); + const [ availableLocalClaims, setAvailableLocalClaims ] = useState([]); // Selected local claims in claim mapping. - const [selectedClaimsWithMapping, setSelectedClaimsWithMapping] + const [ selectedClaimsWithMapping, setSelectedClaimsWithMapping ] = useState([]); // Selected provisioning claims. - const [selectedProvisioningClaimsWithDefaultValue, setSelectedProvisioningClaimsWithDefaultValue] + const [ selectedProvisioningClaimsWithDefaultValue, setSelectedProvisioningClaimsWithDefaultValue ] = useState([]); // Selected subject. - const [subjectClaimUri, setSubjectClaimUri] = useState(); + const [ subjectClaimUri, setSubjectClaimUri ] = useState(); // Selected role. - const [roleClaimUri, setRoleClaimUri] = useState(); + const [ roleClaimUri, setRoleClaimUri ] = useState(); // Sets if the form is submitting. const [ isSubmitting, setIsSubmitting ] = useState(false); @@ -157,11 +167,11 @@ export const AttributeSettings: FunctionComponent(false); // Selected role mapping. - const [roleMapping, setRoleMapping] = useState(undefined); + const [ roleMapping, setRoleMapping ] = useState(undefined); const [ isSubmissionLoading, setIsSubmissionLoading ] = useState(false); // Trigger role mapping field to submission. - const [triggerSubmission, setTriggerSubmission] = useTrigger(); + const [ triggerSubmission, setTriggerSubmission ] = useTrigger(); /** * When IdP loads, this component is responsible for fetching the @@ -209,7 +219,7 @@ export const AttributeSettings: FunctionComponent { // Provisioning claims, subject URI and role UR depend on the IdP claim mapping unless there are no claim @@ -237,7 +247,7 @@ export const AttributeSettings: FunctionComponent { @@ -271,6 +281,7 @@ export const AttributeSettings: FunctionComponent element.uri === subjectClaimUri); + claimConfigurations["userIdClaim"] = matchingLocalClaim ? matchingLocalClaim : { uri: subjectClaimUri } as IdentityProviderClaimInterface; @@ -285,6 +296,7 @@ export const AttributeSettings: FunctionComponent element.uri === roleClaimUri); + claimConfigurations[ "roleClaim" ] = matchingLocalClaim ? matchingLocalClaim : { uri: roleClaimUri } as IdentityProviderClaimInterface; } else { @@ -316,119 +328,120 @@ export const AttributeSettings: FunctionComponent; + } return ( - !isLoading && !isLocalClaimsLoading - ? ( - - -
- - - { - setSelectedClaimsWithMapping([ ...mappingsToBeAdded ]); - } } - attributeList={ - hideIdentityClaimAttributes - ? availableLocalClaims.filter(({ uri }) => !isLocalIdentityClaim(uri)) - : availableLocalClaims - } - mappedAttributesList={ [ ...selectedClaimsWithMapping ] } - isReadOnly = { isReadOnly } - /> - - -
+
+
); }; diff --git a/apps/console/src/features/identity-providers/components/settings/authenticator-settings.tsx b/apps/console/src/features/identity-providers/components/settings/authenticator-settings.tsx index 4e1f18f8409..71e2330f1d5 100644 --- a/apps/console/src/features/identity-providers/components/settings/authenticator-settings.tsx +++ b/apps/console/src/features/identity-providers/components/settings/authenticator-settings.tsx @@ -21,28 +21,33 @@ import { AlertLevels, TestableComponentInterface } from "@wso2is/core/models"; import { addAlert } from "@wso2is/core/store"; import { ConfirmationModal, - ContentLoader, EmphasizedSegment, EmptyPlaceholder, PrimaryButton, SegmentedAccordionTitleActionInterface } from "@wso2is/react-components"; +import cloneDeep from "lodash-es/cloneDeep"; import isEmpty from "lodash-es/isEmpty"; -import React, { FormEvent, FunctionComponent, MouseEvent, ReactElement, useEffect, useState } from "react"; +import keyBy from "lodash-es/keyBy"; +import React, { FormEvent, FunctionComponent, MouseEvent, ReactElement, useEffect, useMemo, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; -import { CheckboxProps, Grid, Icon } from "semantic-ui-react"; +import { AccordionTitleProps, CheckboxProps, Grid, Icon } from "semantic-ui-react"; +import { identityProviderConfig } from "../../../../extensions/configs/identity-provider"; import { AppState, ConfigReducerStateInterface, getEmptyPlaceholderIllustrations } from "../../../core"; +import { AuthenticatorAccordion } from "../../../core/components"; import { getFederatedAuthenticatorDetails, getFederatedAuthenticatorMeta, getIdentityProviderTemplate, - getIdentityProviderTemplateList, updateFederatedAuthenticator, updateFederatedAuthenticators } from "../../api"; +import { getIdPIcons } from "../../configs/ui"; import { IdentityProviderManagementConstants } from "../../constants"; +import ExpertModeIdpTemplate from "../../data/identity-provider-templates/templates/expert-mode/expert-mode.json"; import { + AuthenticatorSettingsFormModes, CommonPluggableComponentMetaPropertyInterface, CommonPluggableComponentPropertyInterface, FederatedAuthenticatorListItemInterface, @@ -50,15 +55,16 @@ import { FederatedAuthenticatorWithMetaInterface, IdentityProviderInterface, IdentityProviderTemplateInterface, + IdentityProviderTemplateItemInterface, IdentityProviderTemplateListItemInterface, - IdentityProviderTemplateListResponseInterface + IdentityProviderTemplateLoadingStrategies } from "../../models"; +import { IdentityProviderManagementUtils, IdentityProviderTemplateManagementUtils } from "../../utils"; import { AuthenticatorFormFactory } from "../forms"; -import { getFederatedAuthenticators } from "../meta"; +import { getConnectorMetadata } from "../meta"; import { handleGetFederatedAuthenticatorMetadataAPICallError, - handleGetIDPTemplateAPICallError, - handleGetIDPTemplateListError + handleGetIDPTemplateAPICallError } from "../utils"; import { AuthenticatorCreateWizard } from "../wizards/authenticator-create-wizard"; @@ -82,9 +88,12 @@ interface IdentityProviderSettingsPropsInterface extends TestableComponentInterf * Specifies if the component should only be read-only. */ isReadOnly: boolean; + /** + * Loading Component. + */ + loader: () => ReactElement; } -const GOOGLE_CLIENT_ID_SECRET_MAX_LENGTH = 100; const OIDC_CLIENT_ID_SECRET_MAX_LENGTH = 100; const URL_MAX_LENGTH: number = 2048; const AUTHORIZED_REDIRECT_URL: string = "callbackUrl"; @@ -104,6 +113,7 @@ export const AuthenticatorSettings: FunctionComponent { + return state.identityProvider.meta.authenticators; + }); + const identityProviderTemplates: IdentityProviderTemplateItemInterface[] = useSelector( + (state: AppState) => state.identityProvider?.groupedTemplates); + const [ showDeleteConfirmationModal, setShowDeleteConfirmationModal ] = useState(false); const [ deletingAuthenticator, @@ -120,40 +136,106 @@ export const AuthenticatorSettings: FunctionComponent([]); const [ availableTemplates, setAvailableTemplates ] = useState(undefined); - const [ availableManualModeOptions, setAvailableManualModeOptions ] = - useState(undefined); + const [ + availableManualModeOptions, + setAvailableManualModeOptions + ] = useState(undefined); const [ showAddAuthenticatorWizard, setShowAddAuthenticatorWizard ] = useState(false); - const [ isTemplatesLoading, setIsTemplatesLoading ] = useState(false); - const [ isPageLoading, setIsPageLoading ] = useState(true); const [ isSubmitting, setIsSubmitting ] = useState(false); + const [ accordionActiveIndexes, setAccordionActiveIndexes ] = useState([]); + const [ isIdPTemplateFetchRequestLoading, setIdPTemplateFetchRequestLoading ] = useState(undefined); + const [ + isFederatedAuthenticatorFetchRequestLoading, + setFederatedAuthenticatorFetchRequestLoading + ] = useState(undefined); const config: ConfigReducerStateInterface = useSelector((state: AppState) => state.config); + const isActiveTemplateExpertMode: boolean = useMemo(() => { + return identityProviderConfig?.templates?.expertMode && + (identityProvider.templateId === ExpertModeIdpTemplate.id || !identityProvider.templateId); + }, [ identityProvider, identityProviderConfig ]); + + /** + * When `availableAuthenticators` updates, filter the templates. + */ + useEffect(() => { + + filterTemplates(); + }, [ availableAuthenticators ]); + + /** + * Get pre-defined IDP templates. + */ + useEffect(() => { + + if (identityProviderTemplates !== undefined) { + return; + } + + setIdPTemplateFetchRequestLoading(true); + + const useAPI: boolean = config.ui.identityProviderTemplateLoadingStrategy + ? config.ui.identityProviderTemplateLoadingStrategy === IdentityProviderTemplateLoadingStrategies.REMOTE + : (IdentityProviderManagementConstants.DEFAULT_IDP_TEMPLATE_LOADING_STRATEGY === + IdentityProviderTemplateLoadingStrategies.REMOTE); + + /** + * With {@link skipGrouping} being {@code false} we say + * we need to group the existing templates based on their + * template-group. + */ + const skipGrouping = false, sortTemplates = true; + + IdentityProviderTemplateManagementUtils + .getIdentityProviderTemplates(useAPI, skipGrouping, sortTemplates) + .finally(() => setIdPTemplateFetchRequestLoading(false)); + }, [ identityProviderTemplates ]); + + /** + * If `availableFederatedAuthenticators` is not in redux, + * Fetch it again and store in the store. + */ + useEffect(() => { + if (!isEmpty(availableFederatedAuthenticators)) { + return; + } + + IdentityProviderManagementUtils.getAuthenticators(); + }, [ availableFederatedAuthenticators ]); + /** * Handles the authenticator config form submit action. * * @param values - Form values. */ - const handleAuthenticatorConfigFormSubmit = (values: FederatedAuthenticatorListItemInterface): void => { + const handleAuthenticatorConfigFormSubmit = (values: FederatedAuthenticatorListItemInterface, + isDefaultAuthSet: boolean = true): void => { addCallbackUrl(values); - setIsPageLoading(true); - // Special checks on Google IDP - if (values.authenticatorId === "R29vZ2xlT0lEQ0F1dGhlbnRpY2F0b3I") { - // Enable/disable the Google authenticator based on client id and secret - const props: CommonPluggableComponentPropertyInterface[] = values.properties; - let isEnabled = true; - props.forEach((prop: CommonPluggableComponentPropertyInterface) => { - if (prop.key === "ClientId" || prop.key === "ClientSecret") { - if (isEmpty(prop.value)) { - isEnabled = false; + + // Only execute this when not in the expert mode since it makes it impossible + // to disable a Google authenticator in expert mode. + if (!isActiveTemplateExpertMode) { + // Special checks on Google IDP + if (values.authenticatorId === IdentityProviderManagementConstants.GOOGLE_OIDC_AUTHENTICATOR_ID) { + // Enable/disable the Google authenticator based on client id and secret + const props: CommonPluggableComponentPropertyInterface[] = values.properties; + let isEnabled = true; + + props.forEach((prop: CommonPluggableComponentPropertyInterface) => { + if (prop.key === "ClientId" || prop.key === "ClientSecret") { + if (isEmpty(prop.value)) { + isEnabled = false; + } } - } - }); - values.isEnabled = isEnabled; + }); + + values.isEnabled = isEnabled; - // Remove scopes - removeElementFromProps(props, "scopes"); + // Remove scopes + removeElementFromProps(props, "scopes"); + } } /** @@ -167,15 +249,17 @@ export const AuthenticatorSettings: FunctionComponent { - dispatch(addAlert({ - description: t("console:develop.features.authenticationProvider" + - ".notifications.updateFederatedAuthenticator." + - "success.description"), - level: AlertLevels.SUCCESS, - message: t("console:develop.features.authenticationProvider.notifications." + - "updateFederatedAuthenticator." + - "success.message") - })); + if (isDefaultAuthSet) { + dispatch(addAlert({ + description: t("console:develop.features.authenticationProvider" + + ".notifications.updateFederatedAuthenticator." + + "success.description"), + level: AlertLevels.SUCCESS, + message: t("console:develop.features.authenticationProvider.notifications." + + "updateFederatedAuthenticator." + + "success.message") + })); + } onUpdate(identityProvider.id); }) .catch((error) => { @@ -205,7 +289,7 @@ export const AuthenticatorSettings: FunctionComponent { setIsSubmitting(false); - }); + }); }; /** @@ -222,6 +306,7 @@ export const AuthenticatorSettings: FunctionComponent object.key === AUTHORIZED_REDIRECT_URL; const index: number = authenticator?.data?.properties.findIndex(search); + if (index >= 0) { values.properties.push(authenticator.data.properties[index]); } @@ -232,7 +317,7 @@ export const AuthenticatorSettings: FunctionComponent { - if (isEmpty(identityProvider.federatedAuthenticators)) { + if (isEmpty(identityProvider.federatedAuthenticators?.authenticators)) { return; } - setIsPageLoading(true); + + setFederatedAuthenticatorFetchRequestLoading(true); setAvailableAuthenticators([]); fetchAuthenticators() .then((res) => { const authenticator = res[ 0 ].data; - authenticator.isEnabled = true; + + // TODO: Validate if this is necessary to do on the FE side. + // Added with: + // https://github.com/wso2/identity-apps/pull/2053/commits/177e8475aa3e48a7933f877a25c82306b7b3739f + // This poses issues with the enable toggle in IdP expert mode. So, not executing when in expert mode. + if (!isActiveTemplateExpertMode) { + authenticator.isEnabled = true; + } // Make default authenticator if not added. if (!identityProvider.federatedAuthenticators.defaultAuthenticatorId && identityProvider.federatedAuthenticators.authenticators.length > 0) { authenticator.isDefault = true; - handleAuthenticatorConfigFormSubmit(authenticator); + + const isDefaultAuthIdSet = Boolean( + identityProvider?.federatedAuthenticators?.defaultAuthenticatorId + ); + + handleAuthenticatorConfigFormSubmit(authenticator, isDefaultAuthIdSet); } setAvailableAuthenticators(res); - setIsPageLoading(false); - }); + }) + .finally(() => setFederatedAuthenticatorFetchRequestLoading(false)); }, [ identityProvider?.federatedAuthenticators ]); /** @@ -320,6 +420,7 @@ export const AuthenticatorSettings: FunctionComponent, data: CheckboxProps, id: string): void => { const authenticator = availableAuthenticators.find(authenticator => (authenticator.id === id)).data; + authenticator.isDefault = data.checked; handleAuthenticatorConfigFormSubmit(authenticator); }; @@ -333,6 +434,7 @@ export const AuthenticatorSettings: FunctionComponent, data: CheckboxProps, id: string): void => { const authenticator = availableAuthenticators.find(authenticator => (authenticator.id === id)).data; + // Validation if (authenticator.isDefault && !data.checked) { dispatch(addAlert({ @@ -432,6 +534,7 @@ export const AuthenticatorSettings: FunctionComponent { - setIsTemplatesLoading(true); - setShowAddAuthenticatorWizard(false); - - // Get the list of available templates from the server - getIdentityProviderTemplateList() - .then((response: IdentityProviderTemplateListResponseInterface) => { - if (!response?.totalResults) { - return; - } - // Load all templates - fetchIDPTemplates(response?.templates) - .then((templates) => { + const filterTemplates = (): void => { - // Filter out already added authenticators and templates with federated authenticators. - const availableAuthenticatorIDs = availableAuthenticators.map((a) => { - return a.id; - }); - const filteredTemplates = templates.filter((template) => - (template.idp.federatedAuthenticators.defaultAuthenticatorId && - !availableAuthenticatorIDs.includes( - template.idp.federatedAuthenticators.defaultAuthenticatorId)) - ); - - // Set filtered manual mode options. - setAvailableManualModeOptions(getFederatedAuthenticators().filter(a => - !availableAuthenticatorIDs.includes(a.authenticatorId))); - - // sort templateList based on display Order - filteredTemplates.sort((a, b) => (a.displayOrder > b.displayOrder) ? 1 : -1); - - setAvailableTemplates(filteredTemplates); - setShowAddAuthenticatorWizard(true); - }); - }) - .catch((error) => { - handleGetIDPTemplateListError(error); - }) - .finally(() => { - setIsTemplatesLoading(false); - }); + // Filter out already added authenticators and templates with federated authenticators. + const availableAuthenticatorIDs = availableAuthenticators?.map((a) => { + return a.id; + }); + + const filteredTemplates = identityProviderTemplates?.filter((template) => + (template?.idp?.federatedAuthenticators?.defaultAuthenticatorId && + !availableAuthenticatorIDs?.includes( + template?.idp?.federatedAuthenticators?.defaultAuthenticatorId)) + ); + + // sort templateList based on display Order + filteredTemplates?.sort((a, b) => (a?.displayOrder > b?.displayOrder) ? 1 : -1); + + const flattenedConnectorMetadata: ({ [ key: string ]: FederatedAuthenticatorMetaDataInterface }) = keyBy( + getConnectorMetadata(), "authenticatorId" + ); + let moderatedManualModeOptions: FederatedAuthenticatorMetaDataInterface[] = cloneDeep( + availableFederatedAuthenticators as FederatedAuthenticatorMetaDataInterface[] + ); + + moderatedManualModeOptions = moderatedManualModeOptions?.map((option) => { + return { + ...option, + ...flattenedConnectorMetadata[ option?.authenticatorId ] + }; + }); + + moderatedManualModeOptions = moderatedManualModeOptions?.filter(a => + !availableAuthenticatorIDs.includes(a?.authenticatorId)); + + setAvailableManualModeOptions(moderatedManualModeOptions); + setAvailableTemplates(filteredTemplates); }; /** @@ -498,9 +596,11 @@ export const AuthenticatorSettings: FunctionComponent { const isDefaultAuthenticator = isDefaultAuthenticatorPredicate(authenticator); + return [ // Checkbox which triggers the default state of authenticator. { @@ -581,18 +682,187 @@ export const AuthenticatorSettings: FunctionComponent= 0) { properties.splice(dataIndex, 1); } }; + /** + * Handles accordion title click. + * + * @param {React.SyntheticEvent} e - Click event. + * @param {AccordionTitleProps} SegmentedAuthenticatedAccordion - Clicked title. + */ + const handleAccordionOnClick = (e: MouseEvent, + SegmentedAuthenticatedAccordion: AccordionTitleProps): void => { + if (!SegmentedAuthenticatedAccordion) { + return; + } + const newIndexes = [ ...accordionActiveIndexes ]; + + if (newIndexes.includes(SegmentedAuthenticatedAccordion.accordionIndex)) { + const removingIndex = newIndexes.indexOf(SegmentedAuthenticatedAccordion.accordionIndex); + + newIndexes.splice(removingIndex, 1); + } else { + newIndexes.push(SegmentedAuthenticatedAccordion.accordionIndex); + } + + setAccordionActiveIndexes(newIndexes); + }; + + /** + * Shows the authenticator list. + * + * @returns {ReactElement} + */ + const showAuthenticatorList = (): ReactElement => { + + const resolveAuthenticatorIcon = (authenticator: FederatedAuthenticatorWithMetaInterface) => { + const found: FederatedAuthenticatorMetaDataInterface = getConnectorMetadata() + .find((fedAuth: FederatedAuthenticatorMetaDataInterface) => { + if (fedAuth?.authenticatorId === authenticator?.id) { + return fedAuth; + } + + return null; + }); + + if (!found?.icon) { + return getIdPIcons().default; + } + + return found.icon; + }; + + const resolveAuthenticatorDisplayName = (authenticator: FederatedAuthenticatorWithMetaInterface) => { + const found: FederatedAuthenticatorMetaDataInterface = getConnectorMetadata() + .find((fedAuth: FederatedAuthenticatorMetaDataInterface) => { + if (fedAuth?.authenticatorId === authenticator?.id) { + return fedAuth; + } + + return null; + }); + + if (!found?.displayName) { + return authenticator.meta?.displayName || authenticator?.data?.name || authenticator.id; + } + + return found.displayName; + }; + + return ( + + + + + + { t("console:develop.features.authenticationProvider.buttons.addAuthenticator") } + + + + + + { + availableAuthenticators.map((authenticator, index) => { + return ( + + ), + hideChevron: isEmpty(authenticator.meta?.properties), + icon: { + icon: resolveAuthenticatorIcon(authenticator) + }, + id: authenticator?.id, + title: resolveAuthenticatorDisplayName(authenticator) + } + ] + } + accordionActiveIndexes={ accordionActiveIndexes } + accordionIndex={ index } + handleAccordionOnClick={ handleAccordionOnClick } + data-testid={ `${ testId }-accordion` } + /> + ); + }) + } + + + + ); + }; + + /** + * Handles the Add authenticator button clicks. + */ + const handleAddAuthenticator = (): void => { + + filterTemplates(); + setShowAddAuthenticatorWizard(true); + }; + + /** + * Shows the Authenticator settings. + */ const showAuthenticator = (): ReactElement => { if (availableAuthenticators.length > 0) { + // Only show the Authenticator listing if `expertMode` is enabled. + if (isActiveTemplateExpertMode) { + return showAuthenticatorList(); + } + const authenticator: FederatedAuthenticatorWithMetaInterface = availableAuthenticators.find(authenticator => ( identityProvider.federatedAuthenticators.defaultAuthenticatorId === authenticator.id )); + if (!authenticator) { + return; + } + // TODO: Need to update below values in the OIDC authenticator metadata API // Set additional meta data if the authenticator is OIDC if (authenticator.id === IdentityProviderManagementConstants.OIDC_AUTHENTICATOR_ID) { @@ -644,6 +914,7 @@ export const AuthenticatorSettings: FunctionComponent ); } else { @@ -680,8 +954,11 @@ export const AuthenticatorSettings: FunctionComponent - + { t("console:develop.features.authenticationProvider.buttons.addAuthenticator") } @@ -705,93 +982,104 @@ export const AuthenticatorSettings: FunctionComponent; + } + return ( - (!isLoading && !isPageLoading) - ? ( -
- - - - - { showAuthenticator() } - - - - - - { /*
- ) - : +
+ + + + + { showAuthenticator() } + + + + + + { /*
); }; diff --git a/apps/console/src/features/identity-providers/components/settings/general-settings.tsx b/apps/console/src/features/identity-providers/components/settings/general-settings.tsx index bdef597602d..8c1a0a5394d 100644 --- a/apps/console/src/features/identity-providers/components/settings/general-settings.tsx +++ b/apps/console/src/features/identity-providers/components/settings/general-settings.tsx @@ -21,7 +21,7 @@ import { AlertLevels, TestableComponentInterface } from "@wso2is/core/models"; import { addAlert } from "@wso2is/core/store"; import { ConfirmationModal, ContentLoader, DangerZone, DangerZoneGroup } from "@wso2is/react-components"; import React, { FunctionComponent, ReactElement, useEffect, useState } from "react"; -import { Trans, useTranslation } from "react-i18next"; +import { useTranslation } from "react-i18next"; import { useDispatch } from "react-redux"; import { CheckboxProps, Divider, List } from "semantic-ui-react"; import { getApplicationDetails } from "../../../applications/api"; @@ -96,6 +96,10 @@ interface GeneralSettingsInterface extends TestableComponentInterface { * IdP is a OIDC provider or not. */ isOidc?: boolean; + /** + * Loading Component. + */ + loader: () => ReactElement; } /** @@ -121,6 +125,7 @@ export const GeneralSettings: FunctionComponent = ( hideIdPLogoEditField, isSaml, isOidc, + loader: Loader, [ "data-testid" ]: testId } = props; @@ -184,6 +189,7 @@ export const GeneralSettings: FunctionComponent = ( ); const appNames: string[] = []; + results.forEach((app) => { appNames.push(app.name); }); @@ -306,16 +312,17 @@ export const GeneralSettings: FunctionComponent = (
+ + + + + { t("console:manage.features.user.updateUser.groups." + + "editGroups.groupList.headers.0") } + + + + + { t("console:manage.features.user.updateUser.groups." + + "editGroups.groupList.headers.1") } + + + + + { resolveTableContent() } +
+
+
+ ) : ( + !isLoadingAssignedGroups + ? ( + + + { t("console:manage.features.roles.edit.groups." + + "emptyPlaceholder.action") } + + ) + } + image={ getEmptyPlaceholderIllustrations().emptyList } + imageSize="tiny" + /> + + ) + : + ) + } + + + + { addNewGroupModal() } + + ); +}; diff --git a/apps/console/src/features/organizations/components/edit-organization-role/edit-organization-permission.tsx b/apps/console/src/features/organizations/components/edit-organization-role/edit-organization-permission.tsx new file mode 100644 index 00000000000..64aaab35733 --- /dev/null +++ b/apps/console/src/features/organizations/components/edit-organization-role/edit-organization-permission.tsx @@ -0,0 +1,160 @@ +/** + * Copyright (c) 2022, WSO2 Inc. (http://www.wso2.com) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License + */ + +import { AlertLevels } from "@wso2is/core/models"; +import { addAlert } from "@wso2is/core/store"; +import React, { FunctionComponent, ReactElement, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useDispatch, useSelector } from "react-redux"; +import { OrganizationPermissionList } from "./organization-role-permission"; +import { AppState } from "../../../core"; +import { patchOrganizationRoleDetails } from "../../api"; +import { OrganizationResponseInterface, OrganizationRoleInterface, TreeNode } from "../../models"; + +/** + * Interface to capture permission edit props. + */ +interface RolePermissionDetailProps { + /** + * Role details + */ + roleObject: OrganizationRoleInterface; + /** + * Show if it is role. + */ + isGroup: boolean; + /** + * Handle role update callback. + */ + onRoleUpdate: () => void; + /** + * Show if the user is read only. + */ + isReadOnly?: boolean; +} + +/** + * Component to update permissions of the selected role. + * @param props Contains role id to get permission details. + */ +export const RolePermissionDetails: FunctionComponent = (props: + RolePermissionDetailProps): ReactElement => { + + const { t } = useTranslation(); + const dispatch = useDispatch(); + const [ isSubmitting, setIsSubmitting ] = useState(false); + const currentOrganization: OrganizationResponseInterface = useSelector( + (state: AppState) => state.organization.organization + ); + + const { + isReadOnly, + roleObject, + onRoleUpdate, + isGroup + } = props; + + const onPermissionUpdate = (updatedPerms: TreeNode[]) => { + const roleData = { + "operations": [ { + "op": "REPLACE", + "path": "permissions", + "value": updatedPerms.map((perm: TreeNode) => perm.key) + } ] + }; + + setIsSubmitting(true); + + patchOrganizationRoleDetails(currentOrganization.id, roleObject.id, roleData) + .then(() => { + dispatch( + addAlert({ + description: isGroup + ? t("console:manage.features.groups.notifications.updateGroup.success.description") + : t("console:manage.features.roles.notifications.updateRole.success.description"), + level: AlertLevels.SUCCESS, + message: isGroup + ? t("console:manage.features.groups.notifications.updateGroup.success.message") + : t("console:manage.features.roles.notifications.updateRole.success.message") + }) + ); + onRoleUpdate(); + }) + .catch(error => { + if (!error.response || error.response.status === 401) { + dispatch( + addAlert({ + description: isGroup + ? t("console:manage.features.groups.notifications.createPermission.error.description") + : t("console:manage.features.roles.notifications.createPermission.error.description"), + level: AlertLevels.ERROR, + message: isGroup + ? t("console:manage.features.groups.notifications.createPermission.error.message") + : t("console:manage.features.roles.notifications.createPermission.error.message") + }) + ); + } else if (error.response && error.response.data.detail) { + dispatch( + addAlert({ + description: isGroup + ? t("console:manage.features.groups.notifications.createPermission.error.description", + { description: error.response.data.detail }) + : t("console:manage.features.roles.notifications.createPermission.error.description", + { description: error.response.data.detail }), + level: AlertLevels.ERROR, + message: isGroup + ? t("console:manage.features.groups.notifications.createPermission.error.message") + : t("console:manage.features.roles.notifications.createPermission.error.message") + }) + ); + } else { + dispatch( + addAlert({ + description: isGroup + ? t("console:manage.features.groups.notifications.createPermission.genericError."+ + "description") + : t("console:manage.features.roles.notifications.createPermission.genericError."+ + "description"), + level: AlertLevels.ERROR, + message: isGroup + ? t("console:manage.features.groups.notifications.createPermission.genericError."+ + "message") + : t("console:manage.features.roles.notifications.createPermission.genericError."+ + "message") + }) + ); + } + }) + .finally(() => { + setIsSubmitting(false); + }); + }; + + return ( +
+ +
+ ); +}; diff --git a/apps/console/src/features/organizations/components/edit-organization-role/edit-organization-role-basic.tsx b/apps/console/src/features/organizations/components/edit-organization-role/edit-organization-role-basic.tsx new file mode 100644 index 00000000000..79e5fb19f26 --- /dev/null +++ b/apps/console/src/features/organizations/components/edit-organization-role/edit-organization-role-basic.tsx @@ -0,0 +1,436 @@ +/** + * Copyright (c) 2022, WSO2 Inc. (http://www.wso2.com) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License + */ + +import { AlertInterface, AlertLevels, TestableComponentInterface } from "@wso2is/core/models"; +import { addAlert } from "@wso2is/core/store"; +import { Field, FormValue, Forms, Validation } from "@wso2is/forms"; +import { ConfirmationModal, DangerZone, DangerZoneGroup, EmphasizedSegment } from "@wso2is/react-components"; +import React, { ChangeEvent, FunctionComponent, ReactElement, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useDispatch, useSelector } from "react-redux"; +import { Button, Divider, Form, Grid, InputOnChangeData } from "semantic-ui-react"; +import { AppConstants, AppState, SharedUserStoreConstants, SharedUserStoreUtils, history } from "../../../core"; +import { PRIMARY_USERSTORE_PROPERTY_VALUES } from "../../../userstores"; +import { deleteOrganizationRole, getOrganizationRoles, patchOrganizationRoleDetails } from "../../api"; +import { OrganizationRoleManagementConstants } from "../../constants"; +import { + OrganizationResponseInterface, + OrganizationRoleInterface, + PatchOrganizationRoleDataInterface +} from "../../models"; + +/** + * Interface to contain props needed for component + */ +interface BasicRoleProps extends TestableComponentInterface { + /** + * Role id. + */ + roleId: string; + /** + * Role details + */ + roleObject: OrganizationRoleInterface; + /** + * Show if it is role. + */ + isGroup: boolean; + /** + * Handle role update callback. + */ + onRoleUpdate: () => void; + /** + * Show if the user is read only. + */ + isReadOnly?: boolean; +} + +/** + * Component to edit basic role details. + * + * @param props Role object containing details which needs to be edited. + */ +export const BasicRoleDetails: FunctionComponent = (props: BasicRoleProps): ReactElement => { + const { t } = useTranslation(); + const dispatch = useDispatch(); + + const { + roleId, + roleObject, + onRoleUpdate, + isGroup, + isReadOnly, + [ "data-testid" ]: testId + } = props; + + const [ showRoleDeleteConfirmation, setShowDeleteConfirmationModal ] = useState(false); + const [ labelText, setLableText ] = useState(""); + const [ nameValue, setNameValue ] = useState(""); + const [ userStoreRegEx, setUserStoreRegEx ] = useState(""); + const [ isRoleNamePatternValid, setIsRoleNamePatternValid ] = useState(true); + const [ isRegExLoading, setRegExLoading ] = useState(false); + const [ userStore ] = useState(SharedUserStoreConstants.PRIMARY_USER_STORE); + const [ isSubmitting, setIsSubmitting ] = useState(false); + const currentOrganization: OrganizationResponseInterface = useSelector( + (state: AppState) => state.organization.organization + ); + + useEffect(() => { + if (roleObject && roleObject.displayName.indexOf("/") !== -1) { + setNameValue(roleObject.displayName.split("/")[1]); + setLableText(roleObject.displayName.split("/")[0]); + } else if (roleObject) { + setNameValue(roleObject.displayName); + } + }, [ roleObject ]); + + useEffect(() => { + if (userStoreRegEx !== "") { + return; + } + fetchUserstoreRegEx() + .then((response) => { + setUserStoreRegEx(response); + setRegExLoading(false); + }); + }, [ nameValue ]); + + const fetchUserstoreRegEx = async (): Promise => { + // TODO: Enable when the role object includes user store. + // if (roleObject && roleObject.displayName.indexOf("/") !== -1) { + // // Get the role name regEx for the secondary user store + // const userstore = roleObject.displayName.split("/")[0].toString().toLowerCase(); + // await getUserstoreRegEx(userstore, USERSTORE_REGEX_PROPERTIES.RolenameRegEx) + // .then((response) => { + // setRegExLoading(true); + // regEx = response; + // }) + // } else if (roleObject) { + // // Get the role name regEx for the primary user store + // regEx = PRIMARY_USERSTORE_PROPERTY_VALUES.RolenameJavaScriptRegEx; + // } + return PRIMARY_USERSTORE_PROPERTY_VALUES.RolenameJavaScriptRegEx; + }; + + /** + * The following function validates role name against the user store regEx. + */ + const validateRoleNamePattern = async (): Promise => { + let userStoreRegEx = ""; + + if (userStore !== SharedUserStoreConstants.PRIMARY_USER_STORE) { + await SharedUserStoreUtils.getUserStoreRegEx(userStore, + SharedUserStoreConstants.USERSTORE_REGEX_PROPERTIES.RolenameRegEx) + .then((response) => { + setRegExLoading(true); + userStoreRegEx = response; + }); + } else { + await SharedUserStoreUtils.getPrimaryUserStore().then((response) => { + setRegExLoading(true); + if (response && response.properties) { + userStoreRegEx = response?.properties?.filter(property => { + return property.name === "RolenameJavaScriptRegEx"; + })[ 0 ].value; + } + }); + } + + setRegExLoading(false); + + return new Promise((resolve, reject) => { + if (userStoreRegEx !== "") { + resolve(userStoreRegEx); + } else { + reject(""); + } + }); + + }; + + /** + * The following function handles the role name change. + * + * @param event + * @param data + */ + const handleRoleNameChange = (event: ChangeEvent, data: InputOnChangeData): void => { + setIsRoleNamePatternValid(SharedUserStoreUtils.validateInputAgainstRegEx(data?.value, userStoreRegEx)); + }; + + /** + * Dispatches the alert object to the redux store. + * + * @param {AlertInterface} alert - Alert object. + */ + const handleAlerts = (alert: AlertInterface): void => { + dispatch(addAlert(alert)); + }; + + /** + * Function which will handle role deletion action. + * + * @param id - Role ID which needs to be deleted + */ + const handleOnDelete = (id: string): void => { + deleteOrganizationRole(currentOrganization.id, id).then(() => { + handleAlerts({ + description: t("console:manage.features.roles.notifications.deleteRole.success.description"), + level: AlertLevels.SUCCESS, + message: t("console:manage.features.roles.notifications.deleteRole.success.message") + }); + if (isGroup) { + history.push(AppConstants.getPaths().get("GROUPS")); + } else { + history.push( + AppConstants.getPaths().get("ORGANIZATION_ROLES") + ); + } + }); + }; + + /** + * Method to update role name for the selected role. + * + */ + const updateRoleName = (values: Map): void => { + const newRoleName: string = values?.get("roleName")?.toString(); + + const roleData: PatchOrganizationRoleDataInterface = { + operations: [ { + "op": "REPLACE", + "path": "displayName", + "value": [ + labelText ? labelText + "/" + newRoleName : newRoleName + ] + } ] + }; + + setIsSubmitting(true); + + patchOrganizationRoleDetails(currentOrganization.id, roleObject.id, roleData) + .then(() => { + onRoleUpdate(); + handleAlerts({ + description: t("console:manage.features.roles.notifications.updateRole.success.description"), + level: AlertLevels.SUCCESS, + message: t("console:manage.features.roles.notifications.updateRole.success.message") + }); + }).catch(() => { + handleAlerts({ + description: t("console:manage.features.roles.notifications.updateRole.error.description"), + level: AlertLevels.ERROR, + message: t("console:manage.features.roles.notifications.updateRole.error.message") + }); + }).finally(() => { + setIsSubmitting(false); + }); + + }; + + return ( + <> + + { + updateRoleName(values); + } } + > + + + + + + { + if (value) { + const filter = "name eq " + value.toString(); + + await getOrganizationRoles( + currentOrganization.id, + filter, + 10, + null + ).then(response => { + if (response?.Resources && response?.Resources?.length !== 0) { + if (response?.Resources[0]?.id !== roleId) { + validation.isValid = false; + validation.errorMessages.push( + t("console:manage.features.roles.addRoleWizard." + + "forms.roleBasicDetails.roleName.validations." + + "duplicate", + { type: "Role" })); + } + } + }).catch(() => { + dispatch(addAlert({ + description: t("console:manage.features.roles.notifications." + + "fetchRoles.genericError.description"), + level: AlertLevels.ERROR, + message: t("console:manage.features.roles.notifications." + + "fetchRoles.genericError.message") + })); + }); + } + } } + onChange={ handleRoleNameChange } + type="text" + data-testid={ + isGroup + ? `${ testId }-group-name-input` + : `${ testId }-role-name-input` + } + loading={ isRegExLoading } + readOnly={ isReadOnly } + /> + + + + + + { + !isReadOnly && ( + + ) + } + + + + + +