resolve conflicts

This commit is contained in:
Elio Bischof 2022-08-24 10:14:05 +02:00
commit d6a05b3a61
No known key found for this signature in database
GPG Key ID: 7B383FDE4DDBF1BD
611 changed files with 34386 additions and 38671 deletions

3
.artifacts/zitadel/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
*
!.gitignore
!.gitkeep

View File

@ -2,7 +2,7 @@
name: "\U0001F41B Bug report" name: "\U0001F41B Bug report"
about: Create a report to help us improve about: Create a report to help us improve
title: '' title: ''
labels: bug labels: 'state: triage, type: bug'
assignees: '' assignees: ''
--- ---

View File

@ -1,13 +0,0 @@
---
name: OKR
about: The objective key result is an overarching goal that we are working towards.
title: ''
labels: OKR
assignees: ''
---
OKR description
- [ ] Link to subordinate story
- [ ] Link to subordinate story

View File

@ -1,14 +0,0 @@
---
name: Story
about: A story describes the basic objective and contains several tasks that are to
be implemented
title: ''
labels: story
assignees: ''
---
As a [type of user], I want [a goal or objective], so that [customer benefit or value].
- [ ] Link to subordinate task
- [ ] Link to subordinate task

View File

@ -3,16 +3,13 @@ name: Task
about: A task describes what is to be implemented and which acceptance criteria must about: A task describes what is to be implemented and which acceptance criteria must
be met. be met.
title: '' title: ''
labels: task labels: 'state: triage'
assignees: '' assignees: ''
--- ---
Description Description
- [ ] Todo
- [ ] Todo
**Acceptance criteria** **Acceptance criteria**
- [ ] ... - [ ] ...
- [ ] ... - [ ] ...

49
.github/workflows/e2e.yml vendored Normal file
View File

@ -0,0 +1,49 @@
name: "ZITADEL e2e Tests"
on:
workflow_run:
workflows: [ZITADEL Release]
types:
- completed
workflow_dispatch:
inputs:
releaseversion:
description: 'Release version to test'
required: true
default: 'latest'
jobs:
test:
strategy:
matrix:
browser: [firefox, chrome]
runs-on: ubuntu-20.04
env:
ZITADEL_IMAGE_REGISTRY: 'ghcr.io/zitadel/zitadel'
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Set TAG env manual trigger
if: github.event_name == 'workflow_dispatch'
run: echo "ZITADEL_IMAGE=${ZITADEL_IMAGE_REGISTRY}:${{ github.event.inputs.releaseversion }}" >> $GITHUB_ENV
- name: Set TAG env on ZITADEL release
if: github.event_name == 'workflow_run'
run: echo "ZITADEL_IMAGE=${ZITADEL_IMAGE_REGISTRY}:${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
with:
driver: docker
install: true
- name: Test ${{ matrix.browser }}
run: docker compose run e2e --browser ${{ matrix.browser }}
working-directory: e2e
- name: Archive production tests ${{ matrix.browser }}
if: always()
uses: actions/upload-artifact@v2
with:
name: production-tests-${{ matrix.browser }}
path: |
e2e/cypress/results
e2e/cypress/videos
e2e/cypress/screenshots
retention-days: 30

52
.github/workflows/release-channels.yml vendored Normal file
View File

@ -0,0 +1,52 @@
name: ZITADEL Release tags
on:
push:
branches:
- "main"
paths:
- 'release-channels.yaml'
workflow_dispatch:
permissions:
contents: write
packages: write
jobs:
Build:
runs-on: ubuntu-20.04
env:
DOCKER_BUILDKIT: 1
steps:
- name: Source checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: get stable tag
run: echo STABLE_RELEASE=$(yq eval '.stable' release-channels.yaml) >> $GITHUB_ENV
- name: checkout stable tag
uses: actions/checkout@v2
with:
fetch-depth: 0
ref: ${{ env.STABLE_RELEASE }}
- name: GitHub Container Registry Login
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: tibdex/github-app-token@v1
id: generate-token
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Google Artifact Registry Login
uses: docker/login-action@v1
with:
registry: europe-docker.pkg.dev
username: _json_key_base64
password: ${{ secrets.GCR_JSON_KEY_BASE64 }}
- name: copy release to stable
run: |
skopeo --version
skopeo copy --all docker://ghcr.io/zitadel/zitadel:$STABLE_RELEASE docker://ghcr.io/zitadel/zitadel:stable

54
.github/workflows/test-code.yml vendored Normal file
View File

@ -0,0 +1,54 @@
name: ZITADEL PR
on:
pull_request:
paths-ignore:
- 'docs/**'
- 'guides/**'
- '**.md'
jobs:
Test:
runs-on: ubuntu-20.04
env:
DOCKER_BUILDKIT: 1
steps:
- name: Source checkout
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
with:
driver: docker
install: true
- name: Install GoReleaser
uses: goreleaser/goreleaser-action@v3
with:
install-only: true
version: v1.8.3
- name: Build and Unit Test
run: GOOS="linux" GOARCH="amd64" goreleaser build --snapshot --single-target --rm-dist --output .artifacts/zitadel/zitadel
- name: Publish go coverage
uses: codecov/codecov-action@v3.1.0
with:
file: .artifacts/codecov/profile.cov
name: go-codecov
# As goreleaser doesn't build a dockerfile in snapshot mode, we have to build it here
- name: Build Docker Image
run: docker build -t zitadel:pr --file build/Dockerfile .artifacts/zitadel
- name: Run E2E Tests
run: docker compose run e2e
working-directory: e2e
env:
ZITADEL_IMAGE: zitadel:pr
- name: Archive Test Results
if: always()
uses: actions/upload-artifact@v2
with:
name: pull-request-tests
path: |
e2e/cypress/results
e2e/cypress/videos
e2e/cypress/screenshots
retention-days: 30

20
.github/workflows/test-docs.yml vendored Normal file
View File

@ -0,0 +1,20 @@
# ATTENTION: Although this workflow doesn't do much, it is still important.
# It is complementary to the workflow in the file test-code.yml.
# It enables to exclude files for the workflow and still mark the Test job as required without having pending PRs.
# GitHub recommends this solution here:
# https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/troubleshooting-required-status-checks#handling-skipped-but-required-checks
name: ZITADEL PR
on:
pull_request:
paths:
- 'docs/**'
- 'guides/**'
- '**.md'
jobs:
Test:
runs-on: ubuntu-20.04
steps:
- run: 'echo "No tests for docs are implemented, yet"'

View File

@ -1,45 +0,0 @@
name: ZITADEL PR
on:
pull_request:
jobs:
Go:
runs-on: ubuntu-20.04
env:
DOCKER_BUILDKIT: 1
steps:
- name: Source checkout
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
with:
driver: docker
install: true
- name: Test
run: docker build -f build/grpc/Dockerfile -t zitadel-base:local . && docker build -f build/zitadel/Dockerfile . -t zitadel-go-test --target go-codecov -o .artifacts/codecov
- name: Publish go coverage
uses: codecov/codecov-action@v3.1.0
with:
file: .artifacts/codecov/profile.cov
name: go-codecov
Angular:
runs-on: ubuntu-20.04
env:
DOCKER_BUILDKIT: 1
steps:
- name: Source checkout
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
with:
driver: docker
install: true
- name: Test
run: docker build -f build/grpc/Dockerfile -t zitadel-base:local . && docker build -f build/console/Dockerfile . -t zitadel-npm-base --target angular-build

3
.gitignore vendored
View File

@ -61,6 +61,7 @@ openapi/**/*.json
build/local/*.env build/local/*.env
migrations/cockroach/migrate_cloud.go migrations/cockroach/migrate_cloud.go
.notifications .notifications
.artifacts /.artifacts/*
!/.artifacts/zitadel
/zitadel /zitadel

View File

@ -5,16 +5,22 @@ release:
owner: zitadel owner: zitadel
name: zitadel name: zitadel
draft: false draft: false
# If set to auto, will mark the release as not ready for production
# in case there is an indicator for this in the tag e.g. v1.0.0-rc1
# If set to true, will mark the release as not ready for production.
# Default is false.
prerelease: auto prerelease: auto
before: before:
hooks: hooks:
# this file would invalidate go source caches
- sh -c "rm openapi/statik/statik.go || true"
- docker build -f build/grpc/Dockerfile -t zitadel-base:local . - docker build -f build/grpc/Dockerfile -t zitadel-base:local .
- docker build -f build/zitadel/Dockerfile . -t zitadel-go-test --target go-codecov -o .artifacts/codecov - docker build -f build/zitadel/Dockerfile . -t zitadel-go-test --target go-codecov -o .artifacts/codecov
- docker build -f build/zitadel/Dockerfile . -t zitadel-go-base --target go-copy -o .artifacts/grpc/go-client - docker build -f build/zitadel/Dockerfile . -t zitadel-go-base --target go-copy -o .artifacts/grpc/go-client
- sh -c "cp -r .artifacts/grpc/go-client/* ." - sh -c "cp -r .artifacts/grpc/go-client/* ."
- docker build -f build/console/Dockerfile . -t zitadel-npm-base --target npm-copy -o .artifacts/grpc/js-client - docker build -f build/console/Dockerfile . -t zitadel-npm-base --target npm-copy -o .artifacts/grpc/js-client
- docker build -f build/console/Dockerfile . -t zitadel-npm-base --target angular-export -o .artifacts/console - docker build -f build/console/Dockerfile . -t zitadel-npm-console --target angular-export -o .artifacts/console
- sh -c "cp -r .artifacts/console/* internal/api/ui/console/static/" - sh -c "cp -r .artifacts/console/* internal/api/ui/console/static/"
builds: builds:
@ -35,9 +41,8 @@ dist: .artifacts/goreleaser
dockers: dockers:
- image_templates: - image_templates:
- ghcr.io/zitadel/zitadel:{{ .Tag }}-amd64 - ghcr.io/zitadel/zitadel:{{ .Tag }}-amd64
- ghcr.io/zitadel/zitadel:{{ .ShortCommit }}-amd64
- europe-docker.pkg.dev/zitadel-common/zitadel-repo/zitadel:{{ .Tag }}-amd64 - europe-docker.pkg.dev/zitadel-common/zitadel-repo/zitadel:{{ .Tag }}-amd64
- europe-docker.pkg.dev/zitadel-common/zitadel-repo/zitadel:{{ .ShortCommit }}-amd64 goarch: amd64
use: buildx use: buildx
dockerfile: build/Dockerfile dockerfile: build/Dockerfile
build_flag_templates: build_flag_templates:
@ -45,11 +50,27 @@ dockers:
- image_templates: - image_templates:
- ghcr.io/zitadel/zitadel:{{ .Tag }}-arm64 - ghcr.io/zitadel/zitadel:{{ .Tag }}-arm64
- ghcr.io/zitadel/zitadel:{{ .ShortCommit }}-arm64 - ghcr.io/zitadel/zitadel:{{ .ShortCommit }}-arm64
goarch: arm64
use: buildx use: buildx
dockerfile: build/Dockerfile dockerfile: build/Dockerfile
build_flag_templates: build_flag_templates:
- "--platform=linux/arm64" - "--platform=linux/arm64"
docker_manifests:
- id: zitadel-latest
name_template: ghcr.io/zitadel/zitadel:latest
image_templates:
- ghcr.io/zitadel/zitadel:{{ .Tag }}-amd64
- ghcr.io/zitadel/zitadel:{{ .Tag }}-arm64
# Skips can and shall be set for individual manifests same as in dockers
skip_push: auto
- id: zitadel-Tag
name_template: ghcr.io/zitadel/zitadel:{{ .Tag }}
image_templates:
- ghcr.io/zitadel/zitadel:{{ .Tag }}-amd64
- ghcr.io/zitadel/zitadel:{{ .Tag }}-arm64
archives: archives:
- name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' - name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
replacements: replacements:
@ -96,6 +117,10 @@ brews:
- name: git - name: git
install: |- install: |-
bin.install "zitadel" bin.install "zitadel"
# If set to auto, the release will not be uploaded to the homebrew tap
# in case there is an indicator for prerelease in the tag e.g. v1.0.0-rc1
# Default is false.
skip_upload: auto
announce: announce:
discord: discord:

View File

@ -1,9 +1,7 @@
module.exports = { module.exports = {
branches: [ branches: [
{name: 'main'}, {name: 'main'},
{name: '1.x.x', range: '1.x.x', channel: '1.x.x'}, {name: '1.87.x', range: '1.87.x', channel: '1.87.x'},
{name: 'v2-alpha', prerelease: true},
{name: 'update-projection-on-query', prerelease: true},
], ],
plugins: [ plugins: [
"@semantic-release/commit-analyzer" "@semantic-release/commit-analyzer"

View File

@ -38,12 +38,10 @@ We accept contributions through pull requests. You need a github account for tha
### Submit a Pull Request (PR) ### Submit a Pull Request (PR)
> :warning: Currently main development is done on branch `v2-alpha`. Make sure you're merging into the correct branch.
1. [Fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo) the [zitadel/zitadel](https://github.com/zitadel/zitadel) repository on GitHub 1. [Fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo) the [zitadel/zitadel](https://github.com/zitadel/zitadel) repository on GitHub
2. On your fork, commit your changes to a new branch 2. On your fork, commit your changes to a new branch
`git checkout -b my-fix-branch v2-alpha` `git checkout -b my-fix-branch main`
3. Make your changes following the [guidelines](#contribute) in this guide. Make sure that all tests pass. 3. Make your changes following the [guidelines](#contribute) in this guide. Make sure that all tests pass.
@ -51,7 +49,7 @@ We accept contributions through pull requests. You need a github account for tha
`git commit --all` `git commit --all`
5. [Merge](https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging) the latest commit of the `v2-alpha`-branch 5. [Merge](https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging) the latest commit of the `main`-branch
6. Push the changes to your branch on Github 6. Push the changes to your branch on Github
@ -59,7 +57,7 @@ We accept contributions through pull requests. You need a github account for tha
7. Use [Semantic Release commit messages](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#type) to simplify creation of release notes. In the title of the pull request [correct tagging](#commit-messages) is required and will be requested by the reviewers. 7. Use [Semantic Release commit messages](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#type) to simplify creation of release notes. In the title of the pull request [correct tagging](#commit-messages) is required and will be requested by the reviewers.
8. On GitHub, [send a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/requesting-a-pull-request-review) to `zitadel:v2-alpha`. Request review from one of the maintainers. 8. On GitHub, [send a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/requesting-a-pull-request-review) to `zitadel:main`. Request review from one of the maintainers.
### Reviewing a Pull Request ### Reviewing a Pull Request
@ -76,7 +74,7 @@ Make sure you use [semantic release messages format](https://github.com/angular/
#### Type #### Type
Must be one of the following: Must be one of the following:
- **feat**: New Feature - **feat**: New Feature
- **fix**: Bug Fix - **fix**: Bug Fix
@ -124,7 +122,7 @@ Make sure to use the following configurations:
To run console locally, navigate to the console subfolder and run `npm install` and then `npm start` for a dev server. Navigate to http://localhost:4200/. The app will automatically reload if you change any of the source files. To run console locally, navigate to the console subfolder and run `npm install` and then `npm start` for a dev server. Navigate to http://localhost:4200/. The app will automatically reload if you change any of the source files.
Console loads its environment from the [environment.json](https://github.com/zitadel/zitadel/blob/v2-alpha/console/src/assets/environment.json), make sure to change the configuration to your instance project. Console loads its environment from the [environment.json](https://github.com/zitadel/zitadel/blob/main/console/src/assets/environment.json), make sure to change the configuration to your instance project.
When the backend is running locally ensure you are specifying your [localhost](http://localhost:8080/ui/console/assets/environment.json) endpoints. When the backend is running locally ensure you are specifying your [localhost](http://localhost:8080/ui/console/assets/environment.json) endpoints.
### API Definitions ### API Definitions
@ -155,7 +153,7 @@ Scope can be left empty (omit the brackets) or refer to the top navigation secti
ZITADEL loads translations from four files: ZITADEL loads translations from four files:
- [Console texts](./console/src/assets/i18n) - [Console texts](./console/src/assets/i18n)
- [Login interface](./internal/api/ui/login/static/i18n) - [Login interface](./internal/api/ui/login/static/i18n)
- [Email notification](./internal/notification/static/i18n) - [Email notification](./internal/notification/static/i18n)
- [Common texts](./internal/static/i18n) for success or error toasts - [Common texts](./internal/static/i18n) for success or error toasts

View File

@ -1,3 +1,14 @@
<p align="right">
<img src="./docs/static/img/github-header01-dark@2x.png#gh-dark-mode-only" alt="ZITADEL Cloud launched" max-height="200px" width="auto" />
<img src="./docs/static/img/github-header01-light@2x.png#gh-light-mode-only" alt="ZITADEL Cloud launched" max-height="200px" width="auto" />
</p>
<p align="center">
We are live on <a href="https://www.producthunt.com/posts/zitadel">ProductHunt</a>. <br> Thank you for the support and feedback.
</p>
---
<p align="center"> <p align="center">
<img src="./docs/static/logos/zitadel-logo-dark@2x.png#gh-light-mode-only" alt="Zitadel Logo" max-height="200px" width="auto" /> <img src="./docs/static/logos/zitadel-logo-dark@2x.png#gh-light-mode-only" alt="Zitadel Logo" max-height="200px" width="auto" />
<img src="./docs/static/logos/zitadel-logo-light@2x.png#gh-dark-mode-only" alt="Zitadel Logo" max-height="200px" width="auto" /> <img src="./docs/static/logos/zitadel-logo-light@2x.png#gh-dark-mode-only" alt="Zitadel Logo" max-height="200px" width="auto" />
@ -41,30 +52,30 @@ With ZITADEL you rely on a battle tested, hardened and extensible turnkey soluti
## Get started ## Get started
### ZITADEL Cloud ### ZITADEL Cloud (SaaS)
The easiest way to get started with ZITADEL is to use our public cloud offering. The easiest way to get started with ZITADEL is to use our public cloud offering.
Currently ZITADEL V2 Beta is available, head over to [https://zitadel.cloud](https://zitadel.cloud) and create your first ZITADEL instance for free. Currently ZITADEL V2 Beta is available, head over to [https://zitadel.cloud](https://zitadel.cloud) and create your first ZITADEL instance for free.
You can also discover our pay-as-you-go [pricing](https://zitadel.com/pricing/v2). You can also discover our pay-as-you-go [pricing](https://zitadel.com/pricing).
### Install ZITADEL ### Install ZITADEL
- [We provide installation guides for multiple platforms here](https://docs.zitadel.com/docs/guides/installation) - [We provide installation guides for multiple platforms here](https://docs.zitadel.com/docs/guides/deploy/overview)
### Quickstarts - Integrate your app ### Quickstarts - Integrate your app
- [Multiple Quickstarts can be found here](https://docs.zitadel.com/docs/quickstarts/introduction) - [Multiple Quickstarts can be found here](https://docs.zitadel.com/docs/guides/start/quickstart)
- [And even more examples are located under zitadel/zitadel-examples](https://github.com/zitadel/zitadel-examples) - [And even more examples are located under zitadel/zitadel-examples](https://github.com/zitadel/zitadel-examples)
> If you miss something please feel free to engage with us [here](https://github.com/zitadel/zitadel/discussions/1717) > If you miss something please feel free to [join the Discussion](https://github.com/zitadel/zitadel/discussions/1717)
## Why ZITADEL ## Why ZITADEL
- [API-first](https://docs.zitadel.com/docs/apis/introduction) - [API-first](https://docs.zitadel.com/docs/apis/introduction)
- Strong audit trail thanks to [event sourcing](https://docs.zitadel.com/docs/concepts/eventstore) - Strong audit trail thanks to [event sourcing](https://docs.zitadel.com/docs/concepts/eventstore/overview)
- [Actions](https://docs.zitadel.ch/docs/concepts/features/actions) to react on events with custom code - [Actions](https://docs.zitadel.ch/docs/concepts/features/actions) to react on events with custom code
- [Branding](https://docs.zitadel.com/docs/guides/customization/branding) for a uniform user experience - [Branding](https://docs.zitadel.com/docs/guides/manage/customize/branding) for a uniform user experience
- [CockroachDB](https://www.cockroachlabs.com/) is the only dependency - [CockroachDB](https://www.cockroachlabs.com/) is the only dependency
## Features ## Features
@ -73,16 +84,17 @@ You can also discover our pay-as-you-go [pricing](https://zitadel.com/pricing/v2
- Passwordless with FIDO2 support - Passwordless with FIDO2 support
- Username / Password - Username / Password
- Multifactor authentication with OTP, U2F - Multifactor authentication with OTP, U2F
- [Identity Brokering](https://docs.zitadel.com/docs/guides/authentication/identity-brokering) - [Identity Brokering](https://docs.zitadel.com/docs/guides/integrate/identity-brokering)
- [Machine-to-machine (JWT profile)](https://docs.zitadel.com/docs/guides/authentication/serviceusers) - [Machine-to-machine (JWT profile)](https://docs.zitadel.com/docs/guides/integrate/serviceusers)
- Personal Access Tokens (PAT) - Personal Access Tokens (PAT)
- Role Based Access Control (RBAC) - Role Based Access Control (RBAC)
- [Delegate role management to third-parties](https://docs.zitadel.com/docs/guides/basics/projects#what-is-a-granted-project) - [Delegate role management to third-parties](https://docs.zitadel.com/docs/guides/manage/console/projects)
- Self-registration including verification - Self-registration including verification
- User self service - User self service
- [Service Accounts](https://docs.zitadel.com/docs/guides/authentication/serviceusers) - [Service Accounts](https://docs.zitadel.com/docs/guides/integrate/serviceusers)
- [OpenID Connect certified](https://openid.net/certification/#OPs) - [OpenID Connect certified](https://openid.net/certification/#OPs)
- 🚧 [SAML 2.0](https://github.com/zitadel/zitadel/pull/3618) - 🚧 [SAML 2.0](https://github.com/zitadel/zitadel/pull/3618)
- 🚧 [Postgres](https://github.com/zitadel/zitadel/pull/3998)
## Client libraries ## Client libraries
@ -125,7 +137,7 @@ Use our login widget to allow easy and secure access to your applications and en
- works on all modern platforms, devices, and browsers - works on all modern platforms, devices, and browsers
- phishing resistant alternative - phishing resistant alternative
- requires only one gesture by the user - requires only one gesture by the user
- easy [enrollment](https://docs.zitadel.com/docs/manuals/user-factors) of the device during registration - easy [enrollment](https://docs.zitadel.com/docs/manuals/user-profile) of the device during registration
![passwordless-windows-hello](https://user-images.githubusercontent.com/1366906/118765435-5d419780-b87b-11eb-95bf-55140119c0d8.gif) ![passwordless-windows-hello](https://user-images.githubusercontent.com/1366906/118765435-5d419780-b87b-11eb-95bf-55140119c0d8.gif)
@ -142,16 +154,6 @@ Delegate the right to assign roles to another organization
Customize login and console with your design Customize login and console with your design
![private_labeling](https://user-images.githubusercontent.com/1366906/123089110-d148ff80-d426-11eb-9598-32b506f6d4fd.gif) ![private_labeling](https://user-images.githubusercontent.com/1366906/123089110-d148ff80-d426-11eb-9598-32b506f6d4fd.gif)
## Usage Data
ZITADEL components send errors and usage data to CAOS Ltd., so that we are able to identify code improvement potential. If you don't want to send this data or don't have an internet connection, pass the global flag `--disable-analytics` when using zitadelctl. For disabling ingestion for already-running components, execute the takeoff command again with the `` flag.
We try to distinguishing the environments from which events come from. As environment identifier, we enrich the events by the domain you have configured in zitadel.yml, as soon as it's available. When it's not available and you passed the --gitops flag, we defer the environment identifier from your git repository URL.
Besides from errors that don't clearly come from misconfiguration or cli misusage, we send an initial event when any binary is started. This is a "<component> invoked" event along with the flags that are passed to it, except secret values of course.
We only ingest operational data. Your ZITADEL workload data from the IAM application itself is never sent anywhere unless you chose to integrate other systems yourself.
## Security ## Security
See the policy [here](./SECURITY.md) See the policy [here](./SECURITY.md)
@ -161,3 +163,4 @@ See the policy [here](./SECURITY.md)
See the exact licensing terms [here](./LICENSE) See the exact licensing terms [here](./LICENSE)
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

View File

@ -3,10 +3,16 @@ ARG NODE_VERSION=16
####################### #######################
## With this step we prepare all node_modules, this helps caching the build ## With this step we prepare all node_modules, this helps caching the build
## Speed up this step by mounting your local node_modules directory ## Speed up this step by mounting your local node_modules directory
## We also copy and generate the source code
####################### #######################
FROM node:${NODE_VERSION} as npm-base FROM node:${NODE_VERSION} as npm-base
WORKDIR /console WORKDIR /console
# Dependencies
COPY console/package.json console/package-lock.json ./
RUN npm ci
# Sources
COPY console . COPY console .
COPY --from=zitadel-base:local /proto /proto COPY --from=zitadel-base:local /proto /proto
COPY --from=zitadel-base:local /usr/local/bin /usr/local/bin/. COPY --from=zitadel-base:local /usr/local/bin /usr/local/bin/.
@ -24,12 +30,8 @@ COPY --from=npm-base /console/src/app/proto/generated /console/src/app/proto/gen
####################### #######################
FROM npm-base as angular-build FROM npm-base as angular-build
COPY console/package.json console/package-lock.json ./
RUN npm ci
RUN npm run lint RUN npm run lint
RUN npm run prodbuild RUN npm run prodbuild
RUN ls -la /console/dist/console
####################### #######################
## Only Copy Assets ## Only Copy Assets

View File

@ -5,18 +5,17 @@ ARG GO_VERSION=1.17
## Speed up this step by mounting your local go mod pkg directory ## Speed up this step by mounting your local go mod pkg directory
####################### #######################
FROM golang:${GO_VERSION} as go-dep FROM golang:${GO_VERSION} as go-dep
RUN mkdir -p src/github.com/zitadel/zitadel
WORKDIR /go/src/github.com/zitadel/zitadel WORKDIR /go/src/github.com/zitadel/zitadel
#download modules #download modules
COPY . . COPY go.mod ./
COPY go.sum ./
RUN go mod download RUN go mod download
# install tools # install tools
COPY tools ./tools COPY tools ./tools
RUN ./tools/install.sh RUN ./tools/install.sh
####################### #######################
## generates static files ## generates static files
####################### #######################
@ -47,17 +46,20 @@ COPY internal/api/assets/generator internal/api/assets/generator
COPY internal/config internal/config COPY internal/config internal/config
COPY internal/errors internal/errors COPY internal/errors internal/errors
RUN build/zitadel/generate-grpc.sh \ RUN build/zitadel/generate-grpc.sh && \
&& go generate openapi/statik/generate.go \ go generate openapi/statik/generate.go && \
&& mkdir -p docs/apis/assets/ \ mkdir -p docs/apis/assets/ && \
&& go run internal/api/assets/generator/asset_generator.go -directory=internal/api/assets/generator/ -assets=docs/apis/assets/assets.md go run internal/api/assets/generator/asset_generator.go -directory=internal/api/assets/generator/ -assets=docs/apis/assets/assets.md
####################### #######################
## Go base build ## Go base build
####################### #######################
FROM go-stub as go-base FROM go-stub as go-base
# copy remaining zitadel files # copy remaining zitadel files
COPY . . COPY cmd cmd
COPY internal internal
COPY pkg pkg
COPY openapi openapi
####################### #######################
## copy for local dev ## copy for local dev

View File

@ -182,6 +182,9 @@ protoc \
-I=/proto/include \ -I=/proto/include \
--doc_out=${DOCS_PATH} --doc_opt=${PROTO_PATH}/docs/zitadel-md.tmpl,settings.md \ --doc_out=${DOCS_PATH} --doc_opt=${PROTO_PATH}/docs/zitadel-md.tmpl,settings.md \
${PROTO_PATH}/settings.proto ${PROTO_PATH}/settings.proto
protoc \
-I=/proto/include \
--doc_out=${DOCS_PATH} --doc_opt=${PROTO_PATH}/docs/zitadel-md.tmpl,v1.md \
${PROTO_PATH}/v1.proto
echo "done generating grpc" echo "done generating grpc"

View File

@ -3,6 +3,11 @@ Log:
Formatter: Formatter:
Format: text Format: text
# Exposes metrics on /debug/metrics
Metrics:
# Select type otel (OpenTelemetry) or none (disables collection and endpoint)
Type: otel
# Port ZITADEL will listen on # Port ZITADEL will listen on
Port: 8080 Port: 8080
# Port ZITADEL is exposed on, it can differ from port e.g. if you proxy the traffic # Port ZITADEL is exposed on, it can differ from port e.g. if you proxy the traffic
@ -40,30 +45,30 @@ HTTP1HostHeader: "host"
WebAuthNName: ZITADEL WebAuthNName: ZITADEL
Database: Database:
Host: localhost cockroach:
Port: 26257 Host: localhost
Database: zitadel Port: 26257
MaxOpenConns: 20 Database: zitadel
MaxConnLifetime: 30m MaxOpenConns: 20
MaxConnIdleTime: 30m MaxConnLifetime: 30m
Options: "" MaxConnIdleTime: 30m
User: Options: ""
Username: zitadel User:
Password: "" Username: zitadel
SSL: Password: ""
Mode: disable SSL:
RootCert: "" Mode: disable
Cert: "" RootCert: ""
Key: "" Cert: ""
Key: ""
AdminUser: Admin:
Username: root Username: root
Password: "" Password: ""
SSL: SSL:
Mode: disable Mode: disable
RootCert: "" RootCert: ""
Cert: "" Cert: ""
Key: "" Key: ""
Machine: Machine:
# Cloud hosted VMs need to specify their metadata endpoint so that the machine can be uniquely identified. # Cloud hosted VMs need to specify their metadata endpoint so that the machine can be uniquely identified.
@ -98,10 +103,20 @@ Machine:
# Url: "http://169.254.169.254/metadata/instance?api-version=2021-02-01" # Url: "http://169.254.169.254/metadata/instance?api-version=2021-02-01"
# JPath: "$.compute.vmId" # JPath: "$.compute.vmId"
# Storage for assets like user avatar, organization logo, icon, font, ...
AssetStorage:
Type: db
# HTTP cache control settings for serving assets in the assets API and login UI
# the assets will also be served with an etag and last-modified header
Cache:
MaxAge: 5s
SharedMaxAge: 168h #7d
Projections: Projections:
RequeueEvery: 10s RequeueEvery: 60s
RetryFailedAfter: 1s RetryFailedAfter: 1s
MaxFailureCount: 5 MaxFailureCount: 5
ConcurrentInstances: 10
BulkLimit: 200 BulkLimit: 200
MaxIterators: 1 MaxIterators: 1
Customizations: Customizations:
@ -112,6 +127,7 @@ Auth:
SearchLimit: 1000 SearchLimit: 1000
Spooler: Spooler:
ConcurrentWorkers: 1 ConcurrentWorkers: 1
ConcurrentInstances: 10
BulkLimit: 10000 BulkLimit: 10000
FailureCountUntilSkip: 5 FailureCountUntilSkip: 5
@ -119,6 +135,7 @@ Admin:
SearchLimit: 1000 SearchLimit: 1000
Spooler: Spooler:
ConcurrentWorkers: 1 ConcurrentWorkers: 1
ConcurrentInstances: 10
BulkLimit: 10000 BulkLimit: 10000
FailureCountUntilSkip: 5 FailureCountUntilSkip: 5
@ -169,12 +186,13 @@ Console:
SharedMaxAge: 5m SharedMaxAge: 5m
LongCache: LongCache:
MaxAge: 12h MaxAge: 12h
SharedMaxAge: 168h SharedMaxAge: 168h #7d
Notification: Notification:
Repository: Repository:
Spooler: Spooler:
ConcurrentWorkers: 1 ConcurrentWorkers: 1
ConcurrentInstances: 10
BulkLimit: 10000 BulkLimit: 10000
FailureCountUntilSkip: 5 FailureCountUntilSkip: 5
Handlers: Handlers:
@ -206,10 +224,11 @@ EncryptionKeys:
SystemAPIUsers: SystemAPIUsers:
# add keys for authentication of the systemAPI here: # add keys for authentication of the systemAPI here:
# you can specify any name for the user, but they will have to match the `issuer` and `sub` claim in the JWT:
# - superuser: # - superuser:
# Path: /path/to/superuser/key.pem # Path: /path/to/superuser/key.pem # you can provide the key either by reference with the path
# - superuser2: # - superuser2:
# Path: /path/to/superuser2/key.pem # KeyData: <base64 encoded key> # or you can directly embed it as base64 encoded value
#TODO: remove as soon as possible #TODO: remove as soon as possible
SystemDefaults: SystemDefaults:
@ -686,6 +705,17 @@ InternalAuthZ:
- "project.grant.read" - "project.grant.read"
- "project.grant.member.read" - "project.grant.member.read"
- "project.grant.user.grant.read" - "project.grant.user.grant.read"
- Role: "ORG_SETTINGS_MANAGER"
Permissions:
- "org.read"
- "org.write"
- "org.member.read"
- "org.idp.read"
- "org.idp.write"
- "org.idp.delete"
- "policy.read"
- "policy.write"
- "policy.delete"
- Role: "ORG_USER_PERMISSION_EDITOR" - Role: "ORG_USER_PERMISSION_EDITOR"
Permissions: Permissions:
- "org.read" - "org.read"

View File

@ -9,15 +9,16 @@ import (
) )
type Config struct { type Config struct {
Database database.Config Database database.Config
AdminUser database.User Machine *id.Config
Machine *id.Config Log *logging.Config
Log *logging.Config
} }
func MustNewConfig(v *viper.Viper) *Config { func MustNewConfig(v *viper.Viper) *Config {
config := new(Config) config := new(Config)
err := v.Unmarshal(config) err := v.Unmarshal(config,
viper.DecodeHook(database.DecodeHook),
)
logging.OnError(err).Fatal("unable to read config") logging.OnError(err).Fatal("unable to read config")
err = config.Log.SetLogger() err = config.Log.SetLogger()
@ -25,21 +26,3 @@ func MustNewConfig(v *viper.Viper) *Config {
return config return config
} }
func adminConfig(config *Config) database.Config {
adminConfig := config.Database
adminConfig.Username = config.AdminUser.Username
adminConfig.Password = config.AdminUser.Password
adminConfig.SSL.Cert = config.AdminUser.SSL.Cert
adminConfig.SSL.Key = config.AdminUser.SSL.Key
if config.AdminUser.SSL.RootCert != "" {
adminConfig.SSL.RootCert = config.AdminUser.SSL.RootCert
}
if config.AdminUser.SSL.Mode != "" {
adminConfig.SSL.Mode = config.AdminUser.SSL.Mode
}
//use default database because the zitadel database might not exist
adminConfig.Database = ""
return adminConfig
}

View File

@ -39,10 +39,10 @@ The user provided by flags needs privileges to
func InitAll(config *Config) { func InitAll(config *Config) {
id.Configure(config.Machine) id.Configure(config.Machine)
err := initialise(config, err := initialise(config.Database,
VerifyUser(config.Database.Username, config.Database.Password), VerifyUser(config.Database.Username(), config.Database.Password()),
VerifyDatabase(config.Database.Database), VerifyDatabase(config.Database.Database()),
VerifyGrant(config.Database.Database, config.Database.Username), VerifyGrant(config.Database.Database(), config.Database.Username()),
) )
logging.OnError(err).Fatal("unable to initialize the database") logging.OnError(err).Fatal("unable to initialize the database")
@ -50,10 +50,10 @@ func InitAll(config *Config) {
logging.OnError(err).Fatal("unable to initialize ZITADEL") logging.OnError(err).Fatal("unable to initialize ZITADEL")
} }
func initialise(config *Config, steps ...func(*sql.DB) error) error { func initialise(config database.Config, steps ...func(*sql.DB) error) error {
logging.Info("initialization started") logging.Info("initialization started")
db, err := database.Connect(adminConfig(config)) db, err := database.Connect(config, true)
if err != nil { if err != nil {
return err return err
} }

View File

@ -34,7 +34,7 @@ The user provided by flags needs priviledge to
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
config := MustNewConfig(viper.New()) config := MustNewConfig(viper.New())
err := initialise(config, VerifyDatabase(config.Database.Database)) err := initialise(config.Database, VerifyDatabase(config.Database.Database()))
logging.OnError(err).Fatal("unable to initialize the database") logging.OnError(err).Fatal("unable to initialize the database")
}, },
} }

View File

@ -28,7 +28,7 @@ Prereqesits:
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
config := MustNewConfig(viper.New()) config := MustNewConfig(viper.New())
err := initialise(config, VerifyGrant(config.Database.Database, config.Database.Username)) err := initialise(config.Database, VerifyGrant(config.Database.Database(), config.Database.Username()))
logging.OnError(err).Fatal("unable to set grant") logging.OnError(err).Fatal("unable to set grant")
}, },
} }

View File

@ -33,7 +33,7 @@ The user provided by flags needs priviledge to
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
config := MustNewConfig(viper.New()) config := MustNewConfig(viper.New())
err := initialise(config, VerifyUser(config.Database.Username, config.Database.Password)) err := initialise(config.Database, VerifyUser(config.Database.Username(), config.Database.Password()))
logging.OnError(err).Fatal("unable to init user") logging.OnError(err).Fatal("unable to init user")
}, },
} }

View File

@ -95,7 +95,7 @@ func VerifyZitadel(db *sql.DB) error {
func verifyZitadel(config database.Config) error { func verifyZitadel(config database.Config) error {
logging.WithFields("database", config.Database).Info("verify zitadel") logging.WithFields("database", config.Database).Info("verify zitadel")
db, err := database.Connect(config) db, err := database.Connect(config, false)
if err != nil { if err != nil {
return err return err
} }

View File

@ -124,7 +124,7 @@ func openFile(fileName string) (io.Reader, error) {
} }
func keyStorage(config database.Config, masterKey string) (crypto.KeyStorage, error) { func keyStorage(config database.Config, masterKey string) (crypto.KeyStorage, error) {
db, err := database.Connect(config) db, err := database.Connect(config, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -16,9 +16,8 @@ import (
"github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore"
) )
type DefaultInstance struct { type FirstInstance struct {
InstanceName string InstanceName string
CustomDomain string
DefaultLanguage language.Tag DefaultLanguage language.Tag
Org command.OrgSetup Org command.OrgSetup
@ -33,9 +32,10 @@ type DefaultInstance struct {
externalDomain string externalDomain string
externalSecure bool externalSecure bool
externalPort uint16 externalPort uint16
domain string
} }
func (mig *DefaultInstance) Execute(ctx context.Context) error { func (mig *FirstInstance) Execute(ctx context.Context) error {
keyStorage, err := crypto_db.NewKeyStorage(mig.db, mig.masterKey) keyStorage, err := crypto_db.NewKeyStorage(mig.db, mig.masterKey)
if err != nil { if err != nil {
return fmt.Errorf("cannot start key storage: %w", err) return fmt.Errorf("cannot start key storage: %w", err)
@ -77,7 +77,7 @@ func (mig *DefaultInstance) Execute(ctx context.Context) error {
} }
mig.instanceSetup.InstanceName = mig.InstanceName mig.instanceSetup.InstanceName = mig.InstanceName
mig.instanceSetup.CustomDomain = mig.CustomDomain mig.instanceSetup.CustomDomain = mig.externalDomain
mig.instanceSetup.DefaultLanguage = mig.DefaultLanguage mig.instanceSetup.DefaultLanguage = mig.DefaultLanguage
mig.instanceSetup.Org = mig.Org mig.instanceSetup.Org = mig.Org
mig.instanceSetup.Org.Human.Email.Address = strings.TrimSpace(mig.instanceSetup.Org.Human.Email.Address) mig.instanceSetup.Org.Human.Email.Address = strings.TrimSpace(mig.instanceSetup.Org.Human.Email.Address)
@ -89,7 +89,7 @@ func (mig *DefaultInstance) Execute(ctx context.Context) error {
return err return err
} }
func (mig *DefaultInstance) String() string { func (mig *FirstInstance) String() string {
return "03_default_instance" return "03_default_instance"
} }

View File

@ -36,6 +36,7 @@ func MustNewConfig(v *viper.Viper) *Config {
hook.TagToLanguageHookFunc(), hook.TagToLanguageHookFunc(),
mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToSliceHookFunc(","), mapstructure.StringToSliceHookFunc(","),
database.DecodeHook,
)), )),
) )
logging.OnError(err).Fatal("unable to read default config") logging.OnError(err).Fatal("unable to read default config")
@ -49,7 +50,7 @@ func MustNewConfig(v *viper.Viper) *Config {
type Steps struct { type Steps struct {
s1ProjectionTable *ProjectionTable s1ProjectionTable *ProjectionTable
s2AssetsTable *AssetTable s2AssetsTable *AssetTable
S3DefaultInstance *DefaultInstance FirstInstance *FirstInstance
} }
type encryptionKeyConfig struct { type encryptionKeyConfig struct {

View File

@ -0,0 +1,60 @@
package setup
import (
"context"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/config/systemdefaults"
"github.com/zitadel/zitadel/internal/eventstore"
)
type externalConfigChange struct {
es *eventstore.Eventstore
ExternalDomain string `json:"externalDomain"`
ExternalSecure bool `json:"externalSecure"`
ExternalPort uint16 `json:"externalPort"`
currentExternalDomain string
currentExternalSecure bool
currentExternalPort uint16
}
func (mig *externalConfigChange) SetLastExecution(lastRun map[string]interface{}) {
mig.currentExternalDomain, _ = lastRun["externalDomain"].(string)
externalPort, _ := lastRun["externalPort"].(float64)
mig.currentExternalPort = uint16(externalPort)
mig.currentExternalSecure, _ = lastRun["externalSecure"].(bool)
}
func (mig *externalConfigChange) Check() bool {
return mig.currentExternalSecure != mig.ExternalSecure ||
mig.currentExternalPort != mig.ExternalPort ||
mig.currentExternalDomain != mig.ExternalDomain
}
func (mig *externalConfigChange) Execute(ctx context.Context) error {
cmd, err := command.StartCommands(mig.es,
systemdefaults.SystemDefaults{},
nil,
nil,
nil,
mig.ExternalDomain,
mig.ExternalSecure,
mig.ExternalPort,
nil,
nil,
nil,
nil,
nil,
nil,
nil)
if err != nil {
return err
}
return cmd.ChangeSystemConfig(ctx, mig.currentExternalDomain, mig.currentExternalPort, mig.currentExternalSecure)
}
func (mig *externalConfigChange) String() string {
return "config_change"
}

View File

@ -54,7 +54,9 @@ func Flags(cmd *cobra.Command) {
} }
func Setup(config *Config, steps *Steps, masterKey string) { func Setup(config *Config, steps *Steps, masterKey string) {
dbClient, err := database.Connect(config.Database) logging.Info("setup started")
dbClient, err := database.Connect(config.Database, false)
logging.OnError(err).Fatal("unable to connect to database") logging.OnError(err).Fatal("unable to connect to database")
eventstoreClient, err := eventstore.Start(dbClient) eventstoreClient, err := eventstore.Start(dbClient)
@ -64,33 +66,37 @@ func Setup(config *Config, steps *Steps, masterKey string) {
steps.s1ProjectionTable = &ProjectionTable{dbClient: dbClient} steps.s1ProjectionTable = &ProjectionTable{dbClient: dbClient}
steps.s2AssetsTable = &AssetTable{dbClient: dbClient} steps.s2AssetsTable = &AssetTable{dbClient: dbClient}
steps.S3DefaultInstance.instanceSetup = config.DefaultInstance steps.FirstInstance.instanceSetup = config.DefaultInstance
steps.S3DefaultInstance.userEncryptionKey = config.EncryptionKeys.User steps.FirstInstance.userEncryptionKey = config.EncryptionKeys.User
steps.S3DefaultInstance.smtpEncryptionKey = config.EncryptionKeys.SMTP steps.FirstInstance.smtpEncryptionKey = config.EncryptionKeys.SMTP
steps.S3DefaultInstance.masterKey = masterKey steps.FirstInstance.masterKey = masterKey
steps.S3DefaultInstance.db = dbClient steps.FirstInstance.db = dbClient
steps.S3DefaultInstance.es = eventstoreClient steps.FirstInstance.es = eventstoreClient
steps.S3DefaultInstance.defaults = config.SystemDefaults steps.FirstInstance.defaults = config.SystemDefaults
steps.S3DefaultInstance.zitadelRoles = config.InternalAuthZ.RolePermissionMappings steps.FirstInstance.zitadelRoles = config.InternalAuthZ.RolePermissionMappings
steps.S3DefaultInstance.externalDomain = config.ExternalDomain steps.FirstInstance.externalDomain = config.ExternalDomain
steps.S3DefaultInstance.externalSecure = config.ExternalSecure steps.FirstInstance.externalSecure = config.ExternalSecure
steps.S3DefaultInstance.externalPort = config.ExternalPort steps.FirstInstance.externalPort = config.ExternalPort
repeatableSteps := []migration.RepeatableMigration{
&externalConfigChange{
es: eventstoreClient,
ExternalDomain: config.ExternalDomain,
ExternalPort: config.ExternalPort,
ExternalSecure: config.ExternalSecure,
},
}
ctx := context.Background() ctx := context.Background()
err = migration.Migrate(ctx, eventstoreClient, steps.s1ProjectionTable) err = migration.Migrate(ctx, eventstoreClient, steps.s1ProjectionTable)
logging.OnError(err).Fatal("unable to migrate step 1") logging.OnError(err).Fatal("unable to migrate step 1")
err = migration.Migrate(ctx, eventstoreClient, steps.s2AssetsTable) err = migration.Migrate(ctx, eventstoreClient, steps.s2AssetsTable)
logging.OnError(err).Fatal("unable to migrate step 2") logging.OnError(err).Fatal("unable to migrate step 2")
err = migration.Migrate(ctx, eventstoreClient, steps.S3DefaultInstance) err = migration.Migrate(ctx, eventstoreClient, steps.FirstInstance)
logging.OnError(err).Fatal("unable to migrate step 3") logging.OnError(err).Fatal("unable to migrate step 3")
}
func initSteps(v *viper.Viper, files ...string) func() { for _, repeatableStep := range repeatableSteps {
return func() { err = migration.Migrate(ctx, eventstoreClient, repeatableStep)
for _, file := range files { logging.OnError(err).Fatalf("unable to migrate repeatable step: %s", repeatableStep.String())
v.SetConfigFile(file)
err := v.MergeInConfig()
logging.WithFields("file", file).OnError(err).Warn("unable to read setup file")
}
} }
} }

View File

@ -1,6 +1,5 @@
S3DefaultInstance: FirstInstance:
InstanceName: Localhost InstanceName: ZITADEL
CustomDomain: localhost
DefaultLanguage: en DefaultLanguage: en
Org: Org:
Name: ZITADEL Name: ZITADEL

View File

@ -20,9 +20,9 @@ import (
"github.com/zitadel/zitadel/internal/config/systemdefaults" "github.com/zitadel/zitadel/internal/config/systemdefaults"
"github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/notification"
"github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/query/projection"
static_config "github.com/zitadel/zitadel/internal/static/config" static_config "github.com/zitadel/zitadel/internal/static/config"
metrics "github.com/zitadel/zitadel/internal/telemetry/metrics/config"
tracing "github.com/zitadel/zitadel/internal/telemetry/tracing/config" tracing "github.com/zitadel/zitadel/internal/telemetry/tracing/config"
) )
@ -38,6 +38,7 @@ type Config struct {
WebAuthNName string WebAuthNName string
Database database.Config Database database.Config
Tracing tracing.Config Tracing tracing.Config
Metrics metrics.Config
Projections projection.Config Projections projection.Config
Auth auth_es.Config Auth auth_es.Config
Admin admin_es.Config Admin admin_es.Config
@ -45,7 +46,6 @@ type Config struct {
OIDC oidc.Config OIDC oidc.Config
Login login.Config Login login.Config
Console console.Config Console console.Config
Notification notification.Config
AssetStorage static_config.AssetStorageConfig AssetStorage static_config.AssetStorageConfig
InternalAuthZ internal_authz.Config InternalAuthZ internal_authz.Config
SystemDefaults systemdefaults.SystemDefaults SystemDefaults systemdefaults.SystemDefaults
@ -65,14 +65,20 @@ func MustNewConfig(v *viper.Viper) *Config {
hook.TagToLanguageHookFunc(), hook.TagToLanguageHookFunc(),
mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToSliceHookFunc(","), mapstructure.StringToSliceHookFunc(","),
database.DecodeHook,
)), )),
) )
logging.OnError(err).Fatal("unable to read config")
err = config.Log.SetLogger() err = config.Log.SetLogger()
logging.OnError(err).Fatal("unable to set logger") logging.OnError(err).Fatal("unable to set logger")
err = config.Tracing.NewTracer() err = config.Tracing.NewTracer()
logging.OnError(err).Fatal("unable to set tracer") logging.OnError(err).Fatal("unable to set tracer")
err = config.Metrics.NewMeter()
logging.OnError(err).Fatal("unable to set meter")
return config return config
} }

View File

@ -81,7 +81,7 @@ Requirements:
func startZitadel(config *Config, masterKey string) error { func startZitadel(config *Config, masterKey string) error {
ctx := context.Background() ctx := context.Background()
dbClient, err := database.Connect(config.Database) dbClient, err := database.Connect(config.Database, false)
if err != nil { if err != nil {
return fmt.Errorf("cannot start client for projection: %w", err) return fmt.Errorf("cannot start client for projection: %w", err)
} }
@ -100,12 +100,12 @@ func startZitadel(config *Config, masterKey string) error {
return fmt.Errorf("cannot start eventstore for queries: %w", err) return fmt.Errorf("cannot start eventstore for queries: %w", err)
} }
queries, err := query.StartQueries(ctx, eventstoreClient, dbClient, config.Projections, keys.OIDC, config.InternalAuthZ.RolePermissionMappings) queries, err := query.StartQueries(ctx, eventstoreClient, dbClient, config.Projections, config.SystemDefaults, keys.IDPConfig, keys.OTP, keys.OIDC, config.InternalAuthZ.RolePermissionMappings)
if err != nil { if err != nil {
return fmt.Errorf("cannot start queries: %w", err) return fmt.Errorf("cannot start queries: %w", err)
} }
authZRepo, err := authz.Start(queries, dbClient, keys.OIDC) authZRepo, err := authz.Start(queries, dbClient, keys.OIDC, config.ExternalSecure)
if err != nil { if err != nil {
return fmt.Errorf("error starting authz repo: %w", err) return fmt.Errorf("error starting authz repo: %w", err)
} }
@ -139,7 +139,7 @@ func startZitadel(config *Config, masterKey string) error {
return fmt.Errorf("cannot start commands: %w", err) return fmt.Errorf("cannot start commands: %w", err)
} }
notification.Start(config.Notification, config.ExternalPort, config.ExternalSecure, commands, queries, dbClient, assets.HandlerPrefix, config.SystemDefaults.Notifications.FileSystemPath, keys.User, keys.SMTP, keys.SMS) notification.Start(ctx, config.Projections.Customizations["notifications"], config.ExternalPort, config.ExternalSecure, commands, queries, eventstoreClient, assets.AssetAPI(config.ExternalSecure), config.SystemDefaults.Notifications.FileSystemPath, keys.User, keys.SMTP, keys.SMS)
router := mux.NewRouter() router := mux.NewRouter()
tlsConfig, err := config.TLS.Config() tlsConfig, err := config.TLS.Config()
@ -175,10 +175,10 @@ func startAPIs(ctx context.Context, router *mux.Router, commands *command.Comman
if err != nil { if err != nil {
return fmt.Errorf("error starting admin repo: %w", err) return fmt.Errorf("error starting admin repo: %w", err)
} }
if err := apis.RegisterServer(ctx, system.CreateServer(commands, queries, adminRepo, config.Database.Database, config.DefaultInstance)); err != nil { if err := apis.RegisterServer(ctx, system.CreateServer(commands, queries, adminRepo, config.Database.Database(), config.DefaultInstance)); err != nil {
return err return err
} }
if err := apis.RegisterServer(ctx, admin.CreateServer(config.Database.Database, commands, queries, adminRepo, config.ExternalSecure, keys.User)); err != nil { if err := apis.RegisterServer(ctx, admin.CreateServer(config.Database.Database(), commands, queries, config.SystemDefaults, adminRepo, config.ExternalSecure, keys.User)); err != nil {
return err return err
} }
if err := apis.RegisterServer(ctx, management.CreateServer(commands, queries, config.SystemDefaults, keys.User, config.ExternalSecure, config.AuditLogRetention)); err != nil { if err := apis.RegisterServer(ctx, management.CreateServer(commands, queries, config.SystemDefaults, keys.User, config.ExternalSecure, config.AuditLogRetention)); err != nil {
@ -189,7 +189,8 @@ func startAPIs(ctx context.Context, router *mux.Router, commands *command.Comman
} }
instanceInterceptor := middleware.InstanceInterceptor(queries, config.HTTP1HostHeader, login.IgnoreInstanceEndpoints...) instanceInterceptor := middleware.InstanceInterceptor(queries, config.HTTP1HostHeader, login.IgnoreInstanceEndpoints...)
apis.RegisterHandler(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, instanceInterceptor.Handler)) assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge)
apis.RegisterHandler(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, instanceInterceptor.Handler, assetsCache.Handler))
userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, keys.UserAgentCookieKey, id.SonyFlakeGenerator(), config.ExternalSecure, login.EndpointResources) userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, keys.UserAgentCookieKey, id.SonyFlakeGenerator(), config.ExternalSecure, login.EndpointResources)
if err != nil { if err != nil {
@ -213,7 +214,7 @@ func startAPIs(ctx context.Context, router *mux.Router, commands *command.Comman
} }
apis.RegisterHandler(console.HandlerPrefix, c) apis.RegisterHandler(console.HandlerPrefix, c)
l, err := login.CreateLogin(config.Login, commands, queries, authRepo, store, console.HandlerPrefix+"/", op.AuthCallbackURL(oidcProvider), config.ExternalSecure, userAgentInterceptor, op.NewIssuerInterceptor(oidcProvider.IssuerFromRequest).Handler, instanceInterceptor.Handler, keys.User, keys.IDPConfig, keys.CSRFCookieKey) l, err := login.CreateLogin(config.Login, commands, queries, authRepo, store, console.HandlerPrefix+"/", op.AuthCallbackURL(oidcProvider), config.ExternalSecure, userAgentInterceptor, op.NewIssuerInterceptor(oidcProvider.IssuerFromRequest).Handler, instanceInterceptor.Handler, assetsCache.Handler, keys.User, keys.IDPConfig, keys.CSRFCookieKey)
if err != nil { if err != nil {
return fmt.Errorf("unable to start login: %w", err) return fmt.Errorf("unable to start login: %w", err)
} }

View File

@ -22,9 +22,17 @@
"main": "src/main.ts", "main": "src/main.ts",
"polyfills": "src/polyfills.ts", "polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json", "tsConfig": "tsconfig.app.json",
"assets": ["src/favicon.ico", "src/assets", "src/manifest.webmanifest"], "assets": [
"styles": ["src/styles.scss"], "src/favicon.ico",
"scripts": ["./node_modules/tinycolor2/dist/tinycolor-min.js"], "src/assets",
"src/manifest.webmanifest"
],
"styles": [
"src/styles.scss"
],
"scripts": [
"./node_modules/tinycolor2/dist/tinycolor-min.js"
],
"allowedCommonJsDependencies": [ "allowedCommonJsDependencies": [
"@angular/common/locales/de", "@angular/common/locales/de",
"codemirror/mode/javascript/javascript", "codemirror/mode/javascript/javascript",
@ -122,15 +130,27 @@
"polyfills": "src/polyfills.ts", "polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json", "tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js", "karmaConfig": "karma.conf.js",
"assets": ["src/favicon.ico", "src/assets", "src/manifest.webmanifest"], "assets": [
"styles": ["./node_modules/@angular/material/prebuilt-themes/pink-bluegrey.css", "src/styles.scss"], "src/favicon.ico",
"scripts": ["./node_modules/tinycolor2/dist/tinycolor-min.js"] "src/assets",
"src/manifest.webmanifest"
],
"styles": [
"./node_modules/@angular/material/prebuilt-themes/pink-bluegrey.css",
"src/styles.scss"
],
"scripts": [
"./node_modules/tinycolor2/dist/tinycolor-min.js"
]
} }
}, },
"lint": { "lint": {
"builder": "@angular-eslint/builder:lint", "builder": "@angular-eslint/builder:lint",
"options": { "options": {
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"] "lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
} }
}, },
"e2e": { "e2e": {

View File

@ -1,14 +0,0 @@
{
"supportFile": "./cypress/support/index.ts",
"reporter": "mochawesome",
"reporterOptions": {
"reportDir": "cypress/results",
"overwrite": false,
"html": true,
"json": true
},
"chromeWebSecurity": false,
"experimentalSessionSupport": true,
"trashAssetsBeforeRuns": false
}

View File

@ -1,21 +0,0 @@
#!/usr/bin/env bash
ACTION=$1
ENVFILE=$2
shift
shift
projectRoot=".."
set -a; source $ENVFILE; set +a
NPX=""
if ! command -v cypress &> /dev/null; then
NPX="npx"
fi
$NPX cypress $ACTION \
--port ${E2E_CYPRESS_PORT} \
--env org="${E2E_ORG}",org_owner_password="${E2E_ORG_OWNER_PW}",org_owner_viewer_password="${E2E_ORG_OWNER_VIEWER_PW}",org_project_creator_password="${E2E_ORG_PROJECT_CREATOR_PW}",login_policy_user_password="${E2E_LOGIN_POLICY_USER_PW}",password_complexity_user_password="${E2E_PASSWORD_COMPLEXITY_USER_PW}",consoleUrl=${E2E_CONSOLE_URL},apiUrl="${E2E_API_URL}",accountsUrl="${E2E_ACCOUNTS_URL}",issuerUrl="${E2E_ISSUER_URL}",serviceAccountKey="${E2E_SERVICEACCOUNT_KEY}",serviceAccountKeyPath="${E2E_SERVICEACCOUNT_KEY_PATH}",otherZitadelIdpInstance="${E2E_OTHER_ZITADEL_IDP_INSTANCE}",zitadelProjectResourceId="${E2E_ZITADEL_PROJECT_RESOURCE_ID}" \
"$@"

View File

@ -1,50 +0,0 @@
import { login, User } from "../../support/login/users";
import { Apps, ensureProjectExists, ensureProjectResourceDoesntExist } from "../../support/api/projects";
import { apiAuth } from "../../support/api/apiauth";
describe('applications', () => {
const testProjectName = 'e2eprojectapplication'
const testAppName = 'e2eappundertest'
;[User.OrgOwner].forEach(user => {
describe(`as user "${user}"`, () => {
beforeEach(`ensure it doesn't exist already`, () => {
login(user)
apiAuth().then(api => {
ensureProjectExists(api, testProjectName).then(projectID => {
ensureProjectResourceDoesntExist(api, projectID, Apps, testAppName).then(() => {
cy.visit(`${Cypress.env('consoleUrl')}/projects/${projectID}`)
})
})
})
})
it('add app', () => {
cy.get('mat-spinner')
cy.get('mat-spinner').should('not.exist')
cy.get('[data-e2e="app-card-add"]').should('be.visible').click()
// select webapp
cy.get('[formcontrolname="name"]').type(testAppName)
cy.get('[for="WEB"]').click()
cy.get('[data-e2e="continue-button-nameandtype"]').click()
//select authentication
cy.get('[for="PKCE"]').click()
cy.get('[data-e2e="continue-button-authmethod"]').click()
//enter URL
cy.get('cnsl-redirect-uris').eq(0).type("https://testurl.org")
cy.get('cnsl-redirect-uris').eq(1).type("https://testlogouturl.org")
cy.get('[data-e2e="continue-button-redirecturis"]').click()
cy.get('[data-e2e="create-button"]').click().then(() => {
cy.get('[id*=overlay]').should('exist')
})
cy.get('.data-e2e-success')
cy.wait(200)
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist')
//TODO: check client ID/Secret
})
})
})
})

View File

@ -1,83 +0,0 @@
import { apiAuth } from '../../support/api/apiauth';
import { ensureHumanUserExists, ensureUserDoesntExist } from '../../support/api/users';
import { login, User, username } from '../../support/login/users';
describe('humans', () => {
const humansPath = `${Cypress.env('consoleUrl')}/users?type=human`;
const testHumanUserNameAdd = 'e2ehumanusernameadd';
const testHumanUserNameRemove = 'e2ehumanusernameremove';
[User.OrgOwner].forEach((user) => {
describe(`as user "${user}"`, () => {
beforeEach(() => {
login(user);
cy.visit(humansPath);
cy.get('[data-cy=timestamp]');
});
describe('add', () => {
before(`ensure it doesn't exist already`, () => {
apiAuth().then((apiCallProperties) => {
ensureUserDoesntExist(apiCallProperties, testHumanUserNameAdd);
});
});
it('should add a user', () => {
cy.get('a[href="/users/create"]').click();
cy.url().should('contain', 'users/create');
cy.get('[formcontrolname="email"]').type(username('e2ehuman'));
//force needed due to the prefilled username prefix
cy.get('[formcontrolname="userName"]').type(testHumanUserNameAdd, { force: true });
cy.get('[formcontrolname="firstName"]').type('e2ehumanfirstname');
cy.get('[formcontrolname="lastName"]').type('e2ehumanlastname');
cy.get('[formcontrolname="phone"]').type('+41 123456789');
cy.get('[data-e2e="create-button"]').click();
cy.get('.data-e2e-success');
cy.wait(200);
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
});
});
describe('remove', () => {
before('ensure it exists', () => {
apiAuth().then((api) => {
ensureHumanUserExists(api, testHumanUserNameRemove);
});
});
it('should delete a human user', () => {
cy.contains('tr', testHumanUserNameRemove, { timeout: 1000 })
.find('button')
//force due to angular hidden buttons
.click({ force: true });
cy.get('[e2e-data="confirm-dialog-input"]').type(username(testHumanUserNameRemove, Cypress.env('org')));
cy.get('[e2e-data="confirm-dialog-button"]').click();
cy.get('.data-e2e-success');
cy.wait(200);
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
});
});
});
});
});
/*
describe("users", ()=> {
before(()=> {
cy.consolelogin(Cypress.env('username'), Cypress.env('password'), Cypress.env('consoleUrl'))
})
it('should show personal information', () => {
cy.log(`USER: show personal information`);
//click on user information
cy.get('a[href*="users/me"').eq(0).click()
cy.url().should('contain', '/users/me')
})
it('should show users', () => {
cy.visit(Cypress.env('consoleUrl') + '/users/list/humans')
cy.url().should('contain', 'users/list/humans')
})
})
*/

View File

@ -1,60 +0,0 @@
import { apiAuth } from '../../support/api/apiauth';
import { ensureMachineUserExists, ensureUserDoesntExist } from '../../support/api/users';
import { login, User, username } from '../../support/login/users';
describe('machines', () => {
const machinesPath = `${Cypress.env('consoleUrl')}/users?type=machine`;
const testMachineUserNameAdd = 'e2emachineusernameadd';
const testMachineUserNameRemove = 'e2emachineusernameremove';
[User.OrgOwner].forEach((user) => {
describe(`as user "${user}"`, () => {
beforeEach(() => {
login(user);
cy.visit(machinesPath);
cy.get('[data-cy=timestamp]');
});
describe('add', () => {
before(`ensure it doesn't exist already`, () => {
apiAuth().then((apiCallProperties) => {
ensureUserDoesntExist(apiCallProperties, testMachineUserNameAdd);
});
});
it('should add a machine', () => {
cy.get('a[href="/users/create-machine"]').click();
cy.url().should('contain', 'users/create-machine');
//force needed due to the prefilled username prefix
cy.get('[formcontrolname="userName"]').type(testMachineUserNameAdd, { force: true });
cy.get('[formcontrolname="name"]').type('e2emachinename');
cy.get('[formcontrolname="description"]').type('e2emachinedescription');
cy.get('[data-e2e="create-button"]').click();
cy.get('.data-e2e-success');
cy.wait(200);
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
});
});
describe('remove', () => {
before('ensure it exists', () => {
apiAuth().then((api) => {
ensureMachineUserExists(api, testMachineUserNameRemove);
});
});
it('should delete a machine', () => {
cy.contains('tr', testMachineUserNameRemove, { timeout: 1000 })
.find('button')
//force due to angular hidden buttons
.click({ force: true });
cy.get('[e2e-data="confirm-dialog-input"]').type(username(testMachineUserNameRemove, Cypress.env('org')));
cy.get('[e2e-data="confirm-dialog-button"]').click();
cy.get('.data-e2e-success');
cy.wait(200);
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
});
});
});
});
});

View File

@ -1,78 +0,0 @@
import { apiAuth } from '../../support/api/apiauth';
import { ensureProjectDoesntExist, ensureProjectExists } from '../../support/api/projects';
import { login, User } from '../../support/login/users';
describe('projects', () => {
const testProjectNameCreate = 'e2eprojectcreate';
const testProjectNameDeleteList = 'e2eprojectdeletelist';
const testProjectNameDeleteGrid = 'e2eprojectdeletegrid';
[User.OrgOwner].forEach((user) => {
describe(`as user "${user}"`, () => {
beforeEach(() => {
login(user);
});
describe('add project', () => {
beforeEach(`ensure it doesn't exist already`, () => {
apiAuth().then((api) => {
ensureProjectDoesntExist(api, testProjectNameCreate);
});
cy.visit(`${Cypress.env('consoleUrl')}/projects`);
});
it('should add a project', () => {
cy.get('.add-project-button').click({ force: true });
cy.get('input').type(testProjectNameCreate);
cy.get('[data-e2e="continue-button"]').click();
cy.get('.data-e2e-success');
cy.wait(200);
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
});
});
describe('remove project', () => {
describe('list view', () => {
beforeEach('ensure it exists', () => {
apiAuth().then((api) => {
ensureProjectExists(api, testProjectNameDeleteList);
});
cy.visit(`${Cypress.env('consoleUrl')}/projects`);
});
it('removes the project', () => {
cy.get('[data-e2e=toggle-grid]').click();
cy.get('[data-cy=timestamp]');
cy.contains('tr', testProjectNameDeleteList, { timeout: 1000 })
.find('[data-e2e=delete-project-button]')
.click({ force: true });
cy.get('[e2e-data="confirm-dialog-button"]').click();
cy.get('.data-e2e-success');
cy.wait(200);
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
});
});
describe('grid view', () => {
beforeEach('ensure it exists', () => {
apiAuth().then((api) => {
ensureProjectExists(api, testProjectNameDeleteGrid);
});
cy.visit(`${Cypress.env('consoleUrl')}/projects`);
});
it('removes the project', () => {
cy.contains('[data-e2e=grid-card]', testProjectNameDeleteGrid)
.find('[data-e2e=delete-project-button]')
.trigger('mouseover')
.click();
cy.get('[e2e-data="confirm-dialog-button"]').click();
cy.get('.data-e2e-success');
cy.wait(200);
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
});
});
});
});
});
});

View File

@ -1,85 +0,0 @@
import { apiAuth, apiCallProperties } from "../../support/api/apiauth";
import { Policy, resetPolicy } from "../../support/api/policies";
import { login, User } from "../../support/login/users";
describe("private labeling", ()=> {
const orgPath = `${Cypress.env('consoleUrl')}/org`
;[User.OrgOwner].forEach(user => {
describe(`as user "${user}"`, () => {
let api: apiCallProperties
beforeEach(()=> {
login(user)
cy.visit(orgPath)
// TODO: Why force?
cy.contains('[data-e2e=policy-card]', 'Private Labeling').contains('button', 'Modify').click({force: true}) // TODO: select data-e2e
})
customize('white', user)
customize('dark', user)
})
})
})
function customize(theme: string, user: User) {
describe(`${theme} theme`, () => {
beforeEach(() => {
apiAuth().then(api => {
resetPolicy(api, Policy.Label)
})
})
describe.skip('logo', () => {
beforeEach('expand logo category', () => {
cy.contains('[data-e2e=policy-category]', 'Logo').click() // TODO: select data-e2e
cy.fixture('logo.png').as('logo')
})
it('should update a logo', () => {
cy.get('[data-e2e=image-part-logo]').find('input').then(function (el) {
const blob = Cypress.Blob.base64StringToBlob(this.logo, 'image/png')
const file = new File([blob], 'images/logo.png', { type: 'image/png' })
const list = new DataTransfer()
list.items.add(file)
const myFileList = list.files
el[0].files = myFileList
el[0].dispatchEvent(new Event('change', { bubbles: true }))
})
})
it('should delete a logo')
})
it('should update an icon')
it('should delete an icon')
it.skip('should update the background color', () => {
cy.contains('[data-e2e=color]', 'Background Color').find('button').click() // TODO: select data-e2e
cy.get('color-editable-input').find('input').clear().type('#ae44dc')
cy.get('[data-e2e=save-colors-button]').click()
cy.get('[data-e2e=header-user-avatar]').click()
cy.contains('Logout All Users').click() // TODO: select data-e2e
login(User.LoginPolicyUser, true, null, () => {
cy.pause()
})
})
it('should update the primary color')
it('should update the warning color')
it('should update the font color')
it('should update the font style')
it('should hide the loginname suffix')
it('should show the loginname suffix')
it('should hide the watermark')
it('should show the watermark')
it('should show the current configuration')
it('should reset the policy')
})
}

View File

@ -1,15 +0,0 @@
import { readFileSync } from "fs";
module.exports = (on, config) => {
require('cypress-terminal-report/src/installLogsPrinter')(on);
config.defaultCommandTimeout = 10_000
config.env.parsedServiceAccountKey = config.env.serviceAccountKey
if (config.env.serviceAccountKeyPath) {
config.env.parsedServiceAccountKey = JSON.parse(readFileSync(config.env.serviceAccountKeyPath, 'utf-8'))
}
return config
}

View File

@ -1,49 +0,0 @@
import { sign } from 'jsonwebtoken'
export interface apiCallProperties {
authHeader: string
mgntBaseURL: string
}
export function apiAuth(): Cypress.Chainable<apiCallProperties> {
const apiUrl = Cypress.env('apiUrl')
const issuerUrl = Cypress.env('issuerUrl')
const zitadelProjectResourceID = (<string>Cypress.env('zitadelProjectResourceId')).replace('bignumber-', '')
const key = Cypress.env("parsedServiceAccountKey")
const now = new Date().getTime()
const iat = Math.floor(now / 1000)
const exp = Math.floor(new Date(now + 1000 * 60 * 55).getTime() / 1000) // 55 minutes
const bearerToken = sign({
iss: key.userId,
sub: key.userId,
aud: `${issuerUrl}`,
iat: iat,
exp: exp
}, key.key, {
header: {
alg: "RS256",
kid: key.keyId
}
})
return cy.request({
method: 'POST',
url: `${apiUrl}/oauth/v2/token`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: {
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
scope: `openid urn:zitadel:iam:org:project:id:${zitadelProjectResourceID}:aud`,
assertion: bearerToken,
}
}).its('body.access_token').then(token => {
return <apiCallProperties>{
authHeader: `Bearer ${token}`,
mgntBaseURL: `${apiUrl}/management/v1/`,
}
})
}

View File

@ -1,164 +0,0 @@
export enum User {
OrgOwner = 'org_owner',
OrgOwnerViewer = 'org_owner_viewer',
OrgProjectCreator = 'org_project_creator',
LoginPolicyUser = 'login_policy_user',
PasswordComplexityUser = 'password_complexity_user',
IAMAdminUser = "zitadel-admin"
}
export function login(user:User, force?: boolean, pw?: string, onUsernameScreen?: () => void, onPasswordScreen?: () => void, onAuthenticated?: () => void): void {
let creds = credentials(user, pw)
const accountsUrl: string = Cypress.env('accountsUrl')
const consoleUrl: string = Cypress.env('consoleUrl')
const otherZitadelIdpInstance: boolean = Cypress.env('otherZitadelIdpInstance')
cy.session(creds.username, () => {
const cookies = new Map<string, string>()
if (otherZitadelIdpInstance) {
cy.intercept({
method: 'GET',
url: `${accountsUrl}/login*`,
times: 1
}, (req) => {
req.headers['cookie'] = requestCookies(cookies)
req.continue((res) => {
updateCookies(res.headers['set-cookie'] as string[], cookies)
})
}).as('login')
cy.intercept({
method: 'POST',
url: `${accountsUrl}/loginname*`,
times: 1
}, (req) => {
req.headers['cookie'] = requestCookies(cookies)
req.continue((res) => {
updateCookies(res.headers['set-cookie'] as string[], cookies)
})
}).as('loginName')
cy.intercept({
method: 'POST',
url: `${accountsUrl}/password*`,
times: 1
}, (req) => {
req.headers['cookie'] = requestCookies(cookies)
req.continue((res) => {
updateCookies(res.headers['set-cookie'] as string[], cookies)
})
}).as('password')
cy.intercept({
method: 'GET',
url: `${accountsUrl}/success*`,
times: 1
}, (req) => {
req.headers['cookie'] = requestCookies(cookies)
req.continue((res) => {
updateCookies(res.headers['set-cookie'] as string[], cookies)
})
}).as('success')
cy.intercept({
method: 'GET',
url: `${accountsUrl}/oauth/v2/authorize/callback*`,
times: 1
}, (req) => {
req.headers['cookie'] = requestCookies(cookies)
req.continue((res) => {
updateCookies(res.headers['set-cookie'] as string[], cookies)
})
}).as('callback')
cy.intercept({
method: 'GET',
url: `${accountsUrl}/oauth/v2/authorize*`,
times: 1,
}, (req) => {
req.continue((res) => {
updateCookies(res.headers['set-cookie'] as string[], cookies)
})
})
}
cy.visit(`${consoleUrl}/loginname`, { retryOnNetworkFailure: true });
otherZitadelIdpInstance && cy.wait('@login')
onUsernameScreen ? onUsernameScreen() : null
cy.get('#loginName').type(creds.username)
cy.get('#submit-button').click()
otherZitadelIdpInstance && cy.wait('@loginName')
onPasswordScreen ? onPasswordScreen() : null
cy.get('#password').type(creds.password)
cy.get('#submit-button').click()
onAuthenticated ? onAuthenticated() : null
otherZitadelIdpInstance && cy.wait('@callback')
cy.location('pathname', {timeout: 5 * 1000}).should('eq', '/');
}, {
validate: () => {
if (force) {
throw new Error("clear session");
}
cy.visit(`${consoleUrl}/users/me`)
}
})
}
export function username(withoutDomain: string, project?: string): string {
return `${withoutDomain}@${project ? `${project}.` : ''}${host(Cypress.env('apiUrl')).replace('api.', '')}`
}
function credentials(user: User, pw?: string) {
const isAdmin = user == User.IAMAdminUser
return {
username: username(isAdmin ? user : `${user}_user_name`, isAdmin ? 'caos-ag' : Cypress.env('org')),
password: pw ? pw : Cypress.env(`${user}_password`)
}
}
function updateCookies(newCookies: string[] | undefined, currentCookies: Map<string, string>) {
if (newCookies === undefined) {
return
}
newCookies.forEach(cs => {
cs.split('; ').forEach(cookie => {
const idx = cookie.indexOf('=')
currentCookies.set(cookie.substring(0,idx), cookie.substring(idx+1))
})
})
}
function requestCookies(currentCookies: Map<string, string>): string[] {
let list: Array<string> = []
currentCookies.forEach((val, key) => {
list.push(key+"="+val)
})
return list
}
export function host(url: string): string {
return stripPort(stripProtocol(url))
}
function stripPort(s: string): string {
const idx = s.indexOf(":")
return idx === -1 ? s : s.substring(0,idx)
}
function stripProtocol(url: string): string {
return url.replace('http://', '').replace('https://', '')
}

View File

@ -1,8 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress"]
},
"include": ["**/*.ts"]
}

View File

@ -1,14 +0,0 @@
E2E_CYPRESS_PORT=5003
E2E_ORG=e2e-tests
E2E_ORG_OWNER_PW=Password1!
E2E_ORG_OWNER_VIEWER_PW=Password1!
E2E_ORG_PROJECT_CREATOR_PW=Password1!
E2E_PASSWORD_COMPLEXITY_USER_PW=Password1!
E2E_LOGIN_POLICY_USER_PW=Password1!
E2E_SERVICEACCOUNT_KEY_PATH="${projectRoot}/.keys/e2e.json"
E2E_CONSOLE_URL="http://localhost:4200"
E2E_API_URL="http://localhost:50002"
E2E_ACCOUNTS_URL="http://localhost:50003"
E2E_ISSUER_URL="http://localhost:50002/oauth/v2"
E2E_OTHER_ZITADEL_IDP_INSTANCE=false
E2E_ZITADEL_PROJECT_RESOURCE_ID="bignumber-$(echo -n $(docker compose -f ${projectRoot}/build/local/docker-compose-local.yml exec --no-TTY db cockroach sql --insecure --execute "select aggregate_id from eventstore.events where event_type = 'project.added' and event_data = '{\"name\": \"Zitadel\"}';" --format tsv) | cut -d " " -f 2)"

12957
console/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,24 +6,22 @@
"start": "ng serve", "start": "ng serve",
"build": "ng build", "build": "ng build",
"prodbuild": "ng build --configuration production --base-href=/ui/console/", "prodbuild": "ng build --configuration production --base-href=/ui/console/",
"lint": "ng lint && stylelint './src/**/*.scss' --syntax scss", "lint": "ng lint && stylelint './src/**/*.scss' --syntax scss"
"e2e": "./cypress.sh run e2e.env",
"e2e:open": "./cypress.sh open e2e.env"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^14.0.1", "@angular/animations": "^14.0.4",
"@angular/cdk": "^14.0.1", "@angular/cdk": "^14.0.4",
"@angular/common": "^14.0.1", "@angular/common": "^14.0.4",
"@angular/compiler": "^14.0.1", "@angular/compiler": "^14.0.4",
"@angular/core": "^14.0.1", "@angular/core": "^14.0.4",
"@angular/forms": "^14.0.1", "@angular/forms": "^14.0.4",
"@angular/material": "^14.0.1", "@angular/material": "^14.0.4",
"@angular/material-moment-adapter": "^14.0.1", "@angular/material-moment-adapter": "^14.0.4",
"@angular/platform-browser": "^14.0.1", "@angular/platform-browser": "^14.0.4",
"@angular/platform-browser-dynamic": "^14.0.1", "@angular/platform-browser-dynamic": "^14.0.4",
"@angular/router": "^14.0.1", "@angular/router": "^14.0.4",
"@angular/service-worker": "^14.0.1", "@angular/service-worker": "^14.0.4",
"@ctrl/ngx-codemirror": "^5.1.1", "@ctrl/ngx-codemirror": "^5.1.1",
"@grpc/grpc-js": "^1.5.7", "@grpc/grpc-js": "^1.5.7",
"@ngx-translate/core": "^14.0.0", "@ngx-translate/core": "^14.0.0",
@ -36,7 +34,7 @@
"codemirror": "^5.65.0", "codemirror": "^5.65.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"google-proto-files": "^2.5.0", "google-proto-files": "^3.0.0",
"google-protobuf": "^3.19.4", "google-protobuf": "^3.19.4",
"grpc-web": "^1.3.0", "grpc-web": "^1.3.0",
"libphonenumber-js": "^1.10.6", "libphonenumber-js": "^1.10.6",
@ -52,34 +50,30 @@
"zone.js": "~0.11.4" "zone.js": "~0.11.4"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^14.0.1", "@angular-devkit/build-angular": "^14.0.4",
"@angular-eslint/builder": "^14.0.0-alpha.3", "@angular-eslint/builder": "^14.0.0",
"@angular-eslint/eslint-plugin": "^14.0.0-alpha.3", "@angular-eslint/eslint-plugin": "^14.0.0",
"@angular-eslint/eslint-plugin-template": "^14.0.0-alpha.3", "@angular-eslint/eslint-plugin-template": "^14.0.0",
"@angular-eslint/schematics": "^14.0.0-alpha.3", "@angular-eslint/schematics": "^14.0.0",
"@angular-eslint/template-parser": "^14.0.0-alpha.3", "@angular-eslint/template-parser": "^14.0.0",
"@angular/cli": "^14.0.1", "@angular/cli": "^14.0.4",
"@angular/compiler-cli": "^14.0.1", "@angular/compiler-cli": "^14.0.4",
"@angular/language-service": "^14.0.1", "@angular/language-service": "^14.0.4",
"@types/jasmine": "~4.0.3", "@types/jasmine": "~4.0.3",
"@types/jasminewd2": "~2.0.10", "@types/jasminewd2": "~2.0.10",
"@types/jsonwebtoken": "^8.5.5", "@types/jsonwebtoken": "^8.5.5",
"@types/node": "^17.0.42", "@types/node": "^17.0.42",
"@typescript-eslint/eslint-plugin": "5.25.0", "@typescript-eslint/eslint-plugin": "5.30.4",
"@typescript-eslint/parser": "5.27.0", "@typescript-eslint/parser": "5.30.4",
"codelyzer": "^6.0.0", "codelyzer": "^6.0.0",
"cypress": "^10.1.0", "eslint": "^8.18.0",
"cypress-terminal-report": "^4.0.1", "jasmine-core": "~4.2.0",
"eslint": "^8.17.0",
"jasmine-core": "~4.1.1",
"jasmine-spec-reporter": "~7.0.0", "jasmine-spec-reporter": "~7.0.0",
"jsonwebtoken": "^8.5.1", "karma": "~6.4.0",
"karma": "~6.3.16",
"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": "~5.0.1", "karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "^2.0.0", "karma-jasmine-html-reporter": "^2.0.0",
"mochawesome": "^7.1.2",
"prettier": "^2.4.1", "prettier": "^2.4.1",
"protractor": "~7.0.0", "protractor": "~7.0.0",
"stylelint": "^13.10.0", "stylelint": "^13.10.0",

View File

@ -13,6 +13,10 @@ const routes: Routes = [
loadChildren: () => import('./pages/home/home.module').then((m) => m.HomeModule), loadChildren: () => import('./pages/home/home.module').then((m) => m.HomeModule),
canActivate: [AuthGuard], canActivate: [AuthGuard],
}, },
{
path: 'signedout',
loadChildren: () => import('./pages/signedout/signedout.module').then((m) => m.SignedoutModule),
},
{ {
path: 'orgs', path: 'orgs',
loadChildren: () => import('./pages/org-list/org-list.module').then((m) => m.OrgListModule), loadChildren: () => import('./pages/org-list/org-list.module').then((m) => m.OrgListModule),
@ -38,12 +42,7 @@ const routes: Routes = [
{ {
path: 'users', path: 'users',
canActivate: [AuthGuard], canActivate: [AuthGuard],
children: [ loadChildren: () => import('src/app/pages/users/users.module').then((m) => m.UsersModule),
{
path: '',
loadChildren: () => import('src/app/pages/users/users.module').then((m) => m.UsersModule),
},
],
}, },
{ {
path: 'instance', path: 'instance',
@ -170,10 +169,6 @@ const routes: Routes = [
roles: ['policy.read'], roles: ['policy.read'],
}, },
}, },
{
path: 'signedout',
loadChildren: () => import('./pages/signedout/signedout.module').then((m) => m.SignedoutModule),
},
{ {
path: '**', path: '**',
redirectTo: '/', redirectTo: '/',

View File

@ -1,32 +1,31 @@
<ng-container *ngIf="(authService.user | async) || undefined as user"> <div class="main-container">
<ng-container *ngIf="['iam.read$', 'iam.write$'] | hasRole as iamuser$"> <ng-container *ngIf="(authService.user | async) || {} as user">
<div class="main-container"> <cnsl-header
<cnsl-header *ngIf="user && user !== {}"
*ngIf="user" [org]="org"
[org]="org" [user]="$any(user)"
[user]="user" [isDarkTheme]="componentCssClass === 'dark-theme'"
[isDarkTheme]="componentCssClass === 'dark-theme'" [labelpolicy]="labelpolicy"
[labelpolicy]="labelpolicy" (changedActiveOrg)="changedOrg($event)"
(changedActiveOrg)="changedOrg($event)" ></cnsl-header>
></cnsl-header>
<cnsl-nav <cnsl-nav
id="mainnav" id="mainnav"
class="nav" class="nav"
[ngClass]="{ shadow: yoffset > 60 }" [ngClass]="{ shadow: yoffset > 60 }"
*ngIf="user" *ngIf="user && user !== {}"
[org]="org" [org]="org"
[user]="user" [user]="$any(user)"
[isDarkTheme]="componentCssClass === 'dark-theme'" [isDarkTheme]="componentCssClass === 'dark-theme'"
[labelpolicy]="labelpolicy" [labelpolicy]="labelpolicy"
></cnsl-nav> ></cnsl-nav>
<div class="router-container" [@routeAnimations]="prepareRoute(outlet)">
<div class="outlet">
<router-outlet class="outlet" #outlet="outlet"></router-outlet>
</div>
</div>
<span class="fill-space"></span>
<cnsl-footer [privateLabelPolicy]="labelpolicy"></cnsl-footer>
</div>
</ng-container> </ng-container>
</ng-container>
<div class="router-container" [@routeAnimations]="prepareRoute(outlet)">
<div class="outlet">
<router-outlet class="outlet" #outlet="outlet"></router-outlet>
</div>
</div>
<span class="fill-space"></span>
<cnsl-footer [privateLabelPolicy]="labelpolicy"></cnsl-footer>
</div>

View File

@ -69,6 +69,7 @@ export class AppComponent implements OnDestroy {
private activatedRoute: ActivatedRoute, private activatedRoute: ActivatedRoute,
@Inject(DOCUMENT) private document: Document, @Inject(DOCUMENT) private document: Document,
) { ) {
this.themeService.loadPrivateLabelling(true);
console.log( console.log(
'%cWait!', '%cWait!',
'text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black; color: #5469D4; font-size: 50px', 'text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black; color: #5469D4; font-size: 50px',

View File

@ -15,6 +15,9 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { AuthConfig, OAuthModule, OAuthStorage } from 'angular-oauth2-oidc'; import { AuthConfig, OAuthModule, OAuthStorage } from 'angular-oauth2-oidc';
import { QuicklinkModule } from 'ngx-quicklink'; import { QuicklinkModule } from 'ngx-quicklink';
import { from, Observable } from 'rxjs'; import { from, Observable } from 'rxjs';
import { AuthGuard } from 'src/app/guards/auth.guard';
import { RoleGuard } from 'src/app/guards/role.guard';
import { UserGuard } from 'src/app/guards/user.guard';
import { InfoOverlayModule } from 'src/app/modules/info-overlay/info-overlay.module'; import { InfoOverlayModule } from 'src/app/modules/info-overlay/info-overlay.module';
import { AssetService } from 'src/app/services/asset.service'; import { AssetService } from 'src/app/services/asset.service';
@ -26,7 +29,6 @@ import { HeaderModule } from './modules/header/header.module';
import { KeyboardShortcutsModule } from './modules/keyboard-shortcuts/keyboard-shortcuts.module'; import { KeyboardShortcutsModule } from './modules/keyboard-shortcuts/keyboard-shortcuts.module';
import { NavModule } from './modules/nav/nav.module'; import { NavModule } from './modules/nav/nav.module';
import { WarnDialogModule } from './modules/warn-dialog/warn-dialog.module'; import { WarnDialogModule } from './modules/warn-dialog/warn-dialog.module';
import { SignedoutComponent } from './pages/signedout/signedout.component';
import { HasRolePipeModule } from './pipes/has-role-pipe/has-role-pipe.module'; import { HasRolePipeModule } from './pipes/has-role-pipe/has-role-pipe.module';
import { AdminService } from './services/admin.service'; import { AdminService } from './services/admin.service';
import { AuthenticationService } from './services/authentication.service'; import { AuthenticationService } from './services/authentication.service';
@ -79,23 +81,13 @@ const authConfig: AuthConfig = {
}; };
@NgModule({ @NgModule({
declarations: [AppComponent, SignedoutComponent], declarations: [AppComponent],
imports: [ imports: [
AppRoutingModule, AppRoutingModule,
CommonModule, CommonModule,
BrowserModule, BrowserModule,
HeaderModule, HeaderModule,
OAuthModule.forRoot({ OAuthModule.forRoot(),
resourceServer: {
allowedUrls: [
'https://test.api.zitadel.caos.ch/caos.zitadel.auth.api.v1.AuthService',
'https://test.api.zitadel.caos.ch/oauth/v2/userinfo',
'https://test.api.zitadel.caos.ch/caos.zitadel.management.api.v1.ManagementService/',
'https://preview.api.zitadel.caos.ch',
],
sendAccessToken: true,
},
}),
TranslateModule.forRoot({ TranslateModule.forRoot({
loader: { loader: {
provide: TranslateLoader, provide: TranslateLoader,
@ -121,6 +113,9 @@ const authConfig: AuthConfig = {
ServiceWorkerModule.register('ngsw-worker.js', { enabled: false }), ServiceWorkerModule.register('ngsw-worker.js', { enabled: false }),
], ],
providers: [ providers: [
AuthGuard,
RoleGuard,
UserGuard,
ThemeService, ThemeService,
{ {
provide: APP_INITIALIZER, provide: APP_INITIALIZER,

View File

@ -1,25 +1,34 @@
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'; import { Directive, Input, OnDestroy, TemplateRef, ViewContainerRef } from '@angular/core';
import { Subject, takeUntil } from 'rxjs';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
@Directive({ @Directive({
selector: '[cnslHasRole]', selector: '[cnslHasRole]',
}) })
export class HasRoleDirective { export class HasRoleDirective implements OnDestroy {
private destroy$: Subject<void> = new Subject();
private hasView: boolean = false; private hasView: boolean = false;
@Input() public set hasRole(roles: string[] | RegExp[] | undefined) { @Input() public set hasRole(roles: string[] | RegExp[] | undefined) {
if (roles && roles.length > 0) { if (roles && roles.length > 0) {
this.authService.isAllowed(roles).subscribe((isAllowed) => { this.authService
if (isAllowed && !this.hasView) { .isAllowed(roles)
this.viewContainerRef.clear(); .pipe(takeUntil(this.destroy$))
this.viewContainerRef.createEmbeddedView(this.templateRef); .subscribe((isAllowed) => {
} else { if (isAllowed && !this.hasView) {
this.viewContainerRef.clear(); if (this.viewContainerRef.length !== 0) {
this.hasView = false; this.viewContainerRef.clear();
} }
}); this.viewContainerRef.createEmbeddedView(this.templateRef);
} else {
this.viewContainerRef.clear();
this.hasView = false;
}
});
} else { } else {
if (!this.hasView) { if (!this.hasView) {
this.viewContainerRef.clear(); if (this.viewContainerRef.length !== 0) {
this.viewContainerRef.clear();
}
this.viewContainerRef.createEmbeddedView(this.templateRef); this.viewContainerRef.createEmbeddedView(this.templateRef);
} }
} }
@ -30,4 +39,9 @@ export class HasRoleDirective {
protected templateRef: TemplateRef<any>, protected templateRef: TemplateRef<any>,
protected viewContainerRef: ViewContainerRef, protected viewContainerRef: ViewContainerRef,
) {} ) {}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
} }

View File

@ -4,13 +4,12 @@ import { AuthConfig } from 'angular-oauth2-oidc';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AuthenticationService } from '../services/authentication.service'; import { AuthenticationService } from '../services/authentication.service';
import { GrpcAuthService } from '../services/grpc-auth.service';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class AuthGuard implements CanActivate { export class AuthGuard implements CanActivate {
constructor(private auth: AuthenticationService, private authService: GrpcAuthService) {} constructor(private auth: AuthenticationService) {}
public canActivate( public canActivate(
route: ActivatedRouteSnapshot, route: ActivatedRouteSnapshot,

View File

@ -1,12 +1,16 @@
<div *ngIf="type !== ActionKeysType.ORGSWITCHER && (isHandset$ | async) === false" class="action-keys-wrapper" <div
[ngSwitch]="type" [ngClass]="{'without-margin': withoutMargin, 'no-contrast-mode': doNotUseContrast}"> *ngIf="type !== ActionKeysType.ORGSWITCHER && (isHandset$ | async) === false"
class="action-keys-wrapper"
[ngSwitch]="type"
[ngClass]="{ 'without-margin': withoutMargin, 'no-contrast-mode': doNotUseContrast }"
>
<div *ngSwitchCase="ActionKeysType.CLEAR" class="action-keys-row"> <div *ngSwitchCase="ActionKeysType.CLEAR" class="action-keys-row">
<div class="action-key esc"> <div class="action-key esc">
<div class="key-overlay"></div> <div class="key-overlay"></div>
<span>ESC</span> <span>ESC</span>
</div> </div>
</div> </div>
<div *ngSwitchCase="ActionKeysType.ADD" class="action-keys-row"> <div *ngSwitchCase="ActionKeysType.ADD" class="action-keys-row" data-e2e="action-key-add">
<div class="action-key"> <div class="action-key">
<div class="key-overlay"></div> <div class="key-overlay"></div>
<span>N</span> <span>N</span>
@ -38,7 +42,6 @@
<div class="action-key"> <div class="action-key">
<div class="key-overlay"></div> <div class="key-overlay"></div>
<span *ngIf="isMacLike || isIOS; else otherOS"></span> <span *ngIf="isMacLike || isIOS; else otherOS"></span>
</div> </div>
+ +
<div class="action-key"> <div class="action-key">
@ -56,4 +59,4 @@
<ng-template #otherOS> <ng-template #otherOS>
<span>crtl</span> <span>crtl</span>
</ng-template> </ng-template>

View File

@ -1,18 +1,17 @@
<div class="card" [ngClass]="{'nomargin': nomargin, 'stretch': stretch, 'warn': warn}" [attr.data-e2e]="'app-card'"> <div class="card" [ngClass]="{ nomargin: nomargin, stretch: stretch, warn: warn }" data-e2e="app-card">
<div *ngIf="title || description" class="header" [ngClass]="{'bottom-margin': expanded}"> <div *ngIf="title || description" class="header" [ngClass]="{ 'bottom-margin': expanded }">
<div *ngIf="title" class="row"> <div *ngIf="title" class="row">
<h2 class="title" [attr.data-e2e]="'app-card-title'">{{title}}</h2> <h2 class="title" data-e2e="app-card-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" type="button" matTooltip="Expand or collapse" mat-icon-button <button class="button" type="button" matTooltip="Expand or collapse" mat-icon-button (click)="expanded = !expanded">
(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>
</div> </div>
<p *ngIf="description" class="desc cnsl-secondary-text">{{description}}</p> <p *ngIf="description" class="desc cnsl-secondary-text">{{ description }}</p>
</div> </div>
<div class="card-content" *ngIf="expanded" [@openClose]="animate"> <div class="card-content" *ngIf="expanded" [@openClose]="animate">
<ng-content></ng-content> <ng-content></ng-content>
</div> </div>
</div> </div>

View File

@ -19,6 +19,7 @@ export enum ChangeType {
USER = 'user', USER = 'user',
ORG = 'org', ORG = 'org',
PROJECT = 'project', PROJECT = 'project',
PROJECT_GRANT= 'project-grant',
APP = 'app', APP = 'app',
} }
@ -93,6 +94,9 @@ export class ChangesComponent implements OnInit, OnDestroy {
case ChangeType.PROJECT: case ChangeType.PROJECT:
first = this.mgmtUserService.listProjectChanges(this.id, 30, 0); first = this.mgmtUserService.listProjectChanges(this.id, 30, 0);
break; break;
case ChangeType.PROJECT_GRANT:
first = this.mgmtUserService.listProjectGrantChanges(this.id, this.secId, 30, 0);
break;
case ChangeType.ORG: case ChangeType.ORG:
first = this.mgmtUserService.listOrgChanges(30, 0); first = this.mgmtUserService.listOrgChanges(30, 0);
break; break;
@ -126,6 +130,9 @@ export class ChangesComponent implements OnInit, OnDestroy {
case ChangeType.PROJECT: case ChangeType.PROJECT:
more = this.mgmtUserService.listProjectChanges(this.id, 20, cursor); more = this.mgmtUserService.listProjectChanges(this.id, 20, cursor);
break; break;
case ChangeType.PROJECT_GRANT:
more = this.mgmtUserService.listProjectGrantChanges(this.id, this.secId, 20, cursor);
break;
case ChangeType.ORG: case ChangeType.ORG:
more = this.mgmtUserService.listOrgChanges(20, cursor); more = this.mgmtUserService.listOrgChanges(20, cursor);
break; break;

View File

@ -9,7 +9,7 @@
.ng-untouched { .ng-untouched {
.cnsl-error { .cnsl-error {
visibility: hidden; visibility: hidden;
transition: visibility .2 ease; transition: visibility 0.2 ease;
} }
} }
@ -24,10 +24,14 @@
[cnslSuffix] { [cnslSuffix] {
position: absolute; position: absolute;
right: .5rem; right: 0.5rem;
top: 9px; top: 9px;
height: inherit; height: inherit;
vertical-align: middle; vertical-align: middle;
max-width: 150px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
input { input {

View File

@ -196,55 +196,56 @@
</a> </a>
</div> </div>
<div class="account-card-wrapper"> <ng-container
<button *ngIf="user && (user.human?.profile?.displayName || (user.human?.profile?.firstName && user.human?.profile?.lastName))"
cdkOverlayOrigin
#accounttrigger="cdkOverlayOrigin"
class="icon-container"
(click)="showAccount = !showAccount"
[ngClass]="{ 'iam-user': (['iam.write$'] | hasRole | async) }"
>
<cnsl-avatar
id="avatartoggle"
*ngIf="
user && (user.human?.profile?.displayName || (user.human?.profile?.firstName && user.human?.profile?.lastName))
"
class="avatar-toggle dontcloseonclick"
[active]="showAccount"
[avatarUrl]="user.human?.profile?.avatarUrl || ''"
[forColor]="user?.preferredLoginName || ''"
[name]="
user.human?.profile?.displayName
? user.human?.profile?.displayName ?? ''
: user.human?.profile?.firstName + ' ' + user.human?.profile?.lastName
"
[size]="38"
>
</cnsl-avatar>
</button>
</div>
<ng-template
cdkConnectedOverlay
[cdkConnectedOverlayOrigin]="accounttrigger"
[flexibleDimensions]="true"
[lockPosition]="true"
[cdkConnectedOverlayOffsetY]="10"
[cdkConnectedOverlayHasBackdrop]="true"
[cdkConnectedOverlayPositions]="accountCardPositions"
cdkConnectedOverlayBackdropClass="transparent-backdrop"
[cdkConnectedOverlayOpen]="showAccount"
(backdropClick)="showAccount = false"
(detach)="showAccount = false"
> >
<cnsl-accounts-card <div class="account-card-wrapper">
@accounts <button
class="a_card" cdkOverlayOrigin
*ngIf="showAccount" #accounttrigger="cdkOverlayOrigin"
(closedCard)="showAccount = false" class="icon-container"
[user]="user" (click)="showAccount = !showAccount"
[iamuser]="['iam.write$'] | hasRole | async" [ngClass]="{ 'iam-user': (['iam.write$'] | hasRole | async) }"
>
<cnsl-avatar
id="avatartoggle"
class="avatar-toggle dontcloseonclick"
[active]="showAccount"
[avatarUrl]="user.human?.profile?.avatarUrl || ''"
[forColor]="user?.preferredLoginName || ''"
[name]="
user.human?.profile?.displayName
? user.human?.profile?.displayName ?? ''
: user.human?.profile?.firstName + ' ' + user.human?.profile?.lastName
"
[size]="38"
>
</cnsl-avatar>
</button>
</div>
<ng-template
cdkConnectedOverlay
[cdkConnectedOverlayOrigin]="accounttrigger"
[flexibleDimensions]="true"
[lockPosition]="true"
[cdkConnectedOverlayOffsetY]="10"
[cdkConnectedOverlayHasBackdrop]="true"
[cdkConnectedOverlayPositions]="accountCardPositions"
cdkConnectedOverlayBackdropClass="transparent-backdrop"
[cdkConnectedOverlayOpen]="showAccount"
(backdropClick)="showAccount = false"
(detach)="showAccount = false"
> >
</cnsl-accounts-card> <cnsl-accounts-card
</ng-template> @accounts
class="a_card"
*ngIf="showAccount"
(closedCard)="showAccount = false"
[user]="user"
[iamuser]="['iam.write$'] | hasRole | async"
>
</cnsl-accounts-card>
</ng-template>
</ng-container>
</div> </div>
</mat-toolbar> </mat-toolbar>

View File

@ -48,6 +48,66 @@
</div> </div>
</div> </div>
<div class="info-row" *ngIf="instance">
<div class="info-wrapper">
<p class="info-row-title">{{ 'IAM.PAGES.STATE' | translate }}</p>
<p
*ngIf="instance && instance.state !== undefined"
class="state"
[ngClass]="{
active: instance.state === State.INSTANCE_STATE_RUNNING,
inactive: instance.state === State.INSTANCE_STATE_STOPPED || instance.state === State.INSTANCE_STATE_STOPPING
}"
>
{{ 'IAM.STATE.' + instance.state | translate }}
</p>
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'RESOURCEID' | translate }}</p>
<p *ngIf="instance && instance.id" class="info-row-desc">{{ instance.id }}</p>
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'NAME' | translate }}</p>
<p *ngIf="instance && instance.name" class="info-row-desc">{{ instance.name }}</p>
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'VERSION' | translate }}</p>
<p *ngIf="instance && instance.version" class="info-row-desc">{{ instance.version }}</p>
</div>
<div class="info-wrapper width">
<p class="info-row-title">{{ 'IAM.PAGES.DOMAINLIST' | translate }}</p>
<div class="copy-row" *ngFor="let domain of instance?.domainsList">
<button
[disabled]="copied === domain.domain"
[matTooltip]="(copied !== domain.domain ? 'ACTIONS.COPY' : 'ACTIONS.COPIED') | translate"
cnslCopyToClipboard
[valueToCopy]="domain.domain"
(copiedValue)="copied = $event"
>
{{ domain.domain }}
</button>
</div>
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'ORG.PAGES.CREATIONDATE' | translate }}</p>
<p *ngIf="instance && instance.details && instance.details.creationDate" class="info-row-desc">
{{ instance.details.creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
</p>
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'ORG.PAGES.DATECHANGED' | translate }}</p>
<p *ngIf="instance && instance.details && instance.details.changeDate" class="info-row-desc">
{{ instance.details.changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
</p>
</div>
</div>
<div class="info-row" *ngIf="org"> <div class="info-row" *ngIf="org">
<div class="info-wrapper"> <div class="info-wrapper">
<p class="info-row-title">{{ 'ORG.PAGES.STATE' | translate }}</p> <p class="info-row-title">{{ 'ORG.PAGES.STATE' | translate }}</p>

View File

@ -1,6 +1,7 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { App, AppState } from 'src/app/proto/generated/zitadel/app_pb'; import { App, AppState } from 'src/app/proto/generated/zitadel/app_pb';
import { IDP, IDPState } from 'src/app/proto/generated/zitadel/idp_pb'; import { IDP, IDPState } from 'src/app/proto/generated/zitadel/idp_pb';
import { InstanceDetail, State } from 'src/app/proto/generated/zitadel/instance_pb';
import { Org, OrgState } from 'src/app/proto/generated/zitadel/org_pb'; import { Org, OrgState } from 'src/app/proto/generated/zitadel/org_pb';
import { GrantedProject, Project, ProjectGrantState, ProjectState } from 'src/app/proto/generated/zitadel/project_pb'; import { GrantedProject, Project, ProjectGrantState, ProjectState } from 'src/app/proto/generated/zitadel/project_pb';
import { User, UserState } from 'src/app/proto/generated/zitadel/user_pb'; import { User, UserState } from 'src/app/proto/generated/zitadel/user_pb';
@ -13,12 +14,14 @@ import { User, UserState } from 'src/app/proto/generated/zitadel/user_pb';
export class InfoRowComponent { export class InfoRowComponent {
@Input() public user!: User.AsObject; @Input() public user!: User.AsObject;
@Input() public org!: Org.AsObject; @Input() public org!: Org.AsObject;
@Input() public instance!: InstanceDetail.AsObject;
@Input() public app!: App.AsObject; @Input() public app!: App.AsObject;
@Input() public idp!: IDP.AsObject; @Input() public idp!: IDP.AsObject;
@Input() public project!: Project.AsObject; @Input() public project!: Project.AsObject;
@Input() public grantedProject!: GrantedProject.AsObject; @Input() public grantedProject!: GrantedProject.AsObject;
public UserState: any = UserState; public UserState: any = UserState;
public State: any = State;
public OrgState: any = OrgState; public OrgState: any = OrgState;
public AppState: any = AppState; public AppState: any = AppState;
public IDPState: any = IDPState; public IDPState: any = IDPState;

View File

@ -6,9 +6,7 @@ import { Observable, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { InstanceMembersDataSource } from 'src/app/pages/instance/instance-members/instance-members-datasource'; import { InstanceMembersDataSource } from 'src/app/pages/instance/instance-members/instance-members-datasource';
import { OrgMembersDataSource } from 'src/app/pages/orgs/org-members/org-members-datasource'; import { OrgMembersDataSource } from 'src/app/pages/orgs/org-members/org-members-datasource';
import { import { ProjectGrantMembersDataSource } from 'src/app/pages/projects/owned-projects/project-grant-detail/project-grant-members-datasource';
ProjectGrantMembersDataSource,
} from 'src/app/pages/projects/owned-projects/project-grant-detail/project-grant-members-datasource';
import { Member } from 'src/app/proto/generated/zitadel/member_pb'; import { Member } from 'src/app/proto/generated/zitadel/member_pb';
import { getMembershipColor } from 'src/app/utils/color'; import { getMembershipColor } from 'src/app/utils/color';
@ -86,7 +84,7 @@ export class MembersTableComponent implements OnInit, OnDestroy {
const newRoles = Object.assign([], member.rolesList); const newRoles = Object.assign([], member.rolesList);
const index = newRoles.findIndex((r) => r === role); const index = newRoles.findIndex((r) => r === role);
if (index > -1) { if (index > -1) {
newRoles.splice(index); newRoles.splice(index, 1);
member.rolesList = newRoles; member.rolesList = newRoles;
this.updateRoles.emit({ member: member, change: newRoles }); this.updateRoles.emit({ member: member, change: newRoles });
} }

View File

@ -0,0 +1,6 @@
<button class="nav-toggle" [ngClass]="{active}" (click)="clicked.emit()">
<div class="c_label">
<span> {{ label }} </span>
<small *ngIf="count" class="count">({{ count }})</small>
</div>
</button>

View File

@ -0,0 +1,71 @@
@use '@angular/material' as mat;
@mixin nav-toggle-theme($theme) {
$primary: map-get($theme, primary);
$warn: map-get($theme, warn);
$background: map-get($theme, background);
$accent: map-get($theme, accent);
$primary-color: mat.get-color-from-palette($primary, 500);
$warn-color: mat.get-color-from-palette($warn, 500);
$accent-color: mat.get-color-from-palette($accent, 500);
$foreground: map-get($theme, foreground);
$is-dark-theme: map-get($theme, is-dark);
$back: map-get($background, background);
.nav-toggle {
display: flex;
align-items: center;
font-size: 14px;
line-height: 14px;
padding: 0.4rem 12px;
color: mat.get-color-from-palette($foreground, text) !important;
transition: all 0.2s ease;
text-decoration: none;
border-radius: 50vw;
border: none;
font-weight: 400;
margin: 0.25rem 2px;
white-space: nowrap;
position: relative;
background: none;
cursor: pointer;
font-family: 'Lato', -apple-system, BlinkMacSystemFont, sans-serif;
.c_label {
display: flex;
align-items: center;
text-align: center;
.count {
display: none;
margin-left: 6px;
}
}
&.external-link {
padding-right: 2rem;
i {
position: absolute;
right: 8px;
font-size: 1.2rem;
}
}
&:hover {
background: if($is-dark-theme, #ffffff40, #00000010);
}
&.active {
background-color: $primary-color;
color: mat.get-color-from-palette($foreground, toolbar-items) !important;
.c_label {
.count {
display: inline-block;
}
}
}
}
}

View File

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

View File

@ -0,0 +1,14 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'cnsl-nav-toggle',
templateUrl: './nav-toggle.component.html',
styleUrls: ['./nav-toggle.component.scss'],
})
export class NavToggleComponent {
@Input() public label: string = '';
@Input() public count: number | null = 0;
@Input() public active: boolean = false;
@Output() public clicked: EventEmitter<void> = new EventEmitter<void>();
constructor() {}
}

View File

@ -0,0 +1,12 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { NavToggleComponent } from './nav-toggle.component';
@NgModule({
declarations: [NavToggleComponent],
imports: [CommonModule, RouterModule],
exports: [NavToggleComponent],
})
export class NavToggleModule {}

View File

@ -3,7 +3,7 @@
<p class="length"> <p class="length">
<span>{{ length }} </span>{{ 'PAGINATOR.COUNT' | translate }} <span>{{ length }} </span>{{ 'PAGINATOR.COUNT' | translate }}
</p> </p>
<p class="ts cnsl-secondary-text" *ngIf="timestamp"> <p class="ts cnsl-secondary-text" *ngIf="timestamp" data-e2e="timestamp">
{{ timestamp | timestampToDate | localizedDate: 'EEEE dd. MMM YYYY, HH:mm' }} {{ timestamp | timestampToDate | localizedDate: 'EEEE dd. MMM YYYY, HH:mm' }}
</p> </p>
</div> </div>

View File

@ -8,7 +8,7 @@
<input cnslInput name="sid" formControlName="sid" /> <input cnslInput name="sid" formControlName="sid" />
</cnsl-form-field> </cnsl-form-field>
<cnsl-form-field class="sms-form-field" label="Token"> <cnsl-form-field *ngIf="token" class="sms-form-field" label="Token">
<cnsl-label>{{ 'SETTING.SMS.TWILIO.TOKEN' | translate }}</cnsl-label> <cnsl-label>{{ 'SETTING.SMS.TWILIO.TOKEN' | translate }}</cnsl-label>
<input cnslInput name="token" formControlName="token" /> <input cnslInput name="token" formControlName="token" />
</cnsl-form-field> </cnsl-form-field>

View File

@ -1,10 +1,10 @@
import { Component, Inject } from '@angular/core'; import { Component, Inject } from '@angular/core';
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { AbstractControl, FormControl, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { import {
AddSMSProviderTwilioRequest, AddSMSProviderTwilioRequest,
UpdateSMSProviderTwilioRequest, UpdateSMSProviderTwilioRequest,
UpdateSMSProviderTwilioTokenRequest, UpdateSMSProviderTwilioTokenRequest,
} from 'src/app/proto/generated/zitadel/admin_pb'; } from 'src/app/proto/generated/zitadel/admin_pb';
import { SMSProvider, TwilioConfig } from 'src/app/proto/generated/zitadel/settings_pb'; import { SMSProvider, TwilioConfig } from 'src/app/proto/generated/zitadel/settings_pb';
import { AdminService } from 'src/app/services/admin.service'; import { AdminService } from 'src/app/services/admin.service';
@ -41,13 +41,14 @@ export class DialogAddSMSProviderComponent {
) { ) {
this.twilioForm = this.fb.group({ this.twilioForm = this.fb.group({
sid: ['', [Validators.required]], sid: ['', [Validators.required]],
token: ['', [Validators.required]],
senderNumber: ['', [Validators.required]], senderNumber: ['', [Validators.required]],
}); });
this.smsProviders = data.smsProviders; this.smsProviders = data.smsProviders;
if (!!this.twilio) { if (!!this.twilio) {
this.twilioForm.patchValue(this.twilio); this.twilioForm.patchValue(this.twilio);
} else {
this.twilioForm.addControl('token', new FormControl('', Validators.required));
} }
} }
@ -82,14 +83,16 @@ export class DialogAddSMSProviderComponent {
}); });
dialogRef.afterClosed().subscribe((token: string) => { dialogRef.afterClosed().subscribe((token: string) => {
if (token) { if (token && this.twilioProvider?.id) {
const tokenReq = new UpdateSMSProviderTwilioTokenRequest(); const tokenReq = new UpdateSMSProviderTwilioTokenRequest();
tokenReq.setToken(token); tokenReq.setToken(token);
tokenReq.setId(this.twilioProvider.id);
this.service this.service
.updateSMSProviderTwilioToken(tokenReq) .updateSMSProviderTwilioToken(tokenReq)
.then(() => { .then(() => {
this.toast.showInfo('SETTING.SMS.TWILIO.TOKENSET', true); this.toast.showInfo('SETTING.SMS.TWILIO.TOKENSET', true);
this.dialogRef.close();
}) })
.catch((error) => { .catch((error) => {
this.toast.showError(error); this.toast.showError(error);
@ -110,6 +113,15 @@ export class DialogAddSMSProviderComponent {
return this.twilioForm.get('sid'); return this.twilioForm.get('sid');
} }
public get twilioProvider(): SMSProvider.AsObject | undefined {
const twilioProvider: SMSProvider.AsObject | undefined = this.smsProviders.find((p) => p.twilio);
if (twilioProvider) {
return twilioProvider;
} else {
return undefined;
}
}
public get twilio(): TwilioConfig.AsObject | undefined { public get twilio(): TwilioConfig.AsObject | undefined {
const twilioProvider: SMSProvider.AsObject | undefined = this.smsProviders.find((p) => p.twilio); const twilioProvider: SMSProvider.AsObject | undefined = this.smsProviders.find((p) => p.twilio);
if (twilioProvider && !!twilioProvider.twilio) { if (twilioProvider && !!twilioProvider.twilio) {

View File

@ -75,9 +75,32 @@
active: twilio?.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_ACTIVE, active: twilio?.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_ACTIVE,
inactive: twilio?.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_INACTIVE inactive: twilio?.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_INACTIVE
}" }"
></span> >{{ 'SETTING.SMS.SMSPROVIDERSTATE.' + twilio?.state | translate }}</span
>
<span class="fill-space"></span> <span class="fill-space"></span>
<button
*ngIf="twilio && twilio.id"
[disabled]="(['iam.write'] | hasRole | async) === false"
mat-stroked-button
(click)="toggleSMSProviderState(twilio.id)"
>
<span *ngIf="twilio.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_ACTIVE">{{
'ACTIONS.DEACTIVATE' | translate
}}</span>
<span *ngIf="twilio.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_INACTIVE">{{
'ACTIONS.ACTIVATE' | translate
}}</span>
</button>
<button
*ngIf="twilio && twilio.id"
color="warn"
[disabled]="(['iam.write'] | hasRole | async) === false"
mat-icon-button
(click)="removeSMSProvider(twilio.id)"
>
<i class="las la-trash"></i>
</button>
<button [disabled]="(['iam.write'] | hasRole | async) === false" mat-icon-button (click)="editSMSProvider()"> <button [disabled]="(['iam.write'] | hasRole | async) === false" mat-icon-button (click)="editSMSProvider()">
<i class="las la-pen"></i> <i class="las la-pen"></i>
</button> </button>

View File

@ -49,6 +49,7 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
width: 350px;
.title { .title {
font-size: 16px; font-size: 16px;

View File

@ -3,12 +3,12 @@ import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, Validators } fro
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { take } from 'rxjs'; import { take } from 'rxjs';
import { import {
AddSMSProviderTwilioRequest, AddSMSProviderTwilioRequest,
AddSMTPConfigRequest, AddSMTPConfigRequest,
UpdateSMSProviderTwilioRequest, UpdateSMSProviderTwilioRequest,
UpdateSMTPConfigPasswordRequest, UpdateSMTPConfigPasswordRequest,
UpdateSMTPConfigPasswordResponse, UpdateSMTPConfigPasswordResponse,
UpdateSMTPConfigRequest, UpdateSMTPConfigRequest,
} from 'src/app/proto/generated/zitadel/admin_pb'; } from 'src/app/proto/generated/zitadel/admin_pb';
import { DebugNotificationProvider, SMSProvider, SMSProviderConfigState } from 'src/app/proto/generated/zitadel/settings_pb'; import { DebugNotificationProvider, SMSProvider, SMSProviderConfigState } from 'src/app/proto/generated/zitadel/settings_pb';
import { AdminService } from 'src/app/services/admin.service'; import { AdminService } from 'src/app/services/admin.service';
@ -16,6 +16,7 @@ import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
import { InfoSectionType } from '../../info-section/info-section.component'; import { InfoSectionType } from '../../info-section/info-section.component';
import { WarnDialogComponent } from '../../warn-dialog/warn-dialog.component';
import { PolicyComponentServiceType } from '../policy-component-types.enum'; import { PolicyComponentServiceType } from '../policy-component-types.enum';
import { DialogAddSMSProviderComponent } from './dialog-add-sms-provider/dialog-add-sms-provider.component'; import { DialogAddSMSProviderComponent } from './dialog-add-sms-provider/dialog-add-sms-provider.component';
import { PasswordDialogComponent } from './password-dialog/password-dialog.component'; import { PasswordDialogComponent } from './password-dialog/password-dialog.component';
@ -185,11 +186,12 @@ export class NotificationSettingsComponent implements OnInit {
dialogRef.afterClosed().subscribe((req: AddSMSProviderTwilioRequest | UpdateSMSProviderTwilioRequest) => { dialogRef.afterClosed().subscribe((req: AddSMSProviderTwilioRequest | UpdateSMSProviderTwilioRequest) => {
if (req) { if (req) {
if (this.hasTwilio) { if (!!this.twilio) {
this.service this.service
.updateSMSProviderTwilio(req as UpdateSMSProviderTwilioRequest) .updateSMSProviderTwilio(req as UpdateSMSProviderTwilioRequest)
.then(() => { .then(() => {
this.toast.showInfo('SETTING.SMS.TWILIO.ADDED', true); this.toast.showInfo('SETTING.SMS.TWILIO.ADDED', true);
this.fetchData();
}) })
.catch((error) => { .catch((error) => {
this.toast.showError(error); this.toast.showError(error);
@ -199,6 +201,7 @@ export class NotificationSettingsComponent implements OnInit {
.addSMSProviderTwilio(req as AddSMSProviderTwilioRequest) .addSMSProviderTwilio(req as AddSMSProviderTwilioRequest)
.then(() => { .then(() => {
this.toast.showInfo('SETTING.SMS.TWILIO.ADDED', true); this.toast.showInfo('SETTING.SMS.TWILIO.ADDED', true);
this.fetchData();
}) })
.catch((error) => { .catch((error) => {
this.toast.showError(error); this.toast.showError(error);
@ -234,6 +237,59 @@ export class NotificationSettingsComponent implements OnInit {
}); });
} }
public toggleSMSProviderState(id: string): void {
const provider = this.smsProviders.find((p) => p.id === id);
if (provider) {
if (provider.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_ACTIVE) {
this.service
.deactivateSMSProvider(id)
.then(() => {
this.toast.showInfo('SETTING.SMS.DEACTIVATED', true);
this.fetchData();
})
.catch((error) => {
this.toast.showError(error);
});
} else if (provider.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_INACTIVE) {
this.service
.activateSMSProvider(id)
.then(() => {
this.toast.showInfo('SETTING.SMS.ACTIVATED', true);
this.fetchData();
})
.catch((error) => {
this.toast.showError(error);
});
}
}
}
public removeSMSProvider(id: string): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'SETTING.SMS.REMOVEPROVIDER',
descriptionKey: 'SETTING.SMS.REMOVEPROVIDER_DESC',
},
width: '400px',
});
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
this.service
.removeSMSProvider(id)
.then(() => {
this.toast.showInfo('SETTING.SMS.TWILIO.REMOVED', true);
this.fetchData();
})
.catch((error) => {
this.toast.showError(error);
});
}
});
}
public get twilio(): SMSProvider.AsObject | undefined { public get twilio(): SMSProvider.AsObject | undefined {
return this.smsProviders.find((p) => p.twilio); return this.smsProviders.find((p) => p.twilio);
} }
@ -257,13 +313,4 @@ export class NotificationSettingsComponent implements OnInit {
public get host(): AbstractControl | null { public get host(): AbstractControl | null {
return this.form.get('host'); return this.form.get('host');
} }
public get hasTwilio(): boolean {
const twilioProvider: SMSProvider.AsObject | undefined = this.smsProviders.find((p) => p.twilio);
if (twilioProvider && !!twilioProvider.twilio) {
return true;
} else {
return false;
}
}
} }

View File

@ -13,6 +13,7 @@ import { CardModule } from '../../card/card.module';
import { FormFieldModule } from '../../form-field/form-field.module'; import { FormFieldModule } from '../../form-field/form-field.module';
import { InfoSectionModule } from '../../info-section/info-section.module'; import { InfoSectionModule } from '../../info-section/info-section.module';
import { InputModule } from '../../input/input.module'; import { InputModule } from '../../input/input.module';
import { WarnDialogModule } from '../../warn-dialog/warn-dialog.module';
import { DialogAddSMSProviderComponent } from './dialog-add-sms-provider/dialog-add-sms-provider.component'; import { DialogAddSMSProviderComponent } from './dialog-add-sms-provider/dialog-add-sms-provider.component';
import { NotificationSettingsComponent } from './notification-settings.component'; import { NotificationSettingsComponent } from './notification-settings.component';
import { PasswordDialogComponent } from './password-dialog/password-dialog.component'; import { PasswordDialogComponent } from './password-dialog/password-dialog.component';
@ -31,6 +32,7 @@ import { PasswordDialogComponent } from './password-dialog/password-dialog.compo
InputModule, InputModule,
MatIconModule, MatIconModule,
FormFieldModule, FormFieldModule,
WarnDialogModule,
MatSelectModule, MatSelectModule,
MatProgressSpinnerModule, MatProgressSpinnerModule,
MatSelectModule, MatSelectModule,

View File

@ -1,4 +1,4 @@
<div [attr.data-e2e]="'color'"> <div data-e2e="color">
<p class="name cnsl-secondary-text">{{ name }}</p> <p class="name cnsl-secondary-text">{{ name }}</p>
<div class="wrapper"> <div class="wrapper">

View File

@ -101,7 +101,7 @@
<div *ngIf="previewData && data" class="lab-policy-content"> <div *ngIf="previewData && data" class="lab-policy-content">
<mat-accordion class="settings"> <mat-accordion class="settings">
<mat-expansion-panel class="expansion"> <mat-expansion-panel class="expansion">
<mat-expansion-panel-header [attr.data-e2e]="'policy-category'"> <mat-expansion-panel-header data-e2e="policy-category">
<mat-panel-title> <mat-panel-title>
<div class="panel-title"> <div class="panel-title">
<i class="icon las la-image"></i> <i class="icon las la-image"></i>
@ -127,7 +127,7 @@
{{ 'POLICY.PRIVATELABELING.EMAILNOSVG' | translate }} {{ 'POLICY.PRIVATELABELING.EMAILNOSVG' | translate }}
</cnsl-info-section> </cnsl-info-section>
<div class="logo-view" [attr.data-e2e]="'image-part-logo'"> <div class="logo-view" data-e2e="image-part-logo">
<span class="label cnsl-secondary-text">Logo</span> <span class="label cnsl-secondary-text">Logo</span>
<div class="img-wrapper"> <div class="img-wrapper">
<ng-container <ng-container
@ -198,7 +198,7 @@
</div> </div>
</div> </div>
<div class="logo-view" [attr.data-e2e]="'image-part-icon'"> <div class="logo-view" data-e2e="image-part-icon">
<span class="label cnsl-secondary-text">Icon</span> <span class="label cnsl-secondary-text">Icon</span>
<div class="img-wrapper icon"> <div class="img-wrapper icon">
<ng-container <ng-container
@ -478,7 +478,7 @@
</mat-expansion-panel> </mat-expansion-panel>
<mat-expansion-panel class="expansion"> <mat-expansion-panel class="expansion">
<mat-expansion-panel-header class="header" [attr.data-e2e]="'policy-category'"> <mat-expansion-panel-header class="header" data-e2e="policy-category">
<mat-panel-title> <mat-panel-title>
<div class="panel-title"> <div class="panel-title">
<i class="icon las la-font"></i> <i class="icon las la-font"></i>

View File

@ -1,15 +1,25 @@
<cnsl-refresh-table [showSelectionActionButton]="showSelectionActionButton" *ngIf="projectId" <cnsl-refresh-table
(refreshed)="refreshPage()" [dataSize]="dataSource?.totalResult ?? 0" [showSelectionActionButton]="showSelectionActionButton"
[emitRefreshOnPreviousRoutes]="['/projects/'+projectId+'/roles/create']" [selection]="selection" *ngIf="projectId"
[loading]="dataSource?.loading$ | async" [timestamp]="dataSource?.viewTimestamp"> (refreshed)="refreshPage()"
[dataSize]="dataSource?.totalResult ?? 0"
[emitRefreshOnPreviousRoutes]="['/projects/' + projectId + '/roles/create']"
[selection]="selection"
[loading]="dataSource?.loading$ | async"
[timestamp]="dataSource?.viewTimestamp"
>
<ng-template cnslHasRole [hasRole]="['project.role.write:' + projectId, 'project.role.write']" actions> <ng-template cnslHasRole [hasRole]="['project.role.write:' + projectId, 'project.role.write']" actions>
<a *ngIf="actionsVisible" [disabled]="disabled" [routerLink]="[ '/projects', projectId, 'roles', 'create']" <a
color="primary" class="cnsl-action-button" mat-raised-button [attr.data-e2e]="'add-new-role'"> *ngIf="actionsVisible"
<mat-icon class="icon">add</mat-icon> [disabled]="disabled"
[routerLink]="['/projects', projectId, 'roles', 'create']"
color="primary"
class="cnsl-action-button"
mat-raised-button
>
<mat-icon data-e2e="add-new-role" class="icon">add</mat-icon>
<span>{{ 'ACTIONS.NEW' | translate }}</span> <span>{{ 'ACTIONS.NEW' | translate }}</span>
<cnsl-action-keys (actionTriggered)="gotoRouterLink([ '/projects', projectId, 'roles', 'create'])"> <cnsl-action-keys (actionTriggered)="gotoRouterLink(['/projects', projectId, 'roles', 'create'])"> </cnsl-action-keys>
</cnsl-action-keys>
</a> </a>
</ng-template> </ng-template>
@ -17,55 +27,70 @@
<table [dataSource]="dataSource" mat-table class="table" matSort aria-label="Elements"> <table [dataSource]="dataSource" mat-table class="table" matSort aria-label="Elements">
<ng-container matColumnDef="select"> <ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef> <th class="selection" mat-header-cell *matHeaderCellDef>
<mat-checkbox [disabled]="disabled" color="primary" (change)="$event ? masterToggle() : null" <mat-checkbox
[disabled]="disabled"
color="primary"
(change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()" [checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()"> [indeterminate]="selection.hasValue() && !isAllSelected()"
>
</mat-checkbox> </mat-checkbox>
</th> </th>
<td class="selection" mat-cell *matCellDef="let row"> <td class="selection" mat-cell *matCellDef="let row">
<mat-checkbox color="primary" [disabled]="disabled" (click)="$event.stopPropagation()" <mat-checkbox
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)"> color="primary"
[disabled]="disabled"
(click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null"
[checked]="selection.isSelected(row)"
>
</mat-checkbox> </mat-checkbox>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="key"> <ng-container matColumnDef="key">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.KEY' | translate }} </th> <th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.ROLE.KEY' | translate }}</th>
<td class="pointer" (click)="openDetailDialog(role)" mat-cell *matCellDef="let role"> <td class="pointer" (click)="openDetailDialog(role)" mat-cell *matCellDef="let role">
<div class="role-key"> <div class="role-key">
<cnsl-project-role-chip [roleName]="role.key">{{ role.key }} <cnsl-project-role-chip [roleName]="role.key">{{ role.key }}</cnsl-project-role-chip>
</cnsl-project-role-chip>
</div> </div>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="displayname"> <ng-container matColumnDef="displayname">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.DISPLAY_NAME' | translate }} </th> <th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.ROLE.DISPLAY_NAME' | translate }}</th>
<td class="pointer" (click)="openDetailDialog(role)" mat-cell *matCellDef="let role"> {{role.displayName}} </td> <td class="pointer" (click)="openDetailDialog(role)" mat-cell *matCellDef="let role">{{ role.displayName }}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="group"> <ng-container matColumnDef="group">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.GROUP' | translate }} </th> <th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.ROLE.GROUP' | translate }}</th>
<td mat-cell *matCellDef="let role" class="pointer"> <td mat-cell *matCellDef="let role" class="pointer">
<span class="role state" [ngClass]="{'no-selection': !selectionAllowed}" *ngIf="role.group" <span
class="role state"
[ngClass]="{ 'no-selection': !selectionAllowed }"
*ngIf="role.group"
(click)="selectionAllowed ? selectAllOfGroup(role.group) : openDetailDialog(role)" (click)="selectionAllowed ? selectAllOfGroup(role.group) : openDetailDialog(role)"
[matTooltip]="selectionAllowed ? ('PROJECT.ROLE.SELECTGROUPTOOLTIP' | translate: role) : null">{{role.group}}</span> [matTooltip]="selectionAllowed ? ('PROJECT.ROLE.SELECTGROUPTOOLTIP' | translate: role) : null"
>{{ role.group }}</span
>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="creationDate"> <ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.CREATIONDATE' | translate }} </th> <th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.ROLE.CREATIONDATE' | translate }}</th>
<td class="pointer" (click)="openDetailDialog(role)" mat-cell *matCellDef="let role"> <td class="pointer" (click)="openDetailDialog(role)" mat-cell *matCellDef="let role">
<span *ngIf="role?.details?.creationDate">{{role.details.creationDate | timestampToDate | <span *ngIf="role?.details?.creationDate">{{
localizedDate: 'dd. MMM, HH:mm' }}</span> role.details.creationDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm'
}}</span>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="changeDate"> <ng-container matColumnDef="changeDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.CHANGEDATE' | translate }} </th> <th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.ROLE.CHANGEDATE' | translate }}</th>
<td class="pointer" (click)="openDetailDialog(role)" mat-cell *matCellDef="let role"> <td class="pointer" (click)="openDetailDialog(role)" mat-cell *matCellDef="let role">
<span *ngIf="role?.details?.changeDate">{{role.details.changeDate | timestampToDate | <span *ngIf="role?.details?.changeDate">{{
localizedDate: 'dd. MMM, HH:mm' }}</span> role.details.changeDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm'
}}</span>
</td> </td>
</ng-container> </ng-container>
@ -73,9 +98,16 @@
<th mat-header-cell *matHeaderCellDef class="role-table-actions"></th> <th mat-header-cell *matHeaderCellDef class="role-table-actions"></th>
<td mat-cell *matCellDef="let role" class="role-table-actions"> <td mat-cell *matCellDef="let role" class="role-table-actions">
<cnsl-table-actions> <cnsl-table-actions>
<button actions <button
[disabled]="disabled || (['project.role.delete', 'project.role.delete:' + projectId] | hasRole | async) === false" actions
mat-icon-button color="warn" matTooltip="{{'ACTIONS.DELETE' | translate}}" (click)="deleteRole(role)"> [disabled]="
disabled || (['project.role.delete', 'project.role.delete:' + projectId] | hasRole | async) === false
"
mat-icon-button
color="warn"
matTooltip="{{ 'ACTIONS.DELETE' | translate }}"
(click)="deleteRole(role)"
>
<i class="las la-trash"></i> <i class="las la-trash"></i>
</button> </button>
</cnsl-table-actions> </cnsl-table-actions>
@ -83,17 +115,22 @@
</ng-container> </ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let role; columns: displayedColumns;"> <tr class="highlight" mat-row *matRowDef="let role; columns: displayedColumns"></tr>
</tr>
</table> </table>
</div> </div>
<div *ngIf="(dataSource.loading$ | async) === false && !dataSource?.totalResult" class="no-content-row"> <div *ngIf="(dataSource.loading$ | async) === false && !dataSource?.totalResult" class="no-content-row">
<i class="las la-exclamation"></i> <i class="las la-exclamation"></i>
<span>{{'PROJECT.ROLE.EMPTY' | translate}}</span> <span>{{ 'PROJECT.ROLE.EMPTY' | translate }}</span>
</div> </div>
<cnsl-paginator #paginator [timestamp]="dataSource?.viewTimestamp" [length]="dataSource.totalResult" [pageSize]="50" <cnsl-paginator
(page)="changePage()" [pageSizeOptions]="[25, 50, 100, 250]"> #paginator
[timestamp]="dataSource?.viewTimestamp"
[length]="dataSource.totalResult"
[pageSize]="50"
(page)="changePage()"
[pageSizeOptions]="[25, 50, 100, 250]"
>
</cnsl-paginator> </cnsl-paginator>
</cnsl-refresh-table> </cnsl-refresh-table>

View File

@ -7,9 +7,10 @@
display: flex; display: flex;
align-items: center; align-items: center;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
box-sizing: border-box;
&.border-bottom { &.border-bottom {
padding-bottom: 0.5rem; padding-bottom: 1rem;
margin-bottom: 1rem; margin-bottom: 1rem;
border-bottom: 1px solid map-get($foreground, divider); border-bottom: 1px solid map-get($foreground, divider);
} }

View File

@ -14,7 +14,7 @@
: [] : []
" "
> >
<div class="p-item card" @policy [attr.data-e2e]="'policy-card'"> <div class="p-item card" @policy data-e2e="policy-card">
<div class="avatar {{ setting.color }}"> <div class="avatar {{ setting.color }}">
<mat-icon *ngIf="setting.svgIcon" class="mat-icon" [svgIcon]="setting.svgIcon"></mat-icon> <mat-icon *ngIf="setting.svgIcon" class="mat-icon" [svgIcon]="setting.svgIcon"></mat-icon>
<i *ngIf="setting.icon" class="icon {{ setting.icon }}"></i> <i *ngIf="setting.icon" class="icon {{ setting.icon }}"></i>

View File

@ -2,8 +2,14 @@
<div class="cnsl-table-action"> <div class="cnsl-table-action">
<ng-content select="[hoverActions]"></ng-content> <ng-content select="[hoverActions]"></ng-content>
<ng-content select="[actions]"></ng-content> <ng-content select="[actions]"></ng-content>
<button (click)="$event.stopPropagation()" *ngIf="hasActions" class="table-actions-trigger" mat-icon-button <button
[matMenuTriggerFor]="actions"> (click)="$event.stopPropagation()"
*ngIf="hasActions"
class="table-actions-trigger"
mat-icon-button
[matMenuTriggerFor]="actions"
data-e2e="table-actions-button"
>
<mat-icon>more_horiz</mat-icon> <mat-icon>more_horiz</mat-icon>
</button> </button>
@ -11,4 +17,4 @@
<ng-content select="[menuActions]"></ng-content> <ng-content select="[menuActions]"></ng-content>
</mat-menu> </mat-menu>
</div> </div>
</div> </div>

View File

@ -10,7 +10,7 @@
}}</cnsl-info-section> }}</cnsl-info-section>
<cnsl-form-field *ngIf="data.confirmation && data.confirmationKey" class="formfield"> <cnsl-form-field *ngIf="data.confirmation && data.confirmationKey" class="formfield">
<cnsl-label>{{ data.confirmationKey | translate: { value: data.confirmation } }}</cnsl-label> <cnsl-label>{{ data.confirmationKey | translate: { value: data.confirmation } }}</cnsl-label>
<input cnslInput [(ngModel)]="confirm" [attr.e2e-data]="'confirm-dialog-input'" /> <input cnslInput [(ngModel)]="confirm" data-e2e="confirm-dialog-input" />
</cnsl-form-field> </cnsl-form-field>
</div> </div>
<div mat-dialog-actions class="action"> <div mat-dialog-actions class="action">
@ -24,7 +24,7 @@
mat-raised-button mat-raised-button
class="ok-button" class="ok-button"
(click)="closeDialogWithSuccess()" (click)="closeDialogWithSuccess()"
[attr.e2e-data]="'confirm-dialog-button'" data-e2e="confirm-dialog-button"
> >
{{ data.confirmKey | translate }} {{ data.confirmKey | translate }}
</button> </button>

View File

@ -1,22 +1,28 @@
<cnsl-refresh-table [hideRefresh]="true" [loading]="loading$ | async" (refreshed)="refreshPage()" <cnsl-refresh-table
[dataSize]="dataSource?.data?.length ?? 0" [timestamp]="actionsResult?.details?.viewTimestamp" [hideRefresh]="true"
[selection]="selection"> [loading]="loading$ | async"
(refreshed)="refreshPage()"
[dataSize]="dataSource?.data?.length ?? 0"
[timestamp]="actionsResult?.details?.viewTimestamp"
[selection]="selection"
>
<div actions *ngIf="selection.isEmpty()"> <div actions *ngIf="selection.isEmpty()">
<a class="cnsl-action-button" color="primary" mat-raised-button (click)="openAddAction()"> <a class="cnsl-action-button" color="primary" mat-raised-button (click)="openAddAction()">
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }} <mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
</a> </a>
</div> </div>
<div actions *ngIf="!selection.isEmpty()"> <div actions *ngIf="!selection.isEmpty()">
<button class="margin-right action-state-btn cnsl-action-button bg-state inactive" mat-raised-button <button
(click)="deactivateSelection()"> class="margin-right action-state-btn cnsl-action-button bg-state inactive"
mat-raised-button
(click)="deactivateSelection()"
>
<span>{{ 'ACTIONS.DEACTIVATE' | translate }}</span> <span>{{ 'ACTIONS.DEACTIVATE' | translate }}</span>
<cnsl-action-keys (actionTriggered)="deactivateSelection()" [type]="ActionKeysType.DEACTIVATE"> <cnsl-action-keys (actionTriggered)="deactivateSelection()" [type]="ActionKeysType.DEACTIVATE"> </cnsl-action-keys>
</cnsl-action-keys>
</button> </button>
<button class="action-state-btn cnsl-action-button bg-state active" mat-raised-button (click)="activateSelection()"> <button class="action-state-btn cnsl-action-button bg-state active" mat-raised-button (click)="activateSelection()">
<span>{{ 'ACTIONS.REACTIVATE' | translate }}</span> <span>{{ 'ACTIONS.REACTIVATE' | translate }}</span>
<cnsl-action-keys (actionTriggered)="activateSelection()" [type]="ActionKeysType.REACTIVATE"> <cnsl-action-keys (actionTriggered)="activateSelection()" [type]="ActionKeysType.REACTIVATE"> </cnsl-action-keys>
</cnsl-action-keys>
</button> </button>
</div> </div>
@ -24,48 +30,61 @@
<table class="table" mat-table [dataSource]="dataSource"> <table class="table" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="select"> <ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef class="action-select-cell"> <th mat-header-cell *matHeaderCellDef class="action-select-cell">
<mat-checkbox color="primary" (change)="$event ? masterToggle() : null" <mat-checkbox
color="primary"
(change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()" [checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()"> [indeterminate]="selection.hasValue() && !isAllSelected()"
>
</mat-checkbox> </mat-checkbox>
</th> </th>
<td mat-cell *matCellDef="let key" class="action-select-cell"> <td mat-cell *matCellDef="let key" class="action-select-cell">
<mat-checkbox color="primary" (click)="$event.stopPropagation()" <mat-checkbox
(change)="$event ? selection.toggle(key) : null" [checked]="selection.isSelected(key)"> color="primary"
(click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(key) : null"
[checked]="selection.isSelected(key)"
>
</mat-checkbox> </mat-checkbox>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="id"> <ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef> {{ 'FLOWS.ID' | translate }} </th> <th mat-header-cell *matHeaderCellDef>{{ 'FLOWS.ID' | translate }}</th>
<td mat-cell *matCellDef="let action" class="pointer"> {{ action?.id }} </td> <td mat-cell *matCellDef="let action" class="pointer">{{ action?.id }}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'FLOWS.NAME' | translate }} </th> <th mat-header-cell *matHeaderCellDef>{{ 'FLOWS.NAME' | translate }}</th>
<td mat-cell *matCellDef="let action" class="pointer"> {{ action?.name }} </td> <td mat-cell *matCellDef="let action" class="pointer">{{ action?.name }}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="state"> <ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'FLOWS.STATE' | translate }} </th> <th mat-header-cell *matHeaderCellDef>{{ 'FLOWS.STATE' | translate }}</th>
<td mat-cell *matCellDef="let action" class="pointer"> <td mat-cell *matCellDef="let action" class="pointer">
<span class="state" <span
[ngClass]="{'active': action.state === ActionState.ACTION_STATE_ACTIVE,'inactive': action.state === ActionState.ACTION_STATE_INACTIVE }"> class="state"
{{'FLOWS.STATES.'+action.state | translate}}</span> [ngClass]="{
active: action.state === ActionState.ACTION_STATE_ACTIVE,
inactive: action.state === ActionState.ACTION_STATE_INACTIVE
}"
>
{{ 'FLOWS.STATES.' + action.state | translate }}</span
>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="timeout"> <ng-container matColumnDef="timeout">
<th mat-header-cell *matHeaderCellDef> {{ 'FLOWS.TIMEOUT' | translate }} </th> <th mat-header-cell *matHeaderCellDef>{{ 'FLOWS.TIMEOUT' | translate }}</th>
<td mat-cell *matCellDef="let key" class="pointer"> <td mat-cell *matCellDef="let key" class="pointer">
{{key.timeout | durationToSeconds}} {{ key.timeout | durationToSeconds }}
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="allowedToFail"> <ng-container matColumnDef="allowedToFail">
<th mat-header-cell *matHeaderCellDef> {{ 'FLOWS.ALLOWEDTOFAIL' | translate }} </th> <th mat-header-cell *matHeaderCellDef>{{ 'FLOWS.ALLOWEDTOFAIL' | translate }}</th>
<td mat-cell *matCellDef="let action" class="pointer"> <td mat-cell *matCellDef="let action" class="pointer">
{{action.allowedToFail}} {{ action.allowedToFail }}
</td> </td>
</ng-container> </ng-container>
@ -73,8 +92,14 @@
<th mat-header-cell *matHeaderCellDef></th> <th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let action" class="pointer"> <td mat-cell *matCellDef="let action" class="pointer">
<cnsl-table-actions> <cnsl-table-actions>
<button [disabled]="(['action.write'] | hasRole | async) === false" actions <button
matTooltip="{{'ACTIONS.REMOVE' | translate}}" color="warn" (click)="deleteAction(action)" mat-icon-button> [disabled]="(['action.write'] | hasRole | async) === false"
actions
matTooltip="{{ 'ACTIONS.REMOVE' | translate }}"
color="warn"
(click)="$event.stopPropagation(); deleteAction(action)"
mat-icon-button
>
<i class="las la-trash"></i> <i class="las la-trash"></i>
</button> </button>
</cnsl-table-actions> </cnsl-table-actions>
@ -82,12 +107,17 @@
</ng-container> </ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let action; columns: displayedColumns;" (click)="openDialog(action)"> <tr class="highlight" mat-row *matRowDef="let action; columns: displayedColumns" (click)="openDialog(action)"></tr>
</tr>
</table> </table>
</div> </div>
<cnsl-paginator #paginator class="paginator" [timestamp]="actionsResult?.details?.viewTimestamp" <cnsl-paginator
[length]="actionsResult?.details?.totalResult || 0" [pageSize]="20" [pageSizeOptions]="[10, 20, 50, 100]" #paginator
(page)="changePage($event)"></cnsl-paginator> class="paginator"
</cnsl-refresh-table> [timestamp]="actionsResult?.details?.viewTimestamp"
[length]="actionsResult?.details?.totalResult || 0"
[pageSize]="20"
[pageSizeOptions]="[10, 20, 50, 100]"
(page)="changePage($event)"
></cnsl-paginator>
</cnsl-refresh-table>

View File

@ -1,32 +1,33 @@
<div class="iam-top"> <cnsl-top-view
<div class="max-width-container"> [hasBackButton]="false"
<div class="iam-top-row"> title="{{ 'IAM.TITLE' | translate }}"
<div> sub="{{ 'IAM.DESCRIPTION' | translate }}"
<div class="iam-title-row"> [isActive]="instance?.state === State.STATE_RUNNING"
<h1 class="iam-title">{{ 'IAM.TITLE' | translate }}</h1> [isInactive]="instance?.state === State.STATE_STOPPED || instance?.state === State.STATE_STOPPING"
</div> [hasContributors]="true"
<p class="iam-sub cnsl-secondary-text">{{ 'IAM.DESCRIPTION' | translate }}</p> stateTooltip="{{ 'INSTANCE.STATE.' + instance?.state | translate }}"
</div> >
<span class="fill-space"></span> <cnsl-contributors
<cnsl-contributors topContributors
[totalResult]="totalMemberResult" [totalResult]="totalMemberResult"
[loading]="loading$ | async" [loading]="loading$ | async"
[membersSubject]="membersSubject" [membersSubject]="membersSubject"
title="{{ 'PROJECT.MEMBER.TITLE' | translate }}" title="{{ 'PROJECT.MEMBER.TITLE' | translate }}"
description="{{ 'PROJECT.MEMBER.TITLEDESC' | translate }}" description="{{ 'PROJECT.MEMBER.TITLEDESC' | translate }}"
(addClicked)="openAddMember()" (addClicked)="openAddMember()"
(showDetailClicked)="showDetail()" (showDetailClicked)="showDetail()"
(refreshClicked)="loadMembers()" (refreshClicked)="loadMembers()"
[disabled]="(['iam.member.write'] | hasRole | async) === false" [disabled]="(['iam.member.write'] | hasRole | async) === false"
> >
</cnsl-contributors> </cnsl-contributors>
</div>
</div>
</div>
<div class="max-width-container">
<h2 class="org-table-title">{{ 'ORG.LIST.TITLE' | translate }}</h2>
<p class="org-table-desc cnsl-secondary-text">{{ 'ORG.LIST.DESCRIPTION' | translate }}</p> <cnsl-info-row topContent *ngIf="instance" [instance]="instance"></cnsl-info-row>
</cnsl-top-view>
<div class="max-width-container">
<h2 class="instance-table-title">{{ 'ORG.LIST.TITLE' | translate }}</h2>
<p class="instance-table-desc cnsl-secondary-text">{{ 'ORG.LIST.DESCRIPTION' | translate }}</p>
<cnsl-org-table></cnsl-org-table> <cnsl-org-table></cnsl-org-table>

View File

@ -4,55 +4,15 @@
$foreground: map-get($theme, foreground); $foreground: map-get($theme, foreground);
$is-dark-theme: map-get($theme, is-dark); $is-dark-theme: map-get($theme, is-dark);
$background: map-get($theme, background); $background: map-get($theme, background);
.iam-top {
border-bottom: 1px solid map-get($foreground, divider);
margin: 0 -2rem;
padding: 2rem 2rem 1rem 2rem;
background: map-get($background, metadata-section);
@media only screen and (max-width: 500px) {
margin: 0 -1rem;
}
.iam-top-row {
display: flex;
align-items: center;
padding-bottom: 1rem;
.iam-title-row {
display: flex;
align-items: center;
.iam-title {
margin: 0;
margin-right: 0.5rem;
}
}
.iam-sub {
margin: 1rem 0 0 0;
font-size: 14px;
}
.iam-top-desc {
font-size: 14px;
}
.fill-space {
flex: 1;
}
}
}
} }
.org-table-title { .instance-table-title {
font-size: 1.2rem; font-size: 1.2rem;
letter-spacing: 0.05em; letter-spacing: 0.05em;
text-transform: uppercase; text-transform: uppercase;
margin-top: 2rem; margin-top: 2rem;
} }
.org-table-desc { .instance-table-desc {
font-size: 14px; font-size: 14px;
} }

View File

@ -5,6 +5,7 @@ import { BehaviorSubject, from, Observable, of } from 'rxjs';
import { catchError, finalize, map } from 'rxjs/operators'; import { catchError, finalize, map } from 'rxjs/operators';
import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component'; import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component';
import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-component-types.enum'; import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-component-types.enum';
import { InstanceDetail, State } from 'src/app/proto/generated/zitadel/instance_pb';
import { Member } from 'src/app/proto/generated/zitadel/member_pb'; import { Member } from 'src/app/proto/generated/zitadel/member_pb';
import { User } from 'src/app/proto/generated/zitadel/user_pb'; import { User } from 'src/app/proto/generated/zitadel/user_pb';
import { AdminService } from 'src/app/services/admin.service'; import { AdminService } from 'src/app/services/admin.service';
@ -17,12 +18,13 @@ import { ToastService } from 'src/app/services/toast.service';
styleUrls: ['./instance.component.scss'], styleUrls: ['./instance.component.scss'],
}) })
export class InstanceComponent { export class InstanceComponent {
public instance!: InstanceDetail.AsObject;
public PolicyComponentServiceType: any = PolicyComponentServiceType; public PolicyComponentServiceType: any = PolicyComponentServiceType;
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable(); public loading$: Observable<boolean> = this.loadingSubject.asObservable();
public totalMemberResult: number = 0; public totalMemberResult: number = 0;
public membersSubject: BehaviorSubject<Member.AsObject[]> = new BehaviorSubject<Member.AsObject[]>([]); public membersSubject: BehaviorSubject<Member.AsObject[]> = new BehaviorSubject<Member.AsObject[]>([]);
public State: any = State;
constructor( constructor(
public adminService: AdminService, public adminService: AdminService,
private dialog: MatDialog, private dialog: MatDialog,
@ -39,6 +41,17 @@ export class InstanceComponent {
}); });
breadcrumbService.setBreadcrumb([instanceBread]); breadcrumbService.setBreadcrumb([instanceBread]);
this.adminService
.getMyInstance()
.then((instanceResp) => {
if (instanceResp.instance) {
this.instance = instanceResp.instance;
}
})
.catch((error) => {
this.toast.showError(error);
});
} }
public loadMembers(): void { public loadMembers(): void {

View File

@ -16,12 +16,14 @@ import { HasRoleModule } from 'src/app/directives/has-role/has-role.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';
import { InfoRowModule } from 'src/app/modules/info-row/info-row.module';
import { InputModule } from 'src/app/modules/input/input.module'; import { InputModule } from 'src/app/modules/input/input.module';
import { MetaLayoutModule } from 'src/app/modules/meta-layout/meta-layout.module'; import { MetaLayoutModule } from 'src/app/modules/meta-layout/meta-layout.module';
import { OrgTableModule } from 'src/app/modules/org-table/org-table.module'; import { OrgTableModule } from 'src/app/modules/org-table/org-table.module';
import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module'; import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module';
import { SettingsGridModule } from 'src/app/modules/settings-grid/settings-grid.module'; import { SettingsGridModule } from 'src/app/modules/settings-grid/settings-grid.module';
import { SharedModule } from 'src/app/modules/shared/shared.module'; import { SharedModule } from 'src/app/modules/shared/shared.module';
import { TopViewModule } from 'src/app/modules/top-view/top-view.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module'; import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module'; import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module';
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module'; import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module';
@ -43,7 +45,9 @@ import { InstanceComponent } from './instance.component';
MatCheckboxModule, MatCheckboxModule,
MetaLayoutModule, MetaLayoutModule,
MatIconModule, MatIconModule,
TopViewModule,
MatTableModule, MatTableModule,
InfoRowModule,
InputModule, InputModule,
MatSortModule, MatSortModule,
MatTooltipModule, MatTooltipModule,

View File

@ -5,7 +5,25 @@
[isInactive]="org?.state === OrgState.ORG_STATE_INACTIVE" [isInactive]="org?.state === OrgState.ORG_STATE_INACTIVE"
[hasContributors]="true" [hasContributors]="true"
stateTooltip="{{ 'ORG.STATE.' + org?.state | translate }}" stateTooltip="{{ 'ORG.STATE.' + org?.state | translate }}"
[hasActions]="['org.write:' + org?.id, 'org.write$'] | hasRole | async"
> >
<ng-template topActions cnslHasRole [hasRole]="['org.write:' + org?.id, 'org.write$']">
<button
mat-menu-item
*ngIf="org?.state === OrgState.ORG_STATE_ACTIVE"
(click)="changeState(OrgState.ORG_STATE_INACTIVE)"
>
{{ 'ORG.PAGES.DEACTIVATE' | translate }}
</button>
<button
mat-menu-item
*ngIf="org?.state === OrgState.ORG_STATE_INACTIVE"
(click)="changeState(OrgState.ORG_STATE_ACTIVE)"
>
{{ 'ORG.PAGES.REACTIVATE' | translate }}
</button>
</ng-template>
<cnsl-contributors <cnsl-contributors
topContributors topContributors
[totalResult]="totalMemberResult" [totalResult]="totalMemberResult"

View File

@ -7,6 +7,7 @@ import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-m
import { ChangeType } from 'src/app/modules/changes/changes.component'; import { ChangeType } from 'src/app/modules/changes/changes.component';
import { InfoSectionType } from 'src/app/modules/info-section/info-section.component'; import { InfoSectionType } from 'src/app/modules/info-section/info-section.component';
import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-component-types.enum'; import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-component-types.enum';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
import { Member } from 'src/app/proto/generated/zitadel/member_pb'; import { Member } from 'src/app/proto/generated/zitadel/member_pb';
import { Org, OrgState } from 'src/app/proto/generated/zitadel/org_pb'; import { Org, OrgState } from 'src/app/proto/generated/zitadel/org_pb';
import { User } from 'src/app/proto/generated/zitadel/user_pb'; import { User } from 'src/app/proto/generated/zitadel/user_pb';
@ -62,6 +63,56 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
this.destroy$.complete(); this.destroy$.complete();
} }
public changeState(newState: OrgState): void {
if (newState === OrgState.ORG_STATE_ACTIVE) {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.REACTIVATE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'ORG.DIALOG.REACTIVATE.TITLE',
descriptionKey: 'ORG.DIALOG.REACTIVATE.DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
this.mgmtService
.reactivateOrg()
.then(() => {
this.toast.showInfo('ORG.TOAST.REACTIVATED', true);
this.org.state = OrgState.ORG_STATE_ACTIVE;
})
.catch((error) => {
this.toast.showError(error);
});
}
});
} else if (newState === OrgState.ORG_STATE_INACTIVE) {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DEACTIVATE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'ORG.DIALOG.DEACTIVATE.TITLE',
descriptionKey: 'ORG.DIALOG.DEACTIVATE.DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
this.mgmtService
.deactivateOrg()
.then(() => {
this.toast.showInfo('ORG.TOAST.DEACTIVATED', true);
this.org.state = OrgState.ORG_STATE_INACTIVE;
})
.catch((error) => {
this.toast.showError(error);
});
}
});
}
}
private async getData(): Promise<void> { private async getData(): Promise<void> {
this.mgmtService this.mgmtService
.getMyOrg() .getMyOrg()

View File

@ -40,7 +40,7 @@
[disabled]="firstFormGroup.invalid" [disabled]="firstFormGroup.invalid"
color="primary" color="primary"
matStepperNext matStepperNext
[attr.data-e2e]="'continue-button-nameandtype'" data-e2e="continue-button-nameandtype"
> >
{{ 'ACTIONS.CONTINUE' | translate }} {{ 'ACTIONS.CONTINUE' | translate }}
</button> </button>
@ -72,7 +72,7 @@
color="primary" color="primary"
[disabled]="secondFormGroup.invalid" [disabled]="secondFormGroup.invalid"
matStepperNext matStepperNext
[attr.data-e2e]="'continue-button-authmethod'" data-e2e="continue-button-authmethod"
> >
{{ 'ACTIONS.CONTINUE' | translate }} {{ 'ACTIONS.CONTINUE' | translate }}
</button> </button>
@ -130,7 +130,7 @@
<div class="app-create-actions"> <div class="app-create-actions">
<button mat-stroked-button class="bck-button" matStepperPrevious>{{ 'ACTIONS.BACK' | translate }}</button> <button mat-stroked-button class="bck-button" matStepperPrevious>{{ 'ACTIONS.BACK' | translate }}</button>
<button mat-raised-button color="primary" matStepperNext [attr.data-e2e]="'continue-button-redirecturis'"> <button mat-raised-button color="primary" matStepperNext data-e2e="continue-button-redirecturis">
{{ 'ACTIONS.CONTINUE' | translate }} {{ 'ACTIONS.CONTINUE' | translate }}
</button> </button>
</div> </div>
@ -234,13 +234,7 @@
<div class="app-create-actions"> <div class="app-create-actions">
<button mat-stroked-button matStepperPrevious class="bck-button">{{ 'ACTIONS.BACK' | translate }}</button> <button mat-stroked-button matStepperPrevious class="bck-button">{{ 'ACTIONS.BACK' | translate }}</button>
<button <button mat-raised-button class="create-button" color="primary" (click)="createApp()" data-e2e="create-button">
mat-raised-button
class="create-button"
color="primary"
(click)="createApp()"
[attr.data-e2e]="'create-button'"
>
{{ 'ACTIONS.CREATE' | translate }} {{ 'ACTIONS.CREATE' | translate }}
</button> </button>
</div> </div>
@ -248,7 +242,7 @@
</mat-horizontal-stepper> </mat-horizontal-stepper>
<div *ngIf="devmode" class="dev"> <div *ngIf="devmode" class="dev">
<form [formGroup]="form" (ngSubmit)="createApp()" [attr.data-e2e]="'create-app-wizzard-3'"> <form [formGroup]="form" (ngSubmit)="createApp()" data-e2e="create-app-wizzard-3">
<div class="content"> <div class="content">
<cnsl-form-field class="formfield"> <cnsl-form-field class="formfield">
<cnsl-label>{{ 'APP.NAME' | translate }}</cnsl-label> <cnsl-label>{{ 'APP.NAME' | translate }}</cnsl-label>

View File

@ -1,33 +1,53 @@
<h1 mat-dialog-title> <h1 mat-dialog-title>
<span class="title">{{'APP.OIDC.CLIENTSECRET' | translate}}</span> <span class="title">{{ 'APP.OIDC.CLIENTSECRET' | translate }}</span>
</h1> </h1>
<p class="desc cnsl-secondary-text">{{'APP.OIDC.CLIENTSECRET_DESCRIPTION' | translate}}</p> <p class="desc cnsl-secondary-text">{{ 'APP.OIDC.CLIENTSECRET_DESCRIPTION' | translate }}</p>
<div mat-dialog-content> <div mat-dialog-content>
<div class="flex" *ngIf="data.clientId"> <div class="flex" *ngIf="data.clientId">
<span class="overflow-auto"><span class="desc">ClientId:</span> {{data.clientId}}</span> <span class="overflow-auto"><span class="desc">ClientId:</span> {{ data.clientId }}</span>
<button color="primary" [disabled]="copied === data.clientId" matTooltip="copy to clipboard" cnslCopyToClipboard <button
[valueToCopy]="data.clientId" (copiedValue)="this.copied = $event" mat-icon-button> color="primary"
[disabled]="copied === data.clientId"
matTooltip="copy to clipboard"
cnslCopyToClipboard
[valueToCopy]="data.clientId"
(copiedValue)="this.copied = $event"
mat-icon-button
>
<i *ngIf="copied !== data.clientId" class="las la-clipboard"></i> <i *ngIf="copied !== data.clientId" class="las la-clipboard"></i>
<i *ngIf="copied === data.clientId" class="las la-clipboard-check"></i> <i *ngIf="copied === data.clientId" class="las la-clipboard-check"></i>
</button> </button>
</div> </div>
<div *ngIf="data.clientSecret; else showNoSecretInfo" class="flex"> <div *ngIf="data.clientSecret; else showNoSecretInfo" class="flex">
<span class="overflow-auto"><span class="desc cnsl-secondary-text">ClientSecret:</span> {{data.clientSecret}}</span> <span class="overflow-auto"><span class="desc cnsl-secondary-text">ClientSecret:</span> {{ data.clientSecret }}</span>
<button color="primary" [disabled]="copied === data.clientSecret" matTooltip="copy to clipboard" cnslCopyToClipboard <button
[valueToCopy]="data.clientSecret" (copiedValue)="this.copied = $event" mat-icon-button> color="primary"
[disabled]="copied === data.clientSecret"
matTooltip="copy to clipboard"
cnslCopyToClipboard
[valueToCopy]="data.clientSecret"
(copiedValue)="this.copied = $event"
mat-icon-button
>
<i *ngIf="copied !== data.clientSecret" class="las la-clipboard"></i> <i *ngIf="copied !== data.clientSecret" class="las la-clipboard"></i>
<i *ngIf="copied === data.clientSecret" class="las la-clipboard-check"></i> <i *ngIf="copied === data.clientSecret" class="las la-clipboard-check"></i>
</button> </button>
</div> </div>
<ng-template #showNoSecretInfo> <ng-template #showNoSecretInfo>
<cnsl-info-section>{{'APP.OIDC.CLIENTSECRET_NOSECRET' | translate}}</cnsl-info-section> <cnsl-info-section>{{ 'APP.OIDC.CLIENTSECRET_NOSECRET' | translate }}</cnsl-info-section>
</ng-template> </ng-template>
</div> </div>
<div mat-dialog-actions class="action"> <div mat-dialog-actions class="action">
<button cdkFocusInitial color="primary" mat-raised-button class="ok-button" (click)="closeDialog()" <button
[attr.data-e2e]="'close-dialog'"> cdkFocusInitial
{{'ACTIONS.CLOSE' | translate}} color="primary"
mat-raised-button
class="ok-button"
(click)="closeDialog()"
data-e2e="close-dialog"
>
{{ 'ACTIONS.CLOSE' | translate }}
</button> </button>
</div> </div>

View File

@ -1,4 +1,4 @@
<form class="redirect-uris-form" (ngSubmit)="add(redInput)" [attr.data-e2e]="'redirect-uris'"> <form class="redirect-uris-form" (ngSubmit)="add(redInput)" data-e2e="redirect-uris">
<cnsl-form-field class="formfield"> <cnsl-form-field class="formfield">
<cnsl-label>{{ title }}</cnsl-label> <cnsl-label>{{ title }}</cnsl-label>

View File

@ -80,7 +80,7 @@
min-width: 320px; min-width: 320px;
.formfield { .formfield {
flex: 1; width: 500px;
} }
button { button {

View File

@ -54,7 +54,7 @@
</ng-template> </ng-template>
<div metainfo> <div metainfo>
<cnsl-changes *ngIf="project" [changeType]="ChangeType.PROJECT" [id]="project.projectId"></cnsl-changes> <cnsl-changes *ngIf="project" [changeType]="ChangeType.PROJECT_GRANT" [id]="project.projectId" [secId]="project.grantId"></cnsl-changes>
</div> </div>
</cnsl-meta-layout> </cnsl-meta-layout>
</div> </div>

View File

@ -12,7 +12,7 @@
<div <div
[routerLink]="['/projects', projectId, 'apps', app.id]" [routerLink]="['/projects', projectId, 'apps', app.id]"
[attr.data-e2e]="'app-card'" data-e2e="app-card"
class="app-wrap" class="app-wrap"
*ngFor="let app of appsSubject | async" *ngFor="let app of appsSubject | async"
matTooltip="{{ 'ACTIONS.EDIT' | translate }}" matTooltip="{{ 'ACTIONS.EDIT' | translate }}"
@ -37,7 +37,7 @@
<div <div
class="app-wrap" class="app-wrap"
*ngIf="!disabled" *ngIf="!disabled"
[attr.data-e2e]="'app-card-add'" data-e2e="app-card-add"
[routerLink]="['/projects', projectId, 'apps', 'create']" [routerLink]="['/projects', projectId, 'apps', 'create']"
> >
<cnsl-app-card class="grid-card" matRipple [type]="OIDCAppType.ADD"> <cnsl-app-card class="grid-card" matRipple [type]="OIDCAppType.ADD">

View File

@ -46,7 +46,7 @@
class="continue-button" class="continue-button"
[disabled]="formArray.invalid" [disabled]="formArray.invalid"
type="submit" type="submit"
[attr.data-e2e]="'save-button'" data-e2e="save-button"
> >
{{ 'ACTIONS.SAVE' | translate }} {{ 'ACTIONS.SAVE' | translate }}
</button> </button>

View File

@ -20,7 +20,7 @@
[disabled]="!project.name" [disabled]="!project.name"
cdkFocusInitial cdkFocusInitial
type="submit" type="submit"
[attr.data-e2e]="'continue-button'" data-e2e="continue-button"
> >
{{ 'ACTIONS.CONTINUE' | translate }} {{ 'ACTIONS.CONTINUE' | translate }}
</button> </button>

View File

@ -47,7 +47,7 @@
*ngFor="let item of notPinned; index as i" *ngFor="let item of notPinned; index as i"
(click)="navigateToProject(type, $any(item), $event)" (click)="navigateToProject(type, $any(item), $event)"
[ngClass]="{ inactive: item.state !== ProjectState.PROJECT_STATE_ACTIVE }" [ngClass]="{ inactive: item.state !== ProjectState.PROJECT_STATE_ACTIVE }"
[attr.data-e2e]="'grid-card'" data-e2e="grid-card"
> >
<div class="text-part"> <div class="text-part">
<span *ngIf="item.details && item.details.changeDate" class="top cnsl-secondary-text" <span *ngIf="item.details && item.details.changeDate" class="top cnsl-secondary-text"
@ -106,7 +106,7 @@
(click)="deleteProject($event, key)" (click)="deleteProject($event, key)"
class="delete-button" class="delete-button"
mat-icon-button mat-icon-button
[attr.data-e2e]="'delete-project-button'" data-e2e="delete-project-button"
> >
<i class="las la-trash"></i> <i class="las la-trash"></i>
</button> </button>

View File

@ -1,81 +1,136 @@
<div class="projects-table-wrapper" *ngIf="projectType$ | async as type"> <div class="projects-table-wrapper" *ngIf="projectType$ | async as type">
<cnsl-refresh-table [hideRefresh]="true" (refreshed)="refreshPage(type)" [dataSize]="totalResult" <cnsl-refresh-table
[timestamp]="viewTimestamp" [selection]="selection" [loading]="loading$ | async"> [hideRefresh]="true"
(refreshed)="refreshPage(type)"
<cnsl-filter-project actions *ngIf="!selection.hasValue()" (filterChanged)="applySearchQuery(type, $any($event))" [dataSize]="totalResult"
(filterOpen)="filterOpen = $event"></cnsl-filter-project> [timestamp]="viewTimestamp"
[selection]="selection"
[loading]="loading$ | async"
>
<cnsl-filter-project
actions
*ngIf="!selection.hasValue()"
(filterChanged)="applySearchQuery(type, $any($event))"
(filterOpen)="filterOpen = $event"
></cnsl-filter-project>
<ng-template actions cnslHasRole [hasRole]="['project.create']"> <ng-template actions cnslHasRole [hasRole]="['project.create']">
<a *ngIf="type === ProjectType.PROJECTTYPE_OWNED" [routerLink]="[ '/projects', 'create']" color="primary" <a
mat-raised-button class="cnsl-action-button"> *ngIf="type === ProjectType.PROJECTTYPE_OWNED"
[routerLink]="['/projects', 'create']"
color="primary"
mat-raised-button
class="cnsl-action-button"
>
<mat-icon class="icon">add</mat-icon> <mat-icon class="icon">add</mat-icon>
<span>{{ 'ACTIONS.NEW' | translate }}</span> <span>{{ 'ACTIONS.NEW' | translate }}</span>
<cnsl-action-keys (actionTriggered)="gotoRouterLink([ '/projects', 'create'])"> <cnsl-action-keys (actionTriggered)="gotoRouterLink(['/projects', 'create'])"> </cnsl-action-keys>
</cnsl-action-keys>
</a> </a>
</ng-template> </ng-template>
<div class="table-wrapper"> <div class="table-wrapper">
<table class="table" mat-table [dataSource]="dataSource"> <table class="table" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="select"> <ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef> <th class="selection" mat-header-cell *matHeaderCellDef>
<mat-checkbox color="primary" (change)="$event ? masterToggle() : null" <mat-checkbox
color="primary"
(change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()" [checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()"> [indeterminate]="selection.hasValue() && !isAllSelected()"
>
</mat-checkbox> </mat-checkbox>
</th> </th>
<td class="selection" mat-cell *matCellDef="let row"> <td class="selection" mat-cell *matCellDef="let row">
<mat-checkbox color="primary" (click)="$event.stopPropagation()" <mat-checkbox
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)"> color="primary"
(click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null"
[checked]="selection.isSelected(row)"
>
</mat-checkbox> </mat-checkbox>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.NAME' | translate }} </th> <th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.NAME' | translate }}</th>
<td class="pointer" <td
[routerLink]="type === ProjectType.PROJECTTYPE_OWNED ? ['/projects', project.id] : ['/granted-projects', project.projectId, 'grant', project.grantId]" class="pointer"
mat-cell *matCellDef="let project"> [routerLink]="
<span *ngIf="project.name">{{project.name}}</span> type === ProjectType.PROJECTTYPE_OWNED
<span *ngIf="project.projectName">{{project.projectName}}</span> ? ['/projects', project.id]
: ['/granted-projects', project.projectId, 'grant', project.grantId]
"
mat-cell
*matCellDef="let project"
>
<span *ngIf="project.name">{{ project.name }}</span>
<span *ngIf="project.projectName">{{ project.projectName }}</span>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="projectOwnerName"> <ng-container matColumnDef="projectOwnerName">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.RESOURCEOWNER' | translate }} </th> <th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.TABLE.RESOURCEOWNER' | translate }}</th>
<td class="pointer" mat-cell *matCellDef="let project"> <td class="pointer" mat-cell *matCellDef="let project">
{{project.projectOwnerName}} </td> {{ project.projectOwnerName }}
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="state"> <ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.STATE' | translate }} </th> <th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.TABLE.STATE' | translate }}</th>
<td class="pointer" <td
[routerLink]="type === ProjectType.PROJECTTYPE_OWNED ? ['/projects', project.id] : ['/granted-projects', project.projectId, 'grant', project.grantId]" class="pointer"
mat-cell *matCellDef="let project"> [routerLink]="
<span class="state" type === ProjectType.PROJECTTYPE_OWNED
[ngClass]="{'active': project.state === ProjectState.PROJECT_STATE_ACTIVE, 'inactive': project.state === ProjectState.PROJECT_STATE_INACTIVE}" ? ['/projects', project.id]
*ngIf="project.state">{{'PROJECT.STATE.'+project.state | translate}}</span> : ['/granted-projects', project.projectId, 'grant', project.grantId]
"
mat-cell
*matCellDef="let project"
>
<span
class="state"
[ngClass]="{
active: project.state === ProjectState.PROJECT_STATE_ACTIVE,
inactive: project.state === ProjectState.PROJECT_STATE_INACTIVE
}"
*ngIf="project.state"
>{{ 'PROJECT.STATE.' + project.state | translate }}</span
>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="creationDate"> <ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CREATIONDATE' | translate }} </th> <th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.TABLE.CREATIONDATE' | translate }}</th>
<td class="pointer" <td
[routerLink]="type === ProjectType.PROJECTTYPE_OWNED ? ['/projects', project.id] : ['/granted-projects', project.projectId, 'grant', project.grantId]" class="pointer"
mat-cell *matCellDef="let project"> [routerLink]="
<span *ngIf="project.details.creationDate">{{project.details.creationDate | timestampToDate | type === ProjectType.PROJECTTYPE_OWNED
localizedDate: 'EEE dd. MMM, HH:mm'}}</span> ? ['/projects', project.id]
: ['/granted-projects', project.projectId, 'grant', project.grantId]
"
mat-cell
*matCellDef="let project"
>
<span *ngIf="project.details.creationDate">{{
project.details.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
}}</span>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="changeDate"> <ng-container matColumnDef="changeDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CHANGEDATE' | translate }} </th> <th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.TABLE.CHANGEDATE' | translate }}</th>
<td class="pointer" <td
[routerLink]="type === ProjectType.PROJECTTYPE_OWNED ? ['/projects', project.id] : ['/granted-projects', project.projectId, 'grant', project.grantId]" class="pointer"
mat-cell *matCellDef="let project"> [routerLink]="
<span *ngIf="project.details.changeDate">{{project.details.changeDate | timestampToDate | type === ProjectType.PROJECTTYPE_OWNED
localizedDate: 'EEE dd. MMM, HH:mm'}}</span> ? ['/projects', project.id]
: ['/granted-projects', project.projectId, 'grant', project.grantId]
"
mat-cell
*matCellDef="let project"
>
<span *ngIf="project.details.changeDate">{{
project.details.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
}}</span>
</td> </td>
</ng-container> </ng-container>
@ -83,8 +138,15 @@
<th mat-header-cell *matHeaderCellDef></th> <th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let project"> <td mat-cell *matCellDef="let project">
<cnsl-table-actions> <cnsl-table-actions>
<button actions *ngIf="project.id !== zitadelProjectId" color="warn" mat-icon-button <button
matTooltip="{{'ACTIONS.DELETE' | translate}}" (click)="deleteProject(project.id, project.name)"> actions
*ngIf="project.id !== zitadelProjectId"
color="warn"
mat-icon-button
matTooltip="{{ 'ACTIONS.DELETE' | translate }}"
(click)="deleteProject(project.id, project.name)"
data-e2e="delete-project-button"
>
<i class="las la-trash"></i> <i class="las la-trash"></i>
</button> </button>
</cnsl-table-actions> </cnsl-table-actions>
@ -92,15 +154,21 @@
</ng-container> </ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table> </table>
</div> </div>
<div *ngIf="(loading$ | async) === false && !dataSource?.data?.length" class="no-content-row"> <div *ngIf="(loading$ | async) === false && !dataSource?.data?.length" class="no-content-row">
<i class="las la-exclamation"></i> <i class="las la-exclamation"></i>
<span>{{'PROJECT.TABLE.EMPTY' | translate}}</span> <span>{{ 'PROJECT.TABLE.EMPTY' | translate }}</span>
</div> </div>
<cnsl-paginator class="paginator" [timestamp]="viewTimestamp" [length]="totalResult" [pageSize]="20" <cnsl-paginator
[pageSizeOptions]="[10, 20, 50, 100]" (page)="changePage(type)"></cnsl-paginator> class="paginator"
[timestamp]="viewTimestamp"
[length]="totalResult"
[pageSize]="20"
[pageSizeOptions]="[10, 20, 50, 100]"
(page)="changePage(type)"
></cnsl-paginator>
</cnsl-refresh-table> </cnsl-refresh-table>
</div> </div>

View File

@ -9,24 +9,23 @@
<p class="sub cnsl-secondary-text max-width-description">{{ 'PROJECT.PAGES.LISTDESCRIPTION' | translate }}</p> <p class="sub cnsl-secondary-text max-width-description">{{ 'PROJECT.PAGES.LISTDESCRIPTION' | translate }}</p>
<div class="projects-controls"> <div class="projects-controls">
<div class="project-type-actions"> <div class="project-toggle-group">
<button <cnsl-nav-toggle
class="type-button" label="{{ 'PROJECT.PAGES.TYPE.OWNED' | translate }}"
[ngClass]="{ active: (projectType$ | async) === ProjectType.PROJECTTYPE_OWNED }" [count]="mgmtService.ownedProjectsCount | async"
(click)="setType(ProjectType.PROJECTTYPE_OWNED)" (clicked)="setType(ProjectType.PROJECTTYPE_OWNED)"
> [active]="(projectType$ | async) === ProjectType.PROJECTTYPE_OWNED"
{{ 'PROJECT.PAGES.TYPE.OWNED' | translate }} ({{ (mgmtService?.ownedProjectsCount | async) ?? 0 }}) ></cnsl-nav-toggle>
</button> <cnsl-nav-toggle
<button label="{{ 'PROJECT.PAGES.TYPE.GRANTED' | translate }}"
class="type-button" [count]="mgmtService.grantedProjectsCount | async"
[ngClass]="{ active: (projectType$ | async) === ProjectType.PROJECTTYPE_GRANTED }" (clicked)="setType(ProjectType.PROJECTTYPE_GRANTED)"
(click)="setType(ProjectType.PROJECTTYPE_GRANTED)" [active]="(projectType$ | async) === ProjectType.PROJECTTYPE_GRANTED"
> ></cnsl-nav-toggle>
{{ 'PROJECT.PAGES.TYPE.GRANTED' | translate }} ({{ (mgmtService?.grantedProjectsCount | async) ?? 0 }})
</button>
</div> </div>
<span class="fill-space"></span> <span class="fill-space"></span>
<button class="grid-btn" (click)="grid = !grid" mat-icon-button [attr.data-e2e]="'toggle-grid'"> <button class="grid-btn" (click)="grid = !grid" mat-icon-button data-e2e="toggle-grid">
<i *ngIf="grid" class="show list view las la-th-list"></i> <i *ngIf="grid" class="show list view las la-th-list"></i>
<i *ngIf="!grid" class="las la-border-all"></i> <i *ngIf="!grid" class="las la-border-all"></i>
</button> </button>

Some files were not shown because too many files have changed in this diff Show More