chore: 🚀 Migrate monorepo from Yarn to pnpm + Turbo integration + Configuration cleanup (#10165)

This PR modernizes the ZITADEL monorepo build system by migrating from
Yarn to pnpm, introducing Turbo for improved build orchestration, and
cleaning up configuration inconsistencies across all apps and packages.

### 🎯 Key Improvements

#### 📦 **Package Manager Migration (Yarn → pnpm)**
- **Performance**: Faster installs with pnpm's efficient symlink-based
node_modules structure
- **Disk space**: Significant reduction in disk usage through
content-addressable storage
- **Lockfile**: More reliable dependency resolution with pnpm-lock.yaml
- **Workspace support**: Better monorepo dependency management

####  **Turbo Integration**
- **Build orchestration**: Dependency-aware task execution across the
monorepo
- **Intelligent caching**: Dramatically faster builds on CI/CD and local
development
- **Parallel execution**: Optimal task scheduling based on dependency
graphs
- **Vercel optimization**: Enhanced build performance and caching on
Vercel deployments

#### 🧹 **Configuration Cleanup & Unification**
- **Removed config packages**: Eliminated `@zitadel/*-config` packages
and inlined configurations
- **Simplified dependencies**: Reduced complexity in package.json files
across all apps
- **Consistent tooling**: Unified prettier, ESLint, and TypeScript
configurations
- **Standalone support**: Improved prepare-standalone.js script for
subtree deployments

### 📋 Detailed Changes

#### **🔧 Build System & Dependencies**
-  Updated all package.json scripts to use `pnpm` instead of `yarn`
-  Replaced `yarn.lock` with pnpm-lock.yaml and regenerated
dependencies
-  Added Turbo configuration (turbo.json) to root and individual
packages
-  Configured proper dependency chains: `@zitadel/proto#generate` →
`@zitadel/client#build` → `console#build`
-  Added missing `@bufbuild/protobuf` dependency to console app for
TypeScript compilation

#### **🚀 CI/CD & Workflows**
-  Updated all GitHub Actions workflows to use `pnpm/action-setup@v4`
-  Migrated build processes to use Turbo with directory-based filters
(`--filter=./console`)
-  **New**: Added `docs.yml` workflow for building documentation
locally (helpful for contributors without Vercel access)
-  Fixed dependency resolution issues in lint workflows
-  Ensured proto generation always runs before builds and linting

#### **📚 Documentation & Proto Generation**
-  **Robust plugin management**: Enhanced plugin-download.sh with retry
logic and error handling
-  **Vercel compatibility**: Fixed protoc-gen-connect-openapi plugin
availability in Vercel builds
-  **API docs generation**: Resolved Docusaurus build errors with
OpenAPI plugin configuration
-  **Type safety**: Improved TypeScript type extraction patterns in
Angular components

#### **🛠️ Developer Experience**
-  Updated all README files to reference pnpm commands
-  Improved Makefile targets to use Turbo for consistent builds
-  Enhanced standalone build process for login app subtree deployments
-  Added debug utilities for troubleshooting build issues

#### **🗂️ File Structure & Cleanup**
-  Removed obsolete configuration packages and their references
-  Cleaned up Docker files to remove non-existent package copies
-  Updated workspace references and import paths
-  Streamlined turbo.json configurations across all packages

### 🎉 Benefits

1. ** Faster Builds**: Turbo's caching and parallel execution
significantly reduce build times
2. **🔄 Better Caching**: Improved cache hits on Vercel and CI/CD
environments
3. **🛠️ Simplified Maintenance**: Unified tooling and configuration
management
4. **📈 Developer Productivity**: Faster local development with optimized
dependency resolution
5. **🚀 Enhanced CI/CD**: More reliable and faster automated builds and
deployments
6. **📖 Better Documentation**: Comprehensive build documentation and
troubleshooting guides

### 🧪 Testing

-  All apps build successfully with new pnpm + Turbo setup
-  Proto generation works correctly across console, login, and docs
-  GitHub Actions workflows pass with new configuration
-  Vercel deployments work with enhanced plugin management
-  Local development workflow verified and documented

This migration sets a solid foundation for future development while
maintaining backward compatibility and improving the overall developer
experience.

---------

Co-authored-by: Elio Bischof <elio@zitadel.com>
This commit is contained in:
Max Peintner
2025-07-16 09:10:19 +02:00
committed by GitHub
parent 6d11145c77
commit 312b7b6010
152 changed files with 34249 additions and 37195 deletions

View File

@@ -33,6 +33,12 @@ jobs:
node_version: "20" node_version: "20"
buf_version: "latest" buf_version: "latest"
docs:
uses: ./.github/workflows/docs.yml
with:
node_version: "20"
buf_version: "latest"
version: version:
uses: ./.github/workflows/version.yml uses: ./.github/workflows/version.yml
with: with:
@@ -87,7 +93,7 @@ jobs:
actions: write actions: write
id-token: write id-token: write
with: with:
ignore-run-cache: ${{ github.event_name == 'workflow_dispatch' || fromJSON(github.run_attempt) > 1 }} ignore-run-cache: ${{ github.event_name == 'workflow_dispatch' || fromJSON(github.run_attempt) > 1 }}
node_version: "20" node_version: "20"
secrets: secrets:
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }} DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
@@ -126,7 +132,16 @@ jobs:
issues: write issues: write
pull-requests: write pull-requests: write
needs: needs:
[version, core-unit-test, core-integration-test, lint, container, login-container, login-quality, e2e] [
version,
core-unit-test,
core-integration-test,
lint,
container,
login-container,
login-quality,
e2e,
]
if: ${{ github.event_name == 'workflow_dispatch' }} if: ${{ github.event_name == 'workflow_dispatch' }}
secrets: secrets:
GCR_JSON_KEY_BASE64: ${{ secrets.GCR_JSON_KEY_BASE64 }} GCR_JSON_KEY_BASE64: ${{ secrets.GCR_JSON_KEY_BASE64 }}

View File

@@ -35,94 +35,57 @@ jobs:
goarch: [amd64, arm64] goarch: [amd64, arm64]
steps: steps:
- - uses: actions/checkout@v4
uses: actions/checkout@v4 - uses: actions/cache/restore@v4
- timeout-minutes: 1
uses: actions/cache/restore@v4 name: restore console
timeout-minutes: 1 with:
name: restore console path: ${{ inputs.console_cache_path }}
with: key: ${{ inputs.console_cache_key }}
path: ${{ inputs.console_cache_path }} fail-on-cache-miss: true
key: ${{ inputs.console_cache_key }} - uses: actions/cache/restore@v4
fail-on-cache-miss: true timeout-minutes: 1
- name: restore core
uses: actions/cache/restore@v4 with:
timeout-minutes: 1 path: ${{ inputs.core_cache_path }}
name: restore core key: ${{ inputs.core_cache_key }}
with: fail-on-cache-miss: true
path: ${{ inputs.core_cache_path }} - uses: actions/setup-go@v5
key: ${{ inputs.core_cache_key }} with:
fail-on-cache-miss: true go-version-file: "go.mod"
- - name: compile
uses: actions/setup-go@v5 timeout-minutes: 5
with: run: |
go-version-file: 'go.mod' GOOS="${{matrix.goos}}" \
- GOARCH="${{matrix.goarch}}" \
name: compile VERSION="${{ inputs.version }}" \
timeout-minutes: 5 COMMIT_SHA="${{ github.sha }}" \
run: | make compile_pipeline
GOOS="${{matrix.goos}}" \ - name: create folder
GOARCH="${{matrix.goarch}}" \ run: |
VERSION="${{ inputs.version }}" \ mkdir zitadel-${{ matrix.goos }}-${{ matrix.goarch }}
COMMIT_SHA="${{ github.sha }}" \ mv zitadel zitadel-${{ matrix.goos }}-${{ matrix.goarch }}/
make compile_pipeline cp LICENSE zitadel-${{ matrix.goos }}-${{ matrix.goarch }}/
- cp README.md zitadel-${{ matrix.goos }}-${{ matrix.goarch }}/
name: create folder tar -czvf zitadel-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz zitadel-${{ matrix.goos }}-${{ matrix.goarch }}
run: | - uses: actions/upload-artifact@v4
mkdir zitadel-${{ matrix.goos }}-${{ matrix.goarch }} with:
mv zitadel zitadel-${{ matrix.goos }}-${{ matrix.goarch }}/ name: zitadel-${{ matrix.goos }}-${{ matrix.goarch }}
cp LICENSE zitadel-${{ matrix.goos }}-${{ matrix.goarch }}/ path: zitadel-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz
cp README.md zitadel-${{ matrix.goos }}-${{ matrix.goarch }}/
tar -czvf zitadel-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz zitadel-${{ matrix.goos }}-${{ matrix.goarch }}
-
uses: actions/upload-artifact@v4
with:
name: zitadel-${{ matrix.goos }}-${{ matrix.goarch }}
path: zitadel-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz
login:
runs-on: ubuntu-latest
steps:
-
uses: actions/checkout@v4
-
uses: depot/setup-action@v1
-
run: make login_standalone_out
env:
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
LOGIN_BAKE_CLI: depot bake
DEPOT_PROJECT_ID: w47wkxzdtw
NODE_VERSION: ${{ inputs.node_version }}
-
name: move files
run: |
cp login/LICENSE login/apps/login/standalone/
cp login/README.md login/apps/login/standalone/
tar -czvf login.tar.gz -C login/apps/login/standalone .
-
uses: actions/upload-artifact@v4
with:
name: login
path: login.tar.gz
checksums: checksums:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [executable, login] needs: [executable]
steps: steps:
- - uses: actions/download-artifact@v4
uses: actions/download-artifact@v4 with:
with: path: executables
path: executables - name: move files one folder up
- run: mv */*.tar.gz . && find . -type d -empty -delete
name: move files one folder up working-directory: executables
run: mv */*.tar.gz . && find . -type d -empty -delete - run: sha256sum * > checksums.txt
working-directory: executables working-directory: executables
- - uses: actions/upload-artifact@v4
run: sha256sum * > checksums.txt with:
working-directory: executables name: checksums.txt
- path: executables/checksums.txt
uses: actions/upload-artifact@v4
with:
name: checksums.txt
path: executables/checksums.txt

View File

@@ -13,7 +13,7 @@ on:
cache_key: cache_key:
value: ${{ jobs.build.outputs.cache_key }} value: ${{ jobs.build.outputs.cache_key }}
cache_path: cache_path:
value: ${{ jobs.build.outputs.cache_path }} value: ${{ jobs.build.outputs.cache_path }}
env: env:
cache_path: console/dist/console cache_path: console/dist/console
@@ -25,38 +25,37 @@ jobs:
cache_path: ${{ env.cache_path }} cache_path: ${{ env.cache_path }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- - uses: actions/checkout@v4
uses: actions/checkout@v4 - uses: actions/cache/restore@v4
- timeout-minutes: 1
uses: actions/cache/restore@v4 continue-on-error: true
timeout-minutes: 1 id: cache
continue-on-error: true with:
id: cache key: console-${{ hashFiles('console', 'proto', '!console/dist') }}
with: restore-keys: |
key: console-${{ hashFiles('console', 'proto', '!console/dist') }} console-
restore-keys: | path: ${{ env.cache_path }}
console- - if: ${{ steps.cache.outputs.cache-hit != 'true' }}
path: ${{ env.cache_path }} uses: bufbuild/buf-setup-action@v1
- with:
if: ${{ steps.cache.outputs.cache-hit != 'true' }} github_token: ${{ github.token }}
uses: bufbuild/buf-setup-action@v1 version: ${{ inputs.buf_version }}
with: - if: ${{ steps.cache.outputs.cache-hit != 'true' }}
github_token: ${{ github.token }} uses: pnpm/action-setup@v4
version: ${{ inputs.buf_version }} - if: ${{ steps.cache.outputs.cache-hit != 'true' }}
- uses: actions/setup-node@v4
if: ${{ steps.cache.outputs.cache-hit != 'true' }} with:
uses: actions/setup-node@v4 node-version: ${{ inputs.node_version }}
with: cache: "pnpm"
node-version: ${{ inputs.node_version }} cache-dependency-path: pnpm-lock.yaml
cache: 'yarn' - if: ${{ steps.cache.outputs.cache-hit != 'true' }}
cache-dependency-path: console/yarn.lock name: Install dependencies
- run: pnpm install
if: ${{ steps.cache.outputs.cache-hit != 'true' }} - if: ${{ steps.cache.outputs.cache-hit != 'true' }}
run: make console_build name: Build console with Turbo
- run: pnpm turbo build --filter=./console
if: ${{ steps.cache.outputs.cache-hit != 'true' }} - if: ${{ steps.cache.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v4 uses: actions/cache/save@v4
with: with:
path: ${{ env.cache_path }} path: ${{ env.cache_path }}
key: ${{ steps.cache.outputs.cache-primary-key }} key: ${{ steps.cache.outputs.cache-primary-key }}

61
.github/workflows/docs.yml vendored Normal file
View File

@@ -0,0 +1,61 @@
name: Build docs
on:
workflow_call:
inputs:
node_version:
required: true
type: string
buf_version:
required: true
type: string
outputs:
cache_key:
value: ${{ jobs.build.outputs.cache_key }}
cache_path:
value: ${{ jobs.build.outputs.cache_path }}
env:
cache_path: docs/build
jobs:
build:
outputs:
cache_key: ${{ steps.cache.outputs.cache-primary-key }}
cache_path: ${{ env.cache_path }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/cache/restore@v4
timeout-minutes: 1
continue-on-error: true
id: cache
with:
key: docs-${{ hashFiles('docs', 'proto', '!docs/build', '!docs/node_modules', '!docs/protoc-gen-connect-openapi') }}
restore-keys: |
docs-
path: ${{ env.cache_path }}
- if: ${{ steps.cache.outputs.cache-hit != 'true' }}
uses: bufbuild/buf-setup-action@v1
with:
github_token: ${{ github.token }}
version: ${{ inputs.buf_version }}
- if: ${{ steps.cache.outputs.cache-hit != 'true' }}
uses: pnpm/action-setup@v4
- if: ${{ steps.cache.outputs.cache-hit != 'true' }}
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node_version }}
cache: "pnpm"
cache-dependency-path: pnpm-lock.yaml
- if: ${{ steps.cache.outputs.cache-hit != 'true' }}
name: Install dependencies
run: pnpm install
- if: ${{ steps.cache.outputs.cache-hit != 'true' }}
name: Build docs with Turbo
run: pnpm turbo build --filter=./docs
- if: ${{ steps.cache.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v4
with:
path: ${{ env.cache_path }}
key: ${{ steps.cache.outputs.cache-primary-key }}

View File

@@ -12,44 +12,47 @@ jobs:
browser: [firefox, chrome] browser: [firefox, chrome]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- - name: Checkout Repository
name: Checkout Repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- - uses: actions/download-artifact@v4
uses: actions/download-artifact@v4
with: with:
path: .artifacts path: .artifacts
name: zitadel-linux-amd64 name: zitadel-linux-amd64
- - name: Unpack executable
name: Unpack executable
run: | run: |
tar -xvf .artifacts/zitadel-linux-amd64.tar.gz tar -xvf .artifacts/zitadel-linux-amd64.tar.gz
mv zitadel-linux-amd64/zitadel ./zitadel mv zitadel-linux-amd64/zitadel ./zitadel
- - name: Set up QEMU
name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
- - name: Set up Docker Buildx
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- - uses: pnpm/action-setup@v4
name: Start DB and ZITADEL - uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
cache-dependency-path: pnpm-lock.yaml
- name: Install dependencies
run: pnpm install
- name: Install Cypress binary
run: cd ./e2e && pnpm exec cypress install
- name: Start DB and ZITADEL
run: | run: |
cd ./e2e cd ./e2e
ZITADEL_IMAGE=zitadel:local docker compose up --detach --wait ZITADEL_IMAGE=zitadel:local docker compose up --detach --wait
- - name: Cypress run
name: Cypress run
uses: cypress-io/github-action@v6 uses: cypress-io/github-action@v6
env: env:
CYPRESS_BASE_URL: http://localhost:8080/ui/console CYPRESS_BASE_URL: http://localhost:8080/ui/console
CYPRESS_WEBHOOK_HANDLER_HOST: host.docker.internal CYPRESS_WEBHOOK_HANDLER_HOST: host.docker.internal
CYPRESS_DATABASE_CONNECTION_URL: 'postgresql://root@localhost:26257/zitadel' CYPRESS_DATABASE_CONNECTION_URL: "postgresql://root@localhost:26257/zitadel"
CYPRESS_BACKEND_URL: http://localhost:8080 CYPRESS_BACKEND_URL: http://localhost:8080
with: with:
working-directory: e2e working-directory: e2e
browser: ${{ matrix.browser }} browser: ${{ matrix.browser }}
config-file: cypress.config.ts config-file: cypress.config.ts
- install: false
uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: always() if: always()
with: with:
name: production-tests-${{ matrix.browser }} name: production-tests-${{ matrix.browser }}

View File

@@ -20,7 +20,6 @@ on:
type: string type: string
jobs: jobs:
lint-skip: lint-skip:
name: lint skip name: lint skip
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -36,64 +35,53 @@ jobs:
continue-on-error: true continue-on-error: true
if: ${{ github.event_name == 'pull_request' }} if: ${{ github.event_name == 'pull_request' }}
steps: steps:
- - uses: actions/checkout@v4
uses: actions/checkout@v4 - uses: bufbuild/buf-setup-action@v1
- with:
uses: bufbuild/buf-setup-action@v1 version: ${{ inputs.buf_version }}
with: github_token: ${{ secrets.GITHUB_TOKEN }}
version: ${{ inputs.buf_version }} - name: lint
github_token: ${{ secrets.GITHUB_TOKEN }} uses: bufbuild/buf-lint-action@v1
- - uses: bufbuild/buf-breaking-action@v1
name: lint with:
uses: bufbuild/buf-lint-action@v1 against: "https://github.com/${{ github.repository }}.git#branch=${{ github.base_ref }}"
-
uses: bufbuild/buf-breaking-action@v1
with:
against: "https://github.com/${{ github.repository }}.git#branch=${{ github.base_ref }}"
console: console:
if: ${{ github.event_name == 'pull_request' }} if: ${{ github.event_name == 'pull_request' }}
name: console name: console
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- - name: Checkout
name: Checkout uses: actions/checkout@v4
uses: actions/checkout@v4 - uses: pnpm/action-setup@v4
- - uses: actions/setup-node@v4
uses: actions/setup-node@v4 with:
with: node-version: ${{ inputs.node_version }}
node-version: ${{ inputs.node_version }} cache: "pnpm"
cache: 'yarn' cache-dependency-path: pnpm-lock.yaml
cache-dependency-path: console/yarn.lock - run: pnpm install --filter=console
- - name: lint
run: cd console && yarn install run: make console_lint
-
name: lint
run: make console_lint
core: core:
name: core name: core
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.event_name == 'pull_request' }} if: ${{ github.event_name == 'pull_request' }}
steps: steps:
- - name: Checkout
name: Checkout uses: actions/checkout@v4
uses: actions/checkout@v4 - uses: actions/setup-go@v5
- with:
uses: actions/setup-go@v5 go-version-file: "go.mod"
with: - uses: actions/cache/restore@v4
go-version-file: 'go.mod' timeout-minutes: 1
- name: restore core
uses: actions/cache/restore@v4 with:
timeout-minutes: 1 path: ${{ inputs.core_cache_path }}
name: restore core key: ${{ inputs.core_cache_key }}
with: fail-on-cache-miss: true
path: ${{ inputs.core_cache_path }} - uses: golangci/golangci-lint-action@v6
key: ${{ inputs.core_cache_key }} with:
fail-on-cache-miss: true version: ${{ inputs.go_lint_version }}
- github-token: ${{ github.token }}
uses: golangci/golangci-lint-action@v6 only-new-issues: true
with:
version: ${{ inputs.go_lint_version }}
github-token: ${{ github.token }}
only-new-issues: true

View File

@@ -4,7 +4,7 @@ on:
workflow_call: workflow_call:
inputs: inputs:
ignore-run-cache: ignore-run-cache:
description: 'Ignore run caches' description: "Ignore run caches"
type: boolean type: boolean
required: true required: true
node_version: node_version:
@@ -44,7 +44,16 @@ jobs:
run: | run: |
tar -xvf .artifacts/zitadel-linux-amd64.tar.gz tar -xvf .artifacts/zitadel-linux-amd64.tar.gz
mv zitadel-linux-amd64/zitadel ./zitadel mv zitadel-linux-amd64/zitadel ./zitadel
- run: make login_quality - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node_version }}
cache: "pnpm"
cache-dependency-path: pnpm-lock.yaml
- name: Install dependencies
run: pnpm install
- name: Run login quality checks with Turbo
run: pnpm turbo test:unit --filter=@zitadel/login
env: env:
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }} DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
LOGIN_BAKE_CLI: depot bake LOGIN_BAKE_CLI: depot bake

6
.gitignore vendored
View File

@@ -84,7 +84,11 @@ go.work.sum
.netlify .netlify
load-test/node_modules load-test/node_modules
load-test/yarn-error.log load-test/pnpm-debug.log
load-test/dist load-test/dist
load-test/output/* load-test/output/*
.vercel .vercel
# Turbo
.turbo/
**/.turbo/

2
.npmrc Normal file
View File

@@ -0,0 +1,2 @@
auto-install-peers = true
ignore-scripts = "postman-code-generators"

View File

@@ -30,6 +30,67 @@ Help shaping the future of ZITADEL:
Follow [@zitadel](https://twitter.com/zitadel) on twitter Follow [@zitadel](https://twitter.com/zitadel) on twitter
## Quick Start for Contributors
ZITADEL uses **pnpm** as package manager and **Turbo** for build orchestration across the monorepo. Here are the most common commands you'll need:
### Prerequisites
- [Node version v20.x](https://nodejs.org/en/download/)
- [pnpm version 9.x](https://pnpm.io/installation)
- [Docker](https://docs.docker.com/engine/install/) for running databases and services
### Common Development Commands
```bash
# Install all dependencies across the monorepo
pnpm install
# Start the backend database and ZITADEL server
docker compose --file ./e2e/docker-compose.yaml up --detach zitadel
# Develop the Console (Angular app)
pnpm turbo dev --filter=console
# Develop the Login UI (Next.js app)
pnpm turbo dev --filter=@zitadel/login
# Develop the Documentation (Docusaurus)
pnpm turbo dev --filter=zitadel-docs
# Build everything
pnpm turbo build
# Lint and fix code across all packages
pnpm turbo lint
# Run tests
pnpm turbo test
# Clean up
docker compose --file ./e2e/docker-compose.yaml down
```
### Monorepo Structure
The repository is organized as follows:
| Package | Description | Technology | Development Command |
| ----------------- | --------------------------- | ------------------- | --------------------------------------------- |
| `console` | Management UI (post-login) | Angular, TypeScript | `pnpm turbo dev --filter=console` |
| `@zitadel/login` | Authentication UI | Next.js, React | `pnpm turbo dev --filter=@zitadel/login` |
| `zitadel-docs` | Documentation site | Docusaurus | `pnpm turbo dev --filter=zitadel-docs` |
| `@zitadel/client` | TypeScript client library | TypeScript | `pnpm turbo build --filter=@zitadel/client` |
| `@zitadel/proto` | Protocol buffer definitions | Protobuf | `pnpm turbo generate --filter=@zitadel/proto` |
### Development Workflow
1. **Start the backend**: `docker compose --file ./e2e/docker-compose.yaml up --detach zitadel`
2. **Choose your focus**: Run one of the development commands above
3. **Make changes**: Edit code with live reload feedback
4. **Test your changes**: Use the appropriate test commands
5. **Cleanup**: `docker compose --file ./e2e/docker-compose.yaml down`
## How to contribute ## How to contribute
We strongly recommend to [talk to us](https://zitadel.com/contact) before you start contributing to streamline our and your work. We strongly recommend to [talk to us](https://zitadel.com/contact) before you start contributing to streamline our and your work.
@@ -108,13 +169,15 @@ Please make sure you cover your changes with tests before marking a Pull Request
The code consists of the following parts: The code consists of the following parts:
| name | description | language | where to find | | name | description | language | where to find |
| --------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -------------------------------------------------- | | --------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| backend | Service that serves the grpc(-web) and RESTful API | [go](https://go.dev) | [API implementation](./internal/api/grpc) | | backend | Service that serves the grpc(-web) and RESTful API | [go](https://go.dev) | [API implementation](./internal/api/grpc) |
| console | Frontend the user interacts with after log in | [Angular](https://angular.io), [Typescript](https://www.typescriptlang.org) | [./console](./console) | | console | Frontend the user interacts with after log in | [Angular](https://angular.io), [Typescript](https://www.typescriptlang.org) | [./console](./console) |
| login | Server side rendered frontend the user interacts with during login | [go](https://go.dev), [go templates](https://pkg.go.dev/html/template) | [./internal/api/ui/login](./internal/api/ui/login) | | login | Modern authentication UI built with Next.js | [Next.js](https://nextjs.org), [React](https://reactjs.org), [TypeScript](https://www.typescriptlang.org) | [./login](./login) |
| API definitions | Specifications of the API | [Protobuf](https://developers.google.com/protocol-buffers) | [./proto/zitadel](./proto/zitadel) | | API definitions | Specifications of the API | [Protobuf](https://developers.google.com/protocol-buffers) | [./proto/zitadel](./proto/zitadel) |
| docs | Project documentation made with docusaurus | [Docusaurus](https://docusaurus.io/) | [./docs](./docs) | | docs | Project documentation made with docusaurus | [Docusaurus](https://docusaurus.io/) | [./docs](./docs) |
**Important**: This repository uses **pnpm** as package manager and **Turbo** for build orchestration. All frontend packages (console, login, docs) are managed as a monorepo with shared dependencies and optimized builds.
Please validate and test the code before you contribute. Please validate and test the code before you contribute.
@@ -261,40 +324,39 @@ export ZITADEL_IMAGE=zitadel:local GOOS=linux
make docker_image make docker_image
# If you made changes in the e2e directory, make sure you reformat the files # If you made changes in the e2e directory, make sure you reformat the files
make console_lint pnpm turbo lint --filter=e2e
# Run the tests # Run the tests
docker compose --file ./e2e/config/host.docker.internal/docker-compose.yaml run --service-ports e2e docker compose --file ./e2e/docker-compose.yaml run --service-ports e2e
``` ```
When you are happy with your changes, you can cleanup your environment. When you are happy with your changes, you can cleanup your environment.
```bash ```bash
# Stop and remove the docker containers for zitadel and the database # Stop and remove the docker containers for zitadel and the database
docker compose --file ./e2e/config/host.docker.internal/docker-compose.yaml down docker compose --file ./e2e/docker-compose.yaml down
``` ```
#### Run Local End-to-End Tests Against Your Dev Server Console #### Run Local End-to-End Tests Against Your Dev Server Console
If you also make [changes to the console](#console), you can run the test suite against your locally built backend code and frontend server. If you also make [changes to the console](#console), you can run the test suite against your locally built backend code and frontend server.
But you will have to install the relevant node dependencies.
```bash ```bash
# Install dependencies # Install dependencies (from repository root)
(cd ./e2e && npm install) pnpm install
# Run the tests interactively # Run the tests interactively
(cd ./e2e && npm run open:golangangular) cd ./e2e && pnpm run open:golangangular
# Run the tests non-interactively # Run the tests non-interactively
(cd ./e2e && npm run e2e:golangangular) cd ./e2e && pnpm run e2e:golangangular
``` ```
When you are happy with your changes, you can cleanup your environment. When you are happy with your changes, you can cleanup your environment.
```bash ```bash
# Stop and remove the docker containers for zitadel and the database # Stop and remove the docker containers for zitadel and the database
docker compose --file ./e2e/config/host.docker.internal/docker-compose.yaml down docker compose --file ./e2e/docker-compose.yaml down
``` ```
### Console ### Console
@@ -304,15 +366,15 @@ Using [Docker Compose](https://docs.docker.com/compose/), you run [PostgreSQL](h
You use the ZITADEL container as backend for your console. You use the ZITADEL container as backend for your console.
The console is run in your [Node](https://nodejs.org/en/about/) environment using [a local development server for Angular](https://angular.io/cli/serve#ng-serve), so you have fast feedback about your changes. The console is run in your [Node](https://nodejs.org/en/about/) environment using [a local development server for Angular](https://angular.io/cli/serve#ng-serve), so you have fast feedback about your changes.
We use angular-eslint/Prettier for linting/formatting, so please run `yarn lint:fix` before committing. (VSCode users, check out [this ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [this Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) to fix lint and formatting issues in development) We use **pnpm** as package manager and **Turbo** for build orchestration. Use angular-eslint/Prettier for linting/formatting, so please run `pnpm turbo lint --filter=console` before committing. (VSCode users, check out [this ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [this Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) to fix lint and formatting issues in development)
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.
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/)
- [Node version v16.17.0](https://nodejs.org/en/download/) - [Node version v20.x](https://nodejs.org/en/download/)
- [npm version 8.18.0](https://docs.npmjs.com/try-the-latest-stable-version-of-npm) - [pnpm version 9.x](https://pnpm.io/installation)
- [Cypress runtime dependencies](https://docs.cypress.io/guides/continuous-integration/introduction#Dependencies) - [Cypress runtime dependencies](https://docs.cypress.io/guides/continuous-integration/introduction#Dependencies)
<details> <details>
@@ -328,11 +390,9 @@ The commands in this section are tested against the following software versions:
Run the database and the latest backend locally. Run the database and the latest backend locally.
```bash ```bash
# Change to the console directory # Start from the root of the repository
cd ./console
# You just need the db and the zitadel services to develop the console against. # You just need the db and the zitadel services to develop the console against.
docker compose --file ../e2e/docker-compose.yaml up --detach zitadel docker compose --file ./e2e/docker-compose.yaml up --detach zitadel
``` ```
When the backend is ready, you have the latest zitadel exposed at http://localhost:8080. When the backend is ready, you have the latest zitadel exposed at http://localhost:8080.
@@ -351,69 +411,244 @@ To allow console access via http://localhost:4200, you have to configure the ZIT
You can run the local console development server now. You can run the local console development server now.
```bash ```bash
# Install npm dependencies # Install dependencies (from repository root)
yarn install pnpm install
# Generate source files from Protos # Option 1: Run console development server with Turbo (recommended)
yarn generate pnpm turbo dev --filter=console
# Start the server # Option 2: Run console development server directly
yarn start cd ./console && pnpm start
# Option 3: Build and serve console (production build)
pnpm turbo build --filter=console
cd ./console && pnpm serve
# If you don't want to develop against http://localhost:8080, you can use another environment # If you don't want to develop against http://localhost:8080, you can use another environment
ENVIRONMENT_JSON_URL=https://my-cloud-instance-abcdef.zitadel.cloud/ui/console/assets/environment.json yarn start ENVIRONMENT_JSON_URL=https://my-cloud-instance-abcdef.zitadel.cloud/ui/console/assets/environment.json pnpm turbo dev --filter=console
``` ```
Navigate to http://localhost:4200/. Navigate to http://localhost:4200/.
Make some changes to the source code and see how the browser is automatically updated. Make some changes to the source code and see how the browser is automatically updated.
#### Console Development Scripts
Here are the most useful scripts for console development:
```bash
# Generate protobuf files (happens automatically with Turbo dependencies)
pnpm turbo generate --filter=console
# Run development server with live reload
pnpm turbo dev --filter=console
# Build for production
pnpm turbo build --filter=console
# Lint and fix code
pnpm turbo lint --filter=console
# Run unit tests
pnpm turbo test --filter=console
# Run all console-related tasks
pnpm turbo dev lint test --filter=console
```
After making changes to the code, you should run the end-to-end-tests. After making changes to the code, you should run the end-to-end-tests.
Open another shell. Open another shell.
```bash ```bash
# Reformat your console code # Reformat your console code using Turbo
yarn lint:fix pnpm turbo lint --filter=console
# Change to the e2e directory # Change to the e2e directory
cd .. && cd e2e/ cd ./e2e
# If you made changes in the e2e directory, make sure you reformat the files here too # If you made changes in the e2e directory, make sure you reformat the files here too
npm run lint:fix pnpm run lint:fix
# Install npm dependencies # Install pnpm dependencies
npm install pnpm install
# Run all e2e tests # Run all e2e tests
npm run e2e:angular -- --headed pnpm run e2e:angular -- --headed
``` ```
You can also open the test suite interactively for fast feedback on specific tests. You can also open the test suite interactively for fast feedback on specific tests.
```bash ```bash
# Run tests interactively # Run tests interactively
npm run open:angular pnpm run open:angular
``` ```
If you also make [changes to the backend code](#backend--login), you can run the test against your locally built backend code and frontend server If you also make [changes to the backend code](#backend--login), you can run the test against your locally built backend code and frontend server
```bash ```bash
npm run open:golangangular pnpm run open:golangangular
npm run e2e:golangangular pnpm run e2e:golangangular
``` ```
When you are happy with your changes, you can format your code and cleanup your environment When you are happy with your changes, you can format your code and cleanup your environment
```bash ```bash
# Stop and remove the docker containers for zitadel and the database # Stop and remove the docker containers for zitadel and the database
docker compose down docker compose --file ./e2e/docker-compose.yaml down
```
### Login UI
The Login UI is a Next.js application that provides the user interface for authentication flows. It's located in the `./login` directory and uses pnpm and Turbo for development.
#### Prerequisites
- [Node version v20.x](https://nodejs.org/en/download/)
- [pnpm version 9.x](https://pnpm.io/installation)
- [Docker](https://docs.docker.com/engine/install/) for running the backend
#### Development Setup
```bash
# Start from the root of the repository
# Start the database and ZITADEL backend
docker compose --file ./e2e/docker-compose.yaml up --detach zitadel
# Install dependencies
pnpm install
# Option 1: Run login development server with Turbo (recommended)
pnpm turbo dev --filter=@zitadel/login
# Option 2: Run login development server directly
cd ./login && pnpm dev
# Option 3: Build and serve login (production build)
pnpm turbo build --filter=@zitadel/login
cd ./login && pnpm start
```
The login UI will be available at http://localhost:3000.
#### Login Development Scripts
Here are the most useful scripts for login development:
```bash
# Generate protobuf files (happens automatically with Turbo dependencies)
pnpm turbo generate --filter=@zitadel/login
# Run development server with live reload
pnpm turbo dev --filter=@zitadel/login
# Build for production
pnpm turbo build --filter=@zitadel/login
# Lint and fix code
pnpm turbo lint --filter=@zitadel/login
# Run unit tests
pnpm turbo test:unit --filter=@zitadel/login
# Run integration tests
pnpm turbo test:integration --filter=@zitadel/login
# Run acceptance tests
pnpm turbo test:acceptance --filter=@zitadel/login
# Run all login-related tasks
pnpm turbo dev lint test:unit --filter=@zitadel/login
```
#### Login Architecture
The login application consists of multiple packages:
- `@zitadel/login` - Main Next.js application
- `@zitadel/client` - TypeScript client library for ZITADEL APIs
- `@zitadel/proto` - Protocol buffer definitions and generated code
The build process uses Turbo to orchestrate dependencies:
1. Proto generation (`@zitadel/proto#generate`)
2. Client library build (`@zitadel/client#build`)
3. Login application build (`@zitadel/login#build`)
#### Testing the Login UI
```bash
# Run unit tests
pnpm turbo test:unit --filter=@zitadel/login
# Run integration tests (requires running backend)
pnpm turbo test:integration --filter=@zitadel/login
# Run acceptance tests
pnpm turbo test:acceptance --filter=@zitadel/login
# Run all tests
pnpm turbo test:unit test:integration test:acceptance --filter=@zitadel/login
```
When you are happy with your changes, cleanup your environment:
```bash
# Stop and remove the docker containers
docker compose --file ./e2e/docker-compose.yaml down
``` ```
## Contribute docs ## Contribute docs
Project documentation is made with docusaurus and is located under [./docs](./docs). Project documentation is made with Docusaurus and is located under [./docs](./docs). The documentation uses **pnpm** and **Turbo** for development and build processes.
### Local Development
```bash
# Install dependencies (from repository root)
pnpm install
# Option 1: Run docs development server with Turbo (recommended)
pnpm turbo dev --filter=zitadel-docs
# Option 2: Run docs development server directly
cd ./docs && pnpm start
# Option 3: Build and serve docs (production build)
pnpm turbo build --filter=zitadel-docs
cd ./docs && pnpm serve
```
#### Docs Development Scripts
Here are the most useful scripts for docs development:
```bash
# Generate API documentation and configuration docs
pnpm turbo generate --filter=zitadel-docs
# Run development server with live reload
pnpm turbo dev --filter=zitadel-docs
# Build for production
pnpm turbo build --filter=zitadel-docs
# Lint and fix code
pnpm turbo lint --filter=zitadel-docs
# Run all docs-related tasks
pnpm turbo dev lint build --filter=zitadel-docs
```
The docs build process automatically:
1. Downloads required protoc plugins
2. Generates gRPC documentation from proto files
3. Generates API documentation from OpenAPI specs
4. Copies configuration files
5. Builds the Docusaurus site
### Local testing ### Local testing
Please refer to the [README](./docs/README.md) for more information and local testing. The documentation server will be available at http://localhost:3000 with live reload for fast development feedback.
### Style guide ### Style guide
@@ -450,6 +685,7 @@ Please make sure that the languages within the files remain in their own languag
If you have added support for a new language, please also ensure that it is added in the list of languages in all the other language files. If you have added support for a new language, please also ensure that it is added in the list of languages in all the other language files.
You also have to add some changes to the following files: You also have to add some changes to the following files:
- [Register Local File](./console/src/app/app.module.ts) - [Register Local File](./console/src/app/app.module.ts)
- [Add Supported Language](./console/src/app/utils/language.ts) - [Add Supported Language](./console/src/app/utils/language.ts)
- [Customized Text Docs](./docs/docs/guides/manage/customize/texts.md) - [Customized Text Docs](./docs/docs/guides/manage/customize/texts.md)

View File

@@ -97,18 +97,17 @@ console_move:
.PHONY: console_dependencies .PHONY: console_dependencies
console_dependencies: console_dependencies:
cd console && \ pnpm install
yarn install --immutable
.PHONY: console_client .PHONY: console_client
console_client: console_client:
cd console && \ cd console && \
yarn generate pnpm generate
.PHONY: console_build .PHONY: console_build
console_build: console_dependencies console_client console_build: console_dependencies console_client
cd console && \ cd console && \
yarn build pnpm build
.PHONY: clean .PHONY: clean
clean: clean:
@@ -166,8 +165,7 @@ core_integration_test: core_integration_server_start core_integration_test_packa
.PHONY: console_lint .PHONY: console_lint
console_lint: console_lint:
cd console && \ pnpm turbo lint --filter=./console
yarn lint
.PHONY: core_lint .PHONY: core_lint
core_lint: core_lint:

View File

@@ -107,10 +107,11 @@ FROM node:20-buster AS console-deps
WORKDIR /zitadel/console WORKDIR /zitadel/console
COPY console/package.json . COPY pnpm-lock.yaml .
COPY console/yarn.lock . COPY pnpm-workspace.yaml .
COPY console/package.json console/
RUN yarn install --frozen-lockfile RUN corepack enable pnpm && pnpm install --frozen-lockfile --filter=console
# ####################################### # #######################################
# generate console client # generate console client
@@ -127,7 +128,7 @@ COPY console/package.json .
COPY console/buf.*.yaml . COPY console/buf.*.yaml .
COPY proto ../proto COPY proto ../proto
RUN yarn generate RUN pnpm generate
# ####################################### # #######################################
# Gather all console files # Gather all console files
@@ -145,7 +146,7 @@ COPY console/tsconfig* .
# Build console # Build console
# ####################################### # #######################################
FROM console-gathered AS console FROM console-gathered AS console
RUN yarn build RUN pnpm build
# ############################################################################## # ##############################################################################
# build the executable # build the executable
@@ -264,7 +265,7 @@ FROM console-gathered AS lint-console
COPY console/.eslintrc.js . COPY console/.eslintrc.js .
COPY console/.prettier* . COPY console/.prettier* .
RUN yarn lint RUN pnpm lint
# ####################################### # #######################################
# core # core

2
console/.gitignore vendored
View File

@@ -36,7 +36,7 @@ speed-measure-plugin*.json
/coverage /coverage
/libpeerconnection.log /libpeerconnection.log
npm-debug.log npm-debug.log
yarn-error.log pnpm-debug.log
testem.log testem.log
/typings /typings

View File

@@ -1,27 +1,137 @@
# Console # Console Angular App
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.3.20. This is the ZITADEL Console Angular application.
## Development server ## Development
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. ### Prerequisites
## Code scaffolding - Node.js 18 or later
- pnpm (latest)
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ### Installation
## Build ```bash
pnpm install
```
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. ### Proto Generation
## Running unit tests The Console app uses **dual proto generation** with Turbo dependency management:
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 1. **`@zitadel/proto` generation**: Modern ES modules with `@bufbuild/protobuf` for v2 APIs
2. **Local `buf.gen.yaml` generation**: Traditional protobuf JavaScript classes for v1 APIs
## Running end-to-end tests The Console app's `turbo.json` ensures that `@zitadel/proto#generate` runs before the Console's own generation, providing both:
Please refer to the [contributing guide](../CONTRIBUTING.md#console) - Modern schemas from `@zitadel/proto` (e.g., `UserSchema`, `DetailsSchema`)
- Legacy classes from `src/app/proto/generated` (e.g., `User`, `Project`)
## Further help Generated files:
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). - **`@zitadel/proto`**: Modern ES modules in `login/packages/zitadel-proto/`
- **Local generation**: Traditional protobuf files in `src/app/proto/generated/`
- TypeScript definition files (`.d.ts`)
- JavaScript files (`.js`)
- gRPC client files (`*ServiceClientPb.ts`)
- OpenAPI/Swagger JSON files (`.swagger.json`)
To generate proto files:
```bash
pnpm run generate
```
This automatically runs both generations in the correct order via Turbo dependencies.
### Development Server
To start the development server:
```bash
pnpm start
```
This will:
1. Fetch the environment configuration from the server
2. Serve the app on the default port
### Building
To build for production:
```bash
pnpm run build
```
This will:
1. Generate proto files (via `prebuild` script)
2. Build the Angular app with production optimizations
### Linting
To run linting and formatting checks:
```bash
pnpm run lint
```
To auto-fix formatting issues:
```bash
pnpm run lint:fix
```
## Project Structure
- `src/app/proto/generated/` - Generated proto files (Angular-specific format)
- `buf.gen.yaml` - Local proto generation configuration
- `turbo.json` - Turbo dependency configuration for proto generation
- `prebuild.development.js` - Development environment configuration script
## Proto Generation Details
The Console app uses **dual proto generation** managed by Turbo dependencies:
### Dependency Chain
The Console app has the following build dependencies managed by Turbo:
1. `@zitadel/proto#generate` - Generates modern protobuf files
2. `@zitadel/client#build` - Builds the TypeScript gRPC client library
3. `console#generate` - Generates Console-specific protobuf files
4. `console#build` - Builds the Angular application
This ensures that the Console always has access to the latest client library and protobuf definitions.
### Legacy v1 API (Traditional Protobuf)
- Uses local `buf.gen.yaml` configuration
- Generates traditional Google protobuf JavaScript classes extending `jspb.Message`
- Uses plugins: `protocolbuffers/js`, `grpc/web`, `grpc-ecosystem/openapiv2`
- Output: `src/app/proto/generated/`
- Used for: Most existing Console functionality
### Modern v2 API (ES Modules)
- Uses `@zitadel/proto` package generation
- Generates modern ES modules with `@bufbuild/protobuf`
- Uses plugin: `@bufbuild/es` with ES modules and JSON types
- Output: `login/packages/zitadel-proto/`
- Used for: New user v2 API and services
### Dependency Management
The Console's `turbo.json` ensures proper execution order:
1. `@zitadel/proto#generate` runs first (modern ES modules)
2. Console's local generation runs second (traditional protobuf)
3. Build/lint/start tasks depend on both generations being complete
This approach allows the Console app to use both v1 and v2 APIs while maintaining proper build dependencies.
## Legacy Information
This project was originally generated with Angular CLI version 8.3.20 and has been updated over time.

View File

@@ -3,12 +3,12 @@
"version": "0.0.0", "version": "0.0.0",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"dev": "node prebuild.development.js && ng serve",
"start": "node prebuild.development.js && ng serve", "start": "node prebuild.development.js && ng serve",
"build": "ng build --configuration production --base-href=/ui/console/", "build": "ng build --configuration production --base-href=/ui/console/",
"prelint": "npm run generate",
"lint": "ng lint && prettier --check src", "lint": "ng lint && prettier --check src",
"lint:fix": "prettier --write src", "lint:fix": "prettier --write src",
"generate": "buf generate ../proto --include-imports --include-wkt" "generate": "pnpm exec buf generate ../proto --include-imports --include-wkt"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
@@ -24,6 +24,7 @@
"@angular/platform-browser-dynamic": "^16.2.12", "@angular/platform-browser-dynamic": "^16.2.12",
"@angular/router": "^16.2.12", "@angular/router": "^16.2.12",
"@angular/service-worker": "^16.2.12", "@angular/service-worker": "^16.2.12",
"@bufbuild/protobuf": "^2.2.2",
"@connectrpc/connect": "^2.0.0", "@connectrpc/connect": "^2.0.0",
"@connectrpc/connect-web": "^2.0.0", "@connectrpc/connect-web": "^2.0.0",
"@ctrl/ngx-codemirror": "^6.1.0", "@ctrl/ngx-codemirror": "^6.1.0",
@@ -31,8 +32,8 @@
"@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2", "@fortawesome/free-brands-svg-icons": "^6.7.2",
"@ngx-translate/core": "^15.0.0", "@ngx-translate/core": "^15.0.0",
"@zitadel/client": "1.2.0", "@zitadel/client": "workspace:*",
"@zitadel/proto": "1.2.0", "@zitadel/proto": "workspace:*",
"angular-oauth2-oidc": "^15.0.1", "angular-oauth2-oidc": "^15.0.1",
"angularx-qrcode": "^16.0.2", "angularx-qrcode": "^16.0.2",
"buffer": "^6.0.3", "buffer": "^6.0.3",

View File

@@ -139,7 +139,7 @@ export class FeaturesComponent {
}, {}); }, {});
// to save special flags they have to be handled here // to save special flags they have to be handled here
req.loginV2 = { req['loginV2'] = {
required: toggleStates.loginV2.enabled, required: toggleStates.loginV2.enabled,
baseUri: toggleStates.loginV2.baseUri, baseUri: toggleStates.loginV2.baseUri,
}; };

View File

@@ -89,7 +89,7 @@ export class ActionTwoAddTargetDialogComponent {
nanos: 0, nanos: 0,
}; };
const targetType: Extract<MessageInitShape<typeof CreateTargetRequestSchema>['targetType'], { case: TargetTypes }> = const targetType: MessageInitShape<typeof CreateTargetRequestSchema>['targetType'] =
type === 'restWebhook' type === 'restWebhook'
? { case: type, value: { interruptOnError } } ? { case: type, value: { interruptOnError } }
: type === 'restCall' : type === 'restCall'

View File

@@ -22,9 +22,8 @@ const CACHE_WARNING_MS = 5 * 60 * 1000; // 5 minutes
templateUrl: './oidc-webkeys.component.html', templateUrl: './oidc-webkeys.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class OidcWebKeysComponent implements OnInit { export class OidcWebKeysComponent {
protected readonly refresh = new Subject<true>(); protected readonly refresh = new Subject<true>();
protected readonly webKeysEnabled$: Observable<boolean>;
protected readonly webKeys$: Observable<WebKey[]>; protected readonly webKeys$: Observable<WebKey[]>;
protected readonly inactiveWebKeys$: Observable<WebKey[]>; protected readonly inactiveWebKeys$: Observable<WebKey[]>;
protected readonly nextWebKeyCandidate$: Observable<WebKey | undefined>; protected readonly nextWebKeyCandidate$: Observable<WebKey | undefined>;
@@ -34,17 +33,12 @@ export class OidcWebKeysComponent implements OnInit {
constructor( constructor(
private readonly webKeysService: WebKeysService, private readonly webKeysService: WebKeysService,
private readonly featureService: NewFeatureService,
private readonly toast: ToastService, private readonly toast: ToastService,
private readonly timestampToDatePipe: TimestampToDatePipe, private readonly timestampToDatePipe: TimestampToDatePipe,
private readonly dialog: MatDialog, private readonly dialog: MatDialog,
private readonly destroyRef: DestroyRef, private readonly destroyRef: DestroyRef,
private readonly router: Router,
private readonly route: ActivatedRoute,
) { ) {
this.webKeysEnabled$ = this.getWebKeysEnabled().pipe(shareReplay({ refCount: true, bufferSize: 1 })); const webKeys$ = this.getWebKeys().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
const webKeys$ = this.getWebKeys(this.webKeysEnabled$).pipe(shareReplay({ refCount: true, bufferSize: 1 }));
this.webKeys$ = webKeys$.pipe(map((webKeys) => webKeys.filter((webKey) => webKey.state !== State.INACTIVE))); this.webKeys$ = webKeys$.pipe(map((webKeys) => webKeys.filter((webKey) => webKey.state !== State.INACTIVE)));
this.inactiveWebKeys$ = webKeys$.pipe(map((webKeys) => webKeys.filter((webKey) => webKey.state === State.INACTIVE))); this.inactiveWebKeys$ = webKeys$.pipe(map((webKeys) => webKeys.filter((webKey) => webKey.state === State.INACTIVE)));
@@ -52,34 +46,7 @@ export class OidcWebKeysComponent implements OnInit {
this.nextWebKeyCandidate$ = this.getNextWebKeyCandidate(this.webKeys$); this.nextWebKeyCandidate$ = this.getNextWebKeyCandidate(this.webKeys$);
} }
ngOnInit(): void { private getWebKeys() {
// redirect away from this page if web keys are not enabled
// this also preloads the web keys enabled state
this.webKeysEnabled$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(async (webKeysEnabled) => {
if (webKeysEnabled) {
return;
}
await this.router.navigate([], {
relativeTo: this.route,
queryParamsHandling: 'merge',
queryParams: {
id: null,
},
});
});
}
private getWebKeysEnabled() {
return defer(() => this.featureService.getInstanceFeatures()).pipe(
map((features) => features.webKey?.enabled ?? false),
catchError((err) => {
this.toast.showError(err);
return of(false);
}),
);
}
private getWebKeys(webKeysEnabled$: Observable<boolean>) {
return this.refresh.pipe( return this.refresh.pipe(
startWith(true), startWith(true),
switchMap(() => { switchMap(() => {
@@ -87,12 +54,6 @@ export class OidcWebKeysComponent implements OnInit {
}), }),
map(({ webKeys }) => webKeys), map(({ webKeys }) => webKeys),
catchError(async (err) => { catchError(async (err) => {
const webKeysEnabled = await firstValueFrom(webKeysEnabled$);
// suppress errors if web keys are not enabled
if (!webKeysEnabled) {
return [];
}
this.toast.showError(err); this.toast.showError(err);
return []; return [];
}), }),

View File

@@ -204,7 +204,7 @@ export class UserCreateV2Component implements OnInit {
if (authenticationFactor.factor === 'initialPassword') { if (authenticationFactor.factor === 'initialPassword') {
const { password } = authenticationFactor.form.getRawValue(); const { password } = authenticationFactor.form.getRawValue();
humanReq.passwordType = { humanReq['passwordType'] = {
case: 'password', case: 'password',
value: { value: {
password, password,

View File

@@ -8,12 +8,14 @@ import { Gender, HumanProfile, HumanProfileSchema } from '@zitadel/proto/zitadel
import { filter, startWith } from 'rxjs/operators'; import { filter, startWith } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Profile } from '@zitadel/proto/zitadel/user_pb'; import { Profile } from '@zitadel/proto/zitadel/user_pb';
import { create } from '@bufbuild/protobuf'; //@ts-ignore
import { create } from '@zitadel/client';
function toHumanProfile(profile: HumanProfile | Profile): HumanProfile { function toHumanProfile(profile: HumanProfile | Profile): HumanProfile {
if (profile.$typeName === 'zitadel.user.v2.HumanProfile') { if (profile.$typeName === 'zitadel.user.v2.HumanProfile') {
return profile; return profile;
} }
return create(HumanProfileSchema, { return create(HumanProfileSchema, {
givenName: profile.firstName, givenName: profile.firstName,
familyName: profile.lastName, familyName: profile.lastName,

View File

@@ -36,10 +36,10 @@ import { AuthenticationService } from 'src/app/services/authentication.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { UserState as UserStateV1 } from 'src/app/proto/generated/zitadel/user_pb'; import { UserState as UserStateV1 } from 'src/app/proto/generated/zitadel/user_pb';
type Query = Exclude< type ListUsersRequest = MessageInitShape<typeof ListUsersRequestSchema>;
Exclude<MessageInitShape<typeof ListUsersRequestSchema>['queries'], undefined>[number]['query'], type QueriesArray = NonNullable<ListUsersRequest['queries']>;
undefined type QueryWrapper = QueriesArray extends readonly (infer T)[] ? T : never;
>; type Query = NonNullable<QueryWrapper extends { query?: infer Q } ? Q : never>;
@Component({ @Component({
selector: 'cnsl-user-table', selector: 'cnsl-user-table',

View File

@@ -229,94 +229,3 @@ export class UserService {
return this.grpcService.userNew.setPassword(create(SetPasswordRequestSchema, req)); return this.grpcService.userNew.setPassword(create(SetPasswordRequestSchema, req));
} }
} }
function userToV2(user: User): UserV2 {
const details = user.getDetails();
return create(UserSchema, {
userId: user.getId(),
details: details && detailsToV2(details),
state: user.getState() as number as UserState,
username: user.getUserName(),
loginNames: user.getLoginNamesList(),
preferredLoginName: user.getPreferredLoginName(),
type: typeToV2(user),
});
}
function detailsToV2(details: ObjectDetails): Details {
const changeDate = details.getChangeDate();
return create(DetailsSchema, {
sequence: BigInt(details.getSequence()),
changeDate: changeDate && timestampToV2(changeDate),
resourceOwner: details.getResourceOwner(),
});
}
function timestampToV2(timestamp: Timestamp): TimestampV2 {
return create(TimestampSchema, {
seconds: BigInt(timestamp.getSeconds()),
nanos: timestamp.getNanos(),
});
}
function typeToV2(user: User): UserV2['type'] {
const human = user.getHuman();
if (human) {
return { case: 'human', value: humanToV2(user, human) };
}
const machine = user.getMachine();
if (machine) {
return { case: 'machine', value: machineToV2(machine) };
}
return { case: undefined };
}
function humanToV2(user: User, human: Human): HumanUser {
const profile = human.getProfile();
const email = human.getEmail()?.getEmail();
const phone = human.getPhone();
const passwordChanged = human.getPasswordChanged();
return create(HumanUserSchema, {
userId: user.getId(),
state: user.getState() as number as UserState,
username: user.getUserName(),
loginNames: user.getLoginNamesList(),
preferredLoginName: user.getPreferredLoginName(),
profile: profile && humanProfileToV2(profile),
email: { email },
phone: phone && humanPhoneToV2(phone),
passwordChangeRequired: false,
passwordChanged: passwordChanged && timestampToV2(passwordChanged),
});
}
function humanProfileToV2(profile: Profile): HumanProfile {
return create(HumanProfileSchema, {
givenName: profile.getFirstName(),
familyName: profile.getLastName(),
nickName: profile.getNickName(),
displayName: profile.getDisplayName(),
preferredLanguage: profile.getPreferredLanguage(),
gender: profile.getGender() as number as Gender,
avatarUrl: profile.getAvatarUrl(),
});
}
function humanPhoneToV2(phone: Phone): HumanPhone {
return create(HumanPhoneSchema, {
phone: phone.getPhone(),
isVerified: phone.getIsPhoneVerified(),
});
}
function machineToV2(machine: Machine): MachineUser {
return create(MachineUserSchema, {
name: machine.getName(),
description: machine.getDescription(),
hasSecret: machine.getHasSecret(),
accessTokenType: machine.getAccessTokenType() as number as AccessTokenType,
});
}

28
console/turbo.json Normal file
View File

@@ -0,0 +1,28 @@
{
"$schema": "https://turbo.build/schema.json",
"extends": ["//"],
"tasks": {
"generate": {
"dependsOn": ["@zitadel/proto#generate"],
"outputs": ["src/app/proto/generated/**"]
},
"build": {
"dependsOn": ["generate", "@zitadel/client#build"],
"outputs": ["dist/**"]
},
"dev": {
"dependsOn": ["generate", "@zitadel/client#build"],
"cache": false,
"persistent": true
},
"start": {
"dependsOn": ["generate", "@zitadel/client#build"],
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["generate"],
"outputs": []
}
}
}

File diff suppressed because it is too large Load Diff

3
docs/.gitignore vendored
View File

@@ -24,7 +24,6 @@ docs/apis/resources
package-lock.json package-lock.json
npm-debug.log* npm-debug.log*
yarn-debug.log* pnpm-debug.log*
yarn-error.log*
.vercel .vercel
/protoc-gen-connect-openapi* /protoc-gen-connect-openapi*

View File

@@ -2,39 +2,78 @@
This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern static website generator. This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern static website generator.
The documentation is part of the ZITADEL monorepo and uses **pnpm** and **Turbo** for development and build processes.
## Quick Start
```bash
# From the repository root
pnpm install
# Start development server (with Turbo)
pnpm turbo dev --filter=zitadel-docs
# Or start directly from docs directory
cd docs && pnpm start
```
The site will be available at http://localhost:3000
## Available Scripts
All scripts can be run from the repository root using Turbo:
```bash
# Development server with live reload
pnpm turbo dev --filter=zitadel-docs
# Build for production
pnpm turbo build --filter=zitadel-docs
# Generate API documentation and configuration docs
pnpm turbo generate --filter=zitadel-docs
# Lint and fix code
pnpm turbo lint --filter=zitadel-docs
# Serve production build locally
cd docs && pnpm serve
```
## Add new Sites to existing Topics ## Add new Sites to existing Topics
To add a new site to the already existing structure simply save the `md` file into the corresponding folder and append the sites id int the file `sidebars.js`. To add a new site to the already existing structure simply save the `md` file into the corresponding folder and append the sites id int the file `sidebars.js`.
If you are introducing new APIs (gRPC), you need to add a new entry to `docusaurus.config.js` under the `plugins` section. If you are introducing new APIs (gRPC), you need to add a new entry to `docusaurus.config.js` under the `plugins` section.
## Installation ## Build Process
Install dependencies with The documentation build process automatically:
```
yarn install
```
then run
```
yarn generate
```
1. **Downloads required protoc plugins** - Ensures `protoc-gen-connect-openapi` is available
2. **Generates gRPC documentation** - Creates API docs from proto files
3. **Generates API documentation** - Creates OpenAPI specification docs
4. **Copies configuration files** - Includes configuration examples
5. **Builds the Docusaurus site** - Generates the final static site
## Local Development ## Local Development
Start a local development server with ### Standard Development
``` ```bash
yarn start # Install dependencies
pnpm install
# Start development server
pnpm start
``` ```
When working on the API docs, run a local development server with ### API Documentation Development
``` When working on the API docs, run a local development server with:
yarn start:api
```bash
pnpm start:api
``` ```
## Container Image ## Container Image

View File

@@ -264,7 +264,7 @@ module.exports = {
outputDir: "docs/apis/resources/auth", outputDir: "docs/apis/resources/auth",
sidebarOptions: { sidebarOptions: {
groupPathsBy: "tag", groupPathsBy: "tag",
categoryLinkSource: "tag", categoryLinkSource: "auto",
}, },
}, },
mgmt: { mgmt: {
@@ -272,7 +272,7 @@ module.exports = {
outputDir: "docs/apis/resources/mgmt", outputDir: "docs/apis/resources/mgmt",
sidebarOptions: { sidebarOptions: {
groupPathsBy: "tag", groupPathsBy: "tag",
categoryLinkSource: "tag", categoryLinkSource: "auto",
}, },
}, },
admin: { admin: {
@@ -280,7 +280,7 @@ module.exports = {
outputDir: "docs/apis/resources/admin", outputDir: "docs/apis/resources/admin",
sidebarOptions: { sidebarOptions: {
groupPathsBy: "tag", groupPathsBy: "tag",
categoryLinkSource: "tag", categoryLinkSource: "auto",
}, },
}, },
system: { system: {
@@ -288,7 +288,7 @@ module.exports = {
outputDir: "docs/apis/resources/system", outputDir: "docs/apis/resources/system",
sidebarOptions: { sidebarOptions: {
groupPathsBy: "tag", groupPathsBy: "tag",
categoryLinkSource: "tag", categoryLinkSource: "auto",
}, },
}, },
user_v2: { user_v2: {

View File

@@ -4,20 +4,23 @@
"private": true, "private": true,
"scripts": { "scripts": {
"docusaurus": "docusaurus", "docusaurus": "docusaurus",
"dev": "docusaurus start",
"start": "docusaurus start", "start": "docusaurus start",
"start:api": "yarn run generate && docusaurus start", "start:api": "pnpm run generate && docusaurus start",
"build": "yarn run generate && docusaurus build", "build": "pnpm run ensure-plugins && pnpm run generate && docusaurus build",
"swizzle": "docusaurus swizzle", "swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy", "deploy": "docusaurus deploy",
"clear": "docusaurus clear", "clear": "docusaurus clear",
"serve": "docusaurus serve", "serve": "docusaurus serve",
"write-translations": "docusaurus write-translations", "write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids", "write-heading-ids": "docusaurus write-heading-ids",
"generate": "yarn run generate:grpc && yarn run generate:apidocs && yarn run generate:configdocs", "ensure-plugins": "if [ ! -f \"protoc-gen-connect-openapi/protoc-gen-connect-openapi\" ]; then sh ./plugin-download.sh; fi",
"generate:grpc": "buf generate ../proto", "debug-plugins": "echo \"PWD: $(pwd)\" && echo \"Plugin file exists: $(test -f protoc-gen-connect-openapi/protoc-gen-connect-openapi && echo 'yes' || echo 'no')\" && echo \"Plugin executable: $(test -x protoc-gen-connect-openapi/protoc-gen-connect-openapi && echo 'yes' || echo 'no')\" && ls -la protoc-gen-connect-openapi/ || echo 'Plugin directory not found'",
"generate": "pnpm run generate:grpc && pnpm run generate:apidocs && pnpm run generate:configdocs",
"generate:grpc": "pnpm run ensure-plugins && buf generate ../proto",
"generate:apidocs": "docusaurus gen-api-docs all", "generate:apidocs": "docusaurus gen-api-docs all",
"generate:configdocs": "cp -r ../cmd/defaults.yaml ./docs/self-hosting/manage/configure/ && cp -r ../cmd/setup/steps.yaml ./docs/self-hosting/manage/configure/", "generate:configdocs": "cp -r ../cmd/defaults.yaml ./docs/self-hosting/manage/configure/ && cp -r ../cmd/setup/steps.yaml ./docs/self-hosting/manage/configure/",
"generate:re-gen": "yarn generate:clean-all && yarn generate", "generate:re-gen": "yarn generate:clean-all && pnpm generate",
"generate:clean-all": "docusaurus clean-api-docs all", "generate:clean-all": "docusaurus clean-api-docs all",
"postinstall": "sh ./plugin-download.sh" "postinstall": "sh ./plugin-download.sh"
}, },
@@ -64,5 +67,5 @@
"@docusaurus/types": "^3.8.1", "@docusaurus/types": "^3.8.1",
"tailwindcss": "^3.2.4" "tailwindcss": "^3.2.4"
}, },
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" "packageManager": "pnpm@9.1.2+sha256.19c17528f9ca20bd442e4ca42f00f1b9808a9cb419383cd04ba32ef19322aba7"
} }

View File

@@ -1,8 +1,28 @@
echo $(uname -m) #!/bin/bash
mkdir protoc-gen-connect-openapi set -e
echo "Downloading protoc-gen-connect-openapi plugin..."
echo "Architecture: $(uname -m)"
echo "OS: $(uname)"
# Create directory if it doesn't exist
mkdir -p protoc-gen-connect-openapi
cd ./protoc-gen-connect-openapi/ cd ./protoc-gen-connect-openapi/
# Skip download if plugin already exists and is executable
if [ -f "protoc-gen-connect-openapi" ] && [ -x "protoc-gen-connect-openapi" ]; then
echo "Plugin already exists and is executable"
./protoc-gen-connect-openapi --version || echo "Plugin version check failed, but file exists"
exit 0
fi
# Clean up any partial downloads
rm -f protoc-gen-connect-openapi.tar.gz protoc-gen-connect-openapi
# Determine download URL based on OS and architecture
if [ "$(uname)" = "Darwin" ]; then if [ "$(uname)" = "Darwin" ]; then
curl -L -o protoc-gen-connect-openapi.tar.gz https://github.com/sudorandom/protoc-gen-connect-openapi/releases/download/v0.18.0/protoc-gen-connect-openapi_0.18.0_darwin_all.tar.gz echo "Downloading for Darwin..."
URL="https://github.com/sudorandom/protoc-gen-connect-openapi/releases/download/v0.18.0/protoc-gen-connect-openapi_0.18.0_darwin_all.tar.gz"
else else
ARCH=$(uname -m) ARCH=$(uname -m)
case $ARCH in case $ARCH in
@@ -17,6 +37,34 @@ else
exit 1 exit 1
;; ;;
esac esac
curl -L -o protoc-gen-connect-openapi.tar.gz https://github.com/sudorandom/protoc-gen-connect-openapi/releases/download/v0.18.0/protoc-gen-connect-openapi_0.18.0_linux_${ARCH}.tar.gz echo "Downloading for Linux ${ARCH}..."
URL="https://github.com/sudorandom/protoc-gen-connect-openapi/releases/download/v0.18.0/protoc-gen-connect-openapi_0.18.0_linux_${ARCH}.tar.gz"
fi fi
tar -xvf protoc-gen-connect-openapi.tar.gz
# Download with retries
echo "Downloading from: $URL"
curl -L -o protoc-gen-connect-openapi.tar.gz "$URL" || {
echo "Download failed, trying with different curl options..."
curl -L --fail --retry 3 --retry-delay 1 -o protoc-gen-connect-openapi.tar.gz "$URL"
}
echo "Extracting plugin..."
tar -xzf protoc-gen-connect-openapi.tar.gz
# Verify extraction
if [ ! -f "protoc-gen-connect-openapi" ]; then
echo "ERROR: Plugin binary not found after extraction"
ls -la
exit 1
fi
# Make sure the plugin is executable
chmod +x protoc-gen-connect-openapi
# Verify plugin works
echo "Plugin installed successfully"
ls -la protoc-gen-connect-openapi
./protoc-gen-connect-openapi --version || echo "Plugin version check failed, but installation completed"
# Clean up
rm -f protoc-gen-connect-openapi.tar.gz

45
docs/turbo.json Normal file
View File

@@ -0,0 +1,45 @@
{
"$schema": "https://turbo.build/schema.json",
"extends": ["//"],
"tasks": {
"generate": {
"dependsOn": ["^generate"],
"outputs": ["docs/api/**", "docs/self-hosting/manage/configure/*.yaml"],
"cache": true
},
"generate:grpc": {
"dependsOn": ["^generate"],
"outputs": ["docs/api/**"],
"cache": true
},
"generate:apidocs": {
"dependsOn": ["generate:grpc"],
"outputs": ["docs/api/**"],
"cache": true
},
"generate:configdocs": {
"outputs": ["docs/self-hosting/manage/configure/*.yaml"],
"cache": true
},
"build": {
"dependsOn": ["generate"],
"outputs": ["build/**"],
"cache": true
},
"dev": {
"dependsOn": ["generate"],
"cache": false,
"persistent": true
},
"start": {
"dependsOn": ["generate"],
"cache": false,
"persistent": true
},
"start:api": {
"dependsOn": ["generate"],
"cache": false,
"persistent": true
}
}
}

View File

@@ -1,64 +1,223 @@
{ {
"github": { "github": {
"enabled": true "enabled": true
}, },
"cleanUrls": true, "cleanUrls": true,
"rewrites": [ "rewrites": [
{ {
"source": "/docs/proxy/js/script.js", "source": "/docs/proxy/js/script.js",
"destination": "https://plausible.io/js/script.tagged-events.pageview-props.outbound-links.js" "destination": "https://plausible.io/js/script.tagged-events.pageview-props.outbound-links.js"
}, },
{ {
"source": "/docs/proxy/api/event", "source": "/docs/proxy/api/event",
"destination": "https://plausible.io/api/event" "destination": "https://plausible.io/api/event"
}, },
{ {
"source": "/docs/:match*", "source": "/docs/:match*",
"destination": "/:match*" "destination": "/:match*"
} }
], ],
"redirects": [ "redirects": [
{ "source": "/", "destination": "/docs" }, { "source": "/", "destination": "/docs" },
{ "source": "/docs/category/apis/:slug*", "destination": "/docs/apis/:slug*", "permanent": true }, {
{ "source": "/docs/apis/mgmt/:slug*", "destination": "/docs/apis/resources/mgmt/:slug*", "permanent": true }, "source": "/docs/category/apis/:slug*",
{ "source": "/docs/apis/auth/:slug*", "destination": "/docs/apis/resources/auth/:slug*", "permanent": true }, "destination": "/docs/apis/:slug*",
{ "source": "/docs/apis/system/:slug*", "destination": "/docs/apis/resources/system/:slug*", "permanent": true }, "permanent": true
{ "source": "/docs/apis/admin/:slug*", "destination": "/docs/apis/resources/admin/:slug*", "permanent": true }, },
{ "source": "/docs/apis/actionsv2/introduction", "destination": "/docs/apis/actions/v2/usage", "permanent": true }, {
{ "source": "/docs/apis/actionsv2/execution-local", "destination": "/docs/apis/actions/v2/testing-locally", "permanent": true }, "source": "/docs/apis/mgmt/:slug*",
{ "source": "/docs/guides/integrate/human-users", "destination": "/docs/guides/integrate/login", "permanent": true }, "destination": "/docs/apis/resources/mgmt/:slug*",
{ "source": "/docs/guides/solution-scenarios/device-authorization", "destination": "/docs/guides/integrate/login/oidc/device-authorization", "permanent": true }, "permanent": true
{ "source": "/docs/guides/integrate/oauth-recommended-flows", "destination": "/docs/guides/integrate/login/oidc/oauth-recommended-flows", "permanent": true }, },
{ "source": "/docs/guides/integrate/login-users", "destination": "/docs/guides/integrate/login/oidc/login-users", "permanent": true }, {
{ "source": "/docs/guides/integrate/logout", "destination": "/docs/guides/integrate/login/oidc/logout", "permanent": true }, "source": "/docs/apis/auth/:slug*",
{ "source": "/docs/guides/solution-scenarios/onboarding", "destination": "/docs/guides/integrate/onboarding", "permanent": true }, "destination": "/docs/apis/resources/auth/:slug*",
{ "source": "/docs/guides/solution-scenarios/onboarding/b2b", "destination": "/docs/guides/integrate/onboarding/b2b", "permanent": true }, "permanent": true
{ "source": "/docs/guides/solution-scenarios/onboarding/end-users", "destination": "/docs/guides/integrate/onboarding/end-users", "permanent": true }, },
{ "source": "/docs/concepts/structure/jwt_idp", "destination": "/docs/guides/integrate/identity-providers/jwt-idp", "permanent": true }, {
{ "source": "/docs/guides/solution-scenarios/onboarding/end-users", "destination": "/docs/guides/integrate/onboarding/end-users", "permanent": true }, "source": "/docs/apis/system/:slug*",
{ "source": "/docs/guides/integrate/serviceusers", "destination": "/docs/guides/integrate/service-users/authenticate-service-users", "permanent": true }, "destination": "/docs/apis/resources/system/:slug*",
{ "source": "/docs/guides/integrate/private-key-jwt", "destination": "/docs/guides/integrate/service-users/private-key-jwt", "permanent": true }, "permanent": true
{ "source": "/docs/guides/integrate/client-credentials", "destination": "/docs/guides/integrate/service-users/client-credentials", "permanent": true }, },
{ "source": "/docs/guides/integrate/pat", "destination": "/docs/guides/integrate/service-users/private-access-token", "permanent": true }, {
{ "source": "/docs/guides/integrate/access-zitadel-apis", "destination": "/docs/guides/integrate/zitadel-apis/access-zitadel-apis", "permanent": true }, "source": "/docs/apis/admin/:slug*",
{ "source": "/docs/guides/integrate/access-zitadel-system-api", "destination": "/docs/guides/integrate/zitadel-apis/access-zitadel-system-api", "permanent": true }, "destination": "/docs/apis/resources/admin/:slug*",
{ "source": "/docs/guides/integrate/event-api", "destination": "/docs/guides/integrate/zitadel-apis/event-api", "permanent": true }, "permanent": true
{ "source": "/docs/examples/call-zitadel-api/go", "destination": "/docs/guides/integrate/zitadel-apis/example-zitadel-api-with-go", "permanent": true }, },
{ "source": "/docs/examples/call-zitadel-api/dot-net", "destination": "/docs/guides/integrate/zitadel-apis/example-zitadel-api-with-dot-net", "permanent": true }, {
{ "source": "/docs/guides/manage/terraform/basics", "destination": "/docs/guides/manage/terraform-provider", "permanent": true }, "source": "/docs/apis/actionsv2/introduction",
{ "source": "/docs/guides/integrate/identity-providers", "destination": "/docs/guides/integrate/identity-providers/introduction", "permanent": true }, "destination": "/docs/apis/actions/v2/usage",
{ "source": "/docs/guides/integrate/login/login-users#centralized-authentication-endpoint", "destination": "/docs/guides/integrate/login/hosted-login#centralized-authentication-endpoint", "permanent": true }, "permanent": true
{ "source": "/docs/guides/integrate/login/login-users#security-and-compliance", "destination": "/docs/guides/integrate/login/hosted-login#security-and-compliance", "permanent": true }, },
{ "source": "/docs/guides/integrate/login/login-users#developer-friendly-integration", "destination": "/docs/guides/integrate/login/hosted-login#developer-friendly-integration", "permanent": true }, {
{ "source": "/docs/guides/integrate/login/login-users#key-features-of-the-hosted-login", "destination": "/docs/guides/integrate/login/hosted-login#key-features-of-the-hosted-login", "permanent": true }, "source": "/docs/apis/actionsv2/execution-local",
{ "source": "/docs/guides/integrate/login/login-users#flexible-usernames", "destination": "/docs/guides/integrate/login/hosted-login#flexible-usernames", "permanent": true }, "destination": "/docs/apis/actions/v2/testing-locally",
{ "source": "/docs/guides/integrate/login/login-users#support-for-multiple-authentication-methods", "destination": "/docs/guides/integrate/login/hosted-login#support-for-multiple-authentication-methods", "permanent": true }, "permanent": true
{ "source": "/docs/guides/integrate/login/login-users#enterprise-single-sign-on", "destination": "/docs/guides/integrate/login/hosted-login#enterprise-single-sign-on", "permanent": true }, },
{ "source": "/docs/guides/integrate/login/login-users#multi-tenancy-authentication", "destination": "/docs/guides/integrate/login/hosted-login#multi-tenancy-authentication", "permanent": true }, {
{ "source": "/docs/guides/integrate/login/login-users#customization-options", "destination": "/docs/guides/integrate/login/hosted-login#customization-options", "permanent": true }, "source": "/docs/guides/integrate/human-users",
{ "source": "/docs/guides/integrate/login/login-users#fast-account-switching", "destination": "/docs/guides/integrate/login/hosted-login#fast-account-switching", "permanent": true }, "destination": "/docs/guides/integrate/login",
{ "source": "/docs/guides/integrate/login/login-users#self-service-for-users", "destination": "/docs/guides/integrate/login/hosted-login#self-service-for-users", "permanent": true }, "permanent": true
{ "source": "/docs/guides/integrate/login/login-users#password-reset", "destination": "/docs/guides/integrate/login/hosted-login#password-reset", "permanent": true } },
] {
"source": "/docs/guides/solution-scenarios/device-authorization",
"destination": "/docs/guides/integrate/login/oidc/device-authorization",
"permanent": true
},
{
"source": "/docs/guides/integrate/oauth-recommended-flows",
"destination": "/docs/guides/integrate/login/oidc/oauth-recommended-flows",
"permanent": true
},
{
"source": "/docs/guides/integrate/login-users",
"destination": "/docs/guides/integrate/login/oidc/login-users",
"permanent": true
},
{
"source": "/docs/guides/integrate/logout",
"destination": "/docs/guides/integrate/login/oidc/logout",
"permanent": true
},
{
"source": "/docs/guides/solution-scenarios/onboarding",
"destination": "/docs/guides/integrate/onboarding",
"permanent": true
},
{
"source": "/docs/guides/solution-scenarios/onboarding/b2b",
"destination": "/docs/guides/integrate/onboarding/b2b",
"permanent": true
},
{
"source": "/docs/guides/solution-scenarios/onboarding/end-users",
"destination": "/docs/guides/integrate/onboarding/end-users",
"permanent": true
},
{
"source": "/docs/concepts/structure/jwt_idp",
"destination": "/docs/guides/integrate/identity-providers/jwt-idp",
"permanent": true
},
{
"source": "/docs/guides/solution-scenarios/onboarding/end-users",
"destination": "/docs/guides/integrate/onboarding/end-users",
"permanent": true
},
{
"source": "/docs/guides/integrate/serviceusers",
"destination": "/docs/guides/integrate/service-users/authenticate-service-users",
"permanent": true
},
{
"source": "/docs/guides/integrate/private-key-jwt",
"destination": "/docs/guides/integrate/service-users/private-key-jwt",
"permanent": true
},
{
"source": "/docs/guides/integrate/client-credentials",
"destination": "/docs/guides/integrate/service-users/client-credentials",
"permanent": true
},
{
"source": "/docs/guides/integrate/pat",
"destination": "/docs/guides/integrate/service-users/private-access-token",
"permanent": true
},
{
"source": "/docs/guides/integrate/access-zitadel-apis",
"destination": "/docs/guides/integrate/zitadel-apis/access-zitadel-apis",
"permanent": true
},
{
"source": "/docs/guides/integrate/access-zitadel-system-api",
"destination": "/docs/guides/integrate/zitadel-apis/access-zitadel-system-api",
"permanent": true
},
{
"source": "/docs/guides/integrate/event-api",
"destination": "/docs/guides/integrate/zitadel-apis/event-api",
"permanent": true
},
{
"source": "/docs/examples/call-zitadel-api/go",
"destination": "/docs/guides/integrate/zitadel-apis/example-zitadel-api-with-go",
"permanent": true
},
{
"source": "/docs/examples/call-zitadel-api/dot-net",
"destination": "/docs/guides/integrate/zitadel-apis/example-zitadel-api-with-dot-net",
"permanent": true
},
{
"source": "/docs/guides/manage/terraform/basics",
"destination": "/docs/guides/manage/terraform-provider",
"permanent": true
},
{
"source": "/docs/guides/integrate/identity-providers",
"destination": "/docs/guides/integrate/identity-providers/introduction",
"permanent": true
},
{
"source": "/docs/guides/integrate/login/login-users#centralized-authentication-endpoint",
"destination": "/docs/guides/integrate/login/hosted-login#centralized-authentication-endpoint",
"permanent": true
},
{
"source": "/docs/guides/integrate/login/login-users#security-and-compliance",
"destination": "/docs/guides/integrate/login/hosted-login#security-and-compliance",
"permanent": true
},
{
"source": "/docs/guides/integrate/login/login-users#developer-friendly-integration",
"destination": "/docs/guides/integrate/login/hosted-login#developer-friendly-integration",
"permanent": true
},
{
"source": "/docs/guides/integrate/login/login-users#key-features-of-the-hosted-login",
"destination": "/docs/guides/integrate/login/hosted-login#key-features-of-the-hosted-login",
"permanent": true
},
{
"source": "/docs/guides/integrate/login/login-users#flexible-usernames",
"destination": "/docs/guides/integrate/login/hosted-login#flexible-usernames",
"permanent": true
},
{
"source": "/docs/guides/integrate/login/login-users#support-for-multiple-authentication-methods",
"destination": "/docs/guides/integrate/login/hosted-login#support-for-multiple-authentication-methods",
"permanent": true
},
{
"source": "/docs/guides/integrate/login/login-users#enterprise-single-sign-on",
"destination": "/docs/guides/integrate/login/hosted-login#enterprise-single-sign-on",
"permanent": true
},
{
"source": "/docs/guides/integrate/login/login-users#multi-tenancy-authentication",
"destination": "/docs/guides/integrate/login/hosted-login#multi-tenancy-authentication",
"permanent": true
},
{
"source": "/docs/guides/integrate/login/login-users#customization-options",
"destination": "/docs/guides/integrate/login/hosted-login#customization-options",
"permanent": true
},
{
"source": "/docs/guides/integrate/login/login-users#fast-account-switching",
"destination": "/docs/guides/integrate/login/hosted-login#fast-account-switching",
"permanent": true
},
{
"source": "/docs/guides/integrate/login/login-users#self-service-for-users",
"destination": "/docs/guides/integrate/login/hosted-login#self-service-for-users",
"permanent": true
},
{
"source": "/docs/guides/integrate/login/login-users#password-reset",
"destination": "/docs/guides/integrate/login/hosted-login#password-reset",
"permanent": true
}
]
} }

File diff suppressed because it is too large Load Diff

View File

@@ -2,16 +2,16 @@
"name": "zitadel-e2e", "name": "zitadel-e2e",
"version": "0.0.0", "version": "0.0.0",
"scripts": { "scripts": {
"open": "npx cypress open", "open": "pnpm exec cypress open",
"e2e": "npx cypress run", "e2e": "pnpm exec cypress run",
"open:golang": "npm run open --", "open:golang": "pnpm run open --",
"e2e:golang": "npm run e2e --", "e2e:golang": "pnpm run e2e --",
"open:golangangular": "CYPRESS_BASE_URL=http://localhost:4200 CYPRESS_BACKEND_URL=http://localhost:8080 npm run open --", "open:golangangular": "CYPRESS_BASE_URL=http://localhost:4200 CYPRESS_BACKEND_URL=http://localhost:8080 pnpm run open --",
"e2e:golangangular": "CYPRESS_BASE_URL=http://localhost:4200 CYPRESS_BACKEND_URL=http://localhost:8080 npm run e2e --", "e2e:golangangular": "CYPRESS_BASE_URL=http://localhost:4200 CYPRESS_BACKEND_URL=http://localhost:8080 pnpm run e2e --",
"open:angulargolang": "npm run open:golangangular --", "open:angulargolang": "pnpm run open:golangangular --",
"e2e:angulargolang": "npm run e2e:golangangular --", "e2e:angulargolang": "pnpm run e2e:golangangular --",
"open:angular": "CYPRESS_BASE_URL=http://localhost:4200 CYPRESS_BACKEND_URL=http://localhost:8080 CYPRESS_WEBHOOK_HANDLER_HOST=host.docker.internal npm run open --", "open:angular": "CYPRESS_BASE_URL=http://localhost:4200 CYPRESS_BACKEND_URL=http://localhost:8080 CYPRESS_WEBHOOK_HANDLER_HOST=host.docker.internal pnpm run open --",
"e2e:angular": "CYPRESS_BASE_URL=http://localhost:4200 CYPRESS_BACKEND_URL=http://localhost:8080 CYPRESS_WEBHOOK_HANDLER_HOST=host.docker.internal npm run e2e --", "e2e:angular": "CYPRESS_BASE_URL=http://localhost:4200 CYPRESS_BACKEND_URL=http://localhost:8080 CYPRESS_WEBHOOK_HANDLER_HOST=host.docker.internal pnpm run e2e --",
"lint": "prettier --check cypress", "lint": "prettier --check cypress",
"lint:fix": "prettier --write cypress" "lint:fix": "prettier --write cypress"
}, },

25
e2e/turbo.json Normal file
View File

@@ -0,0 +1,25 @@
{
"$schema": "https://turbo.build/schema.json",
"extends": ["//"],
"tasks": {
"e2e": {
"cache": false,
"persistent": false
},
"e2e:golang": {
"cache": false,
"persistent": false
},
"e2e:golangangular": {
"cache": false,
"persistent": false
},
"e2e:angular": {
"cache": false,
"persistent": false
},
"lint": {
"outputs": []
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
module.exports = { module.exports = {
root: true, root: true,
// This tells ESLint to load the config from the package `@zitadel/eslint-config` // Use basic ESLint config since the login app has its own detailed config
extends: ["@zitadel/eslint-config"], extends: ["eslint:recommended"],
settings: { settings: {
next: { next: {
rootDir: ["apps/*/"], rootDir: ["apps/*/"],

View File

@@ -36,8 +36,6 @@ We think the easiest path of getting up and running, is the following:
- `login`: The login UI used by ZITADEL Cloud, powered by Next.js - `login`: The login UI used by ZITADEL Cloud, powered by Next.js
- `@zitadel/client`: shared client utilities for node and browser environments - `@zitadel/client`: shared client utilities for node and browser environments
- `@zitadel/proto`: Protocol Buffers (proto) definitions used by ZITADEL projects - `@zitadel/proto`: Protocol Buffers (proto) definitions used by ZITADEL projects
- `@zitadel/tsconfig`: shared `tsconfig.json`s used throughout the monorepo
- `@zitadel/eslint-config`: ESLint preset
Each package and app is 100% [TypeScript](https://www.typescriptlang.org/). Each package and app is 100% [TypeScript](https://www.typescriptlang.org/).

19
login/apps/login/.eslintrc.cjs Executable file → Normal file
View File

@@ -1,12 +1,21 @@
module.exports = { module.exports = {
extends: ["next/core-web-vitals"], parser: "@typescript-eslint/parser",
ignorePatterns: ["external/**/*.ts"], extends: ["next", "prettier"],
plugins: ["@typescript-eslint"],
rules: { rules: {
"@next/next/no-html-link-for-pages": "off", "@next/next/no-html-link-for-pages": "off",
"@next/next/no-img-element": "off",
"react/no-unescaped-entities": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
"no-undef": "off",
}, },
settings: { parserOptions: {
react: { ecmaVersion: "latest",
version: "detect", sourceType: "module",
ecmaFeatures: {
jsx: true,
}, },
project: "./tsconfig.json",
}, },
}; };

View File

@@ -1,3 +1,10 @@
custom-config.js custom-config.js
.env*.local .env*.local
standalone standalone
# Generated standalone files (temporary)
*.generated.*
package.monorepo.backup.json
# TypeScript build info
tsconfig.tsbuildinfo

View File

@@ -0,0 +1,54 @@
# Dockerfile for standalone ZITADEL Login UI
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Prepare standalone and install dependencies
COPY prepare-standalone.sh package*.json ./
COPY *.standalone.* ./
RUN ./prepare-standalone.sh
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Prepare standalone configs
RUN ./prepare-standalone.sh --no-install
# Build application
ENV NEXT_TELEMETRY_DISABLED 1
RUN npm run build:standalone
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
CMD ["node", "server.js"]

View File

@@ -3,10 +3,10 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "pnpm next dev --turbopack", "dev": "next dev",
"test:unit": "pnpm vitest", "dev:turbo": "next dev --turbopack",
"test:unit": "pnpm vitest --run",
"test:unit:standalone": "pnpm test:unit", "test:unit:standalone": "pnpm test:unit",
"test:unit:watch": "pnpm test:unit --watch",
"lint": "pnpm exec next lint && pnpm exec prettier --check .", "lint": "pnpm exec next lint && pnpm exec prettier --check .",
"lint:fix": "pnpm exec prettier --write .", "lint:fix": "pnpm exec prettier --write .",
"lint-staged": "lint-staged", "lint-staged": "lint-staged",
@@ -14,7 +14,7 @@
"build:login:standalone": "NEXT_PUBLIC_BASE_PATH=/ui/v2/login NEXT_OUTPUT_MODE=standalone pnpm build", "build:login:standalone": "NEXT_PUBLIC_BASE_PATH=/ui/v2/login NEXT_OUTPUT_MODE=standalone pnpm build",
"start": "pnpm build && pnpm exec next start", "start": "pnpm build && pnpm exec next start",
"start:built": "pnpm exec next start", "start:built": "pnpm exec next start",
"clean": "pnpm mock:stop && rm -rf .turbo && rm -rf node_modules && rm -rf .next" "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next"
}, },
"git": { "git": {
"pre-commit": "lint-staged" "pre-commit": "lint-staged"
@@ -27,8 +27,8 @@
"@heroicons/react": "2.1.3", "@heroicons/react": "2.1.3",
"@tailwindcss/forms": "0.5.7", "@tailwindcss/forms": "0.5.7",
"@vercel/analytics": "^1.2.2", "@vercel/analytics": "^1.2.2",
"@zitadel/client": "workspace:*", "@zitadel/client": "latest",
"@zitadel/proto": "workspace:*", "@zitadel/proto": "latest",
"clsx": "1.2.1", "clsx": "1.2.1",
"copy-to-clipboard": "^3.3.3", "copy-to-clipboard": "^3.3.3",
"deepmerge": "^4.3.1", "deepmerge": "^4.3.1",
@@ -46,6 +46,7 @@
"uuid": "^11.1.0" "uuid": "^11.1.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.23.0",
"@bufbuild/buf": "^1.53.0", "@bufbuild/buf": "^1.53.0",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
@@ -55,21 +56,25 @@
"@types/react-dom": "19.1.2", "@types/react-dom": "19.1.2",
"@types/tinycolor2": "1.4.3", "@types/tinycolor2": "1.4.3",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"@vercel/git-hooks": "1.0.0", "@vercel/git-hooks": "1.0.0",
"@zitadel/eslint-config": "workspace:*",
"@zitadel/prettier-config": "workspace:*",
"@zitadel/tailwind-config": "workspace:*",
"@zitadel/tsconfig": "workspace:*",
"autoprefixer": "10.4.21", "autoprefixer": "10.4.21",
"eslint": "^8.57.0",
"eslint-config-next": "15.4.0-canary.86",
"eslint-config-prettier": "^9.1.0",
"grpc-tools": "1.13.0", "grpc-tools": "1.13.0",
"jsdom": "^26.1.0", "jsdom": "^26.1.0",
"lint-staged": "15.5.1", "lint-staged": "15.5.1",
"make-dir-cli": "4.0.0", "make-dir-cli": "4.0.0",
"postcss": "8.5.3", "postcss": "8.5.3",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.0",
"prettier-plugin-tailwindcss": "0.6.11", "prettier-plugin-tailwindcss": "0.6.11",
"sass": "^1.87.0", "sass": "^1.87.0",
"tailwindcss": "3.4.14", "tailwindcss": "3.4.14",
"ts-proto": "^2.7.0", "ts-proto": "^2.7.0",
"typescript": "^5.8.3" "typescript": "^5.8.3",
"vitest": "^2.0.0"
} }
} }

View File

@@ -1 +1,11 @@
export { default } from "@zitadel/prettier-config"; export default {
printWidth: 80,
tabWidth: 2,
useTabs: false,
semi: true,
singleQuote: false,
trailingComma: "all",
bracketSpacing: true,
arrowParens: "always",
plugins: ["prettier-plugin-organize-imports", "prettier-plugin-tailwindcss"],
};

View File

@@ -10,7 +10,7 @@ import {
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { UserPlusIcon } from "@heroicons/react/24/outline"; import { UserPlusIcon } from "@heroicons/react/24/outline";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { getLocale } from "next-intl/server"; // import { getLocale } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
import Link from "next/link"; import Link from "next/link";
@@ -33,7 +33,6 @@ export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>; searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) { }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale();
const requestId = searchParams?.requestId; const requestId = searchParams?.requestId;
const organization = searchParams?.organization; const organization = searchParams?.organization;
@@ -78,11 +77,11 @@ export default async function Page(props: {
<Translated i18nKey="description" namespace="accounts" /> <Translated i18nKey="description" namespace="accounts" />
</p> </p>
<div className="flex flex-col w-full space-y-2"> <div className="flex w-full flex-col space-y-2">
<SessionsList sessions={sessions} requestId={requestId} /> <SessionsList sessions={sessions} requestId={requestId} />
<Link href={`/loginname?` + params}> <Link href={`/loginname?` + params}>
<div className="flex flex-row items-center py-3 px-4 hover:bg-black/10 dark:hover:bg-white/10 rounded-md transition-all"> <div className="flex flex-row items-center rounded-md px-4 py-3 transition-all hover:bg-black/10 dark:hover:bg-white/10">
<div className="w-8 h-8 mr-4 flex flex-row justify-center items-center rounded-full bg-black/5 dark:bg-white/5"> <div className="mr-4 flex h-8 w-8 flex-row items-center justify-center rounded-full bg-black/5 dark:bg-white/5">
<UserPlusIcon className="h-5 w-5" /> <UserPlusIcon className="h-5 w-5" />
</div> </div>
<span className="text-sm"> <span className="text-sm">

View File

@@ -18,7 +18,7 @@ import {
listAuthenticationMethodTypes, listAuthenticationMethodTypes,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { getLocale } from "next-intl/server"; // import { getLocale } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
@@ -26,7 +26,6 @@ export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>; searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) { }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale();
const { loginName, requestId, organization, sessionId } = searchParams; const { loginName, requestId, organization, sessionId } = searchParams;
@@ -193,7 +192,7 @@ export default async function Page(props: {
{loginSettings?.allowExternalIdp && !!identityProviders.length && ( {loginSettings?.allowExternalIdp && !!identityProviders.length && (
<> <>
<div className="py-3 flex flex-col"> <div className="flex flex-col py-3">
<p className="ztdl-p text-center"> <p className="ztdl-p text-center">
<Translated i18nKey="linkWithIDP" namespace="authenticator" /> <Translated i18nKey="linkWithIDP" namespace="authenticator" />
</p> </p>

View File

@@ -27,13 +27,13 @@ export default async function RootLayout({
<Suspense <Suspense
fallback={ fallback={
<div <div
className={`relative min-h-screen bg-background-light-600 dark:bg-background-dark-600 flex flex-col justify-center`} className={`relative flex min-h-screen flex-col justify-center bg-background-light-600 dark:bg-background-dark-600`}
> >
<div className="relative mx-auto max-w-[440px] py-8 w-full"> <div className="relative mx-auto w-full max-w-[440px] py-8">
<Skeleton> <Skeleton>
<div className="h-40"></div> <div className="h-40"></div>
</Skeleton> </Skeleton>
<div className="flex flex-row justify-end py-4 items-center space-x-4"> <div className="flex flex-row items-center justify-end space-x-4 py-4">
<Theme /> <Theme />
</div> </div>
</div> </div>
@@ -42,11 +42,11 @@ export default async function RootLayout({
> >
<LanguageProvider> <LanguageProvider>
<div <div
className={`relative min-h-screen bg-background-light-600 dark:bg-background-dark-600 flex flex-col justify-center`} className={`relative flex min-h-screen flex-col justify-center bg-background-light-600 dark:bg-background-dark-600`}
> >
<div className="relative mx-auto max-w-[440px] py-8 w-full "> <div className="relative mx-auto w-full max-w-[440px] py-8">
{children} {children}
<div className="flex flex-row justify-end py-4 items-center space-x-4"> <div className="flex flex-row items-center justify-end space-x-4 py-4">
<LanguageSwitcher /> <LanguageSwitcher />
<Theme /> <Theme />
</div> </div>

View File

@@ -79,7 +79,7 @@ export default async function Page(props: {
></UsernameForm> ></UsernameForm>
{identityProviders && loginSettings?.allowExternalIdp && ( {identityProviders && loginSettings?.allowExternalIdp && (
<div className="w-full pt-6 pb-4"> <div className="w-full pb-4 pt-6">
<SignInWithIdp <SignInWithIdp
identityProviders={identityProviders} identityProviders={identityProviders}
requestId={requestId} requestId={requestId}

View File

@@ -72,7 +72,7 @@ export default async function Page(props: {
<Translated i18nKey="description" namespace="logout" /> <Translated i18nKey="description" namespace="logout" />
</p> </p>
<div className="flex flex-col w-full space-y-2"> <div className="flex w-full flex-col space-y-2">
<SessionsClearList <SessionsClearList
sessions={sessions} sessions={sessions}
logoutHint={logoutHint} logoutHint={logoutHint}

View File

@@ -11,7 +11,6 @@ import {
getLoginSettings, getLoginSettings,
getSession, getSession,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { getLocale } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
export default async function Page(props: { export default async function Page(props: {
@@ -20,7 +19,6 @@ export default async function Page(props: {
}) { }) {
const params = await props.params; const params = await props.params;
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale();
const _headers = await headers(); const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers); const { serviceUrl } = getServiceUrlFromHeaders(_headers);

View File

@@ -13,7 +13,7 @@ export default async function Page(props: {
}) { }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const { loginName, prompt, organization, requestId, userId } = searchParams; const { loginName, prompt, organization, requestId } = searchParams;
const _headers = await headers(); const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers); const { serviceUrl } = getServiceUrlFromHeaders(_headers);
@@ -54,7 +54,7 @@ export default async function Page(props: {
<span> <span>
<Translated i18nKey="set.info.description" namespace="passkey" /> <Translated i18nKey="set.info.description" namespace="passkey" />
<a <a
className="text-primary-light-500 dark:text-primary-dark-500 hover:text-primary-light-300 hover:dark:text-primary-dark-300" className="text-primary-light-500 hover:text-primary-light-300 dark:text-primary-dark-500 hover:dark:text-primary-dark-300"
target="_blank" target="_blank"
href="https://zitadel.com/docs/guides/manage/user/reg-create-user#with-passwordless" href="https://zitadel.com/docs/guides/manage/user/reg-create-user#with-passwordless"
> >

View File

@@ -13,14 +13,12 @@ import {
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { getLocale } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
export default async function Page(props: { export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>; searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) { }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale();
const { userId, loginName, organization, requestId, code, initial } = const { userId, loginName, organization, requestId, code, initial } =
searchParams; searchParams;

View File

@@ -14,14 +14,12 @@ import {
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { getLocale } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
export default async function Page(props: { export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>; searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) { }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale();
let { firstname, lastname, email, organization, requestId } = searchParams; let { firstname, lastname, email, organization, requestId } = searchParams;
@@ -117,7 +115,7 @@ export default async function Page(props: {
{loginSettings?.allowExternalIdp && !!identityProviders.length && ( {loginSettings?.allowExternalIdp && !!identityProviders.length && (
<> <>
<div className="py-3 flex flex-col items-center"> <div className="flex flex-col items-center py-3">
<p className="ztdl-p text-center"> <p className="ztdl-p text-center">
<Translated i18nKey="orUseIDP" namespace="register" /> <Translated i18nKey="orUseIDP" namespace="register" />
</p> </p>

View File

@@ -7,14 +7,12 @@ import { getSessionCookieById } from "@/lib/cookies";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings, getSession } from "@/lib/zitadel"; import { getBrandingSettings, getSession } from "@/lib/zitadel";
import { getLocale } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
export default async function Page(props: { export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>; searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) { }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale();
const { loginName, requestId, sessionId, organization } = searchParams; const { loginName, requestId, sessionId, organization } = searchParams;

View File

@@ -6,14 +6,12 @@ import { UserAvatar } from "@/components/user-avatar";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings } from "@/lib/zitadel"; import { getBrandingSettings } from "@/lib/zitadel";
import { getLocale } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
export default async function Page(props: { export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>; searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) { }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale();
const { loginName, organization, requestId, checkAfter } = searchParams; const { loginName, organization, requestId, checkAfter } = searchParams;

View File

@@ -8,12 +8,10 @@ import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings, getUserByID } from "@/lib/zitadel"; import { getBrandingSettings, getUserByID } from "@/lib/zitadel";
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { getLocale } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
export default async function Page(props: { searchParams: Promise<any> }) { export default async function Page(props: { searchParams: Promise<any> }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale();
const { userId, loginName, code, organization, requestId, invite, send } = const { userId, loginName, code, organization, requestId, invite, send } =
searchParams; searchParams;
@@ -136,7 +134,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
)} )}
{id && send && ( {id && send && (
<div className="py-4 w-full"> <div className="w-full py-4">
<Alert type={AlertType.INFO}> <Alert type={AlertType.INFO}>
<Translated i18nKey="verify.codeSent" namespace="verify" /> <Translated i18nKey="verify.codeSent" namespace="verify" />
</Alert> </Alert>

View File

@@ -3,11 +3,7 @@ import { Translated } from "@/components/translated";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { import { getBrandingSettings, getUserByID } from "@/lib/zitadel";
getBrandingSettings,
getLoginSettings,
getUserByID,
} from "@/lib/zitadel";
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { headers } from "next/headers"; import { headers } from "next/headers";
@@ -31,14 +27,6 @@ export default async function Page(props: { searchParams: Promise<any> }) {
console.warn("Error loading session:", error); console.warn("Error loading session:", error);
}); });
let loginSettings;
if (!requestId) {
loginSettings = await getLoginSettings({
serviceUrl,
organization,
});
}
const id = userId ?? sessionFactors?.factors?.user?.id; const id = userId ?? sessionFactors?.factors?.user?.id;
if (!id) { if (!id) {

View File

@@ -11,7 +11,7 @@ export function AddressBar({ domain }: Props) {
const pathname = usePathname(); const pathname = usePathname();
return ( return (
<div className="flex items-center space-x-2 p-3.5 lg:px-5 lg:py-3 overflow-hidden"> <div className="flex items-center space-x-2 overflow-hidden p-3.5 lg:px-5 lg:py-3">
<div className="text-gray-600"> <div className="text-gray-600">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -27,7 +27,7 @@ export function AddressBar({ domain }: Props) {
</svg> </svg>
</div> </div>
<div className="flex space-x-1 text-sm font-medium"> <div className="flex space-x-1 text-sm font-medium">
<div className="max-w-[150px] px-2 overflow-hidden text-gray-500 text-ellipsis"> <div className="max-w-[150px] overflow-hidden text-ellipsis px-2 text-gray-500">
<span className="whitespace-nowrap">{domain}</span> <span className="whitespace-nowrap">{domain}</span>
</div> </div>
{pathname ? ( {pathname ? (

View File

@@ -26,7 +26,7 @@ export function Alert({ children, type = AlertType.ALERT }: Props) {
return ( return (
<div <div
className={clsx( className={clsx(
"flex flex-row items-center justify-center border rounded-md py-2 pr-2 scroll-px-40", "flex scroll-px-40 flex-row items-center justify-center rounded-md border py-2 pr-2",
{ {
[yellow]: type === AlertType.ALERT, [yellow]: type === AlertType.ALERT,
[neutral]: type === AlertType.INFO, [neutral]: type === AlertType.INFO,
@@ -34,12 +34,12 @@ export function Alert({ children, type = AlertType.ALERT }: Props) {
)} )}
> >
{type === AlertType.ALERT && ( {type === AlertType.ALERT && (
<ExclamationTriangleIcon className="flex-shrink-0 h-5 w-5 mr-2 ml-2" /> <ExclamationTriangleIcon className="ml-2 mr-2 h-5 w-5 flex-shrink-0" />
)} )}
{type === AlertType.INFO && ( {type === AlertType.INFO && (
<InformationCircleIcon className="flex-shrink-0 h-5 w-5 mr-2 ml-2" /> <InformationCircleIcon className="ml-2 mr-2 h-5 w-5 flex-shrink-0" />
)} )}
<span className="text-sm w-full ">{children}</span> <span className="w-full text-sm">{children}</span>
</div> </div>
); );
} }

View File

@@ -27,7 +27,7 @@ export function AppAvatar({ appName, imageUrl, shadow }: AvatarProps) {
return ( return (
<div <div
className={`w-[100px] h-[100px] flex justify-center items-center cursor-default pointer-events-none group-focus:outline-none group-focus:ring-2 transition-colors duration-200 dark:group-focus:ring-offset-blue bg-primary-light-500 text-primary-light-contrast-500 hover:bg-primary-light-400 hover:dark:bg-primary-dark-500 group-focus:ring-primary-light-200 dark:group-focus:ring-primary-dark-400 dark:bg-primary-dark-300 dark:text-primary-dark-contrast-300 dark:text-blue rounded-full ${ className={`dark:group-focus:ring-offset-blue dark:text-blue pointer-events-none flex h-[100px] w-[100px] cursor-default items-center justify-center rounded-full bg-primary-light-500 text-primary-light-contrast-500 transition-colors duration-200 hover:bg-primary-light-400 group-focus:outline-none group-focus:ring-2 group-focus:ring-primary-light-200 dark:bg-primary-dark-300 dark:text-primary-dark-contrast-300 hover:dark:bg-primary-dark-500 dark:group-focus:ring-primary-dark-400 ${
shadow ? "shadow" : "" shadow ? "shadow" : ""
}`} }`}
style={resolvedTheme === "light" ? avatarStyleLight : avatarStyleDark} style={resolvedTheme === "light" ? avatarStyleLight : avatarStyleDark}
@@ -37,11 +37,11 @@ export function AppAvatar({ appName, imageUrl, shadow }: AvatarProps) {
height={48} height={48}
width={48} width={48}
alt="avatar" alt="avatar"
className="w-full h-full border border-divider-light dark:border-divider-dark rounded-full" className="h-full w-full rounded-full border border-divider-light dark:border-divider-dark"
src={imageUrl} src={imageUrl}
/> />
) : ( ) : (
<span className={`uppercase text-3xl`}>{credentials}</span> <span className={`text-3xl uppercase`}>{credentials}</span>
)} )}
</div> </div>
); );

View File

@@ -35,12 +35,12 @@ export const TOTP = (alreadyAdded: boolean, link: string) => {
<LinkWrapper key={link} alreadyAdded={alreadyAdded} link={link}> <LinkWrapper key={link} alreadyAdded={alreadyAdded} link={link}>
<div <div
className={clsx( className={clsx(
"font-medium flex items-center", "flex items-center font-medium",
alreadyAdded ? "opacity-50" : "", alreadyAdded ? "opacity-50" : "",
)} )}
> >
<svg <svg
className="h-8 w-8 transform -translate-x-[2px] mr-4 fill-current text-black dark:text-white" className="mr-4 h-8 w-8 -translate-x-[2px] transform fill-current text-black dark:text-white"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
@@ -63,7 +63,7 @@ export const U2F = (alreadyAdded: boolean, link: string) => {
<LinkWrapper key={link} alreadyAdded={alreadyAdded} link={link}> <LinkWrapper key={link} alreadyAdded={alreadyAdded} link={link}>
<div <div
className={clsx( className={clsx(
"font-medium flex items-center", "flex items-center font-medium",
alreadyAdded ? "" : "", alreadyAdded ? "" : "",
)} )}
> >
@@ -73,7 +73,7 @@ export const U2F = (alreadyAdded: boolean, link: string) => {
viewBox="0 0 24 24" viewBox="0 0 24 24"
strokeWidth="1.5" strokeWidth="1.5"
stroke="currentColor" stroke="currentColor"
className="w-8 h-8 mr-4" className="mr-4 h-8 w-8"
> >
<path <path
strokeLinecap="round" strokeLinecap="round"
@@ -97,12 +97,12 @@ export const EMAIL = (alreadyAdded: boolean, link: string) => {
<LinkWrapper key={link} alreadyAdded={alreadyAdded} link={link}> <LinkWrapper key={link} alreadyAdded={alreadyAdded} link={link}>
<div <div
className={clsx( className={clsx(
"font-medium flex items-center", "flex items-center font-medium",
alreadyAdded ? "" : "", alreadyAdded ? "" : "",
)} )}
> >
<svg <svg
className="w-8 h-8 mr-4" className="mr-4 h-8 w-8"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -132,12 +132,12 @@ export const SMS = (alreadyAdded: boolean, link: string) => {
<LinkWrapper key={link} alreadyAdded={alreadyAdded} link={link}> <LinkWrapper key={link} alreadyAdded={alreadyAdded} link={link}>
<div <div
className={clsx( className={clsx(
"font-medium flex items-center", "flex items-center font-medium",
alreadyAdded ? "" : "", alreadyAdded ? "" : "",
)} )}
> >
<svg <svg
className="w-8 h-8 mr-4" className="mr-4 h-8 w-8"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -166,7 +166,7 @@ export const PASSKEYS = (alreadyAdded: boolean, link: string) => {
<LinkWrapper key={link} alreadyAdded={alreadyAdded} link={link}> <LinkWrapper key={link} alreadyAdded={alreadyAdded} link={link}>
<div <div
className={clsx( className={clsx(
"font-medium flex items-center", "flex items-center font-medium",
alreadyAdded ? "" : "", alreadyAdded ? "" : "",
)} )}
> >
@@ -176,7 +176,7 @@ export const PASSKEYS = (alreadyAdded: boolean, link: string) => {
viewBox="0 0 24 24" viewBox="0 0 24 24"
strokeWidth="1.5" strokeWidth="1.5"
stroke="currentColor" stroke="currentColor"
className="w-8 h-8 mr-4" className="mr-4 h-8 w-8"
> >
<path <path
strokeLinecap="round" strokeLinecap="round"
@@ -200,12 +200,12 @@ export const PASSWORD = (alreadyAdded: boolean, link: string) => {
<LinkWrapper key={link} alreadyAdded={alreadyAdded} link={link}> <LinkWrapper key={link} alreadyAdded={alreadyAdded} link={link}>
<div <div
className={clsx( className={clsx(
"font-medium flex items-center", "flex items-center font-medium",
alreadyAdded ? "" : "", alreadyAdded ? "" : "",
)} )}
> >
<svg <svg
className="w-8 h-7 mr-4 fill-current" className="mr-4 h-7 w-8 fill-current"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
@@ -225,9 +225,9 @@ export const PASSWORD = (alreadyAdded: boolean, link: string) => {
function Setup() { function Setup() {
return ( return (
<div className="transform absolute right-2 top-0"> <div className="absolute right-2 top-0 transform">
<StateBadge evenPadding={true} state={BadgeState.Success}> <StateBadge evenPadding={true} state={BadgeState.Success}>
<CheckIcon className="w-4 h-4" /> <CheckIcon className="h-4 w-4" />
</StateBadge> </StateBadge>
</div> </div>
); );

View File

@@ -34,20 +34,18 @@ export function AuthenticationMethodRadio({
className={({ active, checked }) => className={({ active, checked }) =>
`${ `${
active active
? "ring-2 ring-opacity-60 ring-primary-light-500 dark:ring-white/20" ? "ring-2 ring-primary-light-500 ring-opacity-60 dark:ring-white/20"
: "" : ""
} } ${
${ checked
checked ? "bg-background-light-400 ring-2 ring-primary-light-500 dark:bg-background-dark-400 dark:ring-primary-dark-500"
? "bg-background-light-400 dark:bg-background-dark-400 ring-2 ring-primary-light-500 dark:ring-primary-dark-500" : "bg-background-light-400 dark:bg-background-dark-400"
: "bg-background-light-400 dark:bg-background-dark-400" } boder-divider-light relative flex h-full flex-1 cursor-pointer rounded-lg border px-5 py-4 hover:shadow-lg focus:outline-none dark:border-divider-dark dark:hover:bg-white/10`
}
h-full flex-1 relative border boder-divider-light dark:border-divider-dark flex cursor-pointer rounded-lg px-5 py-4 focus:outline-none hover:shadow-lg dark:hover:bg-white/10`
} }
> >
{({ active, checked }) => ( {({ active, checked }) => (
<> <>
<div className="flex flex-col items-center w-full text-sm"> <div className="flex w-full flex-col items-center text-sm">
{method === "passkey" && ( {method === "passkey" && (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -55,7 +53,7 @@ export function AuthenticationMethodRadio({
viewBox="0 0 24 24" viewBox="0 0 24 24"
strokeWidth="1.5" strokeWidth="1.5"
stroke="currentColor" stroke="currentColor"
className="w-8 h-8 mb-3" className="mb-3 h-8 w-8"
> >
<path <path
strokeLinecap="round" strokeLinecap="round"
@@ -66,7 +64,7 @@ export function AuthenticationMethodRadio({
)} )}
{method === "password" && ( {method === "password" && (
<svg <svg
className="w-8 h-8 mb-3 fill-current" className="mb-3 h-8 w-8 fill-current"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
@@ -76,7 +74,7 @@ export function AuthenticationMethodRadio({
)} )}
<RadioGroup.Label <RadioGroup.Label
as="p" as="p"
className={`font-medium ${checked ? "" : ""}`} className={`font-medium ${checked ? "" : ""}`}
> >
{method === AuthenticationMethod.Passkey && ( {method === AuthenticationMethod.Passkey && (
<Translated <Translated

View File

@@ -64,16 +64,16 @@ export function Avatar({
return ( return (
<div <div
className={`w-full h-full flex-shrink-0 flex justify-center items-center cursor-default pointer-events-none group-focus:outline-none group-focus:ring-2 transition-colors duration-200 dark:group-focus:ring-offset-blue bg-primary-light-500 text-primary-light-contrast-500 hover:bg-primary-light-400 hover:dark:bg-primary-dark-500 group-focus:ring-primary-light-200 dark:group-focus:ring-primary-dark-400 dark:bg-primary-dark-300 dark:text-primary-dark-contrast-300 dark:text-blue rounded-full ${ className={`dark:group-focus:ring-offset-blue dark:text-blue pointer-events-none flex h-full w-full flex-shrink-0 cursor-default items-center justify-center rounded-full bg-primary-light-500 text-primary-light-contrast-500 transition-colors duration-200 hover:bg-primary-light-400 group-focus:outline-none group-focus:ring-2 group-focus:ring-primary-light-200 dark:bg-primary-dark-300 dark:text-primary-dark-contrast-300 hover:dark:bg-primary-dark-500 dark:group-focus:ring-primary-dark-400 ${
shadow ? "shadow" : "" shadow ? "shadow" : ""
} ${ } ${
size === "large" size === "large"
? "h-20 w-20 font-normal" ? "h-20 w-20 font-normal"
: size === "base" : size === "base"
? "w-[38px] h-[38px] font-bold" ? "h-[38px] w-[38px] font-bold"
: size === "small" : size === "small"
? "!w-[32px] !h-[32px] font-bold text-[13px]" ? "!h-[32px] !w-[32px] text-[13px] font-bold"
: "w-12 h-12" : "h-12 w-12"
}`} }`}
style={resolvedTheme === "light" ? avatarStyleLight : avatarStyleDark} style={resolvedTheme === "light" ? avatarStyleLight : avatarStyleDark}
> >
@@ -82,7 +82,7 @@ export function Avatar({
height={48} height={48}
width={48} width={48}
alt="avatar" alt="avatar"
className="w-full h-full border border-divider-light dark:border-divider-dark rounded-full" className="h-full w-full rounded-full border border-divider-light dark:border-divider-dark"
src={imageUrl} src={imageUrl}
/> />
) : ( ) : (

View File

@@ -33,8 +33,7 @@ export const getButtonClasses = (
color: ButtonColors, color: ButtonColors,
) => ) =>
clsx({ clsx({
"box-border font-normal leading-36px text-14px inline-flex items-center rounded-md focus:outline-none transition-colors transition-shadow duration-300": "box-border font-normal leading-36px text-14px inline-flex items-center rounded-md focus:outline-none transition-colors transition-shadow duration-300": true,
true,
"shadow hover:shadow-xl active:shadow-xl disabled:border-none disabled:bg-gray-300 disabled:text-gray-600 disabled:shadow-none disabled:cursor-not-allowed disabled:dark:bg-gray-800 disabled:dark:text-gray-900": "shadow hover:shadow-xl active:shadow-xl disabled:border-none disabled:bg-gray-300 disabled:text-gray-600 disabled:shadow-none disabled:cursor-not-allowed disabled:dark:bg-gray-800 disabled:dark:text-gray-900":
variant === ButtonVariants.Primary, variant === ButtonVariants.Primary,
"bg-primary-light-500 dark:bg-primary-dark-500 hover:bg-primary-light-400 hover:dark:bg-primary-dark-400 text-primary-light-contrast-500 dark:text-primary-dark-contrast-500": "bg-primary-light-500 dark:bg-primary-dark-500 hover:bg-primary-light-400 hover:dark:bg-primary-dark-400 text-primary-light-contrast-500 dark:text-primary-dark-contrast-500":

View File

@@ -149,7 +149,7 @@ export function ChangePasswordForm({
return ( return (
<form className="w-full"> <form className="w-full">
<div className="pt-4 grid grid-cols-1 gap-4 mb-4"> <div className="mb-4 grid grid-cols-1 gap-4 pt-4">
<div className=""> <div className="">
<TextInput <TextInput
type="password" type="password"
@@ -202,7 +202,7 @@ export function ChangePasswordForm({
onClick={handleSubmit(submitChange)} onClick={handleSubmit(submitChange)}
data-testid="submit-button" data-testid="submit-button"
> >
{loading && <Spinner className="h-5 w-5 mr-2" />}{" "} {loading && <Spinner className="mr-2 h-5 w-5" />}{" "}
<Translated i18nKey="change.submit" namespace="password" /> <Translated i18nKey="change.submit" namespace="password" />
</Button> </Button>
</div> </div>

View File

@@ -36,7 +36,7 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
return ( return (
<div className="relative flex items-start"> <div className="relative flex items-start">
<div className="flex items-center h-5"> <div className="flex h-5 items-center">
<div className="box-sizing block"> <div className="box-sizing block">
<input <input
ref={ref} ref={ref}
@@ -48,7 +48,7 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
disabled={disabled} disabled={disabled}
type="checkbox" type="checkbox"
className={classNames( className={classNames(
"form-checkbox rounded border-gray-300 text-primary-light-500 dark:text-primary-dark-500 shadow-sm focus:border-indigo-300 focus:ring focus:ring-offset-0 focus:ring-indigo-200 focus:ring-opacity-50", "form-checkbox rounded border-gray-300 text-primary-light-500 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 focus:ring-offset-0 dark:text-primary-dark-500",
className, className,
)} )}
{...props} {...props}

View File

@@ -25,7 +25,7 @@ export function ChooseAuthenticatorToLogin({
<Translated i18nKey="chooseAlternativeMethod" namespace="idp" /> <Translated i18nKey="chooseAlternativeMethod" namespace="idp" />
</div> </div>
)} )}
<div className="grid grid-cols-1 gap-5 w-full pt-4"> <div className="grid w-full grid-cols-1 gap-5 pt-4">
{authMethods.includes(AuthenticationMethodType.PASSWORD) && {authMethods.includes(AuthenticationMethodType.PASSWORD) &&
loginSettings?.allowUsernamePassword && loginSettings?.allowUsernamePassword &&
PASSWORD(false, "/password?" + params)} PASSWORD(false, "/password?" + params)}

View File

@@ -37,7 +37,7 @@ export function ChooseAuthenticatorToSetup({
</Alert> </Alert>
)} )}
<div className="grid grid-cols-1 gap-5 w-full pt-4"> <div className="grid w-full grid-cols-1 gap-5 pt-4">
{!authMethods.includes(AuthenticationMethodType.PASSWORD) && {!authMethods.includes(AuthenticationMethodType.PASSWORD) &&
loginSettings.allowUsernamePassword && loginSettings.allowUsernamePassword &&
PASSWORD(false, "/password/set?" + params)} PASSWORD(false, "/password/set?" + params)}

View File

@@ -58,7 +58,7 @@ export function ChooseSecondFactorToSetup({
return ( return (
<> <>
<div className="grid grid-cols-1 gap-5 w-full pt-4"> <div className="grid w-full grid-cols-1 gap-5 pt-4">
{loginSettings.secondFactors.map((factor) => { {loginSettings.secondFactors.map((factor) => {
switch (factor) { switch (factor) {
case SecondFactorType.OTP: case SecondFactorType.OTP:
@@ -94,7 +94,7 @@ export function ChooseSecondFactorToSetup({
</div> </div>
{!force && ( {!force && (
<button <button
className="transition-all text-sm hover:text-primary-light-500 dark:hover:text-primary-dark-500" className="text-sm transition-all hover:text-primary-light-500 dark:hover:text-primary-dark-500"
onClick={async () => { onClick={async () => {
const resp = await skipMFAAndContinueWithNextUrl({ const resp = await skipMFAAndContinueWithNextUrl({
userId, userId,

View File

@@ -34,7 +34,7 @@ export function ChooseSecondFactor({
} }
return ( return (
<div className="grid grid-cols-1 gap-5 w-full pt-4"> <div className="grid w-full grid-cols-1 gap-5 pt-4">
{userMethods.map((method, i) => { {userMethods.map((method, i) => {
return ( return (
<div key={"method-" + i}> <div key={"method-" + i}>

View File

@@ -47,16 +47,16 @@ export function ConsentScreen({
const scopes = scope?.filter((s) => !!s); const scopes = scope?.filter((s) => !!s);
return ( return (
<div className="pt-4 w-full flex flex-col items-center space-y-4"> <div className="flex w-full flex-col items-center space-y-4 pt-4">
<ul className="list-disc space-y-2 w-full"> <ul className="w-full list-disc space-y-2">
{scopes?.length === 0 && ( {scopes?.length === 0 && (
<span className="w-full text-sm flex flex-row items-center bg-background-light-400 dark:bg-background-dark-400 border border-divider-light py-2 px-4 rounded-md transition-all"> <span className="flex w-full flex-row items-center rounded-md border border-divider-light bg-background-light-400 px-4 py-2 text-sm transition-all dark:bg-background-dark-400">
<Translated i18nKey="device.scope.openid" namespace="device" /> <Translated i18nKey="device.scope.openid" namespace="device" />
</span> </span>
)} )}
{scopes?.map((s) => { {scopes?.map((s) => {
const translationKey = `device.scope.${s}`; const translationKey = `device.scope.${s}`;
const description = t(translationKey, null); const description = t(translationKey);
// Check if the key itself is returned and provide a fallback // Check if the key itself is returned and provide a fallback
const resolvedDescription = const resolvedDescription =
@@ -65,7 +65,7 @@ export function ConsentScreen({
return ( return (
<li <li
key={s} key={s}
className="w-full text-sm flex flex-row items-center bg-background-light-400 dark:bg-background-dark-400 border border-divider-light py-2 px-4 rounded-md transition-all" className="flex w-full flex-row items-center rounded-md border border-divider-light bg-background-light-400 px-4 py-2 text-sm transition-all dark:bg-background-dark-400"
> >
<span>{resolvedDescription}</span> <span>{resolvedDescription}</span>
</li> </li>
@@ -73,7 +73,7 @@ export function ConsentScreen({
})} })}
</ul> </ul>
<p className="ztdl-p text-xs text-left"> <p className="ztdl-p text-left text-xs">
<Translated <Translated
i18nKey="request.disclaimer" i18nKey="request.disclaimer"
namespace="device" namespace="device"
@@ -95,7 +95,7 @@ export function ConsentScreen({
variant={ButtonVariants.Secondary} variant={ButtonVariants.Secondary}
data-testid="deny-button" data-testid="deny-button"
> >
{loading && <Spinner className="h-5 w-5 mr-2" />} {loading && <Spinner className="mr-2 h-5 w-5" />}
<Translated i18nKey="device.request.deny" namespace="device" /> <Translated i18nKey="device.request.deny" namespace="device" />
</Button> </Button>
<span className="flex-grow"></span> <span className="flex-grow"></span>

View File

@@ -27,7 +27,7 @@ export function CopyToClipboard({ value }: Props) {
<button <button
id="tooltip-ctc" id="tooltip-ctc"
type="button" type="button"
className=" text-primary-light-500 dark:text-primary-dark-500" className="text-primary-light-500 dark:text-primary-dark-500"
onClick={() => setCopied(true)} onClick={() => setCopied(true)}
> >
{!copied ? ( {!copied ? (

View File

@@ -85,7 +85,7 @@ export function DeviceCodeForm({ userCode }: { userCode?: string }) {
onClick={handleSubmit(submitCodeAndContinue)} onClick={handleSubmit(submitCodeAndContinue)}
data-testid="submit-button" data-testid="submit-button"
> >
{loading && <Spinner className="h-5 w-5 mr-2" />}{" "} {loading && <Spinner className="mr-2 h-5 w-5" />}{" "}
<Translated i18nKey="verify.submit" namespace="verify" /> <Translated i18nKey="verify.submit" namespace="verify" />
</Button> </Button>
</div> </div>

View File

@@ -17,7 +17,7 @@ export function DynamicTheme({
}) { }) {
return ( return (
<ThemeWrapper branding={branding}> <ThemeWrapper branding={branding}>
<div className="rounded-lg bg-background-light-400 dark:bg-background-dark-500 px-8 py-12"> <div className="rounded-lg bg-background-light-400 px-8 py-12 dark:bg-background-dark-500">
<div className="mx-auto flex flex-col items-center space-y-4"> <div className="mx-auto flex flex-col items-center space-y-4">
<div className="relative flex flex-row items-center justify-center gap-8"> <div className="relative flex flex-row items-center justify-center gap-8">
{branding && ( {branding && (

View File

@@ -26,15 +26,15 @@ export const BaseButton = forwardRef<
ref={ref} ref={ref}
disabled={formStatus.pending} disabled={formStatus.pending}
className={clsx( className={clsx(
"flex-1 transition-all cursor-pointer flex flex-row items-center bg-background-light-400 text-text-light-500 dark:bg-background-dark-500 dark:text-text-dark-500 border border-divider-light hover:border-black dark:border-divider-dark hover:dark:border-white focus:border-primary-light-500 focus:dark:border-primary-dark-500 outline-none rounded-md px-4 text-sm", "flex flex-1 cursor-pointer flex-row items-center rounded-md border border-divider-light bg-background-light-400 px-4 text-sm text-text-light-500 outline-none transition-all hover:border-black focus:border-primary-light-500 dark:border-divider-dark dark:bg-background-dark-500 dark:text-text-dark-500 hover:dark:border-white focus:dark:border-primary-dark-500",
props.className, props.className,
)} )}
> >
<div className="flex-1 justify-between flex items-center gap-4"> <div className="flex flex-1 items-center justify-between gap-4">
<div className="flex-1 flex flex-row items-center"> <div className="flex flex-1 flex-row items-center">
{props.children} {props.children}
</div> </div>
{formStatus.pending && <Loader2Icon className="w-4 h-4 animate-spin" />} {formStatus.pending && <Loader2Icon className="h-4 w-4 animate-spin" />}
</div> </div>
</button> </button>
); );

View File

@@ -12,7 +12,7 @@ export const SignInWithApple = forwardRef<
return ( return (
<BaseButton {...restProps} ref={ref}> <BaseButton {...restProps} ref={ref}>
<div className="h-12 w-12 flex items-center justify-center"> <div className="flex h-12 w-12 items-center justify-center">
<div className="h-6 w-6"> <div className="h-6 w-6">
<svg viewBox="0 0 170 170" fill="currentColor"> <svg viewBox="0 0 170 170" fill="currentColor">
<title>Apple Logo</title> <title>Apple Logo</title>

View File

@@ -12,13 +12,13 @@ export const SignInWithAzureAd = forwardRef<
return ( return (
<BaseButton {...restProps} ref={ref}> <BaseButton {...restProps} ref={ref}>
<div className="h-12 p-[10px] w-12 flex items-center justify-center"> <div className="flex h-12 w-12 items-center justify-center p-[10px]">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="21" width="21"
height="21" height="21"
viewBox="0 0 21 21" viewBox="0 0 21 21"
className="w-full h-full" className="h-full w-full"
> >
<path fill="#f25022" d="M1 1H10V10H1z"></path> <path fill="#f25022" d="M1 1H10V10H1z"></path>
<path fill="#00a4ef" d="M1 11H10V20H1z"></path> <path fill="#00a4ef" d="M1 11H10V20H1z"></path>

View File

@@ -11,7 +11,7 @@ function GitHubLogo() {
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 1024 1024" viewBox="0 0 1024 1024"
className="h-8 w-8 hidden dark:block" className="hidden h-8 w-8 dark:block"
> >
<path <path
fill="#fafafa" fill="#fafafa"
@@ -24,7 +24,7 @@ function GitHubLogo() {
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 1024 1024" viewBox="0 0 1024 1024"
className="h-8 w-8 block dark:hidden" className="block h-8 w-8 dark:hidden"
> >
<path <path
fill="#1B1F23" fill="#1B1F23"

View File

@@ -12,7 +12,7 @@ export const SignInWithGitlab = forwardRef<
return ( return (
<BaseButton {...restProps} ref={ref}> <BaseButton {...restProps} ref={ref}>
<div className="h-12 w-12 flex items-center justify-center"> <div className="flex h-12 w-12 items-center justify-center">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width={25} width={25}

View File

@@ -12,7 +12,7 @@ export const SignInWithGoogle = forwardRef<
return ( return (
<BaseButton {...restProps} ref={ref}> <BaseButton {...restProps} ref={ref}>
<div className="h-12 w-12 flex items-center justify-center"> <div className="flex h-12 w-12 items-center justify-center">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlSpace="preserve" xmlSpace="preserve"

View File

@@ -27,12 +27,9 @@ export type TextInputProps = DetailedHTMLProps<
const styles = (error: boolean, disabled: boolean) => const styles = (error: boolean, disabled: boolean) =>
clsx({ clsx({
"h-[40px] mb-[2px] rounded p-[7px] bg-input-light-background dark:bg-input-dark-background transition-colors duration-300 grow": "h-[40px] mb-[2px] rounded p-[7px] bg-input-light-background dark:bg-input-dark-background transition-colors duration-300 grow": true,
true, "border border-input-light-border dark:border-input-dark-border hover:border-black hover:dark:border-white focus:border-primary-light-500 focus:dark:border-primary-dark-500": true,
"border border-input-light-border dark:border-input-dark-border hover:border-black hover:dark:border-white focus:border-primary-light-500 focus:dark:border-primary-dark-500": "focus:outline-none focus:ring-0 text-base text-black dark:text-white placeholder:italic placeholder-gray-700 dark:placeholder-gray-700": true,
true,
"focus:outline-none focus:ring-0 text-base text-black dark:text-white placeholder:italic placeholder-gray-700 dark:placeholder-gray-700":
true,
"border border-warn-light-500 dark:border-warn-dark-500 hover:border-warn-light-500 hover:dark:border-warn-dark-500 focus:border-warn-light-500 focus:dark:border-warn-dark-500": "border border-warn-light-500 dark:border-warn-dark-500 hover:border-warn-light-500 hover:dark:border-warn-dark-500 focus:border-warn-light-500 focus:dark:border-warn-dark-500":
error, error,
"pointer-events-none text-gray-500 dark:text-gray-800 border border-input-light-border dark:border-input-dark-border hover:border-light-hoverborder hover:dark:border-hoverborder cursor-default": "pointer-events-none text-gray-500 dark:text-gray-800 border border-input-light-border dark:border-input-dark-border hover:border-light-hoverborder hover:dark:border-hoverborder cursor-default":
@@ -60,7 +57,7 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
return ( return (
<label className="relative flex flex-col text-12px text-input-light-label dark:text-input-dark-label"> <label className="relative flex flex-col text-12px text-input-light-label dark:text-input-dark-label">
<span <span
className={`leading-3 mb-1 ${ className={`mb-1 leading-3 ${
error ? "text-warn-light-500 dark:text-warn-dark-500" : "" error ? "text-warn-light-500 dark:text-warn-dark-500" : ""
}`} }`}
> >
@@ -81,12 +78,12 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
/> />
{suffix && ( {suffix && (
<span className="z-30 absolute right-[3px] bottom-[22px] transform translate-y-1/2 bg-background-light-500 dark:bg-background-dark-500 p-2 rounded-sm"> <span className="absolute bottom-[22px] right-[3px] z-30 translate-y-1/2 transform rounded-sm bg-background-light-500 p-2 dark:bg-background-dark-500">
@{suffix} @{suffix}
</span> </span>
)} )}
<div className="leading-14.5px h-14.5px text-warn-light-500 dark:text-warn-dark-500 flex flex-row items-center text-12px"> <div className="leading-14.5px h-14.5px flex flex-row items-center text-12px text-warn-light-500 dark:text-warn-dark-500">
<span>{error ? error : " "}</span> <span>{error ? error : " "}</span>
</div> </div>

View File

@@ -37,13 +37,13 @@ export function LanguageSwitcher() {
<Listbox value={selected} onChange={handleChange}> <Listbox value={selected} onChange={handleChange}>
<ListboxButton <ListboxButton
className={clsx( className={clsx(
"relative block w-full rounded-lg bg-black/5 dark:bg-white/5 py-1.5 pr-8 pl-3 text-left text-sm/6 text-black dark:text-white", "relative block w-full rounded-lg bg-black/5 py-1.5 pl-3 pr-8 text-left text-sm/6 text-black dark:bg-white/5 dark:text-white",
"focus:outline-none data-[focus]:outline-2 data-[focus]:-outline-offset-2 data-[focus]:outline-white/25", "focus:outline-none data-[focus]:outline-2 data-[focus]:-outline-offset-2 data-[focus]:outline-white/25",
)} )}
> >
{selected.name} {selected.name}
<ChevronDownIcon <ChevronDownIcon
className="group pointer-events-none absolute top-2.5 right-2.5 size-4" className="group pointer-events-none absolute right-2.5 top-2.5 size-4"
aria-hidden="true" aria-hidden="true"
/> />
</ListboxButton> </ListboxButton>
@@ -51,7 +51,7 @@ export function LanguageSwitcher() {
anchor="bottom" anchor="bottom"
transition transition
className={clsx( className={clsx(
"w-[var(--button-width)] rounded-xl border border-black/5 dark:border-white/5 bg-background-light-500 dark:bg-background-dark-500 p-1 [--anchor-gap:var(--spacing-1)] focus:outline-none", "w-[var(--button-width)] rounded-xl border border-black/5 bg-background-light-500 p-1 [--anchor-gap:var(--spacing-1)] focus:outline-none dark:border-white/5 dark:bg-background-dark-500",
"transition duration-100 ease-in data-[leave]:data-[closed]:opacity-0", "transition duration-100 ease-in data-[leave]:data-[closed]:opacity-0",
)} )}
> >
@@ -59,7 +59,7 @@ export function LanguageSwitcher() {
<ListboxOption <ListboxOption
key={lang.code} key={lang.code}
value={lang} value={lang}
className="group flex cursor-default items-center gap-2 rounded-lg py-1.5 px-3 select-none data-[focus]:bg-black/10 dark:data-[focus]:bg-white/10" className="group flex cursor-default select-none items-center gap-2 rounded-lg px-3 py-1.5 data-[focus]:bg-black/10 dark:data-[focus]:bg-white/10"
> >
<CheckIcon className="invisible size-4 group-data-[selected]:visible" /> <CheckIcon className="invisible size-4 group-data-[selected]:visible" />
<div className="text-sm/6 text-black dark:text-white"> <div className="text-sm/6 text-black dark:text-white">

View File

@@ -100,7 +100,7 @@ export function LDAPUsernamePasswordForm({ idpId, link }: Props) {
onClick={handleSubmit(submitUsernamePassword)} onClick={handleSubmit(submitUsernamePassword)}
data-testid="submit-button" data-testid="submit-button"
> >
{loading && <Spinner className="h-5 w-5 mr-2" />} {loading && <Spinner className="mr-2 h-5 w-5" />}
<Translated i18nKey="submit" namespace="ldap" /> <Translated i18nKey="submit" namespace="ldap" />
</Button> </Button>
</div> </div>

View File

@@ -220,14 +220,14 @@ export function LoginOTP({
{["email", "sms"].includes(method) && ( {["email", "sms"].includes(method) && (
<Alert type={AlertType.INFO}> <Alert type={AlertType.INFO}>
<div className="flex flex-row"> <div className="flex flex-row">
<span className="flex-1 mr-auto text-left"> <span className="mr-auto flex-1 text-left">
<Translated i18nKey="verify.noCodeReceived" namespace="otp" /> <Translated i18nKey="verify.noCodeReceived" namespace="otp" />
</span> </span>
<button <button
aria-label="Resend OTP Code" aria-label="Resend OTP Code"
disabled={loading} disabled={loading}
type="button" type="button"
className="ml-4 text-primary-light-500 dark:text-primary-dark-500 hover:dark:text-primary-dark-400 hover:text-primary-light-400 cursor-pointer disabled:cursor-default disabled:text-gray-400 dark:disabled:text-gray-700" className="ml-4 cursor-pointer text-primary-light-500 hover:text-primary-light-400 disabled:cursor-default disabled:text-gray-400 dark:text-primary-dark-500 hover:dark:text-primary-dark-400 dark:disabled:text-gray-700"
onClick={() => { onClick={() => {
setLoading(true); setLoading(true);
updateSessionForOTPChallenge() updateSessionForOTPChallenge()
@@ -275,7 +275,7 @@ export function LoginOTP({
})} })}
data-testid="submit-button" data-testid="submit-button"
> >
{loading && <Spinner className="h-5 w-5 mr-2" />}{" "} {loading && <Spinner className="mr-2 h-5 w-5" />}{" "}
<Translated i18nKey="verify.submit" namespace="otp" /> <Translated i18nKey="verify.submit" namespace="otp" />
</Button> </Button>
</div> </div>

View File

@@ -271,7 +271,7 @@ export function LoginPasskey({
}} }}
data-testid="submit-button" data-testid="submit-button"
> >
{loading && <Spinner className="h-5 w-5 mr-2" />}{" "} {loading && <Spinner className="mr-2 h-5 w-5" />}{" "}
<Translated i18nKey="verify.submit" namespace="passkey" /> <Translated i18nKey="verify.submit" namespace="passkey" />
</Button> </Button>
</div> </div>

View File

@@ -19,7 +19,7 @@ const check = (
viewBox="0 0 24 24" viewBox="0 0 24 24"
strokeWidth={1.5} strokeWidth={1.5}
stroke="currentColor" stroke="currentColor"
className="w-6 h-6 las la-check text-green-500 dark:text-green-500 mr-2 text-lg" className="las la-check mr-2 h-6 w-6 text-lg text-green-500 dark:text-green-500"
role="img" role="img"
> >
<title>Matches</title> <title>Matches</title>
@@ -32,7 +32,7 @@ const check = (
); );
const cross = ( const cross = (
<svg <svg
className="w-6 h-6 las la-times text-warn-light-500 dark:text-warn-dark-500 mr-2 text-lg" className="las la-times mr-2 h-6 w-6 text-lg text-warn-light-500 dark:text-warn-dark-500"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"

View File

@@ -124,7 +124,7 @@ export function PasswordForm({
/> />
{!loginSettings?.hidePasswordReset && ( {!loginSettings?.hidePasswordReset && (
<button <button
className="transition-all text-sm hover:text-primary-light-500 dark:hover:text-primary-dark-500" className="text-sm transition-all hover:text-primary-light-500 dark:hover:text-primary-dark-500"
onClick={() => resetPasswordAndContinue()} onClick={() => resetPasswordAndContinue()}
type="button" type="button"
disabled={loading} disabled={loading}
@@ -167,7 +167,7 @@ export function PasswordForm({
onClick={handleSubmit(submitPassword)} onClick={handleSubmit(submitPassword)}
data-testid="submit-button" data-testid="submit-button"
> >
{loading && <Spinner className="h-5 w-5 mr-2" />}{" "} {loading && <Spinner className="mr-2 h-5 w-5" />}{" "}
<Translated i18nKey="verify.submit" namespace="password" /> <Translated i18nKey="verify.submit" namespace="password" />
</Button> </Button>
</div> </div>

View File

@@ -23,7 +23,7 @@ export function PrivacyPolicyCheckboxes({ legal, onChange }: Props) {
return ( return (
<> <>
<p className="flex flex-row items-center text-text-light-secondary-500 dark:text-text-dark-secondary-500 mt-4 text-sm"> <p className="mt-4 flex flex-row items-center text-sm text-text-light-secondary-500 dark:text-text-dark-secondary-500">
<Translated i18nKey="agreeTo" namespace="register" /> <Translated i18nKey="agreeTo" namespace="register" />
{legal?.helpLink && ( {legal?.helpLink && (
<span> <span>
@@ -34,7 +34,7 @@ export function PrivacyPolicyCheckboxes({ legal, onChange }: Props) {
viewBox="0 0 24 24" viewBox="0 0 24 24"
strokeWidth={1.5} strokeWidth={1.5}
stroke="currentColor" stroke="currentColor"
className="ml-1 w-5 h-5" className="ml-1 h-5 w-5"
> >
<path <path
strokeLinecap="round" strokeLinecap="round"

View File

@@ -96,7 +96,7 @@ export function RegisterFormIDPIncomplete({
return ( return (
<form className="w-full"> <form className="w-full">
<div className="grid grid-cols-2 gap-4 mb-4"> <div className="mb-4 grid grid-cols-2 gap-4">
<div className=""> <div className="">
<TextInput <TextInput
type="firstname" type="firstname"
@@ -147,7 +147,7 @@ export function RegisterFormIDPIncomplete({
onClick={handleSubmit(submitAndRegister)} onClick={handleSubmit(submitAndRegister)}
data-testid="submit-button" data-testid="submit-button"
> >
{loading && <Spinner className="h-5 w-5 mr-2" />}{" "} {loading && <Spinner className="mr-2 h-5 w-5" />}{" "}
<Translated i18nKey="submit" namespace="register" /> <Translated i18nKey="submit" namespace="register" />
</Button> </Button>
</div> </div>

View File

@@ -124,7 +124,7 @@ export function RegisterForm({
const [tosAndPolicyAccepted, setTosAndPolicyAccepted] = useState(false); const [tosAndPolicyAccepted, setTosAndPolicyAccepted] = useState(false);
return ( return (
<form className="w-full"> <form className="w-full">
<div className="grid grid-cols-2 gap-4 mb-4"> <div className="mb-4 grid grid-cols-2 gap-4">
<div className=""> <div className="">
<TextInput <TextInput
type="firstname" type="firstname"
@@ -170,7 +170,7 @@ export function RegisterForm({
loginSettings.allowUsernamePassword && loginSettings.allowUsernamePassword &&
loginSettings.passkeysType == PasskeysType.ALLOWED && ( loginSettings.passkeysType == PasskeysType.ALLOWED && (
<> <>
<p className="mt-4 ztdl-p mb-6 block text-left"> <p className="ztdl-p mb-6 mt-4 block text-left">
<Translated i18nKey="selectMethod" namespace="register" /> <Translated i18nKey="selectMethod" namespace="register" />
</p> </p>
@@ -211,14 +211,14 @@ export function RegisterForm({
const usePasswordToContinue: boolean = const usePasswordToContinue: boolean =
loginSettings?.allowUsernamePassword && loginSettings?.allowUsernamePassword &&
loginSettings?.passkeysType == PasskeysType.ALLOWED loginSettings?.passkeysType == PasskeysType.ALLOWED
? !!!(selected === methods[0]) // choose selection if both available ? !(selected === methods[0]) // choose selection if both available
: !!loginSettings?.allowUsernamePassword; // if password is chosen : !!loginSettings?.allowUsernamePassword; // if password is chosen
// set password as default if only password is allowed // set password as default if only password is allowed
return submitAndContinue(values, usePasswordToContinue); return submitAndContinue(values, usePasswordToContinue);
})} })}
data-testid="submit-button" data-testid="submit-button"
> >
{loading && <Spinner className="h-5 w-5 mr-2" />} {loading && <Spinner className="mr-2 h-5 w-5" />}
<Translated i18nKey="submit" namespace="register" /> <Translated i18nKey="submit" namespace="register" />
</Button> </Button>
</div> </div>

View File

@@ -211,7 +211,7 @@ export function RegisterPasskey({
onClick={handleSubmit(submitRegisterAndContinue)} onClick={handleSubmit(submitRegisterAndContinue)}
data-testid="submit-button" data-testid="submit-button"
> >
{loading && <Spinner className="h-5 w-5 mr-2" />}{" "} {loading && <Spinner className="mr-2 h-5 w-5" />}{" "}
<Translated i18nKey="set.submit" namespace="passkey" /> <Translated i18nKey="set.submit" namespace="passkey" />
</Button> </Button>
</div> </div>

View File

@@ -216,7 +216,7 @@ export function RegisterU2f({
onClick={submitRegisterAndContinue} onClick={submitRegisterAndContinue}
data-testid="submit-button" data-testid="submit-button"
> >
{loading && <Spinner className="h-5 w-5 mr-2" />}{" "} {loading && <Spinner className="mr-2 h-5 w-5" />}{" "}
<Translated i18nKey="set.submit" namespace="u2f" /> <Translated i18nKey="set.submit" namespace="u2f" />
</Button> </Button>
</div> </div>

View File

@@ -15,7 +15,7 @@ export function SelfServiceMenu({ sessionId }: { sessionId: string }) {
// } // }
return ( return (
<div className="w-full flex flex-col space-y-2"> <div className="flex w-full flex-col space-y-2">
{list.map((menuitem, index) => { {list.map((menuitem, index) => {
return ( return (
<SelfServiceItem <SelfServiceItem
@@ -34,7 +34,7 @@ const SelfServiceItem = ({ name, link }: { name: string; link: string }) => {
<Link <Link
prefetch={false} prefetch={false}
href={link} href={link}
className="w-full group flex flex-row items-center bg-background-light-400 dark:bg-background-dark-400 border border-divider-light hover:shadow-lg dark:hover:bg-white/10 py-2 px-4 rounded-md transition-all" className="group flex w-full flex-row items-center rounded-md border border-divider-light bg-background-light-400 px-4 py-2 transition-all hover:shadow-lg dark:bg-background-dark-400 dark:hover:bg-white/10"
> >
{name} {name}
</Link> </Link>

View File

@@ -52,7 +52,7 @@ export function SessionClearItem({
reload(); reload();
}); });
}} }}
className="group flex flex-row items-center bg-background-light-400 dark:bg-background-dark-400 border border-divider-light hover:shadow-lg dark:hover:bg-white/10 py-2 px-4 rounded-md transition-all" className="group flex flex-row items-center rounded-md border border-divider-light bg-background-light-400 px-4 py-2 transition-all hover:shadow-lg dark:bg-background-dark-400 dark:hover:bg-white/10"
> >
<div className="pr-4"> <div className="pr-4">
<Avatar <Avatar
@@ -64,11 +64,11 @@ export function SessionClearItem({
<div className="flex flex-col items-start overflow-hidden"> <div className="flex flex-col items-start overflow-hidden">
<span className="">{session.factors?.user?.displayName}</span> <span className="">{session.factors?.user?.displayName}</span>
<span className="text-xs opacity-80 text-ellipsis"> <span className="text-ellipsis text-xs opacity-80">
{session.factors?.user?.loginName} {session.factors?.user?.loginName}
</span> </span>
{valid ? ( {valid ? (
<span className="text-xs opacity-80 text-ellipsis"> <span className="text-ellipsis text-xs opacity-80">
{verifiedAt && ( {verifiedAt && (
<Translated <Translated
i18nKey="verfiedAt" i18nKey="verfiedAt"
@@ -79,7 +79,7 @@ export function SessionClearItem({
</span> </span>
) : ( ) : (
verifiedAt && ( verifiedAt && (
<span className="text-xs opacity-80 text-ellipsis"> <span className="text-ellipsis text-xs opacity-80">
expired{" "} expired{" "}
{session.expirationDate && {session.expirationDate &&
moment(timestampDate(session.expirationDate)).fromNow()} moment(timestampDate(session.expirationDate)).fromNow()}
@@ -90,14 +90,14 @@ export function SessionClearItem({
<span className="flex-grow"></span> <span className="flex-grow"></span>
<div className="relative flex flex-row items-center"> <div className="relative flex flex-row items-center">
<div className="mr-6 px-2 py-[2px] text-xs hidden group-hover:block transition-all text-warn-light-500 dark:text-warn-dark-500 bg-[#ff0000]/10 dark:bg-[#ff0000]/10 rounded-full flex items-center justify-center"> <div className="mr-6 flex hidden items-center justify-center rounded-full bg-[#ff0000]/10 px-2 py-[2px] text-xs text-warn-light-500 transition-all group-hover:block dark:bg-[#ff0000]/10 dark:text-warn-dark-500">
<Translated i18nKey="clear" namespace="logout" /> <Translated i18nKey="clear" namespace="logout" />
</div> </div>
{valid ? ( {valid ? (
<div className="absolute h-2 w-2 bg-green-500 rounded-full mx-2 transform right-0 transition-all"></div> <div className="absolute right-0 mx-2 h-2 w-2 transform rounded-full bg-green-500 transition-all"></div>
) : ( ) : (
<div className="absolute h-2 w-2 bg-red-500 rounded-full mx-2 transform right-0 transition-all"></div> <div className="absolute right-0 mx-2 h-2 w-2 transform rounded-full bg-red-500 transition-all"></div>
)} )}
</div> </div>
</button> </button>

View File

@@ -102,7 +102,7 @@ export function SessionItem({
} }
} }
}} }}
className="group flex flex-row items-center bg-background-light-400 dark:bg-background-dark-400 border border-divider-light hover:shadow-lg dark:hover:bg-white/10 py-2 px-4 rounded-md transition-all" className="group flex flex-row items-center rounded-md border border-divider-light bg-background-light-400 px-4 py-2 transition-all hover:shadow-lg dark:bg-background-dark-400 dark:hover:bg-white/10"
> >
<div className="pr-4"> <div className="pr-4">
<Avatar <Avatar
@@ -114,16 +114,16 @@ export function SessionItem({
<div className="flex flex-col items-start overflow-hidden"> <div className="flex flex-col items-start overflow-hidden">
<span className="">{session.factors?.user?.displayName}</span> <span className="">{session.factors?.user?.displayName}</span>
<span className="text-xs opacity-80 text-ellipsis"> <span className="text-ellipsis text-xs opacity-80">
{session.factors?.user?.loginName} {session.factors?.user?.loginName}
</span> </span>
{valid ? ( {valid ? (
<span className="text-xs opacity-80 text-ellipsis"> <span className="text-ellipsis text-xs opacity-80">
{verifiedAt && moment(timestampDate(verifiedAt)).fromNow()} {verifiedAt && moment(timestampDate(verifiedAt)).fromNow()}
</span> </span>
) : ( ) : (
verifiedAt && ( verifiedAt && (
<span className="text-xs opacity-80 text-ellipsis"> <span className="text-ellipsis text-xs opacity-80">
expired{" "} expired{" "}
{session.expirationDate && {session.expirationDate &&
moment(timestampDate(session.expirationDate)).fromNow()} moment(timestampDate(session.expirationDate)).fromNow()}
@@ -135,13 +135,13 @@ export function SessionItem({
<span className="flex-grow"></span> <span className="flex-grow"></span>
<div className="relative flex flex-row items-center"> <div className="relative flex flex-row items-center">
{valid ? ( {valid ? (
<div className="absolute h-2 w-2 bg-green-500 rounded-full mx-2 transform right-0 group-hover:right-6 transition-all"></div> <div className="absolute right-0 mx-2 h-2 w-2 transform rounded-full bg-green-500 transition-all group-hover:right-6"></div>
) : ( ) : (
<div className="absolute h-2 w-2 bg-red-500 rounded-full mx-2 transform right-0 group-hover:right-6 transition-all"></div> <div className="absolute right-0 mx-2 h-2 w-2 transform rounded-full bg-red-500 transition-all group-hover:right-6"></div>
)} )}
<XCircleIcon <XCircleIcon
className="hidden group-hover:block h-5 w-5 transition-all opacity-50 hover:opacity-100" className="hidden h-5 w-5 opacity-50 transition-all hover:opacity-100 group-hover:block"
onClick={(event) => { onClick={(event) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();

View File

@@ -188,18 +188,18 @@ export function SetPasswordForm({
return ( return (
<form className="w-full"> <form className="w-full">
<div className="pt-4 grid grid-cols-1 gap-4 mb-4"> <div className="mb-4 grid grid-cols-1 gap-4 pt-4">
{codeRequired && ( {codeRequired && (
<Alert type={AlertType.INFO}> <Alert type={AlertType.INFO}>
<div className="flex flex-row"> <div className="flex flex-row">
<span className="flex-1 mr-auto text-left"> <span className="mr-auto flex-1 text-left">
<Translated i18nKey="set.noCodeReceived" namespace="password" /> <Translated i18nKey="set.noCodeReceived" namespace="password" />
</span> </span>
<button <button
aria-label="Resend OTP Code" aria-label="Resend OTP Code"
disabled={loading} disabled={loading}
type="button" type="button"
className="ml-4 text-primary-light-500 dark:text-primary-dark-500 hover:dark:text-primary-dark-400 hover:text-primary-light-400 cursor-pointer disabled:cursor-default disabled:text-gray-400 dark:disabled:text-gray-700" className="ml-4 cursor-pointer text-primary-light-500 hover:text-primary-light-400 disabled:cursor-default disabled:text-gray-400 dark:text-primary-dark-500 hover:dark:text-primary-dark-400 dark:disabled:text-gray-700"
onClick={() => { onClick={() => {
resendCode(); resendCode();
}} }}
@@ -277,7 +277,7 @@ export function SetPasswordForm({
onClick={handleSubmit(submitPassword)} onClick={handleSubmit(submitPassword)}
data-testid="submit-button" data-testid="submit-button"
> >
{loading && <Spinner className="h-5 w-5 mr-2" />}{" "} {loading && <Spinner className="mr-2 h-5 w-5" />}{" "}
<Translated i18nKey="set.submit" namespace="password" /> <Translated i18nKey="set.submit" namespace="password" />
</Button> </Button>
</div> </div>

View File

@@ -108,7 +108,7 @@ export function SetRegisterPasswordForm({
return ( return (
<form className="w-full"> <form className="w-full">
<div className="pt-4 grid grid-cols-1 gap-4 mb-4"> <div className="mb-4 grid grid-cols-1 gap-4 pt-4">
<div className=""> <div className="">
<TextInput <TextInput
type="password" type="password"
@@ -161,7 +161,7 @@ export function SetRegisterPasswordForm({
onClick={handleSubmit(submitRegister)} onClick={handleSubmit(submitRegister)}
data-testid="submit-button" data-testid="submit-button"
> >
{loading && <Spinner className="h-5 w-5 mr-2" />}{" "} {loading && <Spinner className="mr-2 h-5 w-5" />}{" "}
<Translated i18nKey="password.submit" namespace="register" /> <Translated i18nKey="password.submit" namespace="register" />
</Button> </Button>
</div> </div>

View File

@@ -76,8 +76,8 @@ export function SignInWithIdp({
}; };
return ( return (
<div className="flex flex-col w-full space-y-2 text-sm"> <div className="flex w-full flex-col space-y-2 text-sm">
<p className="text-center ztdl-p"> <p className="ztdl-p text-center">
<Translated i18nKey="orSignInWith" namespace="idp" /> <Translated i18nKey="orSignInWith" namespace="idp" />
</p> </p>
{!!identityProviders.length && identityProviders?.map(renderIDPButton)} {!!identityProviders.length && identityProviders?.map(renderIDPButton)}

View File

@@ -2,7 +2,7 @@ import { ReactNode } from "react";
export function Skeleton({ children }: { children?: ReactNode }) { export function Skeleton({ children }: { children?: ReactNode }) {
return ( return (
<div className="skeleton py-12 px-8 rounded-lg bg-background-light-600 dark:bg-background-dark-600 flex flex-row items-center justify-center"> <div className="skeleton flex flex-row items-center justify-center rounded-lg bg-background-light-600 px-8 py-12 dark:bg-background-dark-600">
{children} {children}
</div> </div>
); );

View File

@@ -4,7 +4,7 @@ export const Spinner: FC<{ className?: string }> = ({ className = "" }) => {
return ( return (
<svg <svg
role="status" role="status"
className={`${className} inline-block animate-spin fill-primary-light-500 dark:fill-primary-dark-500 text-black/10 dark:text-white/10`} className={`${className} inline-block animate-spin fill-primary-light-500 text-black/10 dark:fill-primary-dark-500 dark:text-white/10`}
viewBox="0 0 100 101" viewBox="0 0 100 101"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -16,8 +16,7 @@ export type StateBadgeProps = {
const getBadgeClasses = (state: BadgeState, evenPadding: boolean) => const getBadgeClasses = (state: BadgeState, evenPadding: boolean) =>
clsx({ clsx({
"w-fit border-box h-18.5px flex flex-row items-center whitespace-nowrap tracking-wider leading-4 items-center justify-center px-2 py-2px text-12px rounded-full shadow-sm": "w-fit border-box h-18.5px flex flex-row items-center whitespace-nowrap tracking-wider leading-4 items-center justify-center px-2 py-2px text-12px rounded-full shadow-sm": true,
true,
"bg-state-success-light-background text-state-success-light-color dark:bg-state-success-dark-background dark:text-state-success-dark-color ": "bg-state-success-light-background text-state-success-light-color dark:bg-state-success-dark-background dark:text-state-success-dark-color ":
state === BadgeState.Success, state === BadgeState.Success,
"bg-state-neutral-light-background text-state-neutral-light-color dark:bg-state-neutral-dark-background dark:text-state-neutral-dark-color": "bg-state-neutral-light-background text-state-neutral-light-color dark:bg-state-neutral-dark-background dark:text-state-neutral-dark-color":

View File

@@ -23,7 +23,7 @@ export const Tab = ({
return ( return (
<Link <Link
href={href} href={href}
className={clsx("mt-2 mr-2 rounded-lg px-3 py-1 text-sm font-medium", { className={clsx("mr-2 mt-2 rounded-lg px-3 py-1 text-sm font-medium", {
"bg-gray-700 text-gray-100 hover:bg-gray-500 hover:text-white": "bg-gray-700 text-gray-100 hover:bg-gray-500 hover:text-white":
!isActive, !isActive,
"bg-blue-500 text-white": isActive, "bg-blue-500 text-white": isActive,

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