fix(instance-create): merge back origin/main

This commit is contained in:
Stefan Benz 2022-11-23 15:57:09 +01:00
commit 39660044ad
No known key found for this signature in database
GPG Key ID: 9D2FE4EA50BEFE68
398 changed files with 9137 additions and 10406 deletions

84
.github/ISSUE_TEMPLATE/BUG_REPORT.yaml vendored Normal file
View File

@ -0,0 +1,84 @@
name: Bug Report
description: Create a report to help us improve ZITADEL
title: "[Bug]: "
labels: ["type: bug", "state: triage"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: checkboxes
id: preflight
attributes:
label: Preflight Checklist
options:
- label:
I could not find a solution in the documentation, the existing issues or discussions
required: true
- label:
I have joined the [ZITADEL chat](https://zitadel.com/chat)
- type: dropdown
id: environment
attributes:
label: Environment
description: How do you use ZITADEL?
options:
- ZITADEL Cloud
- Self-hosted
validations:
required: true
- type: textarea
id: description
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: To reproduce
description: Steps to reproduce the behaviour
placeholder: |
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If applicable, add screenshots to help explain your problem.
- type: textarea
id: expected
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen.
- type: input
id: version
attributes:
label: Version
description: Which version of ZITADEL are you using.
- type: textarea
id: os
attributes:
label: Operating System
description: Please complete information about your operating-system, device, browser, etc.
placeholder: |
Device: [e.g. Windows, Mac, iPhone6]
OS: [e.g. iOS8.1, Windows]
Browser [e.g. chrome, stock browser, safari]
Version [e.g. 22]
- type: textarea
id: config
attributes:
label: Relevant Configuration
description: Add any relevant configurations that could help as. Make sure to redact any sensitive information.
- type: textarea
id: additional
attributes:
label: Additional Context
description: Please add any other infos that could be useful.

View File

@ -1,38 +0,0 @@
---
name: "\U0001F41B Bug report"
about: Create a report to help us improve
title: ''
labels: 'state: triage, type: bug'
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@ -6,3 +6,5 @@ contact_links:
- name: ❓ Questions - name: ❓ Questions
url: https://github.com/zitadel/zitadel/discussions/categories/q-a url: https://github.com/zitadel/zitadel/discussions/categories/q-a
about: Ask for help in our Q&A discussions about: Ask for help in our Q&A discussions
- name: 💬 ZITADEL Chat
url: https://zitadel.com/chat

View File

@ -8,17 +8,17 @@ on:
- '**.md' - '**.md'
jobs: jobs:
Test: Build-ZITADEL:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
env: env:
DOCKER_BUILDKIT: 1 DOCKER_BUILDKIT: 1
steps: steps:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v2 uses: actions/setup-go@v3
with: with:
go-version: 1.19 go-version: 1.19
- name: Source checkout - name: Source checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx - name: Set up Docker Buildx
@ -33,6 +33,12 @@ jobs:
version: v1.10.3 version: v1.10.3
- name: Build and Unit Test - name: Build and Unit Test
run: GOOS="linux" GOARCH="amd64" goreleaser build --id prod --snapshot --single-target --rm-dist --output .artifacts/zitadel/zitadel run: GOOS="linux" GOARCH="amd64" goreleaser build --id prod --snapshot --single-target --rm-dist --output .artifacts/zitadel/zitadel
- name: linting
uses: golangci/golangci-lint-action@v3
with:
version: v1.50.1
only-new-issues: true
skip-pkg-cache: true
- name: Publish go coverage - name: Publish go coverage
uses: codecov/codecov-action@v3.1.0 uses: codecov/codecov-action@v3.1.0
with: with:

View File

@ -14,7 +14,7 @@ on:
- '**.md' - '**.md'
jobs: jobs:
Test: Build-ZITADEL:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- run: 'echo "No tests for docs are implemented, yet"' - run: 'echo "No tests for docs are implemented, yet"'

303
.golangci.yaml Normal file
View File

@ -0,0 +1,303 @@
issues:
new: true
fix: false
run:
concurrency: 2
timeout: 10m
go: '1.19'
skip-dirs:
- .artifacts
- .backups
- .codecov
- .github
- .keys
- .vscode
- build
- console
- deploy
- docs
- guides
- internal/api/ui/login/static
- openapi
- proto
- tools
linters:
enable:
# Simple linter to check that your code does not contain non-ASCII identifiers [fast: true, auto-fix: false]
- asciicheck
# checks whether HTTP response body is closed successfully [fast: false, auto-fix: false]
- bodyclose
# check the function whether use a non-inherited context [fast: false, auto-fix: false]
- contextcheck
# Computes and checks the cognitive complexity of functions [fast: true, auto-fix: false]
- gocognit
# Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false]
- unused
# Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: false, auto-fix: false]
- errcheck
# Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`. [fast: false, auto-fix: false]
- errname
# errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. [fast: false, auto-fix: false]
- errorlint
# check exhaustiveness of enum switch statements [fast: false, auto-fix: false]
- exhaustive
# Gci controls golang package import order and makes it always deterministic. [fast: true, auto-fix: false]
- gci
# Provides diagnostics that check for bugs, performance and style issues. [fast: false, auto-fix: false]
- gocritic
# Linter for Go source code that specializes in simplifying a code [fast: false, auto-fix: false]
- gosimple
# Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: false, auto-fix: false]
- govet
# In addition to fixing imports, goimports also formats your code in the same style as gofmt. [fast: true, auto-fix: true]
- goimports
# Detects when assignments to existing variables are not used [fast: true, auto-fix: false]
- ineffassign
# Finds commonly misspelled English words in comments [fast: true, auto-fix: true]
- misspell
# Finds naked returns in functions greater than a specified function length [fast: true, auto-fix: false]
- nakedret
# Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: false, auto-fix: false]
- staticcheck
# Like the front-end of a Go compiler, parses and type-checks Go code [fast: false, auto-fix: false]
- typecheck
# Reports ill-formed or insufficient nolint directives [fast: true, auto-fix: false]
- nolintlint
# Checks for misuse of Sprintf to construct a host with port in a URL.
- nosprintfhostport
# checks whether Err of rows is checked successfully in `sql.Rows` [fast: false, auto-fix: false]
- rowserrcheck
# Checks that sql.Rows and sql.Stmt are closed. [fast: false, auto-fix: false]
- sqlclosecheck
# Remove unnecessary type conversions [fast: false, auto-fix: false]
- unconvert
disable:
# Checks for dangerous unicode character sequences [fast: true, auto-fix: false]
# not needed because github does that out of the box
- bidichk
# containedctx is a linter that detects struct contained context.Context field [fast: true, auto-fix: false]
# using contextcheck which looks more active
- containedctx
# checks function and package cyclomatic complexity [fast: false, auto-fix: false]
# not use because gocognit is used
- cyclop
# The owner seems to have abandoned the linter. Replaced by unused.
# deprecated, replaced by unused
- deadcode
# check declaration order and count of types, constants, variables and functions [fast: true, auto-fix: false]
# FUTURE: IMO it sometimes makes sense to declare consts or types after a func
- decorder
# Go linter that checks if package imports are in a list of acceptable packages [fast: false, auto-fix: false]
# not required because of dependabot
- depguard
# Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false]
# FUTURE: old code is not compatible
- dogsled
# Tool for code clone detection [fast: true, auto-fix: false]
# FUTURE: old code is not compatible
- dupl
# checks for duplicate words in the source code
# not sure if it makes sense
- dupword
# check for two durations multiplied together [fast: false, auto-fix: false]
# FUTURE: checks for accident `1 * time.Second * time.Second`
- durationcheck
# Checks types passed to the json encoding functions. Reports unsupported types and optionally reports occations, where the check for the returned error can be omitted. [fast: false, auto-fix: false]
# FUTURE: use asap, because we use json alot. nice feature is possiblity to check if err check is required
- errchkjson
# execinquery is a linter about query string checker in Query function which reads your Go src files and warning it finds
# FUTURE: might find some errors in sql queries
- execinquery
# Checks if all struct's fields are initialized [fast: false, auto-fix: false]
# deprecated
- exhaustivestruct
# Checks if all structure fields are initialized
# Not all fields have to be initialized
- exhaustruct
# checks for pointers to enclosing loop variables [fast: false, auto-fix: false]
# FUTURE: finds bugs hard to find, could occur much later
- exportloopref
# Forbids identifiers [fast: true, auto-fix: false]
# see no reason. allows to define regexp which are not allowed to use
- forbidigo
# finds forced type assertions [fast: true, auto-fix: false]
# not used because we mostly use `_, _ = a.(int)`
- forcetypeassert
# Tool for detection of long functions [fast: true, auto-fix: false]
# not used because it ignores complexity
- funlen
# check that no global variables exist [fast: true, auto-fix: false]
# We use some global variables which is ok IMO
- gochecknoglobals
# Checks that no init functions are present in Go code [fast: true, auto-fix: false]
# we use inits for the database abstraction
- gochecknoinits
# Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false]
# FUTURE: might be cool to check
- goconst
# Computes and checks the cyclomatic complexity of functions [fast: true, auto-fix: false]
# not used because cyclop also checks complexity of package
- gocyclo
# Check if comments end in a period [fast: true, auto-fix: true]
# FUTURE: checks if comments are written as specified
- godot
# Tool for detection of FIXME, TODO and other comment keywords [fast: true, auto-fix: false]
# FUTURE: maybe makes sense later. IMO some view todos are ok for later tasks.
- godox
# Golang linter to check the errors handling expressions [fast: false, auto-fix: false]
# Not used in favore of errorlint
- goerr113
# Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification [fast: true, auto-fix: true]
# ignored in favor of goimports
- gofmt
# Gofumpt checks whether code was gofumpt-ed. [fast: true, auto-fix: true]
# ignored in favor of goimports
- gofumpt
# Checks is file header matches to pattern [fast: true, auto-fix: false]
# ignored because we don't write licenses as headers
- goheader
#deprecated]: Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes [fast: false, auto-fix: false]
# ignored in favor of goimports
- golint
# An analyzer to detect magic numbers. [fast: true, auto-fix: false]
# FUTURE: not that critical at the moment
- gomnd
# Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. [fast: true, auto-fix: false]
# FUTURE: not a problem at the moment
- gomoddirectives
# Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations. [fast: true, auto-fix: false]
# FUTURE: maybe interesting because of licenses
- gomodguard
# Checks that printf-like functions are named with `f` at the end [fast: true, auto-fix: false]
# FUTURE: not a problem at the moment
- goprintffuncname
# Inspects source code for security problems [fast: false, auto-fix: false]
# TODO: I think it would be more interesting to integrate into gh code scanning: https://github.com/securego/gosec#integrating-with-code-scanning
- gosec
# An analyzer to analyze expression groups. [fast: true, auto-fix: false]
# I think the groups (vars, consts, imports, ...) we have atm are ok
- grouper
# Checks that your code uses short syntax for if-statements whenever possible [fast: true, auto-fix: false]
# Dont't use its deprecated
- ifshort
# Enforces consistent import aliases [fast: false, auto-fix: false]
# FUTURE: aliasing of imports is more or less consistent
- importas
# A linter that checks the number of methods inside an interface.
# No need at the moment, repository abstraction was removed
- interfacebloat
# A linter that suggests interface types
# Don't use it's archived
- interfacer
# Accept Interfaces, Return Concrete Types [fast: false, auto-fix: false]
# FUTURE: check if no interface is returned
- ireturn
# Reports long lines [fast: true, auto-fix: false]
# FUTURE: would make code more readable
- lll
# Checks key valur pairs for common logger libraries (kitlog,klog,logr,zap).
# FUTURE: useable as soon as we switch logger library
- loggercheck
# maintidx measures the maintainability index of each function. [fast: true, auto-fix: false]
# not used because volume of halstead complexity feels strange as measurement https://en.wikipedia.org/wiki/Halstead_complexity_measures
- maintidx
# Finds slice declarations with non-zero initial length [fast: false, auto-fix: false]
# I would prefer to use https://github.com/alexkohler/prealloc
- makezero
# Reports deeply nested if statements [fast: true, auto-fix: false]
# focus only on if's
- nestif
# Finds the code that returns nil even if it checks that the error is not nil. [fast: false, auto-fix: false]
# FUTURE: check if it is allowed to return nil partially in error catch
- nilerr
# Checks that there is no simultaneous return of `nil` error and an invalid value. [fast: false, auto-fix: false]
# FUTURE: would reduce checks and panics
- nilnil
# nlreturn checks for a new line before return and branch statements to increase code clarity [fast: true, auto-fix: false]
# DISCUSS: IMO the readability of does not always increase using more empty lines
- nlreturn
# noctx finds sending http request without context.Context [fast: false, auto-fix: false]
# only interesting if using http
- noctx
# Reports all names returns
# Named returns are not allowed which IMO reduces readability of code
- nonamedreturns
# detects snake case of variable naming and function name.
# has not been a problem in our code and deprecated
- nosnakecase
# paralleltest detects missing usage of t.Parallel() method in your Go test [fast: true, auto-fix: false]
# FUTURE: will break all of our tests
- paralleltest
# Finds slice declarations that could potentially be preallocated [fast: true, auto-fix: false]
# FUTURE: would improve performance
- prealloc
# find code that shadows one of Go's predeclared identifiers [fast: true, auto-fix: false]
# FUTURE: checks for overwrites
- predeclared
# Check Prometheus metrics naming via promlint [fast: true, auto-fix: false]
# Not interesting at the moment
- promlinter
# Checks that package variables are not reassigned
# FUTURE: checks if vars like Err's are reassigned which might break code
- reassign
# Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. [fast: false, auto-fix: false]
# Linter aggregator, would allow to use less other linters
- revive
# checks for unpinned variables in go programs
# deprecated
- scopelint
# Finds unused struct fields [fast: false, auto-fix: false]
# deprecated, replaced by unused
- structcheck
# Stylecheck is a replacement for golint [fast: false, auto-fix: false]
# we use goimports
- stylecheck
# Checks the struct tags. [fast: true, auto-fix: false]
# FUTURE: would help for new structs
- tagliatelle
# tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17 [fast: false, auto-fix: false]
# FUTURE: currently are no env vars set
- tenv
# linter checks if examples are testable (have an expected output)
# FUTURE: as soon as examples are added
- testableexamples
# linter that makes you use a separate _test package [fast: true, auto-fix: false]
# don't use because we test some unexported functions
- testpackage
# thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers [fast: false, auto-fix: false]
# FUTURE: nice to improve test quality
- thelper
# tparallel detects inappropriate usage of t.Parallel() method in your Go test codes [fast: false, auto-fix: false]
# FUTURE: nice to improve test quality
- tparallel
# Reports unused function parameters [fast: false, auto-fix: false]
# DISCUSS: nice idea and would improve code quality, but how to handle false positives?
- unparam
# A linter that detect the possibility to use variables/constants from the Go standard library.
# FUTURE: improves code quality
- usestdlibvars
# Finds unused global variables and constants [fast: false, auto-fix: false]
# deprecated, replaced by unused
- varcheck
# checks that the length of a variable's name matches its scope [fast: false, auto-fix: false]
# I would not use it because it more or less checks if var lenght matches
- varnamelen
# wastedassign finds wasted assignment statements. [fast: false, auto-fix: false]
# FUTURE: would improve code quality (maybe already checked by vet?)
- wastedassign
# Tool for detection of leading and trailing whitespace [fast: true, auto-fix: true]
# Not sure if it improves code readability
- whitespace
# Checks that errors returned from external packages are wrapped [fast: false, auto-fix: false]
# FUTURE: improves UX because all the errors will be ZITADEL errors
- wrapcheck
# Whitespace Linter - Forces you to use empty lines! [fast: true, auto-fix: false]
# FUTURE: improves code quality by allowing and blocking line breaks
- wsl
linters-settings:
gci:
sections:
- standard # Standard section: captures all standard packages.
- default # Default section: contains all imports that could not be matched to another section type.
- prefix(github.com/zitadel/zitadel) # Custom section: groups all imports with the specified Prefix.

View File

@ -1,7 +1,8 @@
module.exports = { module.exports = {
branches: [ branches: [
{name: 'main'}, {name: 'main'},
{name: '1.87.x', range: '1.87.x', channel: '1.87.x'} {name: '1.87.x', range: '1.87.x', channel: '1.87.x'},
{name: 'startup-times', prerelease: 'beta'}
], ],
plugins: [ plugins: [
"@semantic-release/commit-analyzer" "@semantic-release/commit-analyzer"

View File

@ -115,6 +115,8 @@ With [goreleaser](https://opencollective.com/goreleaser), you build a debuggable
Then, you test your changes via the console your binary is serving at http://<span because="breaks the link"></span>localhost:8080 and by verifying the database. Then, you test your changes via the console your binary is serving at http://<span because="breaks the link"></span>localhost:8080 and by verifying the database.
Once you are happy with your changes, you run end-to-end tests and tear everything down. Once you are happy with your changes, you run end-to-end tests and tear everything down.
ZITADEL uses [golangci-lint](https://golangci-lint.run) for code quality checks. Please use [this configuration](.golangci.yaml) when running `golangci-lint`. We recommend to set golangci-lint as linter in your IDE.
The commands in this section are tested against the following software versions: The commands in this section are tested against the following software versions:
- [Docker version 20.10.17](https://docs.docker.com/engine/install/) - [Docker version 20.10.17](https://docs.docker.com/engine/install/)
@ -122,8 +124,6 @@ The commands in this section are tested against the following software versions:
- [Go version 1.19](https://go.dev/doc/install) - [Go version 1.19](https://go.dev/doc/install)
- [Delve 1.9.1](https://github.com/go-delve/delve/tree/v1.9.1/Documentation/installation) - [Delve 1.9.1](https://github.com/go-delve/delve/tree/v1.9.1/Documentation/installation)
<!-- TODO: Describe linting (@adlerhurst) -->
Make some changes to the source code, then run the database locally. Make some changes to the source code, then run the database locally.
```bash ```bash

View File

@ -4,6 +4,7 @@
</p> </p>
<p align="center"> <p align="center">
<a href="https://bestpractices.coreinfrastructure.org/projects/6662"><img src="https://bestpractices.coreinfrastructure.org/projects/6662/badge"></a>
<a href="https://github.com/zitadel/zitadel/graphs/contributors" alt="Release"> <a href="https://github.com/zitadel/zitadel/graphs/contributors" alt="Release">
<img src="https://badgen.net/github/contributors/zitadel/zitadel" /></a> <img src="https://badgen.net/github/contributors/zitadel/zitadel" /></a>
<a href="https://github.com/semantic-release/semantic-release" alt="semantic-release"> <a href="https://github.com/semantic-release/semantic-release" alt="semantic-release">
@ -27,13 +28,16 @@
<img src="./docs/static/logos/oidc-cert.png" /></a> <img src="./docs/static/logos/oidc-cert.png" /></a>
</p> </p>
You want auth that's quickly set up like Auth0 but open source like Keycloak? Look no further — ZITADEL combines the ease of Auth0 and the versatility of Keycloak. Do you look for a user management that's quickly set up like Auth0 and open source like Keycloak?
We provide a wide range of out of the box features like secure login, self-service, OpenID Connect, OAuth2.x, SAML2, branding, Passwordless with FIDO2, OTP, U2F, and an unlimited audit trail to improve the life of developers. Especially noteworthy is that ZITADEL supports not only B2C and B2E scenarios but also B2B. This is super useful for people who build B2B Solutions, as ZITADEL can handle all the delegated user and access management. Do you have project that requires a multi-tenant user management with self-service for your customers?
With ZITADEL you rely on a battle tested, hardened and extensible turnkey solution to solve all of your authentication and authorization needs. With the unique way of how ZITADEL stores data it gives you an unlimited audit trail which provides a peace of mind for even the harshest audit and analytics requirements. Look no further — ZITADEL combines the ease of Auth0 with the versatility of Keycloak.
<!-- TODO: Insert Video here--> We provide you with a wide range of out of the box features to accelerate your project.
Multi-tenancy with branding customization, secure login, self-service, OpenID Connect, OAuth2.x, SAML2, Passwordless with FIDO2 (including Passkeys), OTP, U2F, and an unlimited audit trail is there for you, ready to use.
With ZITADEL you can rely on a hardened and extensible turnkey solution to solve all of your authentication and authorization needs.
--- ---
@ -41,34 +45,43 @@ With ZITADEL you rely on a battle tested, hardened and extensible turnkey soluti
## Get started ## Get started
### ZITADEL Cloud (SaaS) ### Deploy ZITADEL (Self-Hosted)
The easiest way to get started with [ZITADEL Cloud](https://zitadel.cloud). Deploying ZITADEL locally takes less than 3 minutes. So go ahead and give it a try!
It's free for up to 25'000 authenticated requests. Learn more about the [pay-as-you-go pricing](https://zitadel.com/pricing). * [Linux](https://docs.zitadel.com/docs/guides/deploy/linux)
* [MacOS](https://docs.zitadel.com/docs/guides/deploy/macos)
* [Docker compose](https://docs.zitadel.com/docs/guides/deploy/compose)
* [Knative](https://docs.zitadel.com/docs/guides/deploy/knative)
* [Kubernetes](https://docs.zitadel.com/docs/guides/deploy/kubernetes)
### Install ZITADEL See all guides [here](https://docs.zitadel.com/docs/guides/deploy/overview)
- [We provide installation guides for multiple platforms here](https://docs.zitadel.com/docs/guides/deploy/overview) > If you are interested to get professional support for your self-hosted ZITADEL [please reach out to us](https://zitadel.com/contact)!
### Quickstarts - Integrate your app ### Setup ZITADEL Cloud (SaaS)
[Multiple Examples can be found here](https://docs.zitadel.com/docs/examples/introduction) If you want to experience a hands-free ZITADEL, you should use [ZITADEL Cloud](https://zitadel.cloud).
> If you miss something please feel free to [join the Discussion](https://github.com/zitadel/zitadel/discussions/1717) It is free for up to 25'000 authenticated requests and provides you all the features that make ZITADEL great.
Learn more about the [pay-as-you-go pricing](https://zitadel.com/pricing).
## Why ZITADEL ## Why choose ZITADEL
- [API-first](https://docs.zitadel.com/docs/apis/introduction) We built ZITADEL with a complex multi-tenancy architecture in mind and provide the best solution to handle B2B customers and partners.
- Strong audit trail thanks to [event sourcing](https://docs.zitadel.com/docs/concepts/eventstore/overview) Yet it offers everything you need for a customer identity (CIAM) use case.
- [Actions](https://docs.zitadel.com/docs/concepts/features/actions) to react on events with custom code
- [Branding](https://docs.zitadel.com/docs/guides/manage/customize/branding) for a uniform user experience - [API-first approach](https://docs.zitadel.com/docs/apis/introduction)
- [CockroachDB](https://www.cockroachlabs.com/) or a Postgres database is the only dependency - Strong audit trail thanks to [event sourcing](https://docs.zitadel.com/docs/concepts/eventstore/overview) as storage pattern
- [Actions](https://docs.zitadel.com/docs/concepts/features/actions) to react on events with custom code and extended ZITADEL for you needs
- [Branding](https://docs.zitadel.com/docs/guides/manage/customize/branding) for a uniform user experience across multiple organizations
- [Self-service](https://docs.zitadel.com/docs/concepts/features/selfservice) for end-users, business customers, and administrators
- [CockroachDB](https://www.cockroachlabs.com/) or a [Postgres](https://www.postgresql.org/) database as reliable and widespread storage option
## Features ## Features
- Single Sign On (SSO) - Single Sign On (SSO)
- Passwordless with FIDO2 support - Passwordless with FIDO2 support (Including Passkeys)
- Username / Password - Username / Password
- Multifactor authentication with OTP, U2F - Multifactor authentication with OTP, U2F
- [Identity Brokering](https://docs.zitadel.com/docs/guides/integrate/identity-brokering) - [Identity Brokering](https://docs.zitadel.com/docs/guides/integrate/identity-brokering)
@ -76,25 +89,39 @@ It's free for up to 25'000 authenticated requests. Learn more about the [pay-as-
- 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/manage/console/projects) - [Delegate role management to third-parties](https://docs.zitadel.com/docs/guides/manage/console/projects)
- Self-registration including verification - [Self-registration](https://docs.zitadel.com/docs/concepts/features/selfservice#registration) including verification
- User self service - [Self-service](https://docs.zitadel.com/docs/concepts/features/selfservice) for end-users, business customers, and administrators
- [Service Accounts](https://docs.zitadel.com/docs/guides/integrate/serviceusers)
- [OpenID Connect certified](https://openid.net/certification/#OPs) => [OIDC Endpoints](https://docs.zitadel.com/docs/apis/openidoauth/endpoints), [OIDC Integration Guides](https://docs.zitadel.com/docs/guides/integrate/auth0-oidc) - [OpenID Connect certified](https://openid.net/certification/#OPs) => [OIDC Endpoints](https://docs.zitadel.com/docs/apis/openidoauth/endpoints), [OIDC Integration Guides](https://docs.zitadel.com/docs/guides/integrate/auth0-oidc)
- [SAML 2.0](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html) => [SAML Endpoints](https://docs.zitadel.com/docs/apis/saml/endpoints), [SAML Integration Guides](https://docs.zitadel.com/docs/guides/integrate/auth0-saml) - [SAML 2.0](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html) => [SAML Endpoints](https://docs.zitadel.com/docs/apis/saml/endpoints), [SAML Integration Guides](https://docs.zitadel.com/docs/guides/integrate/auth0-saml)
- [Postgres](https://docs.zitadel.com/docs/guides/manage/self-hosted/database#postgres) (version >= 14) or [CockroachDB](https://docs.zitadel.com/docs/guides/manage/self-hosted/database#cockroach) (version >= 22.0) - [Postgres](https://docs.zitadel.com/docs/guides/manage/self-hosted/database#postgres) (version >= 14) or [CockroachDB](https://docs.zitadel.com/docs/guides/manage/self-hosted/database#cockroach) (version >= 22.0)
Track upcoming features on our [roadmap](https://zitadel.com/roadmap). Track upcoming features on our [roadmap](https://zitadel.com/roadmap).
## Client libraries ## Integrate your application
<!-- TODO: check other libraries --> ### Quickstarts
| Language | Client | API | Machine auth (\*) | Auth check (\*\*) | Thanks to the maintainers | Get started with your preferred language with our [Quickstarts](https://docs.zitadel.com/docs/examples/introduction).
> If you miss something please feel free to [join the Discussion](https://github.com/zitadel/zitadel/discussions/1717)
### Example applications
Clone one of our [example applications](https://docs.zitadel.com/docs/examples/introduction#clone-a-sample-project) or deploy them directly to Vercel.
### OpenID Connect RP Libraries
Use any of the [Open ID Connect certified RP implementations](https://openid.net/developers/certified/) in your preferred language.
As certified OpenID Provider, ZITADEL is compatible with any of the implementations.
### Client libraries
| Language / Framework | Client | API | Machine auth (\*) | Auth check (\*\*) | Thanks to the maintainers |
|----------|--------|--------------|----------|---------|---------------------------| |----------|--------|--------------|----------|---------|---------------------------|
| .NET | [zitadel-net](https://github.com/smartive/zitadel-net) | GRPC | ✔️ | ✔️ | [smartive 👑](https://github.com/smartive/) | | .NET | [zitadel-net](https://github.com/smartive/zitadel-net) | GRPC | ✔️ | ✔️ | [smartive 👑](https://github.com/smartive/) |
| Dart | [zitadel-dart](https://github.com/smartive/zitadel-dart) | GRPC | ✔️ | ❌ | [smartive 👑](https://github.com/smartive/) | | Dart | [zitadel-dart](https://github.com/smartive/zitadel-dart) | GRPC | ✔️ | ❌ | [smartive 👑](https://github.com/smartive/) |
| Elixir | [zitadel_api](https://github.com/jshmrtn/zitadel_api) | GRPC | ✔️ | ✔️ | [jshmrtn 🙏🏻](https://github.com/jshmrtn) | | Elixir | [zitadel_api](https://github.com/jshmrtn/zitadel_api) | GRPC | ✔️ | ✔️ | [jshmrtn 🙏🏻](https://github.com/jshmrtn) |
| Go | [zitadel-go](https://github.com/zitadel/zitadel-go) | GRPC | ✔️ | ✔️ | ZITADEL | | Go | [zitadel-go](https://github.com/zitadel/zitadel-go) | GRPC | ✔️ | ✔️ | [ZITADEL](https://github.com/zitadel/) |
| Rust | [zitadel-rust](https://crates.io/crates/zitadel) | GRPC | ✔️ | ❌ | [smartive 👑](https://github.com/smartive/) | | Rust | [zitadel-rust](https://crates.io/crates/zitadel) | GRPC | ✔️ | ❌ | [smartive 👑](https://github.com/smartive/) |
| JVM | 🚧 [WIP](https://github.com/zitadel/zitadel/discussions/3650) | ❓ | ❓ | | TBD | | JVM | 🚧 [WIP](https://github.com/zitadel/zitadel/discussions/3650) | ❓ | ❓ | | TBD |
| Python | 🚧 [WIP](https://github.com/zitadel/zitadel/issues/3675) | ❓ | ❓ | | TBD | | Python | 🚧 [WIP](https://github.com/zitadel/zitadel/issues/3675) | ❓ | ❓ | | TBD |
@ -117,8 +144,6 @@ Made with [contrib.rocks](https://contrib.rocks).
## Showcase ## Showcase
<!-- TODO: Replace Images-->
### Passwordless Login ### Passwordless Login
Use our login widget to allow easy and secure access to your applications and enjoy all the benefits of passwordless (FIDO 2 / WebAuthN): Use our login widget to allow easy and secure access to your applications and enjoy all the benefits of passwordless (FIDO 2 / WebAuthN):
@ -134,14 +159,7 @@ Use our login widget to allow easy and secure access to your applications and en
Use [Console](https://docs.zitadel.com/docs/manuals/introduction) or our [APIs](https://docs.zitadel.com/docs/apis/introduction) to setup organizations, projects and applications. Use [Console](https://docs.zitadel.com/docs/manuals/introduction) or our [APIs](https://docs.zitadel.com/docs/apis/introduction) to setup organizations, projects and applications.
Register new applications [![Console Showcase](http://img.youtube.com/vi/RPpHktAcCtk/0.jpg)](http://www.youtube.com/watch?v=RPpHktAcCtk "Console Showcase")
![OIDC-Client-Register](https://user-images.githubusercontent.com/1366906/118765446-62064b80-b87b-11eb-8b24-4f4c365b8c58.gif)
Delegate the right to assign roles to another organization
![projects_create_org_grant](https://user-images.githubusercontent.com/1366906/118766069-39cb1c80-b87c-11eb-84cf-f5becce4e9b6.gif)
Customize login and console with your design
![private_labeling](https://user-images.githubusercontent.com/1366906/123089110-d148ff80-d426-11eb-9598-32b506f6d4fd.gif)
## Security ## Security
@ -151,4 +169,5 @@ 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

@ -10,6 +10,10 @@ var (
) )
func Version() string { func Version() string {
if version != "" {
return version
}
version = Date().Format(time.RFC3339)
return version return version
} }

View File

@ -8,6 +8,12 @@ Metrics:
# Select type otel (OpenTelemetry) or none (disables collection and endpoint) # Select type otel (OpenTelemetry) or none (disables collection and endpoint)
Type: otel Type: otel
Tracing:
# Choose one in "otel", "google", "log" and "none"
Type: none
Fraction: 1
MetricPrefix: zitadel
# 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
@ -220,8 +226,6 @@ SAML:
# Company: ZITADEL # Company: ZITADEL
# EmailAddress: hi@zitadel.com # EmailAddress: hi@zitadel.com
Login: Login:
LanguageCookieName: zitadel.login.lang LanguageCookieName: zitadel.login.lang
CSRFCookieName: zitadel.login.csrf CSRFCookieName: zitadel.login.csrf
@ -312,7 +316,7 @@ Actions:
# wildcard sub domains are currently unsupported # wildcard sub domains are currently unsupported
DenyList: DenyList:
- localhost - localhost
- '127.0.0.1' - "127.0.0.1"
DefaultInstance: DefaultInstance:
InstanceName: InstanceName:
@ -423,7 +427,7 @@ DefaultInstance:
BackgroundColor: "#fafafa" BackgroundColor: "#fafafa"
WarnColor: "#cd3d56" WarnColor: "#cd3d56"
FontColor: "#000000" FontColor: "#000000"
PrimaryColorDark: "#bbbafa" PrimaryColorDark: "#2073c4"
BackgroundColorDark: "#111827" BackgroundColorDark: "#111827"
WarnColorDark: "#ff3b5b" WarnColorDark: "#ff3b5b"
FontColorDark: "#ffffff" FontColorDark: "#ffffff"

25
cmd/setup/05.go Normal file
View File

@ -0,0 +1,25 @@
package setup
import (
"context"
"database/sql"
_ "embed"
)
var (
//go:embed 05.sql
lastFailedStmts string
)
type LastFailed struct {
dbClient *sql.DB
}
func (mig *LastFailed) Execute(ctx context.Context) error {
_, err := mig.dbClient.ExecContext(ctx, lastFailedStmts)
return err
}
func (mig *LastFailed) String() string {
return "05_last_failed"
}

11
cmd/setup/05.sql Normal file
View File

@ -0,0 +1,11 @@
CREATE INDEX instance_id_idx ON adminapi.current_sequences (instance_id);
CREATE INDEX instance_id_idx ON auth.current_sequences (instance_id);
CREATE INDEX instance_id_idx ON projections.current_sequences (instance_id);
CREATE INDEX instance_id_idx ON adminapi.failed_events (instance_id);
CREATE INDEX instance_id_idx ON auth.failed_events (instance_id);
CREATE INDEX instance_id_idx ON projections.failed_events (instance_id);
ALTER TABLE adminapi.failed_events ADD COLUMN last_failed TIMESTAMPTZ;
ALTER TABLE auth.failed_events ADD COLUMN last_failed TIMESTAMPTZ;
ALTER TABLE projections.failed_events ADD COLUMN last_failed TIMESTAMPTZ;

View File

@ -15,6 +15,7 @@ import (
"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/id" "github.com/zitadel/zitadel/internal/id"
"github.com/zitadel/zitadel/internal/query/projection"
) )
type Config struct { type Config struct {
@ -28,6 +29,7 @@ type Config struct {
EncryptionKeys *encryptionKeyConfig EncryptionKeys *encryptionKeyConfig
DefaultInstance command.InstanceSetup DefaultInstance command.InstanceSetup
Machine *id.Config Machine *id.Config
Projections projection.Config
} }
func MustNewConfig(v *viper.Viper) *Config { func MustNewConfig(v *viper.Viper) *Config {
@ -56,6 +58,7 @@ type Steps struct {
s2AssetsTable *AssetTable s2AssetsTable *AssetTable
FirstInstance *FirstInstance FirstInstance *FirstInstance
s4EventstoreIndexes *EventstoreIndexes s4EventstoreIndexes *EventstoreIndexes
s5LastFailed *LastFailed
} }
type encryptionKeyConfig struct { type encryptionKeyConfig struct {

31
cmd/setup/projections.go Normal file
View File

@ -0,0 +1,31 @@
package setup
import (
"context"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/query/projection"
)
type projectionTables struct {
es *eventstore.Eventstore
currentVersion string
Version string `json:"version"`
}
func (mig *projectionTables) SetLastExecution(lastRun map[string]interface{}) {
mig.currentVersion, _ = lastRun["version"].(string)
}
func (mig *projectionTables) Check() bool {
return mig.currentVersion != mig.Version
}
func (mig *projectionTables) Execute(ctx context.Context) error {
return projection.Init(ctx)
}
func (mig *projectionTables) String() string {
return "projection_tables"
}

View File

@ -8,11 +8,13 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/zitadel/logging" "github.com/zitadel/logging"
"github.com/zitadel/zitadel/cmd/build"
"github.com/zitadel/zitadel/cmd/key" "github.com/zitadel/zitadel/cmd/key"
"github.com/zitadel/zitadel/cmd/tls" "github.com/zitadel/zitadel/cmd/tls"
"github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/migration" "github.com/zitadel/zitadel/internal/migration"
"github.com/zitadel/zitadel/internal/query/projection"
) )
var ( var (
@ -54,6 +56,7 @@ func Flags(cmd *cobra.Command) {
} }
func Setup(config *Config, steps *Steps, masterKey string) { func Setup(config *Config, steps *Steps, masterKey string) {
ctx := context.Background()
logging.Info("setup started") logging.Info("setup started")
dbClient, err := database.Connect(config.Database, false) dbClient, err := database.Connect(config.Database, false)
@ -79,6 +82,10 @@ func Setup(config *Config, steps *Steps, masterKey string) {
steps.FirstInstance.externalPort = config.ExternalPort steps.FirstInstance.externalPort = config.ExternalPort
steps.s4EventstoreIndexes = &EventstoreIndexes{dbClient: dbClient, dbType: config.Database.Type()} steps.s4EventstoreIndexes = &EventstoreIndexes{dbClient: dbClient, dbType: config.Database.Type()}
steps.s5LastFailed = &LastFailed{dbClient: dbClient}
err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil)
logging.OnError(err).Fatal("unable to start projections")
repeatableSteps := []migration.RepeatableMigration{ repeatableSteps := []migration.RepeatableMigration{
&externalConfigChange{ &externalConfigChange{
@ -87,9 +94,12 @@ func Setup(config *Config, steps *Steps, masterKey string) {
ExternalPort: config.ExternalPort, ExternalPort: config.ExternalPort,
ExternalSecure: config.ExternalSecure, ExternalSecure: config.ExternalSecure,
}, },
&projectionTables{
es: eventstoreClient,
Version: build.Version(),
},
} }
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)
@ -98,6 +108,8 @@ func Setup(config *Config, steps *Steps, masterKey string) {
logging.OnError(err).Fatal("unable to migrate step 3") logging.OnError(err).Fatal("unable to migrate step 3")
err = migration.Migrate(ctx, eventstoreClient, steps.s4EventstoreIndexes) err = migration.Migrate(ctx, eventstoreClient, steps.s4EventstoreIndexes)
logging.OnError(err).Fatal("unable to migrate step 4") logging.OnError(err).Fatal("unable to migrate step 4")
err = migration.Migrate(ctx, eventstoreClient, steps.s5LastFailed)
logging.OnError(err).Fatal("unable to migrate step 5")
for _, repeatableStep := range repeatableSteps { for _, repeatableStep := range repeatableSteps {
err = migration.Migrate(ctx, eventstoreClient, repeatableStep) err = migration.Migrate(ctx, eventstoreClient, repeatableStep)

View File

@ -12,6 +12,7 @@ import (
"github.com/zitadel/logging" "github.com/zitadel/logging"
"github.com/zitadel/zitadel/cmd/admin" "github.com/zitadel/zitadel/cmd/admin"
"github.com/zitadel/zitadel/cmd/build"
"github.com/zitadel/zitadel/cmd/initialise" "github.com/zitadel/zitadel/cmd/initialise"
"github.com/zitadel/zitadel/cmd/key" "github.com/zitadel/zitadel/cmd/key"
"github.com/zitadel/zitadel/cmd/setup" "github.com/zitadel/zitadel/cmd/setup"
@ -33,6 +34,7 @@ func New(out io.Writer, in io.Reader, args []string) *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return errors.New("no additional command provided") return errors.New("no additional command provided")
}, },
Version: build.Version(),
} }
viper.AutomaticEnv() viper.AutomaticEnv()
@ -55,6 +57,8 @@ func New(out io.Writer, in io.Reader, args []string) *cobra.Command {
key.New(), key.New(),
) )
cmd.InitDefaultVersionFlag()
return cmd return cmd
} }

View File

@ -85,7 +85,7 @@
"budgets": [ "budgets": [
{ {
"type": "initial", "type": "initial",
"maximumWarning": "5mb", "maximumWarning": "6mb",
"maximumError": "6mb" "maximumError": "6mb"
}, },
{ {

7210
console/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,18 +12,18 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^14.2.4", "@angular/animations": "^14.2.9",
"@angular/cdk": "^14.2.3", "@angular/cdk": "^14.2.3",
"@angular/common": "^14.2.4", "@angular/common": "^14.2.9",
"@angular/compiler": "^14.2.4", "@angular/compiler": "^14.2.9",
"@angular/core": "^14.2.4", "@angular/core": "^14.2.9",
"@angular/forms": "^14.2.4", "@angular/forms": "^14.2.9",
"@angular/material": "^14.2.3", "@angular/material": "^14.2.3",
"@angular/material-moment-adapter": "^14.2.3", "@angular/material-moment-adapter": "^14.2.3",
"@angular/platform-browser": "^14.2.4", "@angular/platform-browser": "^14.2.9",
"@angular/platform-browser-dynamic": "^14.2.4", "@angular/platform-browser-dynamic": "^14.2.9",
"@angular/router": "^14.2.4", "@angular/router": "^14.2.9",
"@angular/service-worker": "^14.2.4", "@angular/service-worker": "^14.2.9",
"@ctrl/ngx-codemirror": "^5.1.1", "@ctrl/ngx-codemirror": "^5.1.1",
"@grpc/grpc-js": "^1.7.1", "@grpc/grpc-js": "^1.7.1",
"@ngx-translate/core": "^14.0.0", "@ngx-translate/core": "^14.0.0",
@ -36,7 +36,7 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"google-proto-files": "^3.0.2", "google-proto-files": "^3.0.2",
"google-protobuf": "^3.19.4", "google-protobuf": "^3.21.2",
"grpc-web": "^1.4.1", "grpc-web": "^1.4.1",
"libphonenumber-js": "^1.10.6", "libphonenumber-js": "^1.10.6",
"material-design-icons-iconfont": "^6.1.1", "material-design-icons-iconfont": "^6.1.1",
@ -46,28 +46,28 @@
"ngx-quicklink": "^0.3.0", "ngx-quicklink": "^0.3.0",
"rxjs": "~7.5.7", "rxjs": "~7.5.7",
"tinycolor2": "^1.4.2", "tinycolor2": "^1.4.2",
"tslib": "^2.2.0", "tslib": "^2.4.1",
"uuid": "^9.0.0", "uuid": "^9.0.0",
"zone.js": "~0.11.4" "zone.js": "~0.11.4"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^14.2.4", "@angular-devkit/build-angular": "^14.2.8",
"@angular-eslint/builder": "^14.1.2", "@angular-eslint/builder": "^14.1.2",
"@angular-eslint/eslint-plugin": "^14.1.2", "@angular-eslint/eslint-plugin": "^14.1.2",
"@angular-eslint/eslint-plugin-template": "^14.1.2", "@angular-eslint/eslint-plugin-template": "^14.1.2",
"@angular-eslint/schematics": "^14.1.2", "@angular-eslint/schematics": "^14.1.2",
"@angular-eslint/template-parser": "^14.1.2", "@angular-eslint/template-parser": "^14.1.2",
"@angular/cli": "^14.2.4", "@angular/cli": "^14.2.8",
"@angular/compiler-cli": "^14.2.4", "@angular/compiler-cli": "^14.2.9",
"@angular/language-service": "^14.2.4", "@angular/language-service": "^14.2.9",
"@types/jasmine": "~4.3.0", "@types/jasmine": "~4.3.0",
"@types/jasminewd2": "~2.0.10", "@types/jasminewd2": "~2.0.10",
"@types/jsonwebtoken": "^8.5.5", "@types/jsonwebtoken": "^8.5.5",
"@types/node": "^18.8.1", "@types/node": "^18.8.1",
"@typescript-eslint/eslint-plugin": "5.39.0", "@typescript-eslint/parser": "5.42.0",
"@typescript-eslint/parser": "5.39.0", "@typescript-eslint/eslint-plugin": "5.42.0",
"codelyzer": "^6.0.0", "codelyzer": "^6.0.0",
"eslint": "^8.24.0", "eslint": "^8.26.0",
"jasmine-core": "~4.4.0", "jasmine-core": "~4.4.0",
"jasmine-spec-reporter": "~7.0.0", "jasmine-spec-reporter": "~7.0.0",
"karma": "~6.4.1", "karma": "~6.4.1",
@ -79,4 +79,4 @@
"protractor": "~7.0.0", "protractor": "~7.0.0",
"typescript": "^4.8.4" "typescript": "^4.8.4"
} }
} }

View File

@ -3,7 +3,9 @@
$warn: map-get($theme, warn); $warn: map-get($theme, warn);
$background: map-get($theme, background); $background: map-get($theme, background);
$accent: map-get($theme, accent); $accent: map-get($theme, accent);
$primary-color: mat.get-color-from-palette($primary, 500);
$lighter-primary-color: mat.get-color-from-palette($primary, 300);
$darker-primary-color: mat.get-color-from-palette($primary, 700);
$warn-color: mat.get-color-from-palette($warn, 500); $warn-color: mat.get-color-from-palette($warn, 500);
$accent-color: mat.get-color-from-palette($accent, 500); $accent-color: mat.get-color-from-palette($accent, 500);
@ -11,6 +13,7 @@
$is-dark-theme: map-get($theme, is-dark); $is-dark-theme: map-get($theme, is-dark);
$back: map-get($background, background); $back: map-get($background, background);
$secondary-text: map-get($foreground, secondary-text); $secondary-text: map-get($foreground, secondary-text);
$link-hover-color: if($is-dark-theme, $lighter-primary-color, $darker-primary-color);
.footer-wrapper { .footer-wrapper {
padding: 2rem; padding: 2rem;
@ -46,7 +49,7 @@
} }
&:hover { &:hover {
color: $primary-color; color: $link-hover-color;
} }
} }
} }
@ -114,7 +117,7 @@
} }
&:hover { &:hover {
color: $primary-color; color: $link-hover-color;
i { i {
visibility: visible; visibility: visible;

View File

@ -284,6 +284,7 @@ export class IdpTableComponent implements OnInit {
if (this.isDefault) { if (this.isDefault) {
return this.addLoginPolicy() return this.addLoginPolicy()
.then(() => { .then(() => {
this.loginPolicy.isDefault = false;
return (this.service as ManagementService).addIDPToLoginPolicy(idp.id, idp.owner).then(() => { return (this.service as ManagementService).addIDPToLoginPolicy(idp.id, idp.owner).then(() => {
this.toast.showInfo('IDP.TOAST.ADDED', true); this.toast.showInfo('IDP.TOAST.ADDED', true);
@ -339,6 +340,7 @@ export class IdpTableComponent implements OnInit {
if (this.isDefault) { if (this.isDefault) {
return this.addLoginPolicy() return this.addLoginPolicy()
.then(() => { .then(() => {
this.loginPolicy.isDefault = false;
return (this.service as ManagementService) return (this.service as ManagementService)
.removeIDPFromLoginPolicy(idp.id) .removeIDPFromLoginPolicy(idp.id)
.then(() => { .then(() => {

View File

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

View File

@ -31,6 +31,8 @@
background: none; background: none;
cursor: pointer; cursor: pointer;
font-family: 'Lato', -apple-system, BlinkMacSystemFont, sans-serif; font-family: 'Lato', -apple-system, BlinkMacSystemFont, sans-serif;
box-sizing: border-box;
height: 27px;
.c_label { .c_label {
display: flex; display: flex;
@ -38,8 +40,19 @@
text-align: center; text-align: center;
.count { .count {
display: none; display: flex;
align-items: center;
justify-content: center;
height: 17px;
width: 17px;
border-radius: 50%;
padding: 2px;
margin: -2px -9px -2px 8px;
margin-left: 6px; margin-left: 6px;
background-color: if($is-dark-theme, #ffffff20, #00000020);
line-height: 18px;
font-size: 11px;
transition: background 0.2s ease;
} }
} }
@ -63,7 +76,7 @@
.c_label { .c_label {
.count { .count {
display: inline-block; background-color: if($is-dark-theme, #00000020, #ffffff20);
} }
} }
} }

View File

@ -121,14 +121,14 @@
> >
<div class="c_label"> <div class="c_label">
<span> {{ 'MENU.PROJECT' | translate }} </span> <span> {{ 'MENU.PROJECT' | translate }} </span>
<small <ng-container *ngIf="projectcounter | async as count">
*ngIf="(mgmtService?.ownedProjectsCount | async) && (mgmtService?.grantedProjectsCount | async)" <div *ngIf="count < 100" class="count">
class="count" <span>{{ count }}</span>
>({{ </div>
((mgmtService.ownedProjectsCount | async) ?? 0) + <div *ngIf="count >= 100" class="count">
((mgmtService.grantedProjectsCount | async) ?? 0) <span>99+</span>
}})</small </div>
> </ng-container>
</div> </div>
</a> </a>
</ng-template> </ng-template>

View File

@ -64,6 +64,8 @@
margin: 0.25rem 2px; margin: 0.25rem 2px;
white-space: nowrap; white-space: nowrap;
position: relative; position: relative;
box-sizing: border-box;
height: 27px;
.c_label { .c_label {
display: flex; display: flex;
@ -72,7 +74,18 @@
.count { .count {
display: none; display: none;
align-items: center;
justify-content: center;
height: 17px;
width: 17px;
border-radius: 50%;
padding: 2px;
margin: -2px -9px -2px 8px;
margin-left: 6px; margin-left: 6px;
background-color: if($is-dark-theme, #ffffff20, #00000020);
line-height: 18px;
font-size: 11px;
transition: background 0.2s ease;
} }
} }
@ -97,6 +110,7 @@
.c_label { .c_label {
.count { .count {
display: inline-block; display: inline-block;
background-color: if($is-dark-theme, #00000020, #ffffff20);
} }
} }
} }

View File

@ -4,7 +4,7 @@ import { HttpClient } from '@angular/common/http';
import { Component, ElementRef, Input, OnDestroy, ViewChild } from '@angular/core'; import { Component, ElementRef, Input, OnDestroy, ViewChild } from '@angular/core';
import { UntypedFormControl } from '@angular/forms'; import { UntypedFormControl } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { BehaviorSubject, map, Observable, Subject, take } from 'rxjs'; import { BehaviorSubject, combineLatest, forkJoin, map, merge, Observable, Subject, switchMap, take } from 'rxjs';
import { Org } from 'src/app/proto/generated/zitadel/org_pb'; import { Org } 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';
import { AuthenticationService } from 'src/app/services/authentication.service'; import { AuthenticationService } from 'src/app/services/authentication.service';
@ -132,4 +132,15 @@ export class NavComponent implements OnDestroy {
public openHelp() { public openHelp() {
this.shortcutService.openOverviewDialog(); this.shortcutService.openOverviewDialog();
} }
public get projectcounter(): Observable<number> {
return combineLatest({
owned: this.mgmtService.ownedProjectsCount,
granted: this.mgmtService.grantedProjectsCount,
}).pipe(
map(({ owned, granted }) => {
return (owned ?? 0) + (granted ?? 0);
}),
);
}
} }

View File

@ -7,7 +7,6 @@ import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { BehaviorSubject, catchError, finalize, from, map, Observable, of, Subject, switchMap, take, takeUntil } from 'rxjs'; import { BehaviorSubject, catchError, finalize, from, map, Observable, of, Subject, switchMap, take, takeUntil } from 'rxjs';
import { Org, OrgFieldName, OrgQuery, OrgState } from 'src/app/proto/generated/zitadel/org_pb'; import { Org, OrgFieldName, OrgQuery, OrgState } from 'src/app/proto/generated/zitadel/org_pb';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ThemeService } from 'src/app/services/theme.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
import { PaginatorComponent } from '../paginator/paginator.component'; import { PaginatorComponent } from '../paginator/paginator.component';
@ -95,7 +94,7 @@ export class OrgTableComponent {
public refresh(): void { public refresh(): void {
this.requestOrgs$.next({ this.requestOrgs$.next({
limit: this.paginator.length, limit: this.paginator.pageSize,
offset: this.paginator.pageSize * this.paginator.pageIndex, offset: this.paginator.pageSize * this.paginator.pageIndex,
queries: this.searchQueries, queries: this.searchQueries,
}); });

View File

@ -34,16 +34,18 @@ export class GeneralSettingsComponent implements OnInit {
public savePolicy(): void { public savePolicy(): void {
const prom = this.updateData(); const prom = this.updateData();
this.loading = true;
if (prom) { if (prom) {
prom prom
.then(() => { .then(() => {
this.toast.showInfo('POLICY.LOGIN_POLICY.SAVED', true); this.toast.showInfo('POLICY.LOGIN_POLICY.SAVED', true);
this.loading = true; this.loading = false;
setTimeout(() => { setTimeout(() => {
this.fetchData(); this.fetchData();
}, 2000); }, 2000);
}) })
.catch((error) => { .catch((error) => {
this.loading = false;
this.toast.showError(error); this.toast.showError(error);
}); });
} }

View File

@ -45,11 +45,11 @@ export class LoginPolicyComponent implements OnInit {
public InfoSectionType: any = InfoSectionType; public InfoSectionType: any = InfoSectionType;
public PasswordlessType: any = PasswordlessType; public PasswordlessType: any = PasswordlessType;
public lifetimeForm: UntypedFormGroup = this.fb.group({ public lifetimeForm: UntypedFormGroup = this.fb.group({
passwordCheckLifetime: [{ disabled: true, value: 240 }, [Validators.required]], passwordCheckLifetime: [{ disabled: true }, [Validators.required]],
externalLoginCheckLifetime: [{ disabled: true, value: 12 }, [Validators.required]], externalLoginCheckLifetime: [{ disabled: true }, [Validators.required]],
mfaInitSkipLifetime: [{ disabled: true, value: 720 }, [Validators.required]], mfaInitSkipLifetime: [{ disabled: true }, [Validators.required]],
secondFactorCheckLifetime: [{ disabled: true, value: 12 }, [Validators.required]], secondFactorCheckLifetime: [{ disabled: true }, [Validators.required]],
multiFactorCheckLifetime: [{ disabled: true, value: 12 }, [Validators.required]], multiFactorCheckLifetime: [{ disabled: true }, [Validators.required]],
}); });
constructor( constructor(
private toast: ToastService, private toast: ToastService,
@ -67,29 +67,29 @@ export class LoginPolicyComponent implements OnInit {
this.loading = false; this.loading = false;
this.passwordCheckLifetime?.setValue( this.passwordCheckLifetime?.setValue(
this.loginData.passwordCheckLifetime?.seconds ? this.loginData.passwordCheckLifetime?.seconds / 60 / 60 : 240, this.loginData.passwordCheckLifetime?.seconds ? this.loginData.passwordCheckLifetime?.seconds / 60 / 60 : 0,
); );
this.externalLoginCheckLifetime?.setValue( this.externalLoginCheckLifetime?.setValue(
this.loginData.externalLoginCheckLifetime?.seconds this.loginData.externalLoginCheckLifetime?.seconds
? this.loginData.externalLoginCheckLifetime?.seconds / 60 / 60 ? this.loginData.externalLoginCheckLifetime?.seconds / 60 / 60
: 12, : 0,
); );
this.mfaInitSkipLifetime?.setValue( this.mfaInitSkipLifetime?.setValue(
this.loginData.mfaInitSkipLifetime?.seconds ? this.loginData.mfaInitSkipLifetime?.seconds / 60 / 60 : 720, this.loginData.mfaInitSkipLifetime?.seconds ? this.loginData.mfaInitSkipLifetime?.seconds / 60 / 60 : 0,
); );
this.secondFactorCheckLifetime?.setValue( this.secondFactorCheckLifetime?.setValue(
this.loginData.secondFactorCheckLifetime?.seconds this.loginData.secondFactorCheckLifetime?.seconds
? this.loginData.secondFactorCheckLifetime?.seconds / 60 / 60 ? this.loginData.secondFactorCheckLifetime?.seconds / 60 / 60
: 12, : 0,
); );
this.multiFactorCheckLifetime?.setValue( this.multiFactorCheckLifetime?.setValue(
this.loginData.multiFactorCheckLifetime?.seconds this.loginData.multiFactorCheckLifetime?.seconds
? this.loginData.multiFactorCheckLifetime?.seconds / 60 / 60 ? this.loginData.multiFactorCheckLifetime?.seconds / 60 / 60
: 12, : 0,
); );
} }
}) })
@ -158,19 +158,19 @@ export class LoginPolicyComponent implements OnInit {
mgmtreq.setDisableLoginWithEmail(this.loginData.disableLoginWithEmail); mgmtreq.setDisableLoginWithEmail(this.loginData.disableLoginWithEmail);
mgmtreq.setDisableLoginWithPhone(this.loginData.disableLoginWithPhone); mgmtreq.setDisableLoginWithPhone(this.loginData.disableLoginWithPhone);
const pcl = new Duration().setSeconds((this.passwordCheckLifetime?.value ?? 240) * 60 * 60); const pcl = new Duration().setSeconds((this.passwordCheckLifetime?.value ?? 0) * 60 * 60);
mgmtreq.setPasswordCheckLifetime(pcl); mgmtreq.setPasswordCheckLifetime(pcl);
const elcl = new Duration().setSeconds((this.externalLoginCheckLifetime?.value ?? 12) * 60 * 60); const elcl = new Duration().setSeconds((this.externalLoginCheckLifetime?.value ?? 0) * 60 * 60);
mgmtreq.setExternalLoginCheckLifetime(elcl); mgmtreq.setExternalLoginCheckLifetime(elcl);
const misl = new Duration().setSeconds((this.mfaInitSkipLifetime?.value ?? 720) * 60 * 60); const misl = new Duration().setSeconds((this.mfaInitSkipLifetime?.value ?? 0) * 60 * 60);
mgmtreq.setMfaInitSkipLifetime(misl); mgmtreq.setMfaInitSkipLifetime(misl);
const sfcl = new Duration().setSeconds((this.secondFactorCheckLifetime?.value ?? 12) * 60 * 60); const sfcl = new Duration().setSeconds((this.secondFactorCheckLifetime?.value ?? 0) * 60 * 60);
mgmtreq.setSecondFactorCheckLifetime(sfcl); mgmtreq.setSecondFactorCheckLifetime(sfcl);
const mficl = new Duration().setSeconds((this.multiFactorCheckLifetime?.value ?? 12) * 60 * 60); const mficl = new Duration().setSeconds((this.multiFactorCheckLifetime?.value ?? 0) * 60 * 60);
mgmtreq.setMultiFactorCheckLifetime(mficl); mgmtreq.setMultiFactorCheckLifetime(mficl);
mgmtreq.setAllowDomainDiscovery(this.loginData.allowDomainDiscovery); mgmtreq.setAllowDomainDiscovery(this.loginData.allowDomainDiscovery);
@ -189,19 +189,19 @@ export class LoginPolicyComponent implements OnInit {
mgmtreq.setDisableLoginWithEmail(this.loginData.disableLoginWithEmail); mgmtreq.setDisableLoginWithEmail(this.loginData.disableLoginWithEmail);
mgmtreq.setDisableLoginWithPhone(this.loginData.disableLoginWithPhone); mgmtreq.setDisableLoginWithPhone(this.loginData.disableLoginWithPhone);
const pcl = new Duration().setSeconds((this.passwordCheckLifetime?.value ?? 240) * 60 * 60); const pcl = new Duration().setSeconds((this.passwordCheckLifetime?.value ?? 0) * 60 * 60);
mgmtreq.setPasswordCheckLifetime(pcl); mgmtreq.setPasswordCheckLifetime(pcl);
const elcl = new Duration().setSeconds((this.externalLoginCheckLifetime?.value ?? 12) * 60 * 60); const elcl = new Duration().setSeconds((this.externalLoginCheckLifetime?.value ?? 0) * 60 * 60);
mgmtreq.setExternalLoginCheckLifetime(elcl); mgmtreq.setExternalLoginCheckLifetime(elcl);
const misl = new Duration().setSeconds((this.mfaInitSkipLifetime?.value ?? 720) * 60 * 60); const misl = new Duration().setSeconds((this.mfaInitSkipLifetime?.value ?? 0) * 60 * 60);
mgmtreq.setMfaInitSkipLifetime(misl); mgmtreq.setMfaInitSkipLifetime(misl);
const sfcl = new Duration().setSeconds((this.secondFactorCheckLifetime?.value ?? 12) * 60 * 60); const sfcl = new Duration().setSeconds((this.secondFactorCheckLifetime?.value ?? 0) * 60 * 60);
mgmtreq.setSecondFactorCheckLifetime(sfcl); mgmtreq.setSecondFactorCheckLifetime(sfcl);
const mficl = new Duration().setSeconds((this.multiFactorCheckLifetime?.value ?? 12) * 60 * 60); const mficl = new Duration().setSeconds((this.multiFactorCheckLifetime?.value ?? 0) * 60 * 60);
mgmtreq.setMultiFactorCheckLifetime(mficl); mgmtreq.setMultiFactorCheckLifetime(mficl);
mgmtreq.setAllowDomainDiscovery(this.loginData.allowDomainDiscovery); mgmtreq.setAllowDomainDiscovery(this.loginData.allowDomainDiscovery);
@ -221,19 +221,19 @@ export class LoginPolicyComponent implements OnInit {
adminreq.setDisableLoginWithEmail(this.loginData.disableLoginWithEmail); adminreq.setDisableLoginWithEmail(this.loginData.disableLoginWithEmail);
adminreq.setDisableLoginWithPhone(this.loginData.disableLoginWithPhone); adminreq.setDisableLoginWithPhone(this.loginData.disableLoginWithPhone);
const admin_pcl = new Duration().setSeconds((this.passwordCheckLifetime?.value ?? 240) * 60 * 60); const admin_pcl = new Duration().setSeconds((this.passwordCheckLifetime?.value ?? 0) * 60 * 60);
adminreq.setPasswordCheckLifetime(admin_pcl); adminreq.setPasswordCheckLifetime(admin_pcl);
const admin_elcl = new Duration().setSeconds((this.externalLoginCheckLifetime?.value ?? 12) * 60 * 60); const admin_elcl = new Duration().setSeconds((this.externalLoginCheckLifetime?.value ?? 0) * 60 * 60);
adminreq.setExternalLoginCheckLifetime(admin_elcl); adminreq.setExternalLoginCheckLifetime(admin_elcl);
const admin_misl = new Duration().setSeconds((this.mfaInitSkipLifetime?.value ?? 720) * 60 * 60); const admin_misl = new Duration().setSeconds((this.mfaInitSkipLifetime?.value ?? 0) * 60 * 60);
adminreq.setMfaInitSkipLifetime(admin_misl); adminreq.setMfaInitSkipLifetime(admin_misl);
const admin_sfcl = new Duration().setSeconds((this.secondFactorCheckLifetime?.value ?? 12) * 60 * 60); const admin_sfcl = new Duration().setSeconds((this.secondFactorCheckLifetime?.value ?? 0) * 60 * 60);
adminreq.setSecondFactorCheckLifetime(admin_sfcl); adminreq.setSecondFactorCheckLifetime(admin_sfcl);
const admin_mficl = new Duration().setSeconds((this.multiFactorCheckLifetime?.value ?? 12) * 60 * 60); const admin_mficl = new Duration().setSeconds((this.multiFactorCheckLifetime?.value ?? 0) * 60 * 60);
adminreq.setMultiFactorCheckLifetime(admin_mficl); adminreq.setMultiFactorCheckLifetime(admin_mficl);
adminreq.setAllowDomainDiscovery(this.loginData.allowDomainDiscovery); adminreq.setAllowDomainDiscovery(this.loginData.allowDomainDiscovery);
adminreq.setIgnoreUnknownUsernames(this.loginData.ignoreUnknownUsernames); adminreq.setIgnoreUnknownUsernames(this.loginData.ignoreUnknownUsernames);

View File

@ -98,7 +98,7 @@ export function mapRequestValues(map: Partial<Map>, req: Req): Req {
r6.setOtpOption(map.initMfaPromptText?.otpOption ?? ''); r6.setOtpOption(map.initMfaPromptText?.otpOption ?? '');
r6.setSkipButtonText(map.initMfaPromptText?.skipButtonText ?? ''); r6.setSkipButtonText(map.initMfaPromptText?.skipButtonText ?? '');
r6.setTitle(map.initMfaPromptText?.title ?? ''); r6.setTitle(map.initMfaPromptText?.title ?? '');
r6.setU2fOption(map.initMfaPromptText?.otpOption ?? ''); r6.setU2fOption(map.initMfaPromptText?.u2fOption ?? '');
req.setInitMfaPromptText(r6); req.setInitMfaPromptText(r6);
const r7 = new InitMFAU2FScreenText(); const r7 = new InitMFAU2FScreenText();
@ -333,7 +333,7 @@ export function mapRequestValues(map: Partial<Map>, req: Req): Req {
r31.setDescriptionClose(map.passwordlessRegistrationDoneText?.descriptionClose ?? ''); r31.setDescriptionClose(map.passwordlessRegistrationDoneText?.descriptionClose ?? '');
r31.setNextButtonText(map.passwordlessRegistrationDoneText?.nextButtonText ?? ''); r31.setNextButtonText(map.passwordlessRegistrationDoneText?.nextButtonText ?? '');
r31.setTitle(map.passwordlessRegistrationDoneText?.title ?? ''); r31.setTitle(map.passwordlessRegistrationDoneText?.title ?? '');
r31.setNextButtonText(map.passwordlessRegistrationDoneText?.cancelButtonText ?? ''); r31.setCancelButtonText(map.passwordlessRegistrationDoneText?.cancelButtonText ?? '');
req.setPasswordlessRegistrationDoneText(r31); req.setPasswordlessRegistrationDoneText(r31);
const r32 = new PasswordlessRegistrationScreenText(); const r32 = new PasswordlessRegistrationScreenText();

View File

@ -20,7 +20,15 @@ import { AdminService } from 'src/app/services/admin.service';
import { AssetEndpoint, AssetService, AssetType } from 'src/app/services/asset.service'; import { AssetEndpoint, AssetService, AssetType } from 'src/app/services/asset.service';
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
import { StorageKey, StorageLocation, StorageService } from 'src/app/services/storage.service'; import { StorageKey, StorageLocation, StorageService } from 'src/app/services/storage.service';
import { ThemeService } from 'src/app/services/theme.service'; import {
BACKGROUND,
DARK_BACKGROUND,
DARK_PRIMARY,
DARK_WARN,
PRIMARY,
ThemeService,
WARN,
} from 'src/app/services/theme.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';
@ -637,14 +645,14 @@ export class PrivateLabelingPolicyComponent implements OnInit, OnDestroy {
} }
private applyToConsole(labelpolicy: LabelPolicy.AsObject): void { private applyToConsole(labelpolicy: LabelPolicy.AsObject): void {
const darkPrimary = labelpolicy?.primaryColorDark || '#bbbafa'; const darkPrimary = labelpolicy?.primaryColorDark || DARK_PRIMARY;
const lightPrimary = labelpolicy?.primaryColor || '#5469d4'; const lightPrimary = labelpolicy?.primaryColor || PRIMARY;
const darkWarn = labelpolicy?.warnColorDark || '#ff3b5b'; const darkWarn = labelpolicy?.warnColorDark || DARK_WARN;
const lightWarn = labelpolicy?.warnColor || '#cd3d56'; const lightWarn = labelpolicy?.warnColor || WARN;
const darkBackground = labelpolicy?.backgroundColorDark || '#111827'; const darkBackground = labelpolicy?.backgroundColorDark || DARK_BACKGROUND;
const lightBackground = labelpolicy?.backgroundColor || '#fafafa'; const lightBackground = labelpolicy?.backgroundColor || BACKGROUND;
this.themeService.savePrimaryColor(darkPrimary, true); this.themeService.savePrimaryColor(darkPrimary, true);
this.themeService.savePrimaryColor(lightPrimary, false); this.themeService.savePrimaryColor(lightPrimary, false);

View File

@ -41,8 +41,8 @@
color="primary" color="primary"
[disabled]="disabled" [disabled]="disabled"
(click)="$event.stopPropagation()" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null" (change)="$event ? selection.toggle(row.key) : null"
[checked]="selection.isSelected(row)" [checked]="selection.isSelected(row.key)"
> >
</mat-checkbox> </mat-checkbox>
</td> </td>

View File

@ -1,5 +1,5 @@
import { SelectionModel } from '@angular/cdk/collections'; import { SelectionModel } from '@angular/cdk/collections';
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { MatTable } from '@angular/material/table'; import { MatTable } from '@angular/material/table';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
@ -27,8 +27,8 @@ export class ProjectRolesTableComponent implements OnInit {
@ViewChild(PaginatorComponent) public paginator?: PaginatorComponent; @ViewChild(PaginatorComponent) public paginator?: PaginatorComponent;
@ViewChild(MatTable) public table?: MatTable<Role.AsObject>; @ViewChild(MatTable) public table?: MatTable<Role.AsObject>;
public dataSource: ProjectRolesDataSource = new ProjectRolesDataSource(this.mgmtService); public dataSource: ProjectRolesDataSource = new ProjectRolesDataSource(this.mgmtService);
public selection: SelectionModel<Role.AsObject> = new SelectionModel<Role.AsObject>(true, []); public selection: SelectionModel<string> = new SelectionModel<string>(true, []);
@Output() public changedSelection: EventEmitter<Array<Role.AsObject>> = new EventEmitter(); @Output() public changedSelection: EventEmitter<Array<string>> = new EventEmitter();
@Input() public displayedColumns: string[] = ['key', 'displayname', 'group', 'creationDate', 'changeDate', 'actions']; @Input() public displayedColumns: string[] = ['key', 'displayname', 'group', 'creationDate', 'changeDate', 'actions'];
constructor( constructor(
@ -47,7 +47,7 @@ export class ProjectRolesTableComponent implements OnInit {
this.dataSource.rolesSubject.subscribe((roles) => { this.dataSource.rolesSubject.subscribe((roles) => {
const selectedRoles: Role.AsObject[] = roles.filter((role) => this.selectedKeys.includes(role.key)); const selectedRoles: Role.AsObject[] = roles.filter((role) => this.selectedKeys.includes(role.key));
this.selection.select(...selectedRoles); this.selection.select(...selectedRoles.map((r) => r.key));
}); });
this.selection.changed.subscribe(() => { this.selection.changed.subscribe(() => {
@ -57,7 +57,7 @@ export class ProjectRolesTableComponent implements OnInit {
public selectAllOfGroup(group: string): void { public selectAllOfGroup(group: string): void {
const groupRoles: Role.AsObject[] = this.dataSource.rolesSubject.getValue().filter((role) => role.group === group); const groupRoles: Role.AsObject[] = this.dataSource.rolesSubject.getValue().filter((role) => role.group === group);
this.selection.select(...groupRoles); this.selection.select(...groupRoles.map((r) => r.key));
} }
private loadRolesPage(): void { private loadRolesPage(): void {
@ -65,20 +65,19 @@ export class ProjectRolesTableComponent implements OnInit {
} }
public changePage(): void { public changePage(): void {
this.selection.clear();
this.loadRolesPage(); this.loadRolesPage();
} }
public isAllSelected(): boolean { public isAllSelected(): boolean {
const numSelected = this.selection.selected.length; const numSelected = this.selection.selected.length;
const numRows = this.dataSource.rolesSubject.value.length; const numRows = this.dataSource.totalResult;
return numSelected === numRows; return numSelected === numRows;
} }
public masterToggle(): void { public masterToggle(): void {
this.isAllSelected() this.isAllSelected()
? this.selection.clear() ? this.selection.clear()
: this.dataSource.rolesSubject.value.forEach((row: Role.AsObject) => this.selection.select(row)); : this.dataSource.rolesSubject.value.forEach((row: Role.AsObject) => this.selection.select(row.key));
} }
public deleteRole(role: Role.AsObject): void { public deleteRole(role: Role.AsObject): void {

View File

@ -3,9 +3,16 @@
@mixin search-user-autocomplete-theme($theme) { @mixin search-user-autocomplete-theme($theme) {
$primary: map-get($theme, primary); $primary: map-get($theme, primary);
$primary-color: mat.get-color-from-palette($primary, 500); $primary-color: mat.get-color-from-palette($primary, 500);
$lighter-primary-color: mat.get-color-from-palette($primary, 300);
$darker-primary-color: mat.get-color-from-palette($primary, 700);
$background: map-get($theme, background); $background: map-get($theme, background);
$foreground: map-get($theme, foreground); $foreground: map-get($theme, foreground);
$secondary-text: map-get($foreground, secondary-text); $secondary-text: map-get($foreground, secondary-text);
$is-dark-theme: map-get($theme, is-dark);
$link-hover-color: if($is-dark-theme, mat.get-color-from-palette($primary, 200), $primary-color);
$link-color: if($is-dark-theme, $lighter-primary-color, $primary-color);
.user-autocomplete-found { .user-autocomplete-found {
margin: 0.5rem 0; margin: 0.5rem 0;
@ -67,11 +74,12 @@
margin-top: 0.5rem; margin-top: 0.5rem;
a { a {
color: $primary-color; color: $link-color;
transition: color 0.2s ease;
&:hover { &:hover {
cursor: pointer; cursor: pointer;
color: mat.get-color-from-palette($primary, 400); color: $link-hover-color;
text-decoration: underline; text-decoration: underline;
} }
} }

View File

@ -17,7 +17,7 @@ import { from, of, Subject } from 'rxjs';
import { debounceTime, switchMap, takeUntil, tap } from 'rxjs/operators'; import { debounceTime, switchMap, takeUntil, tap } from 'rxjs/operators';
import { ListUsersResponse } from 'src/app/proto/generated/zitadel/management_pb'; import { ListUsersResponse } from 'src/app/proto/generated/zitadel/management_pb';
import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb'; import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb';
import { SearchQuery, User, UserNameQuery } from 'src/app/proto/generated/zitadel/user_pb'; import { LoginNameQuery, SearchQuery, User } from 'src/app/proto/generated/zitadel/user_pb';
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
@ -78,11 +78,11 @@ export class SearchUserAutocompleteComponent implements OnInit, AfterContentChec
switchMap((value) => { switchMap((value) => {
const query = new SearchQuery(); const query = new SearchQuery();
const unQuery = new UserNameQuery(); const lnQuery = new LoginNameQuery();
unQuery.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE); lnQuery.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE);
unQuery.setUserName(value); lnQuery.setLoginName(value);
query.setUserNameQuery(unQuery); query.setLoginNameQuery(lnQuery);
if (this.target === UserTarget.SELF) { if (this.target === UserTarget.SELF) {
return from(this.userService.listUsers(10, 0, [query])); return from(this.userService.listUsers(10, 0, [query]));

View File

@ -1,16 +1,18 @@
<span class="title" mat-dialog-title>{{ data.i18nTitle | translate }}</span> <span class="title" mat-dialog-title>{{ data.i18nTitle | translate }}</span>
<div mat-dialog-content> <div mat-dialog-content>
<cnsl-project-roles-table <div class="dialog-table-wrapper">
class="role-table" <cnsl-project-roles-table
*ngIf="projectId" class="role-table"
[displayedColumns]="['select', 'key', 'displayname', 'group']" *ngIf="projectId"
(changedSelection)="selectRoles($event)" [displayedColumns]="['select', 'key', 'displayname', 'group']"
[projectId]="projectId" (changedSelection)="selectRoles($event)"
[grantId]="grantId" [projectId]="projectId"
[selectedKeys]="selectedRoleKeysList" [grantId]="grantId"
[showSelectionActionButton]="false" [selectedKeys]="selectedRoleKeysList"
> [showSelectionActionButton]="false"
</cnsl-project-roles-table> >
</cnsl-project-roles-table>
</div>
</div> </div>
<div mat-dialog-actions class="action"> <div mat-dialog-actions class="action">
<button mat-stroked-button (click)="closeDialog()"> <button mat-stroked-button (click)="closeDialog()">

View File

@ -3,10 +3,15 @@
margin-top: 0; margin-top: 0;
} }
.role-table { .dialog-table-wrapper {
display: block; max-height: 500px;
width: 100%; overflow-y: auto;
margin-top: 1rem;
.role-table {
display: block;
width: 100%;
margin-top: 1rem;
}
} }
.action { .action {

View File

@ -12,7 +12,7 @@ export class UserGrantRoleDialogComponent {
public grantId: string = ''; public grantId: string = '';
public selectedRoleKeysList: string[] = []; public selectedRoleKeysList: string[] = [];
public selectedRoles: Role.AsObject[] = []; public selectedRoleKeys: string[] = [];
constructor(public dialogRef: MatDialogRef<UserGrantRoleDialogComponent>, @Inject(MAT_DIALOG_DATA) public data: any) { constructor(public dialogRef: MatDialogRef<UserGrantRoleDialogComponent>, @Inject(MAT_DIALOG_DATA) public data: any) {
this.projectId = data.projectId; this.projectId = data.projectId;
@ -20,8 +20,8 @@ export class UserGrantRoleDialogComponent {
this.selectedRoleKeysList = data.selectedRoleKeysList; this.selectedRoleKeysList = data.selectedRoleKeysList;
} }
public selectRoles(selected: any): void { public selectRoles(selected: string[]): void {
this.selectedRoles = selected; this.selectedRoleKeys = selected;
} }
public closeDialog(): void { public closeDialog(): void {
@ -29,6 +29,6 @@ export class UserGrantRoleDialogComponent {
} }
public closeDialogWithSuccess(): void { public closeDialogWithSuccess(): void {
this.dialogRef.close({ roles: this.selectedRoles.map((r) => r.key) }); this.dialogRef.close({ roles: this.selectedRoleKeys });
} }
} }

View File

@ -8,6 +8,9 @@
<cnsl-info-section *ngIf="data.warnSectionKey" [type]="InfoSectionType.WARN">{{ <cnsl-info-section *ngIf="data.warnSectionKey" [type]="InfoSectionType.WARN">{{
data.warnSectionKey | translate data.warnSectionKey | translate
}}</cnsl-info-section> }}</cnsl-info-section>
<p *ngIf="data.hintKey" class="desc cnsl-secondary-text">{{ data.hintKey | translate: { value: data.confirmation } }}</p>
<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" data-e2e="confirm-dialog-input" /> <input cnslInput [(ngModel)]="confirm" data-e2e="confirm-dialog-input" />

View File

@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { InfoSectionType } from 'src/app/modules/info-section/info-section.component'; import { InfoSectionType } from 'src/app/modules/info-section/info-section.component';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component'; import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
import { Domain } from 'src/app/proto/generated/zitadel/org_pb'; import { Domain, DomainValidationType } from 'src/app/proto/generated/zitadel/org_pb';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service'; import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
@ -62,13 +62,16 @@ export class DomainsComponent implements OnInit {
width: '400px', width: '400px',
}); });
dialogRef.afterClosed().subscribe((resp) => { dialogRef.afterClosed().subscribe((domainName) => {
if (resp) { if (domainName) {
this.mgmtService this.mgmtService
.addOrgDomain(resp) .addOrgDomain(domainName)
.then(() => { .then(() => {
this.toast.showInfo('ORG.TOAST.DOMAINADDED', true); this.toast.showInfo('ORG.TOAST.DOMAINADDED', true);
this.verifyDomain({
domainName: domainName,
validationType: DomainValidationType.DOMAIN_VALIDATION_TYPE_UNSPECIFIED,
});
setTimeout(() => { setTimeout(() => {
this.loadDomains(); this.loadDomains();
}, 1000); }, 1000);
@ -91,8 +94,8 @@ export class DomainsComponent implements OnInit {
width: '400px', width: '400px',
}); });
dialogRef.afterClosed().subscribe((resp) => { dialogRef.afterClosed().subscribe((del) => {
if (resp) { if (del) {
this.mgmtService this.mgmtService
.removeOrgDomain(domain) .removeOrgDomain(domain)
.then(() => { .then(() => {
@ -109,7 +112,7 @@ export class DomainsComponent implements OnInit {
}); });
} }
public verifyDomain(domain: Domain.AsObject): void { public verifyDomain(domain: Partial<Domain.AsObject>): void {
const dialogRef = this.dialog.open(DomainVerificationComponent, { const dialogRef = this.dialog.open(DomainVerificationComponent, {
data: { data: {
domain: domain, domain: domain,

View File

@ -29,6 +29,13 @@
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="lastFailed">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.FAILEDEVENTS.LASTFAILED' | translate }}</th>
<td mat-cell *matCellDef="let event">
<span>{{ event?.lastFailed | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span>
</td>
</ng-container>
<ng-container matColumnDef="errorMessage"> <ng-container matColumnDef="errorMessage">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.FAILEDEVENTS.ERRORMESSAGE' | translate }}</th> <th mat-header-cell *matHeaderCellDef>{{ 'IAM.FAILEDEVENTS.ERRORMESSAGE' | translate }}</th>
<td mat-cell *matCellDef="let event"> <td mat-cell *matCellDef="let event">

View File

@ -22,6 +22,7 @@ export class FailedEventsComponent implements AfterViewInit {
'database', 'database',
'failedSequence', 'failedSequence',
'failureCount', 'failureCount',
'lastFailed',
'errorMessage', 'errorMessage',
'actions', 'actions',
]; ];

View File

@ -73,7 +73,6 @@ export class OrgCreateComponent {
private _location: Location, private _location: Location,
private fb: UntypedFormBuilder, private fb: UntypedFormBuilder,
private mgmtService: ManagementService, private mgmtService: ManagementService,
private authService: GrpcAuthService,
breadcrumbService: BreadcrumbService, breadcrumbService: BreadcrumbService,
) { ) {
const instanceBread = new Breadcrumb({ const instanceBread = new Breadcrumb({
@ -83,16 +82,6 @@ export class OrgCreateComponent {
}); });
breadcrumbService.setBreadcrumb([instanceBread]); breadcrumbService.setBreadcrumb([instanceBread]);
this.authService
.isAllowed(['iam.write'])
.pipe(take(1))
.subscribe((allowed) => {
if (allowed) {
this.forSelf = false;
}
});
this.initForm(); this.initForm();
this.adminService.getSupportedLanguages().then((supportedResp) => { this.adminService.getSupportedLanguages().then((supportedResp) => {

View File

@ -89,8 +89,8 @@ export class ProjectGrantCreateComponent implements OnInit, OnDestroy {
} }
} }
public selectRoles(roles: Role.AsObject[]): void { public selectRoles(roles: string[]): void {
this.rolesKeyList = roles.map((role) => role.key); this.rolesKeyList = roles;
} }
public next(): void { public next(): void {

View File

@ -208,8 +208,8 @@ export class UserGrantCreateComponent implements OnDestroy {
} }
} }
public selectRoles(roles: Role.AsObject[]): void { public selectRoles(roleKeys: string[]): void {
this.rolesList = roles.map((role) => role.key); this.rolesList = roleKeys;
} }
public next(): void { public next(): void {

View File

@ -30,7 +30,7 @@
required required
[ngStyle]="{ 'padding-right': suffixPadding ? suffixPadding : '10px' }" [ngStyle]="{ 'padding-right': suffixPadding ? suffixPadding : '10px' }"
/> />
<span #suffix *ngIf="envSuffixLabel" cnslSuffix>{{ envSuffixLabel }}</span> <span #suffix *ngIf="envSuffix" cnslSuffix>{{ envSuffix }}</span>
<span cnslError *ngIf="userName?.invalid && userName?.errors?.required"> <span cnslError *ngIf="userName?.invalid && userName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }} {{ 'USER.VALIDATION.REQUIRED' | translate }}

View File

@ -46,8 +46,6 @@ export class UserCreateComponent implements OnDestroy {
public languages: string[] = ['de', 'en', 'it', 'fr']; public languages: string[] = ['de', 'en', 'it', 'fr'];
public userForm!: UntypedFormGroup; public userForm!: UntypedFormGroup;
public pwdForm!: UntypedFormGroup; public pwdForm!: UntypedFormGroup;
public envSuffixLabel: string = '';
private destroyed$: Subject<void> = new Subject(); private destroyed$: Subject<void> = new Subject();
public userLoginMustBeDomain: boolean = false; public userLoginMustBeDomain: boolean = false;
@ -84,14 +82,12 @@ export class UserCreateComponent implements OnDestroy {
} }
this.initForm(); this.initForm();
this.loading = false; this.loading = false;
this.envSuffixLabel = this.envSuffix();
this.changeDetRef.detectChanges(); this.changeDetRef.detectChanges();
}) })
.catch((error) => { .catch((error) => {
console.error(error); console.error(error);
this.initForm(); this.initForm();
this.loading = false; this.loading = false;
this.envSuffixLabel = this.envSuffix();
this.changeDetRef.detectChanges(); this.changeDetRef.detectChanges();
}); });
@ -249,7 +245,8 @@ export class UserCreateComponent implements OnDestroy {
public get confirmPassword(): AbstractControl | null { public get confirmPassword(): AbstractControl | null {
return this.pwdForm.get('confirmPassword'); return this.pwdForm.get('confirmPassword');
} }
private envSuffix(): string {
public get envSuffix(): string {
if (this.userLoginMustBeDomain && this.primaryDomain?.domainName) { if (this.userLoginMustBeDomain && this.primaryDomain?.domainName) {
return `@${this.primaryDomain.domainName}`; return `@${this.primaryDomain.domainName}`;
} else { } else {

View File

@ -12,7 +12,9 @@
</cnsl-top-view> </cnsl-top-view>
<div *ngIf="loading" class="max-width-container"> <div *ngIf="loading" class="max-width-container">
<mat-progress-spinner diameter="25" color="primary" mode="indeterminate"></mat-progress-spinner> <div class="user-spinner-wrapper">
<mat-progress-spinner diameter="25" color="primary" mode="indeterminate"></mat-progress-spinner>
</div>
</div> </div>
<div class="max-width-container"> <div class="max-width-container">
@ -57,6 +59,7 @@
<cnsl-contact <cnsl-contact
*ngIf="user.human" *ngIf="user.human"
[human]="user.human" [human]="user.human"
[username]="user.preferredLoginName"
[state]="user.state" [state]="user.state"
[canWrite]="true" [canWrite]="true"
(editType)="openEditDialog($event)" (editType)="openEditDialog($event)"
@ -97,7 +100,12 @@
</div> </div>
<div class="right"> <div class="right">
<a matTooltip="{{ 'USER.PASSWORD.SET' | translate }}" [routerLink]="['password']" mat-icon-button> <a
matTooltip="{{ 'USER.PASSWORD.SET' | translate }}"
[routerLink]="['password']"
[queryParams]="{ username: user.preferredLoginName }"
mat-icon-button
>
<i class="las la-pen"></i> <i class="las la-pen"></i>
</a> </a>
</div> </div>

View File

@ -88,6 +88,10 @@
} }
} }
.user-spinner-wrapper {
margin: 1rem 0;
}
.icon-button { .icon-button {
.icon { .icon {
font-size: 1.2rem; font-size: 1.2rem;

View File

@ -15,6 +15,7 @@ export class ContactComponent {
@Input() disablePhoneCode: boolean = false; @Input() disablePhoneCode: boolean = false;
@Input() canWrite: boolean | null = false; @Input() canWrite: boolean | null = false;
@Input() human?: Human.AsObject; @Input() human?: Human.AsObject;
@Input() username: string = '';
@Input() state!: UserState; @Input() state!: UserState;
@Output() editType: EventEmitter<EditDialogType> = new EventEmitter<EditDialogType>(); @Output() editType: EventEmitter<EditDialogType> = new EventEmitter<EditDialogType>();
@Output() resendEmailVerification: EventEmitter<void> = new EventEmitter<void>(); @Output() resendEmailVerification: EventEmitter<void> = new EventEmitter<void>();

View File

@ -1,12 +1,17 @@
<cnsl-detail-layout [hasBackButton]="true" title="{{ 'USER.PASSWORD.TITLE' | translate }}"> <cnsl-detail-layout [hasBackButton]="true" title="{{ 'USER.PASSWORD.TITLE' | translate }}">
<p class="password-info cnsl-secondary-text" sub>{{ 'USER.PASSWORD.DESCRIPTION' | translate }}</p> <p class="password-info cnsl-secondary-text" sub>{{ 'USER.PASSWORD.DESCRIPTION' | translate }}</p>
<ng-container *ngIf="userId; else authUser"> <ng-container *ngIf="userId; else authUser">
<form <form *ngIf="passwordForm" [formGroup]="passwordForm" (ngSubmit)="setInitialPassword(userId)">
*ngIf="passwordForm" <input
autocomplete="new-password" *ngIf="username"
[formGroup]="passwordForm" class="hiddeninput"
(ngSubmit)="setInitialPassword(userId)" type="hidden"
> autocomplete="username"
name="username"
type="text"
[value]="username"
/>
<div class="password-content"> <div class="password-content">
<div class="side-by-side"> <div class="side-by-side">
<cnsl-form-field class="formfield"> <cnsl-form-field class="formfield">
@ -39,6 +44,16 @@
<ng-template #authUser> <ng-template #authUser>
<form *ngIf="passwordForm" [formGroup]="passwordForm" (ngSubmit)="setPassword()"> <form *ngIf="passwordForm" [formGroup]="passwordForm" (ngSubmit)="setPassword()">
<input
*ngIf="username"
class="hiddeninput"
type="hidden"
autocomplete="username"
name="username"
type="text"
[value]="username"
/>
<div class="password-content"> <div class="password-content">
<cnsl-form-field class="formfield"> <cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PASSWORD.OLD' | translate }}</cnsl-label> <cnsl-label>{{ 'USER.PASSWORD.OLD' | translate }}</cnsl-label>

View File

@ -3,35 +3,40 @@
font-size: 14px; font-size: 14px;
} }
.password-content { form {
display: flex; .hiddeninput {
flex-direction: row; display: none;
flex-wrap: wrap; }
max-width: 40rem;
.side-by-side { .password-content {
display: flex; display: flex;
flex-wrap: wrap;
flex-direction: row; flex-direction: row;
margin: 0 -0.5rem; flex-wrap: wrap;
width: 100%; max-width: 40rem;
@media only screen and (max-width: 500px) { .side-by-side {
flex-direction: column; display: flex;
flex-wrap: wrap;
flex-direction: row;
margin: 0 -0.5rem;
width: 100%;
@media only screen and (max-width: 500px) {
flex-direction: column;
}
.formfield {
flex: 1;
margin: 0 0.5rem;
}
} }
.formfield { .formfield {
flex: 1; max-width: 400px;
margin: 0 0.5rem; width: 100%;
} }
} }
.formfield {
max-width: 400px;
width: 100%;
}
} }
.validation { .validation {
&.between { &.between {
margin: 0 0.5rem; margin: 0 0.5rem;

View File

@ -1,7 +1,7 @@
import { Component, OnDestroy } from '@angular/core'; import { Component, OnDestroy } from '@angular/core';
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { Subscription, take } from 'rxjs'; import { Subject, Subscription, take, takeUntil } from 'rxjs';
import { lowerCaseValidator, numberValidator, symbolValidator, upperCaseValidator } from 'src/app/pages/validators'; import { lowerCaseValidator, numberValidator, symbolValidator, upperCaseValidator } from 'src/app/pages/validators';
import { PasswordComplexityPolicy } from 'src/app/proto/generated/zitadel/policy_pb'; import { PasswordComplexityPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service'; import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
@ -31,11 +31,13 @@ function passwordConfirmValidator(c: AbstractControl): any {
}) })
export class PasswordComponent implements OnDestroy { export class PasswordComponent implements OnDestroy {
userId: string = ''; userId: string = '';
public username: string = '';
public policy!: PasswordComplexityPolicy.AsObject; public policy!: PasswordComplexityPolicy.AsObject;
public passwordForm!: UntypedFormGroup; public passwordForm!: UntypedFormGroup;
private formSub: Subscription = new Subscription(); private formSub: Subscription = new Subscription();
private destroy$: Subject<void> = new Subject();
constructor( constructor(
activatedRoute: ActivatedRoute, activatedRoute: ActivatedRoute,
@ -45,11 +47,14 @@ export class PasswordComponent implements OnDestroy {
private toast: ToastService, private toast: ToastService,
private breadcrumbService: BreadcrumbService, private breadcrumbService: BreadcrumbService,
) { ) {
activatedRoute.params.subscribe((data) => { activatedRoute.queryParams.pipe(takeUntil(this.destroy$)).subscribe((data) => {
const { username } = data;
this.username = username;
});
activatedRoute.params.pipe(takeUntil(this.destroy$)).subscribe((data) => {
const { id } = data; const { id } = data;
if (id) { if (id) {
this.userId = id; this.userId = id;
breadcrumbService.setBreadcrumb([ breadcrumbService.setBreadcrumb([
new Breadcrumb({ new Breadcrumb({
type: BreadcrumbType.ORG, type: BreadcrumbType.ORG,
@ -59,6 +64,7 @@ export class PasswordComponent implements OnDestroy {
} else { } else {
this.authService.user.pipe(take(1)).subscribe((user) => { this.authService.user.pipe(take(1)).subscribe((user) => {
if (user) { if (user) {
this.username = user.preferredLoginName;
this.breadcrumbService.setBreadcrumb([ this.breadcrumbService.setBreadcrumb([
new Breadcrumb({ new Breadcrumb({
type: BreadcrumbType.AUTHUSER, type: BreadcrumbType.AUTHUSER,
@ -102,6 +108,8 @@ export class PasswordComponent implements OnDestroy {
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
this.formSub.unsubscribe(); this.formSub.unsubscribe();
} }

View File

@ -103,6 +103,7 @@
<cnsl-contact <cnsl-contact
[disablePhoneCode]="true" [disablePhoneCode]="true"
[state]="user.state" [state]="user.state"
[username]="user.preferredLoginName"
[canWrite]="['user.write:' + user.id, 'user.write$'] | hasRole | async" [canWrite]="['user.write:' + user.id, 'user.write$'] | hasRole | async"
*ngIf="user?.human" *ngIf="user?.human"
[human]="user.human" [human]="user.human"
@ -186,6 +187,7 @@
matTooltip="{{ 'USER.PASSWORD.SET' | translate }}" matTooltip="{{ 'USER.PASSWORD.SET' | translate }}"
[disabled]="(['user.write:' + user.id, 'user.write$'] | hasRole | async) === false" [disabled]="(['user.write:' + user.id, 'user.write$'] | hasRole | async) === false"
[routerLink]="['password']" [routerLink]="['password']"
[queryParams]="{ username: user.preferredLoginName }"
mat-icon-button mat-icon-button
> >
<i class="las la-pen"></i> <i class="las la-pen"></i>

View File

@ -256,8 +256,10 @@ export class UserTableComponent implements OnInit {
confirmKey: 'ACTIONS.DELETE', confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL', cancelKey: 'ACTIONS.CANCEL',
titleKey: 'USER.DIALOG.DELETE_SELF_TITLE', titleKey: 'USER.DIALOG.DELETE_SELF_TITLE',
descriptionKey: 'USER.DIALOG.DELETE_SELF_DESCRIPTION', warnSectionKey: 'USER.DIALOG.DELETE_SELF_DESCRIPTION',
confirmationKey: 'USER.DIALOG.TYPEUSERNAME', hintKey: 'USER.DIALOG.TYPEUSERNAME',
hintParam: 'USER.DIALOG.DELETE_DESCRIPTION',
confirmationKey: 'USER.DIALOG.USERNAME',
confirmation: user.preferredLoginName, confirmation: user.preferredLoginName,
}; };
@ -265,8 +267,10 @@ export class UserTableComponent implements OnInit {
confirmKey: 'ACTIONS.DELETE', confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL', cancelKey: 'ACTIONS.CANCEL',
titleKey: 'USER.DIALOG.DELETE_TITLE', titleKey: 'USER.DIALOG.DELETE_TITLE',
descriptionKey: 'USER.DIALOG.DELETE_DESCRIPTION', warnSectionKey: 'USER.DIALOG.DELETE_DESCRIPTION',
confirmationKey: 'USER.DIALOG.TYPEUSERNAME', hintKey: 'USER.DIALOG.TYPEUSERNAME',
hintParam: 'USER.DIALOG.DELETE_DESCRIPTION',
confirmationKey: 'USER.DIALOG.USERNAME',
confirmation: user.preferredLoginName, confirmation: user.preferredLoginName,
}; };

View File

@ -13,17 +13,17 @@ export interface Color {
contrastColor: string; contrastColor: string;
} }
const DARK_PRIMARY = '#bbbafa'; export const DARK_PRIMARY = '#2073c4';
const PRIMARY = '#5469d4'; export const PRIMARY = '#5469d4';
const DARK_WARN = '#ff3b5b'; export const DARK_WARN = '#ff3b5b';
const WARN = '#cd3d56'; export const WARN = '#cd3d56';
const DARK_BACKGROUND = '#111827'; export const DARK_BACKGROUND = '#111827';
const BACKGROUND = '#fafafa'; export const BACKGROUND = '#fafafa';
const DARK_TEXT = '#ffffff'; export const DARK_TEXT = '#ffffff';
const TEXT = '#000000'; export const TEXT = '#000000';
@Injectable() @Injectable()
export class ThemeService { export class ThemeService {

View File

@ -256,6 +256,7 @@
"DELETE_SELF_DESCRIPTION": "Sie sind im Begriff Ihren eigenen Benutzer endgültig zu löschen. Dadurch werden Sie ausgeloggt und Ihr Account wird gelöscht. Diese Aktion kann nicht rückgängig gemacht werden!", "DELETE_SELF_DESCRIPTION": "Sie sind im Begriff Ihren eigenen Benutzer endgültig zu löschen. Dadurch werden Sie ausgeloggt und Ihr Account wird gelöscht. Diese Aktion kann nicht rückgängig gemacht werden!",
"DELETE_AUTH_DESCRIPTION": "Sie sind im Begriff Ihren Account endgültig zu löschen. Wollen Sie dies wirklich tun?", "DELETE_AUTH_DESCRIPTION": "Sie sind im Begriff Ihren Account endgültig zu löschen. Wollen Sie dies wirklich tun?",
"TYPEUSERNAME": "Wiederholen Sie '{{value}}', um den Benutzer zu löschen.", "TYPEUSERNAME": "Wiederholen Sie '{{value}}', um den Benutzer zu löschen.",
"USERNAME": "Loginname",
"DELETE_BTN": "Endgültig löschen" "DELETE_BTN": "Endgültig löschen"
}, },
"SENDEMAILDIALOG": { "SENDEMAILDIALOG": {
@ -742,6 +743,7 @@
"DATABASE": "Datenbank", "DATABASE": "Datenbank",
"FAILEDSEQUENCE": "betroffene Sequenz", "FAILEDSEQUENCE": "betroffene Sequenz",
"FAILURECOUNT": "Fehleranzahl", "FAILURECOUNT": "Fehleranzahl",
"LASTFAILED": "Zuletzt gescheitert um",
"ERRORMESSAGE": "Meldung", "ERRORMESSAGE": "Meldung",
"ACTIONS": "Aktionen", "ACTIONS": "Aktionen",
"DELETE": "Entfernen", "DELETE": "Entfernen",

View File

@ -256,6 +256,7 @@
"DELETE_SELF_DESCRIPTION": "You are about to permanently delete your personal account. This will log you out and delete your user. This action cannot be undone!", "DELETE_SELF_DESCRIPTION": "You are about to permanently delete your personal account. This will log you out and delete your user. This action cannot be undone!",
"DELETE_AUTH_DESCRIPTION": "You are about to permanently delete your personal account. Are you sure?", "DELETE_AUTH_DESCRIPTION": "You are about to permanently delete your personal account. Are you sure?",
"TYPEUSERNAME": "Type '{{value}}', to confirm and delete the user.", "TYPEUSERNAME": "Type '{{value}}', to confirm and delete the user.",
"USERNAME": "Loginname",
"DELETE_BTN": "Delete permanently" "DELETE_BTN": "Delete permanently"
}, },
"SENDEMAILDIALOG": { "SENDEMAILDIALOG": {
@ -742,6 +743,7 @@
"DATABASE": "Database", "DATABASE": "Database",
"FAILEDSEQUENCE": "Failed Sequence", "FAILEDSEQUENCE": "Failed Sequence",
"FAILURECOUNT": "Failure Count", "FAILURECOUNT": "Failure Count",
"LASTFAILED": "Last failed at",
"ERRORMESSAGE": "Error Message", "ERRORMESSAGE": "Error Message",
"ACTIONS": "Actions", "ACTIONS": "Actions",
"DELETE": "Remove", "DELETE": "Remove",

View File

@ -256,6 +256,7 @@
"DELETE_SELF_DESCRIPTION": "Vous êtes sur le point de supprimer définitivement votre compte personnel. Cela vous déconnectera et supprimera votre utilisateur. Cette action ne peut être annulée !", "DELETE_SELF_DESCRIPTION": "Vous êtes sur le point de supprimer définitivement votre compte personnel. Cela vous déconnectera et supprimera votre utilisateur. Cette action ne peut être annulée !",
"DELETE_AUTH_DESCRIPTION": "Vous êtes sur le point de supprimer définitivement votre compte personnel. En êtes-vous sûr ?", "DELETE_AUTH_DESCRIPTION": "Vous êtes sur le point de supprimer définitivement votre compte personnel. En êtes-vous sûr ?",
"TYPEUSERNAME": "Tapez '{{value}}', pour confirmer et supprimer l'utilisateur.", "TYPEUSERNAME": "Tapez '{{value}}', pour confirmer et supprimer l'utilisateur.",
"USERNAME": "Identifiant",
"DELETE_BTN": "Supprimer définitivement" "DELETE_BTN": "Supprimer définitivement"
}, },
"SENDEMAILDIALOG": { "SENDEMAILDIALOG": {
@ -742,6 +743,7 @@
"DATABASE": "Base de données", "DATABASE": "Base de données",
"FAILEDSEQUENCE": "Séquence échouée", "FAILEDSEQUENCE": "Séquence échouée",
"FAILURECOUNT": "Nombre d'échecs", "FAILURECOUNT": "Nombre d'échecs",
"LASTFAILED": "Dernier échec à",
"ERRORMESSAGE": "Message d'erreur", "ERRORMESSAGE": "Message d'erreur",
"ACTIONS": "Actions", "ACTIONS": "Actions",
"DELETE": "Supprimer", "DELETE": "Supprimer",

View File

@ -256,6 +256,7 @@
"DELETE_SELF_DESCRIPTION": "Stai per eliminare definitivamente il tuo account. Questo ti disconnetterà ed eliminerà il tuo utente. Questa azione non può essere annullata!", "DELETE_SELF_DESCRIPTION": "Stai per eliminare definitivamente il tuo account. Questo ti disconnetterà ed eliminerà il tuo utente. Questa azione non può essere annullata!",
"DELETE_AUTH_DESCRIPTION": "Stai per eleminare il tuo account personale in modo permanente. Vuoi continuare?", "DELETE_AUTH_DESCRIPTION": "Stai per eleminare il tuo account personale in modo permanente. Vuoi continuare?",
"TYPEUSERNAME": "Ripeti '{{value}}' per confermare ed eliminare l'utente.", "TYPEUSERNAME": "Ripeti '{{value}}' per confermare ed eliminare l'utente.",
"USERNAME": "Nome utente",
"DELETE_BTN": "Elimina" "DELETE_BTN": "Elimina"
}, },
"SENDEMAILDIALOG": { "SENDEMAILDIALOG": {
@ -742,6 +743,7 @@
"DATABASE": "Database", "DATABASE": "Database",
"FAILEDSEQUENCE": "Sequenza fallita", "FAILEDSEQUENCE": "Sequenza fallita",
"FAILURECOUNT": "Conteggio dei fallimenti", "FAILURECOUNT": "Conteggio dei fallimenti",
"LASTFAILED": "L'ultimo fallimento a",
"ERRORMESSAGE": "Messaggio di errore", "ERRORMESSAGE": "Messaggio di errore",
"ACTIONS": "Azioni", "ACTIONS": "Azioni",
"DELETE": "Rimuovi", "DELETE": "Rimuovi",
@ -868,7 +870,7 @@
"GENERAL": "Generale", "GENERAL": "Generale",
"LOGIN": "Comportamento login e sicurezza", "LOGIN": "Comportamento login e sicurezza",
"LOCKOUT": "Meccanismi di bloccaggio", "LOCKOUT": "Meccanismi di bloccaggio",
"COMPLEXITY": "complessità della password", "COMPLEXITY": "Complessità della password",
"NOTIFICATIONS": "Notifiche", "NOTIFICATIONS": "Notifiche",
"NOTIFICATIONS_DESC": "Impostazioni SMTP e SMS", "NOTIFICATIONS_DESC": "Impostazioni SMTP e SMS",
"MESSAGETEXTS": "Testi di notifica", "MESSAGETEXTS": "Testi di notifica",

View File

@ -256,6 +256,7 @@
"DELETE_SELF_DESCRIPTION": "您即将永久删除您的个人帐户。这将使您注销并删除您的用户。此操作无法撤消!", "DELETE_SELF_DESCRIPTION": "您即将永久删除您的个人帐户。这将使您注销并删除您的用户。此操作无法撤消!",
"DELETE_AUTH_DESCRIPTION": "您即将永久删除您的个人帐户。你确定吗?", "DELETE_AUTH_DESCRIPTION": "您即将永久删除您的个人帐户。你确定吗?",
"TYPEUSERNAME": "输入 '{{value}}',确认并删除该用户。", "TYPEUSERNAME": "输入 '{{value}}',确认并删除该用户。",
"USERNAME": "登录名",
"DELETE_BTN": "永久删除" "DELETE_BTN": "永久删除"
}, },
"SENDEMAILDIALOG": { "SENDEMAILDIALOG": {
@ -742,6 +743,7 @@
"DATABASE": "数据库", "DATABASE": "数据库",
"FAILEDSEQUENCE": "失败的序列", "FAILEDSEQUENCE": "失败的序列",
"FAILURECOUNT": "失败计数", "FAILURECOUNT": "失败计数",
"LASTFAILED": "最后一次失败是在",
"ERRORMESSAGE": "错误消息", "ERRORMESSAGE": "错误消息",
"ACTIONS": "操作", "ACTIONS": "操作",
"DELETE": "删除", "DELETE": "删除",

View File

@ -20,7 +20,14 @@ yarn install
yarn start yarn start
``` ```
This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server. As an alternative you can use this docker compose command:
```console
docker compose up
```
These commands start a local development server.
Most changes are reflected live without having to restart the server.
## Build ## Build

View File

@ -1,3 +1,3 @@
module.exports = { module.exports = {
presets: [require.resolve('@docusaurus/core/lib/babel/preset')], presets: [require.resolve("@docusaurus/core/lib/babel/preset")],
}; };

11
docs/docker-compose.yml Normal file
View File

@ -0,0 +1,11 @@
version: '3'
services:
docusaurus:
image: node:lts-alpine3.15
working_dir: /app
volumes:
- ./:/app
ports:
- 3000:3000
command: sh -c "yarn install && yarn start --host 0.0.0.0"

View File

@ -104,3 +104,4 @@ ZITADEL supports only the external authentication flow at the moment.
- [Actions concept](../concepts/features/actions) - [Actions concept](../concepts/features/actions)
- [Actions guide](../guides/manage/customize/behavior) - [Actions guide](../guides/manage/customize/behavior)
- [Actions Marketplace: Find example actions to use in ZITADEL](https://github.com/zitadel/actions)

View File

@ -2020,6 +2020,7 @@ This is an empty request
| failed_sequence | uint64 | - | | | failed_sequence | uint64 | - | |
| failure_count | uint64 | - | | | failure_count | uint64 | - | |
| error_message | string | - | | | error_message | string | - | |
| last_failed | google.protobuf.Timestamp | - | |
@ -4633,7 +4634,7 @@ this is en empty request
| database | string | - | | | database | string | - | |
| view_name | string | - | | | view_name | string | - | |
| processed_sequence | uint64 | - | | | processed_sequence | uint64 | - | |
| event_timestamp | google.protobuf.Timestamp | The timestamp the event occured | | | event_timestamp | google.protobuf.Timestamp | The timestamp the event occurred | |
| last_successful_spooler_run | google.protobuf.Timestamp | - | | | last_successful_spooler_run | google.protobuf.Timestamp | - | |

View File

@ -5408,6 +5408,7 @@ This is an empty response
| password_change_required | bool | - | | | password_change_required | bool | - | |
| request_passwordless_registration | bool | - | | | request_passwordless_registration | bool | - | |
| otp_code | string | - | | | otp_code | string | - | |
| idps | repeated ImportHumanUserRequest.IDP | - | |
@ -5436,6 +5437,19 @@ This is an empty response
### ImportHumanUserRequest.IDP
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| config_id | string | internal id of the IDP in ZITADEL | string.min_len: 1<br /> string.max_len: 200<br /> |
| external_user_id | string | id of the user on the IDP | string.min_len: 1<br /> string.max_len: 200<br /> |
| display_name | string | (display) name of the user on the IDP | string.max_len: 200<br /> |
### ImportHumanUserRequest.Phone ### ImportHumanUserRequest.Phone

View File

@ -506,6 +506,7 @@ This is an empty response
| failed_sequence | uint64 | - | | | failed_sequence | uint64 | - | |
| failure_count | uint64 | - | | | failure_count | uint64 | - | |
| error_message | string | - | | | error_message | string | - | |
| last_failed | google.protobuf.Timestamp | - | |
@ -687,6 +688,7 @@ This is an empty request
| database | string | - | string.min_len: 1<br /> string.max_len: 200<br /> | | database | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| view_name | string | - | string.min_len: 1<br /> string.max_len: 200<br /> | | view_name | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| failed_sequence | uint64 | - | | | failed_sequence | uint64 | - | |
| instance_id | string | - | |

View File

@ -113,6 +113,18 @@ title: zitadel/user.proto
### LoginNameQuery
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| login_name | string | - | string.max_len: 200<br /> |
| method | zitadel.v1.TextQueryMethod | - | enum.defined_only: true<br /> |
### Machine ### Machine
@ -288,6 +300,7 @@ this query is always equals
| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) query.email_query | EmailQuery | - | | | [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) query.email_query | EmailQuery | - | |
| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) query.state_query | StateQuery | - | | | [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) query.state_query | StateQuery | - | |
| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) query.type_query | TypeQuery | - | | | [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) query.type_query | TypeQuery | - | |
| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) query.login_name_query | LoginNameQuery | - | |

View File

@ -35,4 +35,5 @@ Within the JavaScript code, you can read and manipulate the state.
## Further reading ## Further reading
- [Assign users a role after they register using an external identity provider](../../guides/manage/customize/behavior) - [Assign users a role after they register using an external identity provider](../../guides/manage/customize/behavior)
- [Actions reference](../../apis/actions) - [Actions reference](../../apis/actions)
- [Actions Marketplace: Find example actions to use in ZITADEL](https://github.com/zitadel/actions)

View File

@ -7,7 +7,7 @@ It shows how to add user login to your application and fetch some data from the
At the end of the guide, your application has login functionality and has access to the current user's profile. At the end of the guide, your application has login functionality and has access to the current user's profile.
> This documentation refers to our [example](https://github.com/zitadel/zitadel-examples/tree/main/angular) in GitHub. Note that we've written ZITADEL Console in Angular, so you can also use that as a reference. > This documentation refers to our [example](https://github.com/zitadel/zitadel-angular) in GitHub. Note that we've written ZITADEL Console in Angular, so you can also use that as a reference.
## Setup Application and Get Keys ## Setup Application and Get Keys
@ -22,7 +22,7 @@ We recommend you use [Authorization Code](../../apis/openidoauth/grant-types#aut
With the Redirect URIs field, you tell ZITADEL where it is allowed to redirect users to after authentication. For development, you can set dev mode to `true` to enable insecure HTTP and redirect to a `localhost` URI. With the Redirect URIs field, you tell ZITADEL where it is allowed to redirect users to after authentication. For development, you can set dev mode to `true` to enable insecure HTTP and redirect to a `localhost` URI.
> If you are following along with the [example](https://github.com/zitadel/zitadel-examples/tree/main/angular), set dev mode to `true` and the Redirect URIs to <http://localhost:4200/auth/callback>. > If you are following along with the [example](https://github.com/zitadel/zitadel-angular), set dev mode to `true` and the Redirect URIs to <http://localhost:4200/auth/callback>.
If you want to redirect the users back to a route on your application after they have logged out, add an optional redirect in the Post Logout URIs field. If you want to redirect the users back to a route on your application after they have logged out, add an optional redirect in the Post Logout URIs field.
@ -48,34 +48,8 @@ npm install angular-oauth2-oidc
Add _OAuthModule_ to your Angular imports in _AppModule_ and provide the _AuthConfig_ in the providers' section. Also, ensure you import the _HTTPClientModule_. Add _OAuthModule_ to your Angular imports in _AppModule_ and provide the _AuthConfig_ in the providers' section. Also, ensure you import the _HTTPClientModule_.
```ts ```ts reference
... https://github.com/zitadel/zitadel-angular/blob/main/src/app/app.module.ts
import { AuthConfig, OAuthModule } from 'angular-oauth2-oidc';
import { HttpClientModule } from '@angular/common/http';
const authConfig: AuthConfig = {
scope: 'openid profile email',
responseType: 'code',
oidc: true,
clientId: 'YOUR-CLIENT-ID', // replace with your appid
issuer: 'https:/[your-domain]-[random-string].zitadel.cloud', // replace with your instance
redirectUri: 'http://localhost:4200/auth/callback',
postLogoutRedirectUri: 'http://localhost:4200/signedout', // optional
requireHttps: false // required for running locally
};
@NgModule({
...
imports: [
OAuthModule.forRoot(),
HttpClientModule,
...
providers: [
{
provide: AuthConfig,
useValue: authConfig
}
...
``` ```
Set _openid_, _profile_ and _email_ as scope, _code_ as responseType, and oidc to _true_. Then create an authentication service to provide the functions to authenticate your user. Set _openid_, _profile_ and _email_ as scope, _code_ as responseType, and oidc to _true_. Then create an authentication service to provide the functions to authenticate your user.
@ -88,103 +62,25 @@ ng g service services/authentication
Copy the following code to your service. This code provides a function `authenticate()` which redirects the user to ZITADEL. After successful login, ZITADEL redirects the user back to the redirect URI configured in _AuthModule_ and ZITADEL Console. Make sure both correspond, otherwise ZITADEL throws an error. Copy the following code to your service. This code provides a function `authenticate()` which redirects the user to ZITADEL. After successful login, ZITADEL redirects the user back to the redirect URI configured in _AuthModule_ and ZITADEL Console. Make sure both correspond, otherwise ZITADEL throws an error.
```ts ```ts reference
import { Injectable } from "@angular/core"; https://github.com/zitadel/zitadel-angular/blob/main/src/app/services/authentication.service.ts
import { AuthConfig, OAuthService } from "angular-oauth2-oidc";
import { BehaviorSubject, from, Observable } from "rxjs";
import { StatehandlerService } from "./statehandler.service";
@Injectable({
providedIn: "root",
})
export class AuthenticationService {
private _authenticated: boolean = false;
private readonly _authenticationChanged: BehaviorSubject<boolean> =
new BehaviorSubject(this.authenticated);
constructor(
private oauthService: OAuthService,
private authConfig: AuthConfig,
private statehandler: StatehandlerService
) {}
public get authenticated(): boolean {
return this._authenticated;
}
public get authenticationChanged(): Observable<boolean> {
return this._authenticationChanged;
}
public getOIDCUser(): Observable<any> {
return from(this.oauthService.loadUserProfile());
}
public async authenticate(setState: boolean = true): Promise<boolean> {
this.oauthService.configure(this.authConfig);
this.oauthService.strictDiscoveryDocumentValidation = false;
await this.oauthService.loadDiscoveryDocumentAndTryLogin();
this._authenticated = this.oauthService.hasValidAccessToken();
if (!this.oauthService.hasValidIdToken() || !this.authenticated) {
const newState = setState
? await this.statehandler.createState().toPromise()
: undefined;
this.oauthService.initCodeFlow(newState);
}
this._authenticationChanged.next(this.authenticated);
return this.authenticated;
}
public signout(): void {
this.oauthService.logOut();
this._authenticated = false;
this._authenticationChanged.next(false);
}
}
``` ```
Our example includes a _StatehandlerService_ to redirect the user back to the route where he initially came from. Our example includes a _StatehandlerService_ to redirect the user back to the route where he initially came from.
If you don't need such behavior, you can omit the following line from the `authenticate()` method above. If you don't need such behavior, you can omit the following line from the `authenticate()` method above.
```ts ```ts reference
... https://github.com/zitadel/zitadel-angular/blob/main/src/app/services/authentication.service.ts#L45
const newState = setState ? await this.statehandler.createState().toPromise() : undefined;
...
``` ```
If you decide to use the _StatehandlerService_, provide it in the `app.module`. Make sure it gets initialized first using Angulars `APP_INITIALIZER`. You find the service implementation in the [example](https://github.com/zitadel/zitadel-examples/tree/main/angular). If you decide to use the _StatehandlerService_, provide it in the `app.module`. Make sure it gets initialized first using Angulars `APP_INITIALIZER`. You find the service implementation in the [example](https://github.com/zitadel/zitadel-angular).
```ts ```ts reference
https://github.com/zitadel/zitadel-angular/blob/main/src/app/app.module.ts#L26-L30
```
const stateHandlerFn = (stateHandler: StatehandlerService) => { ```ts reference
return () => { https://github.com/zitadel/zitadel-angular/blob/main/src/app/app.module.ts#L55-L78
return stateHandler.initStateHandler();
};
};
...
providers: [
{
provide: APP_INITIALIZER,
useFactory: stateHandlerFn,
multi: true,
deps: [StatehandlerService],
},
{
provide: StatehandlerProcessorService,
useClass: StatehandlerProcessorServiceImpl,
},
{
provide: StatehandlerService,
useClass: StatehandlerServiceImpl,
},
]
...
``` ```
### Add Login to Your Application ### Add Login to Your Application
@ -213,77 +109,28 @@ ng g guard guards/auth
This code shows the _AuthGuard_ used in ZITADEL Console. This code shows the _AuthGuard_ used in ZITADEL Console.
```ts ```ts reference
import { Injectable } from "@angular/core"; https://github.com/zitadel/zitadel-angular/blob/main/src/app/guards/auth.guard.ts
import {
ActivatedRouteSnapshot,
CanActivate,
RouterStateSnapshot,
UrlTree,
} from "@angular/router";
import { Observable } from "rxjs";
import { AuthenticationService } from "../services/authentication.service";
@Injectable({
providedIn: "root",
})
export class AuthGuard implements CanActivate {
constructor(private auth: AuthenticationService) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
):
| Observable<boolean | UrlTree>
| Promise<boolean | UrlTree>
| boolean
| UrlTree {
if (!this.auth.authenticated) {
return this.auth.authenticate();
}
return this.auth.authenticated;
}
}
``` ```
Add the guard to your _RouterModule_ similar to this: Add the guard to your _RouterModule_ similar to this:
```ts ```ts reference
... https://github.com/zitadel/zitadel-angular/blob/main/src/app/app-routing.module.ts#L9-L31
const routes: Routes = [
{
path: '',
loadChildren: () => import('./pages/home/home.module').then(m => m.HomeModule),
canActivate: [AuthGuard],
},
...
``` ```
> Note: Make sure you redirect the user from your callback URL to a guarded page, so `authenticate()` is called again and the access token is stored. > Note: Make sure you redirect the user from your callback URL to a guarded page, so `authenticate()` is called again and the access token is stored.
```ts ```ts reference
... https://github.com/zitadel/zitadel-angular/blob/main/src/app/app-routing.module.ts#L19-L21
{
path: 'auth/callback',
redirectTo: 'user',
},
....
``` ```
### Add Logout to Your Application ### Add Logout to Your Application
Call `auth.signout()` for logging the current user out. Note that you can also configure a logout redirect URI if you want your users to be redirected after logout. Call `auth.signout()` for logging the current user out. Note that you can also configure a logout redirect URI if you want your users to be redirected after logout.
```ts ```ts reference
import { AuthenticationService } from "src/app/services/authentication.service"; https://github.com/zitadel/zitadel-angular/blob/main/src/app/components/user/user.component.ts#L20-L22
export class SomeComponentWithLogout {
constructor(private authService: AuthenticationService) {}
public signout(): Promise<void> {
return this.authService.signout();
}
}
``` ```
### Show User Information ### Show User Information
@ -291,29 +138,21 @@ export class SomeComponentWithLogout {
To fetch user data, ZITADEL's user info endpoint has to be called. This data contains sensitive information and artifacts related to the current user's identity and the scopes you defined in your _AuthConfig_. To fetch user data, ZITADEL's user info endpoint has to be called. This data contains sensitive information and artifacts related to the current user's identity and the scopes you defined in your _AuthConfig_.
Our _AuthenticationService_ already includes a method called _getOIDCUser()_. You can call it wherever you need this information. Our _AuthenticationService_ already includes a method called _getOIDCUser()_. You can call it wherever you need this information.
```ts ```ts reference
import { AuthenticationService } from 'src/app/services/authentication.service'; https://github.com/zitadel/zitadel-angular/blob/main/src/app/components/user/user.component.ts
public user$: Observable<any>;
constructor(private auth: AuthenticationService) {
this.user$ = this.auth.getOIDCUser();
}
``` ```
And in your HTML file: And in your HTML file:
```html ```html reference
<div *ngIf="user$ | async as user"> https://github.com/zitadel/zitadel-angular/blob/main/src/app/components/user/user.component.html
<p>{{user | json}}</p>
</div>
``` ```
## Completion ## Completion
You have successfully integrated your Angular application with ZITADEL! You have successfully integrated your Angular application with ZITADEL!
If you get stuck, consider checking out our [example](https://github.com/zitadel/zitadel-examples/tree/main/angular) application. It includes all the mentioned functionality of this quickstart. You can simply start by cloning the repository and replacing the _AuthConfig_ in the _AppModule_ by your own configuration. If you run into issues, contact us or raise an issue on [GitHub](https://github.com/zitadel/zitadel). If you get stuck, consider checking out our [example](https://github.com/zitadel/zitadel-angular) application. It includes all the mentioned functionality of this quickstart. You can simply start by cloning the repository and replacing the _AuthConfig_ in the _AppModule_ by your own configuration. If you run into issues, contact us or raise an issue on [GitHub](https://github.com/zitadel/zitadel).
![App in console](/img/angular/app-screen.png) ![App in console](/img/angular/app-screen.png)

View File

@ -2,61 +2,58 @@
title: Flutter title: Flutter
--- ---
This guide demonstrates how you integrate **ZITADEL** as an idendity provider to a Flutter app. This guide demonstrates how you integrate **ZITADEL** into a Flutter app. It refers to our example on [GitHub](https://github.com/zitadel/zitadel_flutter)
At the end of the guide you have a mobile application on Android and iOS with the ability At the end of the guide you have a mobile application for **Android**, **iOS** and **Web** with the ability to authenticate users via ZITADEL.
to authenticate users via ZITADEL.
If you need any other information about Flutter, head over to the [documentation](https://flutter.dev/). If you need any other information about Flutter, head over to the [documentation](https://flutter.dev/).
## Prerequisites ## Setup Application
Before we can start building our application, we have to do a few configuration steps in ZITADEL Console.
You will need to provide some information about your app. We recommend creating a new app to start from scratch. Navigate to your Project, then add a new application at the top of the page.
Select **Native** application type and continue.
![Create app in console](/img/flutter/nativeapp.png)
### Redirect URIs
With the Redirect URIs field, you tell ZITADEL where it is allowed to redirect users to after authentication.
As our application will also support web, we have to make sure to set redirects for http and https, as well as a **custom-scheme** for our native Android and IOS Setup.
For our local web development, add a redirectURI for `http://localhost:4444/auth.html` with your custom port. For Android and IOS, add your **custom scheme**. In our case it is `com.example.zitadelflutter`.
:::caution Use Custom Redirect URI!
Your custom scheme has to be compliant with the OAuth 2.0
authentication for mobile devices ([RFC 8252 specification](https://tools.ietf.org/html/rfc8252)).
Otherwise your app might get rejected.
:::
For development, you need to set dev mode to `true` to enable insecure HTTP and redirect to a `localhost` URI.
If you want to redirect the users back to a route on your application after they have logged out, add an optional redirect in the Post Logout URIs field.
Continue and create the application.
After creation, go to **token settings** and check the refresh token checkbox. This allows us to request a refresh_token via `offline_access` scope.
Make sure to save the application.
### Client ID
After successful app creation, a pop-up will appear, showing the app's client ID. Copy the client ID, as you will need it to configure your Flutter application.
## Flutter Prerequisites
To move further in this quickstart, you'll need the following things prepared: To move further in this quickstart, you'll need the following things prepared:
- Have Flutter (and Dart) installed ([how-to](https://flutter.dev/docs/get-started/install)) - Have Flutter (and Dart) installed ([how-to](https://flutter.dev/docs/get-started/install))
- Have an IDE set up for developing Flutter ([how-to](https://flutter.dev/docs/get-started/editor)) - Have an IDE set up for developing Flutter ([how-to](https://flutter.dev/docs/get-started/editor))
- Create a basic Flutter app ([how-to](https://flutter.dev/docs/get-started/codelab)) - Create a basic Flutter app ([how-to](https://flutter.dev/docs/get-started/codelab))
- Create a "Native" application in a ZITADEL project - Create a "Native" application in ZITADEL
## Flutter with ZITADEL After you created the starter Flutter app, the app will show a simple, templated Flutter app.
In your native application on ZITADEL, you need to add a callback (redirect) uri
which matches the selected custom url scheme. As an example, if you intend to
use `ch.myexample.app://sign-me-in` as redirect URI on ZITADEL and in your app,
you need to register the `ch.myexample.app://` custom url scheme within Android and iOS.
:::caution Use Custom Redirect URI!
You'll need the custom redirect url to be compliant with the OAuth 2.0
authentication for mobile devices ([RFC 8252 specification](https://tools.ietf.org/html/rfc8252)).
Otherwise your app might get rejected.
:::
### Hello World
After you created the basic Flutter app, the app will show the following screen:
<div style={{'text-align': 'center', 'margin-bottom': '1rem'}}>
<img src="/img/flutter/hello-world.png" alt="Flutter Hello World" height="500px" />
</div>
You may want to change the Flutter SDK version in `pubspec.yaml` from
```yaml
environment:
sdk: ">=2.7.0 <3.0.0"
```
to
```yaml
environment:
sdk: ">=2.12.0 <3.0.0"
```
With this, you'll enable "nullable by default" mode in Flutter, as well as new language features.
For this quickstart, the minimal Flutter SDK version is set to the default: `sdk: ">=2.7.0 <3.0.0"`.
### Install Dependencies ### Install Dependencies
@ -69,345 +66,112 @@ Basically, there are two major points in this specification:
2. It does not allow third party apps to open the browser for the login process, 2. It does not allow third party apps to open the browser for the login process,
the app must open the login page within the embedded browser view the app must open the login page within the embedded browser view
Install the [`appauth`](https://appauth.io/) package and a secure storage (to store the auth / refresh tokens): First install [http](https://pub.dev/packages/http) a library for making HTTP calls,
then [`flutter_web_auth_2`](https://pub.dev/packages/flutter_web_auth_2) and a secure storage to store the auth / refresh tokens [flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage).
To install run:
```bash ```bash
flutter pub add http flutter pub add http
flutter pub add flutter_appauth flutter pub add flutter_web_auth_2
flutter pub add flutter_secure_storage flutter pub add flutter_secure_storage
``` ```
#### Important on Android #### Setup for Android
To use this app auth method on Android 11, you'll need to add a `query` to the `AndroidManifest.xml`. Navigate to your `AndroidManifest.xml` at `<projectRoot>/android/app/src/main/AndroidManifest.xml` and add the following activity with your custom scheme.
Go to `<projectRoot>/android/app/src/main/AndroidManifest.xml` and add to the `<manifest>` root:
```xml title="<projectRoot>/android/app/src/main/AndroidManifest.xml" ```xml reference
<queries> https://github.com/zitadel/zitadel_flutter/blob/main/android/app/src/main/AndroidManifest.xml#L29-L38
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.APP_BROWSER" />
<data android:scheme="https" />
</intent>
</queries>
``` ```
This allows the app to query for internal browser activities.
Furthermore, for `secure_storage`, you need to set the minimum SDK version to 18 Furthermore, for `secure_storage`, you need to set the minimum SDK version to 18
in `<projectRoot>/android/app/src/build.gradle`. Then, add the manifest placeholder in `<projectRoot>/android/app/src/build.gradle`.
for your redirect url (the custom url scheme). In the end, the `defaultConfig`
section of the `build.gradle` file should look like this:
```groovy title="<projectRoot>/android/app/src/build.gradle"
defaultConfig {
applicationId "<<YOUR APPLICATION ID, for example ch.myexample.my_fancy_app>>"
minSdkVersion 18
targetSdkVersion 30
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
manifestPlaceholders = [
'appAuthRedirectScheme': '<<YOUR CUSTOM URL SCHEME, for example ch.myexample.app>>'
]
}
```
#### Important on iOS
In a similar way to Android, you need to register the custom url scheme within iOS to
be able to use custom redirect schemes. In the `Info.plist` file of the Runner
project, you can add the `CFBundleTypeRole` and the `CFBundleUrlSchemes`.
```xml title="<projectRoot>/ios/Runner/Info.plist"
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>YOUR CUSTOM URL SCHEME, for example ch.myexample.app</string>
</array>
</dict>
</array>
```
### Add Authentication ### Add Authentication
:::note
The auth redirect scheme "`ch.myexample.app`" does register all auth urls with the given
scheme for the app. So an url pointing to `ch.myexample.app://signin` and another one
for `ch.myexample.app://logout` will work with the same registration.
:::
To reduce the commented default code, we will modify the `main.dart` file. To reduce the commented default code, we will modify the `main.dart` file.
First, the `MyApp` class: it remains a stateless widget: First, the `MyApp` class: it remains a stateless widget:
```dart ```dart reference
class MyApp extends StatelessWidget { https://github.com/zitadel/zitadel_flutter/blob/main/lib/main.dart#L14-L28
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter ZITADEL',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter ZITADEL Quickstart'),
);
}
}
``` ```
Second, the `MyHomePage` class will remain a stateful widget with Second, the `MyHomePage` class will remain a stateful widget with
its title, we don't change any code here. its title, we don't change any code here.
```dart ```dart reference
class MyHomePage extends StatefulWidget { https://github.com/zitadel/zitadel_flutter/blob/main/lib/main.dart#L30-L37
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
``` ```
What we'll change now, is the `_MyHomePageState` class to enable What we'll change now, is the `_MyHomePageState` class to enable
authentication via ZITADEL and remove the counter button of the hello authentication via ZITADEL and remove the counter button of the starter application. We'll show the username of the authenticated user.
world application. We'll show the username of the authenticated user.
We define the needed elements for our state: We define the needed elements for our state:
```dart ```dart
final _appAuth = FlutterAppAuth();
final _secureStorage = const FlutterSecureStorage();
var _busy = false; var _busy = false;
var _authenticated = false; var _authenticated = false;
var _username = ''; var _username = '';
final storage = const FlutterSecureStorage();
``` ```
Then the builder method, which does show the login button if you're not Then the builder method, which does show the login button if you're not
authenticated, a loading bar if the login process is going on and authenticated, a loading bar if the login process is going on and
your name if you are authenticated: your name if you are authenticated:
```dart ```dart reference
@override https://github.com/zitadel/zitadel_flutter/blob/main/lib/main.dart#L119-L159
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (!_authenticated && !_busy)
Text(
'You are not authenticated.',
),
if (!_authenticated && !_busy)
ElevatedButton.icon(
icon: Icon(Icons.fingerprint),
label: Text('login'),
onPressed: _authenticate),
if (_busy)
Stack(
children: [
Center(child: Text("Busy, logging in.")),
Opacity(
opacity: 0.5,
child: Container(
color: Colors.black,
),
),
Center(child: CircularProgressIndicator()),
],
),
if (_authenticated && !_busy)
Text(
'Hello $_username!',
),
],
),
),
);
}
``` ```
And finally the `_authenticate` method which calls the authorization endpoint, And finally the `_authenticate` method which calls the authorization endpoint,
then fetches the user info and stores the tokens into the secure storage. then fetches the user info and stores the tokens into the secure storage.
```dart ```dart reference
Future<void> _authenticate() async { https://github.com/zitadel/zitadel_flutter/blob/main/lib/main.dart#L45-L117
setState(() {
_busy = true;
});
try {
final result = await _appAuth.authorizeAndExchangeCode(
AuthorizationTokenRequest(
'<<CLIENT_ID>>', // Client ID of the native application
'<<CALLBACK_URL>>', // The registered url from zitadel (e.g. ch.myexample.app://signin)
issuer: '<<ISSUER>>', // most of the cases: https:/[your-domain]-[random-string].zitadel.cloud
scopes: [
'openid',
'profile',
'email',
'offline_access',
],
),
);
final userInfoResponse = await get(
Uri.parse('https://[your-instance].zitadel.cloud/oidc/v1/userinfo'),
headers: {
HttpHeaders.authorizationHeader: 'Bearer ${result.accessToken}',
HttpHeaders.acceptHeader: 'application/json; charset=UTF-8'
},
);
final userJson = jsonDecode(utf8.decode(userInfoResponse.bodyBytes));
await _secureStorage.write(
key: 'auth_access_token', value: result?.accessToken);
await _secureStorage.write(
key: 'refresh_token', value: result?.refreshToken);
await _secureStorage.write(key: 'id_token', value: result?.idToken);
setState(() {
_busy = false;
_authenticated = true;
_username = '${userJson['given_name']} ${userJson['family_name']}';
});
} catch (e, s) {
print('error on authorizeAndExchangeCode token: $e - stack: $s');
setState(() {
_busy = false;
_authenticated = false;
});
}
}
``` ```
Now, you should be able to login with a valid ZITADEL user. Note that we have to use our http redirect URL for web applications or otherwise use our custom scheme for Android and iOS devices.
To setup other platforms, read the documentation of the [Flutter Web Auth](https://pub.dev/packages/flutter_web_auth_2).
#### Result To ensure our application catches the callback URL, you have to create a `auth.html` file in the `/web`
folder with the following content:
In the end, our state class looks like: ```html reference
https://github.com/zitadel/zitadel_flutter/blob/main/web/auth.html
```dart
class _MyHomePageState extends State<MyHomePage> {
final _appAuth = FlutterAppAuth();
final _secureStorage = const FlutterSecureStorage();
var _busy = false;
var _authenticated = false;
var _username = '';
Future<void> _authenticate() async {
setState(() {
_busy = true;
});
try {
final result = await _appAuth.authorizeAndExchangeCode(
AuthorizationTokenRequest(
'CLIENT_ID',
'CALLBACK_URL',
issuer: 'ISSUER',
scopes: [
'openid',
'profile',
'email',
'offline_access',
],
),
);
final userInfoResponse = await get(
Uri.parse('https:/[your-domain]-[random-string].zitadel.cloud/oidc/v1/userinfo'), // replace with your instance
headers: {
HttpHeaders.authorizationHeader: 'Bearer ${result.accessToken}',
HttpHeaders.acceptHeader: 'application/json; charset=UTF-8'
},
);
final userJson = jsonDecode(utf8.decode(userInfoResponse.bodyBytes));
await _secureStorage.write(
key: 'auth_access_token', value: result?.accessToken);
await _secureStorage.write(
key: 'refresh_token', value: result?.refreshToken);
await _secureStorage.write(key: 'id_token', value: result?.idToken);
setState(() {
_busy = false;
_authenticated = true;
_username = '${userJson['given_name']} ${userJson['family_name']}';
});
} catch (e, s) {
print('error on authorizeAndExchangeCode token: $e - stack: $s');
setState(() {
_busy = false;
_authenticated = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (!_authenticated && !_busy)
Text(
'You are not authenticated.',
),
if (!_authenticated && !_busy)
ElevatedButton.icon(
icon: Icon(Icons.fingerprint),
label: Text('login'),
onPressed: _authenticate),
if (_busy)
Stack(
children: [
Center(child: Text("Busy, logging in.")),
Opacity(
opacity: 0.5,
child: Container(
color: Colors.black,
),
),
Center(child: CircularProgressIndicator()),
],
),
if (_authenticated && !_busy)
Text(
'Hello $_username!',
),
],
),
),
);
}
}
``` ```
If you run this application, you can authenticate with a valid ZITADEL user. Now, you can run your application for iOS and Android devices with
<div style={{display: 'grid', 'grid-column-gap': '1rem', 'grid-template-columns': '1fr 1fr'}}> ```bash
flutter run
```
or by directly selecting your device
```bash
flutter run -d iphone
```
For Web make sure you run the application on your fixed port such that it matches your redirect URI in your ZITADEL application. We used 4444 as port before so the command would look like this:
```bash
flutter run -d chrome --web-port=4444
```
Our Android and iOS Application opens ZITADEL's login within a custom tab, on Web a new tab is opened.
### Result
If everything works out correctly, your applications should look like this:
<div style={{display: 'grid', 'grid-column-gap': '1rem', 'grid-template-columns': '1fr 1fr', 'max-width': '500px', 'margin': '0 auto'}}>
<img src="/img/flutter/not-authed.png" alt="Unauthenticated" height="500px" /> <img src="/img/flutter/not-authed.png" alt="Unauthenticated" height="500px" />
<img src="/img/flutter/authed.png" alt="Flutter Authenticated" height="500px" /> <img src="/img/flutter/authed.png" alt="Flutter Authenticated" height="500px" />
</div> </div>
<div style={{display: 'grid', 'grid-column-gap': '1rem', 'grid-template-columns': '1fr 1fr', 'max-width': '800px', 'margin': '0 auto'}}>
<img src="/img/flutter/web-not-authed.png" alt="Unauthenticated" height="500px" />
<img src="/img/flutter/web-authed.png" alt="Flutter Authenticated" height="500px" />
</div>

View File

@ -16,7 +16,7 @@ npx create-next-app --typescript
yarn create next-app --typescript yarn create next-app --typescript
``` ```
# Install Authentication library ### Install Authentication library
To keep the template as easy as possible we use [next-auth](https://next-auth.js.org/) as our main authentication library. To install, run: To keep the template as easy as possible we use [next-auth](https://next-auth.js.org/) as our main authentication library. To install, run:
@ -34,25 +34,42 @@ npm run dev
then open [http://localhost:3000](http://localhost:3000) with your browser to see the result. then open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
# Configuration ## Setup Application and Get Keys
Before we can start building our application, we have to do a few configuration steps in ZITADEL Console.
You will need to provide some information about your app.
Navigate to your Project, then add a new application at the top of the page.
Select Web application type and continue.
We recommend you use [Authorization Code](../../apis/openidoauth/grant-types#authorization-code) in combination with [Proof Key for Code Exchange (PKCE)](../../apis/openidoauth/grant-types#proof-key-for-code-exchange) for all web applications.
![Create app in console](/img/nextjs/app-create.png)
### Redirect URIs
With the Redirect URIs field, you tell ZITADEL where it is allowed to redirect users to after authentication. For development, you can set dev mode to `true` to enable insecure HTTP and redirect to a `localhost` URI.
> If you are following along with the [example](https://github.com/zitadel/zitadel-examples/tree/main/angular), set dev mode to `true` and the Redirect URIs to <http://localhost:300/api/auth/callback/zitadel>.
If you want to redirect the users back to a route on your application after they have logged out, add an optional redirect in the Post Logout URIs field.
Continue and create the application.
### Client ID
After successful app creation, a pop-up will appear, showing the app's client ID. Copy the client ID, as you will need it to configure your NextJS app.
## NextJS Setup
Now that you have your web application configured on the ZITADEL side, you can go ahead and integrate your NextJS app.
### Configuration
NextAuth.js exposes a REST API which is used by your client. NextAuth.js exposes a REST API which is used by your client.
To setup your configuration, create a file called [...nextauth].tsx in `pages/api/auth`. To setup your configuration, create a file called [...nextauth].tsx in `pages/api/auth`.
You can directly import the ZITADEL provider from [next-auth](https://next-auth.js.org/providers/zitadel). You can directly import the ZITADEL provider from [next-auth](https://next-auth.js.org/providers/zitadel).
```ts ```ts reference
import NextAuth from "next-auth"; https://github.com/zitadel/zitadel-nextjs/blob/main/pages/api/auth/%5B...nextauth%5D.tsx
import ZitadelProvider from "next-auth/providers/zitadel";
export default NextAuth({
providers: [
ZitadelProvider({
issuer: process.env.ZITADEL_ISSUER,
clientId: process.env.ZITADEL_CLIENT_ID,
clientSecret: process.env.ZITADEL_CLIENT_SECRET,
}),
],
});
``` ```
You can overwrite the default callbacks, just append them to the ZITADEL provider. You can overwrite the default callbacks, just append them to the ZITADEL provider.
@ -89,81 +106,41 @@ Hit Create, then in the detail view of your application make sure to enable dev
Now go to Token settings and check the checkbox for **User Info inside ID Token** to get your users name directly on authentication. Now go to Token settings and check the checkbox for **User Info inside ID Token** to get your users name directly on authentication.
## Environment ### Environment
Create a file `.env` in the root of the project and add the following keys to it. Create a file `.env` in the root of the project and add the following keys to it.
You can find your Issuer Url on the application detail page in console. You can find your Issuer Url on the application detail page in console.
``` ```env reference
NEXTAUTH_URL=http://localhost:3000 https://github.com/zitadel/zitadel-nextjs/blob/main/.env
ZITADEL_ISSUER=[yourIssuerUrl]
ZITADEL_CLIENT_ID=[yourClientId]
ZITADEL_CLIENT_SECRET=[randomvalue]
NEXTAUTH_SECRET=[randomvalue]
``` ```
next-auth requires a secret for all providers, so just define a random value here. next-auth requires a secret for all providers, so just define a random value here.
# User interface ### User interface
Now we can start editing the homepage by modifying `pages/index.tsx`. Now we can start editing the homepage by modifying `pages/index.tsx`. On the homepage, your authenticated user or a Signin button is shown.
Add this snippet your file. This code gets your auth session from next-auth, renders a Logout button if your authenticated or shows a Signup button if your not.
Note that signIn method requires the id of the provider we provided earlier, and provides a possibilty to add a callback url, Auth Next will redirect you to the specified route if logged in successfully.
```ts Add the following component to render the UI elements:
import { signIn, signOut, useSession } from 'next-auth/client';
export default function Page() { ```ts reference
const { data: session } = useSession();
... https://github.com/zitadel/zitadel-nextjs/blob/main/components/profile.tsx#L4-L38
{!session && <>
Not signed in <br />
<button onClick={() => signIn('zitadel', { callbackUrl: 'http://localhost:3000/profile' })}>
Sign in
</button>
</>}
{session && <>
Signed in as {session.user.email} <br />
<button onClick={() => signOut()}>Sign out</button>
</>}
...
}
``` ```
Note that the signIn method requires the id of our provider which is in our case `zitadel`.
### Session state ### Session state
To allow session state to be shared between pages - which improves performance, reduces network traffic and avoids component state changes while rendering - you can use the NextAuth.js Provider in `/pages/_app.tsx`. To allow session state to be shared between pages - which improves performance, reduces network traffic and avoids component state changes while rendering - you can use the NextAuth.js Provider in `/pages/_app.tsx`.
Take a loot at the template `_app.tsx`. Take a loot at the template `_app.tsx`.
```ts ```ts reference
import { SessionProvider } from "next-auth/react"; https://github.com/zitadel/zitadel-nextjs/blob/main/pages/_app.tsx
function MyApp({ Component, pageProps }) {
return (
<SessionProvider session={pageProps.session}>
<Component {...pageProps} />
</SessionProvider>
);
}
export default MyApp;
``` ```
Last thing: create a `profile.tsx` in /pages which renders the callback page. Last thing: create a `profile.tsx` in /pages which renders the callback page.
```ts ```ts reference
import Link from "next/link"; https://github.com/zitadel/zitadel-nextjs/blob/main/pages/profile.tsx
import styles from "../styles/Home.module.css";
export default function Profile() {
return (
<div className={styles.container}>
<h1>Login successful</h1>
<Link href="/">
<button>Back to Home</button>
</Link>
</div>
);
}
``` ```

View File

@ -18,7 +18,7 @@ services:
image: 'ghcr.io/zitadel/zitadel:stable' image: 'ghcr.io/zitadel/zitadel:stable'
command: 'start-from-init --config /example-zitadel-config.yaml --config /example-zitadel-secrets.yaml --steps /example-zitadel-init-steps.yaml --masterkey "${ZITADEL_MASTERKEY}" --tlsMode external' command: 'start-from-init --config /example-zitadel-config.yaml --config /example-zitadel-secrets.yaml --steps /example-zitadel-init-steps.yaml --masterkey "${ZITADEL_MASTERKEY}" --tlsMode external'
depends_on: depends_on:
chown: certs:
condition: 'service_completed_successfully' condition: 'service_completed_successfully'
volumes: volumes:
@ -27,10 +27,10 @@ services:
- './example-zitadel-init-steps.yaml:/example-zitadel-init-steps.yaml:ro' - './example-zitadel-init-steps.yaml:/example-zitadel-init-steps.yaml:ro'
- 'zitadel-certs:/crdb-certs:ro' - 'zitadel-certs:/crdb-certs:ro'
chown: certs:
image: 'cockroachdb/cockroach:v22.1.0' image: 'cockroachdb/cockroach:v22.1.0'
entrypoint: [ '/bin/bash', '-c' ] entrypoint: [ '/bin/bash', '-c' ]
command: [ 'cp /certs/ca.crt /zitadel-certs/ && cp /certs/client.root.crt /zitadel-certs/ && cp /certs/client.root.key /zitadel-certs/ && chown 1000:1000 /zitadel-certs/* && chmod 0400 /zitadel-certs/*' ] command: [ 'cp /certs/* /zitadel-certs/ && cockroach cert create-client --overwrite --certs-dir /zitadel-certs/ --ca-key /zitadel-certs/ca.key zitadel_user && chown 1000:1000 /zitadel-certs/*' ]
volumes: volumes:
- 'certs:/certs:ro' - 'certs:/certs:ro'
- 'zitadel-certs:/zitadel-certs:rw' - 'zitadel-certs:/zitadel-certs:rw'

View File

@ -6,7 +6,5 @@ Database:
User: User:
# If the user doesn't exist already, it is created # If the user doesn't exist already, it is created
Username: 'zitadel_user' Username: 'zitadel_user'
Password: 'Secret_DB_User_Password'
Admin: Admin:
Username: 'root' Username: 'root'
Password: ''

View File

@ -3,8 +3,6 @@ title: Overview
--- ---
Choose your platform and run ZITADEL with the most minimal configuration possible. Choose your platform and run ZITADEL with the most minimal configuration possible.
For an easy self-hosted production setup, we recommend running ZITADEL on [Kubernetes](https://kubernetes.io/docs/home/), using our official [Helm](https://helm.sh/docs/) chart.
By default, it runs a highly available ZITADEL instance along with a secure and highly available [CockroachDB](https://www.cockroachlabs.com/docs/stable/) instance.
- [Linux](./linux) - [Linux](./linux)
- [MacOS](./macos) - [MacOS](./macos)
@ -12,12 +10,10 @@ By default, it runs a highly available ZITADEL instance along with a secure and
- [Knative](./knative) - [Knative](./knative)
- [Kubernetes](./kubernetes) - [Kubernetes](./kubernetes)
## Prerequisits ## Prerequisites
- ZITADEL does not need many resources, 1 CPU and 512MB memory are more than enough. (With more CPU, the password hashing might be faster) - For test environments, ZITADEL does not need many resources, 1 CPU and 512MB memory are more than enough. (With more CPU, the password hashing might be faster)
- A cockroachDB or Postgresql (currently in beta) as only needed storage - A cockroachDB or Postgresql (currently in beta) as only needed storage
- If you want to front ZITADEL with a reverse proxy, web application firewall or content delivery network, make sure to support [HTTP/2](../manage/self-hosted/http2)
## Releases ## Releases
@ -25,4 +21,13 @@ The easiest way to use ZITADEL is to run one of our container releases
- ZITADEL does provide latest and stable [container images](https://github.com/zitadel/zitadel/pkgs/container/zitadel) - ZITADEL does provide latest and stable [container images](https://github.com/zitadel/zitadel/pkgs/container/zitadel)
- **stable** is the current **production** release of ZITADEL. - **stable** is the current **production** release of ZITADEL.
- **latest** is the **last created** release from our pipelines that gets updated on a high frequency. - **latest** is the **last created** release from our pipelines that gets updated in a high frequency.
# Production Setup
As soon as you successfully created your first test environment using one of the deployment guides in this section,
you might want to configure ZITADEL for production and embed it into your system landscape.
To do so, jump straight to the [production setup guide](../manage/self-hosted/production).
To achieving high availability, we recommend to use a [Kubernetes](https://kubernetes.io/docs/home/) Cluster.
We have an official [Helm chart](https://artifacthub.io/packages/helm/zitadel/zitadel) for easy deployment and maintenance.

View File

@ -23,7 +23,7 @@ Browse to the [App registration menus create dialog](https://portal.azure.com/#v
![Create an Application](/img/guides/azure_app_register.png) ![Create an Application](/img/guides/azure_app_register.png)
:::info :::info
Mare sure to select `web` as application type in the `Redirect URI (optional)` section. Make sure to select `web` as application type in the `Redirect URI (optional)` section.
You can leave the second field empty since we will change this in the next step. You can leave the second field empty since we will change this in the next step.
::: :::
@ -79,9 +79,49 @@ Once you created the IdP you need to activate it, to make it usable for your use
![Active AzureAD](/img/guides/azure_zitadel_active.png) ![Active AzureAD](/img/guides/azure_zitadel_active.png)
#### Disable 2-Factor prompt
If a user has no 2-factor configured, ZITADEL does ask on a regularly basis, if the user likes to add a new 2-factor for more security.
If you don't want your users to get this prompt when using Azure, you have to disable this feature.
1. Go to the login behaviour settings of your instance or organization, depending if you like to disable it for all or just a specific organization respectively
2. Set "Multi-factor init lifetimes" to 0
![img.png](../../../static/img/guides/login_lifetimes.png)
#### Create user with verified email
Azure AD does not send the "email verified claim" in its token.
Due to that the user will get an email verification mail to verify his email address.
To create the user with a verified email address you must add an action.
1. Go to the actions of your organization
2. Create a new action with the following code to set the email to verified automatically
3. Make sure the action name matches the function in the action itself e.g: "setEmailVerified"
```js reference
https://github.com/zitadel/actions/blob/main/examples/verify_email.js
```
![img.png](../../../static/img/guides/action_email_verify.png)
3. Add the action "email verify" to the flow "external authentication" and to the trigger "pre creation"
![img.png](../../../static/img/guides/action_pre_creation_email_verify.png)
#### Automatically redirect to Azure AD
If you like to get automatically redirected to your Azure AD login instead of showing the ZITADEL login with the Username/Password and a button "Login with AzureAD" you have to do the following steps:
1. Go to the login behaviour settings of your instance or organization
2. Disable login with username and password
3. Make sure you have only configured AzureAD as external identity provider
4. If you did all your settings on the organization level make sure to send the organization scope in your authorization request: [scope](../../apis/openidoauth/scopes#reserved-scopes)
### Test the setup ### Test the setup
To test the setup use a incognito mode and browse to your login page. To test the setup use incognito mode and browse to your login page.
If you succeeded you should see a new button which should redirect you to your AzureAD Tenant. If you succeeded you should see a new button which should redirect you to your AzureAD Tenant.
![AzureAD Button](/img/guides/azure_zitadel_button.png) ![AzureAD Button](/img/guides/azure_zitadel_button.png)

View File

@ -109,6 +109,17 @@ Secondfactors:
- OTP (One Time Password), Authenticator Apps like Google/Microsoft Authenticator, Authy, etc. - OTP (One Time Password), Authenticator Apps like Google/Microsoft Authenticator, Authy, etc.
- U2F (Universal Second Factor), e.g FaceID, WindowsHello, Fingerprint, Hardwaretokens like Yubikey - U2F (Universal Second Factor), e.g FaceID, WindowsHello, Fingerprint, Hardwaretokens like Yubikey
### Login Lifetimes
Configure the different lifetimes checks for the login process:
- **Password Check Lifetime** specifies after which period a user has to reenter his password during the login process
- **External Login Check Lifetime** specifies after which period a user will be redirected to the IDP during the login process
- **Multifactor Init Lifetime** specifies after which period a user will be prompted to setup a 2-Factor / Multi Factor during the login process (value 0 will deactivate the prompt)
- **Second Factor Check Lifetime** specifies after which period a user has to revalidate the 2-Factor during the login process
- **External Login Check Lifetime** specifies after which period a user has to revalidate the Multi Factor during the login process
## Identity Providers ## Identity Providers
You can configure all kinds of external identity providers for identity brokering, which support OIDC (OpenID Connect). You can configure all kinds of external identity providers for identity brokering, which support OIDC (OpenID Connect).

View File

@ -29,13 +29,9 @@ Before you start, make sure you have everything set up correctly.
1. Paste this snippet into the multiline textfield. 1. Paste this snippet into the multiline textfield.
1. Replace the snippets placeholders and select **Save**. 1. Replace the snippets placeholders and select **Save**.
```js
function addGrant(ctx, api) { ```js reference
api.userGrants.push({ https://github.com/zitadel/actions/blob/main/examples/add_user_grant.js
ProjectID: '<the projects resource ID you copied above>',
Roles: ['<the role key you copied above>']
});
}
``` ```
## Run the action when a user registers ## Run the action when a user registers

View File

@ -1,53 +1,55 @@
version: '3.8' version: "3.8"
services: services:
zitadel: zitadel:
restart: 'always' restart: "always"
networks: networks:
- 'zitadel' - "zitadel"
image: 'ghcr.io/zitadel/zitadel:stable' image: "ghcr.io/zitadel/zitadel:stable"
command: 'start-from-init --config /example-zitadel-config.yaml --config /example-zitadel-secrets.yaml --steps /example-zitadel-init-steps.yaml --masterkey "${ZITADEL_MASTERKEY}" --tlsMode disabled' command: 'start-from-init --config /example-zitadel-config.yaml --config /example-zitadel-secrets.yaml --steps /example-zitadel-init-steps.yaml --masterkey "${ZITADEL_MASTERKEY}" --tlsMode disabled'
depends_on: depends_on:
chown: certs:
condition: 'service_completed_successfully' condition: "service_completed_successfully"
ports: ports:
- '8080:8080' - "8080:8080"
volumes: volumes:
- './example-zitadel-config.yaml:/example-zitadel-config.yaml:ro' - "./example-zitadel-config.yaml:/example-zitadel-config.yaml:ro"
- './example-zitadel-secrets.yaml:/example-zitadel-secrets.yaml:ro' - "./example-zitadel-secrets.yaml:/example-zitadel-secrets.yaml:ro"
- './example-zitadel-init-steps.yaml:/example-zitadel-init-steps.yaml:ro' - "./example-zitadel-init-steps.yaml:/example-zitadel-init-steps.yaml:ro"
- 'zitadel-certs:/crdb-certs:ro' - "zitadel-certs:/crdb-certs:ro"
chown: certs:
image: 'cockroachdb/cockroach:v22.1.0' image: "cockroachdb/cockroach:v22.1.0"
entrypoint: [ '/bin/bash', '-c' ] entrypoint: ["/bin/bash", "-c"]
command: [ 'cp /certs/ca.crt /zitadel-certs/ && cp /certs/client.root.crt /zitadel-certs/ && cp /certs/client.root.key /zitadel-certs/ && chown 1000:1000 /zitadel-certs/* && chmod 0400 /zitadel-certs/*' ] command:
[
"cp /certs/* /zitadel-certs/ && cockroach cert create-client --overwrite --certs-dir /zitadel-certs/ --ca-key /zitadel-certs/ca.key zitadel_user && chown 1000:1000 /zitadel-certs/*",
]
volumes: volumes:
- 'certs:/certs:ro' - "certs:/certs:ro"
- 'zitadel-certs:/zitadel-certs:rw' - "zitadel-certs:/zitadel-certs:rw"
depends_on: depends_on:
my-cockroach-db: my-cockroach-db:
condition: 'service_healthy' condition: "service_healthy"
my-cockroach-db: my-cockroach-db:
restart: 'always' restart: "always"
networks: networks:
- 'zitadel' - "zitadel"
image: 'cockroachdb/cockroach:v22.1.0' image: "cockroachdb/cockroach:v22.1.0"
command: 'start-single-node --advertise-addr my-cockroach-db' command: "start-single-node --advertise-addr my-cockroach-db"
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health?ready=1"] test: ["CMD", "curl", "-f", "http://localhost:8080/health?ready=1"]
interval: '10s' interval: "10s"
timeout: '30s' timeout: "30s"
retries: 5 retries: 5
start_period: '20s' start_period: "20s"
ports: ports:
- '9090:8080' - "9090:8080"
- '26257:26257' - "26257:26257"
volumes: volumes:
- 'certs:/cockroach/certs:rw' - "certs:/cockroach/certs:rw"
- 'data:/cockroach/cockroach-data:rw' - "data:/cockroach/cockroach-data:rw"
networks: networks:
zitadel: zitadel:

View File

@ -6,7 +6,5 @@ Database:
User: User:
# If the user doesn't exist already, it is created # If the user doesn't exist already, it is created
Username: 'zitadel_user' Username: 'zitadel_user'
Password: 'Secret_DB_User_Password'
Admin: Admin:
Username: 'root' Username: 'root'
Password: ''

View File

@ -1,8 +1,9 @@
## Cockroach ## Cockroach
The default database of ZITADEL is [CockroachDB](https://www.cockroachlabs.com). The SQL database provides a bunch of features like horizontal scalability, data reginality and many more. The default database of ZITADEL is [CockroachDB](https://www.cockroachlabs.com). The SQL database provides a bunch of features like horizontal scalability, data regionality and many more.
The default configuration of the database looks like this: The default configuration of the database looks like this:
```yaml ```yaml
Database: Database:
cockroach: cockroach:
@ -29,4 +30,4 @@ Database:
RootCert: "" RootCert: ""
Cert: "" Cert: ""
Key: "" Key: ""
``` ```

View File

@ -2,14 +2,14 @@
title: HTTP/2 Support title: HTTP/2 Support
--- ---
ZITADEL follows a strict API first approach and makes heavy use of the modern API framework called [gRPC](https://grpc.io/). ZITADEL follows a strict API first approach and makes heavy use of the modern API framework called [gRPC](https://grpc.io/).
Besides gRPC all APIs are also available in an openapi Rest fashion as well as in gRPC-web for compatibilty towards browser integrations. Besides gRPC all APIs are also available in an openapi Rest fashion as well as in gRPC-web for compatibilty towards browser integrations.
To make us of gRPC it is vital to allow your clients to communicate with ZITADEL with [HTTP/2](https://en.wikipedia.org/wiki/HTTP/2). To make us of gRPC it is vital to allow your clients to communicate with ZITADEL with [HTTP/2](https://en.wikipedia.org/wiki/HTTP/2).
Sometimes you need to configure explicitly that you want to use HTTP/2 if you run ZITADEL behind a proxy and below you should find examples for different vendors and projects. Sometimes you need to configure explicitly that you want to use HTTP/2 if you run ZITADEL behind a reverse proxy and below you should find examples for different vendors and projects.
Furthermore it is important to notice that by default HTTP/2 is always encrypted, but if you want to run ZITADEL without TLS from your proxy or service mesh this is possible through [h2c](https://httpd.apache.org/docs/2.4/howto/http2.html). Furthermore it is important to notice that by default HTTP/2 is always encrypted, but if you want to run ZITADEL without TLS from your reverse proxy or service mesh this is possible through [h2c](https://httpd.apache.org/docs/2.4/howto/http2.html).
Oftentimes when you run ZITADEL inside a service mesh, or a servelerss offering (e.g. Google Cloud Run, Knative, ...) you will need h2c. Oftentimes when you run ZITADEL inside a service mesh, or a servelerss offering (e.g. Google Cloud Run, Knative, ...) you will need h2c.
You can read more about ZITADEL's [TLSs modes here](/docs/guides/manage/self-hosted/tls_modes). You can read more about ZITADEL's [TLSs modes here](/docs/guides/manage/self-hosted/tls_modes).

View File

@ -0,0 +1,101 @@
---
title: Production Checklist
---
As soon as you successfully deployed ZITADEL as a proof of concept using one of our [deployment guides](/docs/guides/deploy/overview),
you are ready to configure ZITADEL for production usage.
## High Availability
We recommend running ZITADEL highly available using an orchestrator that schedules ZITADEL on multiple servers, like [Kubernetes](/docs/guides/deploy/kubernetes). For keeping startup times fast when scaling ZITADEL, you should also consider using separate jobs with `zitadel init` and `zitadel setup`, so your workload containers just have to execute `zitadel start`.
## Configuration
Read [on the configure page](/docs/guides/manage/self-hosted/configure) about the available options you have to configure ZITADEL.
## Networking
- To make ZITADEL available at the domain of your choice, [you need to configure the ExternalDomain property](/docs/guides/manage/self-hosted/custom-domain).
- To enable and restrict access to **HTTPS**, head over to [the description of your TLS options](/docs/guides/manage/self-hosted/tls_modes).
- If you want to front ZITADEL with a reverse proxy, web application firewall or content delivery network, make sure to support **[HTTP/2](/docs/guides/manage/self-hosted/http2)**.
- You can also refer to some **[example reverse proxy configurations](/docs/guides/manage/self-hosted/reverseproxy/reverse_proxy)**.
## Monitoring
By default, [**metrics**](docs/apis/observability/metrics) are exposed at /debug/metrics in OpenTelemetry (otel) format.
Also, you can enable **tracing** in the ZITADEL configuration.
```yaml
Tracing:
# Choose one in "otel", "google", "log" and "none"
Type: google
Fraction: 1
MetricPrefix: zitadel
```
## Database
Depending on your environment, you maybe would want to tweak some settings about how ZITADEL interacts with the database in the database section of your ZITADEL configuration. Read more about your [database configuration options](/docs/guides/manage/self-hosted/database).
```yaml
Database:
cockroach:
Host: localhost
Port: 26257
Database: zitadel
//highlight-start
MaxOpenConns: 20
MaxConnLifetime: 30m
MaxConnIdleTime: 30m
//highlight-end
Options: ""
```
You also might want to configure how [projections](/docs/concepts/eventstore/implementation#projections) are computed. These are the default values:
```yaml
Projections:
RequeueEvery: 60s
RetryFailedAfter: 1s
MaxFailureCount: 5
ConcurrentInstances: 1
BulkLimit: 200
MaxIterators: 1
Customizations:
projects:
BulkLimit: 2000
```
## Data Initialization
- You can configure instance defaults in the DefaultInstance section.
If you plan to eventually create [multiple virtual instances](/docs/concepts/structure/instance#multiple-virtual-instances), these defaults take effect.
Also, these configurations apply to the first instance, that ZITADEL automatically creates for you.
Especially the following properties are of special interest for your production setup.
```yaml
DefaultInstance:
OIDCSettings:
AccessTokenLifetime: 12h
IdTokenLifetime: 12h
RefreshTokenIdleExpiration: 720h #30d
RefreshTokenExpiration: 2160h #90d
# this configuration sets the default email configuration
SMTPConfiguration:
# configuration of the host
SMTP:
#for example smtp.mailtrap.io:2525
Host:
User:
Password:
TLS:
# if the host of the sender is different from ExternalDomain set DefaultInstance.DomainPolicy.SMTPSenderAddressMatchesInstanceDomain to false
From:
FromName:
```
- If you don't want to use the DefaultInstance configuration for the first instance that ZITADEL automatically creates for you during the [setup phase](/docs/guides/manage/self-hosted/configure#database-initialization), you can provide a FirstInstance YAML section using the --steps argument.
- Learn how to configure ZITADEL via the [Console user interface](/docs/guides/manage/console/overview).
- Probably, you also want to [apply your custom branding](/docs/guides/manage/customize/branding), [hook into certain events](/docs/guides/manage/customize/behavior), [customize texts](/docs/guides/manage/customize/texts) or [add metadata to your users](/docs/guides/manage/customize/user-metadata).
- If you want to automatically create ZITADEL resources, you can use the [ZITADEL Terraform Provider](/docs/guides/manage/terraform/basics).

View File

@ -1,4 +0,0 @@
## More information
- [You can read here about the TLS Modes](./tls_modes)
- [And here about how ZITADEL makes use of HTTP/2](./http2)

View File

@ -1,53 +0,0 @@
---
title: Proxy Configuration
---
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
import Zcloud from './_zitadel_cloud.mdx'
import Nginx from './_nginx.mdx'
import Traefik from './_traefik.mdx'
import Caddy from './_caddy.mdx'
import Cftunnel from './_cloudflare_tunnel.mdx'
import Cloudflare from './_cloudflare.mdx'
import More from './_more.mdx'
# Proxy Configuration
<Tabs
groupId="proxy-vendor"
default="zcloud"
values={[
{'label': 'ZITADEL Cloud', 'value': 'zcloud'},
{'label': 'NGINX', 'value': 'nginx'},
{'label': 'Traefik', 'value': 'traefik'},
{'label': 'Caddy', 'value': 'caddy'},
{'label': 'Cloudflare Tunnel', 'value': 'cftunnel'},
{'label': 'Cloudflare', 'value': 'cf'}
]}
>
<TabItem value="zcloud">
<Zcloud/>
<More/>
</TabItem>
<TabItem value="nginx">
<Nginx/>
<More/>
</TabItem>
<TabItem value="traefik">
<Traefik/>
<More/>
</TabItem>
<TabItem value="caddy">
<Caddy/>
<More/>
</TabItem>
<TabItem value="cftunnel">
<Cftunnel/>
<More/>
</TabItem>
<TabItem value="cf">
<Cloudflare/>
<More/>
</TabItem>
</Tabs>

View File

@ -1,6 +1,6 @@
## TLS mode external ## TLS mode external
```bash ```
https://localhost { https://localhost {
reverse_proxy h2c://localhost:8080 reverse_proxy h2c://localhost:8080
tls internal #only non production tls internal #only non production
@ -9,7 +9,7 @@ https://localhost {
## TLS mode enabled ## TLS mode enabled
```bash ```
https://localhost { https://localhost {
reverse_proxy https://localhost:8080 reverse_proxy https://localhost:8080
tls internal #only non production tls internal #only non production
@ -18,7 +18,7 @@ https://localhost {
## TLS mode disabled ## TLS mode disabled
```bash ```
http://localhost { http://localhost {
reverse_proxy h2c://localhost:8080 reverse_proxy h2c://localhost:8080
} }

View File

@ -3,7 +3,7 @@
- [Make sure HTTP/2 is enabled](https://support.cloudflare.com/hc/en-us/articles/200168076-Understanding-Cloudflare-HTTP-2-and-HTTP-3-Support) - [Make sure HTTP/2 is enabled](https://support.cloudflare.com/hc/en-us/articles/200168076-Understanding-Cloudflare-HTTP-2-and-HTTP-3-Support)
- [Verify that gRPC is enabled](https://support.cloudflare.com/hc/en-us/articles/360050483011-Understanding-Cloudflare-gRPC-support) - [Verify that gRPC is enabled](https://support.cloudflare.com/hc/en-us/articles/360050483011-Understanding-Cloudflare-gRPC-support)
- [Verify that traffic is proxied through cloudflare](https://developers.cloudflare.com/dns/manage-dns-records/reference/proxied-dns-records/) - [Verify that traffic is proxied through cloudflare](https://developers.cloudflare.com/dns/manage-dns-records/reference/proxied-dns-records/)
- [Configure ZITADEL to use the TLS Mode enabled](./tls_modes#enabled) - [Configure ZITADEL to use the TLS Mode enabled](/docs/guides/manage/self-hosted/tls_modes#enabled)
:::info :::info
[Cloudflare does only support gRPC with TLS!](https://support.cloudflare.com/hc/en-us/articles/360050483011-Understanding-Cloudflare-gRPC-support) [Cloudflare does only support gRPC with TLS!](https://support.cloudflare.com/hc/en-us/articles/360050483011-Understanding-Cloudflare-gRPC-support)
@ -14,5 +14,5 @@
If something is not working please check the cloudflare WAF rules for potential violations. If something is not working please check the cloudflare WAF rules for potential violations.
These two rules are known to be triggered: These two rules are known to be triggered:
- 100001 Anomaly:Header:User-Agent - Missing Cloudflare Specials - 100001 Anomaly:Header:User-Agent - Missing Cloudflare Specials
- 100004 Anomaly:Header:User-Agent, Anomaly:Header:Referer - Missing or empty - 100004 Anomaly:Header:User-Agent, Anomaly:Header:Referer - Missing or empty

View File

@ -0,0 +1,166 @@
## TLS mode external
```
LoadModule mpm_event_module modules/mod_mpm_event.so
LoadModule authn_file_module modules/mod_authn_file.so
LoadModule authn_core_module modules/mod_authn_core.so
LoadModule authz_host_module modules/mod_authz_host.so
LoadModule authz_groupfile_module modules/mod_authz_groupfile.so
LoadModule authz_user_module modules/mod_authz_user.so
LoadModule authz_core_module modules/mod_authz_core.so
LoadModule access_compat_module modules/mod_access_compat.so
LoadModule auth_basic_module modules/mod_auth_basic.so
LoadModule reqtimeout_module modules/mod_reqtimeout.so
LoadModule filter_module modules/mod_filter.so
LoadModule mime_module modules/mod_mime.so
LoadModule log_config_module modules/mod_log_config.so
LoadModule env_module modules/mod_env.so
LoadModule headers_module modules/mod_headers.so
LoadModule setenvif_module modules/mod_setenvif.so
LoadModule version_module modules/mod_version.so
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule ssl_module modules/mod_ssl.so
LoadModule proxy_http2_module modules/mod_proxy_http2.so
LoadModule unixd_module modules/mod_unixd.so
LoadModule status_module modules/mod_status.so
LoadModule autoindex_module modules/mod_autoindex.so
LoadModule dir_module modules/mod_dir.so
LoadModule alias_module modules/mod_alias.so
LoadModule rewrite_module modules/mod_rewrite.so
ServerRoot "/usr/local/apache2"
LogLevel warn
ErrorLog /proc/self/fd/2
CustomLog /proc/self/fd/1 "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\""
ServerName my.domain
Listen 80
Listen 443
SSLRandomSeed startup builtin
SSLRandomSeed connect builtin
<VirtualHost *:80>
ServerName my.domain
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI}
</VirtualHost>
<VirtualHost *:443>
ServerName my.domain
ProxyPreserveHost On
SSLCertificateFile /certs/server.crt
SSLCertificateKeyFile /certs/server.key
ProxyPass / h2c://localhost:8080/
ProxyPassReverse / h2c://localhost:8080/
</VirtualHost>
```
## TLS mode enabled
```
LoadModule mpm_event_module modules/mod_mpm_event.so
LoadModule authn_file_module modules/mod_authn_file.so
LoadModule authn_core_module modules/mod_authn_core.so
LoadModule authz_host_module modules/mod_authz_host.so
LoadModule authz_groupfile_module modules/mod_authz_groupfile.so
LoadModule authz_user_module modules/mod_authz_user.so
LoadModule authz_core_module modules/mod_authz_core.so
LoadModule access_compat_module modules/mod_access_compat.so
LoadModule auth_basic_module modules/mod_auth_basic.so
LoadModule reqtimeout_module modules/mod_reqtimeout.so
LoadModule filter_module modules/mod_filter.so
LoadModule mime_module modules/mod_mime.so
LoadModule log_config_module modules/mod_log_config.so
LoadModule env_module modules/mod_env.so
LoadModule headers_module modules/mod_headers.so
LoadModule setenvif_module modules/mod_setenvif.so
LoadModule version_module modules/mod_version.so
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule ssl_module modules/mod_ssl.so
LoadModule proxy_http2_module modules/mod_proxy_http2.so
LoadModule unixd_module modules/mod_unixd.so
LoadModule status_module modules/mod_status.so
LoadModule autoindex_module modules/mod_autoindex.so
LoadModule dir_module modules/mod_dir.so
LoadModule alias_module modules/mod_alias.so
LoadModule rewrite_module modules/mod_rewrite.so
LoadModule http2_module modules/mod_http2.so
ServerRoot "/usr/local/apache2"
LogLevel debug
ErrorLog /proc/self/fd/2
CustomLog /proc/self/fd/1 "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\""
ServerName my.domain
Listen 80
Listen 443
SSLRandomSeed startup builtin
SSLRandomSeed connect builtin
<VirtualHost *:80>
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI}
</VirtualHost>
<VirtualHost *:443>
ProxyPreserveHost On
SSLEngine on
SSLProxyEngine on
SSLCertificateFile /certs/server.crt
SSLCertificateKeyFile /certs/server.key
ProxyPass / h2://localhost:8080/
</VirtualHost>
```
## TLS mode disabled
```
LoadModule mpm_event_module modules/mod_mpm_event.so
LoadModule authn_file_module modules/mod_authn_file.so
LoadModule authn_core_module modules/mod_authn_core.so
LoadModule authz_host_module modules/mod_authz_host.so
LoadModule authz_groupfile_module modules/mod_authz_groupfile.so
LoadModule authz_user_module modules/mod_authz_user.so
LoadModule authz_core_module modules/mod_authz_core.so
LoadModule access_compat_module modules/mod_access_compat.so
LoadModule auth_basic_module modules/mod_auth_basic.so
LoadModule reqtimeout_module modules/mod_reqtimeout.so
LoadModule filter_module modules/mod_filter.so
LoadModule mime_module modules/mod_mime.so
LoadModule log_config_module modules/mod_log_config.so
LoadModule env_module modules/mod_env.so
LoadModule headers_module modules/mod_headers.so
LoadModule setenvif_module modules/mod_setenvif.so
LoadModule version_module modules/mod_version.so
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule ssl_module modules/mod_ssl.so
LoadModule proxy_http2_module modules/mod_proxy_http2.so
LoadModule unixd_module modules/mod_unixd.so
LoadModule status_module modules/mod_status.so
LoadModule autoindex_module modules/mod_autoindex.so
LoadModule dir_module modules/mod_dir.so
LoadModule alias_module modules/mod_alias.so
LoadModule rewrite_module modules/mod_rewrite.so
ServerRoot "/usr/local/apache2"
LogLevel warn
ErrorLog /proc/self/fd/2
CustomLog /proc/self/fd/1 "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\""
ServerName my.domain
Listen 80
<VirtualHost *:80>
ServerName my.domain
ProxyPreserveHost On
ProxyPass / h2c://localhost:8080/
ProxyPassReverse / h2c://localhost:8080/
</VirtualHost>
```

View File

@ -0,0 +1,4 @@
## More information
- [You can read here about the TLS Modes](/docs/guides/manage/self-hosted/tls_modes)
- [And here about how ZITADEL makes use of HTTP/2](/docs/guides/manage/self-hosted/http2)

View File

@ -1,6 +1,6 @@
## TLS mode external ## TLS mode external
```bash ```
worker_processes 1; worker_processes 1;
events { events {
worker_connections 1024; worker_connections 1024;
@ -12,7 +12,7 @@ http {
ssl_certificate ssl/certificate.pem; ssl_certificate ssl/certificate.pem;
ssl_certificate_key ssl/key.pem; ssl_certificate_key ssl/key.pem;
location / { location / {
grpc_pass grpc://localhost:8080; grpc_pass grpc://localhost:8080;
grpc_set_header Host $host; grpc_set_header Host $host;
@ -33,7 +33,7 @@ with
## TLS mode enabled ## TLS mode enabled
```bash ```
worker_processes 1; worker_processes 1;
events { events {
worker_connections 1024; worker_connections 1024;
@ -45,7 +45,7 @@ http {
ssl_certificate ssl/certificate.pem; ssl_certificate ssl/certificate.pem;
ssl_certificate_key ssl/key.pem; ssl_certificate_key ssl/key.pem;
location / { location / {
grpc_pass grpcs://localhost:8080; grpc_pass grpcs://localhost:8080;
grpc_set_header Host $host; grpc_set_header Host $host;
@ -66,7 +66,7 @@ with
## TLS mode disabled ## TLS mode disabled
```bash ```
worker_processes 1; worker_processes 1;
events { events {
worker_connections 1024; worker_connections 1024;
@ -75,7 +75,7 @@ events {
http { http {
server { server {
listen 80; listen 80;
location / { location / {
grpc_pass grpc://localhost:8080; grpc_pass grpc://localhost:8080;
grpc_set_header Host $host; grpc_set_header Host $host;

View File

@ -0,0 +1,59 @@
---
title: Reverse Proxy Configuration
---
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
import Zcloud from "./_zitadel_cloud.mdx";
import Nginx from "./_nginx.mdx";
import Traefik from "./_traefik.mdx";
import Caddy from "./_caddy.mdx";
import Httpd from "./_httpd.mdx";
import Cftunnel from "./_cloudflare_tunnel.mdx";
import Cloudflare from "./_cloudflare.mdx";
import More from "./_more.mdx";
# Proxy Configuration
<Tabs
groupId="proxy-vendor"
default="zcloud"
values={[
{ label: "ZITADEL Cloud", value: "zcloud" },
{ label: "NGINX", value: "nginx" },
{ label: "Traefik", value: "traefik" },
{ label: "Caddy", value: "caddy" },
{ label: "Apache httpd", value: "httpd" },
{ label: "Cloudflare Tunnel", value: "cftunnel" },
{ label: "Cloudflare", value: "cf" },
]}
>
<TabItem value="zcloud">
<Zcloud />
<More />
</TabItem>
<TabItem value="nginx">
<Nginx />
<More />
</TabItem>
<TabItem value="traefik">
<Traefik />
<More />
</TabItem>
<TabItem value="caddy">
<Caddy />
<More />
</TabItem>
<TabItem value="httpd">
<Httpd />
<More />
</TabItem>
<TabItem value="cftunnel">
<Cftunnel />
<More />
</TabItem>
<TabItem value="cf">
<Cloudflare />
<More />
</TabItem>
</Tabs>

View File

@ -0,0 +1,48 @@
---
title: How to configure ZITADEL for your scenario
---
Each customer does have different needs and use-cases. In ZITADEL you are able to configure your settings depending on your needs.
In this section we show you the different use-cases we have already experienced, that could interest you.
## Automatically redirect users if the organization has only one identity provider
You have different customers (organizations) in your ZITADEL instance and they have different needs on how to authenticate their users. One of your customers does only allow login with an external identity provider like Google, Azure AD, and so on.
If a user of this organization wants to login, you don't want them to enter their username in the ZITADEL Login UI, they should be redirected directly to the identity provider without their interaction.
### Settings
1. Go to the "Identity Providers" Settings of the organization
2. Configure the needed identity provider: Read this [guide](../integrate/identity-brokering.md) if you don't know how
3. Go to the "Login Behavior and Security" settings of the organization
4. Disable "Username Password Allowed" and enable "External IDP allowed" in the Advanced Section
Now your application can send either the organizations id (`urn:zitadel:iam:org:id:{id}`) or organizations primary domain (`urn:zitadel:iam:org:domain:primary:{domainname}`) scope on your authorization request to identify on which organization the users should be logged in.
More about the [scopes](../../apis/openidoauth/scopes#reserved-scopes)
## Custom Application Domain per Organization
If you have an application that runs a dedicated domain for each customer you need to instruct ZITADEL to allow redirection for each domain specifically to safeguard against phishing attacks.
Example:
MyApplication: customer-a.app.com
ZITADEL Login: login.app.com
In the OIDC Authorization request you always have to send the redirect URI to where you like to be redirected after login.
To handle this scenario it is possible to register multiple URIs on each application in ZITADEL, the only criteria is that the requested URI has to match one of the registered URIs.
Read more about [applications](../manage/console/applications) and the [redirect urls](../manage/console/applications#redirect-uris)
### Trigger organization in ZITADEL login
It is possible to trigger the organization directly with the authorization request to ZITADEL.
This will have the following impacts:
- Trigger organization login behaviour settings
- Trigger organization branding
- Only allow users from selected organization to login
To request the organization send either the the organization id (`urn:zitadel:iam:org:id:{id}`) or organization primary domain (`urn:zitadel:iam:org:domain:primary:{domainname}`) scope on your authentication request from your application.
More about the [scopes](../../apis/openidoauth/scopes#reserved-scopes)

View File

@ -14,7 +14,6 @@ In this session your second level support and operations team will gain an under
- Event types - Event types
- Database schemas and compute models - Database schemas and compute models
- Accessing database - Accessing database
- Validation of tokens
- Observability (Logs, Errors, Metrics, Tracing) - Observability (Logs, Errors, Metrics, Tracing)
- Operations best practices (Deployment, Backup, Networking etc.) - Operations best practices (Deployment, Backup, Networking etc.)
- Q&A - Q&A
@ -32,5 +31,6 @@ In this hands-on training your employees will get a complete overview of the sys
- Walk-though all features - Walk-though all features
- Users / Manuals - Users / Manuals
- APIs - APIs
- Validation of tokens
- Client integration best-practices - Client integration best-practices
- Q&A - Q&A

View File

@ -18,15 +18,19 @@ module.exports = {
"data-api": "/proxy/api/event", "data-api": "/proxy/api/event",
}, },
], ],
customFields: {
description: "Documentation for ZITADEL - The best of Auth0 and Keycloak combined. Built for the serverless era.",
},
themeConfig: { themeConfig: {
metadata: [{name: 'keywords', content: 'zitadel, documentation, jwt, saml, oauth2, authentication, serverless, login, auth, authorization, sso, openid-connect, oidc, mfa, 2fa, passkeys, fido2, docker'}],
zoom: { zoom: {
selector: '.markdown :not(em) > img', selector: ".markdown :not(em) > img",
background: { background: {
light: 'rgb(243, 244, 246)', light: "rgb(243, 244, 246)",
dark: 'rgb(55, 59, 82)' dark: "rgb(55, 59, 82)",
}, },
// options you can specify via https://github.com/francoischalifour/medium-zoom#usage // options you can specify via https://github.com/francoischalifour/medium-zoom#usage
config: {} config: {},
}, },
navbar: { navbar: {
// title: 'ZITADEL', // title: 'ZITADEL',
@ -119,7 +123,6 @@ module.exports = {
{ {
title: "Legal", title: "Legal",
items: [ items: [
{ {
label: "Terms and Conditions", label: "Terms and Conditions",
href: "/docs/legal/terms-of-service", href: "/docs/legal/terms-of-service",
@ -152,22 +155,26 @@ module.exports = {
{ {
label: "Docs v1 (deprecated)", label: "Docs v1 (deprecated)",
href: "https://docs-v1.zitadel.com/", href: "https://docs-v1.zitadel.com/",
} },
], ],
}, },
], ],
copyright: `Copyright © ${new Date().getFullYear()} ZITADEL Docs - Built with Docusaurus.`, copyright: `Copyright © ${new Date().getFullYear()} ZITADEL Docs - Built with Docusaurus.`,
}, },
algolia: { algolia: {
appId: "8H6ZKXENLO", appId: "8H6ZKXENLO",
apiKey: "124fe1c102a184bc6fc70c75dc84f96f", apiKey: "124fe1c102a184bc6fc70c75dc84f96f",
indexName: 'zitadel', indexName: "zitadel",
selector: 'div#' selector: "div#",
}, },
prism: { prism: {
additionalLanguages: ["csharp", "dart", "groovy", "regex"], additionalLanguages: ["csharp", "dart", "groovy", "regex"],
}, },
colorMode: {
defaultMode: "dark",
disableSwitch: false,
respectPrefersColorScheme: true,
},
}, },
presets: [ presets: [
[ [
@ -187,4 +194,5 @@ module.exports = {
], ],
], ],
plugins: [require.resolve("docusaurus-plugin-image-zoom")], plugins: [require.resolve("docusaurus-plugin-image-zoom")],
themes: ["@saucelabs/theme-github-codeblock"],
}; };

View File

@ -53,6 +53,7 @@
"@leichtgewicht/ip-codec": "2.0.4", "@leichtgewicht/ip-codec": "2.0.4",
"@mdx-js/mdx": "1.6.22", "@mdx-js/mdx": "1.6.22",
"@mdx-js/react": "^1.6.22", "@mdx-js/react": "^1.6.22",
"@saucelabs/theme-github-codeblock": "^0.1.1",
"@slorber/static-site-generator-webpack-plugin": "4.0.4", "@slorber/static-site-generator-webpack-plugin": "4.0.4",
"@svgr/core": "6.2.1", "@svgr/core": "6.2.1",
"@svgr/hast-util-to-babel-ast": "6.2.1", "@svgr/hast-util-to-babel-ast": "6.2.1",
@ -159,6 +160,7 @@
"stylehacks": "5.1.0", "stylehacks": "5.1.0",
"terser-webpack-plugin": "5.3.1", "terser-webpack-plugin": "5.3.1",
"type-fest": "2.12.2", "type-fest": "2.12.2",
"url": "^0.11.0",
"wait-on": "6.0.1", "wait-on": "6.0.1",
"webpack-bundle-analyzer": "4.5.0", "webpack-bundle-analyzer": "4.5.0",
"webpack-dev-middleware": "5.3.1", "webpack-dev-middleware": "5.3.1",

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