feat(console): OIDC setup (#1272)

* feat: delete app

* radio button mods, i18n

* radio style, recommended flag

* fix form, emitter, module, styles

* app oidc

* form value change

* cleanup

* app grid, new app detail, redirect, i18n

* new uri format

* seperate uris

* cleanup export, create redirect

* fix custom two way binding, switch

* chore(deps): bump grpc from 1.24.3 to 1.24.5 in /console (#1287)

* chore: add local migrate_local.go again (#1261)

* chore: pass params in migrate_local.go (#1264)

* fix: login policy bug (#1268)

* fix: permissions on login policy multifactors and secondfactors

* fix idp restriction

Co-authored-by: Max Peintner <max@caos.ch>

* fix: redirect after idp create (#1269)

* fix(pipeline): corrected and combined operator and zitadel release into combined workflow (#1273)

* fix(pipeline): combined operator and zitadel workflow to only release once

* fix(pipeline): add dev releases for zitadelctl

* fix(pipeline): delete unused name attribute

* fix(pipeline): corrected use of github token env-variable

* fix(pipeline): corrected download of artifacts to globally defined folder

* fix(pipeline): corrected download of artifacts to globally defined folder

* fix(pipeline): corrected ref to get branch name for release

* fix(pipeline): last corrections and use of different github action (#1270)

* fix(pipeline): corrected loop for dev release

* fix(pipeline): exclude tags from starting build workflow

* fix(pipeline): use different release create action for already existing release

* fix(pipeline): use correct name for release

* fix(pipeline): push image with branch name tag and replace slashes with underscores

* fix(pipeline): corrected indenting for yaml syntax

* fix(pipeline): corrected handling of branch name

* fix(pipeline): list artifacts after download

* fix(pipeline): use github env for artifacts folder

* fix(pipeline): replace slash with underscore in all jobs

* fix(pipeline): pre-calculate refs for all jobs

* fix(pipeline): corrected yaml indenting

* fix(pipeline): deleted missed step

* fix(pipeline): deleted unexpected input for dev-release

* fix(pipeline): corrected echo for version in refs job

* fix(pipeline): remove empty if in job

* chore(pipeline): use correct path to zitadelctl binaries (#1277)

* fix(pipeline): use correct version for zitadelctl build (#1278)

* chore(deps): bump grpc from 1.24.3 to 1.24.5 in /console

Bumps [grpc](https://github.com/grpc/grpc-node) from 1.24.3 to 1.24.5.
- [Release notes](https://github.com/grpc/grpc-node/releases)
- [Commits](https://github.com/grpc/grpc-node/compare/grpc@1.24.3...grpc@1.24.5)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: Livio Amstutz <livio.a@gmail.com>
Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
Co-authored-by: Max Peintner <max@caos.ch>
Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump @types/node from 14.14.22 to 14.14.28 in /console (#1286)

* chore: add local migrate_local.go again (#1261)

* chore: pass params in migrate_local.go (#1264)

* fix: login policy bug (#1268)

* fix: permissions on login policy multifactors and secondfactors

* fix idp restriction

Co-authored-by: Max Peintner <max@caos.ch>

* fix: redirect after idp create (#1269)

* fix(pipeline): corrected and combined operator and zitadel release into combined workflow (#1273)

* fix(pipeline): combined operator and zitadel workflow to only release once

* fix(pipeline): add dev releases for zitadelctl

* fix(pipeline): delete unused name attribute

* fix(pipeline): corrected use of github token env-variable

* fix(pipeline): corrected download of artifacts to globally defined folder

* fix(pipeline): corrected download of artifacts to globally defined folder

* fix(pipeline): corrected ref to get branch name for release

* fix(pipeline): last corrections and use of different github action (#1270)

* fix(pipeline): corrected loop for dev release

* fix(pipeline): exclude tags from starting build workflow

* fix(pipeline): use different release create action for already existing release

* fix(pipeline): use correct name for release

* fix(pipeline): push image with branch name tag and replace slashes with underscores

* fix(pipeline): corrected indenting for yaml syntax

* fix(pipeline): corrected handling of branch name

* fix(pipeline): list artifacts after download

* fix(pipeline): use github env for artifacts folder

* fix(pipeline): replace slash with underscore in all jobs

* fix(pipeline): pre-calculate refs for all jobs

* fix(pipeline): corrected yaml indenting

* fix(pipeline): deleted missed step

* fix(pipeline): deleted unexpected input for dev-release

* fix(pipeline): corrected echo for version in refs job

* fix(pipeline): remove empty if in job

* chore(pipeline): use correct path to zitadelctl binaries (#1277)

* fix(pipeline): use correct version for zitadelctl build (#1278)

* chore(deps-dev): bump @types/node from 14.14.22 to 14.14.28 in /console

Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 14.14.22 to 14.14.28.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: Livio Amstutz <livio.a@gmail.com>
Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
Co-authored-by: Max Peintner <max@caos.ch>
Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump @angular-devkit/build-angular from 0.1101.2 to 0.1102.0 in /console (#1285)

* chore: add local migrate_local.go again (#1261)

* chore: pass params in migrate_local.go (#1264)

* fix: login policy bug (#1268)

* fix: permissions on login policy multifactors and secondfactors

* fix idp restriction

Co-authored-by: Max Peintner <max@caos.ch>

* fix: redirect after idp create (#1269)

* fix(pipeline): corrected and combined operator and zitadel release into combined workflow (#1273)

* fix(pipeline): combined operator and zitadel workflow to only release once

* fix(pipeline): add dev releases for zitadelctl

* fix(pipeline): delete unused name attribute

* fix(pipeline): corrected use of github token env-variable

* fix(pipeline): corrected download of artifacts to globally defined folder

* fix(pipeline): corrected download of artifacts to globally defined folder

* fix(pipeline): corrected ref to get branch name for release

* fix(pipeline): last corrections and use of different github action (#1270)

* fix(pipeline): corrected loop for dev release

* fix(pipeline): exclude tags from starting build workflow

* fix(pipeline): use different release create action for already existing release

* fix(pipeline): use correct name for release

* fix(pipeline): push image with branch name tag and replace slashes with underscores

* fix(pipeline): corrected indenting for yaml syntax

* fix(pipeline): corrected handling of branch name

* fix(pipeline): list artifacts after download

* fix(pipeline): use github env for artifacts folder

* fix(pipeline): replace slash with underscore in all jobs

* fix(pipeline): pre-calculate refs for all jobs

* fix(pipeline): corrected yaml indenting

* fix(pipeline): deleted missed step

* fix(pipeline): deleted unexpected input for dev-release

* fix(pipeline): corrected echo for version in refs job

* fix(pipeline): remove empty if in job

* chore(pipeline): use correct path to zitadelctl binaries (#1277)

* fix(pipeline): use correct version for zitadelctl build (#1278)

* chore(deps-dev): bump @angular-devkit/build-angular in /console

Bumps [@angular-devkit/build-angular](https://github.com/angular/angular-cli) from 0.1101.2 to 0.1102.0.
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Commits](https://github.com/angular/angular-cli/commits)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: Livio Amstutz <livio.a@gmail.com>
Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
Co-authored-by: Max Peintner <max@caos.ch>
Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump typescript from 4.0.5 to 4.0.7 in /console (#1284)

* chore: add local migrate_local.go again (#1261)

* chore: pass params in migrate_local.go (#1264)

* fix: login policy bug (#1268)

* fix: permissions on login policy multifactors and secondfactors

* fix idp restriction

Co-authored-by: Max Peintner <max@caos.ch>

* fix: redirect after idp create (#1269)

* fix(pipeline): corrected and combined operator and zitadel release into combined workflow (#1273)

* fix(pipeline): combined operator and zitadel workflow to only release once

* fix(pipeline): add dev releases for zitadelctl

* fix(pipeline): delete unused name attribute

* fix(pipeline): corrected use of github token env-variable

* fix(pipeline): corrected download of artifacts to globally defined folder

* fix(pipeline): corrected download of artifacts to globally defined folder

* fix(pipeline): corrected ref to get branch name for release

* fix(pipeline): last corrections and use of different github action (#1270)

* fix(pipeline): corrected loop for dev release

* fix(pipeline): exclude tags from starting build workflow

* fix(pipeline): use different release create action for already existing release

* fix(pipeline): use correct name for release

* fix(pipeline): push image with branch name tag and replace slashes with underscores

* fix(pipeline): corrected indenting for yaml syntax

* fix(pipeline): corrected handling of branch name

* fix(pipeline): list artifacts after download

* fix(pipeline): use github env for artifacts folder

* fix(pipeline): replace slash with underscore in all jobs

* fix(pipeline): pre-calculate refs for all jobs

* fix(pipeline): corrected yaml indenting

* fix(pipeline): deleted missed step

* fix(pipeline): deleted unexpected input for dev-release

* fix(pipeline): corrected echo for version in refs job

* fix(pipeline): remove empty if in job

* chore(pipeline): use correct path to zitadelctl binaries (#1277)

* fix(pipeline): use correct version for zitadelctl build (#1278)

* chore(deps-dev): bump typescript from 4.0.5 to 4.0.7 in /console

Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.0.5 to 4.0.7.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.0.5...v4.0.7)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: Livio Amstutz <livio.a@gmail.com>
Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
Co-authored-by: Max Peintner <max@caos.ch>
Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump karma from 6.0.3 to 6.1.1 in /console (#1283)

* chore: add local migrate_local.go again (#1261)

* chore: pass params in migrate_local.go (#1264)

* fix: login policy bug (#1268)

* fix: permissions on login policy multifactors and secondfactors

* fix idp restriction

Co-authored-by: Max Peintner <max@caos.ch>

* fix: redirect after idp create (#1269)

* fix(pipeline): corrected and combined operator and zitadel release into combined workflow (#1273)

* fix(pipeline): combined operator and zitadel workflow to only release once

* fix(pipeline): add dev releases for zitadelctl

* fix(pipeline): delete unused name attribute

* fix(pipeline): corrected use of github token env-variable

* fix(pipeline): corrected download of artifacts to globally defined folder

* fix(pipeline): corrected download of artifacts to globally defined folder

* fix(pipeline): corrected ref to get branch name for release

* fix(pipeline): last corrections and use of different github action (#1270)

* fix(pipeline): corrected loop for dev release

* fix(pipeline): exclude tags from starting build workflow

* fix(pipeline): use different release create action for already existing release

* fix(pipeline): use correct name for release

* fix(pipeline): push image with branch name tag and replace slashes with underscores

* fix(pipeline): corrected indenting for yaml syntax

* fix(pipeline): corrected handling of branch name

* fix(pipeline): list artifacts after download

* fix(pipeline): use github env for artifacts folder

* fix(pipeline): replace slash with underscore in all jobs

* fix(pipeline): pre-calculate refs for all jobs

* fix(pipeline): corrected yaml indenting

* fix(pipeline): deleted missed step

* fix(pipeline): deleted unexpected input for dev-release

* fix(pipeline): corrected echo for version in refs job

* fix(pipeline): remove empty if in job

* chore(pipeline): use correct path to zitadelctl binaries (#1277)

* fix(pipeline): use correct version for zitadelctl build (#1278)

* chore(deps-dev): bump karma from 6.0.3 to 6.1.1 in /console

Bumps [karma](https://github.com/karma-runner/karma) from 6.0.3 to 6.1.1.
- [Release notes](https://github.com/karma-runner/karma/releases)
- [Changelog](https://github.com/karma-runner/karma/blob/master/CHANGELOG.md)
- [Commits](https://github.com/karma-runner/karma/compare/v6.0.3...v6.1.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: Livio Amstutz <livio.a@gmail.com>
Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
Co-authored-by: Max Peintner <max@caos.ch>
Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump @angular/language-service from 11.1.1 to 11.2.0 in /console (#1282)

* chore: add local migrate_local.go again (#1261)

* chore: pass params in migrate_local.go (#1264)

* fix: login policy bug (#1268)

* fix: permissions on login policy multifactors and secondfactors

* fix idp restriction

Co-authored-by: Max Peintner <max@caos.ch>

* fix: redirect after idp create (#1269)

* fix(pipeline): corrected and combined operator and zitadel release into combined workflow (#1273)

* fix(pipeline): combined operator and zitadel workflow to only release once

* fix(pipeline): add dev releases for zitadelctl

* fix(pipeline): delete unused name attribute

* fix(pipeline): corrected use of github token env-variable

* fix(pipeline): corrected download of artifacts to globally defined folder

* fix(pipeline): corrected download of artifacts to globally defined folder

* fix(pipeline): corrected ref to get branch name for release

* fix(pipeline): last corrections and use of different github action (#1270)

* fix(pipeline): corrected loop for dev release

* fix(pipeline): exclude tags from starting build workflow

* fix(pipeline): use different release create action for already existing release

* fix(pipeline): use correct name for release

* fix(pipeline): push image with branch name tag and replace slashes with underscores

* fix(pipeline): corrected indenting for yaml syntax

* fix(pipeline): corrected handling of branch name

* fix(pipeline): list artifacts after download

* fix(pipeline): use github env for artifacts folder

* fix(pipeline): replace slash with underscore in all jobs

* fix(pipeline): pre-calculate refs for all jobs

* fix(pipeline): corrected yaml indenting

* fix(pipeline): deleted missed step

* fix(pipeline): deleted unexpected input for dev-release

* fix(pipeline): corrected echo for version in refs job

* fix(pipeline): remove empty if in job

* chore(pipeline): use correct path to zitadelctl binaries (#1277)

* fix(pipeline): use correct version for zitadelctl build (#1278)

* chore(deps-dev): bump @angular/language-service in /console

Bumps [@angular/language-service](https://github.com/angular/angular/tree/HEAD/packages/language-service) from 11.1.1 to 11.2.0.
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/master/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/11.2.0/packages/language-service)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: Livio Amstutz <livio.a@gmail.com>
Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
Co-authored-by: Max Peintner <max@caos.ch>
Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump stylelint from 13.9.0 to 13.10.0 in /console (#1281)

* chore: add local migrate_local.go again (#1261)

* chore: pass params in migrate_local.go (#1264)

* fix: login policy bug (#1268)

* fix: permissions on login policy multifactors and secondfactors

* fix idp restriction

Co-authored-by: Max Peintner <max@caos.ch>

* fix: redirect after idp create (#1269)

* fix(pipeline): corrected and combined operator and zitadel release into combined workflow (#1273)

* fix(pipeline): combined operator and zitadel workflow to only release once

* fix(pipeline): add dev releases for zitadelctl

* fix(pipeline): delete unused name attribute

* fix(pipeline): corrected use of github token env-variable

* fix(pipeline): corrected download of artifacts to globally defined folder

* fix(pipeline): corrected download of artifacts to globally defined folder

* fix(pipeline): corrected ref to get branch name for release

* fix(pipeline): last corrections and use of different github action (#1270)

* fix(pipeline): corrected loop for dev release

* fix(pipeline): exclude tags from starting build workflow

* fix(pipeline): use different release create action for already existing release

* fix(pipeline): use correct name for release

* fix(pipeline): push image with branch name tag and replace slashes with underscores

* fix(pipeline): corrected indenting for yaml syntax

* fix(pipeline): corrected handling of branch name

* fix(pipeline): list artifacts after download

* fix(pipeline): use github env for artifacts folder

* fix(pipeline): replace slash with underscore in all jobs

* fix(pipeline): pre-calculate refs for all jobs

* fix(pipeline): corrected yaml indenting

* fix(pipeline): deleted missed step

* fix(pipeline): deleted unexpected input for dev-release

* fix(pipeline): corrected echo for version in refs job

* fix(pipeline): remove empty if in job

* chore(pipeline): use correct path to zitadelctl binaries (#1277)

* fix(pipeline): use correct version for zitadelctl build (#1278)

* chore(deps-dev): bump stylelint from 13.9.0 to 13.10.0 in /console

Bumps [stylelint](https://github.com/stylelint/stylelint) from 13.9.0 to 13.10.0.
- [Release notes](https://github.com/stylelint/stylelint/releases)
- [Changelog](https://github.com/stylelint/stylelint/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stylelint/stylelint/compare/13.9.0...13.10.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: Livio Amstutz <livio.a@gmail.com>
Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
Co-authored-by: Max Peintner <max@caos.ch>
Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump @angular/cli from 11.1.2 to 11.2.0 in /console (#1280)

* chore: add local migrate_local.go again (#1261)

* chore: pass params in migrate_local.go (#1264)

* fix: login policy bug (#1268)

* fix: permissions on login policy multifactors and secondfactors

* fix idp restriction

Co-authored-by: Max Peintner <max@caos.ch>

* fix: redirect after idp create (#1269)

* fix(pipeline): corrected and combined operator and zitadel release into combined workflow (#1273)

* fix(pipeline): combined operator and zitadel workflow to only release once

* fix(pipeline): add dev releases for zitadelctl

* fix(pipeline): delete unused name attribute

* fix(pipeline): corrected use of github token env-variable

* fix(pipeline): corrected download of artifacts to globally defined folder

* fix(pipeline): corrected download of artifacts to globally defined folder

* fix(pipeline): corrected ref to get branch name for release

* fix(pipeline): last corrections and use of different github action (#1270)

* fix(pipeline): corrected loop for dev release

* fix(pipeline): exclude tags from starting build workflow

* fix(pipeline): use different release create action for already existing release

* fix(pipeline): use correct name for release

* fix(pipeline): push image with branch name tag and replace slashes with underscores

* fix(pipeline): corrected indenting for yaml syntax

* fix(pipeline): corrected handling of branch name

* fix(pipeline): list artifacts after download

* fix(pipeline): use github env for artifacts folder

* fix(pipeline): replace slash with underscore in all jobs

* fix(pipeline): pre-calculate refs for all jobs

* fix(pipeline): corrected yaml indenting

* fix(pipeline): deleted missed step

* fix(pipeline): deleted unexpected input for dev-release

* fix(pipeline): corrected echo for version in refs job

* fix(pipeline): remove empty if in job

* chore(pipeline): use correct path to zitadelctl binaries (#1277)

* fix(pipeline): use correct version for zitadelctl build (#1278)

* chore(deps-dev): bump @angular/cli from 11.1.2 to 11.2.0 in /console

Bumps [@angular/cli](https://github.com/angular/angular-cli) from 11.1.2 to 11.2.0.
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Commits](https://github.com/angular/angular-cli/compare/v11.1.2...v11.2.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: Livio Amstutz <livio.a@gmail.com>
Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
Co-authored-by: Max Peintner <max@caos.ch>
Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump stylelint-scss from 3.18.0 to 3.19.0 in /console (#1279)

* chore: add local migrate_local.go again (#1261)

* chore: pass params in migrate_local.go (#1264)

* fix: login policy bug (#1268)

* fix: permissions on login policy multifactors and secondfactors

* fix idp restriction

Co-authored-by: Max Peintner <max@caos.ch>

* fix: redirect after idp create (#1269)

* fix(pipeline): corrected and combined operator and zitadel release into combined workflow (#1273)

* fix(pipeline): combined operator and zitadel workflow to only release once

* fix(pipeline): add dev releases for zitadelctl

* fix(pipeline): delete unused name attribute

* fix(pipeline): corrected use of github token env-variable

* fix(pipeline): corrected download of artifacts to globally defined folder

* fix(pipeline): corrected download of artifacts to globally defined folder

* fix(pipeline): corrected ref to get branch name for release

* fix(pipeline): last corrections and use of different github action (#1270)

* fix(pipeline): corrected loop for dev release

* fix(pipeline): exclude tags from starting build workflow

* fix(pipeline): use different release create action for already existing release

* fix(pipeline): use correct name for release

* fix(pipeline): push image with branch name tag and replace slashes with underscores

* fix(pipeline): corrected indenting for yaml syntax

* fix(pipeline): corrected handling of branch name

* fix(pipeline): list artifacts after download

* fix(pipeline): use github env for artifacts folder

* fix(pipeline): replace slash with underscore in all jobs

* fix(pipeline): pre-calculate refs for all jobs

* fix(pipeline): corrected yaml indenting

* fix(pipeline): deleted missed step

* fix(pipeline): deleted unexpected input for dev-release

* fix(pipeline): corrected echo for version in refs job

* fix(pipeline): remove empty if in job

* chore(pipeline): use correct path to zitadelctl binaries (#1277)

* fix(pipeline): use correct version for zitadelctl build (#1278)

* chore(deps-dev): bump stylelint-scss from 3.18.0 to 3.19.0 in /console

Bumps [stylelint-scss](https://github.com/kristerkari/stylelint-scss) from 3.18.0 to 3.19.0.
- [Release notes](https://github.com/kristerkari/stylelint-scss/releases)
- [Changelog](https://github.com/kristerkari/stylelint-scss/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kristerkari/stylelint-scss/compare/3.18.0...3.19.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: Livio Amstutz <livio.a@gmail.com>
Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
Co-authored-by: Max Peintner <max@caos.ch>
Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix custom change, highlight current config, links

* info app-detail

* app card component

* applications list, fix project-grant-owner

* fix member write

* colorize warn in app

* redirect warnings

* Update console/src/assets/i18n/de.json

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* Update console/src/assets/i18n/de.json

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* Update console/src/assets/i18n/en.json

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* Update console/src/assets/i18n/de.json

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* Update console/src/assets/i18n/de.json

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* Update console/src/assets/i18n/de.json

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* Update console/src/assets/i18n/de.json

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* remove comments

* Update console/src/assets/i18n/de.json

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* Update console/src/assets/i18n/de.json

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Livio Amstutz <livio.a@gmail.com>
Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
This commit is contained in:
Max Peintner 2021-02-17 09:04:45 +01:00 committed by GitHub
parent f01921371c
commit a555d6aaaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 3710 additions and 2174 deletions

3781
console/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -33,7 +33,7 @@
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"google-proto-files": "^2.4.0", "google-proto-files": "^2.4.0",
"google-protobuf": "^3.13.0", "google-protobuf": "^3.13.0",
"grpc": "^1.24.3", "grpc": "^1.24.5",
"grpc-web": "^1.2.1", "grpc-web": "^1.2.1",
"moment": "^2.29.1", "moment": "^2.29.1",
"ngx-quicklink": "^0.2.6", "ngx-quicklink": "^0.2.6",
@ -44,28 +44,28 @@
"zone.js": "~0.11.3" "zone.js": "~0.11.3"
}, },
"devDependencies": { "devDependencies": {
"@angular/cli": "~11.1.2", "@angular/cli": "~11.2.0",
"@angular-devkit/build-angular": "~0.1101.2", "@angular-devkit/build-angular": "~0.1102.0",
"@angular/compiler-cli": "~11.0.0", "@angular/compiler-cli": "~11.0.0",
"@types/jasmine": "~3.6.3", "@types/jasmine": "~3.6.3",
"@angular/language-service": "~11.1.1", "@angular/language-service": "~11.2.0",
"@types/jasminewd2": "~2.0.3", "@types/jasminewd2": "~2.0.3",
"@types/node": "^14.14.22", "@types/node": "^14.14.28",
"codelyzer": "^6.0.0", "codelyzer": "^6.0.0",
"jasmine-core": "~3.6.0", "jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~6.0.0", "jasmine-spec-reporter": "~6.0.0",
"karma": "~6.0.3", "karma": "~6.1.1",
"karma-chrome-launcher": "~3.1.0", "karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2", "karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~4.0.0", "karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0", "karma-jasmine-html-reporter": "^1.5.0",
"prettier": "^2.2.1", "prettier": "^2.2.1",
"protractor": "~7.0.0", "protractor": "~7.0.0",
"stylelint": "^13.9.0", "stylelint": "^13.10.0",
"stylelint-config-standard": "^20.0.0", "stylelint-config-standard": "^20.0.0",
"stylelint-scss": "^3.18.0", "stylelint-scss": "^3.19.0",
"ts-node": "~9.1.1", "ts-node": "~9.1.1",
"tslint": "~6.1.3", "tslint": "~6.1.3",
"typescript": "^4.0.5" "typescript": "^4.0.7"
} }
} }

View File

@ -0,0 +1,5 @@
<div class="cnsl-app-card" [ngClass]="{'web': type == OIDCApplicationType.OIDCAPPLICATIONTYPE_WEB,
'useragent': type == OIDCApplicationType.OIDCAPPLICATIONTYPE_USER_AGENT,
'native': type == OIDCApplicationType.OIDCAPPLICATIONTYPE_NATIVE}">
<ng-content></ng-content>
</div>

View File

@ -0,0 +1,49 @@
@import '~@angular/material/theming';
@mixin app-card-theme($theme) {
/* stylelint-disable */
$primary: map-get($theme, primary);
$primary-dark: mat-color($primary, A900);
$accent: map-get($theme, accent);
$is-dark-theme: map-get($theme, is-dark);
$accent-color: mat-color($primary, 500);
/* stylelint-enable */
.cnsl-app-card {
padding: 1rem;
box-sizing: border-box;
cursor: pointer;
animation: all .2s;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 2rem;
height: 80px;
width: 80px;
margin: 1rem;
text-transform: uppercase;
border-radius: .5rem;
font-weight: 800;
background-color: $primary-dark;
transition: background-color box-shadow .3s ease-in;
&.web {
background-color: rgb(80, 110, 110);
color: white;
border: none;
}
&.native {
background-color: #595d80;
color: white;
border: none;
}
&.useragent {
background-color: #6a506e;
color: white;
border: none;
}
}
}

View File

@ -0,0 +1,14 @@
import { Component, Input } from '@angular/core';
import { OIDCApplicationType } from 'src/app/proto/generated/management_pb';
@Component({
selector: 'cnsl-app-card',
templateUrl: './app-card.component.html',
styleUrls: ['./app-card.component.scss'],
})
export class AppCardComponent {
@Input() public outline: boolean = false;
@Input() public type!: OIDCApplicationType;
public OIDCApplicationType: any = OIDCApplicationType;
}

View File

@ -0,0 +1,15 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { AppCardComponent } from './app-card.component';
@NgModule({
declarations: [AppCardComponent],
imports: [
CommonModule,
],
exports: [
AppCardComponent,
],
})
export class AppCardModule { }

View File

@ -0,0 +1,35 @@
<div class="radio-button-wrapper">
<ng-container *ngFor="let method of authMethods; index as i">
<input type="radio" [disabled]="method.disabled" (change)="emitChange()" [value]="method.key" [id]="method.key"
[(ngModel)]="selected" />
<label class="cnsl-radio-button" [ngClass]="{'first': i == 0, 'last': i == authMethods.length - 1}"
[for]="method.key">
<div class="recommended" [ngClass]="{'not': method.notRecommended}"
*ngIf="method.recommended || method.notRecommended">
{{(method.recommended ?
'APP.OIDC.RECOMMENDED' : 'APP.OIDC.NOTRECOMMENDED') | translate }}</div>
<div class="cnsl-radio-header" [ngStyle]="{'background': method.background}">
<span>{{method.prefix}}</span>
<div class="current" *ngIf="current == method.key">{{'APP.OIDC.CURRENT' | translate}}</div>
</div>
<p>{{method.titleI18nKey | translate}}</p>
<p class="type-desc">{{method.descI18nKey | translate}}</p>
<span class="fill-space"></span>
<div class="app-specs">
<div class="row" *ngIf="method && method.responseType != undefined">
<span>{{'APP.OIDC.RESPONSE' | translate}}</span>
<span>{{('APP.OIDC.RESPONSE'+method.responseType.toString()) | translate}}</span>
</div>
<div class="row" *ngIf="method.grantType != undefined">
<span>{{'APP.GRANT' | translate}}</span>
<span>{{('APP.OIDC.GRANT'+method.grantType.toString()) | translate}}</span>
</div>
<div class="row" *ngIf="method.authMethod != undefined">
<span>{{'APP.OIDC.AUTHMETHOD' | translate}}</span>
<span>{{('APP.OIDC.AUTHMETHOD'+method.authMethod.toString()) | translate}}</span>
</div>
</div>
</label>
</ng-container>
</div>

View File

@ -0,0 +1,140 @@
@import '~@angular/material/theming';
.radio-button-wrapper {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
overflow-x: auto;
overflow-y: hidden;
margin: 0;
padding-bottom: .5rem;
padding-top: 1rem;
}
@mixin app-auth-method-radio-theme($theme) {
$primary: map-get($theme, primary);
$primary-color: mat-color($primary, 500);
$is-dark-theme: map-get($theme, is-dark);
input[type="radio"]{
appearance: none;
opacity: 0;
display: none;
}
input:checked + label {
border-color: if($is-dark-theme, white, var(--grey));
.cnsl-radio-header span {
color: if($is-dark-theme, white, white);
}
}
.cnsl-radio-button {
margin: .5rem;
border-radius: .5rem;
border: 1px solid if($is-dark-theme, var(--grey), white);
display: flex;
flex-direction: column;
flex: 0 1 230px;
cursor: pointer;
position: relative;
padding-bottom: 1rem;
box-shadow: inset 0 0 6px rgba(0, 0, 0, .1);
&.first {
margin-left: 0;
}
&.last {
margin-right: 0;
}
.recommended {
position: absolute;
bottom: 0;
left: 50%;
transform: translateY(50%) translateX(-50%);
border-radius: 50vw;
font-size: 11px;
background: white;
color: black;
padding: 3px 1rem;
box-shadow: 0 0 6px rgb(0 0 0 / 10%);
white-space: nowrap;
&.not {
background: rgb(144 75 75);
color: white;
}
}
.cnsl-radio-header {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background: rgb(80, 110, 110);
border-top-left-radius: 6px;
border-top-right-radius: 6px;
position: relative;
.current {
position: absolute;
bottom: .5rem;
left: 50%;
transform: translateX(-50%);
display: block;
color: #ffffff60;
white-space: nowrap;
font-size: 12px;
}
span {
margin: 2rem;
font-size: 30px;
color: if($is-dark-theme,#21222450, #ffffff50);
}
}
p {
text-align: center;
padding: 0 1rem;
}
.type-desc {
font-size: 14px;
color: var(--grey);
}
.fill-space {
flex: 1;
}
.app-specs {
display: block;
padding: 1rem 0;
margin: 0 1rem;
.row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: var(--grey);
margin: 3px 0;
span {
white-space: nowrap;
}
:first-child {
margin-right: 1rem;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
}

View File

@ -0,0 +1,32 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { OIDCAuthMethodType, OIDCGrantType, OIDCResponseType } from 'src/app/proto/generated/management_pb';
export interface RadioItemAuthType {
key: string;
titleI18nKey: string;
descI18nKey: string;
disabled: boolean,
prefix: string;
background: string;
responseType?: OIDCResponseType;
grantType?: OIDCGrantType;
authMethod?: OIDCAuthMethodType;
recommended?: boolean;
notRecommended?: boolean;
}
@Component({
selector: 'app-auth-method-radio',
templateUrl: './app-auth-method-radio.component.html',
styleUrls: ['./app-auth-method-radio.component.scss'],
})
export class AppAuthMethodRadioComponent {
@Input() current: string = '';
@Input() selected: string = '';
@Input() authMethods!: RadioItemAuthType[];
@Output() selectedMethod: EventEmitter<string> = new EventEmitter();
public emitChange(): void {
this.selectedMethod.emit(this.selected);
}
}

View File

@ -0,0 +1,27 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatRippleModule } from '@angular/material/core';
import { TranslateModule } from '@ngx-translate/core';
import { AppAuthMethodRadioComponent } from './app-auth-method-radio/app-auth-method-radio.component';
import { AppTypeRadioComponent } from './app-type-radio/app-type-radio.component';
@NgModule({
declarations: [
AppTypeRadioComponent,
AppAuthMethodRadioComponent,
],
imports: [
CommonModule,
FormsModule,
MatRippleModule,
TranslateModule,
],
exports: [
AppAuthMethodRadioComponent,
AppTypeRadioComponent,
],
})
export class AppRadioModule { }

View File

@ -0,0 +1,14 @@
<div class="radio-button-wrapper">
<ng-container *ngFor="let type of types">
<input type="radio" [disabled]="type.disabled" (change)="emitChange()" [value]="type.type"
[(ngModel)]="selected" [id]="type.type" />
<label class="cnsl-type-radio-button" [for]="type.type">
<div class="cnsl-type-radio-header" [ngStyle]="{'background': type.background}">
<span>{{type.prefix}}</span>
</div>
<p>{{type.titleI18nKey | translate}}</p>
<p class="type-desc">{{type.descI18nKey | translate}}</p>
<span class="fill-space"></span>
</label>
</ng-container>
</div>

View File

@ -0,0 +1,68 @@
@import '~@angular/material/theming';
.radio-button-wrapper {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 0 -0.5rem;
}
@mixin app-type-radio-theme($theme) {
$primary: map-get($theme, primary);
$primary-color: mat-color($primary, 500);
$is-dark-theme: map-get($theme, is-dark);
input[type="radio"]{
appearance: none;
opacity: 0;
display: none;
}
input:checked + label {
border-color: if($is-dark-theme, white, var(--grey));
.cnsl-type-radio-header span {
color: if($is-dark-theme, white, white);
}
}
.cnsl-type-radio-button {
margin: .5rem;
border-radius: .5rem;
border: 1px solid if($is-dark-theme, var(--grey), white);
display: flex;
flex-direction: column;
flex: 0 1 240px;
cursor: pointer;
position: relative;
padding-bottom: 1rem;
box-shadow: inset 0 0 6px rgba(0, 0, 0, .1);
.cnsl-type-radio-header {
display: flex;
align-items: center;
justify-content: center;
background: rgb(80, 110, 110);
margin-bottom: 1rem;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
span {
margin: 2rem;
font-size: 30px;
color: if($is-dark-theme,#21222450, #ffffff50);
}
}
p {
text-align: center;
padding: 0 1rem;
}
.type-desc {
font-size: 14px;
color: var(--grey);
}
}
}

View File

@ -0,0 +1,27 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { OIDCApplicationType } from 'src/app/proto/generated/management_pb';
export interface RadioItemAppType {
type: OIDCApplicationType;
titleI18nKey: string;
descI18nKey: string;
checked: boolean,
disabled: boolean,
prefix: string;
background: string;
}
@Component({
selector: 'app-type-radio',
templateUrl: './app-type-radio.component.html',
styleUrls: ['./app-type-radio.component.scss'],
})
export class AppTypeRadioComponent {
@Input() selected: OIDCApplicationType = OIDCApplicationType.OIDCAPPLICATIONTYPE_WEB;
@Input() types!: RadioItemAppType[];
@Output() selectedType: EventEmitter<OIDCApplicationType> = new EventEmitter();
public emitChange(): void {
this.selectedType.emit(this.selected);
}
}

View File

@ -4,7 +4,8 @@
<h2 class="title">{{title}}</h2> <h2 class="title">{{title}}</h2>
<span class="fill-space"></span> <span class="fill-space"></span>
<ng-content select="[card-actions]"></ng-content> <ng-content select="[card-actions]"></ng-content>
<button class="button" matTooltip="Expand or collapse" mat-icon-button (click)="expanded = !expanded"> <button class="button" type="button" matTooltip="Expand or collapse" mat-icon-button
(click)="expanded = !expanded">
<mat-icon *ngIf="!expanded">keyboard_arrow_down</mat-icon> <mat-icon *ngIf="!expanded">keyboard_arrow_down</mat-icon>
<mat-icon *ngIf="expanded">keyboard_arrow_up</mat-icon> <mat-icon *ngIf="expanded">keyboard_arrow_up</mat-icon>
</button> </button>

View File

@ -24,6 +24,8 @@
font-size: 16px; font-size: 16px;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: .05em; letter-spacing: .05em;
text-overflow: ellipsis;
overflow: hidden;
} }
.fill-space { .fill-space {

View File

@ -18,7 +18,7 @@ import { Component, Input } from '@angular/core';
], ],
}) })
export class CardComponent { export class CardComponent {
public expanded: boolean = true; @Input() public expanded: boolean = true;
@Input() public title: string = ''; @Input() public title: string = '';
@Input() public description: string = ''; @Input() public description: string = '';
@Input() public animate: boolean = false; @Input() public animate: boolean = false;

View File

@ -1,5 +1,7 @@
<div class="info-section-row"> <div class="info-section-row" [ngClass]="{'info': type == 'INFO', 'warn': type == 'WARN'}">
<i class="icon las la-info"></i> <i *ngIf="type == 'INFO'" class="icon las la-info"></i>
<i *ngIf="type == 'WARN'" class="las la-exclamation"></i>
<div class="info-section-content"> <div class="info-section-content">
<ng-content></ng-content> <ng-content></ng-content>
</div> </div>

View File

@ -7,24 +7,36 @@
.info-section-row { .info-section-row {
display: flex; display: flex;
background-color: if($is-dark-theme, #ffffff13, #f3f3f3);
border-radius: 4px; border-radius: 4px;
padding: .5rem 0; padding: .5rem 0;
padding-right: 1rem; padding-right: 1rem;
color: if($is-dark-theme, #d6d6d6, #3c4257);
font-size: 14px; font-size: 14px;
.icon { .icon {
margin-right: 1rem; margin-right: 1rem;
height: 1.2rem; height: 1.2rem;
line-height: 1.2rem; line-height: 1.2rem;
font-size: 1.2rem; font-size: 1.2rem;
margin-left: .5rem; margin-left: .5rem;
color: $primary-color;
} }
.info-section-content { .info-section-content {
flex: 1; flex: 1;
}
&.info {
background-color: if($is-dark-theme, #4f566b, #cbf4c9);
color: if($is-dark-theme, #cbf4c9, #0e6245);
.icon {
color: $primary-color;
}
}
&.warn {
background-color: if($is-dark-theme, #4f566b, #ffc1c1);
color: if($is-dark-theme, #ffc1c1, #620e0e);
} }
} }
} }

View File

@ -1,8 +1,16 @@
import { Component } from '@angular/core'; import { Component, Input } from '@angular/core';
enum InfoSectionType {
INFO = 'INFO',
WARN = 'WARN',
}
@Component({ @Component({
selector: 'cnsl-info-section', selector: 'cnsl-info-section',
templateUrl: './info-section.component.html', templateUrl: './info-section.component.html',
styleUrls: ['./info-section.component.scss'], styleUrls: ['./info-section.component.scss'],
}) })
export class InfoSectionComponent { } export class InfoSectionComponent {
@Input() type = InfoSectionType.INFO;
}

View File

@ -23,18 +23,14 @@
<p class="step-title">{{'APP.OIDC.TITLEFIRST' | translate}}</p> <p class="step-title">{{'APP.OIDC.TITLEFIRST' | translate}}</p>
<cnsl-form-field appearance="outline" class="formfield"> <cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'APP.NAME' | translate }}</cnsl-label> <cnsl-label>{{ 'APP.NAME' | translate }}</cnsl-label>
<input cnslInput formControlName="name" /> <input cnslInput cdkFocusInitial formControlName="name" />
<span cnsl-error *ngIf="name?.errors?.required">{{'PROJECT.APP.NAMEREQUIRED' | translate}}</span> <span cnsl-error *ngIf="name?.errors?.required">{{'PROJECT.APP.NAMEREQUIRED' | translate}}</span>
</cnsl-form-field> </cnsl-form-field>
<p class="step-title">{{'APP.OIDC.TYPETITLE' | translate}}</p> <p class="step-title">{{'APP.OIDC.TYPETITLE' | translate}}</p>
<mat-radio-group color="primary" aria-labelledby="radio-group-label" class="radio-group"
formControlName="applicationType"> <app-type-radio [types]="oidcAppTypes" (selectedType)="changedAppType($event)"
<mat-radio-button class="radio-button" *ngFor="let type of oidcAppTypes | keyvalue" [selected]="applicationType?.value"></app-type-radio>
[value]="type.value">
<div>{{('APP.OIDC.APPTYPE'+type.key.toString()) | translate}}</div>
</mat-radio-button>
</mat-radio-group>
<div class="actions"> <div class="actions">
<button mat-raised-button [disabled]="firstFormGroup.invalid" color="primary" <button mat-raised-button [disabled]="firstFormGroup.invalid" color="primary"
matStepperNext>{{'ACTIONS.CONTINUE' | translate}}</button> matStepperNext>{{'ACTIONS.CONTINUE' | translate}}</button>
@ -42,37 +38,19 @@
</form> </form>
</mat-step> </mat-step>
<mat-step [editable]="true" <!-- skip for native applications -->
*ngIf="oidcApp.applicationType === OIDCApplicationType.OIDCAPPLICATIONTYPE_USER_AGENT"> <mat-step *ngIf="oidcApp.applicationType !== OIDCApplicationType.OIDCAPPLICATIONTYPE_NATIVE"
<ng-template matStepLabel>{{'APP.OIDC.RESPONSESECTION' | translate}}</ng-template>
<div class="checkbox-container">
<mat-checkbox class="checkbox" *ngFor="let responsetype of oidcResponseTypes"
(change)="changeResponseType()" color="primary" [(ngModel)]="responsetype.checked"
[disabled]="responsetype.disabled">
{{'APP.OIDC.RESPONSE'+responsetype.type | translate}}
</mat-checkbox>
</div>
<div class="actions">
<button mat-stroked-button color="primary" matStepperPrevious>{{'ACTIONS.BACK' | translate}}</button>
<button mat-raised-button color="primary" matStepperNext>{{'ACTIONS.CONTINUE' | translate}}</button>
</div>
</mat-step>
<mat-step *ngIf="oidcApp.applicationType === OIDCApplicationType.OIDCAPPLICATIONTYPE_WEB"
[stepControl]="secondFormGroup" [editable]="true"> [stepControl]="secondFormGroup" [editable]="true">
<form [formGroup]="secondFormGroup"> <form [formGroup]="secondFormGroup">
<ng-template matStepLabel>{{'APP.OIDC.AUTHMETHODSECTION' | translate}}</ng-template> <ng-template matStepLabel>{{'APP.OIDC.AUTHMETHODSECTION' | translate}}</ng-template>
<mat-radio-group color="primary" aria-labelledby="example-radio-group-label" class="radio-group" <app-auth-method-radio [authMethods]="authMethods" [selected]="authMethod?.value"
formControlName="authMethodType"> (selectedMethod)="changedAppAuthMethod($event)">
<mat-radio-button class="radio-button" *ngFor="let authmethod of oidcAuthMethodType" </app-auth-method-radio>
[disabled]="authmethod.disabled" [value]="authmethod.type">
{{'APP.OIDC.AUTHMETHOD'+authmethod.type | translate}}
</mat-radio-button>
</mat-radio-group>
<div class="actions"> <div class="actions">
<button mat-stroked-button color="primary" <button mat-stroked-button color="primary" matStepperPrevious>{{'ACTIONS.BACK' |
matStepperPrevious>{{'ACTIONS.BACK' | translate}}</button> translate}}</button>
<button mat-raised-button color="primary" [disabled]="secondFormGroup.invalid" <button mat-raised-button color="primary" [disabled]="secondFormGroup.invalid"
matStepperNext>{{'ACTIONS.CONTINUE' | translate}}</button> matStepperNext>{{'ACTIONS.CONTINUE' | translate}}</button>
</div> </div>
@ -89,25 +67,10 @@
<p class="step-description" *ngIf="oidcApp.applicationType === OIDCApplicationType.OIDCAPPLICATIONTYPE_WEB"> <p class="step-description" *ngIf="oidcApp.applicationType === OIDCApplicationType.OIDCAPPLICATIONTYPE_WEB">
{{'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate}}</p> {{'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate}}</p>
<form class="chip-form" (ngSubmit)="addUri(redInput, 'REDIRECT')"> <cnsl-redirect-uris class="redirect-section" [canWrite]="true"
<cnsl-form-field appearance="outline" class="full-width"> (changedUris)="oidcApp.redirectUrisList = $event" [urisList]="oidcApp.redirectUrisList"
<cnsl-label>{{ 'APP.OIDC.REDIRECT' | translate }}</cnsl-label> title="{{ 'APP.OIDC.REDIRECT' | translate }}">
<input #redInput cnslInput placeholder="{{'APP.OIDC.COMMAORENTERSEPERATION' | translate}}" </cnsl-redirect-uris>
[formControl]="redirectControl">
</cnsl-form-field>
<button matTooltip="{{'ACTIONS.ADD' | translate}}" type="submit" mat-icon-button>
<mat-icon>add</mat-icon>
</button>
</form>
<mat-chip-list #chipRedirectList aria-label="uri selection">
<mat-chip #redInput class="chip" *ngFor="let uri of oidcApp.redirectUrisList" selected removable
[matTooltip]="!uri?.startsWith('https://') ? ('APP.OIDC.UNSECUREREDIRECT' | translate): ''"
[color]="!uri?.startsWith('https://') ? 'warn': 'white'" (removed)="removeUri(uri, 'REDIRECT')">
{{uri}} <mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
</mat-chip-list>
<p *ngIf="redirectControl.invalid" class="error">{{'APP.OIDC.REDIRECTNOTVALID' | translate}}</p>
<p class="step-title">{{'APP.OIDC.POSTREDIRECTTITLE' | translate}}</p> <p class="step-title">{{'APP.OIDC.POSTREDIRECTTITLE' | translate}}</p>
<p class="step-description" <p class="step-description"
@ -117,26 +80,10 @@
*ngIf="oidcApp.applicationType === OIDCApplicationType.OIDCAPPLICATIONTYPE_WEB || oidcApp.applicationType === OIDCApplicationType.OIDCAPPLICATIONTYPE_USER_AGENT"> *ngIf="oidcApp.applicationType === OIDCApplicationType.OIDCAPPLICATIONTYPE_WEB || oidcApp.applicationType === OIDCApplicationType.OIDCAPPLICATIONTYPE_USER_AGENT">
{{'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate}}</p> {{'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate}}</p>
<form class="chip-form" (ngSubmit)="addUri(postInput, 'POSTREDIRECT')"> <cnsl-redirect-uris class="redirect-section" [canWrite]="true"
<cnsl-form-field appearance="outline" class="full-width"> (changedUris)="oidcApp.postLogoutRedirectUrisList = $event"
<cnsl-label>{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}</cnsl-label> [urisList]="oidcApp.postLogoutRedirectUrisList" title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}">
<input #postInput cnslInput placeholder="{{'APP.OIDC.COMMAORENTERSEPERATION' | translate}}" </cnsl-redirect-uris>
[formControl]="postRedirectControl">
</cnsl-form-field>
<button matTooltip="{{'ACTIONS.ADD' | translate}}" type="submit" mat-icon-button>
<mat-icon>add</mat-icon>
</button>
</form>
<mat-chip-list #chipPostRedirectList aria-label="uri selection">
<mat-chip class="chip" *ngFor="let uri of oidcApp.postLogoutRedirectUrisList"
[matTooltip]="!uri?.startsWith('https://') ? ('APP.OIDC.UNSECUREREDIRECT' | translate): ''"
removable (removed)="removeUri(uri, 'POSTREDIRECT')" selected
[color]="!uri?.startsWith('https://') ? 'warn': 'white'">
{{uri}} <mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
</mat-chip-list>
<p *ngIf="postRedirectControl.invalid" class="error">{{'APP.OIDC.REDIRECTNOTVALID' | translate}}</p>
<div class="actions"> <div class="actions">
<button mat-stroked-button color="primary" matStepperPrevious>{{'ACTIONS.BACK' | translate}}</button> <button mat-stroked-button color="primary" matStepperPrevious>{{'ACTIONS.BACK' | translate}}</button>
@ -171,9 +118,8 @@
<span class="right" *ngIf="oidcApp.grantTypesList?.length > 0"> <span class="right" *ngIf="oidcApp.grantTypesList?.length > 0">
[<span *ngFor="let element of oidcApp.grantTypesList; index as i"> [<span *ngFor="let element of oidcApp.grantTypesList; index as i">
{{'APP.OIDC.GRANT'+element | translate}} {{'APP.OIDC.GRANT'+element | translate}}
{{i < oidcApp.grantTypesList.length - 1 ? ', ': ''}} {{i < oidcApp.grantTypesList.length - 1 ? ', ' : '' }} </span>]
</span>] </span>
</span>
</div> </div>
<div class="row"> <div class="row">
<span class="left"> <span class="left">
@ -182,9 +128,8 @@
<span class="right" *ngIf="oidcApp.responseTypesList?.length > 0"> <span class="right" *ngIf="oidcApp.responseTypesList?.length > 0">
[<span *ngFor="let element of oidcApp.responseTypesList; index as i"> [<span *ngFor="let element of oidcApp.responseTypesList; index as i">
{{('APP.OIDC.RESPONSE'+element | translate)}} {{('APP.OIDC.RESPONSE'+element | translate)}}
{{i < oidcApp.responseTypesList.length - 1 ? ', ': ''}} {{i < oidcApp.responseTypesList.length - 1 ? ', ' : '' }} </span>]
</span>] </span>
</span>
</div> </div>
<div class="row"> <div class="row">
@ -205,9 +150,8 @@
<span class="right" *ngIf="oidcApp.redirectUrisList?.length > 0"> <span class="right" *ngIf="oidcApp.redirectUrisList?.length > 0">
[<span *ngFor="let redirect of oidcApp.redirectUrisList; index as i"> [<span *ngFor="let redirect of oidcApp.redirectUrisList; index as i">
{{redirect}} {{redirect}}
{{i < oidcApp.redirectUrisList.length - 1 ? ', ': ''}} {{i < oidcApp.redirectUrisList.length - 1 ? ', ' : '' }} </span>]
</span>] </span>
</span>
</div> </div>
<div class="row"> <div class="row">
@ -217,15 +161,14 @@
<span class="right" *ngIf="oidcApp.postLogoutRedirectUrisList?.length > 0"> <span class="right" *ngIf="oidcApp.postLogoutRedirectUrisList?.length > 0">
[<span *ngFor="let redirect of oidcApp.postLogoutRedirectUrisList; index as i"> [<span *ngFor="let redirect of oidcApp.postLogoutRedirectUrisList; index as i">
{{redirect}} {{redirect}}
{{i < oidcApp.postLogoutRedirectUrisList.length - 1 ? ', ': ''}} {{i < oidcApp.postLogoutRedirectUrisList.length - 1 ? ', ' : '' }} </span>]
</span>] </span>
</span>
</div> </div>
<div class="actions"> <div class="actions">
<button mat-stroked-button color="primary" matStepperPrevious>{{'ACTIONS.BACK' | translate}}</button> <button mat-stroked-button color="primary" matStepperPrevious>{{'ACTIONS.BACK' | translate}}</button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary" (click)="saveOIDCApp()">{{'ACTIONS.CREATE' |
(click)="saveOIDCApp()">{{'ACTIONS.CREATE' | translate}}</button> translate}}</button>
</div> </div>
</mat-step> </mat-step>
</mat-horizontal-stepper> </mat-horizontal-stepper>
@ -241,8 +184,8 @@
<cnsl-form-field appearance="outline" class="formfield"> <cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'APP.OIDC.APPTYPE' | translate }}</cnsl-label> <cnsl-label>{{ 'APP.OIDC.APPTYPE' | translate }}</cnsl-label>
<mat-select formControlName="applicationType"> <mat-select formControlName="applicationType">
<mat-option *ngFor="let type of oidcAppTypes" [value]="type"> <mat-option *ngFor="let type of oidcAppTypes" [value]="type.type">
{{ 'APP.OIDC.APPTYPE'+type | translate }} {{ 'APP.OIDC.APPTYPE'+type.type | translate }}
</mat-option> </mat-option>
</mat-select> </mat-select>
</cnsl-form-field> </cnsl-form-field>
@ -274,32 +217,18 @@
</mat-select> </mat-select>
</cnsl-form-field> </cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield full-width"> <div class="formfield full-width">
<cnsl-label>{{ 'APP.OIDC.REDIRECT' | translate }}</cnsl-label> <cnsl-redirect-uris class="redirect-section" [canWrite]="true"
<mat-chip-list #chipRedirectList aria-label="uri selection"> (changedUris)="oidcApp.redirectUrisList = $event" [urisList]="oidcApp.redirectUrisList"
<mat-chip class="chip" *ngFor="let uri of oidcApp.redirectUrisList" removable title="{{ 'APP.OIDC.REDIRECT' | translate }}">
(removed)="removeUri(uri, 'REDIRECT')"> </cnsl-redirect-uris>
{{uri}} <mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input cnslInput [matChipInputFor]="chipRedirectList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" [matChipInputAddOnBlur]="true"
(matChipInputTokenEnd)="addUri($event, 'REDIRECT')">
</mat-chip-list>
</cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield full-width">
<cnsl-label>{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}</cnsl-label>
<mat-chip-list #chipPostRedirectList aria-label="uri selection">
<mat-chip class="chip" *ngFor="let uri of oidcApp.postLogoutRedirectUrisList" removable
(removed)="removeUri(uri, 'POSTREDIRECT')">
{{uri}} <mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input cnslInput [matChipInputFor]="chipPostRedirectList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" [matChipInputAddOnBlur]="true"
(matChipInputTokenEnd)="addUri($event, 'POSTREDIRECT')">
</mat-chip-list>
</cnsl-form-field>
<cnsl-redirect-uris class="redirect-section" [canWrite]="true"
(changedUris)="oidcApp.postLogoutRedirectUrisList = $event"
[urisList]="oidcApp.postLogoutRedirectUrisList"
title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}">
</cnsl-redirect-uris>
</div>
</div> </div>
<button color="primary" mat-raised-button class="continue-button" [disabled]="form.invalid" cdkFocusInitial <button color="primary" mat-raised-button class="continue-button" [disabled]="form.invalid" cdkFocusInitial

View File

@ -42,11 +42,6 @@ p.desc {
margin-right: .5rem; margin-right: .5rem;
} }
.formfield {
width: 400px;
display: block;
}
.full-width { .full-width {
width: 100%; width: 100%;
} }
@ -55,6 +50,10 @@ p.desc {
background: inherit !important; background: inherit !important;
margin: 0 -1.5rem; margin: 0 -1.5rem;
.formfield {
max-width: 400px;
}
.step-title { .step-title {
font-size: 1rem; font-size: 1rem;
text-transform: uppercase; text-transform: uppercase;
@ -65,44 +64,6 @@ p.desc {
font-size: .9rem; font-size: .9rem;
color: var(--grey); color: var(--grey);
} }
.chip-form {
width: 100%;
display: flex;
align-items: center;
.formfield {
flex: 1;
}
button {
margin-top: 1rem;
}
}
.error {
font-size: 13px;
color: #f44336;
margin-top: 0;
}
.chip {
border-radius: 4px;
height: 40px;
}
.chip[color='white'] {
background-color: #fafafa;
}
}
.radio-group {
display: flex;
flex-direction: column;
.radio-button {
margin: .5rem 0;
}
} }
.checkbox-container { .checkbox-container {
@ -137,7 +98,6 @@ p.desc {
float: right; float: right;
margin-top: 1rem; margin-top: 1rem;
border-radius: .5rem; border-radius: .5rem;
padding: .5rem 1rem;
min-width: 100px; min-width: 100px;
} }
} }
@ -147,9 +107,11 @@ p.desc {
display: flex; display: flex;
margin: 0 -.5rem; margin: 0 -.5rem;
flex-wrap: wrap; flex-wrap: wrap;
flex-direction: row;
.formfield { .formfield {
flex: 1 0 40%; flex: 1;
box-sizing: border-box;
margin: 0 .5rem; margin: 0 .5rem;
&.full-width { &.full-width {
@ -164,4 +126,4 @@ p.desc {
padding: .5rem 4rem; padding: .5rem 4rem;
float: right; float: right;
} }
} }

View File

@ -6,6 +6,7 @@ import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Params, Router } from '@angular/router'; import { ActivatedRoute, Params, Router } from '@angular/router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { debounceTime } from 'rxjs/operators'; import { debounceTime } from 'rxjs/operators';
import { RadioItemAuthType } from 'src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component';
import { import {
Application, Application,
OIDCApplicationCreate, OIDCApplicationCreate,
@ -17,7 +18,15 @@ import {
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
import {
WEB_TYPE,
NATIVE_TYPE,
USER_AGENT_TYPE
} from '../authtypes';
import { AppSecretDialogComponent } from '../app-secret-dialog/app-secret-dialog.component'; import { AppSecretDialogComponent } from '../app-secret-dialog/app-secret-dialog.component';
import { CODE_METHOD, getPartialConfigFromAuthMethod, IMPLICIT_METHOD, PKCE_METHOD, PK_JWT_METHOD, POST_METHOD } from '../authmethods';
@Component({ @Component({
selector: 'app-app-create', selector: 'app-app-create',
@ -37,10 +46,16 @@ export class AppCreateComponent implements OnInit, OnDestroy {
{ type: OIDCResponseType.OIDCRESPONSETYPE_ID_TOKEN_TOKEN, checked: false, disabled: false }, { type: OIDCResponseType.OIDCRESPONSETYPE_ID_TOKEN_TOKEN, checked: false, disabled: false },
]; ];
public oidcAppTypes: OIDCApplicationType[] = [ public oidcAppTypes: any = [
OIDCApplicationType.OIDCAPPLICATIONTYPE_WEB, WEB_TYPE,
OIDCApplicationType.OIDCAPPLICATIONTYPE_USER_AGENT, NATIVE_TYPE,
OIDCApplicationType.OIDCAPPLICATIONTYPE_NATIVE, USER_AGENT_TYPE,
];
public authMethods: RadioItemAuthType[] = [
PKCE_METHOD,
CODE_METHOD,
POST_METHOD,
]; ];
public oidcAuthMethodType: { type: OIDCAuthMethodType, checked: boolean, disabled: boolean; }[] = [ public oidcAuthMethodType: { type: OIDCAuthMethodType, checked: boolean, disabled: boolean; }[] = [
@ -52,7 +67,6 @@ export class AppCreateComponent implements OnInit, OnDestroy {
// stepper // stepper
firstFormGroup!: FormGroup; firstFormGroup!: FormGroup;
secondFormGroup!: FormGroup; secondFormGroup!: FormGroup;
// thirdFormGroup!: FormGroup;
// devmode // devmode
public form!: FormGroup; public form!: FormGroup;
@ -72,9 +86,6 @@ export class AppCreateComponent implements OnInit, OnDestroy {
// TODO show when implemented // TODO show when implemented
]; ];
public redirectControl: FormControl = new FormControl('');
public postRedirectControl: FormControl = new FormControl('');
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE]; public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
constructor( constructor(
@ -104,64 +115,58 @@ export class AppCreateComponent implements OnInit, OnDestroy {
this.firstFormGroup = this.fb.group({ this.firstFormGroup = this.fb.group({
name: ['', [Validators.required]], name: ['', [Validators.required]],
applicationType: ['', [Validators.required]], applicationType: [OIDCApplicationType.OIDCAPPLICATIONTYPE_WEB, [Validators.required]],
}); });
this.firstFormGroup.valueChanges.subscribe(value => { this.firstFormGroup.valueChanges.subscribe(value => {
if (this.firstFormGroup.valid) { if (this.firstFormGroup.valid) {
switch (value.applicationType) {
case OIDCApplicationType.OIDCAPPLICATIONTYPE_NATIVE:
this.oidcResponseTypes[0].checked = true;
this.oidcApp.responseTypesList = [OIDCResponseType.OIDCRESPONSETYPE_CODE];
this.oidcApp.grantTypesList =
[OIDCGrantType.OIDCGRANTTYPE_AUTHORIZATION_CODE];
this.oidcApp.authMethodType = OIDCAuthMethodType.OIDCAUTHMETHODTYPE_NONE;
// this.redirectControl = new FormControl('', [nativeValidator as ValidatorFn]);
// this.postRedirectControl = new FormControl('', [nativeValidator as ValidatorFn]);
break;
case OIDCApplicationType.OIDCAPPLICATIONTYPE_WEB:
this.oidcAuthMethodType[0].disabled = false;
this.oidcAuthMethodType[1].disabled = false;
this.oidcAuthMethodType[2].disabled = false;
this.authMethodType?.setValue(OIDCAuthMethodType.OIDCAUTHMETHODTYPE_BASIC);
this.oidcApp.authMethodType = OIDCAuthMethodType.OIDCAUTHMETHODTYPE_BASIC;
this.oidcResponseTypes[0].checked = true;
this.oidcApp.responseTypesList = [OIDCResponseType.OIDCRESPONSETYPE_CODE];
this.changeResponseType();
this.oidcApp.grantTypesList =
[OIDCGrantType.OIDCGRANTTYPE_AUTHORIZATION_CODE];
// this.redirectControl = new FormControl('', [webValidator as ValidatorFn]);
// this.postRedirectControl = new FormControl('', [webValidator as ValidatorFn]);
break;
case OIDCApplicationType.OIDCAPPLICATIONTYPE_USER_AGENT:
this.oidcResponseTypes[0].checked = true;
this.oidcApp.responseTypesList = [OIDCResponseType.OIDCRESPONSETYPE_CODE];
this.oidcApp.grantTypesList =
[OIDCGrantType.OIDCGRANTTYPE_AUTHORIZATION_CODE, OIDCGrantType.OIDCGRANTTYPE_IMPLICIT];
this.oidcApp.authMethodType = OIDCAuthMethodType.OIDCAUTHMETHODTYPE_NONE;
// this.redirectControl = new FormControl('', [webValidator as ValidatorFn]);
// this.postRedirectControl = new FormControl('', [webValidator as ValidatorFn]);
break;
}
this.oidcApp.name = this.name?.value; this.oidcApp.name = this.name?.value;
this.oidcApp.applicationType = this.applicationType?.value; this.oidcApp.applicationType = this.applicationType?.value;
switch (this.applicationType?.value) {
case OIDCApplicationType.OIDCAPPLICATIONTYPE_NATIVE:
this.authMethods = [
PKCE_METHOD,
];
// automatically set to PKCE and skip step
this.oidcApp.responseTypesList = [OIDCResponseType.OIDCRESPONSETYPE_CODE];
this.oidcApp.grantTypesList = [OIDCGrantType.OIDCGRANTTYPE_AUTHORIZATION_CODE];
this.oidcApp.authMethodType = OIDCAuthMethodType.OIDCAUTHMETHODTYPE_NONE;
break;
case OIDCApplicationType.OIDCAPPLICATIONTYPE_WEB:
this.authMethods = [
PKCE_METHOD,
CODE_METHOD,
POST_METHOD,
];
this.authMethod?.setValue(PKCE_METHOD.key);
break;
case OIDCApplicationType.OIDCAPPLICATIONTYPE_USER_AGENT:
this.authMethods = [
PKCE_METHOD,
IMPLICIT_METHOD,
];
this.authMethod?.setValue(PKCE_METHOD.key);
break;
}
} }
}); });
this.secondFormGroup = this.fb.group({ this.secondFormGroup = this.fb.group({
authMethodType: [OIDCAuthMethodType.OIDCAUTHMETHODTYPE_BASIC, [Validators.required]], authMethod: [this.authMethods[0].key, [Validators.required]],
}); });
this.secondFormGroup.valueChanges.subscribe(value => { this.secondFormGroup.valueChanges.subscribe(form => {
this.oidcApp.authMethodType = value.authMethodType; const partialConfig = getPartialConfigFromAuthMethod(form.authMethod);
if (partialConfig) {
this.oidcApp.responseTypesList = partialConfig.responseTypesList ?? [];
this.oidcApp.grantTypesList = partialConfig.grantTypesList ?? [];
this.oidcApp.authMethodType = partialConfig.authMethodType ?? OIDCAuthMethodType.OIDCAUTHMETHODTYPE_NONE;
}
}); });
} }
@ -173,6 +178,15 @@ export class AppCreateComponent implements OnInit, OnDestroy {
this.subscription?.unsubscribe(); this.subscription?.unsubscribe();
} }
public changedAppType(type: OIDCApplicationType) {
this.firstFormGroup.controls['applicationType'].setValue(type);
}
public changedAppAuthMethod(methodKey: string) {
console.log(methodKey);
this.secondFormGroup.controls['authMethod'].setValue(methodKey);
}
private async getData({ projectid }: Params): Promise<void> { private async getData({ projectid }: Params): Promise<void> {
this.projectId = projectid; this.projectId = projectid;
this.oidcApp.projectId = projectid; this.oidcApp.projectId = projectid;
@ -215,66 +229,20 @@ export class AppCreateComponent implements OnInit, OnDestroy {
} }
} }
public addUri(input: any, target: string): void {
const value = input.value.trim();
if (value !== '') {
if (target === 'REDIRECT' && this.redirectControl.valid) {
this.oidcApp.redirectUrisList.push(value);
if (input) {
input.value = '';
}
} else if (target === 'POSTREDIRECT' && this.redirectControl.valid) {
this.oidcApp.postLogoutRedirectUrisList.push(value);
if (input) {
input.value = '';
}
}
}
}
public removeUri(uri: string, target: string): void {
if (target === 'REDIRECT') {
const index = this.oidcApp.redirectUrisList.indexOf(uri);
if (index !== undefined && index >= 0) {
this.oidcApp.redirectUrisList.splice(index, 1);
}
} else if (target === 'POSTREDIRECT') {
const index = this.oidcApp.postLogoutRedirectUrisList.indexOf(uri);
if (index !== undefined && index >= 0) {
this.oidcApp.postLogoutRedirectUrisList.splice(index, 1);
}
}
}
changeResponseType(): void {
this.oidcApp.responseTypesList = this.oidcResponseTypes.filter(gt => gt.checked).map(gt => gt.type);
}
moreThanOneOption(options: Array<{ type: OIDCGrantType, checked: boolean, disabled: boolean; }>): boolean {
return options.filter(option => option.disabled === false).length > 1;
}
get name(): AbstractControl | null { get name(): AbstractControl | null {
return this.firstFormGroup.get('name'); return this.firstFormGroup.get('name');
} }
get applicationType(): AbstractControl | null { get applicationType(): AbstractControl | null {
return this.firstFormGroup.get('applicationType'); return this.firstFormGroup.get('applicationType');
} }
public grantTypeChecked(type: OIDCGrantType): boolean { public grantTypeChecked(type: OIDCGrantType): boolean {
return this.oidcGrantTypes.filter(gt => gt.checked).map(gt => gt.type).findIndex(t => t === type) > -1; return this.oidcGrantTypes.filter(gt => gt.checked).map(gt => gt.type).findIndex(t => t === type) > -1;
} }
get responseTypesList(): AbstractControl | null { get responseTypesList(): AbstractControl | null {
return this.secondFormGroup.get('responseTypesList'); return this.secondFormGroup.get('responseTypesList');
} }
get authMethod(): AbstractControl | null {
get authMethodType(): AbstractControl | null { return this.secondFormGroup.get('authMethod');
return this.secondFormGroup.get('authMethodType');
} }
// devmode // devmode
@ -282,24 +250,17 @@ export class AppCreateComponent implements OnInit, OnDestroy {
get formname(): AbstractControl | null { get formname(): AbstractControl | null {
return this.form.get('name'); return this.form.get('name');
} }
get formresponseTypesList(): AbstractControl | null { get formresponseTypesList(): AbstractControl | null {
return this.form.get('responseTypesList'); return this.form.get('responseTypesList');
} }
get formgrantTypesList(): AbstractControl | null { get formgrantTypesList(): AbstractControl | null {
return this.form.get('grantTypesList'); return this.form.get('grantTypesList');
} }
get formapplicationType(): AbstractControl | null { get formapplicationType(): AbstractControl | null {
return this.form.get('applicationType'); return this.form.get('applicationType');
} }
get formauthMethodType(): AbstractControl | null { get formauthMethodType(): AbstractControl | null {
return this.form.get('authMethodType'); return this.form.get('authMethodType');
} }
} };

View File

@ -1,10 +1,32 @@
<app-meta-layout> <app-meta-layout>
<div class="max-width-container"> <div class="enlarged-container">
<div class="head"> <div class="head">
<a [routerLink]="['/projects', projectId]" mat-icon-button> <a [routerLink]="['/projects', projectId]" mat-icon-button>
<mat-icon class="icon">arrow_back</mat-icon> <mat-icon class="icon">arrow_back</mat-icon>
</a> </a>
<h1>{{ 'APP.PAGES.TITLE' | translate }} {{app?.name}}</h1> <div class="title-col">
<h1>{{app?.name}}</h1>
<span>{{'APP.OIDC.APPTYPE'+app?.oidcConfig?.applicationType | translate}}</span>
</div>
<ng-container *ngIf="isZitadel === false">
<ng-template appHasRole [appHasRole]="['project.app.write:'+projectId, 'project.app.write']">
<button *ngIf="!editState" matTooltip="{{'ACTIONS.EDIT' | translate}}" mat-icon-button
(click)="editState = !editState" aria-label="edit app name">
<i class="las la-edit"></i>
</button>
<button *ngIf="editState" (click)="saveApp()" [disabled]="appNameForm.invalid || name?.disabled"
mat-icon-button>
<i class="las la-save"></i>
</button>
</ng-template>
<ng-template appHasRole [appHasRole]="['project.app.delete:'+projectId, 'project.app.delete']">
<button matTooltip="{{'ACTIONS.DELETE' | translate}}" color="warn" mat-icon-button
(click)="deleteApp()" aria-label="delete app">
<i class="las la-trash"></i>
</button>
</ng-template>
</ng-container>
<p class="desc">{{ 'APP.PAGES.DESCRIPTION' | translate }}</p> <p class="desc">{{ 'APP.PAGES.DESCRIPTION' | translate }}</p>
<p *ngIf="isZitadel" class="zitadel-warning">{{'PROJECT.PAGES.ZITADELPROJECT' | translate}}</p> <p *ngIf="isZitadel" class="zitadel-warning">{{'PROJECT.PAGES.ZITADELPROJECT' | translate}}</p>
@ -12,52 +34,85 @@
<span *ngIf="errorMessage" class="err-container">{{errorMessage}}</span> <span *ngIf="errorMessage" class="err-container">{{errorMessage}}</span>
<app-card title="{{ 'APP.PAGES.DETAIL.TITLE' | translate }}" *ngIf="app"> <form *ngIf="app && editState" [formGroup]="appNameForm">
<form [formGroup]="appNameForm" (ngSubmit)="saveApp()"> <div class="name-content">
<div class="name-content"> <mat-button-toggle-group formControlName="state" class="toggle" (change)="changeState($event)">
<mat-button-toggle-group formControlName="state" class="toggle" (change)="changeState($event)"> <mat-button-toggle [value]="AppState.APPSTATE_INACTIVE"
<mat-button-toggle [value]="AppState.APPSTATE_INACTIVE" matTooltip="{{ 'ACTIONS.DEACTIVATE' | translate}}">
matTooltip="{{ 'ACTIONS.DEACTIVATE' | translate}}"> {{'APP.PAGES.DETAIL.STATE.'+AppState.APPSTATE_INACTIVE | translate}}
{{'APP.PAGES.DETAIL.STATE.'+AppState.APPSTATE_INACTIVE | translate}} </mat-button-toggle>
</mat-button-toggle> <mat-button-toggle [value]="AppState.APPSTATE_ACTIVE"
<mat-button-toggle [value]="AppState.APPSTATE_ACTIVE" matTooltip="{{ 'ACTIONS.REACTIVATE' | translate}}">
matTooltip="{{ 'ACTIONS.REACTIVATE' | translate}}"> {{'APP.PAGES.DETAIL.STATE.'+AppState.APPSTATE_ACTIVE | translate}}
{{'APP.PAGES.DETAIL.STATE.'+AppState.APPSTATE_ACTIVE | translate}} </mat-button-toggle>
</mat-button-toggle> </mat-button-toggle-group>
</mat-button-toggle-group>
<cnsl-form-field class="name-field"> <cnsl-form-field class="name-field">
<cnsl-label>{{ 'APP.NAME' | translate }}</cnsl-label> <cnsl-label>{{ 'APP.NAME' | translate }}</cnsl-label>
<input cnslInput formControlName="name" /> <input cnslInput formControlName="name" />
</cnsl-form-field> </cnsl-form-field>
<cnsl-info-section class="docs-line" *ngIf="docs?.discoveryEndpoint">
<p><strong>Discovery Endpoint:</strong> {{docs.discoveryEndpoint}}</p>
<p><strong>Issuer:</strong> {{docs.issuer}}</p>
</cnsl-info-section>
</div>
<div class="btn-container">
<button type="submit" color="primary" [disabled]="appNameForm.invalid || name?.disabled"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
</div>
</form>
</app-card>
<app-card title="{{ 'APP.OIDC.TITLE' | translate }}" *ngIf="app && app.oidcConfig">
<div card-actions *ngIf="app?.oidcConfig?.authMethodType !== OIDCAuthMethodType.OIDCAUTHMETHODTYPE_NONE">
<button [disabled]="!canWrite" mat-stroked-button
(click)="regenerateOIDCClientSecret()">{{'APP.OIDC.REGENERATESECRET' | translate}}</button>
</div> </div>
</form>
<!-- <cnsl-info-section class="docs-line" *ngIf="docs?.discoveryEndpoint">
<p><strong>Discovery Endpoint:</strong> {{docs.discoveryEndpoint}}</p>
<p><strong>Issuer:</strong> {{docs.issuer}}</p>
</cnsl-info-section> -->
<div class="compliance"
*ngIf="app?.oidcConfig?.complianceProblemsList && app.oidcConfig?.complianceProblemsList?.length">
<cnsl-info-section class="problem" type="WARN">
<ul style="margin: 0;">
<li style="margin: 0 0 .5rem 0;"
*ngFor="let problem of app.oidcConfig?.complianceProblemsList || []">
{{problem.localizedMessage}}</li>
</ul>
</cnsl-info-section>
</div>
<div class="content">
<h3 class="full-width section-title">{{'APP.OIDC.REDIRECTSECTIONTITLE' | translate}}</h3>
<mat-slide-toggle color="primary" class="devmode" [formControl]="devMode" name="devMode"
matTooltip="{{'APP.OIDC.DEVMODEDESC' | translate}}">
{{ 'APP.OIDC.DEVMODE' | translate }}
</mat-slide-toggle>
<cnsl-info-section class="step-description"
*ngIf="applicationType?.value == OIDCApplicationType.OIDCAPPLICATIONTYPE_NATIVE">
<span>{{'APP.OIDC.REDIRECTDESCRIPTIONNATIVE' | translate}}</span>
</cnsl-info-section>
<p class="step-description"
*ngIf="applicationType?.value == OIDCApplicationType.OIDCAPPLICATIONTYPE_WEB || applicationType?.value == OIDCApplicationType.OIDCAPPLICATIONTYPE_USER_AGENT">
{{'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate}}</p>
<div style="margin: .5rem" class="divider"></div>
<cnsl-redirect-uris class="redirect-section" [canWrite]="canWrite" [devMode]="devMode?.value"
(changedUris)="redirectUrisList = $event" [urisList]="redirectUrisList"
title="{{ 'APP.OIDC.REDIRECT' | translate }}">
</cnsl-redirect-uris>
<cnsl-redirect-uris class="redirect-section" [canWrite]="canWrite" [devMode]="devMode?.value"
(changedUris)="postLogoutRedirectUrisList = $event" [urisList]="postLogoutRedirectUrisList"
title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}">
</cnsl-redirect-uris>
</div>
<app-auth-method-radio *ngIf="authMethods && initialAuthMethod && app?.oidcConfig" [authMethods]="authMethods"
[selected]="initialAuthMethod" [current]="currentAuthMethod"
(selectedMethod)="setPartialConfigFromAuthMethod($event)">
</app-auth-method-radio>
<form *ngIf="appForm" [formGroup]="appForm" (ngSubmit)="saveOIDCApp()">
<app-card title=" {{ 'APP.OIDC.TITLE' | translate }}" *ngIf="app && app.oidcConfig" [expanded]="false">
<div card-actions
*ngIf="app?.oidcConfig?.authMethodType !== OIDCAuthMethodType.OIDCAUTHMETHODTYPE_NONE">
<button [disabled]="!canWrite" mat-stroked-button
(click)="regenerateOIDCClientSecret()">{{'APP.OIDC.REGENERATESECRET' | translate}}</button>
</div>
<div class="compliance">
<cnsl-info-section class="problem">
<ul style="margin: 0;">
<li style="margin: 0 0 .5rem 0;" *ngFor="let problem of app.oidcConfig.complianceProblemsList">
{{problem.localizedMessage}}</li>
</ul>
</cnsl-info-section>
</div>
<form *ngIf="appForm" [formGroup]="appForm" (ngSubmit)="saveOIDCApp()">
<div class="content"> <div class="content">
<cnsl-form-field class="formfield" appearance="outline"> <cnsl-form-field class="formfield" appearance="outline">
<cnsl-label>{{ 'APP.OIDC.CLIENTID' | translate }}</cnsl-label> <cnsl-label>{{ 'APP.OIDC.CLIENTID' | translate }}</cnsl-label>
@ -141,89 +196,46 @@
<cnsl-info-section class="full-width desc"> <cnsl-info-section class="full-width desc">
<span>{{'APP.OIDC.CLOCKSKEW' | translate}}</span> <span>{{'APP.OIDC.CLOCKSKEW' | translate}}</span>
</cnsl-info-section> </cnsl-info-section>
<div class="divider"></div>
<p class="full-width section-title">{{'APP.OIDC.REDIRECTSECTIONTITLE' | translate}}</p>
<mat-slide-toggle color="primary" class="devmode" formControlName="devMode" name="devMode">
{{ 'APP.OIDC.DEVMODE' | translate }}
</mat-slide-toggle>
<cnsl-info-section class="step-description">
<span style="margin-bottom: 2rem;">{{'APP.OIDC.DEVMODEDESC' | translate}}</span>
</cnsl-info-section>
<cnsl-info-section class="step-description"
*ngIf="applicationType?.value == OIDCApplicationType.OIDCAPPLICATIONTYPE_NATIVE">
<span>{{'APP.OIDC.REDIRECTDESCRIPTIONNATIVE' | translate}}</span>
</cnsl-info-section>
<p class="step-description"
*ngIf="applicationType?.value == OIDCApplicationType.OIDCAPPLICATIONTYPE_WEB || applicationType?.value == OIDCApplicationType.OIDCAPPLICATIONTYPE_USER_AGENT">
{{'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate}}</p>
<form class="chip-form" (ngSubmit)="add(redInput, RedirectType.REDIRECT)">
<cnsl-form-field class="formfield full-width">
<cnsl-label>{{ 'APP.OIDC.REDIRECT' | translate }}</cnsl-label>
<input #redInput cnslInput placeholder="{{'APP.OIDC.COMMAORENTERSEPERATION' | translate}}"
[formControl]="redirectControl">
</cnsl-form-field>
<button matTooltip="{{'ACTIONS.ADD' | translate}}" type="submit" mat-icon-button
[disabled]="redirectControl.invalid || !canWrite">
<mat-icon>add</mat-icon>
</button>
</form>
<mat-chip-list class="chiplist formfield full-width" [disabled]="!canWrite" #chipRedirectList>
<mat-chip class="chip" *ngFor="let redirect of redirectUrisList" selected
[matTooltip]="!redirect?.startsWith('https://') ? ('APP.OIDC.UNSECUREREDIRECT' | translate): ''"
[color]="!redirect?.startsWith('https://') ? 'warn': 'green'"
(removed)="remove(redirect, RedirectType.REDIRECT)">
{{redirect}}
<mat-icon matChipRemove *ngIf="removable">cancel</mat-icon>
</mat-chip>
</mat-chip-list>
<p *ngIf="redirectControl.value && redirectControl.invalid" class="error">
{{'APP.OIDC.REDIRECTNOTVALID' | translate}}</p>
<form class="chip-form" (ngSubmit)="add(postInput, RedirectType.POSTREDIRECT)">
<cnsl-form-field class="formfield full-width">
<cnsl-label>{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}</cnsl-label>
<input #postInput cnslInput placeholder="{{'APP.OIDC.COMMAORENTERSEPERATION' | translate}}"
[formControl]="postRedirectControl">
</cnsl-form-field>
<button matTooltip="{{'ACTIONS.ADD' | translate}}" type="submit" mat-icon-button
[disabled]="postRedirectControl.invalid || !canWrite">
<mat-icon>add</mat-icon>
</button>
</form>
<mat-chip-list class="chiplist formfield full-width" [disabled]="!canWrite" #chipPostRedirectList>
<mat-chip class="chip" *ngFor="let redirect of postLogoutRedirectUrisList" selected
(removed)="remove(redirect, RedirectType.POSTREDIRECT)"
[matTooltip]="!redirect?.startsWith('https://') ? ('APP.OIDC.UNSECUREREDIRECT' | translate): ''"
[color]="!redirect?.startsWith('https://') ? 'warn': 'green'">
{{redirect}}
<mat-icon matChipRemove *ngIf="removable">cancel</mat-icon>
</mat-chip>
</mat-chip-list>
<p *ngIf="postRedirectControl.value && postRedirectControl.invalid" class="error">
{{'APP.OIDC.REDIRECTNOTVALID' | translate}}</p>
</div> </div>
<div class="btn-container"> </app-card>
<button class="submit-button" type="submit" color="primary" <div class="btn-container">
[disabled]="appForm.invalid || !canWrite" <button class="submit-button" type="submit" color="primary" [disabled]="appForm.invalid || !canWrite"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button> mat-raised-button>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>
</form>
<div class="next-steps">
<h5>{{'APP.PAGES.NEXTSTEPS.TITLE' | translate}}</h5>
<div class="row">
<div class="step">
<h6>{{ 'APP.PAGES.NEXTSTEPS.0.TITLE' | translate }}</h6>
<p>{{'APP.PAGES.NEXTSTEPS.0.DESC' | translate}}</p>
<span class="fill-space"></span>
<a [routerLink]="['/projects', this.projectId]" color="primary" mat-mini-fab><i
class="las la-angle-right"></i></a>
</div> </div>
</form> <div class="step">
<h6>{{ 'APP.PAGES.NEXTSTEPS.1.TITLE' | translate }}</h6>
</app-card> <p>{{'APP.PAGES.NEXTSTEPS.1.DESC' | translate}}</p>
<span class="fill-space"></span>
<a [routerLink]="['/users', 'create']" color="primary" mat-mini-fab><i
class="las la-angle-right"></i></a>
</div>
<div class="step">
<h6>{{ 'APP.PAGES.NEXTSTEPS.2.TITLE' | translate }}</h6>
<p>{{'APP.PAGES.NEXTSTEPS.2.DESC' | translate}}</p>
<span class="fill-space"></span>
<a href="https://docs.zitadel.ch" color="primary" mat-mini-fab><i
class="las la-angle-right"></i></a>
</div>
</div>
</div>
</div> </div>
<div class="side" metainfo> <div class="side" metainfo>
<div class="meta-details"> <div class="meta-details">
<div class="meta-row"> <div class="meta-row">

View File

@ -8,11 +8,21 @@
display: block; display: block;
} }
h1 { .title-col {
font-size: 1.2rem;
margin: 0 1rem;
margin-left: 2rem; margin-left: 2rem;
font-weight: normal; margin-right: 1rem;
min-width: 200px;
h1 {
font-size: 1.2rem;
margin: 0 0 0 0;
font-weight: normal;
}
span {
font-size: 12px;
color: var(--grey);
}
} }
.desc { .desc {
@ -20,6 +30,7 @@
display: block; display: block;
font-size: .9rem; font-size: .9rem;
color: var(--grey); color: var(--grey);
margin-top: 1.5rem;
} }
.zitadel-warning { .zitadel-warning {
@ -41,10 +52,10 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center;
.toggle { .toggle {
outline: none; outline: none;
align-self: flex-start;
margin-top: 1rem; margin-top: 1rem;
margin-right: 1rem; margin-right: 1rem;
border-radius: .5rem; border-radius: .5rem;
@ -91,6 +102,11 @@
align-items: center; align-items: center;
} }
.redirect-section {
flex: 1;
margin: 0 .5rem;
}
.formfield { .formfield {
flex: 1 1 30%; flex: 1 1 30%;
margin: 0 .5rem; margin: 0 .5rem;
@ -99,27 +115,9 @@
flex-basis: 100%; flex-basis: 100%;
} }
} }
.chiplist {
margin-bottom: 1rem;
}
.chip-form {
width: 100%;
display: flex;
align-items: center;
.formfield {
flex: 1;
}
button {
margin-top: 1rem;
}
}
.section-title { .section-title {
margin-bottom: 1.5rem; margin: 1.5rem 0 0 0;
} }
.full-width { .full-width {
@ -142,7 +140,7 @@
.desc { .desc {
color: var(--grey); color: var(--grey);
font-size: 14px; font-size: 14px;
margin-bottom: 1rem; margin-bottom: 1.5rem;
} }
.devmode { .devmode {
@ -170,18 +168,67 @@
margin-top: 1rem; margin-top: 1rem;
} }
.chip {
border-radius: 4px;
height: 40px;
}
.chip[color='green'] {
background-color: #56a392 !important;
}
.divider { .divider {
flex-basis: 100%; flex-basis: 100%;
margin: 1.5rem .5rem; margin: 1.5rem .5rem;
height: 1px; height: 1px;
background-color: rgba(#8795a1, .2); background-color: rgba(#8795a1, .2);
} }
.next-steps {
h5 {
text-transform: uppercase;
font-size: 14px;
color: var(--grey);
}
.row {
width: 100%;
display: flex;
overflow-x: auto;
.step {
min-width: 220px;
max-width: 280px;
padding: 1rem;
margin: 0 .5rem;
border: 1px solid var(--grey);
border-radius: .5rem;
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
flex: 1;
h6 {
font-size: 1rem;
text-align: center;
margin: 0 0 1rem 0;
}
p {
font-size: 14px;
text-align: center;
color: var(--grey);
}
.fill-space {
flex: 1;
}
button {
display: block;
margin: auto;
}
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
}
}

View File

@ -4,12 +4,14 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
import { AbstractControl, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; import { AbstractControl, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { MatButtonToggleChange } from '@angular/material/button-toggle'; import { MatButtonToggleChange } from '@angular/material/button-toggle';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Params } from '@angular/router'; import { ActivatedRoute, Params, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Duration } from 'google-protobuf/google/protobuf/duration_pb'; import { Duration } from 'google-protobuf/google/protobuf/duration_pb';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { RadioItemAuthType } from 'src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component';
import { ChangeType } from 'src/app/modules/changes/changes.component'; import { ChangeType } from 'src/app/modules/changes/changes.component';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
import { import {
Application, Application,
AppState, AppState,
@ -27,11 +29,7 @@ import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
import { AppSecretDialogComponent } from '../app-secret-dialog/app-secret-dialog.component'; import { AppSecretDialogComponent } from '../app-secret-dialog/app-secret-dialog.component';
import { CODE_METHOD, getAuthMethodFromPartialConfig, getPartialConfigFromAuthMethod, IMPLICIT_METHOD, PKCE_METHOD, PK_JWT_METHOD, POST_METHOD, CUSTOM_METHOD } from '../authmethods';
enum RedirectType {
REDIRECT = 'redirect',
POSTREDIRECT = 'postredirect',
}
@Component({ @Component({
selector: 'app-app-detail', selector: 'app-app-detail',
@ -39,12 +37,20 @@ enum RedirectType {
styleUrls: ['./app-detail.component.scss'], styleUrls: ['./app-detail.component.scss'],
}) })
export class AppDetailComponent implements OnInit, OnDestroy { export class AppDetailComponent implements OnInit, OnDestroy {
public editState: boolean = false;
public currentAuthMethod: string = CUSTOM_METHOD.key;
public initialAuthMethod: string = CUSTOM_METHOD.key;
public canWrite: boolean = false; public canWrite: boolean = false;
public errorMessage: string = ''; public errorMessage: string = '';
public removable: boolean = true; public removable: boolean = true;
public addOnBlur: boolean = true; public addOnBlur: boolean = true;
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE]; public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
public authMethods: RadioItemAuthType[] = [
PKCE_METHOD,
CODE_METHOD,
POST_METHOD,
];
private subscription?: Subscription; private subscription?: Subscription;
public projectId: string = ''; public projectId: string = '';
public app!: Application.AsObject; public app!: Application.AsObject;
@ -78,11 +84,10 @@ export class AppDetailComponent implements OnInit, OnDestroy {
public AppState: any = AppState; public AppState: any = AppState;
public appNameForm!: FormGroup; public appNameForm!: FormGroup;
public appForm!: FormGroup; public appForm!: FormGroup;
public redirectUrisList: string[] = []; public redirectUrisList: string[] = [];
public postLogoutRedirectUrisList: string[] = []; public postLogoutRedirectUrisList: string[] = [];
public RedirectType: any = RedirectType;
public isZitadel: boolean = false; public isZitadel: boolean = false;
public docs!: ZitadelDocs.AsObject; public docs!: ZitadelDocs.AsObject;
@ -90,9 +95,6 @@ export class AppDetailComponent implements OnInit, OnDestroy {
public OIDCAuthMethodType: any = OIDCAuthMethodType; public OIDCAuthMethodType: any = OIDCAuthMethodType;
public OIDCTokenType: any = OIDCTokenType; public OIDCTokenType: any = OIDCTokenType;
public redirectControl: FormControl = new FormControl({ value: '', disabled: true });
public postRedirectControl: FormControl = new FormControl({ value: '', disabled: true });
public ChangeType: any = ChangeType; public ChangeType: any = ChangeType;
constructor( constructor(
public translate: TranslateService, public translate: TranslateService,
@ -103,6 +105,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
private dialog: MatDialog, private dialog: MatDialog,
private mgmtService: ManagementService, private mgmtService: ManagementService,
private authService: GrpcAuthService, private authService: GrpcAuthService,
private router: Router,
) { ) {
this.appNameForm = this.fb.group({ this.appNameForm = this.fb.group({
state: [{ value: '', disabled: true }, []], state: [{ value: '', disabled: true }, []],
@ -145,12 +148,23 @@ export class AppDetailComponent implements OnInit, OnDestroy {
this.mgmtService.GetApplicationById(projectid, id).then(app => { this.mgmtService.GetApplicationById(projectid, id).then(app => {
this.app = app.toObject(); this.app = app.toObject();
this.appNameForm.patchValue(this.app); this.appNameForm.patchValue(this.app);
console.log(this.app);
this.getAuthMethodOptions();
if (this.app.oidcConfig) {
this.initialAuthMethod = this.authMethodFromPartialConfig(this.app.oidcConfig);
this.currentAuthMethod = this.initialAuthMethod;
if (this.initialAuthMethod === CUSTOM_METHOD.key) {
if (!this.authMethods.includes(CUSTOM_METHOD)) {
this.authMethods.push(CUSTOM_METHOD);
}
} else {
this.authMethods = this.authMethods.filter(element => element != CUSTOM_METHOD);
}
}
if (allowed) { if (allowed) {
this.appNameForm.enable(); this.appNameForm.enable();
this.appForm.enable(); this.appForm.enable();
this.redirectControl.enable();
this.postRedirectControl.enable();
} }
if (this.app.oidcConfig?.redirectUrisList) { if (this.app.oidcConfig?.redirectUrisList) {
@ -161,12 +175,22 @@ export class AppDetailComponent implements OnInit, OnDestroy {
} }
if (this.app.oidcConfig?.clockSkew) { if (this.app.oidcConfig?.clockSkew) {
const inSecs = this.app.oidcConfig?.clockSkew.seconds + this.app.oidcConfig?.clockSkew.nanos / 100000; const inSecs = this.app.oidcConfig?.clockSkew.seconds + this.app.oidcConfig?.clockSkew.nanos / 100000;
console.log(inSecs);
this.appForm.controls['clockSkewSeconds'].setValue(inSecs); this.appForm.controls['clockSkewSeconds'].setValue(inSecs);
} }
if (this.app.oidcConfig) { if (this.app.oidcConfig) {
this.appForm.patchValue(this.app.oidcConfig); this.appForm.patchValue(this.app.oidcConfig);
} }
this.appForm.valueChanges.subscribe(oidcConfig => {
this.initialAuthMethod = this.authMethodFromPartialConfig(oidcConfig);
if (this.initialAuthMethod === CUSTOM_METHOD.key) {
if (!this.authMethods.includes(CUSTOM_METHOD)) {
this.authMethods.push(CUSTOM_METHOD);
}
} else {
this.authMethods = this.authMethods.filter(element => element != CUSTOM_METHOD);
}
});
}).catch(error => { }).catch(error => {
console.error(error); console.error(error);
this.toast.showError(error); this.toast.showError(error);
@ -176,6 +200,69 @@ export class AppDetailComponent implements OnInit, OnDestroy {
this.docs = (await this.mgmtService.GetZitadelDocs()).toObject(); this.docs = (await this.mgmtService.GetZitadelDocs()).toObject();
} }
private getAuthMethodOptions(): void {
switch (this.app.oidcConfig?.applicationType) {
case OIDCApplicationType.OIDCAPPLICATIONTYPE_NATIVE:
this.authMethods = [
PKCE_METHOD,
CUSTOM_METHOD,
];
break;
case OIDCApplicationType.OIDCAPPLICATIONTYPE_WEB:
this.authMethods = [
PKCE_METHOD,
CODE_METHOD,
POST_METHOD,
];
break;
case OIDCApplicationType.OIDCAPPLICATIONTYPE_USER_AGENT:
this.authMethods = [
PKCE_METHOD,
IMPLICIT_METHOD,
];
break;
}
}
public authMethodFromPartialConfig(config: OIDCConfig.AsObject): string {
const key = getAuthMethodFromPartialConfig(config);
return key;
}
public setPartialConfigFromAuthMethod(authMethod: string): void {
const partialConfig = getPartialConfigFromAuthMethod(authMethod);
if (partialConfig && this.app.oidcConfig) {
this.app.oidcConfig.responseTypesList = partialConfig.responseTypesList ?? [];
this.app.oidcConfig.grantTypesList = partialConfig.grantTypesList ?? [];
this.app.oidcConfig.authMethodType = partialConfig.authMethodType ?? OIDCAuthMethodType.OIDCAUTHMETHODTYPE_NONE;
this.appForm.patchValue(this.app.oidcConfig);
}
}
public deleteApp(): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'APP.PAGES.DIALOG.DELETE.TITLE',
descriptionKey: 'APP.PAGES.DIALOG.DELETE.DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp && this.projectId && this.app.id) {
this.mgmtService.RemoveApplication(this.projectId, this.app.id).then(() => {
this.toast.showInfo('APP.TOAST.DELETED', true);
this.router.navigate(['/projects', this.projectId]);
}).catch(error => {
this.toast.showError(error);
});
}
});
}
public changeState(event: MatButtonToggleChange): void { public changeState(event: MatButtonToggleChange): void {
if (event.value === AppState.APPSTATE_ACTIVE) { if (event.value === AppState.APPSTATE_ACTIVE) {
this.mgmtService.ReactivateApplication(this.projectId, this.app.id).then(() => { this.mgmtService.ReactivateApplication(this.projectId, this.app.id).then(() => {
@ -192,40 +279,6 @@ export class AppDetailComponent implements OnInit, OnDestroy {
} }
} }
public add(input: any, target: RedirectType): void {
if (target === RedirectType.POSTREDIRECT && this.postRedirectControl.valid) {
if (input.value !== '' && input.value !== ' ' && input.value !== '/') {
this.postLogoutRedirectUrisList.push(input.value);
}
if (input) {
input.value = '';
}
} else if (target === RedirectType.REDIRECT && this.redirectControl.valid) {
if (input.value !== '' && input.value !== ' ' && input.value !== '/') {
this.redirectUrisList.push(input.value);
}
if (input) {
input.value = '';
}
}
}
public remove(redirect: any, target: RedirectType): void {
if (target === RedirectType.POSTREDIRECT) {
const index = this.postLogoutRedirectUrisList.indexOf(redirect);
if (index >= 0) {
this.postLogoutRedirectUrisList.splice(index, 1);
}
} else if (target === RedirectType.REDIRECT) {
const index = this.redirectUrisList.indexOf(redirect);
if (index >= 0) {
this.redirectUrisList.splice(index, 1);
}
}
}
public saveApp(): void { public saveApp(): void {
if (this.appNameForm.valid) { if (this.appNameForm.valid) {
this.app.name = this.name?.value; this.app.name = this.name?.value;
@ -234,6 +287,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
.UpdateApplication(this.projectId, this.app.id, this.name?.value) .UpdateApplication(this.projectId, this.app.id, this.name?.value)
.then(() => { .then(() => {
this.toast.showInfo('APP.TOAST.OIDCUPDATED', true); this.toast.showInfo('APP.TOAST.OIDCUPDATED', true);
this.editState = false;
}) })
.catch(error => { .catch(error => {
this.toast.showError(error); this.toast.showError(error);
@ -282,10 +336,12 @@ export class AppDetailComponent implements OnInit, OnDestroy {
dur.setNanos((Math.floor(this.clockSkewSeconds?.value % 1) * 10000)); dur.setNanos((Math.floor(this.clockSkewSeconds?.value % 1) * 10000));
req.setClockSkew(dur); req.setClockSkew(dur);
} }
console.log(req.toObject());
this.mgmtService this.mgmtService
.UpdateOIDCAppConfig(req) .UpdateOIDCAppConfig(req)
.then(() => { .then(() => {
if (this.app.oidcConfig) {
this.currentAuthMethod = this.authMethodFromPartialConfig(this.app.oidcConfig);
}
this.toast.showInfo('APP.TOAST.OIDCUPDATED', true); this.toast.showInfo('APP.TOAST.OIDCUPDATED', true);
}) })
.catch(error => { .catch(error => {

View File

@ -19,6 +19,7 @@ import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { CopyToClipboardModule } from 'src/app/directives/copy-to-clipboard/copy-to-clipboard.module'; import { CopyToClipboardModule } from 'src/app/directives/copy-to-clipboard/copy-to-clipboard.module';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { AppRadioModule } from 'src/app/modules/app-radio/app-radio.module';
import { CardModule } from 'src/app/modules/card/card.module'; import { CardModule } from 'src/app/modules/card/card.module';
import { ChangesModule } from 'src/app/modules/changes/changes.module'; import { ChangesModule } from 'src/app/modules/changes/changes.module';
import { InfoSectionModule } from 'src/app/modules/info-section/info-section.module'; import { InfoSectionModule } from 'src/app/modules/info-section/info-section.module';
@ -29,15 +30,20 @@ import { AppCreateComponent } from './app-create/app-create.component';
import { AppDetailComponent } from './app-detail/app-detail.component'; import { AppDetailComponent } from './app-detail/app-detail.component';
import { AppSecretDialogComponent } from './app-secret-dialog/app-secret-dialog.component'; import { AppSecretDialogComponent } from './app-secret-dialog/app-secret-dialog.component';
import { AppsRoutingModule } from './apps-routing.module'; import { AppsRoutingModule } from './apps-routing.module';
import { A11yModule } from '@angular/cdk/a11y';
import { RedirectUrisComponent } from './redirect-uris/redirect-uris.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
AppCreateComponent, AppCreateComponent,
AppDetailComponent, AppDetailComponent,
AppSecretDialogComponent, AppSecretDialogComponent,
RedirectUrisComponent,
], ],
imports: [ imports: [
CommonModule, CommonModule,
A11yModule,
AppRadioModule,
AppsRoutingModule, AppsRoutingModule,
FormsModule, FormsModule,
TranslateModule, TranslateModule,

View File

@ -0,0 +1,166 @@
import { RadioItemAuthType } from 'src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component';
import { OIDCAuthMethodType, OIDCConfig, OIDCGrantType, OIDCResponseType } from 'src/app/proto/generated/management_pb';
export const CODE_METHOD: RadioItemAuthType = {
key: 'CODE',
titleI18nKey: 'APP.OIDC.SELECTION.AUTHMETHOD.CODE.TITLE',
descI18nKey: 'APP.OIDC.SELECTION.AUTHMETHOD.CODE.DESCRIPTION',
disabled: false,
prefix: 'CODE',
background: 'rgb(89 115 128)',
responseType: OIDCResponseType.OIDCRESPONSETYPE_CODE,
grantType: OIDCGrantType.OIDCGRANTTYPE_AUTHORIZATION_CODE,
authMethod: OIDCAuthMethodType.OIDCAUTHMETHODTYPE_BASIC,
recommended: false,
};
export const PKCE_METHOD: RadioItemAuthType = {
key: 'PKCE',
titleI18nKey: 'APP.OIDC.SELECTION.AUTHMETHOD.PKCE.TITLE',
descI18nKey: 'APP.OIDC.SELECTION.AUTHMETHOD.PKCE.DESCRIPTION',
disabled: false,
prefix: 'PKCE',
background: 'rgb(80 110 92)',
responseType: OIDCResponseType.OIDCRESPONSETYPE_CODE,
grantType: OIDCGrantType.OIDCGRANTTYPE_AUTHORIZATION_CODE,
authMethod: OIDCAuthMethodType.OIDCAUTHMETHODTYPE_NONE,
recommended: true,
};
export const POST_METHOD: RadioItemAuthType = {
key: 'POST',
titleI18nKey: 'APP.OIDC.SELECTION.AUTHMETHOD.POST.TITLE',
descI18nKey: 'APP.OIDC.SELECTION.AUTHMETHOD.POST.DESCRIPTION',
disabled: false,
prefix: 'POST',
background: '#595d80',
responseType: OIDCResponseType.OIDCRESPONSETYPE_CODE,
grantType: OIDCGrantType.OIDCGRANTTYPE_AUTHORIZATION_CODE,
authMethod: OIDCAuthMethodType.OIDCAUTHMETHODTYPE_POST,
notRecommended: true,
};
export const PK_JWT_METHOD: RadioItemAuthType = {
key: 'PK_JWT',
titleI18nKey: 'APP.OIDC.SELECTION.AUTHMETHOD.ALTERNATIVE.TITLE',
descI18nKey: 'APP.OIDC.SELECTION.AUTHMETHOD.ALTERNATIVE.DESCRIPTION',
disabled: false,
prefix: 'PK_JWT',
background: '#6a506e',
responseType: OIDCResponseType.OIDCRESPONSETYPE_CODE,
grantType: OIDCGrantType.OIDCGRANTTYPE_AUTHORIZATION_CODE,
authMethod: OIDCAuthMethodType.OIDCAUTHMETHODTYPE_POST,
};
export const IMPLICIT_METHOD: RadioItemAuthType = {
key: 'IMPLICIT',
titleI18nKey: 'APP.OIDC.SELECTION.AUTHMETHOD.IMPLICIT.TITLE',
descI18nKey: 'APP.OIDC.SELECTION.AUTHMETHOD.IMPLICIT.DESCRIPTION',
disabled: false,
prefix: 'IMP',
background: 'rgb(144 75 75)',
responseType: OIDCResponseType.OIDCRESPONSETYPE_ID_TOKEN,
grantType: OIDCGrantType.OIDCGRANTTYPE_IMPLICIT,
authMethod: OIDCAuthMethodType.OIDCAUTHMETHODTYPE_NONE,
notRecommended: true,
};
export const CUSTOM_METHOD: RadioItemAuthType = {
key: 'CUSTOM',
titleI18nKey: 'APP.OIDC.SELECTION.AUTHMETHOD.CUSTOM.TITLE',
descI18nKey: 'APP.OIDC.SELECTION.AUTHMETHOD.CUSTOM.DESCRIPTION',
disabled: false,
prefix: 'CUSTOM',
background: '#333',
};
export function getPartialConfigFromAuthMethod(authMethod: string): Partial<OIDCConfig.AsObject> | undefined {
let config: Partial<OIDCConfig.AsObject>;
switch (authMethod) {
case CODE_METHOD.key:
config = {
responseTypesList: [OIDCResponseType.OIDCRESPONSETYPE_CODE],
grantTypesList: [OIDCGrantType.OIDCGRANTTYPE_AUTHORIZATION_CODE],
authMethodType: OIDCAuthMethodType.OIDCAUTHMETHODTYPE_BASIC,
};
return config;
case PKCE_METHOD.key:
config = {
responseTypesList: [OIDCResponseType.OIDCRESPONSETYPE_CODE],
grantTypesList: [OIDCGrantType.OIDCGRANTTYPE_AUTHORIZATION_CODE],
authMethodType: OIDCAuthMethodType.OIDCAUTHMETHODTYPE_NONE,
};
return config;
case POST_METHOD.key:
config = {
responseTypesList: [OIDCResponseType.OIDCRESPONSETYPE_CODE],
grantTypesList: [OIDCGrantType.OIDCGRANTTYPE_AUTHORIZATION_CODE],
authMethodType: OIDCAuthMethodType.OIDCAUTHMETHODTYPE_POST,
};
return config;
// case PK_JWT_METHOD.key:
// config = {
// responseTypesList: [OIDCResponseType.OIDCRESPONSETYPE_CODE],
// grantTypesList: [OIDCGrantType.OIDCGRANTTYPE_AUTHORIZATION_CODE],
// authMethodType: OIDCAuthMethodType.OIDCAUTHMETHODTYPE_NONE,
// };
// return config;
case IMPLICIT_METHOD.key:
config = {
responseTypesList: [OIDCResponseType.OIDCRESPONSETYPE_ID_TOKEN_TOKEN],
grantTypesList: [OIDCGrantType.OIDCGRANTTYPE_IMPLICIT],
authMethodType: OIDCAuthMethodType.OIDCAUTHMETHODTYPE_NONE,
};
return config;
default:
return undefined;
}
}
export function getAuthMethodFromPartialConfig(config: Partial<OIDCConfig.AsObject> | OIDCConfig.AsObject): string {
const toCheck = [config.responseTypesList, config.grantTypesList, config.authMethodType];
const code = JSON.stringify(
[
[OIDCResponseType.OIDCRESPONSETYPE_CODE],
[OIDCGrantType.OIDCGRANTTYPE_AUTHORIZATION_CODE],
OIDCAuthMethodType.OIDCAUTHMETHODTYPE_BASIC,
]
);
const pkce = JSON.stringify(
[
[OIDCResponseType.OIDCRESPONSETYPE_CODE],
[OIDCGrantType.OIDCGRANTTYPE_AUTHORIZATION_CODE],
OIDCAuthMethodType.OIDCAUTHMETHODTYPE_NONE,
]
);
const post = JSON.stringify(
[
[OIDCResponseType.OIDCRESPONSETYPE_CODE],
[OIDCGrantType.OIDCGRANTTYPE_AUTHORIZATION_CODE],
OIDCAuthMethodType.OIDCAUTHMETHODTYPE_POST,
]
);
// const pk_jwt = JSON.stringify(
// [
// [OIDCResponseType.OIDCRESPONSETYPE_CODE],
// [OIDCGrantType.OIDCGRANTTYPE_AUTHORIZATION_CODE],
// OIDCAuthMethodType.OIDCAUTHMETHODTYPE_BASIC,
// ]
// );
const implicit = JSON.stringify(
[
[OIDCResponseType.OIDCRESPONSETYPE_ID_TOKEN_TOKEN],
[OIDCGrantType.OIDCGRANTTYPE_IMPLICIT],
OIDCAuthMethodType.OIDCAUTHMETHODTYPE_NONE,
]
);
switch (JSON.stringify(toCheck)) {
case code: return CODE_METHOD.key;
case pkce: return PKCE_METHOD.key;
case post: return POST_METHOD.key;
// case pk_jwt: return PK_JWT_METHOD.key;
case implicit: return IMPLICIT_METHOD.key;
default:
return CUSTOM_METHOD.key;
}
}

View File

@ -0,0 +1,25 @@
import { OIDCApplicationType } from 'src/app/proto/generated/management_pb';
export const WEB_TYPE = {
titleI18nKey: 'APP.OIDC.SELECTION.APPTYPE.WEB.TITLE',
descI18nKey: 'APP.OIDC.SELECTION.APPTYPE.WEB.DESCRIPTION',
type: OIDCApplicationType.OIDCAPPLICATIONTYPE_WEB,
prefix: 'WEB',
background: 'rgb(80, 110, 110)',
};
export const USER_AGENT_TYPE = {
titleI18nKey: 'APP.OIDC.SELECTION.APPTYPE.USERAGENT.TITLE',
descI18nKey: 'APP.OIDC.SELECTION.APPTYPE.USERAGENT.DESCRIPTION',
type: OIDCApplicationType.OIDCAPPLICATIONTYPE_USER_AGENT,
prefix: 'UA',
background: '#6a506e',
};
export const NATIVE_TYPE = {
titleI18nKey: 'APP.OIDC.SELECTION.APPTYPE.NATIVE.TITLE',
descI18nKey: 'APP.OIDC.SELECTION.APPTYPE.NATIVE.DESCRIPTION',
type: OIDCApplicationType.OIDCAPPLICATIONTYPE_NATIVE,
prefix: 'N',
background: '#595d80',
};

View File

@ -0,0 +1,28 @@
<form class="form" (ngSubmit)="add(redInput)">
<cnsl-form-field class="formfield">
<cnsl-label>{{ title }}</cnsl-label>
<input #redInput cnslInput placeholder="ex. https://" [formControl]="redirectControl">
</cnsl-form-field>
<button matTooltip="{{'ACTIONS.ADD' | translate}}" type="submit" mat-icon-button
[disabled]="redirectControl.invalid || !canWrite">
<mat-icon>add</mat-icon>
</button>
</form>
<div class="uri-list">
<div *ngFor="let uri of urisList" class="uri-line">
<span class="uri"
[ngClass]="{'green': !devMode && uri?.startsWith('https://'), 'red': !devMode && !uri?.startsWith('https://')}">{{uri}}</span>
<span class="fill-space"></span>
<i *ngIf="!devMode && !uri?.startsWith('https://')" class="las la-exclamation red"></i>
<!-- <i *ngIf="!devMode && uri?.startsWith('https://')" class="las la-check green"></i> -->
<button matTooltip="{{'ACTIONS.DELETE' | translate}}" mat-icon-button (click)="remove(uri)" class="icon-button">
<mat-icon>cancel</mat-icon>
</button>
</div>
</div>
<p *ngIf="redirectControl.value && redirectControl.invalid" class="error">
{{'APP.OIDC.REDIRECTNOTVALID' | translate}}</p>

View File

@ -0,0 +1,61 @@
.form {
display: flex;
align-items: flex-end;
min-width: 320px;
.formfield {
flex: 1;
}
button {
margin-bottom: 14px;
margin-right: -0.5rem;
}
}
.uri-list {
margin: 0 .5rem;
width: 100%;
.uri-line {
display: flex;
align-items: center;
.uri {
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
font-size: 14px;
}
.fill-space {
flex: 1;
}
i.green {
font-size: 1rem;
line-height: 35px;
height: 30px;
}
i.red {
font-size: 1.2rem;
}
.icon-button {
height: 30px;
line-height: 30px;
mat-icon {
font-size: 1rem !important;
}
&:not(:hover) {
color: var(--grey);
}
}
}
}
.error {
font-size: 13px;
color: #f44336;
margin: 0 .5rem 1.5rem .5rem;
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RedirectUrisComponent } from './redirect-uris.component';
describe('RedirectUrisComponent', () => {
let component: RedirectUrisComponent;
let fixture: ComponentFixture<RedirectUrisComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ RedirectUrisComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(RedirectUrisComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,43 @@
import { Component, EventEmitter, Input, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
@Component({
selector: 'cnsl-redirect-uris',
templateUrl: './redirect-uris.component.html',
styleUrls: ['./redirect-uris.component.scss']
})
export class RedirectUrisComponent implements OnInit {
@Input() title: string = '';
@Input() devMode: boolean = false;
@Input() canWrite: boolean = false;
@Input() public urisList: string[] = [];
@Input() public redirectControl: FormControl = new FormControl({ value: '', disabled: true });
@Input() public changedUris: EventEmitter<string[]> = new EventEmitter();
constructor() { }
ngOnInit(): void {
if (this.canWrite) {
this.redirectControl.enable();
}
}
public add(input: any): void {
if (this.redirectControl.valid) {
if (input.value !== '' && input.value !== ' ' && input.value !== '/') {
this.urisList.push(input.value);
}
if (input) {
input.value = '';
}
}
}
public remove(redirect: any): void {
console.log(redirect);
const index = this.urisList.indexOf(redirect);
if (index >= 0) {
this.urisList.splice(index, 1);
}
}
}

View File

@ -11,18 +11,27 @@
</div> </div>
<div [routerLink]="['/projects', projectId, 'apps', app.id ]" class="app-wrap" <div [routerLink]="['/projects', projectId, 'apps', app.id ]" class="app-wrap"
*ngFor="let app of appsSubject | async"> *ngFor="let app of appsSubject | async"
<div class="morph-card" matRipple> matTooltip="{{'APP.OIDC.APPTYPE'+app.oidcConfig.applicationType | translate}}">
<cnsl-app-card class="grid-card" matRipple [type]="app.oidcConfig?.applicationType">
{{ app.name.charAt(0)}} {{ app.name.charAt(0)}}
</div> <i *ngIf="app.oidcConfig.applicationType == OIDCApplicationType.OIDCAPPLICATIONTYPE_NATIVE"
class="las la-mobile"></i>
<i *ngIf="app.oidcConfig.applicationType == OIDCApplicationType.OIDCAPPLICATIONTYPE_WEB"
class="las la-code"></i>
<i *ngIf="app.oidcConfig.applicationType == OIDCApplicationType.OIDCAPPLICATIONTYPE_USER_AGENT"
class="las la-code"></i>
</cnsl-app-card>
<span class="name">{{app.name}}</span> <span class="name">{{app.name}}</span>
<span class="type">{{'APP.OIDC.APPTYPE'+app.oidcConfig.applicationType | translate}}</span>
</div> </div>
<ng-template appHasRole [appHasRole]="['project.app.write']"> <ng-template appHasRole [appHasRole]="['project.app.write']">
<div class="app-wrap" *ngIf="!disabled" [routerLink]="[ '/projects', projectId, 'apps', 'create']"> <div class="app-wrap" *ngIf="!disabled" [routerLink]="[ '/projects', projectId, 'apps', 'create']">
<div class="morph-card add" matRipple> <cnsl-app-card class="grid-card add" matRipple>
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
</div> </cnsl-app-card>
<span class="name">{{ 'ACTIONS.NEW' | translate }}</span> <span class="name">{{ 'ACTIONS.NEW' | translate }}</span>
</div> </div>
</ng-template> </ng-template>

View File

@ -1,82 +1,62 @@
@import '~@angular/material/theming';
@mixin application-grid-theme($theme) {
/* stylelint-disable */
$primary: map-get($theme, primary);
$primary-dark: mat-color($primary, A900);
$accent: map-get($theme, accent);
$accent-color: mat-color($primary, 500);
/* stylelint-enable */
.app-grid-header { .app-grid-header {
display: flex; display: flex;
align-items: center; align-items: center;
.fill-space { .fill-space {
flex: 1; flex: 1;
} }
} }
.app-container { .app-container {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
margin: 0 -1rem; margin: 0 -1rem;
padding-bottom: 2rem; padding-bottom: 2rem;
.sp-container { .sp-container {
display: flex;
justify-content: center;
align-items: center;
width: calc(82px + 2rem);
height: calc(82px + 2rem);
}
.app-wrap {
outline: none;
display: flex;
flex-direction: column;
align-items: center;
max-width: 150px;
.morph-card {
cursor: pointer;
animation: all .2s;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
font-size: 2rem; width: calc(82px + 2rem);
height: 80px; height: calc(82px + 2rem);
width: 80px; }
margin: 1rem;
text-transform: uppercase; .app-wrap {
border-radius: .5rem; outline: none;
border: 2px solid $accent-color; display: flex;
font-weight: 800; flex-direction: column;
background-color: $primary-dark; align-items: center;
transition: background-color box-shadow .3s ease-in; max-width: 150px;
background-image:
linear-gradient(transparent 11px, rgba($accent-color, .5) 12px, transparent 12px), .grid-card {
linear-gradient(90deg, transparent 11px, rgba($accent-color, .5) 12px, transparent 12px); cursor: pointer;
background-size: 100% 12px, 12px 100%; display: flex;
flex-direction: column;
&:hover { justify-content: center;
background-color: rgba($accent-color, .1); align-items: center;
} font-size: 2rem;
height: 80px;
&.add { width: 80px;
background: $accent-color; margin: 1rem;
color: white; text-transform: uppercase;
border-radius: .5rem;
&:hover { font-weight: 800;
background-color: rgba($accent-color, .8); box-sizing: border-box;
}
} &.add {
} border: 2px solid var(--grey);
}
.name { }
font-size: .8rem;
color: #8a868a; .name {
} font-size: 14px;
}
.type {
font-size: 12px;
color: #8a868a;
}
} }
}
} }

View File

@ -1,8 +1,9 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { BehaviorSubject, from, Observable, of } from 'rxjs'; import { BehaviorSubject, from, Observable, of } from 'rxjs';
import { catchError, finalize, map } from 'rxjs/operators'; import { catchError, finalize, map } from 'rxjs/operators';
import { Application } from 'src/app/proto/generated/management_pb'; import { Application, OIDCApplicationType, OIDCResponseType } from 'src/app/proto/generated/management_pb';
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
import { NATIVE_TYPE, USER_AGENT_TYPE, WEB_TYPE } from '../../../apps/authtypes';
@Component({ @Component({
selector: 'app-application-grid', selector: 'app-application-grid',
@ -16,6 +17,11 @@ export class ApplicationGridComponent implements OnInit {
public appsSubject: BehaviorSubject<Application.AsObject[]> = new BehaviorSubject<Application.AsObject[]>([]); public appsSubject: BehaviorSubject<Application.AsObject[]> = new BehaviorSubject<Application.AsObject[]>([]);
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true); private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
public loading$: Observable<boolean> = this.loadingSubject.asObservable(); public loading$: Observable<boolean> = this.loadingSubject.asObservable();
public OIDCApplicationType: any = OIDCApplicationType;
public NATIVE_TYPE: any = NATIVE_TYPE;
public WEB_TYPE: any = WEB_TYPE;
public USER_AGENT_TYPE: any = USER_AGENT_TYPE;
constructor(private mgmtService: ManagementService) { } constructor(private mgmtService: ManagementService) { }

View File

@ -24,10 +24,17 @@
</ng-container> </ng-container>
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.NAME' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'APP.NAME' | translate }} </th>
<td class="pointer" [routerLink]="['/projects', projectId, 'apps', role.id ]" mat-cell <td class="pointer" [routerLink]="['/projects', projectId, 'apps', app.id ]" mat-cell
*matCellDef="let role"> *matCellDef="let app">
{{role.name}} </td> {{app.name}} </td>
</ng-container>
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef> {{ 'APP.OIDC.APPTYPE' | translate }} </th>
<td class="pointer" [routerLink]="['/projects', projectId, 'apps', app.id ]" mat-cell
*matCellDef="let app">
{{'APP.OIDC.APPTYPE'+app?.oidcConfig?.applicationType | translate}} </td>
</ng-container> </ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>

View File

@ -26,7 +26,7 @@ export class ApplicationsComponent implements AfterViewInit, OnInit {
public dataSource!: ProjectApplicationsDataSource; public dataSource!: ProjectApplicationsDataSource;
public selection: SelectionModel<Application.AsObject> = new SelectionModel<Application.AsObject>(true, []); public selection: SelectionModel<Application.AsObject> = new SelectionModel<Application.AsObject>(true, []);
public displayedColumns: string[] = ['select', 'name']; public displayedColumns: string[] = ['select', 'name', 'type'];
constructor(private mgmtService: ManagementService, private toast: ToastService) { } constructor(private mgmtService: ManagementService, private toast: ToastService) { }

View File

@ -8,7 +8,7 @@
<ng-template appHasRole [appHasRole]="['project.write:'+projectId, 'project.write']"> <ng-template appHasRole [appHasRole]="['project.write:'+projectId, 'project.write']">
<button matTooltip="{{'ACTIONS.EDIT' | translate}}" mat-icon-button (click)="editstate = !editstate" <button matTooltip="{{'ACTIONS.EDIT' | translate}}" mat-icon-button (click)="editstate = !editstate"
aria-label="Edit project name" *ngIf="isZitadel === false"> aria-label="Edit project name" *ngIf="isZitadel === false">
<mat-icon *ngIf="!editstate">edit</mat-icon> <i *ngIf="!editstate" class="las la-edit"></i>
<mat-icon *ngIf="editstate">close</mat-icon> <mat-icon *ngIf="editstate">close</mat-icon>
</button> </button>
</ng-template> </ng-template>
@ -24,11 +24,13 @@
<button mat-stroked-button color="warn" <button mat-stroked-button color="warn"
[disabled]="isZitadel || (['project.write$', 'project.write:'+ project.projectId]| hasRole | async) == false" [disabled]="isZitadel || (['project.write$', 'project.write:'+ project.projectId]| hasRole | async) == false"
*ngIf="project?.state === ProjectState.PROJECTSTATE_ACTIVE" *ngIf="project?.state === ProjectState.PROJECTSTATE_ACTIVE"
(click)="changeState(ProjectState.PROJECTSTATE_INACTIVE)">{{'PROJECT.TABLE.DEACTIVATE' | translate}}</button> (click)="changeState(ProjectState.PROJECTSTATE_INACTIVE)">{{'PROJECT.TABLE.DEACTIVATE' |
translate}}</button>
<button mat-stroked-button color="warn" <button mat-stroked-button color="warn"
[disabled]="isZitadel || (['project.write$', 'project.write:'+ project.projectId]| hasRole | async) == false" [disabled]="isZitadel || (['project.write$', 'project.write:'+ project.projectId]| hasRole | async) == false"
*ngIf="project?.state === ProjectState.PROJECTSTATE_INACTIVE" *ngIf="project?.state === ProjectState.PROJECTSTATE_INACTIVE"
(click)="changeState(ProjectState.PROJECTSTATE_ACTIVE)">{{'PROJECT.TABLE.ACTIVATE' | translate}}</button> (click)="changeState(ProjectState.PROJECTSTATE_ACTIVE)">{{'PROJECT.TABLE.ACTIVATE' |
translate}}</button>
<div class="full-width"> <div class="full-width">
<div class="line"> <div class="line">
@ -78,7 +80,7 @@
</ng-template> </ng-template>
<ng-template appHasRole [appHasRole]="['project.role.read:' + project.projectId, 'project.role.read']"> <ng-template appHasRole [appHasRole]="['project.role.read:' + project.projectId, 'project.role.read']">
<app-card title="{{ 'PROJECT.ROLE.TITLE' | translate }}" <app-card id="roles" title="{{ 'PROJECT.ROLE.TITLE' | translate }}"
description="{{ 'PROJECT.ROLE.DESCRIPTION' | translate }}"> description="{{ 'PROJECT.ROLE.DESCRIPTION' | translate }}">
<p>{{'PROJECT.ROLE.OPTIONS' | translate}}</p> <p>{{'PROJECT.ROLE.OPTIONS' | translate}}</p>
<mat-checkbox [(ngModel)]="project.projectRoleAssertion" (change)="saveProject()" <mat-checkbox [(ngModel)]="project.projectRoleAssertion" (change)="saveProject()"
@ -118,7 +120,8 @@
<div class="meta-row"> <div class="meta-row">
<span class="first">{{'PROJECT.STATE.TITLE' | translate}}:</span> <span class="first">{{'PROJECT.STATE.TITLE' | translate}}:</span>
<span *ngIf="project && project.state !== undefined" class="state" <span *ngIf="project && project.state !== undefined" class="state"
[ngClass]="{'active': project.state === ProjectState.PROJECTSTATE_ACTIVE, 'inactive': project.state === ProjectState.PROJECTSTATE_INACTIVE}">{{'PROJECT.STATE.'+project.state | translate}}</span> [ngClass]="{'active': project.state === ProjectState.PROJECTSTATE_ACTIVE, 'inactive': project.state === ProjectState.PROJECTSTATE_INACTIVE}">{{'PROJECT.STATE.'+project.state
| translate}}</span>
</div> </div>
</div> </div>

View File

@ -14,6 +14,7 @@ import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { MemberCreateDialogModule } from 'src/app/modules/add-member-dialog/member-create-dialog.module'; import { MemberCreateDialogModule } from 'src/app/modules/add-member-dialog/member-create-dialog.module';
import { AppCardModule } from 'src/app/modules/app-card/app-card.module';
import { CardModule } from 'src/app/modules/card/card.module'; import { CardModule } from 'src/app/modules/card/card.module';
import { ChangesModule } from 'src/app/modules/changes/changes.module'; import { ChangesModule } from 'src/app/modules/changes/changes.module';
import { ContributorsModule } from 'src/app/modules/contributors/contributors.module'; import { ContributorsModule } from 'src/app/modules/contributors/contributors.module';
@ -43,6 +44,7 @@ import { ProjectGrantsComponent } from './project-grants/project-grants.componen
imports: [ imports: [
CommonModule, CommonModule,
FormsModule, FormsModule,
AppCardModule,
OwnedProjectDetailRoutingModule, OwnedProjectDetailRoutingModule,
TranslateModule, TranslateModule,
ReactiveFormsModule, ReactiveFormsModule,

View File

@ -1362,4 +1362,11 @@ export class ManagementService {
public UpdateOIDCAppConfig(req: OIDCConfigUpdate): Promise<OIDCConfig> { public UpdateOIDCAppConfig(req: OIDCConfigUpdate): Promise<OIDCConfig> {
return this.grpcService.mgmt.updateApplicationOIDCConfig(req); return this.grpcService.mgmt.updateApplicationOIDCConfig(req);
} }
public RemoveApplication(projectId: string, appId: string): Promise<Empty> {
const req = new ApplicationID();
req.setId(appId);
req.setProjectId(projectId);
return this.grpcService.mgmt.removeApplication(req);
}
} }

View File

@ -961,12 +961,34 @@
"1": "Aktiv", "1": "Aktiv",
"2": "Inaktiv" "2": "Inaktiv"
} }
},
"DIALOG": {
"DELETE": {
"TITLE": "App löschen",
"DESCRIPTION":"Wollen Sie diese App wirklich löschen?"
}
},
"NEXTSTEPS": {
"TITLE":"Nächste Schritte",
"0": {
"TITLE":"Rollen festlegen",
"DESC":"Erfassen Sie Rollen für ihr Projekt"
},
"1": {
"TITLE":"Benutzer hinzufügen",
"DESC":"Fügen Sie Nutzer ihrer Organisation hinzu"
},
"2": {
"TITLE":"Hilfe & Support",
"DESC":"Lesen Sie unsere Dokumentation zum Erstellen von Applikation oder kontaktieren Sie unseren Support"
}
} }
}, },
"NAME": "Name", "NAME": "Name",
"TYPE":"Anwendungstyp", "TYPE":"Anwendungstyp",
"GRANT":"Berechtigungstypen", "GRANT":"Berechtigungstypen",
"OIDC": { "OIDC": {
"CURRENT":"Aktuelle Konfiguration",
"TOKENSECTIONTITLE":"AuthToken Optionen", "TOKENSECTIONTITLE":"AuthToken Optionen",
"REDIRECTSECTIONTITLE":"Weiterleitungseinstellungen", "REDIRECTSECTIONTITLE":"Weiterleitungseinstellungen",
"PROSWITCH":"Konfigurator überspringen", "PROSWITCH":"Konfigurator überspringen",
@ -1022,13 +1044,58 @@
"IDTOKENROLEASSERTION_DESCRIPTION":"Bei Auswahl werden dem Id Token die Rollen des Authentifizierten Benutzers hinzugefügt.", "IDTOKENROLEASSERTION_DESCRIPTION":"Bei Auswahl werden dem Id Token die Rollen des Authentifizierten Benutzers hinzugefügt.",
"IDTOKENUSERINFOASSERTION":"User Info im ID Token", "IDTOKENUSERINFOASSERTION":"User Info im ID Token",
"IDTOKENUSERINFOASSERTION_DESCRIPTION":"Ermöglich OIDC clients claims von profile, email, phone und address direkt vom ID Token zu beziehen.", "IDTOKENUSERINFOASSERTION_DESCRIPTION":"Ermöglich OIDC clients claims von profile, email, phone und address direkt vom ID Token zu beziehen.",
"CLOCKSKEW":"ermöglicht Clients, den Taktversatz von OP und Client zu verarbeiten. Die Dauer (0-5s) wird der exp addiert und von iats, auth_time und nbf abgezogen." "CLOCKSKEW":"ermöglicht Clients, den Taktversatz von OP und Client zu verarbeiten. Die Dauer (0-5s) wird der exp addiert und von iats, auth_time und nbf abgezogen.",
"RECOMMENDED":"Empfohlen",
"NOTRECOMMENDED":"nicht empfohlen",
"SELECTION":{
"APPTYPE": {
"WEB": {
"TITLE":"Web",
"DESCRIPTION":"Standard Web applications wie .net, PHP, Node.js, Java, etc."
},
"NATIVE": {
"TITLE":"NATIVE",
"DESCRIPTION":"Mobile Apps, Desktop, Smart Devices, etc."
},
"USERAGENT": {
"TITLE":"User Agent",
"DESCRIPTION":"Single Page Applications (SPA) und grundsätzlich alle im Browser aufgeführten JS Frameworks"
}
},
"AUTHMETHOD": {
"CODE": {
"TITLE":"Code",
"DESCRIPTION":"Tausche den Authorization Code gegen Tokens ein"
},
"PKCE": {
"TITLE":"PKCE",
"DESCRIPTION":"Nutze einen Zufalls Hash Wert anstelle des Client Secret für mehr Sicherheit"
},
"POST": {
"TITLE":"POST",
"DESCRIPTION":"Sende client_id und client_secret im (HTML) Formular"
},
"PK_JWT": {
"TITLE":"Private Key JWT",
"DESCRIPTION":"Nutze einen Private Key um deine Application zu authentifizieren"
},
"IMPLICIT": {
"TITLE":"Implicit",
"DESCRIPTION":"Erhalte die Token direkt vom authorize Endpoint"
},
"CUSTOM": {
"TITLE":"Custom",
"DESCRIPTION":"Deine Konfiguration entspricht keiner anderen Option."
}
}
}
}, },
"TOAST": { "TOAST": {
"REACTIVATED":"Anwendung reaktiviert.", "REACTIVATED":"Anwendung reaktiviert.",
"DEACTIVATED":"Anwendung deaktiviert.", "DEACTIVATED":"Anwendung deaktiviert.",
"OIDCUPDATED":"OIDC-Konfiguration geändert.", "OIDCUPDATED":"OIDC-Konfiguration geändert.",
"OIDCCLIENTSECRETREGENERATED":"OIDC-Client Secret generiert." "OIDCCLIENTSECRETREGENERATED":"OIDC-Client Secret generiert.",
"DELETED":"App gelöscht."
} }
}, },
"GENDERS": { "GENDERS": {

View File

@ -961,12 +961,34 @@
"1": "Active", "1": "Active",
"2": "Inactive" "2": "Inactive"
} }
},
"DIALOG": {
"DELETE": {
"TITLE": "Delete App",
"DESCRIPTION":"Do you really want to delete this application?"
}
},
"NEXTSTEPS": {
"TITLE":"Next Steps",
"0": {
"TITLE":"Add roles",
"DESC":"Enter your project roles"
},
"1": {
"TITLE":"Add users",
"DESC":"Add new users of your organization"
},
"2": {
"TITLE":"Help & Support",
"DESC":"Read our documentation on creating applications or contact our support"
}
} }
}, },
"NAME": "Name", "NAME": "Name",
"TYPE":"Application Type", "TYPE":"Application Type",
"GRANT":"Grant Types", "GRANT":"Grant Types",
"OIDC": { "OIDC": {
"CURRENT":"Current Config",
"TOKENSECTIONTITLE":"AuthToken Options", "TOKENSECTIONTITLE":"AuthToken Options",
"REDIRECTSECTIONTITLE":"Redirect Settings", "REDIRECTSECTIONTITLE":"Redirect Settings",
"PROSWITCH":"I'm a pro. Skip this wizard.", "PROSWITCH":"I'm a pro. Skip this wizard.",
@ -1022,13 +1044,58 @@
"IDTOKENROLEASSERTION_DESCRIPTION":"If selected, the roles of the authenticated user are added to the ID token.", "IDTOKENROLEASSERTION_DESCRIPTION":"If selected, the roles of the authenticated user are added to the ID token.",
"IDTOKENUSERINFOASSERTION":"User Info inside ID Token", "IDTOKENUSERINFOASSERTION":"User Info inside ID Token",
"IDTOKENUSERINFOASSERTION_DESCRIPTION":"Enables clients to retrieve profile, email, phone and address claims from ID token.", "IDTOKENUSERINFOASSERTION_DESCRIPTION":"Enables clients to retrieve profile, email, phone and address claims from ID token.",
"CLOCKSKEW":"Enables clients to handle clock skew of OP and client. The duration (0-5s) will be added to exp claim and subtracted from iats, auth_time and nbf." "CLOCKSKEW":"Enables clients to handle clock skew of OP and client. The duration (0-5s) will be added to exp claim and subtracted from iats, auth_time and nbf.",
"RECOMMENDED":"recommended",
"NOTRECOMMENDED":"not recommended",
"SELECTION":{
"APPTYPE": {
"WEB": {
"TITLE":"Web",
"DESCRIPTION":"Regular Web applications like .net, PHP, Node.js, Java, etc."
},
"NATIVE": {
"TITLE":"NATIVE",
"DESCRIPTION":"Mobile Apps, Desktop, Smart Devices, etc."
},
"USERAGENT": {
"TITLE":"User Agent",
"DESCRIPTION":"Single Page Applications (SPA) and in general all JS frameworks executed in browsers"
}
},
"AUTHMETHOD": {
"CODE": {
"TITLE":"Code",
"DESCRIPTION":"Exchange the authorization code for the tokens"
},
"PKCE": {
"TITLE":"PKCE",
"DESCRIPTION":"Use a random hash instead of a static client secret for more security"
},
"POST": {
"TITLE":"POST",
"DESCRIPTION":"Send client_id and client_secret as part of the form"
},
"PK_JWT": {
"TITLE":"Private Key JWT",
"DESCRIPTION":"Use a private key to authorize your application"
},
"IMPLICIT": {
"TITLE":"Implicit",
"DESCRIPTION":"Get the tokens directly from the authorization endpoint"
},
"CUSTOM": {
"TITLE":"Custom",
"DESCRIPTION":"Your setting doesn't correspond to any other option."
}
}
}
}, },
"TOAST": { "TOAST": {
"REACTIVATED":"Application reactivated.", "REACTIVATED":"Application reactivated.",
"DEACTIVATED":"Application deactivated.", "DEACTIVATED":"Application deactivated.",
"OIDCUPDATED":"OIDC configuration updated.", "OIDCUPDATED":"OIDC configuration updated.",
"OIDCCLIENTSECRETREGENERATED":"OIDC client secret generated." "OIDCCLIENTSECRETREGENERATED":"OIDC client secret generated.",
"DELETED":"App deleted."
} }
}, },
"GENDERS": { "GENDERS": {

View File

@ -5,10 +5,12 @@
@import './styles/link.scss'; @import './styles/link.scss';
@import './styles/sidenav-list'; @import './styles/sidenav-list';
@import 'src/app/modules/avatar/avatar.component'; @import 'src/app/modules/avatar/avatar.component';
@import 'src/app/modules/app-radio/app-type-radio/app-type-radio.component';
@import 'src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component';
@import 'src/app/modules/changes/changes.component'; @import 'src/app/modules/changes/changes.component';
@import 'src/app/modules/info-section/info-section.component'; @import 'src/app/modules/info-section/info-section.component';
@import 'src/app/modules/detail-layout/detail-layout.component'; @import 'src/app/modules/detail-layout/detail-layout.component';
@import 'src/app/pages/projects/owned-projects/owned-project-detail/application-grid/application-grid.component'; @import 'src/app/modules/app-card/app-card.component';
@import 'src/app/pages/users/user-detail/auth-user-detail/theme-setting/theme-card'; @import 'src/app/pages/users/user-detail/auth-user-detail/theme-setting/theme-card';
@import 'src/app/pages/users/user-detail/memberships/memberships.component'; @import 'src/app/pages/users/user-detail/memberships/memberships.component';
@import 'src/app/app.component.scss'; @import 'src/app/app.component.scss';
@ -19,11 +21,13 @@
@mixin component-themes($theme) { @mixin component-themes($theme) {
@include avatar-theme($theme); @include avatar-theme($theme);
@include app-type-radio-theme($theme);
@include app-auth-method-radio-theme($theme);
@include card-theme($theme); @include card-theme($theme);
@include table-theme($theme); @include table-theme($theme);
@include detail-layout-theme($theme); @include detail-layout-theme($theme);
@include sidenav-list-theme($theme); @include sidenav-list-theme($theme);
@include application-grid-theme($theme); @include app-card-theme($theme);
@include membership-theme($theme); @include membership-theme($theme);
@include changes-theme($theme); @include changes-theme($theme);
@include theme-card($theme); @include theme-card($theme);

View File

@ -64,7 +64,8 @@
} }
} }
.mat-checkbox { td .mat-checkbox,
th .mat-checkbox {
margin-left: 1rem; margin-left: 1rem;
} }