diff --git a/.eslintrc.js b/.eslintrc.cjs similarity index 100% rename from .eslintrc.js rename to .eslintrc.cjs diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 0ea84c5c8a..138d4919af 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -7,6 +7,7 @@ - [ ] All open todos and follow ups are defined in a new ticket and justified - [ ] Deviations from the acceptance criteria and design are agreed with the PO and documented. - [ ] Vitest unit tests ensure that components produce expected outputs on different inputs. -- [ ] Cypress integration tests ensure that login app pages work as expected. The ZITADEL API is mocked. +- [ ] Cypress integration tests ensure that login app pages work as expected on good and bad user inputs, ZITADEL responses or IDP redirects. The ZITADEL API is mocked, IDP redirects are simulated. +- [ ] Playwright acceptances tests ensure that the happy paths of common user journeys work as expected. The ZITADEL API is not mocked but IDP redirects are simulated. - [ ] No debug or dead code - [ ] My code has no repetitions diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 91c78a4db2..cac00bde39 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,8 +1,39 @@ name: Quality -on: pull_request +on: + pull_request: + schedule: + # Every morning at 6:00 AM CET + - cron: '0 4 * * *' + workflow_dispatch: + inputs: + target-env: + description: 'Zitadel target environment to run the acceptance tests against.' + required: true + type: choice + options: + - 'qa' + - 'prod' jobs: + matrix: + # If the workflow is triggered by a schedule event, only the acceptance tests run against QA and Prod. + name: Matrix + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.matrix.outputs.matrix }} + steps: + - name: Matrix + id: matrix + run: | + if [ -n "${{ github.event.schedule }}" ]; then + echo 'matrix=["test:acceptance:qa", "test:acceptance:prod"]' >> $GITHUB_OUTPUT + elif [ -n "${{ github.event.inputs.target-env }}" ]; then + echo 'matrix=["test:acceptance:${{ github.event.inputs.target-env }}"]' >> $GITHUB_OUTPUT + else + echo 'matrix=["format --check", "lint", "test:unit", "test:integration", "test:acceptance"]' >> $GITHUB_OUTPUT + fi + quality: name: Ensure Quality @@ -13,46 +44,29 @@ jobs: permissions: contents: "read" + needs: + - matrix + strategy: fail-fast: false matrix: - command: - - format --check - - lint - - test:unit - - test:integration + command: ${{ fromJson( needs.matrix.outputs.matrix ) }} steps: - name: Checkout Repo uses: actions/checkout@v4.1.6 - - name: Setup Node.js 20.x - uses: actions/setup-node@v4.0.2 - with: - node-version: 20.x + - name: Setup Buf + uses: bufbuild/buf-setup-action@v1.45.0 - name: Setup pnpm uses: pnpm/action-setup@v4.0.0 - - uses: pnpm/action-setup@v4.0.0 - name: Install pnpm - id: pnpm-install + - name: Setup Node.js 20.x + uses: actions/setup-node@v4.0.2 with: - run_install: false - - - name: Get pnpm store directory - id: pnpm-cache - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - uses: actions/cache@v4.0.2 - name: Setup pnpm cache - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- + node-version: 20.x + cache: 'pnpm' - uses: actions/cache@v4.0.2 name: Setup Cypress binary cache @@ -61,11 +75,52 @@ jobs: key: ${{ runner.os }}-cypress-binary-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-cypress-binary- - if: ${{ matrix.command }} == "test:integration" + # The Cypress binary cache needs to be updated together with the pnpm dependencies cache. + # That's why we don't conditionally cache it using if: ${{ matrix.command == 'test:integration' }} - name: Install Dependencies - run: pnpm install + run: pnpm install --frozen-lockfile + + # We can cache the Playwright binary independently from the pnpm cache, because we install it separately. + # After pnpm install --frozen-lockfile, we can get the version so we only have to download the binary once per version. + - run: echo "PLAYWRIGHT_VERSION=$(npx playwright --version | cut -d ' ' -f 2)" >> $GITHUB_ENV + if: ${{ startsWith(matrix.command, 'test:acceptance') }} + + - uses: actions/cache@v4.0.2 + name: Setup Playwright binary cache + id: playwright-cache + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-binary-${{ env.PLAYWRIGHT_VERSION }} + restore-keys: | + ${{ runner.os }}-playwright-binary- + if: ${{ startsWith(matrix.command, 'test:acceptance') }} + + - name: Install Playwright Browsers + run: pnpm exec playwright install --with-deps + if: ${{ startsWith(matrix.command, 'test:acceptance') && steps.playwright-cache.outputs.cache-hit != 'true' }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + if: ${{ matrix.command == 'test:acceptance' }} + + - name: Run ZITADEL + run: ZITADEL_DEV_UID=root pnpm run-sink + if: ${{ matrix.command == 'test:acceptance' }} + + - name: Create Cloud Env File + run: | + if [ "${{ matrix.command }}" == "test:acceptance:prod" ]; then + echo "${{ secrets.ENV_FILE_CONTENT_ACCEPTANCE_PROD }}" | tee apps/login/.env.local acceptance/tests/.env.local > /dev/null + else + echo "${{ secrets.ENV_FILE_CONTENT_ACCEPTANCE_QA }}" | tee apps/login/.env.local acceptance/tests/.env.local > /dev/null + fi + if: ${{ matrix.command == 'test:acceptance:qa' || matrix.command == 'test:acceptance:prod' }} + + - name: Create Production Build + run: pnpm build + if: ${{ startsWith(matrix.command, 'test:acceptance') }} - name: Check id: check - run: pnpm ${{ matrix.command }} + run: pnpm ${{ contains(matrix.command, 'test:acceptance') && 'test:acceptance' || matrix.command }} diff --git a/.gitignore b/.gitignore index 978119f359..5cf3100aa4 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,7 @@ packages/zitadel-server/src/app/proto .idea .vercel .env*.local +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/.prettierignore b/.prettierignore index c7dc50f307..a37ac4ff79 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,8 @@ .next/ +.changeset/ +.github/ dist/ +packages/zitadel-proto/google +packages/zitadel-proto/protoc-gen-openapiv2 +packages/zitadel-proto/validate packages/zitadel-proto/zitadel diff --git a/.prettierrc.json b/.prettierrc similarity index 99% rename from .prettierrc.json rename to .prettierrc index 55a45eb397..6d0c388d7a 100644 --- a/.prettierrc.json +++ b/.prettierrc @@ -3,3 +3,4 @@ "trailingComma": "all", "plugins": ["prettier-plugin-organize-imports"] } + \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 18038ed32b..1b4e0b5bd8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -84,10 +84,9 @@ The application is now available at `http://localhost:3000` You can execute the following commands `pnpm test` for a single test run or `pnpm test:watch` in the following directories: - apps/login +- packages/zitadel-proto - packages/zitadel-client -- packages/zitadel-server -- packages/zitadel-react -- packages/zitadel-next +- packages/zitadel-node - The projects root directory: all tests in the project are executed In apps/login, these commands also spin up the application and a ZITADEL gRPC API mock server to run integration tests using [Cypress](https://www.cypress.io/) against them. diff --git a/README.md b/README.md index cfe1f022fb..b08fbba545 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,16 @@ -# ZITADEL TypeScript with Turborepo and Changesets +# ZITADEL TypeScript with Turborepo This repository contains all TypeScript and JavaScript packages and applications you need to create your own ZITADEL Login UI. -The repo makes use of the [build system Turbo](https://turbo.build/repo) and -the [Changesets CLI for versioning the packages](https://github.com/changesets/changesets). + +collage of login screens **⚠️ This repo and packages are in alpha state and subject to change ⚠️** -The scope of functionality of this repo and packages is limited and under active development. -Once the package structure is set and all APIs are fully implemented we'll move this repo to beta state. +The scope of functionality of this repo and packages is under active development. + +The `@zitadel/client` package is using [@connectrpc/connect](https://github.com/connectrpc/connect-es#readme). + You can read the [contribution guide](/CONTRIBUTING.md) on how to contribute. Questions can be raised in our [Discord channel](https://discord.gg/erh5Brh7jE) or as a [GitHub issue](https://github.com/zitadel/typescript/issues). @@ -28,11 +30,8 @@ We think the easiest path of getting up and running, is the following: ## Included Apps And Packages - `login`: The login UI used by ZITADEL Cloud, powered by Next.js -- `@zitadel/node`: core components for establishing node client connection, grpc stub -- `@zitadel/client`: shared client utilities +- `@zitadel/client`: shared client utilities for node and browser environments - `@zitadel/proto`: shared protobuf types -- `@zitadel/react`: shared React utilities and components built with tailwindcss -- `@zitadel/next`: shared Next.js utilities - `@zitadel/tsconfig`: shared `tsconfig.json`s used throughout the monorepo - `eslint-config-zitadel`: ESLint preset @@ -50,17 +49,19 @@ the features will be extended. This list should show the current implementation state, and also what is missing. You can already use the current state, and extend it with your needs. +#### Features list + - [x] Local User Registration (with Password) - [x] User Registration and Login with external Provider - [x] Google - - [ ] GitHub - - [ ] GitHub Enterprise + - [x] GitHub + - [x] GitHub Enterprise - [x] GitLab - - [ ] GitLab Enterprise - - [ ] Azure - - [ ] Apple - - [ ] Generic OIDC - - [ ] Generic OAuth + - [x] GitLab Enterprise + - [x] Azure + - [x] Apple + - [x] Generic OIDC + - [x] Generic OAuth - [ ] Generic JWT - [ ] LDAP - [ ] SAML SP @@ -70,9 +71,10 @@ You can already use the current state, and extend it with your needs. - [x] OTP: Email Code - [x] OTP: SMS Code - [ ] Password Change/Reset -- [ ] Domain Discovery +- [x] Domain Discovery - [x] Branding - OIDC Standard + - [x] Authorization Code Flow with PKCE - [x] AuthRequest `hintUserId` - [x] AuthRequest `loginHint` @@ -84,12 +86,60 @@ You can already use the current state, and extend it with your needs. - Scopes - [x] `openid email profile address`` - [x] `offline access` - - [ ] `urn:zitadel:iam:org:idp:id:{idp_id}` + - [x] `urn:zitadel:iam:org:idp:id:{idp_id}` - [x] `urn:zitadel:iam:org:project:id:zitadel:aud` - [x] `urn:zitadel:iam:org:id:{orgid}` - [x] `urn:zitadel:iam:org:domain:primary:{domain}` - [ ] AuthRequest UI locales + #### Flow diagram + + This diagram shows the available pages and flows. + + > Note that back navigation or retries are not displayed. + +```mermaid + flowchart TD + A[Start] --> register + A[Start] --> accounts + A[Start] --> loginname + loginname -- signInWithIDP --> idp-success + loginname -- signInWithIDP --> idp-failure + idp-success --> B[signedin] + loginname --> password + loginname -- hasPasskey --> passkey + loginname -- allowRegister --> register + passkey-add --passwordAllowed --> password + passkey -- hasPassword --> password + passkey --> B[signedin] + password -- hasMFA --> mfa + password -- allowPasskeys --> passkey-add + password -- reset --> password-set + email -- reset --> password-set + password-set --> B[signedin] + password-change --> B[signedin] + password -- userstate=initial --> password-change + + mfa --> otp + otp --> B[signedin] + mfa--> u2f + u2f -->B[signedin] + register -- password/passkey --> B[signedin] + password --> B[signedin] + password-- forceMFA -->mfaset + mfaset --> u2fset + mfaset --> otpset + u2fset --> B[signedin] + otpset --> B[signedin] + accounts--> loginname + password -- not verified yet -->verify + register-- withpassword -->verify + passkey-- notVerified --> verify + verify --> B[signedin] +``` + +You can find a more detailed documentation of the different pages [here](./apps/login/readme.md). + ## Tooling - [TypeScript](https://www.typescriptlang.org/) for static type checking @@ -119,22 +169,6 @@ settings. The [Changesets bot](https://github.com/apps/changeset-bot) should als Read the [changesets documentation](https://github.com/changesets/changesets/blob/main/docs/automating-changesets.md) for more information about this automation -### NPM - -If you want to publish a package to the public npm registry and make them publicly available, this is already setup. - -To publish packages to a private npm organization scope, **remove** the following from each of the `package.json`'s - -```diff -- "publishConfig": { -- "access": "public" -- }, -``` - -### GitHub Package Registry - -See [working with the npm registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry#publishing-a-package-using-publishconfig-in-the-packagejson-file) - ### Run Login UI To run the application make sure to install the dependencies with @@ -143,32 +177,75 @@ To run the application make sure to install the dependencies with pnpm install ``` -then setup the environment for the login application which needs a `.env.local` in `/apps/login`. -Go to your instance and create a service user for the application having the `IAM_OWNER` manager role. -This user is required to have access to create users on your primary organization and reading policy data so it can be -restricted to your personal use case but we'll stick with `IAM_OWNER` for convenience. Create a PAT and copy the value to -paste it under the `ZITADEL_SERVICE_USER_TOKEN` key. -The file should look as follows: - -``` -ZITADEL_API_URL=[yourinstanceurl] -ZITADEL_ORG_ID=[yourprimaryorg] -ZITADEL_SERVICE_USER_TOKEN=[yourserviceuserpersonalaccesstoken] -``` - then generate the GRPC stubs with ```sh pnpm generate ``` -and then run it with +To run the application against a local ZITADEL instance, run the following command: + +```sh +pnpm run-zitadel +``` + +This sets up ZITADEL using docker compose and writes the configuration to the file `apps/login/.env.local`. + +
+Alternatively, use another environment +You can develop against any ZITADEL instance in which you have sufficient rights to execute the following steps. +Just create or overwrite the file `apps/login/.env.local` yourself. +Add your instances base URL to the file at the key `ZITADEL_API_URL`. +Go to your instance and create a service user for the login application. +The login application creates users on your primary organization and reads policy data. +For the sake of simplicity, just make the service user an instance member with the role `IAM_OWNER`. +Create a PAT and copy it to the file `apps/login/.env.local` using the key `ZITADEL_SERVICE_USER_TOKEN`. +Also add the users ID to the file using the key `ZITADEL_SERVICE_USER_ID`. + +The file should look similar to this: + +``` +ZITADEL_API_URL=https://zitadel-tlx3du.us1.zitadel.cloud +ZITADEL_SERVICE_USER_ID=289106423158521850 +ZITADEL_SERVICE_USER_TOKEN=1S6w48thfWFI2klgfwkCnhXJLf9FQ457E-_3H74ePQxfO3Af0Tm4V5Xi-ji7urIl_xbn-Rk +``` + +
+ +Start the login application in dev mode: ```sh pnpm dev ``` Open the login application with your favorite browser at `localhost:3000`. +Change the source code and see the changes live in your browser. + +Make sure the application still behaves as expected by running all tests + +```sh +pnpm test +``` + +To satisfy your unique workflow requirements, check out the package.json in the root directory for more detailed scripts. + +### Run Login UI Acceptance tests + +To run the acceptance tests you need a running ZITADEL environment and a component which receives HTTP requests for the emails and sms's. +This component should also be able to return the content of these notifications, as the codes and links are used in the login flows. +There is a basic implementation in Golang available under [the sink package](./acceptance/sink). + +To setup ZITADEL with the additional Sink container for handling the notifications: + +```sh +pnpm run-sink +``` + +Then you can start the acceptance tests with: + +```sh +pnpm test:acceptance +``` ### Deploy to Vercel diff --git a/acceptance/Dockerfile b/acceptance/Dockerfile index a2283d09a9..36f6ba8f19 100644 --- a/acceptance/Dockerfile +++ b/acceptance/Dockerfile @@ -1,6 +1,5 @@ FROM golang:1.19-alpine RUN apk add curl jq -RUN go install github.com/zitadel/zitadel-tools@v0.4.0 COPY setup.sh /setup.sh RUN chmod +x /setup.sh ENTRYPOINT [ "/setup.sh" ] diff --git a/acceptance/docker-compose.yaml b/acceptance/docker-compose.yaml index ac2fa512f5..de6990387d 100644 --- a/acceptance/docker-compose.yaml +++ b/acceptance/docker-compose.yaml @@ -1,40 +1,38 @@ -version: "3.8" - services: zitadel: user: "${ZITADEL_DEV_UID}" - image: "${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:latest}" + image: "${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:v2.65.0}" command: 'start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled --config /zitadel.yaml --steps /zitadel.yaml' ports: - "8080:8080" volumes: - - ./machinekey:/machinekey + - ./pat:/pat - ./zitadel.yaml:/zitadel.yaml depends_on: db: condition: "service_healthy" db: - image: "cockroachdb/cockroach:v22.2.2" - command: "start-single-node --insecure --http-addr :9090" + restart: "always" + image: postgres:17.0-alpine3.19 + environment: + - POSTGRES_USER=zitadel + - PGUSER=zitadel + - POSTGRES_DB=zitadel + - POSTGRES_HOST_AUTH_METHOD=trust + command: postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=all -c shared_buffers=1GB -c work_mem=16MB -c effective_io_concurrency=100 -c wal_level=minimal -c archive_mode=off -c max_wal_senders=0 healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9090/health?ready=1"] + test: [ "CMD-SHELL", "pg_isready" ] interval: "10s" timeout: "30s" retries: 5 start_period: "20s" ports: - - "26257:26257" - - "9090:9090" + - 5432:5432 wait_for_zitadel: image: curlimages/curl:8.00.1 - command: - [ - "/bin/sh", - "-c", - "i=0; while ! curl http://zitadel:8080/debug/ready && [ $$i -lt 30 ]; do sleep 1; i=$$((i+1)); done; [ $$i -eq 30 ] && exit 1 || exit 0", - ] + command: /bin/sh -c "until curl -s -o /dev/null -i -f http://zitadel:8080/debug/ready; do echo 'waiting' && sleep 1; done; echo 'ready' && sleep 5;" || false depends_on: - zitadel @@ -43,12 +41,29 @@ services: container_name: setup build: . environment: - KEY: /key/zitadel-admin-sa.json - SERVICE: http://zitadel:8080 - WRITE_ENVIRONMENT_FILE: /apps/login/.env.acceptance + PAT_FILE: /pat/zitadel-admin-sa.pat + ZITADEL_API_INTERNAL_URL: http://zitadel:8080 + WRITE_ENVIRONMENT_FILE: /apps/login/.env.local + WRITE_TEST_ENVIRONMENT_FILE: /acceptance/tests/.env.local + SINK_EMAIL_INTERNAL_URL: http://sink:3333/email + SINK_SMS_INTERNAL_URL: http://sink:3333/sms + SINK_NOTIFICATION_URL: http://localhost:3333/notification volumes: - - "./machinekey:/key" + - "./pat:/pat" - "../apps/login:/apps/login" + - "../acceptance/tests:/acceptance/tests" depends_on: wait_for_zitadel: condition: "service_completed_successfully" + + sink: + image: golang:1.19-alpine + container_name: sink + command: go run /sink/main.go -port '3333' -email '/email' -sms '/sms' -notification '/notification' + ports: + - 3333:3333 + volumes: + - "./sink:/sink" + depends_on: + setup: + condition: "service_completed_successfully" diff --git a/acceptance/machinekey/.gitignore b/acceptance/machinekey/.gitignore deleted file mode 100644 index 7c9f54d04c..0000000000 --- a/acceptance/machinekey/.gitignore +++ /dev/null @@ -1 +0,0 @@ -zitadel-admin-sa.json diff --git a/acceptance/machinekey/.kitkeep b/acceptance/machinekey/.kitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/acceptance/pat/.gitignore b/acceptance/pat/.gitignore new file mode 100644 index 0000000000..f0fa09f556 --- /dev/null +++ b/acceptance/pat/.gitignore @@ -0,0 +1,2 @@ +* +!.gitkeep \ No newline at end of file diff --git a/acceptance/machinekey/.gitkeep b/acceptance/pat/.gitkeep similarity index 100% rename from acceptance/machinekey/.gitkeep rename to acceptance/pat/.gitkeep diff --git a/acceptance/setup.sh b/acceptance/setup.sh index d3db4f2554..e6277854a7 100755 --- a/acceptance/setup.sh +++ b/acceptance/setup.sh @@ -1,125 +1,112 @@ #!/bin/sh -set -e +set -ex -KEY=${KEY:-./machinekey/zitadel-admin-sa.json} -echo "Using key path ${KEY} to the instance admin service account." +PAT_FILE=${PAT_FILE:-./pat/zitadel-admin-sa.pat} +ZITADEL_API_PROTOCOL="${ZITADEL_API_PROTOCOL:-http}" +ZITADEL_API_DOMAIN="${ZITADEL_API_DOMAIN:-localhost}" +ZITADEL_API_PORT="${ZITADEL_API_PORT:-8080}" +ZITADEL_API_URL="${ZITADEL_API_URL:-${ZITADEL_API_PROTOCOL}://${ZITADEL_API_DOMAIN}:${ZITADEL_API_PORT}}" +ZITADEL_API_INTERNAL_URL="${ZITADEL_API_INTERNAL_URL:-${ZITADEL_API_URL}}" +SINK_EMAIL_INTERNAL_URL="${SINK_EMAIL_INTERNAL_URL:-"http://sink:3333/email"}" +SINK_SMS_INTERNAL_URL="${SINK_SMS_INTERNAL_URL:-"http://sink:3333/sms"}" +SINK_NOTIFICATION_URL="${SINK_NOTIFICATION_URL:-"http://localhost:3333/notification"}" -AUDIENCE=${AUDIENCE:-http://localhost:8080} -echo "Using audience ${AUDIENCE} for which the key is used." +if [ -z "${PAT}" ]; then + echo "Reading PAT from file ${PAT_FILE}" + PAT=$(cat ${PAT_FILE}) +fi -SERVICE=${SERVICE:-$AUDIENCE} -echo "Using the service ${SERVICE} to connect to ZITADEL. For example in docker compose this can differ from the audience." +if [ -z "${ZITADEL_SERVICE_USER_ID}" ]; then + echo "Reading ZITADEL_SERVICE_USER_ID from userinfo endpoint" + USERINFO_RESPONSE=$(curl -s --request POST \ + --url "${ZITADEL_API_INTERNAL_URL}/oidc/v1/userinfo" \ + --header "Authorization: Bearer ${PAT}" \ + --header "Host: ${ZITADEL_API_DOMAIN}") + echo "Received userinfo response: ${USERINFO_RESPONSE}" + ZITADEL_SERVICE_USER_ID=$(echo "${USERINFO_RESPONSE}" | jq --raw-output '.sub') +fi -WRITE_ENVIRONMENT_FILE=${WRITE_ENVIRONMENT_FILE:-$(dirname "$0")/../apps/login/.env.acceptance} +################################################################# +# Environment files +################################################################# + +WRITE_ENVIRONMENT_FILE=${WRITE_ENVIRONMENT_FILE:-$(dirname "$0")/../apps/login/.env.local} echo "Writing environment file to ${WRITE_ENVIRONMENT_FILE} when done." +WRITE_TEST_ENVIRONMENT_FILE=${WRITE_TEST_ENVIRONMENT_FILE:-$(dirname "$0")/../acceptance/tests/.env.local} +echo "Writing environment file to ${WRITE_TEST_ENVIRONMENT_FILE} when done." -AUDIENCE_HOST="$(echo $AUDIENCE | cut -d/ -f3)" -echo "Deferred the Host header ${AUDIENCE_HOST} which will be sent in requests that ZITADEL then maps to a virtual instance" - -JWT=$(zitadel-tools key2jwt --key ${KEY} --audience ${AUDIENCE}) -echo "Created JWT from Admin service account key ${JWT}" - -TOKEN_RESPONSE=$(curl -s --request POST \ - --url ${SERVICE}/oauth/v2/token \ - --header 'Content-Type: application/x-www-form-urlencoded' \ - --header "Host: ${AUDIENCE_HOST}" \ - --data grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer \ - --data scope='openid profile email urn:zitadel:iam:org:project:id:zitadel:aud' \ - --data assertion="${JWT}") -echo "Got response from token endpoint:" -echo "${TOKEN_RESPONSE}" | jq - -TOKEN=$(echo -n ${TOKEN_RESPONSE} | jq --raw-output '.access_token') -echo "Extracted access token ${TOKEN}" - -ORG_RESPONSE=$(curl -s --request GET \ - --url ${SERVICE}/admin/v1/orgs/default \ - --header 'Accept: application/json' \ - --header "Authorization: Bearer ${TOKEN}" \ - --header "Host: ${AUDIENCE_HOST}") -echo "Got default org response:" -echo "${ORG_RESPONSE}" | jq - -ORG_ID=$(echo -n ${ORG_RESPONSE} | jq --raw-output '.org.id') -echo "Extracted default org id ${ORG_ID}" - -echo "ZITADEL_API_URL=${AUDIENCE} -ZITADEL_ORG_ID=${ORG_ID} -ZITADEL_SERVICE_USER_TOKEN=${TOKEN}" > ${WRITE_ENVIRONMENT_FILE} +echo "ZITADEL_API_URL=${ZITADEL_API_URL} +ZITADEL_SERVICE_USER_ID=${ZITADEL_SERVICE_USER_ID} +ZITADEL_SERVICE_USER_TOKEN=${PAT} +SINK_NOTIFICATION_URL=${SINK_NOTIFICATION_URL} +DEBUG=true"| tee "${WRITE_ENVIRONMENT_FILE}" "${WRITE_TEST_ENVIRONMENT_FILE}" > /dev/null echo "Wrote environment file ${WRITE_ENVIRONMENT_FILE}" cat ${WRITE_ENVIRONMENT_FILE} -if ! grep -q 'localhost' ${WRITE_ENVIRONMENT_FILE}; then - echo "Not developing against localhost, so creating a human user might not be necessary" - exit 0 -fi +echo "Wrote environment file ${WRITE_TEST_ENVIRONMENT_FILE}" +cat ${WRITE_TEST_ENVIRONMENT_FILE} -HUMAN_USER_USERNAME="zitadel-admin@zitadel.localhost" -HUMAN_USER_PASSWORD="Password1!" +################################################################# +# SMS provider with HTTP +################################################################# -HUMAN_USER_PAYLOAD=$(cat << EOM -{ - "userName": "${HUMAN_USER_USERNAME}", - "profile": { - "firstName": "ZITADEL", - "lastName": "Admin", - "displayName": "ZITADEL Admin", - "preferredLanguage": "en" - }, - "email": { - "email": "zitadel-admin@zitadel.localhost", - "isEmailVerified": true - }, - "password": "${HUMAN_USER_PASSWORD}", - "passwordChangeRequired": false -} -EOM -) -echo "Creating human user" -echo "${HUMAN_USER_PAYLOAD}" | jq +SMSHTTP_RESPONSE=$(curl -s --request POST \ + --url "${ZITADEL_API_INTERNAL_URL}/admin/v1/sms/http" \ + --header "Authorization: Bearer ${PAT}" \ + --header "Host: ${ZITADEL_API_DOMAIN}" \ + --header "Content-Type: application/json" \ + -d "{\"endpoint\": \"${SINK_SMS_INTERNAL_URL}\", \"description\": \"test\"}") +echo "Received SMS HTTP response: ${SMSHTTP_RESPONSE}" -HUMAN_USER_RESPONSE=$(curl -s --request POST \ - --url ${SERVICE}/management/v1/users/human/_import \ - --header 'Content-Type: application/json' \ - --header 'Accept: application/json' \ - --header "Authorization: Bearer ${TOKEN}" \ - --header "Host: ${AUDIENCE_HOST}" \ - --data-raw "${HUMAN_USER_PAYLOAD}") -echo "Create human user response" -echo "${HUMAN_USER_RESPONSE}" | jq +SMSHTTP_ID=$(echo ${SMSHTTP_RESPONSE} | jq -r '. | .id') +echo "Received SMS HTTP ID: ${SMSHTTP_ID}" -if [ "$(echo -n "${HUMAN_USER_RESPONSE}" | jq --raw-output '.code')" == "6" ]; then - echo "admin user already exists" - exit 0 -fi +SMS_ACTIVE_RESPONSE=$(curl -s --request POST \ + --url "${ZITADEL_API_INTERNAL_URL}/admin/v1/sms/${SMSHTTP_ID}/_activate" \ + --header "Authorization: Bearer ${PAT}" \ + --header "Host: ${ZITADEL_API_DOMAIN}" \ + --header "Content-Type: application/json") +echo "Received SMS active response: ${SMS_ACTIVE_RESPONSE}" -HUMAN_USER_ID=$(echo -n ${HUMAN_USER_RESPONSE} | jq --raw-output '.userId') -echo "Extracted human user id ${HUMAN_USER_ID}" +################################################################# +# Email provider with HTTP +################################################################# -HUMAN_ADMIN_PAYLOAD=$(cat << EOM -{ - "userId": "${HUMAN_USER_ID}", - "roles": [ - "IAM_OWNER" - ] -} -EOM -) -echo "Granting iam owner to human user" -echo "${HUMAN_ADMIN_PAYLOAD}" | jq +EMAILHTTP_RESPONSE=$(curl -s --request POST \ + --url "${ZITADEL_API_INTERNAL_URL}/admin/v1/email/http" \ + --header "Authorization: Bearer ${PAT}" \ + --header "Host: ${ZITADEL_API_DOMAIN}" \ + --header "Content-Type: application/json" \ + -d "{\"endpoint\": \"${SINK_EMAIL_INTERNAL_URL}\", \"description\": \"test\"}") +echo "Received Email HTTP response: ${EMAILHTTP_RESPONSE}" -HUMAN_ADMIN_RESPONSE=$(curl -s --request POST \ - --url ${SERVICE}/admin/v1/members \ - --header 'Content-Type: application/json' \ - --header 'Accept: application/json' \ - --header "Authorization: Bearer ${TOKEN}" \ - --header "Host: ${AUDIENCE_HOST}" \ - --data-raw "${HUMAN_ADMIN_PAYLOAD}") +EMAILHTTP_ID=$(echo ${EMAILHTTP_RESPONSE} | jq -r '. | .id') +echo "Received Email HTTP ID: ${EMAILHTTP_ID}" -echo "Grant iam owner to human user response" -echo "${HUMAN_ADMIN_RESPONSE}" | jq +EMAIL_ACTIVE_RESPONSE=$(curl -s --request POST \ + --url "${ZITADEL_API_INTERNAL_URL}/admin/v1/email/${EMAILHTTP_ID}/_activate" \ + --header "Authorization: Bearer ${PAT}" \ + --header "Host: ${ZITADEL_API_DOMAIN}" \ + --header "Content-Type: application/json") +echo "Received Email active response: ${EMAIL_ACTIVE_RESPONSE}" + +################################################################# +# Wait for projection of default organization in ZITADEL +################################################################# + +DEFAULTORG_RESPONSE_RESULTS=0 +# waiting for default organization +until [ ${DEFAULTORG_RESPONSE_RESULTS} -eq 1 ] +do + DEFAULTORG_RESPONSE=$(curl -s --request POST \ + --url "${ZITADEL_API_INTERNAL_URL}/v2/organizations/_search" \ + --header "Authorization: Bearer ${PAT}" \ + --header "Host: ${ZITADEL_API_DOMAIN}" \ + --header "Content-Type: application/json" \ + -d "{\"queries\": [{\"defaultQuery\":{}}]}" ) + echo "Received default organization response: ${DEFAULTORG_RESPONSE}" + DEFAULTORG_RESPONSE_RESULTS=$(echo $DEFAULTORG_RESPONSE | jq -r '.result | length') + echo "Received default organization response result: ${DEFAULTORG_RESPONSE_RESULTS}" +done -echo "You can now log in at ${AUDIENCE}/ui/login" -echo "username: ${HUMAN_USER_USERNAME}" -echo "password: ${HUMAN_USER_PASSWORD}" \ No newline at end of file diff --git a/acceptance/sink/go.mod b/acceptance/sink/go.mod new file mode 100644 index 0000000000..a33d6ae8bd --- /dev/null +++ b/acceptance/sink/go.mod @@ -0,0 +1,3 @@ +module github.com/zitadel/typescript/acceptance/sink + +go 1.22.6 diff --git a/acceptance/sink/main.go b/acceptance/sink/main.go new file mode 100644 index 0000000000..d591981a34 --- /dev/null +++ b/acceptance/sink/main.go @@ -0,0 +1,104 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io" + "net/http" +) + +type serializableData struct { + ContextInfo map[string]interface{} `json:"contextInfo,omitempty"` + Args map[string]interface{} `json:"args,omitempty"` +} + +type response struct { + Recipient string `json:"recipient,omitempty"` +} + +func main() { + port := flag.String("port", "3333", "used port for the sink") + email := flag.String("email", "/email", "path for a sent email") + emailKey := flag.String("email-key", "recipientEmailAddress", "value in the sent context info of the email used as key to retrieve the notification") + sms := flag.String("sms", "/sms", "path for a sent sms") + smsKey := flag.String("sms-key", "recipientPhoneNumber", "value in the sent context info of the sms used as key to retrieve the notification") + notification := flag.String("notification", "/notification", "path to receive the notification") + flag.Parse() + + messages := make(map[string]serializableData) + + http.HandleFunc(*email, func(w http.ResponseWriter, r *http.Request) { + data, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + serializableData := serializableData{} + if err := json.Unmarshal(data, &serializableData); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + email, ok := serializableData.ContextInfo[*emailKey].(string) + if !ok { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + fmt.Println(email + ": " + string(data)) + messages[email] = serializableData + io.WriteString(w, "Email!\n") + }) + + http.HandleFunc(*sms, func(w http.ResponseWriter, r *http.Request) { + data, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + serializableData := serializableData{} + if err := json.Unmarshal(data, &serializableData); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + phone, ok := serializableData.ContextInfo[*smsKey].(string) + if !ok { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + fmt.Println(phone + ": " + string(data)) + messages[phone] = serializableData + io.WriteString(w, "SMS!\n") + }) + + http.HandleFunc(*notification, func(w http.ResponseWriter, r *http.Request) { + data, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + response := response{} + if err := json.Unmarshal(data, &response); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + serializableData, err := json.Marshal(messages[response.Recipient]) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + io.WriteString(w, string(serializableData)) + }) + + fmt.Println("Starting server on", *port) + fmt.Println(*email, " for email handling") + fmt.Println(*sms, " for sms handling") + fmt.Println(*notification, " for retrieving notifications") + err := http.ListenAndServe(":"+*port, nil) + if err != nil { + panic("Server could not be started: " + err.Error()) + } +} diff --git a/acceptance/tests/admin.spec.ts b/acceptance/tests/admin.spec.ts new file mode 100644 index 0000000000..7ca28e4419 --- /dev/null +++ b/acceptance/tests/admin.spec.ts @@ -0,0 +1,7 @@ +import { test } from "@playwright/test"; +import { loginScreenExpect, loginWithPassword } from "./login"; + +test("admin login", async ({ page }) => { + await loginWithPassword(page, "zitadel-admin@zitadel.localhost", "Password1!"); + await loginScreenExpect(page, "ZITADEL Admin"); +}); diff --git a/acceptance/tests/code-screen.ts b/acceptance/tests/code-screen.ts new file mode 100644 index 0000000000..3ab9dad26d --- /dev/null +++ b/acceptance/tests/code-screen.ts @@ -0,0 +1,12 @@ +import { expect, Page } from "@playwright/test"; + +const codeTextInput = "code-text-input"; + +export async function codeScreen(page: Page, code: string) { + await page.getByTestId(codeTextInput).pressSequentially(code); +} + +export async function codeScreenExpect(page: Page, code: string) { + await expect(page.getByTestId(codeTextInput)).toHaveValue(code); + await expect(page.getByTestId("error").locator("div")).toContainText("Could not verify OTP code"); +} diff --git a/acceptance/tests/code.ts b/acceptance/tests/code.ts new file mode 100644 index 0000000000..1ae8f69791 --- /dev/null +++ b/acceptance/tests/code.ts @@ -0,0 +1,19 @@ +import { Page } from "@playwright/test"; +import { codeScreen } from "./code-screen"; +import { getOtpFromSink } from "./sink"; + +export async function otpFromSink(page: Page, key: string) { + // wait for send of the code + await page.waitForTimeout(3000); + const c = await getOtpFromSink(key); + await code(page, c); +} + +export async function code(page: Page, code: string) { + await codeScreen(page, code); + await page.getByTestId("submit-button").click(); +} + +export async function codeResend(page: Page) { + await page.getByTestId("resend-button").click(); +} diff --git a/acceptance/tests/idp-apple.spec.ts b/acceptance/tests/idp-apple.spec.ts new file mode 100644 index 0000000000..89ed734ece --- /dev/null +++ b/acceptance/tests/idp-apple.spec.ts @@ -0,0 +1,94 @@ +// Note for all tests, in case Apple doesn't deliver all relevant information per default +// We should add an action in the needed cases + +import test from "@playwright/test"; + +test("login with Apple IDP", async ({ page }) => { + // Given an Apple IDP is configured on the organization + // Given the user has an Apple added as auth method + // User authenticates with Apple + // User is redirected back to login + // User is redirected to the app +}); + +test("login with Apple IDP - error", async ({ page }) => { + // Given an Apple IDP is configured on the organization + // Given the user has an Apple added as auth method + // User is redirected to Apple + // User authenticates with Apple and gets an error + // User is redirect back to login + // An error is shown to the user "Something went wrong in Apple Login" +}); + +test("login with Apple IDP, no user existing - auto register", async ({ page }) => { + // Given idp Apple is configure on the organization as only authencation method + // Given idp Apple is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Apple + // User authenticates in Apple + // User is redirect to ZITADEL login + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Apple IDP, no user existing - auto register not possible", async ({ page }) => { + // Given idp Apple is configure on the organization as only authencation method + // Given idp Apple is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Apple + // User authenticates in Apple + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // User will see the registration page with pre filled user information + // User fills missing information + // User clicks register button + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Apple IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({ + page, +}) => { + // Given idp Apple is configure on the organization as only authencation method + // Given idp Apple is configure with account creation not allowed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Apple + // User authenticates in Apple + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // Error message is shown, that registration of the user was not possible due to missing information +}); + +test("login with Apple IDP, no user linked - auto link", async ({ page }) => { + // Given idp Apple is configure on the organization as only authencation method + // Given idp Apple is configure with account linking allowed, and linking set to existing email + // Given user with email address user@zitadel.com exists + // User is automatically redirected to Apple + // User authenticates in Apple with user@zitadel.com + // User is redirect to ZITADEL login + // User is linked with existing user in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Apple IDP, no user linked, linking not possible", async ({ page }) => { + // Given idp Apple is configure on the organization as only authencation method + // Given idp Apple is configure with manually account linking not allowed, and linking set to existing email + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Apple + // User authenticates in Apple with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User will get an error message that account linking wasn't possible +}); + +test("login with Apple IDP, no user linked, user link successful", async ({ page }) => { + // Given idp Apple is configure on the organization as only authencation method + // Given idp Apple is configure with manually account linking allowed, and linking set to existing email + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Apple + // User authenticates in Apple with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User is prompted to link the account manually + // User is redirected to the app (default redirect url) +}); diff --git a/acceptance/tests/idp-generic-jwt.spec.ts b/acceptance/tests/idp-generic-jwt.spec.ts new file mode 100644 index 0000000000..054c147844 --- /dev/null +++ b/acceptance/tests/idp-generic-jwt.spec.ts @@ -0,0 +1,91 @@ +import test from "@playwright/test"; + +test("login with Generic JWT IDP", async ({ page }) => { + // Given a Generic JWT IDP is configured on the organization + // Given the user has Generic JWT IDP added as auth method + // User authenticates with the Generic JWT IDP + // User is redirected back to login + // User is redirected to the app +}); + +test("login with Generic JWT IDP - error", async ({ page }) => { + // Given the Generic JWT IDP is configured on the organization + // Given the user has Generic JWT IDP added as auth method + // User is redirected to the Generic JWT IDP + // User authenticates with the Generic JWT IDP and gets an error + // User is redirected back to login + // An error is shown to the user "Something went wrong" +}); + +test("login with Generic JWT IDP, no user existing - auto register", async ({ page }) => { + // Given idp Generic JWT is configure on the organization as only authencation method + // Given idp Generic JWT is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Generic JWT + // User authenticates in Generic JWT + // User is redirect to ZITADEL login + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Generic JWT IDP, no user existing - auto register not possible", async ({ page }) => { + // Given idp Generic JWT is configure on the organization as only authencation method + // Given idp Generic JWT is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Generic JWT + // User authenticates in Generic JWT + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // User will see the registration page with pre filled user information + // User fills missing information + // User clicks register button + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Generic JWT IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({ + page, +}) => { + // Given idp Generic JWT is configure on the organization as only authencation method + // Given idp Generic JWT is configure with account creation not allowed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Generic JWT + // User authenticates in Generic JWT + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // Error message is shown, that registration of the user was not possible due to missing information +}); + +test("login with Generic JWT IDP, no user linked - auto link", async ({ page }) => { + // Given idp Generic JWT is configure on the organization as only authencation method + // Given idp Generic JWT is configure with account linking allowed, and linking set to existing email + // Given user with email address user@zitadel.com exists + // User is automatically redirected to Generic JWT + // User authenticates in Generic JWT with user@zitadel.com + // User is redirect to ZITADEL login + // User is linked with existing user in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Generic JWT IDP, no user linked, linking not possible", async ({ page }) => { + // Given idp Generic JWT is configure on the organization as only authencation method + // Given idp Generic JWT is configure with manually account linking not allowed, and linking set to existing email + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Generic JWT + // User authenticates in Generic JWT with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User will get an error message that account linking wasn't possible +}); + +test("login with Generic JWT IDP, no user linked, linking successful", async ({ page }) => { + // Given idp Generic JWT is configure on the organization as only authencation method + // Given idp Generic JWT is configure with manually account linking allowed, and linking set to existing email + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Generic JWT + // User authenticates in Generic JWT with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User is prompted to link the account manually + // User is redirected to the app (default redirect url) +}); diff --git a/acceptance/tests/idp-generic-oauth.spec.ts b/acceptance/tests/idp-generic-oauth.spec.ts new file mode 100644 index 0000000000..7973e67c14 --- /dev/null +++ b/acceptance/tests/idp-generic-oauth.spec.ts @@ -0,0 +1,91 @@ +import test from "@playwright/test"; + +test("login with Generic OAuth IDP", async ({ page }) => { + // Given a Generic OAuth IDP is configured on the organization + // Given the user has Generic OAuth IDP added as auth method + // User authenticates with the Generic OAuth IDP + // User is redirected back to login + // User is redirected to the app +}); + +test("login with Generic OAuth IDP - error", async ({ page }) => { + // Given the Generic OAuth IDP is configured on the organization + // Given the user has Generic OAuth IDP added as auth method + // User is redirected to the Generic OAuth IDP + // User authenticates with the Generic OAuth IDP and gets an error + // User is redirected back to login + // An error is shown to the user "Something went wrong" +}); + +test("login with Generic OAuth IDP, no user existing - auto register", async ({ page }) => { + // Given idp Generic OAuth is configure on the organization as only authencation method + // Given idp Generic OAuth is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Generic OAuth + // User authenticates in Generic OAuth + // User is redirect to ZITADEL login + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Generic OAuth IDP, no user existing - auto register not possible", async ({ page }) => { + // Given idp Generic OAuth is configure on the organization as only authencation method + // Given idp Generic OAuth is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Generic OAuth + // User authenticates in Generic OAuth + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // User will see the registration page with pre filled user information + // User fills missing information + // User clicks register button + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Generic OAuth IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({ + page, +}) => { + // Given idp Generic OAuth is configure on the organization as only authencation method + // Given idp Generic OAuth is configure with account creation not allowed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Generic OAuth + // User authenticates in Generic OAuth + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // Error message is shown, that registration of the user was not possible due to missing information +}); + +test("login with Generic OAuth IDP, no user linked - auto link", async ({ page }) => { + // Given idp Generic OAuth is configure on the organization as only authencation method + // Given idp Generic OAuth is configure with account linking allowed, and linking set to existing email + // Given user with email address user@zitadel.com exists + // User is automatically redirected to Generic OAuth + // User authenticates in Generic OAuth with user@zitadel.com + // User is redirect to ZITADEL login + // User is linked with existing user in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Generic OAuth IDP, no user linked, linking not possible", async ({ page }) => { + // Given idp Generic OAuth is configure on the organization as only authencation method + // Given idp Generic OAuth is configure with manually account linking not allowed, and linking set to existing email + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Generic OAuth + // User authenticates in Generic OAuth with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User will get an error message that account linking wasn't possible +}); + +test("login with Generic OAuth IDP, no user linked, linking successful", async ({ page }) => { + // Given idp Generic OAuth is configure on the organization as only authencation method + // Given idp Generic OAuth is configure with manually account linking allowed, and linking set to existing email + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Generic OAuth + // User authenticates in Generic OAuth with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User is prompted to link the account manually + // User is redirected to the app (default redirect url) +}); diff --git a/acceptance/tests/idp-generic-oidc.spec.ts b/acceptance/tests/idp-generic-oidc.spec.ts new file mode 100644 index 0000000000..4ed536f613 --- /dev/null +++ b/acceptance/tests/idp-generic-oidc.spec.ts @@ -0,0 +1,93 @@ +// Note, we should use a provider such as Google to test this, where we know OIDC standard is properly implemented + +import test from "@playwright/test"; + +test("login with Generic OIDC IDP", async ({ page }) => { + // Given a Generic OIDC IDP is configured on the organization + // Given the user has Generic OIDC IDP added as auth method + // User authenticates with the Generic OIDC IDP + // User is redirected back to login + // User is redirected to the app +}); + +test("login with Generic OIDC IDP - error", async ({ page }) => { + // Given the Generic OIDC IDP is configured on the organization + // Given the user has Generic OIDC IDP added as auth method + // User is redirected to the Generic OIDC IDP + // User authenticates with the Generic OIDC IDP and gets an error + // User is redirected back to login + // An error is shown to the user "Something went wrong" +}); + +test("login with Generic OIDC IDP, no user existing - auto register", async ({ page }) => { + // Given idp Generic OIDC is configure on the organization as only authencation method + // Given idp Generic OIDC is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Generic OIDC + // User authenticates in Generic OIDC + // User is redirect to ZITADEL login + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Generic OIDC IDP, no user existing - auto register not possible", async ({ page }) => { + // Given idp Generic OIDC is configure on the organization as only authencation method + // Given idp Generic OIDC is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Generic OIDC + // User authenticates in Generic OIDC + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // User will see the registration page with pre filled user information + // User fills missing information + // User clicks register button + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Generic OIDC IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({ + page, +}) => { + // Given idp Generic OIDC is configure on the organization as only authencation method + // Given idp Generic OIDC is configure with account creation not allowed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Generic OIDC + // User authenticates in Generic OIDC + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // Error message is shown, that registration of the user was not possible due to missing information +}); + +test("login with Generic OIDC IDP, no user linked - auto link", async ({ page }) => { + // Given idp Generic OIDC is configure on the organization as only authencation method + // Given idp Generic OIDC is configure with account linking allowed, and linking set to existing email + // Given user with email address user@zitadel.com exists + // User is automatically redirected to Generic OIDC + // User authenticates in Generic OIDC with user@zitadel.com + // User is redirect to ZITADEL login + // User is linked with existing user in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Generic OIDC IDP, no user linked, linking not possible", async ({ page }) => { + // Given idp Generic OIDC is configure on the organization as only authencation method + // Given idp Generic OIDC is configure with manually account linking not allowed, and linking set to existing email + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Generic OIDC + // User authenticates in Generic OIDC with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User will get an error message that account linking wasn't possible +}); + +test("login with Generic OIDC IDP, no user linked, linking successful", async ({ page }) => { + // Given idp Generic OIDC is configure on the organization as only authencation method + // Given idp Generic OIDC is configure with manually account linking allowed, and linking set to existing email + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Generic OIDC + // User authenticates in Generic OIDC with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User is prompted to link the account manually + // User is redirected to the app (default redirect url) +}); diff --git a/acceptance/tests/idp-github-enterprise.spec.ts b/acceptance/tests/idp-github-enterprise.spec.ts new file mode 100644 index 0000000000..0a567c444b --- /dev/null +++ b/acceptance/tests/idp-github-enterprise.spec.ts @@ -0,0 +1,95 @@ +import test from "@playwright/test"; + +test("login with GitHub Enterprise IDP", async ({ page }) => { + // Given a GitHub Enterprise IDP is configured on the organization + // Given the user has GitHub Enterprise IDP added as auth method + // User authenticates with the GitHub Enterprise IDP + // User is redirected back to login + // User is redirected to the app +}); + +test("login with GitHub Enterprise IDP - error", async ({ page }) => { + // Given the GitHub Enterprise IDP is configured on the organization + // Given the user has GitHub Enterprise IDP added as auth method + // User is redirected to the GitHub Enterprise IDP + // User authenticates with the GitHub Enterprise IDP and gets an error + // User is redirected back to login + // An error is shown to the user "Something went wrong" +}); + +test("login with GitHub Enterprise IDP, no user existing - auto register", async ({ page }) => { + // Given idp GitHub Enterprise is configure on the organization as only authencation method + // Given idp GitHub Enterprise is configure with account creation alloweed, and automatic creation enabled + // Given ZITADEL Action is added to autofill missing user information + // Given no user exists yet + // User is automatically redirected to GitHub Enterprise + // User authenticates in GitHub Enterprise + // User is redirect to ZITADEL login + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with GitHub Enterprise IDP, no user existing - auto register not possible", async ({ page }) => { + // Given idp GitHub Enterprise is configure on the organization as only authencation method + // Given idp GitHub Enterprise is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to GitHub Enterprise + // User authenticates in GitHub Enterprise + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // User will see the registration page with pre filled user information + // User fills missing information + // User clicks register button + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with GitHub Enterprise IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({ + page, +}) => { + // Given idp GitHub Enterprise is configure on the organization as only authencation method + // Given idp GitHub Enterprise is configure with account creation not allowed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to GitHub Enterprise + // User authenticates in GitHub Enterprise + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // Error message is shown, that registration of the user was not possible due to missing information +}); + +test("login with GitHub Enterprise IDP, no user linked - auto link", async ({ page }) => { + // Given idp GitHub Enterprise is configure on the organization as only authencation method + // Given idp GitHub Enterprise is configure with account linking allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com exists + // User is automatically redirected to GitHub Enterprise + // User authenticates in GitHub Enterprise with user@zitadel.com + // User is redirect to ZITADEL login + // User is linked with existing user in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with GitHub Enterprise IDP, no user linked, linking not possible", async ({ page }) => { + // Given idp GitHub Enterprise is configure on the organization as only authencation method + // Given idp GitHub Enterprise is configure with manually account linking not allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to GitHub Enterprise + // User authenticates in GitHub Enterprise with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User will get an error message that account linking wasn't possible +}); + +test("login with GitHub Enterprise IDP, no user linked, linking successful", async ({ page }) => { + // Given idp GitHub Enterprise is configure on the organization as only authencation method + // Given idp GitHub Enterprise is configure with manually account linking allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to GitHub Enterprise + // User authenticates in GitHub Enterprise with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User is prompted to link the account manually + // User is redirected to the app (default redirect url) +}); diff --git a/acceptance/tests/idp-github.spec.ts b/acceptance/tests/idp-github.spec.ts new file mode 100644 index 0000000000..be36a542f0 --- /dev/null +++ b/acceptance/tests/idp-github.spec.ts @@ -0,0 +1,95 @@ +import test from "@playwright/test"; + +test("login with GitHub IDP", async ({ page }) => { + // Given a GitHub IDP is configured on the organization + // Given the user has GitHub IDP added as auth method + // User authenticates with the GitHub IDP + // User is redirected back to login + // User is redirected to the app +}); + +test("login with GitHub IDP - error", async ({ page }) => { + // Given the GitHub IDP is configured on the organization + // Given the user has GitHub IDP added as auth method + // User is redirected to the GitHub IDP + // User authenticates with the GitHub IDP and gets an error + // User is redirected back to login + // An error is shown to the user "Something went wrong" +}); + +test("login with GitHub IDP, no user existing - auto register", async ({ page }) => { + // Given idp GitHub is configure on the organization as only authencation method + // Given idp GitHub is configure with account creation alloweed, and automatic creation enabled + // Given ZITADEL Action is added to autofill missing user information + // Given no user exists yet + // User is automatically redirected to GitHub + // User authenticates in GitHub + // User is redirect to ZITADEL login + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with GitHub IDP, no user existing - auto register not possible", async ({ page }) => { + // Given idp GitHub is configure on the organization as only authencation method + // Given idp GitHub is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to GitHub + // User authenticates in GitHub + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // User will see the registration page with pre filled user information + // User fills missing information + // User clicks register button + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with GitHub IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({ + page, +}) => { + // Given idp GitHub is configure on the organization as only authencation method + // Given idp GitHub is configure with account creation not allowed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to GitHub + // User authenticates in GitHub + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // Error message is shown, that registration of the user was not possible due to missing information +}); + +test("login with GitHub IDP, no user linked - auto link", async ({ page }) => { + // Given idp GitHub is configure on the organization as only authencation method + // Given idp GitHub is configure with account linking allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com exists + // User is automatically redirected to GitHub + // User authenticates in GitHub with user@zitadel.com + // User is redirect to ZITADEL login + // User is linked with existing user in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with GitHub IDP, no user linked, linking not possible", async ({ page }) => { + // Given idp GitHub is configure on the organization as only authencation method + // Given idp GitHub is configure with manually account linking not allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to GitHub + // User authenticates in GitHub with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User will get an error message that account linking wasn't possible +}); + +test("login with GitHub IDP, no user linked, linking successful", async ({ page }) => { + // Given idp GitHub is configure on the organization as only authencation method + // Given idp GitHub is configure with manually account linking allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to GitHub + // User authenticates in GitHub with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User is prompted to link the account manually + // User is redirected to the app (default redirect url) +}); diff --git a/acceptance/tests/idp-gitlab-self-hosted.spec.ts b/acceptance/tests/idp-gitlab-self-hosted.spec.ts new file mode 100644 index 0000000000..d254062e2a --- /dev/null +++ b/acceptance/tests/idp-gitlab-self-hosted.spec.ts @@ -0,0 +1,95 @@ +import test from "@playwright/test"; + +test("login with GitLab Self-Hosted IDP", async ({ page }) => { + // Given a GitLab Self-Hosted IDP is configured on the organization + // Given the user has GitLab Self-Hosted IDP added as auth method + // User authenticates with the GitLab Self-Hosted IDP + // User is redirected back to login + // User is redirected to the app +}); + +test("login with GitLab Self-Hosted IDP - error", async ({ page }) => { + // Given the GitLab Self-Hosted IDP is configured on the organization + // Given the user has GitLab Self-Hosted IDP added as auth method + // User is redirected to the GitLab Self-Hosted IDP + // User authenticates with the GitLab Self-Hosted IDP and gets an error + // User is redirected back to login + // An error is shown to the user "Something went wrong" +}); + +test("login with Gitlab Self-Hosted IDP, no user existing - auto register", async ({ page }) => { + // Given idp Gitlab Self-Hosted is configure on the organization as only authencation method + // Given idp Gitlab Self-Hosted is configure with account creation alloweed, and automatic creation enabled + // Given ZITADEL Action is added to autofill missing user information + // Given no user exists yet + // User is automatically redirected to Gitlab Self-Hosted + // User authenticates in Gitlab Self-Hosted + // User is redirect to ZITADEL login + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Gitlab Self-Hosted IDP, no user existing - auto register not possible", async ({ page }) => { + // Given idp Gitlab Self-Hosted is configure on the organization as only authencation method + // Given idp Gitlab Self-Hosted is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Gitlab Self-Hosted + // User authenticates in Gitlab Self-Hosted + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // User will see the registration page with pre filled user information + // User fills missing information + // User clicks register button + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Gitlab Self-Hosted IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({ + page, +}) => { + // Given idp Gitlab Self-Hosted is configure on the organization as only authencation method + // Given idp Gitlab Self-Hosted is configure with account creation not allowed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Gitlab Self-Hosted + // User authenticates in Gitlab Self-Hosted + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // Error message is shown, that registration of the user was not possible due to missing information +}); + +test("login with Gitlab Self-Hosted IDP, no user linked - auto link", async ({ page }) => { + // Given idp Gitlab Self-Hosted is configure on the organization as only authencation method + // Given idp Gitlab Self-Hosted is configure with account linking allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com exists + // User is automatically redirected to Gitlab Self-Hosted + // User authenticates in Gitlab Self-Hosted with user@zitadel.com + // User is redirect to ZITADEL login + // User is linked with existing user in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Gitlab Self-Hosted IDP, no user linked, linking not possible", async ({ page }) => { + // Given idp Gitlab Self-Hosted is configure on the organization as only authencation method + // Given idp Gitlab Self-Hosted is configure with manually account linking not allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Gitlab Self-Hosted + // User authenticates in Gitlab Self-Hosted with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User will get an error message that account linking wasn't possible +}); + +test("login with Gitlab Self-Hosted IDP, no user linked, linking successful", async ({ page }) => { + // Given idp Gitlab Self-Hosted is configure on the organization as only authencation method + // Given idp Gitlab Self-Hosted is configure with manually account linking allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Gitlab Self-Hosted + // User authenticates in Gitlab Self-Hosted with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User is prompted to link the account manually + // User is redirected to the app (default redirect url) +}); diff --git a/acceptance/tests/idp-gitlab.spec.ts b/acceptance/tests/idp-gitlab.spec.ts new file mode 100644 index 0000000000..e38b70a1e1 --- /dev/null +++ b/acceptance/tests/idp-gitlab.spec.ts @@ -0,0 +1,95 @@ +import test from "@playwright/test"; + +test("login with GitLab IDP", async ({ page }) => { + // Given a GitLab IDP is configured on the organization + // Given the user has GitLab IDP added as auth method + // User authenticates with the GitLab IDP + // User is redirected back to login + // User is redirected to the app +}); + +test("login with GitLab IDP - error", async ({ page }) => { + // Given the GitLab IDP is configured on the organization + // Given the user has GitLab IDP added as auth method + // User is redirected to the GitLab IDP + // User authenticates with the GitLab IDP and gets an error + // User is redirected back to login + // An error is shown to the user "Something went wrong" +}); + +test("login with Gitlab IDP, no user existing - auto register", async ({ page }) => { + // Given idp Gitlab is configure on the organization as only authencation method + // Given idp Gitlab is configure with account creation alloweed, and automatic creation enabled + // Given ZITADEL Action is added to autofill missing user information + // Given no user exists yet + // User is automatically redirected to Gitlab + // User authenticates in Gitlab + // User is redirect to ZITADEL login + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Gitlab IDP, no user existing - auto register not possible", async ({ page }) => { + // Given idp Gitlab is configure on the organization as only authencation method + // Given idp Gitlab is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Gitlab + // User authenticates in Gitlab + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // User will see the registration page with pre filled user information + // User fills missing information + // User clicks register button + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Gitlab IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({ + page, +}) => { + // Given idp Gitlab is configure on the organization as only authencation method + // Given idp Gitlab is configure with account creation not allowed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Gitlab + // User authenticates in Gitlab + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // Error message is shown, that registration of the user was not possible due to missing information +}); + +test("login with Gitlab IDP, no user linked - auto link", async ({ page }) => { + // Given idp Gitlab is configure on the organization as only authencation method + // Given idp Gitlab is configure with account linking allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com exists + // User is automatically redirected to Gitlab + // User authenticates in Gitlab with user@zitadel.com + // User is redirect to ZITADEL login + // User is linked with existing user in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Gitlab IDP, no user linked, linking not possible", async ({ page }) => { + // Given idp Gitlab is configure on the organization as only authencation method + // Given idp Gitlab is configure with manually account linking not allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Gitlab + // User authenticates in Gitlab with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User will get an error message that account linking wasn't possible +}); + +test("login with Gitlab IDP, no user linked, linking successful", async ({ page }) => { + // Given idp Gitlab is configure on the organization as only authencation method + // Given idp Gitlab is configure with manually account linking allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Gitlab + // User authenticates in Gitlab with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User is prompted to link the account manually + // User is redirected to the app (default redirect url) +}); diff --git a/acceptance/tests/idp-google.spec.ts b/acceptance/tests/idp-google.spec.ts new file mode 100644 index 0000000000..c6219722a9 --- /dev/null +++ b/acceptance/tests/idp-google.spec.ts @@ -0,0 +1,91 @@ +import test from "@playwright/test"; + +test("login with Google IDP", async ({ page }) => { + // Given a Google IDP is configured on the organization + // Given the user has Google IDP added as auth method + // User authenticates with the Google IDP + // User is redirected back to login + // User is redirected to the app +}); + +test("login with Google IDP - error", async ({ page }) => { + // Given the Google IDP is configured on the organization + // Given the user has Google IDP added as auth method + // User is redirected to the Google IDP + // User authenticates with the Google IDP and gets an error + // User is redirected back to login + // An error is shown to the user "Something went wrong" +}); + +test("login with Google IDP, no user existing - auto register", async ({ page }) => { + // Given idp Google is configure on the organization as only authencation method + // Given idp Google is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Google + // User authenticates in Google + // User is redirect to ZITADEL login + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Google IDP, no user existing - auto register not possible", async ({ page }) => { + // Given idp Google is configure on the organization as only authencation method + // Given idp Google is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Google + // User authenticates in Google + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // User will see the registration page with pre filled user information + // User fills missing information + // User clicks register button + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Google IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({ + page, +}) => { + // Given idp Google is configure on the organization as only authencation method + // Given idp Google is configure with account creation not allowed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Google + // User authenticates in Google + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // Error message is shown, that registration of the user was not possible due to missing information +}); + +test("login with Google IDP, no user linked - auto link", async ({ page }) => { + // Given idp Google is configure on the organization as only authencation method + // Given idp Google is configure with account linking allowed, and linking set to existing email + // Given user with email address user@zitadel.com exists + // User is automatically redirected to Google + // User authenticates in Google with user@zitadel.com + // User is redirect to ZITADEL login + // User is linked with existing user in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Google IDP, no user linked, linking not possible", async ({ page }) => { + // Given idp Google is configure on the organization as only authencation method + // Given idp Google is configure with manually account linking not allowed, and linking set to existing email + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Google + // User authenticates in Google with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User will get an error message that account linking wasn't possible +}); + +test("login with Google IDP, no user linked, linking successful", async ({ page }) => { + // Given idp Google is configure on the organization as only authencation method + // Given idp Google is configure with manually account linking allowed, and linking set to existing email + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Google + // User authenticates in Google with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User is prompted to link the account manually + // User is redirected to the app (default redirect url) +}); diff --git a/acceptance/tests/idp-ldap.spec.ts b/acceptance/tests/idp-ldap.spec.ts new file mode 100644 index 0000000000..9ab0310fa0 --- /dev/null +++ b/acceptance/tests/idp-ldap.spec.ts @@ -0,0 +1,91 @@ +import test from "@playwright/test"; + +test("login with LDAP IDP", async ({ page }) => { + // Given a LDAP IDP is configured on the organization + // Given the user has LDAP IDP added as auth method + // User authenticates with the LDAP IDP + // User is redirected back to login + // User is redirected to the app +}); + +test("login with LDAP IDP - error", async ({ page }) => { + // Given the LDAP IDP is configured on the organization + // Given the user has LDAP IDP added as auth method + // User is redirected to the LDAP IDP + // User authenticates with the LDAP IDP and gets an error + // User is redirected back to login + // An error is shown to the user "Something went wrong" +}); + +test("login with LDAP IDP, no user existing - auto register", async ({ page }) => { + // Given idp LDAP is configure on the organization as only authencation method + // Given idp LDAP is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to LDAP + // User authenticates in LDAP + // User is redirect to ZITADEL login + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with LDAP IDP, no user existing - auto register not possible", async ({ page }) => { + // Given idp LDAP is configure on the organization as only authencation method + // Given idp LDAP is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to LDAP + // User authenticates in LDAP + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // User will see the registration page with pre filled user information + // User fills missing information + // User clicks register button + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with LDAP IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({ + page, +}) => { + // Given idp LDAP is configure on the organization as only authencation method + // Given idp LDAP is configure with account creation not allowed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to LDAP + // User authenticates in LDAP + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // Error message is shown, that registration of the user was not possible due to missing information +}); + +test("login with LDAP IDP, no user linked - auto link", async ({ page }) => { + // Given idp LDAP is configure on the organization as only authencation method + // Given idp LDAP is configure with account linking allowed, and linking set to existing email + // Given user with email address user@zitadel.com exists + // User is automatically redirected to LDAP + // User authenticates in LDAP with user@zitadel.com + // User is redirect to ZITADEL login + // User is linked with existing user in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with LDAP IDP, no user linked, linking not possible", async ({ page }) => { + // Given idp LDAP is configure on the organization as only authencation method + // Given idp LDAP is configure with manually account linking not allowed, and linking set to existing email + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to LDAP + // User authenticates in LDAP with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User will get an error message that account linking wasn't possible +}); + +test("login with LDAP IDP, no user linked, linking successful", async ({ page }) => { + // Given idp LDAP is configure on the organization as only authencation method + // Given idp LDAP is configure with manually account linking allowed, and linking set to existing email + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to LDAP + // User authenticates in LDAP with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User is prompted to link the account manually + // User is redirected to the app (default redirect url) +}); diff --git a/acceptance/tests/idp-microsoft.spec.ts b/acceptance/tests/idp-microsoft.spec.ts new file mode 100644 index 0000000000..40d44d577f --- /dev/null +++ b/acceptance/tests/idp-microsoft.spec.ts @@ -0,0 +1,94 @@ +// Note for all tests, in case Microsoft doesn't deliver all relevant information per default +// We should add an action in the needed cases + +import test from "@playwright/test"; + +test("login with Microsoft IDP", async ({ page }) => { + // Given a Microsoft IDP is configured on the organization + // Given the user has Microsoft IDP added as auth method + // User authenticates with the Microsoft IDP + // User is redirected back to login + // User is redirected to the app +}); + +test("login with Microsoft IDP - error", async ({ page }) => { + // Given the Microsoft IDP is configured on the organization + // Given the user has Microsoft IDP added as auth method + // User is redirected to the Microsoft IDP + // User authenticates with the Microsoft IDP and gets an error + // User is redirected back to login + // An error is shown to the user "Something went wrong" +}); + +test("login with Microsoft IDP, no user existing - auto register", async ({ page }) => { + // Given idp Microsoft is configure on the organization as only authencation method + // Given idp Microsoft is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Microsoft + // User authenticates in Microsoft + // User is redirect to ZITADEL login + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Microsoft IDP, no user existing - auto register not possible", async ({ page }) => { + // Given idp Microsoft is configure on the organization as only authencation method + // Given idp Microsoft is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Microsoft + // User authenticates in Microsoft + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // User will see the registration page with pre filled user information + // User fills missing information + // User clicks register button + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Microsoft IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({ + page, +}) => { + // Given idp Microsoft is configure on the organization as only authencation method + // Given idp Microsoft is configure with account creation not allowed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Microsoft + // User authenticates in Microsoft + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // Error message is shown, that registration of the user was not possible due to missing information +}); + +test("login with Microsoft IDP, no user linked - auto link", async ({ page }) => { + // Given idp Microsoft is configure on the organization as only authencation method + // Given idp Microsoft is configure with account linking allowed, and linking set to existing email + // Given user with email address user@zitadel.com exists + // User is automatically redirected to Microsoft + // User authenticates in Microsoft with user@zitadel.com + // User is redirect to ZITADEL login + // User is linked with existing user in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Microsoft IDP, no user linked, linking not possible", async ({ page }) => { + // Given idp Microsoft is configure on the organization as only authencation method + // Given idp Microsoft is configure with manually account linking not allowed, and linking set to existing email + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Microsoft + // User authenticates in Microsoft with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User will get an error message that account linking wasn't possible +}); + +test("login with Microsoft IDP, no user linked, linking successful", async ({ page }) => { + // Given idp Microsoft is configure on the organization as only authencation method + // Given idp Microsoft is configure with manually account linking allowed, and linking set to existing email + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Microsoft + // User authenticates in Microsoft with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User is prompted to link the account manually + // User is redirected to the app (default redirect url) +}); diff --git a/acceptance/tests/idp-saml.spec.ts b/acceptance/tests/idp-saml.spec.ts new file mode 100644 index 0000000000..e9e145909c --- /dev/null +++ b/acceptance/tests/idp-saml.spec.ts @@ -0,0 +1,95 @@ +import test from "@playwright/test"; + +test("login with SAML IDP", async ({ page }) => { + // Given a SAML IDP is configured on the organization + // Given the user has SAML IDP added as auth method + // User authenticates with the SAML IDP + // User is redirected back to login + // User is redirected to the app +}); + +test("login with SAML IDP - error", async ({ page }) => { + // Given the SAML IDP is configured on the organization + // Given the user has SAML IDP added as auth method + // User is redirected to the SAML IDP + // User authenticates with the SAML IDP and gets an error + // User is redirected back to login + // An error is shown to the user "Something went wrong" +}); + +test("login with SAML IDP, no user existing - auto register", async ({ page }) => { + // Given idp SAML is configure on the organization as only authencation method + // Given idp SAML is configure with account creation alloweed, and automatic creation enabled + // Given ZITADEL Action is added to autofill missing user information + // Given no user exists yet + // User is automatically redirected to SAML + // User authenticates in SAML + // User is redirect to ZITADEL login + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with SAML IDP, no user existing - auto register not possible", async ({ page }) => { + // Given idp SAML is configure on the organization as only authencation method + // Given idp SAML is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to SAML + // User authenticates in SAML + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // User will see the registration page with pre filled user information + // User fills missing information + // User clicks register button + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with SAML IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({ + page, +}) => { + // Given idp SAML is configure on the organization as only authencation method + // Given idp SAML is configure with account creation not allowed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to SAML + // User authenticates in SAML + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // Error message is shown, that registration of the user was not possible due to missing information +}); + +test("login with SAML IDP, no user linked - auto link", async ({ page }) => { + // Given idp SAML is configure on the organization as only authencation method + // Given idp SAML is configure with account linking allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com exists + // User is automatically redirected to SAML + // User authenticates in SAML with user@zitadel.com + // User is redirect to ZITADEL login + // User is linked with existing user in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with SAML IDP, no user linked, linking not possible", async ({ page }) => { + // Given idp SAML is configure on the organization as only authencation method + // Given idp SAML is configure with manually account linking not allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to SAML + // User authenticates in SAML with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User will get an error message that account linking wasn't possible +}); + +test("login with SAML IDP, no user linked, linking successful", async ({ page }) => { + // Given idp SAML is configure on the organization as only authencation method + // Given idp SAML is configure with manually account linking allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to SAML + // User authenticates in SAML with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User is prompted to link the account manually + // User is redirected to the app (default redirect url) +}); diff --git a/acceptance/tests/login-configuration-possiblities.spec.ts b/acceptance/tests/login-configuration-possiblities.spec.ts new file mode 100644 index 0000000000..f82f99364a --- /dev/null +++ b/acceptance/tests/login-configuration-possiblities.spec.ts @@ -0,0 +1,51 @@ +import test from "@playwright/test"; + +test("login with mfa setup, mfa setup prompt", async ({ page }) => { + // Given the organization has enabled at least one mfa types + // Given the user has a password but no mfa registered + // User authenticates with login name and password + // User is prompted to setup a mfa, mfa providers are listed, the user can choose the provider +}); + +test("login with mfa setup, no mfa setup prompt", async ({ page }) => { + // Given the organization has set "multifactor init check time" to 0 + // Given the organization has enabled mfa types + // Given the user has a password but no mfa registered + // User authenticates with loginname and password + // user is directly loged in and not prompted to setup mfa +}); + +test("login with mfa setup, force mfa for local authenticated users", async ({ page }) => { + // Given the organization has enabled force mfa for local authentiacted users + // Given the organization has enabled all possible mfa types + // Given the user has a password but no mfa registered + // User authenticates with loginname and password + // User is prompted to setup a mfa, all possible mfa providers are listed, the user can choose the provider +}); + +test("login with mfa setup, force mfa - local user", async ({ page }) => { + // Given the organization has enabled force mfa for local authentiacted users + // Given the organization has enabled all possible mfa types + // Given the user has a password but no mfa registered + // User authenticates with loginname and password + // User is prompted to setup a mfa, all possible mfa providers are listed, the user can choose the provider +}); + +test("login with mfa setup, force mfa - external user", async ({ page }) => { + // Given the organization has enabled force mfa + // Given the organization has enabled all possible mfa types + // Given the user has an idp but no mfa registered + // enter login name + // redirect to configured external idp + // User is prompted to setup a mfa, all possible mfa providers are listed, the user can choose the provider +}); + +test("login with mfa setup, force mfa - local user, wrong password", async ({ page }) => { + // Given the organization has a password lockout policy set to 1 on the max password attempts + // Given the user has only a password as auth methos + // enter login name + // enter wrong password + // User will get an error "Wrong password" + // enter password + // User will get an error "Max password attempts reached - user is locked. Please reach out to your administrator" +}); diff --git a/acceptance/tests/login.ts b/acceptance/tests/login.ts new file mode 100644 index 0000000000..32c0007a3c --- /dev/null +++ b/acceptance/tests/login.ts @@ -0,0 +1,41 @@ +import { expect, Page } from "@playwright/test"; +import { code, otpFromSink } from "./code"; +import { loginname } from "./loginname"; +import { password } from "./password"; +import { totp } from "./zitadel"; + +export async function startLogin(page: Page) { + await page.goto("/loginname"); +} + +export async function loginWithPassword(page: Page, username: string, pw: string) { + await startLogin(page); + await loginname(page, username); + await password(page, pw); +} + +export async function loginWithPasskey(page: Page, authenticatorId: string, username: string) { + await startLogin(page); + await loginname(page, username); + // await passkey(page, authenticatorId); +} + +export async function loginScreenExpect(page: Page, fullName: string) { + await expect(page).toHaveURL(/signedin.*/); + await expect(page.getByRole("heading")).toContainText(fullName); +} + +export async function loginWithPasswordAndEmailOTP(page: Page, username: string, password: string, email: string) { + await loginWithPassword(page, username, password); + await otpFromSink(page, email); +} + +export async function loginWithPasswordAndPhoneOTP(page: Page, username: string, password: string, phone: string) { + await loginWithPassword(page, username, password); + await otpFromSink(page, phone); +} + +export async function loginWithPasswordAndTOTP(page: Page, username: string, password: string, secret: string) { + await loginWithPassword(page, username, password); + await code(page, totp(secret)); +} diff --git a/acceptance/tests/loginname-screen.ts b/acceptance/tests/loginname-screen.ts new file mode 100644 index 0000000000..be41a28eda --- /dev/null +++ b/acceptance/tests/loginname-screen.ts @@ -0,0 +1,12 @@ +import { expect, Page } from "@playwright/test"; + +const usernameTextInput = "username-text-input"; + +export async function loginnameScreen(page: Page, username: string) { + await page.getByTestId(usernameTextInput).pressSequentially(username); +} + +export async function loginnameScreenExpect(page: Page, username: string) { + await expect(page.getByTestId(usernameTextInput)).toHaveValue(username); + await expect(page.getByTestId("error").locator("div")).toContainText("User not found in the system"); +} diff --git a/acceptance/tests/loginname.ts b/acceptance/tests/loginname.ts new file mode 100644 index 0000000000..2050ec1d3c --- /dev/null +++ b/acceptance/tests/loginname.ts @@ -0,0 +1,7 @@ +import { Page } from "@playwright/test"; +import { loginnameScreen } from "./loginname-screen"; + +export async function loginname(page: Page, username: string) { + await loginnameScreen(page, username); + await page.getByTestId("submit-button").click(); +} diff --git a/acceptance/tests/passkey.ts b/acceptance/tests/passkey.ts new file mode 100644 index 0000000000..d8cda10ddb --- /dev/null +++ b/acceptance/tests/passkey.ts @@ -0,0 +1,109 @@ +import { expect, Page } from "@playwright/test"; +import { CDPSession } from "playwright-core"; + +interface session { + client: CDPSession; + authenticatorId: string; +} + +async function client(page: Page): Promise { + const cdpSession = await page.context().newCDPSession(page); + await cdpSession.send("WebAuthn.enable", { enableUI: false }); + const result = await cdpSession.send("WebAuthn.addVirtualAuthenticator", { + options: { + protocol: "ctap2", + transport: "internal", + hasResidentKey: true, + hasUserVerification: true, + isUserVerified: true, + automaticPresenceSimulation: true, + }, + }); + return { client: cdpSession, authenticatorId: result.authenticatorId }; +} + +export async function passkeyRegister(page: Page): Promise { + const session = await client(page); + + await passkeyNotExisting(session.client, session.authenticatorId); + await simulateSuccessfulPasskeyRegister(session.client, session.authenticatorId, () => + page.getByTestId("submit-button").click(), + ); + await passkeyRegistered(session.client, session.authenticatorId); + + return session.authenticatorId; +} + +export async function passkey(page: Page, authenticatorId: string) { + const cdpSession = await page.context().newCDPSession(page); + await cdpSession.send("WebAuthn.enable", { enableUI: false }); + + const signCount = await passkeyExisting(cdpSession, authenticatorId); + + await simulateSuccessfulPasskeyInput(cdpSession, authenticatorId, () => page.getByTestId("submit-button").click()); + + await passkeyUsed(cdpSession, authenticatorId, signCount); +} + +async function passkeyNotExisting(client: CDPSession, authenticatorId: string) { + const result = await client.send("WebAuthn.getCredentials", { authenticatorId }); + expect(result.credentials).toHaveLength(0); +} + +async function passkeyRegistered(client: CDPSession, authenticatorId: string) { + const result = await client.send("WebAuthn.getCredentials", { authenticatorId }); + expect(result.credentials).toHaveLength(1); + await passkeyUsed(client, authenticatorId, 0); +} + +async function passkeyExisting(client: CDPSession, authenticatorId: string): Promise { + const result = await client.send("WebAuthn.getCredentials", { authenticatorId }); + expect(result.credentials).toHaveLength(1); + return result.credentials[0].signCount; +} + +async function passkeyUsed(client: CDPSession, authenticatorId: string, signCount: number) { + const result = await client.send("WebAuthn.getCredentials", { authenticatorId }); + expect(result.credentials).toHaveLength(1); + expect(result.credentials[0].signCount).toBeGreaterThan(signCount); +} + +async function simulateSuccessfulPasskeyRegister( + client: CDPSession, + authenticatorId: string, + operationTrigger: () => Promise, +) { + // initialize event listeners to wait for a successful passkey input event + const operationCompleted = new Promise((resolve) => { + client.on("WebAuthn.credentialAdded", () => { + console.log("Credential Added!"); + resolve(); + }); + }); + + // perform a user action that triggers passkey prompt + await operationTrigger(); + + // wait to receive the event that the passkey was successfully registered or verified + await operationCompleted; +} + +async function simulateSuccessfulPasskeyInput( + client: CDPSession, + authenticatorId: string, + operationTrigger: () => Promise, +) { + // initialize event listeners to wait for a successful passkey input event + const operationCompleted = new Promise((resolve) => { + client.on("WebAuthn.credentialAsserted", () => { + console.log("Credential Asserted!"); + resolve(); + }); + }); + + // perform a user action that triggers passkey prompt + await operationTrigger(); + + // wait to receive the event that the passkey was successfully registered or verified + await operationCompleted; +} diff --git a/acceptance/tests/password-screen.ts b/acceptance/tests/password-screen.ts new file mode 100644 index 0000000000..57334a07d2 --- /dev/null +++ b/acceptance/tests/password-screen.ts @@ -0,0 +1,82 @@ +import { expect, Page } from "@playwright/test"; +import { getCodeFromSink } from "./sink"; + +const codeField = "code-text-input"; +const passwordField = "password-text-input"; +const passwordConfirmField = "password-confirm-text-input"; +const lengthCheck = "length-check"; +const symbolCheck = "symbol-check"; +const numberCheck = "number-check"; +const uppercaseCheck = "uppercase-check"; +const lowercaseCheck = "lowercase-check"; +const equalCheck = "equal-check"; + +const matchText = "Matches"; +const noMatchText = "Doesn't match"; + +export async function changePasswordScreen(page: Page, password1: string, password2: string) { + await page.getByTestId(passwordField).pressSequentially(password1); + await page.getByTestId(passwordConfirmField).pressSequentially(password2); +} + +export async function passwordScreen(page: Page, password: string) { + await page.getByTestId(passwordField).pressSequentially(password); +} + +export async function passwordScreenExpect(page: Page, password: string) { + await expect(page.getByTestId(passwordField)).toHaveValue(password); + await expect(page.getByTestId("error").locator("div")).toContainText("Could not verify password"); +} + +export async function changePasswordScreenExpect( + page: Page, + password1: string, + password2: string, + length: boolean, + symbol: boolean, + number: boolean, + uppercase: boolean, + lowercase: boolean, + equals: boolean, +) { + await expect(page.getByTestId(passwordField)).toHaveValue(password1); + await expect(page.getByTestId(passwordConfirmField)).toHaveValue(password2); + + await checkContent(page, lengthCheck, length); + await checkContent(page, symbolCheck, symbol); + await checkContent(page, numberCheck, number); + await checkContent(page, uppercaseCheck, uppercase); + await checkContent(page, lowercaseCheck, lowercase); + await checkContent(page, equalCheck, equals); +} + +async function checkContent(page: Page, testid: string, match: boolean) { + if (match) { + await expect(page.getByTestId(testid)).toContainText(matchText); + } else { + await expect(page.getByTestId(testid)).toContainText(noMatchText); + } +} + +export async function resetPasswordScreen(page: Page, username: string, password1: string, password2: string) { + // wait for send of the code + await page.waitForTimeout(3000); + const c = await getCodeFromSink(username); + await page.getByTestId(codeField).pressSequentially(c); + await page.getByTestId(passwordField).pressSequentially(password1); + await page.getByTestId(passwordConfirmField).pressSequentially(password2); +} + +export async function resetPasswordScreenExpect( + page: Page, + password1: string, + password2: string, + length: boolean, + symbol: boolean, + number: boolean, + uppercase: boolean, + lowercase: boolean, + equals: boolean, +) { + await changePasswordScreenExpect(page, password1, password2, length, symbol, number, uppercase, lowercase, equals); +} diff --git a/acceptance/tests/password.ts b/acceptance/tests/password.ts new file mode 100644 index 0000000000..b3d31fcaee --- /dev/null +++ b/acceptance/tests/password.ts @@ -0,0 +1,30 @@ +import { Page } from "@playwright/test"; +import { changePasswordScreen, passwordScreen, resetPasswordScreen } from "./password-screen"; + +const passwordSubmitButton = "submit-button"; +const passwordResetButton = "reset-button"; + +export async function startChangePassword(page: Page, loginname: string) { + await page.goto("/password/change?" + new URLSearchParams({ loginName: loginname })); +} + +export async function changePassword(page: Page, loginname: string, password: string) { + await startChangePassword(page, loginname); + await changePasswordScreen(page, password, password); + await page.getByTestId(passwordSubmitButton).click(); +} + +export async function password(page: Page, password: string) { + await passwordScreen(page, password); + await page.getByTestId(passwordSubmitButton).click(); +} + +export async function startResetPassword(page: Page) { + await page.getByTestId(passwordResetButton).click(); +} + +export async function resetPassword(page: Page, username: string, password: string) { + await startResetPassword(page); + await resetPasswordScreen(page, username, password, password); + await page.getByTestId(passwordSubmitButton).click(); +} diff --git a/acceptance/tests/register-screen.ts b/acceptance/tests/register-screen.ts new file mode 100644 index 0000000000..d14f5dc970 --- /dev/null +++ b/acceptance/tests/register-screen.ts @@ -0,0 +1,27 @@ +import { Page } from "@playwright/test"; + +const passwordField = "password-text-input"; +const passwordConfirmField = "password-confirm-text-input"; + +export async function registerUserScreenPassword(page: Page, firstname: string, lastname: string, email: string) { + await registerUserScreen(page, firstname, lastname, email); + await page.getByTestId("password-radio").click(); +} + +export async function registerUserScreenPasskey(page: Page, firstname: string, lastname: string, email: string) { + await registerUserScreen(page, firstname, lastname, email); + await page.getByTestId("passkey-radio").click(); +} + +export async function registerPasswordScreen(page: Page, password1: string, password2: string) { + await page.getByTestId(passwordField).pressSequentially(password1); + await page.getByTestId(passwordConfirmField).pressSequentially(password2); +} + +export async function registerUserScreen(page: Page, firstname: string, lastname: string, email: string) { + await page.getByTestId("firstname-text-input").pressSequentially(firstname); + await page.getByTestId("lastname-text-input").pressSequentially(lastname); + await page.getByTestId("email-text-input").pressSequentially(email); + await page.getByTestId("privacy-policy-checkbox").check(); + await page.getByTestId("tos-checkbox").check(); +} diff --git a/acceptance/tests/register.spec.ts b/acceptance/tests/register.spec.ts new file mode 100644 index 0000000000..a3ffc7a67e --- /dev/null +++ b/acceptance/tests/register.spec.ts @@ -0,0 +1,173 @@ +import { faker } from "@faker-js/faker"; +import { test } from "@playwright/test"; +import dotenv from "dotenv"; +import path from "path"; +import { loginScreenExpect } from "./login"; +import { registerWithPasskey, registerWithPassword } from "./register"; +import { removeUserByUsername } from "./zitadel"; + +// Read from ".env" file. +dotenv.config({ path: path.resolve(__dirname, ".env.local") }); + +test("register with password", async ({ page }) => { + const username = faker.internet.email(); + const password = "Password1!"; + const firstname = faker.person.firstName(); + const lastname = faker.person.lastName(); + + await registerWithPassword(page, firstname, lastname, username, password, password); + await loginScreenExpect(page, firstname + " " + lastname); + + // wait for projection of user + await page.waitForTimeout(2000); + await removeUserByUsername(username); +}); + +test("register with passkey", async ({ page }) => { + const username = faker.internet.email(); + const firstname = faker.person.firstName(); + const lastname = faker.person.lastName(); + + await registerWithPasskey(page, firstname, lastname, username); + await loginScreenExpect(page, firstname + " " + lastname); + + // wait for projection of user + await page.waitForTimeout(2000); + await removeUserByUsername(username); +}); + +test("register with username and password - only password enabled", async ({ page }) => { + // Given on the default organization "username and password is allowed" is enabled + // Given on the default organization "username registeration allowed" is enabled + // Given on the default organization no idp is configured and enabled + // Given on the default organization passkey is not enabled + // Given user doesn't exist + // Click on button "register new user" + // User is redirected to registration page + // Only password is shown as an option - no passkey + // User enters "firstname", "lastname", "username" and "password" + // User is redirected to app (default redirect url) +}); + +test("register with username and password - wrong password not enough characters", async ({ page }) => { + // Given on the default organization "username and password is allowed" is enabled + // Given on the default organization "username registeration allowed" is enabled + // Given on the default organization no idp is configured and enabled + // Given on the default organization passkey is not enabled + // Given password policy is set to 8 characters and must include number, symbol, lower and upper letter + // Given user doesn't exist + // Click on button "register new user" + // User is redirected to registration page + // Only password is shown as an option - no passkey + // User enters "firstname", "lastname", "username" and a password thats to short + // Error is shown "Password doesn't match the policy - it must have at least 8 characters" +}); + +test("register with username and password - wrong password number missing", async ({ page }) => { + // Given on the default organization "username and password is allowed" is enabled + // Given on the default organization "username registeration allowed" is enabled + // Given on the default organization no idp is configured and enabled + // Given on the default organization passkey is not enabled + // Given password policy is set to 8 characters and must include number, symbol, lower and upper letter + // Given user doesn't exist + // Click on button "register new user" + // User is redirected to registration page + // Only password is shown as an option - no passkey + // User enters "firstname", "lastname", "username" and a password without a number + // Error is shown "Password doesn't match the policy - number missing" +}); + +test("register with username and password - wrong password upper case missing", async ({ page }) => { + // Given on the default organization "username and password is allowed" is enabled + // Given on the default organization "username registeration allowed" is enabled + // Given on the default organization no idp is configured and enabled + // Given on the default organization passkey is not enabled + // Given password policy is set to 8 characters and must include number, symbol, lower and upper letter + // Given user doesn't exist + // Click on button "register new user" + // User is redirected to registration page + // Only password is shown as an option - no passkey + // User enters "firstname", "lastname", "username" and a password without an upper case + // Error is shown "Password doesn't match the policy - uppercase letter missing" +}); + +test("register with username and password - wrong password lower case missing", async ({ page }) => { + // Given on the default organization "username and password is allowed" is enabled + // Given on the default organization "username registeration allowed" is enabled + // Given on the default organization no idp is configured and enabled + // Given on the default organization passkey is not enabled + // Given password policy is set to 8 characters and must include number, symbol, lower and upper letter + // Given user doesn't exist + // Click on button "register new user" + // User is redirected to registration page + // Only password is shown as an option - no passkey + // User enters "firstname", "lastname", "username" and a password without an lower case + // Error is shown "Password doesn't match the policy - lowercase letter missing" +}); + +test("register with username and password - wrong password symboo missing", async ({ page }) => { + // Given on the default organization "username and password is allowed" is enabled + // Given on the default organization "username registeration allowed" is enabled + // Given on the default organization no idp is configured and enabled + // Given on the default organization passkey is not enabled + // Given password policy is set to 8 characters and must include number, symbol, lower and upper letter + // Given user doesn't exist + // Click on button "register new user" + // User is redirected to registration page + // Only password is shown as an option - no passkey + // User enters "firstname", "lastname", "username" and a password without an symbol + // Error is shown "Password doesn't match the policy - symbol missing" +}); + +test("register with username and password - password and passkey enabled", async ({ page }) => { + // Given on the default organization "username and password is allowed" is enabled + // Given on the default organization "username registeration allowed" is enabled + // Given on the default organization no idp is configured and enabled + // Given on the default organization passkey is enabled + // Given user doesn't exist + // Click on button "register new user" + // User is redirected to registration page + // User enters "firstname", "lastname", "username" + // Password and passkey are shown as authentication option + // User clicks password + // User enters password + // User is redirected to app (default redirect url) +}); + +test("register with username and passkey - password and passkey enabled", async ({ page }) => { + // Given on the default organization "username and password is allowed" is enabled + // Given on the default organization "username registeration allowed" is enabled + // Given on the default organization no idp is configured and enabled + // Given on the default organization passkey is enabled + // Given user doesn't exist + // Click on button "register new user" + // User is redirected to registration page + // User enters "firstname", "lastname", "username" + // Password and passkey are shown as authentication option + // User clicks passkey + // Passkey is opened automatically + // User verifies passkey + // User is redirected to app (default redirect url) +}); + +test("register with username and password - registration disabled", async ({ page }) => { + // Given on the default organization "username and password is allowed" is enabled + // Given on the default organization "username registeration allowed" is enabled + // Given on the default organization no idp is configured and enabled + // Given user doesn't exist + // Button "register new user" is not available +}); + +test("register with username and password - multiple registration options", async ({ page }) => { + // Given on the default organization "username and password is allowed" is enabled + // Given on the default organization "username registeration allowed" is enabled + // Given on the default organization one idp is configured and enabled + // Given user doesn't exist + // Click on button "register new user" + // User is redirected to registration options + // Local User and idp button are shown + // User clicks idp button + // User enters "firstname", "lastname", "username" and "password" + // User clicks next + // User is redirected to app (default redirect url) +}); diff --git a/acceptance/tests/register.ts b/acceptance/tests/register.ts new file mode 100644 index 0000000000..693bdfbc0d --- /dev/null +++ b/acceptance/tests/register.ts @@ -0,0 +1,29 @@ +import { Page } from "@playwright/test"; +import { passkeyRegister } from "./passkey"; +import { registerPasswordScreen, registerUserScreenPasskey, registerUserScreenPassword } from "./register-screen"; + +export async function registerWithPassword( + page: Page, + firstname: string, + lastname: string, + email: string, + password1: string, + password2: string, +) { + await page.goto("/register"); + await registerUserScreenPassword(page, firstname, lastname, email); + await page.getByTestId("submit-button").click(); + await registerPasswordScreen(page, password1, password2); + await page.getByTestId("submit-button").click(); +} + +export async function registerWithPasskey(page: Page, firstname: string, lastname: string, email: string): Promise { + await page.goto("/register"); + await registerUserScreenPasskey(page, firstname, lastname, email); + await page.getByTestId("submit-button").click(); + + // wait for projection of user + await page.waitForTimeout(2000); + + return await passkeyRegister(page); +} diff --git a/acceptance/tests/sink.ts b/acceptance/tests/sink.ts new file mode 100644 index 0000000000..fc13a98dc7 --- /dev/null +++ b/acceptance/tests/sink.ts @@ -0,0 +1,55 @@ +import axios from "axios"; + +export async function getOtpFromSink(key: string): Promise { + try { + const response = await axios.post( + process.env.SINK_NOTIFICATION_URL!, + { + recipient: key, + }, + { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.ZITADEL_SERVICE_USER_TOKEN}`, + }, + }, + ); + + if (response.status >= 400) { + const error = `HTTP Error: ${response.status} - ${response.statusText}`; + console.error(error); + throw new Error(error); + } + return response.data.args.oTP; + } catch (error) { + console.error("Error making request:", error); + throw error; + } +} + +export async function getCodeFromSink(key: string): Promise { + try { + const response = await axios.post( + process.env.SINK_NOTIFICATION_URL!, + { + recipient: key, + }, + { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.ZITADEL_SERVICE_USER_TOKEN}`, + }, + }, + ); + + if (response.status >= 400) { + const error = `HTTP Error: ${response.status} - ${response.statusText}`; + console.error(error); + throw new Error(error); + } + return response.data.args.code; + } catch (error) { + console.error("Error making request:", error); + throw error; + } +} diff --git a/acceptance/tests/user.ts b/acceptance/tests/user.ts new file mode 100644 index 0000000000..3daefdff08 --- /dev/null +++ b/acceptance/tests/user.ts @@ -0,0 +1,171 @@ +import { Page } from "@playwright/test"; +import { registerWithPasskey } from "./register"; +import { activateOTP, addTOTP, addUser, getUserByUsername, removeUser } from "./zitadel"; + +export interface userProps { + email: string; + firstName: string; + lastName: string; + organization: string; + password: string; + phone: string; +} + +class User { + private readonly props: userProps; + private user: string; + + constructor(userProps: userProps) { + this.props = userProps; + } + + async ensure(page: Page) { + const response = await addUser(this.props); + + this.setUserId(response.userId); + } + + async cleanup() { + await removeUser(this.getUserId()); + } + + public setUserId(userId: string) { + this.user = userId; + } + + public getUserId() { + return this.user; + } + + public getUsername() { + return this.props.email; + } + + public getPassword() { + return this.props.password; + } + + public getFirstname() { + return this.props.firstName; + } + + public getLastname() { + return this.props.lastName; + } + + public getPhone() { + return this.props.phone; + } + + public getFullName() { + return `${this.props.firstName} ${this.props.lastName}`; + } +} + +export class PasswordUser extends User { + async ensure(page: Page) { + await super.ensure(page); + // wait for projection of user + await page.waitForTimeout(2000); + } +} + +export enum OtpType { + sms = "sms", + email = "email", +} + +export interface otpUserProps { + email: string; + firstName: string; + lastName: string; + organization: string; + password: string; + phone: string; + type: OtpType; +} + +export class PasswordUserWithOTP extends User { + private type: OtpType; + + constructor(props: otpUserProps) { + super({ + email: props.email, + firstName: props.firstName, + lastName: props.lastName, + organization: props.organization, + password: props.password, + phone: props.phone, + }); + this.type = props.type; + } + + async ensure(page: Page) { + await super.ensure(page); + + await activateOTP(this.getUserId(), this.type); + + // wait for projection of user + await page.waitForTimeout(2000); + } +} + +export class PasswordUserWithTOTP extends User { + private secret: string; + + async ensure(page: Page) { + await super.ensure(page); + + this.secret = await addTOTP(this.getUserId()); + + // wait for projection of user + await page.waitForTimeout(2000); + } + + public getSecret(): string { + return this.secret; + } +} + +export interface passkeyUserProps { + email: string; + firstName: string; + lastName: string; + organization: string; + phone: string; +} + +export class PasskeyUser extends User { + private authenticatorId: string; + + constructor(props: passkeyUserProps) { + super({ + email: props.email, + firstName: props.firstName, + lastName: props.lastName, + organization: props.organization, + password: "", + phone: props.phone, + }); + } + + public async ensure(page: Page) { + const authId = await registerWithPasskey(page, this.getFirstname(), this.getLastname(), this.getUsername()); + this.authenticatorId = authId; + + // wait for projection of user + await page.waitForTimeout(2000); + } + + async cleanup() { + const resp: any = await getUserByUsername(this.getUsername()); + if (!resp || !resp.result || !resp.result[0]) { + return; + } + await removeUser(resp.result[0].userId); + } + + public getAuthenticatorId(): string { + return this.authenticatorId; + } +} diff --git a/acceptance/tests/username-passkey.spec.ts b/acceptance/tests/username-passkey.spec.ts new file mode 100644 index 0000000000..e73de3547f --- /dev/null +++ b/acceptance/tests/username-passkey.spec.ts @@ -0,0 +1,40 @@ +import { faker } from "@faker-js/faker"; +import { test as base } from "@playwright/test"; +import dotenv from "dotenv"; +import path from "path"; +import { loginScreenExpect, loginWithPasskey } from "./login"; +import { PasskeyUser } from "./user"; + +// Read from ".env" file. +dotenv.config({ path: path.resolve(__dirname, ".env.local") }); + +const test = base.extend<{ user: PasskeyUser }>({ + user: async ({ page }, use) => { + const user = new PasskeyUser({ + email: faker.internet.email(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + organization: "", + phone: faker.phone.number(), + }); + await user.ensure(page); + await use(user); + await user.cleanup(); + }, +}); + +test("username and passkey login", async ({ user, page }) => { + await loginWithPasskey(page, user.getAuthenticatorId(), user.getUsername()); + await loginScreenExpect(page, user.getFullName()); +}); + +test("username and passkey login, multiple auth methods", async ({ page }) => { + // Given passkey and password is enabled on the organization of the user + // Given the user has password and passkey registered + // enter username + // passkey popup is directly shown + // user aborts passkey authentication + // user switches to password authentication + // user enters password + // user is redirected to app +}); diff --git a/acceptance/tests/username-password-changed.spec.ts b/acceptance/tests/username-password-changed.spec.ts new file mode 100644 index 0000000000..c185e51ec9 --- /dev/null +++ b/acceptance/tests/username-password-changed.spec.ts @@ -0,0 +1,53 @@ +import { faker } from "@faker-js/faker"; +import { test as base } from "@playwright/test"; +import dotenv from "dotenv"; +import path from "path"; +import { loginWithPassword } from "./login"; +import { startChangePassword } from "./password"; +import { changePasswordScreen, changePasswordScreenExpect } from "./password-screen"; +import { PasswordUser } from "./user"; + +// Read from ".env" file. +dotenv.config({ path: path.resolve(__dirname, ".env.local") }); + +const test = base.extend<{ user: PasswordUser }>({ + user: async ({ page }, use) => { + const user = new PasswordUser({ + email: faker.internet.email(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + organization: "", + phone: faker.phone.number(), + password: "Password1!", + }); + await user.ensure(page); + await use(user); + await user.cleanup(); + }, +}); + +test("username and password changed login", async ({ user, page }) => { + // commented, fix in https://github.com/zitadel/zitadel/pull/8807 + /* + const changedPw = "ChangedPw1!"; + await loginWithPassword(page, user.getUsername(), user.getPassword()); + + // wait for projection of token + await page.waitForTimeout(2000); + + await changePassword(page, user.getUsername(), changedPw); + await loginScreenExpect(page, user.getFullName()); + + await loginWithPassword(page, user.getUsername(), changedPw); + await loginScreenExpect(page, user.getFullName()); + */ +}); + +test("password change not with desired complexity", async ({ user, page }) => { + const changedPw1 = "change"; + const changedPw2 = "chang"; + await loginWithPassword(page, user.getUsername(), user.getPassword()); + await startChangePassword(page, user.getUsername()); + await changePasswordScreen(page, changedPw1, changedPw2); + await changePasswordScreenExpect(page, changedPw1, changedPw2, false, false, false, false, true, false); +}); diff --git a/acceptance/tests/username-password-otp_email.spec.ts b/acceptance/tests/username-password-otp_email.spec.ts new file mode 100644 index 0000000000..daa2e0a429 --- /dev/null +++ b/acceptance/tests/username-password-otp_email.spec.ts @@ -0,0 +1,93 @@ +import { faker } from "@faker-js/faker"; +import { test as base } from "@playwright/test"; +import dotenv from "dotenv"; +import path from "path"; +import { code, codeResend, otpFromSink } from "./code"; +import { codeScreenExpect } from "./code-screen"; +import { loginScreenExpect, loginWithPassword, loginWithPasswordAndEmailOTP } from "./login"; +import { OtpType, PasswordUserWithOTP } from "./user"; + +// Read from ".env" file. +dotenv.config({ path: path.resolve(__dirname, ".env.local") }); + +const test = base.extend<{ user: PasswordUserWithOTP; sink: any }>({ + user: async ({ page }, use) => { + const user = new PasswordUserWithOTP({ + email: faker.internet.email(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + organization: "", + phone: faker.phone.number(), + password: "Password1!", + type: OtpType.email, + }); + + await user.ensure(page); + await use(user); + await user.cleanup(); + }, +}); + +test("username, password and email otp login, enter code manually", async ({ user, page }) => { + // Given email otp is enabled on the organization of the user + // Given the user has only email otp configured as second factor + // User enters username + // User enters password + // User receives an email with a verification code + // User enters the code into the ui + // User is redirected to the app (default redirect url) + await loginWithPasswordAndEmailOTP(page, user.getUsername(), user.getPassword(), user.getUsername()); + await loginScreenExpect(page, user.getFullName()); +}); + +test("username, password and email otp login, click link in email", async ({ page }) => { + // Given email otp is enabled on the organization of the user + // Given the user has only email otp configured as second factor + // User enters username + // User enters password + // User receives an email with a verification code + // User clicks link in the email + // User is redirected to the app (default redirect url) +}); + +test("username, password and email otp login, resend code", async ({ user, page }) => { + // Given email otp is enabled on the organization of the user + // Given the user has only email otp configured as second factor + // User enters username + // User enters password + // User receives an email with a verification code + // User clicks resend code + // User receives a new email with a verification code + // User enters the new code in the ui + // User is redirected to the app (default redirect url) + await loginWithPassword(page, user.getUsername(), user.getPassword()); + await codeResend(page); + await otpFromSink(page, user.getUsername()); + await loginScreenExpect(page, user.getFullName()); +}); + +test("username, password and email otp login, wrong code", async ({ user, page }) => { + // Given email otp is enabled on the organization of the user + // Given the user has only email otp configured as second factor + // User enters username + // User enters password + // User receives an email with a verification code + // User enters a wrong code + // Error message - "Invalid code" is shown + const c = "wrongcode"; + await loginWithPassword(page, user.getUsername(), user.getPassword()); + await code(page, c); + await codeScreenExpect(page, c); +}); + +test("username, password and email otp login, multiple mfa options", async ({ page }) => { + // Given email otp and sms otp is enabled on the organization of the user + // Given the user has email and sms otp configured as second factor + // User enters username + // User enters password + // User receives an email with a verification code + // User clicks button to use sms otp as second factor + // User receives a sms with a verification code + // User enters code in ui + // User is redirected to the app (default redirect url) +}); diff --git a/acceptance/tests/username-password-otp_sms.spec.ts b/acceptance/tests/username-password-otp_sms.spec.ts new file mode 100644 index 0000000000..8fb91a66c7 --- /dev/null +++ b/acceptance/tests/username-password-otp_sms.spec.ts @@ -0,0 +1,68 @@ +import { faker } from "@faker-js/faker"; +import { test as base } from "@playwright/test"; +import dotenv from "dotenv"; +import path from "path"; +import { code } from "./code"; +import { codeScreenExpect } from "./code-screen"; +import { loginScreenExpect, loginWithPassword, loginWithPasswordAndPhoneOTP } from "./login"; +import { OtpType, PasswordUserWithOTP } from "./user"; + +// Read from ".env" file. +dotenv.config({ path: path.resolve(__dirname, ".env.local") }); + +const test = base.extend<{ user: PasswordUserWithOTP; sink: any }>({ + user: async ({ page }, use) => { + const user = new PasswordUserWithOTP({ + email: faker.internet.email(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + organization: "", + phone: faker.phone.number({ style: "international" }), + password: "Password1!", + type: OtpType.sms, + }); + + await user.ensure(page); + await use(user); + await user.cleanup(); + }, +}); + +test("username, password and sms otp login, enter code manually", async ({ user, page }) => { + // Given sms otp is enabled on the organization of the user + // Given the user has only sms otp configured as second factor + // User enters username + // User enters password + // User receives a sms with a verification code + // User enters the code into the ui + // User is redirected to the app (default redirect url) + await loginWithPasswordAndPhoneOTP(page, user.getUsername(), user.getPassword(), user.getPhone()); + await loginScreenExpect(page, user.getFullName()); +}); + +test("username, password and sms otp login, resend code", async ({ user, page }) => { + // Given sms otp is enabled on the organization of the user + // Given the user has only sms otp configured as second factor + // User enters username + // User enters password + // User receives a sms with a verification code + // User clicks resend code + // User receives a new sms with a verification code + // User is redirected to the app (default redirect url) + await loginWithPasswordAndPhoneOTP(page, user.getUsername(), user.getPassword(), user.getPhone()); + await loginScreenExpect(page, user.getFullName()); +}); + +test("username, password and sms otp login, wrong code", async ({ user, page }) => { + // Given sms otp is enabled on the organization of the user + // Given the user has only sms otp configured as second factor + // User enters username + // User enters password + // User receives a sms with a verification code + // User enters a wrong code + // Error message - "Invalid code" is shown + const c = "wrongcode"; + await loginWithPassword(page, user.getUsername(), user.getPassword()); + await code(page, c); + await codeScreenExpect(page, c); +}); diff --git a/acceptance/tests/username-password-set.spec.ts b/acceptance/tests/username-password-set.spec.ts new file mode 100644 index 0000000000..30d442df50 --- /dev/null +++ b/acceptance/tests/username-password-set.spec.ts @@ -0,0 +1,51 @@ +import { faker } from "@faker-js/faker"; +import { test as base } from "@playwright/test"; +import dotenv from "dotenv"; +import path from "path"; +import { loginScreenExpect, loginWithPassword, startLogin } from "./login"; +import { loginname } from "./loginname"; +import { resetPassword, startResetPassword } from "./password"; +import { resetPasswordScreen, resetPasswordScreenExpect } from "./password-screen"; +import { PasswordUser } from "./user"; + +// Read from ".env" file. +dotenv.config({ path: path.resolve(__dirname, ".env.local") }); + +const test = base.extend<{ user: PasswordUser }>({ + user: async ({ page }, use) => { + const user = new PasswordUser({ + email: faker.internet.email(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + organization: "", + phone: faker.phone.number(), + password: "Password1!", + }); + await user.ensure(page); + await use(user); + await user.cleanup(); + }, +}); + +test("username and password set login", async ({ user, page }) => { + // commented, fix in https://github.com/zitadel/zitadel/pull/8807 + + const changedPw = "ChangedPw1!"; + await startLogin(page); + await loginname(page, user.getUsername()); + await resetPassword(page, user.getUsername(), changedPw); + await loginScreenExpect(page, user.getFullName()); + + await loginWithPassword(page, user.getUsername(), changedPw); + await loginScreenExpect(page, user.getFullName()); +}); + +test("password set not with desired complexity", async ({ user, page }) => { + const changedPw1 = "change"; + const changedPw2 = "chang"; + await startLogin(page); + await loginname(page, user.getUsername()); + await startResetPassword(page); + await resetPasswordScreen(page, user.getUsername(), changedPw1, changedPw2); + await resetPasswordScreenExpect(page, changedPw1, changedPw2, false, false, false, false, true, false); +}); diff --git a/acceptance/tests/username-password-totp.spec.ts b/acceptance/tests/username-password-totp.spec.ts new file mode 100644 index 0000000000..4b6e678931 --- /dev/null +++ b/acceptance/tests/username-password-totp.spec.ts @@ -0,0 +1,67 @@ +import { faker } from "@faker-js/faker"; +import { test as base } from "@playwright/test"; +import dotenv from "dotenv"; +import path from "path"; +import { code } from "./code"; +import { codeScreenExpect } from "./code-screen"; +import { loginScreenExpect, loginWithPassword, loginWithPasswordAndTOTP } from "./login"; +import { PasswordUserWithTOTP } from "./user"; + +// Read from ".env" file. +dotenv.config({ path: path.resolve(__dirname, ".env.local") }); + +const test = base.extend<{ user: PasswordUserWithTOTP; sink: any }>({ + user: async ({ page }, use) => { + const user = new PasswordUserWithTOTP({ + email: faker.internet.email(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + organization: "", + phone: faker.phone.number({ style: "international" }), + password: "Password1!", + }); + + await user.ensure(page); + await use(user); + await user.cleanup(); + }, +}); + +test("username, password and totp login", async ({ user, page }) => { + // Given totp is enabled on the organization of the user + // Given the user has only totp configured as second factor + // User enters username + // User enters password + // Screen for entering the code is shown directly + // User enters the code into the ui + // User is redirected to the app (default redirect url) + await loginWithPasswordAndTOTP(page, user.getUsername(), user.getPassword(), user.getSecret()); + await loginScreenExpect(page, user.getFullName()); +}); + +test("username, password and totp otp login, wrong code", async ({ user, page }) => { + // Given totp is enabled on the organization of the user + // Given the user has only totp configured as second factor + // User enters username + // User enters password + // Screen for entering the code is shown directly + // User enters a wrond code + // Error message - "Invalid code" is shown + const c = "wrongcode"; + await loginWithPassword(page, user.getUsername(), user.getPassword()); + await code(page, c); + await codeScreenExpect(page, c); +}); + +test("username, password and totp login, multiple mfa options", async ({ page }) => { + // Given totp and email otp is enabled on the organization of the user + // Given the user has totp and email otp configured as second factor + // User enters username + // User enters password + // Screen for entering the code is shown directly + // Button to switch to email otp is shown + // User clicks button to use email otp instead + // User receives an email with a verification code + // User enters code in ui + // User is redirected to the app (default redirect url) +}); diff --git a/acceptance/tests/username-password-u2f.spec.ts b/acceptance/tests/username-password-u2f.spec.ts new file mode 100644 index 0000000000..f6f918478f --- /dev/null +++ b/acceptance/tests/username-password-u2f.spec.ts @@ -0,0 +1,24 @@ +import { test } from "@playwright/test"; + +test("username, password and u2f login", async ({ page }) => { + // Given u2f is enabled on the organization of the user + // Given the user has only u2f configured as second factor + // User enters username + // User enters password + // Popup for u2f is directly opened + // User verifies u2f + // User is redirected to the app (default redirect url) +}); + +test("username, password and u2f login, multiple mfa options", async ({ page }) => { + // Given u2f and semailms otp is enabled on the organization of the user + // Given the user has u2f and email otp configured as second factor + // User enters username + // User enters password + // Popup for u2f is directly opened + // User aborts u2f verification + // User clicks button to use email otp as second factor + // User receives an email with a verification code + // User enters code in ui + // User is redirected to the app (default redirect url) +}); diff --git a/acceptance/tests/username-password.spec.ts b/acceptance/tests/username-password.spec.ts new file mode 100644 index 0000000000..fcb6aad037 --- /dev/null +++ b/acceptance/tests/username-password.spec.ts @@ -0,0 +1,142 @@ +import { faker } from "@faker-js/faker"; +import { test as base } from "@playwright/test"; +import dotenv from "dotenv"; +import path from "path"; +import { loginScreenExpect, loginWithPassword, startLogin } from "./login"; +import { loginname } from "./loginname"; +import { loginnameScreenExpect } from "./loginname-screen"; +import { password } from "./password"; +import { passwordScreenExpect } from "./password-screen"; +import { PasswordUser } from "./user"; + +// Read from ".env" file. +dotenv.config({ path: path.resolve(__dirname, ".env.local") }); + +const test = base.extend<{ user: PasswordUser }>({ + user: async ({ page }, use) => { + const user = new PasswordUser({ + email: faker.internet.email(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + organization: "", + phone: faker.phone.number(), + password: "Password1!", + }); + await user.ensure(page); + await use(user); + await user.cleanup(); + }, +}); + +test("username and password login", async ({ user, page }) => { + await loginWithPassword(page, user.getUsername(), user.getPassword()); + await loginScreenExpect(page, user.getFullName()); +}); + +test("username and password login, unknown username", async ({ page }) => { + const username = "unknown"; + await startLogin(page); + await loginname(page, username); + await loginnameScreenExpect(page, username); +}); + +test("username and password login, wrong password", async ({ user, page }) => { + await startLogin(page); + await loginname(page, user.getUsername()); + await password(page, "wrong"); + await passwordScreenExpect(page, "wrong"); +}); + +test("username and password login, wrong username, ignore unknown usernames", async ({ user, page }) => { + // Given user doesn't exist but ignore unknown usernames setting is set to true + // Given username password login is enabled on the users organization + // enter login name + // enter password + // redirect to loginname page --> error message username or password wrong +}); + +test("username and password login, initial password change", async ({ user, page }) => { + // Given user is created and has changePassword set to true + // Given username password login is enabled on the users organization + // enter login name + // enter password + // create new password +}); + +test("username and password login, reset password hidden", async ({ user, page }) => { + // Given the organization has enabled "Password reset hidden" in the login policy + // Given username password login is enabled on the users organization + // enter login name + // password reset link should not be shown on password screen +}); + +test("username and password login, reset password - enter code manually", async ({ user, page }) => { + // Given user has forgotten password and clicks the forgot password button + // Given username password login is enabled on the users organization + // enter login name + // click password forgotten + // enter code from email + // user is redirected to app (default redirect url) +}); + +test("username and password login, reset password - click link", async ({ user, page }) => { + // Given user has forgotten password and clicks the forgot password button, and then the link in the email + // Given username password login is enabled on the users organization + // enter login name + // click password forgotten + // click link in email + // set new password + // redirect to app (default redirect url) +}); + +test("username and password login, reset password, resend code", async ({ user, page }) => { + // Given user has forgotten password and clicks the forgot password button and then resend code + // Given username password login is enabled on the users organization + // enter login name + // click password forgotten + // click resend code + // enter code from second email + // user is redirected to app (default redirect url) +}); + +test("email login enabled", async ({ user, page }) => { + // Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists + // Given no other user with the same email address exists + // enter email address "test@zitadel.com " in login screen + // user will get to password screen +}); + +test("email login disabled", async ({ user, page }) => { + // Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists + // Given no other user with the same email address exists + // enter email address "test@zitadel.com" in login screen + // user will see error message "user not found" +}); + +test("email login enabled - multiple users", async ({ user, page }) => { + // Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists + // Given a second user with the username "testuser2", email test@zitadel.com and phone number 0711111111 exists + // enter email address "test@zitadel.com" in login screen + // user will see error message "user not found" +}); + +test("phone login enabled", async ({ user, page }) => { + // Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists + // Given no other user with the same phon number exists + // enter phone number "0711111111" in login screen + // user will get to password screen +}); + +test("phone login disabled", async ({ user, page }) => { + // Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists + // Given no other user with the same phone number exists + // enter phone number "0711111111" in login screen + // user will see error message "user not found" +}); + +test("phone login enabled - multiple users", async ({ user, page }) => { + // Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists + // Given a second user with the username "testuser2", email test@zitadel.com and phone number 0711111111 exists + // enter phone number "0711111111" in login screen + // user will see error message "user not found" +}); diff --git a/acceptance/tests/welcome.ts b/acceptance/tests/welcome.ts new file mode 100644 index 0000000000..7ff6b7d1c5 --- /dev/null +++ b/acceptance/tests/welcome.ts @@ -0,0 +1,6 @@ +import { test } from "@playwright/test"; + +test("login is accessible", async ({ page }) => { + await page.goto("http://localhost:3000/"); + await page.getByRole("heading", { name: "Welcome back!" }).isVisible(); +}); diff --git a/acceptance/tests/zitadel.ts b/acceptance/tests/zitadel.ts new file mode 100644 index 0000000000..587723f10d --- /dev/null +++ b/acceptance/tests/zitadel.ts @@ -0,0 +1,159 @@ +import { Authenticator } from "@otplib/core"; +import { createDigest, createRandomBytes } from "@otplib/plugin-crypto"; +import { keyDecoder, keyEncoder } from "@otplib/plugin-thirty-two"; // use your chosen base32 plugin +import axios from "axios"; +import { OtpType, userProps } from "./user"; + +export async function addUser(props: userProps) { + const body = { + username: props.email, + organization: { + orgId: props.organization, + }, + profile: { + givenName: props.firstName, + familyName: props.lastName, + }, + email: { + email: props.email, + isVerified: true, + }, + phone: { + phone: props.phone!, + isVerified: true, + }, + password: { + password: props.password!, + }, + }; + + return await listCall(`${process.env.ZITADEL_API_URL}/v2/users/human`, body); +} + +export async function removeUserByUsername(username: string) { + const resp = await getUserByUsername(username); + if (!resp || !resp.result || !resp.result[0]) { + return; + } + await removeUser(resp.result[0].userId); +} + +export async function removeUser(id: string) { + await deleteCall(`${process.env.ZITADEL_API_URL}/v2/users/${id}`); +} + +async function deleteCall(url: string) { + try { + const response = await axios.delete(url, { + headers: { + Authorization: `Bearer ${process.env.ZITADEL_SERVICE_USER_TOKEN}`, + }, + }); + + if (response.status >= 400 && response.status !== 404) { + const error = `HTTP Error: ${response.status} - ${response.statusText}`; + console.error(error); + throw new Error(error); + } + } catch (error) { + console.error("Error making request:", error); + throw error; + } +} + +export async function getUserByUsername(username: string): Promise { + const listUsersBody = { + queries: [ + { + userNameQuery: { + userName: username, + }, + }, + ], + }; + + return await listCall(`${process.env.ZITADEL_API_URL}/v2/users`, listUsersBody); +} + +async function listCall(url: string, data: any): Promise { + try { + const response = await axios.post(url, data, { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.ZITADEL_SERVICE_USER_TOKEN}`, + }, + }); + + if (response.status >= 400) { + const error = `HTTP Error: ${response.status} - ${response.statusText}`; + console.error(error); + throw new Error(error); + } + + return response.data; + } catch (error) { + console.error("Error making request:", error); + throw error; + } +} + +export async function activateOTP(userId: string, type: OtpType) { + let url = "otp_"; + switch (type) { + case OtpType.sms: + url = url + "sms"; + break; + case OtpType.email: + url = url + "email"; + break; + } + + await pushCall(`${process.env.ZITADEL_API_URL}/v2/users/${userId}/${url}`, {}); +} + +async function pushCall(url: string, data: any) { + try { + const response = await axios.post(url, data, { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.ZITADEL_SERVICE_USER_TOKEN}`, + }, + }); + + if (response.status >= 400) { + const error = `HTTP Error: ${response.status} - ${response.statusText}`; + console.error(error); + throw new Error(error); + } + } catch (error) { + console.error("Error making request:", error); + throw error; + } +} + +export async function addTOTP(userId: string): Promise { + const response = await listCall(`${process.env.ZITADEL_API_URL}/v2/users/${userId}/totp`, {}); + const code = totp(response.secret); + await pushCall(`${process.env.ZITADEL_API_URL}/v2/users/${userId}/totp/verify`, { code: code }); + return response.secret; +} + +export function totp(secret: string) { + const authenticator = new Authenticator({ + createDigest, + createRandomBytes, + keyDecoder, + keyEncoder, + }); + // google authenticator usage + const token = authenticator.generate(secret); + + // check if token can be used + if (!authenticator.verify({ token: token, secret: secret })) { + const error = `Generated token could not be verified`; + console.error(error); + throw new Error(error); + } + + return token; +} diff --git a/acceptance/zitadel.yaml b/acceptance/zitadel.yaml index 2b471db040..5a17264eb6 100644 --- a/acceptance/zitadel.yaml +++ b/acceptance/zitadel.yaml @@ -1,22 +1,49 @@ FirstInstance: - MachineKeyPath: /machinekey/zitadel-admin-sa.json + PatPath: /pat/zitadel-admin-sa.pat Org: + Human: + UserName: zitadel-admin + FirstName: ZITADEL + LastName: Admin + Password: Password1! + PasswordChangeRequired: false + PreferredLanguage: en Machine: Machine: Username: zitadel-admin-sa Name: Admin - MachineKey: - Type: 1 + Pat: + ExpirationDate: 2099-01-01T00:00:00Z + +DefaultInstance: + PrivacyPolicy: + TOSLink: "https://zitadel.com/docs/legal/terms-of-service" + PrivacyLink: "https://zitadel.com/docs/legal/policies/privacy-policy" + HelpLink: "https://zitadel.com/docs" + SupportEmail: "support@zitadel.com" + DocsLink: "https://zitadel.com/docs" Database: - Cockroach: + EventPushConnRatio: 0.2 # 4 + ProjectionSpoolerConnRatio: 0.3 # 6 + postgres: Host: db + Port: 5432 + Database: zitadel + MaxOpenConns: 20 + MaxIdleConns: 20 + MaxConnLifetime: 1h + MaxConnIdleTime: 5m + User: + Username: zitadel + SSL: + Mode: disable + Admin: + Username: zitadel + SSL: + Mode: disable Logstore: Access: Stdout: Enabled: true - -DefaultInstance: - LoginPolicy: - MfaInitSkipLifetime: 0h \ No newline at end of file diff --git a/apps/login/.env.integration b/apps/login/.env.integration index 44e8f7b6bf..90adb84eee 100644 --- a/apps/login/.env.integration +++ b/apps/login/.env.integration @@ -1 +1,3 @@ -ZITADEL_API_URL=http://localhost:22222 \ No newline at end of file +ZITADEL_API_URL=http://localhost:22222 +EMAIL_VERIFICATION=true +DEBUG=true \ No newline at end of file diff --git a/apps/login/.eslintrc.js b/apps/login/.eslintrc.cjs similarity index 71% rename from apps/login/.eslintrc.js rename to apps/login/.eslintrc.cjs index 8cf53b3cd8..f5383dd47a 100755 --- a/apps/login/.eslintrc.js +++ b/apps/login/.eslintrc.cjs @@ -4,4 +4,9 @@ module.exports = { rules: { "@next/next/no-html-link-for-pages": "off", }, + settings: { + react: { + version: "detect", + }, + }, }; diff --git a/apps/login/.gitignore b/apps/login/.gitignore new file mode 100644 index 0000000000..63ddd0c9eb --- /dev/null +++ b/apps/login/.gitignore @@ -0,0 +1,2 @@ +custom-config.js +.env.local \ No newline at end of file diff --git a/apps/login/cypress/integration/invite.cy.ts b/apps/login/cypress/integration/invite.cy.ts new file mode 100644 index 0000000000..3014f5a2e5 --- /dev/null +++ b/apps/login/cypress/integration/invite.cy.ts @@ -0,0 +1,114 @@ +import { stub } from "../support/mock"; + +describe("verify invite", () => { + beforeEach(() => { + stub("zitadel.org.v2.OrganizationService", "ListOrganizations", { + data: { + details: { + totalResult: 1, + }, + result: [{ id: "256088834543534543" }], + }, + }); + + stub("zitadel.user.v2.UserService", "ListAuthenticationMethodTypes", { + data: { + authMethodTypes: [], // user with no auth methods was invited + }, + }); + + stub("zitadel.user.v2.UserService", "GetUserByID", { + data: { + user: { + userId: "221394658884845598", + state: 1, + username: "john@zitadel.com", + loginNames: ["john@zitadel.com"], + preferredLoginName: "john@zitadel.com", + human: { + userId: "221394658884845598", + state: 1, + username: "john@zitadel.com", + loginNames: ["john@zitadel.com"], + preferredLoginName: "john@zitadel.com", + profile: { + givenName: "John", + familyName: "Doe", + avatarUrl: "https://zitadel.com/avatar.jpg", + }, + email: { + email: "john@zitadel.com", + isVerified: false, + }, + }, + }, + }, + }); + + stub("zitadel.session.v2.SessionService", "CreateSession", { + data: { + details: { + sequence: 859, + changeDate: new Date("2024-04-04T09:40:55.577Z"), + resourceOwner: "220516472055706145", + }, + sessionId: "221394658884845598", + sessionToken: + "SDMc7DlYXPgwRJ-Tb5NlLqynysHjEae3csWsKzoZWLplRji0AYY3HgAkrUEBqtLCvOayLJPMd0ax4Q", + challenges: undefined, + }, + }); + + stub("zitadel.session.v2.SessionService", "GetSession", { + data: { + session: { + id: "221394658884845598", + creationDate: new Date("2024-04-04T09:40:55.577Z"), + changeDate: new Date("2024-04-04T09:40:55.577Z"), + sequence: 859, + factors: { + user: { + id: "221394658884845598", + loginName: "john@zitadel.com", + }, + password: undefined, + webAuthN: undefined, + intent: undefined, + }, + metadata: {}, + }, + }, + }); + + stub("zitadel.settings.v2.SettingsService", "GetLoginSettings", { + data: { + settings: { + passkeysType: 1, + allowUsernamePassword: true, + }, + }, + }); + }); + + it.only("shows authenticators after successful invite verification", () => { + stub("zitadel.user.v2.UserService", "VerifyInviteCode"); + + cy.visit("/verify?userId=221394658884845598&code=abc&invite=true"); + cy.location("pathname", { timeout: 10_000 }).should( + "eq", + "/authenticator/set", + ); + }); + + it("shows an error if invite code validation failed", () => { + stub("zitadel.user.v2.UserService", "VerifyInviteCode", { + code: 3, + error: "error validating code", + }); + + // TODO: Avoid uncaught exception in application + cy.once("uncaught:exception", () => false); + cy.visit("/verify?userId=221394658884845598&code=abc&invite=true"); + cy.contains("Could not verify invite", { timeout: 10_000 }); + }); +}); diff --git a/apps/login/cypress/integration/login.cy.ts b/apps/login/cypress/integration/login.cy.ts index d4edf570d6..3e74c0f7fe 100644 --- a/apps/login/cypress/integration/login.cy.ts +++ b/apps/login/cypress/integration/login.cy.ts @@ -2,6 +2,14 @@ import { stub } from "../support/mock"; describe("login", () => { beforeEach(() => { + stub("zitadel.org.v2.OrganizationService", "ListOrganizations", { + data: { + details: { + totalResult: 1, + }, + result: [{ id: "256088834543534543" }], + }, + }); stub("zitadel.session.v2.SessionService", "CreateSession", { data: { details: { @@ -41,6 +49,7 @@ describe("login", () => { data: { settings: { passkeysType: 1, + allowUsernamePassword: true, }, }, }); @@ -104,16 +113,16 @@ describe("login", () => { }, }); }); - it("should prompt a user to setup passwordless authentication if passkey is allowed in the login settings", () => { - cy.visit("/loginname?loginName=john%40zitadel.com&submit=true"); - cy.location("pathname", { timeout: 10_000 }).should("eq", "/password"); - cy.get('input[type="password"]').focus().type("MyStrongPassword!1"); - cy.get('button[type="submit"]').click(); - cy.location("pathname", { timeout: 10_000 }).should( - "eq", - "/passkey/add", - ); - }); + // it("should prompt a user to setup passwordless authentication if passkey is allowed in the login settings", () => { + // cy.visit("/loginname?loginName=john%40zitadel.com&submit=true"); + // cy.location("pathname", { timeout: 10_000 }).should("eq", "/password"); + // cy.get('input[type="password"]').focus().type("MyStrongPassword!1"); + // cy.get('button[type="submit"]').click(); + // cy.location("pathname", { timeout: 10_000 }).should( + // "eq", + // "/passkey/set", + // ); + // }); }); }); describe("passkey login", () => { @@ -156,12 +165,10 @@ describe("login", () => { }, }); }); - it("should redirect a user with passwordless authentication to /passkey/login", () => { + + it("should redirect a user with passwordless authentication to /passkey", () => { cy.visit("/loginname?loginName=john%40zitadel.com&submit=true"); - cy.location("pathname", { timeout: 10_000 }).should( - "eq", - "/passkey/login", - ); + cy.location("pathname", { timeout: 10_000 }).should("eq", "/passkey"); }); }); }); diff --git a/apps/login/cypress/integration/register.cy.ts b/apps/login/cypress/integration/register.cy.ts index 4cad0dbd81..262302c4c3 100644 --- a/apps/login/cypress/integration/register.cy.ts +++ b/apps/login/cypress/integration/register.cy.ts @@ -2,21 +2,75 @@ import { stub } from "../support/mock"; describe("register", () => { beforeEach(() => { + stub("zitadel.org.v2.OrganizationService", "ListOrganizations", { + data: { + details: { + totalResult: 1, + }, + result: [{ id: "256088834543534543" }], + }, + }); + stub("zitadel.settings.v2.SettingsService", "GetLoginSettings", { + data: { + settings: { + passkeysType: 1, + allowRegister: true, + allowUsernamePassword: true, + defaultRedirectUri: "", + }, + }, + }); stub("zitadel.user.v2.UserService", "AddHumanUser", { data: { - userId: "123", + userId: "221394658884845598", + }, + }); + stub("zitadel.session.v2.SessionService", "CreateSession", { + data: { + details: { + sequence: 859, + changeDate: new Date("2024-04-04T09:40:55.577Z"), + resourceOwner: "220516472055706145", + }, + sessionId: "221394658884845598", + sessionToken: + "SDMc7DlYXPgwRJ-Tb5NlLqynysHjEae3csWsKzoZWLplRji0AYY3HgAkrUEBqtLCvOayLJPMd0ax4Q", + challenges: undefined, + }, + }); + + stub("zitadel.session.v2.SessionService", "GetSession", { + data: { + session: { + id: "221394658884845598", + creationDate: new Date("2024-04-04T09:40:55.577Z"), + changeDate: new Date("2024-04-04T09:40:55.577Z"), + sequence: 859, + factors: { + user: { + id: "221394658884845598", + loginName: "john@zitadel.com", + }, + password: undefined, + webAuthN: undefined, + intent: undefined, + }, + metadata: {}, + }, }, }); }); - it("should redirect a user who selects passwordless on register to /passkeys/add", () => { + it("should redirect a user who selects passwordless on register to /passkey/set", () => { cy.visit("/register"); - cy.get('input[autocomplete="firstname"]').focus().type("John"); - cy.get('input[autocomplete="lastname"]').focus().type("Doe"); - cy.get('input[autocomplete="email"]').focus().type("john@zitadel.com"); + cy.get('input[data-testid="firstname-text-input"]').focus().type("John"); + cy.get('input[data-testid="lastname-text-input"]').focus().type("Doe"); + cy.get('input[data-testid="email-text-input"]') + .focus() + .type("john@zitadel.com"); cy.get('input[type="checkbox"][value="privacypolicy"]').check(); cy.get('input[type="checkbox"][value="tos"]').check(); cy.get('button[type="submit"]').click(); - cy.location("pathname", { timeout: 10_000 }).should("eq", "/passkey/add"); + cy.location("pathname", { timeout: 10_000 }).should("eq", "/passkey/set"); }); }); diff --git a/apps/login/cypress/integration/verify.cy.ts b/apps/login/cypress/integration/verify.cy.ts index fbf9a7c39d..464bf02e59 100644 --- a/apps/login/cypress/integration/verify.cy.ts +++ b/apps/login/cypress/integration/verify.cy.ts @@ -1,19 +1,96 @@ import { stub } from "../support/mock"; -describe("/verify", () => { - it("redirects after successful email verification", () => { - stub("zitadel.user.v2.UserService", "VerifyEmail"); - cy.visit("/verify?userId=123&code=abc&submit=true"); - cy.location("pathname", { timeout: 10_000 }).should("eq", "/loginname"); +describe("verify email", () => { + beforeEach(() => { + stub("zitadel.org.v2.OrganizationService", "ListOrganizations", { + data: { + details: { + totalResult: 1, + }, + result: [{ id: "256088834543534543" }], + }, + }); + + stub("zitadel.user.v2.UserService", "ListAuthenticationMethodTypes", { + data: { + authMethodTypes: [1], // set one method such that we know that the user was not invited + }, + }); + + stub("zitadel.user.v2.UserService", "SendEmailCode"); + + stub("zitadel.user.v2.UserService", "GetUserByID", { + data: { + user: { + userId: "221394658884845598", + state: 1, + username: "john@zitadel.com", + loginNames: ["john@zitadel.com"], + preferredLoginName: "john@zitadel.com", + human: { + userId: "221394658884845598", + state: 1, + username: "john@zitadel.com", + loginNames: ["john@zitadel.com"], + preferredLoginName: "john@zitadel.com", + profile: { + givenName: "John", + familyName: "Doe", + avatarUrl: "https://zitadel.com/avatar.jpg", + }, + email: { + email: "john@zitadel.com", + isVerified: false, // email is not verified yet + }, + }, + }, + }, + }); + + stub("zitadel.session.v2.SessionService", "CreateSession", { + data: { + details: { + sequence: 859, + changeDate: new Date("2024-04-04T09:40:55.577Z"), + resourceOwner: "220516472055706145", + }, + sessionId: "221394658884845598", + sessionToken: + "SDMc7DlYXPgwRJ-Tb5NlLqynysHjEae3csWsKzoZWLplRji0AYY3HgAkrUEBqtLCvOayLJPMd0ax4Q", + challenges: undefined, + }, + }); + + stub("zitadel.session.v2.SessionService", "GetSession", { + data: { + session: { + id: "221394658884845598", + creationDate: new Date("2024-04-04T09:40:55.577Z"), + changeDate: new Date("2024-04-04T09:40:55.577Z"), + sequence: 859, + factors: { + user: { + id: "221394658884845598", + loginName: "john@zitadel.com", + }, + password: undefined, + webAuthN: undefined, + intent: undefined, + }, + metadata: {}, + }, + }, + }); }); - it("shows an error if validation failed", () => { + + it("shows an error if email code validation failed", () => { stub("zitadel.user.v2.UserService", "VerifyEmail", { code: 3, error: "error validating code", }); // TODO: Avoid uncaught exception in application cy.once("uncaught:exception", () => false); - cy.visit("/verify?userId=123&code=abc&submit=true"); - cy.contains("error validating code"); + cy.visit("/verify?userId=221394658884845598&code=abc"); + cy.contains("Could not verify email", { timeout: 10_000 }); }); }); diff --git a/apps/login/locales/de.json b/apps/login/locales/de.json new file mode 100644 index 0000000000..5e8756c89b --- /dev/null +++ b/apps/login/locales/de.json @@ -0,0 +1,195 @@ +{ + "common": { + "back": "Zurück" + }, + "accounts": { + "title": "Konten", + "description": "Wählen Sie das Konto aus, das Sie verwenden möchten.", + "addAnother": "Ein weiteres Konto hinzufügen", + "noResults": "Keine Konten gefunden" + }, + "loginname": { + "title": "Willkommen zurück!", + "description": "Geben Sie Ihre Anmeldedaten ein.", + "register": "Neuen Benutzer registrieren" + }, + "password": { + "verify": { + "title": "Passwort", + "description": "Geben Sie Ihr Passwort ein.", + "resetPassword": "Passwort zurücksetzen", + "submit": "Weiter" + }, + "set": { + "title": "Passwort festlegen", + "description": "Legen Sie das Passwort für Ihr Konto fest", + "codeSent": "Ein Code wurde an Ihre E-Mail-Adresse gesendet.", + "noCodeReceived": "Keinen Code erhalten?", + "resend": "Erneut senden", + "submit": "Weiter" + }, + "change": { + "title": "Passwort ändern", + "description": "Legen Sie das Passwort für Ihr Konto fest", + "submit": "Weiter" + } + }, + "idp": { + "title": "Mit SSO anmelden", + "description": "Wählen Sie einen der folgenden Anbieter, um sich anzumelden", + "signInWithApple": "Mit Apple anmelden", + "signInWithGoogle": "Mit Google anmelden", + "signInWithAzureAD": "Mit AzureAD anmelden", + "signInWithGithub": "Mit GitHub anmelden", + "signInWithGitlab": "Mit GitLab anmelden", + "loginSuccess": { + "title": "Anmeldung erfolgreich", + "description": "Sie haben sich erfolgreich angemeldet!" + }, + "linkingSuccess": { + "title": "Konto verknüpft", + "description": "Sie haben Ihr Konto erfolgreich verknüpft!" + }, + "registerSuccess": { + "title": "Registrierung erfolgreich", + "description": "Sie haben sich erfolgreich registriert!" + }, + "loginError": { + "title": "Anmeldung fehlgeschlagen", + "description": "Beim Anmelden ist ein Fehler aufgetreten." + }, + "linkingError": { + "title": "Konto-Verknüpfung fehlgeschlagen", + "description": "Beim Verknüpfen Ihres Kontos ist ein Fehler aufgetreten." + } + }, + "mfa": { + "verify": { + "title": "Bestätigen Sie Ihre Identität", + "description": "Wählen Sie einen der folgenden Faktoren.", + "noResults": "Keine zweiten Faktoren verfügbar, um sie einzurichten." + }, + "set": { + "title": "2-Faktor einrichten", + "description": "Wählen Sie einen der folgenden zweiten Faktoren." + } + }, + "otp": { + "verify": { + "title": "2-Faktor bestätigen", + "totpDescription": "Geben Sie den Code aus Ihrer Authentifizierungs-App ein.", + "smsDescription": "Geben Sie den Code ein, den Sie per SMS erhalten haben.", + "emailDescription": "Geben Sie den Code ein, den Sie per E-Mail erhalten haben.", + "noCodeReceived": "Keinen Code erhalten?", + "resendCode": "Code erneut senden", + "submit": "Weiter" + }, + "set": { + "title": "2-Faktor einrichten", + "totpDescription": "Scannen Sie den QR-Code mit Ihrer Authentifizierungs-App.", + "smsDescription": "Geben Sie Ihre Telefonnummer ein, um einen Code per SMS zu erhalten.", + "emailDescription": "Geben Sie Ihre E-Mail-Adresse ein, um einen Code per E-Mail zu erhalten.", + "totpRegisterDescription": "Scannen Sie den QR-Code oder navigieren Sie manuell zur URL.", + "submit": "Weiter" + } + }, + "passkey": { + "verify": { + "title": "Mit einem Passkey authentifizieren", + "description": "Ihr Gerät wird nach Ihrem Fingerabdruck, Gesicht oder Bildschirmsperre fragen", + "usePassword": "Passwort verwenden", + "submit": "Weiter" + }, + "set": { + "title": "Passkey einrichten", + "description": "Ihr Gerät wird nach Ihrem Fingerabdruck, Gesicht oder Bildschirmsperre fragen", + "info": { + "description": "Ein Passkey ist eine Authentifizierungsmethode auf einem Gerät wie Ihr Fingerabdruck, Apple FaceID oder ähnliches.", + "link": "Passwortlose Authentifizierung" + }, + "skip": "Überspringen", + "submit": "Weiter" + } + }, + "u2f": { + "verify": { + "title": "2-Faktor bestätigen", + "description": "Bestätigen Sie Ihr Konto mit Ihrem Gerät." + }, + "set": { + "title": "2-Faktor einrichten", + "description": "Richten Sie ein Gerät als zweiten Faktor ein.", + "submit": "Weiter" + } + }, + "register": { + "methods": { + "passkey": "Passkey", + "password": "Password" + }, + "disabled": { + "title": "Registrierung deaktiviert", + "description": "Die Registrierung ist deaktiviert. Bitte wenden Sie sich an den Administrator." + }, + "missingdata": { + "title": "Registrierung fehlgeschlagen", + "description": "Einige Daten fehlen. Bitte überprüfen Sie Ihre Eingaben." + }, + "title": "Registrieren", + "description": "Erstellen Sie Ihr ZITADEL-Konto.", + "selectMethod": "Wählen Sie die Methode, mit der Sie sich authentifizieren möchten", + "agreeTo": "Um sich zu registrieren, müssen Sie den Nutzungsbedingungen zustimmen", + "termsOfService": "Nutzungsbedingungen", + "privacyPolicy": "Datenschutzrichtlinie", + "submit": "Weiter", + "password": { + "title": "Passwort festlegen", + "description": "Legen Sie das Passwort für Ihr Konto fest", + "submit": "Weiter" + } + }, + "invite": { + "title": "Benutzer einladen", + "description": "Geben Sie die E-Mail-Adresse des Benutzers ein, den Sie einladen möchten.", + "info": "Der Benutzer erhält eine E-Mail mit einem Link, um sich zu registrieren.", + "notAllowed": "Sie haben keine Berechtigung, Benutzer einzuladen.", + "submit": "Einladen", + "success": { + "title": "Einladung erfolgreich", + "description": "Der Benutzer wurde erfolgreich eingeladen.", + "verified": "Der Benutzer wurde eingeladen und hat seine E-Mail bereits verifiziert.", + "notVerifiedYet": "Der Benutzer wurde eingeladen. Er erhält eine E-Mail mit weiteren Anweisungen.", + "submit": "Weiteren Benutzer einladen" + } + }, + "signedin": { + "title": "Willkommen {user}!", + "description": "Sie sind angemeldet.", + "continue": "Weiter" + }, + "verify": { + "userIdMissing": "Keine Benutzer-ID angegeben!", + "success": "Erfolgreich verifiziert", + "setupAuthenticator": "Authentifikator einrichten", + "verify": { + "title": "Benutzer verifizieren", + "description": "Geben Sie den Code ein, der in der Bestätigungs-E-Mail angegeben ist.", + "noCodeReceived": "Keinen Code erhalten?", + "resendCode": "Code erneut senden", + "submit": "Weiter" + } + }, + "authenticator": { + "title": "Authentifizierungsmethode auswählen", + "description": "Wählen Sie die Methode, mit der Sie sich authentifizieren möchten.", + "noMethodsAvailable": "Keine Authentifizierungsmethoden verfügbar", + "allSetup": "Sie haben bereits einen Authentifikator eingerichtet!", + "linkWithIDP": "oder verknüpfe mit einem Identitätsanbieter" + }, + "error": { + "unknownContext": "Der Kontext des Benutzers konnte nicht ermittelt werden. Stellen Sie sicher, dass Sie zuerst den Benutzernamen eingeben oder einen loginName als Suchparameter angeben.", + "sessionExpired": "Ihre aktuelle Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.", + "failedLoading": "Daten konnten nicht geladen werden. Bitte versuchen Sie es erneut.", + "tryagain": "Erneut versuchen" + } +} diff --git a/apps/login/locales/en.json b/apps/login/locales/en.json new file mode 100644 index 0000000000..3101f222d5 --- /dev/null +++ b/apps/login/locales/en.json @@ -0,0 +1,195 @@ +{ + "common": { + "back": "Back" + }, + "accounts": { + "title": "Accounts", + "description": "Select the account you want to use.", + "addAnother": "Add another account", + "noResults": "No accounts found" + }, + "loginname": { + "title": "Welcome back!", + "description": "Enter your login data.", + "register": "Register new user" + }, + "password": { + "verify": { + "title": "Password", + "description": "Enter your password.", + "resetPassword": "Reset Password", + "submit": "Continue" + }, + "set": { + "title": "Set Password", + "description": "Set the password for your account", + "codeSent": "A code has been sent to your email address.", + "noCodeReceived": "Didn't receive a code?", + "resend": "Resend code", + "submit": "Continue" + }, + "change": { + "title": "Change Password", + "description": "Set the password for your account", + "submit": "Continue" + } + }, + "idp": { + "title": "Sign in with SSO", + "description": "Select one of the following providers to sign in", + "signInWithApple": "Sign in with Apple", + "signInWithGoogle": "Sign in with Google", + "signInWithAzureAD": "Sign in with AzureAD", + "signInWithGithub": "Sign in with GitHub", + "signInWithGitlab": "Sign in with GitLab", + "loginSuccess": { + "title": "Login successful", + "description": "You have successfully been loggedIn!" + }, + "linkingSuccess": { + "title": "Account linked", + "description": "You have successfully linked your account!" + }, + "registerSuccess": { + "title": "Registration successful", + "description": "You have successfully registered!" + }, + "loginError": { + "title": "Login failed", + "description": "An error occurred while trying to login." + }, + "linkingError": { + "title": "Account linking failed", + "description": "An error occurred while trying to link your account." + } + }, + "mfa": { + "verify": { + "title": "Verify your identity", + "description": "Choose one of the following factors.", + "noResults": "No second factors available to setup." + }, + "set": { + "title": "Set up 2-Factor", + "description": "Choose one of the following second factors." + } + }, + "otp": { + "verify": { + "title": "Verify 2-Factor", + "totpDescription": "Enter the code from your authenticator app.", + "smsDescription": "Enter the code you received via SMS.", + "emailDescription": "Enter the code you received via email.", + "noCodeReceived": "Didn't receive a code?", + "resendCode": "Resend code", + "submit": "Continue" + }, + "set": { + "title": "Set up 2-Factor", + "totpDescription": "Scan the QR code with your authenticator app.", + "smsDescription": "Enter your phone number to receive a code via SMS.", + "emailDescription": "Enter your email address to receive a code via email.", + "totpRegisterDescription": "Scan the QR Code or navigate to the URL manually.", + "submit": "Continue" + } + }, + "passkey": { + "verify": { + "title": "Authenticate with a passkey", + "description": "Your device will ask for your fingerprint, face, or screen lock", + "usePassword": "Use password", + "submit": "Continue" + }, + "set": { + "title": "Setup a passkey", + "description": "Your device will ask for your fingerprint, face, or screen lock", + "info": { + "description": "A passkey is an authentication method on a device like your fingerprint, Apple FaceID or similar. ", + "link": "Passwordless Authentication" + }, + "skip": "Skip", + "submit": "Continue" + } + }, + "u2f": { + "verify": { + "title": "Verify 2-Factor", + "description": "Verify your account with your device." + }, + "set": { + "title": "Set up 2-Factor", + "description": "Set up a device as a second factor.", + "submit": "Continue" + } + }, + "register": { + "methods": { + "passkey": "Passkey", + "password": "Password" + }, + "disabled": { + "title": "Registration disabled", + "description": "The registration is disabled. Please contact your administrator." + }, + "missingdata": { + "title": "Missing data", + "description": "Provide email, first and last name to register." + }, + "title": "Register", + "description": "Create your ZITADEL account.", + "selectMethod": "Select the method you would like to authenticate", + "agreeTo": "To register you must agree to the terms and conditions", + "termsOfService": "Terms of Service", + "privacyPolicy": "Privacy Policy", + "submit": "Continue", + "password": { + "title": "Set Password", + "description": "Set the password for your account", + "submit": "Continue" + } + }, + "invite": { + "title": "Invite User", + "description": "Provide the email address and the name of the user you want to invite.", + "info": "The user will receive an email with further instructions.", + "notAllowed": "Your settings do not allow you to invite users.", + "submit": "Continue", + "success": { + "title": "User invited", + "description": "The email has successfully been sent.", + "verified": "The user has been invited and has already verified his email.", + "notVerifiedYet": "The user has been invited. They will receive an email with further instructions.", + "submit": "Invite another user" + } + }, + "signedin": { + "title": "Welcome {user}!", + "description": "You are signed in.", + "continue": "Continue" + }, + "verify": { + "userIdMissing": "No userId provided!", + "success": "The user has been verified successfully.", + "setupAuthenticator": "Setup authenticator", + "verify": { + "title": "Verify user", + "description": "Enter the Code provided in the verification email.", + "noCodeReceived": "Didn't receive a code?", + "resendCode": "Resend code", + "submit": "Continue" + } + }, + "authenticator": { + "title": "Choose authentication method", + "description": "Select the method you would like to authenticate", + "noMethodsAvailable": "No authentication methods available", + "allSetup": "You have already setup an authenticator!", + "linkWithIDP": "or link with an Identity Provider" + }, + "error": { + "unknownContext": "Could not get the context of the user. Make sure to enter the username first or provide a loginName as searchParam.", + "sessionExpired": "Your current session has expired. Please login again.", + "failedLoading": "Failed to load data. Please try again.", + "tryagain": "Try Again" + } +} diff --git a/apps/login/locales/es.json b/apps/login/locales/es.json new file mode 100644 index 0000000000..5a9b6f6324 --- /dev/null +++ b/apps/login/locales/es.json @@ -0,0 +1,195 @@ +{ + "common": { + "back": "Atrás" + }, + "accounts": { + "title": "Cuentas", + "description": "Selecciona la cuenta que deseas usar.", + "addAnother": "Agregar otra cuenta", + "noResults": "No se encontraron cuentas" + }, + "loginname": { + "title": "¡Bienvenido de nuevo!", + "description": "Introduce tus datos de acceso.", + "register": "Registrar nuevo usuario" + }, + "password": { + "verify": { + "title": "Contraseña", + "description": "Introduce tu contraseña.", + "resetPassword": "Restablecer contraseña", + "submit": "Continuar" + }, + "set": { + "title": "Establecer Contraseña", + "description": "Establece la contraseña para tu cuenta", + "codeSent": "Se ha enviado un código a su correo electrónico.", + "noCodeReceived": "¿No recibiste un código?", + "resend": "Reenviar código", + "submit": "Continuar" + }, + "change": { + "title": "Cambiar Contraseña", + "description": "Establece la contraseña para tu cuenta", + "submit": "Continuar" + } + }, + "idp": { + "title": "Iniciar sesión con SSO", + "description": "Selecciona uno de los siguientes proveedores para iniciar sesión", + "signInWithApple": "Iniciar sesión con Apple", + "signInWithGoogle": "Iniciar sesión con Google", + "signInWithAzureAD": "Iniciar sesión con AzureAD", + "signInWithGithub": "Iniciar sesión con GitHub", + "signInWithGitlab": "Iniciar sesión con GitLab", + "loginSuccess": { + "title": "Inicio de sesión exitoso", + "description": "¡Has iniciado sesión con éxito!" + }, + "linkingSuccess": { + "title": "Cuenta vinculada", + "description": "¡Has vinculado tu cuenta con éxito!" + }, + "registerSuccess": { + "title": "Registro exitoso", + "description": "¡Te has registrado con éxito!" + }, + "loginError": { + "title": "Error de inicio de sesión", + "description": "Ocurrió un error al intentar iniciar sesión." + }, + "linkingError": { + "title": "Error al vincular la cuenta", + "description": "Ocurrió un error al intentar vincular tu cuenta." + } + }, + "mfa": { + "verify": { + "title": "Verifica tu identidad", + "description": "Elige uno de los siguientes factores.", + "noResults": "No hay factores secundarios disponibles para configurar." + }, + "set": { + "title": "Configurar autenticación de 2 factores", + "description": "Elige uno de los siguientes factores secundarios." + } + }, + "otp": { + "verify": { + "title": "Verificar autenticación de 2 factores", + "totpDescription": "Introduce el código de tu aplicación de autenticación.", + "smsDescription": "Introduce el código que recibiste por SMS.", + "emailDescription": "Introduce el código que recibiste por correo electrónico.", + "noCodeReceived": "¿No recibiste un código?", + "resendCode": "Reenviar código", + "submit": "Continuar" + }, + "set": { + "title": "Configurar autenticación de 2 factores", + "totpDescription": "Escanea el código QR con tu aplicación de autenticación.", + "smsDescription": "Introduce tu número de teléfono para recibir un código por SMS.", + "emailDescription": "Introduce tu dirección de correo electrónico para recibir un código por correo electrónico.", + "totpRegisterDescription": "Escanea el código QR o navega manualmente a la URL.", + "submit": "Continuar" + } + }, + "passkey": { + "verify": { + "title": "Autenticar con una clave de acceso", + "description": "Tu dispositivo pedirá tu huella digital, rostro o bloqueo de pantalla", + "usePassword": "Usar contraseña", + "submit": "Continuar" + }, + "set": { + "title": "Configurar una clave de acceso", + "description": "Tu dispositivo pedirá tu huella digital, rostro o bloqueo de pantalla", + "info": { + "description": "Una clave de acceso es un método de autenticación en un dispositivo como tu huella digital, Apple FaceID o similar.", + "link": "Autenticación sin contraseña" + }, + "skip": "Omitir", + "submit": "Continuar" + } + }, + "u2f": { + "verify": { + "title": "Verificar autenticación de 2 factores", + "description": "Verifica tu cuenta con tu dispositivo." + }, + "set": { + "title": "Configurar autenticación de 2 factores", + "description": "Configura un dispositivo como segundo factor.", + "submit": "Continuar" + } + }, + "register": { + "methods": { + "passkey": "Clave de acceso", + "password": "Contraseña" + }, + "disabled": { + "title": "Registro deshabilitado", + "description": "Registrarse está deshabilitado en este momento." + }, + "missingdata": { + "title": "Datos faltantes", + "description": "No se proporcionaron datos suficientes para el registro." + }, + "title": "Registrarse", + "description": "Crea tu cuenta ZITADEL.", + "selectMethod": "Selecciona el método con el que deseas autenticarte", + "agreeTo": "Para registrarte debes aceptar los términos y condiciones", + "termsOfService": "Términos de Servicio", + "privacyPolicy": "Política de Privacidad", + "submit": "Continuar", + "password": { + "title": "Establecer Contraseña", + "description": "Establece la contraseña para tu cuenta", + "submit": "Continuar" + } + }, + "invite": { + "title": "Invitar usuario", + "description": "Introduce el correo electrónico del usuario que deseas invitar.", + "info": "El usuario recibirá un correo electrónico con un enlace para completar el registro.", + "notAllowed": "No tienes permiso para invitar usuarios.", + "submit": "Invitar usuario", + "success": { + "title": "¡Usuario invitado!", + "description": "El usuario ha sido invitado.", + "verified": "El usuario ha sido invitado y ya ha verificado su correo electrónico.", + "notVerifiedYet": "El usuario ha sido invitado. Recibirá un correo electrónico con más instrucciones.", + "submit": "Invitar a otro usuario" + } + }, + "signedin": { + "title": "¡Bienvenido {user}!", + "description": "Has iniciado sesión.", + "continue": "Continuar" + }, + "verify": { + "userIdMissing": "¡No se proporcionó userId!", + "success": "¡Verificación exitosa!", + "setupAuthenticator": "Configurar autenticador", + "verify": { + "title": "Verificar usuario", + "description": "Introduce el código proporcionado en el correo electrónico de verificación.", + "noCodeReceived": "¿No recibiste un código?", + "resendCode": "Reenviar código", + "submit": "Continuar" + } + }, + "authenticator": { + "title": "Seleccionar método de autenticación", + "description": "Selecciona el método con el que deseas autenticarte", + "noMethodsAvailable": "No hay métodos de autenticación disponibles", + "allSetup": "¡Ya has configurado un autenticador!", + "linkWithIDP": "o vincúlalo con un proveedor de identidad" + }, + "error": { + "unknownContext": "No se pudo obtener el contexto del usuario. Asegúrate de ingresar primero el nombre de usuario o proporcionar un loginName como parámetro de búsqueda.", + "sessionExpired": "Tu sesión actual ha expirado. Por favor, inicia sesión de nuevo.", + "failedLoading": "No se pudieron cargar los datos. Por favor, inténtalo de nuevo.", + "tryagain": "Intentar de nuevo" + } +} diff --git a/apps/login/locales/it.json b/apps/login/locales/it.json new file mode 100644 index 0000000000..1423c43cfe --- /dev/null +++ b/apps/login/locales/it.json @@ -0,0 +1,195 @@ +{ + "common": { + "back": "Indietro" + }, + "accounts": { + "title": "Account", + "description": "Seleziona l'account che desideri utilizzare.", + "addAnother": "Aggiungi un altro account", + "noResults": "Nessun account trovato" + }, + "loginname": { + "title": "Bentornato!", + "description": "Inserisci i tuoi dati di accesso.", + "register": "Registrati come nuovo utente" + }, + "password": { + "verify": { + "title": "Password", + "description": "Inserisci la tua password.", + "resetPassword": "Reimposta Password", + "submit": "Continua" + }, + "set": { + "title": "Imposta Password", + "description": "Imposta la password per il tuo account", + "codeSent": "Un codice è stato inviato al tuo indirizzo email.", + "noCodeReceived": "Non hai ricevuto un codice?", + "resend": "Invia di nuovo", + "submit": "Continua" + }, + "change": { + "title": "Cambia Password", + "description": "Imposta la password per il tuo account", + "submit": "Continua" + } + }, + "idp": { + "title": "Accedi con SSO", + "description": "Seleziona uno dei seguenti provider per accedere", + "signInWithApple": "Accedi con Apple", + "signInWithGoogle": "Accedi con Google", + "signInWithAzureAD": "Accedi con AzureAD", + "signInWithGithub": "Accedi con GitHub", + "signInWithGitlab": "Accedi con GitLab", + "loginSuccess": { + "title": "Accesso riuscito", + "description": "Accesso effettuato con successo!" + }, + "linkingSuccess": { + "title": "Account collegato", + "description": "Hai collegato con successo il tuo account!" + }, + "registerSuccess": { + "title": "Registrazione riuscita", + "description": "Registrazione effettuata con successo!" + }, + "loginError": { + "title": "Accesso fallito", + "description": "Si è verificato un errore durante il tentativo di accesso." + }, + "linkingError": { + "title": "Collegamento account fallito", + "description": "Si è verificato un errore durante il tentativo di collegare il tuo account." + } + }, + "mfa": { + "verify": { + "title": "Verifica la tua identità", + "description": "Scegli uno dei seguenti fattori.", + "noResults": "Nessun secondo fattore disponibile per la configurazione." + }, + "set": { + "title": "Configura l'autenticazione a 2 fattori", + "description": "Scegli uno dei seguenti secondi fattori." + } + }, + "otp": { + "verify": { + "title": "Verifica l'autenticazione a 2 fattori", + "totpDescription": "Inserisci il codice dalla tua app di autenticazione.", + "smsDescription": "Inserisci il codice ricevuto via SMS.", + "emailDescription": "Inserisci il codice ricevuto via email.", + "noCodeReceived": "Non hai ricevuto un codice?", + "resendCode": "Invia di nuovo il codice", + "submit": "Continua" + }, + "set": { + "title": "Configura l'autenticazione a 2 fattori", + "totpDescription": "Scansiona il codice QR con la tua app di autenticazione.", + "smsDescription": "Inserisci il tuo numero di telefono per ricevere un codice via SMS.", + "emailDescription": "Inserisci il tuo indirizzo email per ricevere un codice via email.", + "totpRegisterDescription": "Scansiona il codice QR o naviga manualmente all'URL.", + "submit": "Continua" + } + }, + "passkey": { + "verify": { + "title": "Autenticati con una passkey", + "description": "Il tuo dispositivo chiederà la tua impronta digitale, il volto o il blocco schermo", + "usePassword": "Usa password", + "submit": "Continua" + }, + "set": { + "title": "Configura una passkey", + "description": "Il tuo dispositivo chiederà la tua impronta digitale, il volto o il blocco schermo", + "info": { + "description": "Una passkey è un metodo di autenticazione su un dispositivo come la tua impronta digitale, Apple FaceID o simili.", + "link": "Autenticazione senza password" + }, + "skip": "Salta", + "submit": "Continua" + } + }, + "u2f": { + "verify": { + "title": "Verifica l'autenticazione a 2 fattori", + "description": "Verifica il tuo account con il tuo dispositivo." + }, + "set": { + "title": "Configura l'autenticazione a 2 fattori", + "description": "Configura un dispositivo come secondo fattore.", + "submit": "Continua" + } + }, + "register": { + "methods": { + "passkey": "Passkey", + "password": "Password" + }, + "disabled": { + "title": "Registration disabled", + "description": "Registrazione disabilitata. Contatta l'amministratore di sistema per assistenza." + }, + "missingdata": { + "title": "Registrazione", + "description": "Inserisci i tuoi dati per registrarti." + }, + "title": "Registrati", + "description": "Crea il tuo account ZITADEL.", + "selectMethod": "Seleziona il metodo con cui desideri autenticarti", + "agreeTo": "Per registrarti devi accettare i termini e le condizioni", + "termsOfService": "Termini di Servizio", + "privacyPolicy": "Informativa sulla Privacy", + "submit": "Continua", + "password": { + "title": "Imposta Password", + "description": "Imposta la password per il tuo account", + "submit": "Continua" + } + }, + "invite": { + "title": "Invita Utente", + "description": "Inserisci l'indirizzo email dell'utente che desideri invitare.", + "info": "L'utente riceverà un'email con ulteriori istruzioni.", + "notAllowed": "Non hai i permessi per invitare un utente.", + "submit": "Invita Utente", + "success": { + "title": "Invito inviato", + "description": "L'utente è stato invitato con successo.", + "verified": "L'utente è stato invitato e ha già verificato la sua email.", + "notVerifiedYet": "L'utente è stato invitato. Riceverà un'email con ulteriori istruzioni.", + "submit": "Invita un altro utente" + } + }, + "signedin": { + "title": "Benvenuto {user}!", + "description": "Sei connesso.", + "continue": "Continua" + }, + "verify": { + "userIdMissing": "Nessun userId fornito!", + "success": "Verifica effettuata con successo!", + "setupAuthenticator": "Configura autenticatore", + "verify": { + "title": "Verifica utente", + "description": "Inserisci il codice fornito nell'email di verifica.", + "noCodeReceived": "Non hai ricevuto un codice?", + "resendCode": "Invia di nuovo il codice", + "submit": "Continua" + } + }, + "authenticator": { + "title": "Seleziona metodo di autenticazione", + "description": "Seleziona il metodo con cui desideri autenticarti", + "noMethodsAvailable": "Nessun metodo di autenticazione disponibile", + "allSetup": "Hai già configurato un autenticatore!", + "linkWithIDP": "o collega con un Identity Provider" + }, + "error": { + "unknownContext": "Impossibile ottenere il contesto dell'utente. Assicurati di inserire prima il nome utente o di fornire un loginName come parametro di ricerca.", + "sessionExpired": "La tua sessione attuale è scaduta. Effettua nuovamente l'accesso.", + "failedLoading": "Impossibile caricare i dati. Riprova.", + "tryagain": "Riprova" + } +} diff --git a/apps/login/locales/zh.json b/apps/login/locales/zh.json new file mode 100644 index 0000000000..acd03cc5b6 --- /dev/null +++ b/apps/login/locales/zh.json @@ -0,0 +1,195 @@ +{ + "common": { + "back": "返回" + }, + "accounts": { + "title": "账户", + "description": "选择您想使用的账户。", + "addAnother": "添加另一个账户", + "noResults": "未找到账户" + }, + "loginname": { + "title": "欢迎回来!", + "description": "请输入您的登录信息。", + "register": "注册新用户" + }, + "password": { + "verify": { + "title": "密码", + "description": "请输入您的密码。", + "resetPassword": "重置密码", + "submit": "继续" + }, + "set": { + "title": "设置密码", + "description": "为您的账户设置密码", + "codeSent": "验证码已发送到您的邮箱。", + "noCodeReceived": "没有收到验证码?", + "resend": "重发验证码", + "submit": "继续" + }, + "change": { + "title": "更改密码", + "description": "为您的账户设置密码", + "submit": "继续" + } + }, + "idp": { + "title": "使用 SSO 登录", + "description": "选择以下提供商中的一个进行登录", + "signInWithApple": "用 Apple 登录", + "signInWithGoogle": "用 Google 登录", + "signInWithAzureAD": "用 AzureAD 登录", + "signInWithGithub": "用 GitHub 登录", + "signInWithGitlab": "用 GitLab 登录", + "loginSuccess": { + "title": "登录成功", + "description": "您已成功登录!" + }, + "linkingSuccess": { + "title": "账户已链接", + "description": "您已成功链接您的账户!" + }, + "registerSuccess": { + "title": "注册成功", + "description": "您已成功注册!" + }, + "loginError": { + "title": "登录失败", + "description": "登录时发生错误。" + }, + "linkingError": { + "title": "账户链接失败", + "description": "链接账户时发生错误。" + } + }, + "mfa": { + "verify": { + "title": "验证您的身份", + "description": "选择以下的一个因素。", + "noResults": "没有可设置的第二因素。" + }, + "set": { + "title": "设置双因素认证", + "description": "选择以下的一个第二因素。" + } + }, + "otp": { + "verify": { + "title": "验证双因素", + "totpDescription": "请输入认证应用程序中的验证码。", + "smsDescription": "输入通过短信收到的验证码。", + "emailDescription": "输入通过电子邮件收到的验证码。", + "noCodeReceived": "没有收到验证码?", + "resendCode": "重发验证码", + "submit": "继续" + }, + "set": { + "title": "设置双因素认证", + "totpDescription": "使用认证应用程序扫描二维码。", + "smsDescription": "输入您的电话号码以接收短信验证码。", + "emailDescription": "输入您的电子邮箱地址以接收电子邮件验证码。", + "totpRegisterDescription": "扫描二维码或手动导航到URL。", + "submit": "继续" + } + }, + "passkey": { + "verify": { + "title": "使用密钥认证", + "description": "您的设备将请求指纹、面部识别或屏幕锁", + "usePassword": "使用密码", + "submit": "继续" + }, + "set": { + "title": "设置密钥", + "description": "您的设备将请求指纹、面部识别或屏幕锁", + "info": { + "description": "密钥是在设备上如指纹、Apple FaceID 或类似的认证方法。", + "link": "无密码认证" + }, + "skip": "跳过", + "submit": "继续" + } + }, + "u2f": { + "verify": { + "title": "验证双因素", + "description": "使用您的设备验证帐户。" + }, + "set": { + "title": "设置双因素认证", + "description": "设置设备为第二因素。", + "submit": "继续" + } + }, + "register": { + "methods": { + "passkey": "密钥", + "password": "密码" + }, + "disabled": { + "title": "注册已禁用", + "description": "您的设置不允许注册新用户。" + }, + "missingdata": { + "title": "缺少数据", + "description": "请提供所有必需的数据。" + }, + "title": "注册", + "description": "创建您的 ZITADEL 账户。", + "selectMethod": "选择您想使用的认证方法", + "agreeTo": "注册即表示您同意条款和条件", + "termsOfService": "服务条款", + "privacyPolicy": "隐私政策", + "submit": "继续", + "password": { + "title": "设置密码", + "description": "为您的账户设置密码", + "submit": "继续" + } + }, + "invite": { + "title": "邀请用户", + "description": "提供您想邀请的用户的电子邮箱地址和姓名。", + "info": "用户将收到一封包含进一步说明的电子邮件。", + "notAllowed": "您的设置不允许邀请用户。", + "submit": "继续", + "success": { + "title": "用户已邀请", + "description": "邮件已成功发送。", + "verified": "用户已被邀请并已验证其电子邮件。", + "notVerifiedYet": "用户已被邀请。他们将收到一封包含进一步说明的电子邮件。", + "submit": "邀请另一位用户" + } + }, + "signedin": { + "title": "欢迎 {user}!", + "description": "您已登录。", + "continue": "继续" + }, + "verify": { + "userIdMissing": "未提供用户 ID!", + "success": "用户验证成功。", + "setupAuthenticator": "设置认证器", + "verify": { + "title": "验证用户", + "description": "输入验证邮件中的验证码。", + "noCodeReceived": "没有收到验证码?", + "resendCode": "重发验证码", + "submit": "继续" + } + }, + "authenticator": { + "title": "选择认证方式", + "description": "选择您想使用的认证方法", + "noMethodsAvailable": "没有可用的认证方法", + "allSetup": "您已经设置好了一个认证器!", + "linkWithIDP": "或将其与身份提供者关联" + }, + "error": { + "unknownContext": "无法获取用户的上下文。请先输入用户名或提供 loginName 作为搜索参数。", + "sessionExpired": "当前会话已过期,请重新登录。", + "failedLoading": "加载数据失败,请再试一次。", + "tryagain": "重试" + } +} diff --git a/apps/login/mock/initial-stubs/zitadel.settings.v2beta.SettingsService.json b/apps/login/mock/initial-stubs/zitadel.settings.v2.SettingsService.json similarity index 96% rename from apps/login/mock/initial-stubs/zitadel.settings.v2beta.SettingsService.json rename to apps/login/mock/initial-stubs/zitadel.settings.v2.SettingsService.json index 6ddf5989ed..07e9980f9b 100644 --- a/apps/login/mock/initial-stubs/zitadel.settings.v2beta.SettingsService.json +++ b/apps/login/mock/initial-stubs/zitadel.settings.v2.SettingsService.json @@ -2,7 +2,9 @@ { "service": "zitadel.settings.v2.SettingsService", "method": "GetBrandingSettings", - "out": {} + "out": { + "data": {} + } }, { "service": "zitadel.settings.v2.SettingsService", diff --git a/apps/login/mock/mocked-services.cfg b/apps/login/mock/mocked-services.cfg index 72e4d98748..6a758ab8c1 100644 --- a/apps/login/mock/mocked-services.cfg +++ b/apps/login/mock/mocked-services.cfg @@ -1,4 +1,5 @@ zitadel/user/v2/user_service.proto +zitadel/org/v2/org_service.proto zitadel/session/v2/session_service.proto zitadel/settings/v2/settings_service.proto zitadel/management.proto diff --git a/apps/login/next-env.d.ts b/apps/login/next-env.d.ts index 4f11a03dc6..1b3be0840f 100755 --- a/apps/login/next-env.d.ts +++ b/apps/login/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/login/next.config.mjs b/apps/login/next.config.mjs index 4e71072736..a6e1d1a6d8 100755 --- a/apps/login/next.config.mjs +++ b/apps/login/next.config.mjs @@ -1,3 +1,7 @@ +import createNextIntlPlugin from "next-intl/plugin"; + +const withNextIntl = createNextIntlPlugin(); + /** @type {import('next').NextConfig} */ const secureHeaders = [ @@ -32,9 +36,8 @@ const secureHeaders = [ const nextConfig = { reactStrictMode: true, // Recommended for the `pages` directory, default in `app`. - swcMinify: true, experimental: { - serverActions: true, + dynamicIO: true, }, images: { remotePatterns: [ @@ -62,4 +65,4 @@ const nextConfig = { }, }; -export default nextConfig; +export default withNextIntl(nextConfig); diff --git a/apps/login/package.json b/apps/login/package.json index aca25b506c..c4031458ca 100644 --- a/apps/login/package.json +++ b/apps/login/package.json @@ -1,14 +1,16 @@ { "name": "@zitadel/login", "private": true, + "type": "module", "scripts": { - "dev": "next dev", + "dev": "next dev --turbopack", "test": "concurrently --timings --kill-others-on-fail 'npm:test:unit' 'npm:test:integration'", "test:watch": "concurrently --kill-others 'npm:test:unit:watch' 'npm:test:integration:watch'", "test:unit": "vitest", "test:unit:watch": "pnpm test:unit --watch", "test:integration": "pnpm mock:build && concurrently --names 'mock,test' --success command-test --kill-others 'pnpm:mock' 'env-cmd -f ./.env.integration start-server-and-test start http://localhost:3000 \"test:integration:run\"'", - "test:integration:watch": "concurrently --names 'mock,test' --kill-others 'pnpm:mock' 'env-cmd -f ./.env.integration start-server-and-test dev http://localhost:3000 \"pnpm nodemon -e js,jsx,ts,tsx,css,scss --ignore \\\"__test__/**\\\" --exec \\\"pnpm test:integration:run\\\"\"'", + "test:integration:watch:run": "concurrently --names 'mock,test' --kill-others 'pnpm:mock' 'env-cmd -f ./.env.integration start-server-and-test dev http://localhost:3000 \"pnpm nodemon -e js,jsx,ts,tsx,css,scss --ignore \\\"__test__/**\\\" --exec \\\"pnpm test:integration:run\\\"\"'", + "test:integration:watch:open": "concurrently --names 'mock,test' --kill-others 'pnpm:mock' 'env-cmd -f ./.env.integration start-server-and-test dev http://localhost:3000 \"pnpm nodemon -e js,jsx,ts,tsx,css,scss --ignore \\\"__test__/**\\\" --exec \\\"pnpm test:integration:open\\\"\"'", "test:integration:run": "cypress run --config-file ./cypress/cypress.config.ts --quiet", "test:integration:open": "cypress open --config-file ./cypress/cypress.config.ts", "mock": "pnpm mock:build && pnpm mock:run", @@ -23,6 +25,7 @@ "build": "next build", "prestart": "pnpm build", "start": "next start", + "start:built": "next start", "clean": "pnpm mock:destroy && rm -rf .turbo && rm -rf node_modules && rm -rf .next" }, "git": { @@ -32,57 +35,58 @@ "*": "prettier --write --ignore-unknown" }, "dependencies": { - "@headlessui/react": "^1.7.18", + "@headlessui/react": "^2.1.9", "@heroicons/react": "2.1.3", "@tailwindcss/forms": "0.5.7", "@vercel/analytics": "^1.2.2", "@zitadel/client": "workspace:*", - "@zitadel/node": "workspace:*", "@zitadel/proto": "workspace:*", - "@zitadel/react": "workspace:*", "clsx": "1.2.1", "copy-to-clipboard": "^3.3.3", + "deepmerge": "^4.3.1", "moment": "^2.29.4", - "next": "14.2.3", + "next": "15.0.4-canary.23", + "next-intl": "^3.25.1", "next-themes": "^0.2.1", "nice-grpc": "2.0.1", "qrcode.react": "^3.1.0", - "react": "18.3.1", - "react-dom": "18.3.1", + "react": "19.0.0-rc-66855b96-20241106", + "react-dom": "19.0.0-rc-66855b96-20241106", "react-hook-form": "7.39.5", "swr": "^2.2.0", "tinycolor2": "1.4.2" }, "devDependencies": { - "@bufbuild/buf": "^1.35.1", - "@testing-library/jest-dom": "^6.4.5", - "@testing-library/react": "^15.0.7", - "@types/ms": "0.7.31", - "@types/node": "18.11.9", - "@types/react": "18.2.8", - "@types/react-dom": "18.0.9", + "@bufbuild/buf": "^1.46.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.0.1", + "@types/ms": "0.7.34", + "@types/node": "22.9.0", + "@types/react": "npm:types-react@19.0.0-rc.1", + "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1", "@types/tinycolor2": "1.4.3", - "@types/uuid": "^9.0.1", + "@types/uuid": "^10.0.0", "@vercel/git-hooks": "1.0.0", "@zitadel/prettier-config": "workspace:*", "@zitadel/tsconfig": "workspace:*", - "autoprefixer": "10.4.13", - "concurrently": "^8.1.0", - "cypress": "^13.9.0", - "del-cli": "5.0.0", + "autoprefixer": "10.4.20", + "concurrently": "^9.1.0", + "cypress": "^13.15.2", + "del-cli": "6.0.0", "env-cmd": "^10.1.0", "eslint-config-zitadel": "workspace:*", - "grpc-tools": "1.11.3", - "lint-staged": "13.0.3", - "make-dir-cli": "3.0.0", - "nodemon": "^2.0.22", - "postcss": "8.4.21", - "prettier-plugin-tailwindcss": "0.1.13", - "sass": "^1.77.1", - "start-server-and-test": "^2.0.0", - "tailwindcss": "3.2.4", - "ts-proto": "^1.139.0", - "typescript": "^5.4.5", + "grpc-tools": "1.12.4", + "jsdom": "^25.0.1", + "lint-staged": "15.2.10", + "make-dir-cli": "4.0.0", + "nodemon": "^3.1.7", + "postcss": "8.4.49", + "prettier-plugin-tailwindcss": "0.6.8", + "sass": "^1.80.7", + "start-server-and-test": "^2.0.8", + "tailwindcss": "3.4.14", + "ts-proto": "^2.2.7", + "typescript": "^5.6.3", "zitadel-tailwind-config": "workspace:*" } } diff --git a/apps/login/postcss.config.js b/apps/login/postcss.config.cjs similarity index 100% rename from apps/login/postcss.config.js rename to apps/login/postcss.config.cjs diff --git a/apps/login/public/checkbox.svg b/apps/login/public/checkbox.svg new file mode 100644 index 0000000000..94a3298ae6 --- /dev/null +++ b/apps/login/public/checkbox.svg @@ -0,0 +1 @@ + diff --git a/apps/login/public/logo/zitadel-logo-solo-darkdesign.svg b/apps/login/public/logo/zitadel-logo-solo-darkdesign.svg index df44ec5398..4a4e8be71b 100644 --- a/apps/login/public/logo/zitadel-logo-solo-darkdesign.svg +++ b/apps/login/public/logo/zitadel-logo-solo-darkdesign.svg @@ -1,6 +1,6 @@ - + diff --git a/apps/login/public/logo/zitadel-logo-solo-lightdesign.svg b/apps/login/public/logo/zitadel-logo-solo-lightdesign.svg index 4d3181174e..33ea6b583b 100644 --- a/apps/login/public/logo/zitadel-logo-solo-lightdesign.svg +++ b/apps/login/public/logo/zitadel-logo-solo-lightdesign.svg @@ -1,6 +1,6 @@ - + diff --git a/apps/login/public/zitadel-logo-dark.svg b/apps/login/public/zitadel-logo-dark.svg index 95ff80187c..6dcfe06e6d 100644 --- a/apps/login/public/zitadel-logo-dark.svg +++ b/apps/login/public/zitadel-logo-dark.svg @@ -1,6 +1,6 @@ - + diff --git a/apps/login/public/zitadel-logo-light.svg b/apps/login/public/zitadel-logo-light.svg index 7edc748903..d48a5eeb94 100644 --- a/apps/login/public/zitadel-logo-light.svg +++ b/apps/login/public/zitadel-logo-light.svg @@ -1,6 +1,6 @@ - + diff --git a/apps/login/readme.md b/apps/login/readme.md index e9510a7cf5..a411b9963f 100644 --- a/apps/login/readme.md +++ b/apps/login/readme.md @@ -2,24 +2,399 @@ This is going to be our next UI for the hosted login. It's based on Next.js 13 and its introduced `app/` directory. -The Login UI should provide the following functionality: +## Flow Diagram -- **Login API:** Uses the new ZITADEL Login API -- **Server Components:** Making server-first components +This diagram shows the available pages and flows. -## Running Locally +> Note that back navigation or retries are not displayed. -1. Install dependencies: `yarn install` -1. Start the dev server: `yarn dev` +```mermaid + flowchart TD + A[Start] --> register + A[Start] --> accounts + A[Start] --> loginname + loginname -- signInWithIDP --> idp-success + loginname -- signInWithIDP --> idp-failure + idp-success --> B[signedin] + loginname --> password + loginname -- hasPasskey --> passkey + loginname -- allowRegister --> register + passkey-add --passwordAllowed --> password + passkey -- hasPassword --> password + passkey --> B[signedin] + password -- hasMFA --> mfa + password -- allowPasskeys --> passkey-add + password -- reset --> password-set + email -- reset --> password-set + password-set --> B[signedin] + password-change --> B[signedin] + password -- userstate=initial --> password-change -## Documentation + mfa --> otp + otp --> B[signedin] + mfa--> u2f + u2f -->B[signedin] + register -- password/passkey --> B[signedin] + password --> B[signedin] + password-- forceMFA -->mfaset + mfaset --> u2fset + mfaset --> otpset + u2fset --> B[signedin] + otpset --> B[signedin] + accounts--> loginname + password -- not verified yet -->verify + register-- withpassword -->verify + passkey-- notVerified --> verify + verify --> B[signedin] +``` -https://beta.nextjs.org/docs +### /loginname - +- `getLoginSettings(org?)` +- `getLegalAndSupportSettings(org?)` +- `getIdentityProviders(org?)` +- `getBrandingSettings(org?)` +- `getActiveIdentityProviders(org?)` +- `startIdentityProviderFlow` +- `listUsers(org?)` +- `listAuthenticationMethodTypes` +- `getOrgsByDomain` +- `createSession()` +- `getSession()` + +After a loginname is entered, a `listUsers` request is made using the loginName query to identify already registered users. + +**USER FOUND:** If only one user is found, we query `listAuthenticationMethodTypes` to identify future steps. +If no authentication methods are found, we render an error stating: _User has no available authentication methods._ (exception see below.) +Now if only one method is found, we continue with the corresponding step (/password, /passkey). +If multiple methods are set, we prefer passkeys over any other method, so we redirect to /passkey, second option is IDP, and third is password. +If password is the next step, we check `loginSettings.passkeysType` for PasskeysType.ALLOWED, and prompt the user to setup passkeys afterwards. + +**NO USER FOUND:** If no user is found, we check whether registering is allowed using `loginSettings.allowRegister`. +If `loginSettings?.allowUsernamePassword` is not allowed we continue to check for available IDPs. If a single IDP is available, we directly redirect the user to signup. + +If no single IDP is set, we check for `loginSettings.allowUsernamePassword` and if no organization is set as context, we check whether we can discover a organization from the loginname of the user (using: `getOrgsByDomain`). Then if an organization is found, we check whether domainDiscovery is allowed on it and redirect the user to /register page including the discovered domain or without. + +If no previous condition is met we throw an error stating the user was not found. + +**EXCEPTIONS:** If the outcome after this order produces a no authentication methods found, or user not found, we check whether `loginSettings?.ignoreUnknownUsernames` is set to `true` as in this case we redirect to the /password page regardless (to prevent username guessing). + +> NOTE: This page at this stage beeing ignores local sessions and executes a reauthentication. This is a feature which is not implemented yet. + +> NOTE: We ignore `loginSettings.allowExternalIdp` as the information whether IDPs are available comes as response from `getActiveIdentityProviders(org?)`. If a user has a cookie for the same loginname, a new session is created regardless and overwrites the old session. The old session is not deleted from the login as for now. + +> NOTE: `listAuthenticationMethodTypes()` does not consider different domains for u2f methods or passkeys. The check whether a user should be redirected to one of the pages `/passkey` or `/u2f`, should be extended to use a domain filter (https://github.com/zitadel/zitadel/issues/8615) + +### /password + +This page shows a password field to hydrate the current session with password as a factor. +Below the password field, a reset password link is shown which allows to send a reset email. + +/password + +Requests to the APIs made: + +- `getLoginSettings(org?)` +- `getBrandingSettings(org?)` +- `listAuthenticationMethodTypes` +- `getSession()` +- `updateSession()` +- `listUsers()` +- `getUserById()` + +**MFA AVAILABLE:** After the password has been submitted, additional authentication methods are loaded. +If the user has set up an additional **single** second factor, it is redirected to add the next factor. Depending on the available method he is redirected to `/otp/time-based`,`/otp/sms?`, `/otp/email?` or `/u2f?`. If the user has multiple second factors, he is redirected to `/mfa` to select his preferred method to continue. + +**NO MFA, USER STATE INITIAL** If the user has no MFA methods and is in an initial state, we redirect to `/password/change` where a new password can be set. + +**NO MFA, FORCE MFA:** If no MFA method is available, and the settings force MFA, the user is sent to `/mfa/set` which prompts to setup a second factor. + +**PROMPT PASSKEY** If the settings do not enforce MFA, we check if passkeys are allowed with `loginSettings?.passkeysType == PasskeysType.ALLOWED` and redirect the user to `/passkey/set` if no passkeys are setup. This step can be skipped. + +If none of the previous conditions apply, we continue to sign in. + +> NOTE: `listAuthenticationMethodTypes()` does not consider different domains for u2f methods or passkeys. The check whether a user should be redirected to one of the pages `/passkey` or `/u2f`, should be extended to use a domain filter (https://github.com/zitadel/zitadel/issues/8615) + +### /password/change + +This page allows to change the password. It is used after a user is in an initial state and is required to change the password, or it can be directly invoked with an active session. + +/password/change + +Requests to the APIs made: + +- `getLoginSettings(org?)` +- `getPasswordComplexitySettings(user?)` +- `getBrandingSettings(org?)` +- `getSession()` +- `setPassword()` + +> NOTE: The request to change the password is using the session of the user itself not the service user, therefore no code is required. + +### /password/set + +This page allows to set a password. It is used after a user has requested to reset the password on the `/password` page. + +/password/set + +Requests to the APIs made: + +- `getLoginSettings(org?)` +- `getPasswordComplexitySettings(user?)` +- `getBrandingSettings(org?)` +- `getUserByID()` +- `setPassword()` + +The page allows to enter a code or be invoked directly from a email link which prefills the code. The user can enter a new password and submit. + +### /otp/[method] + +This page shows a code field to check an otp method. The session of the user is then hydrated with the respective factor. Supported methods are `time-based`, `sms` and `email`. + +/otp/[method] + +Requests to the APIs made: + +- `getBrandingSettings(org?)` +- `getSession()` +- `updateSession()` + +If `email` or `sms` is requested as method, the current session of the user is updated to request the challenge. This will trigger an email or sms which can be entered in the code field. +The `time-based` (TOTP) method does not require a trigger, therefore no `updateSession()` is performed and no resendLink under the code field is shown. + +The submission of the code updates the session and continues to sign in the user. + +### /u2f + +This page requests a webAuthN challenge for the user and updates the session afterwards. + +/u2f + +Requests to the APIs made: + +- `getBrandingSettings(org?)` +- `getSession()` +- `updateSession()` + +When updating the session for the webAuthN challenge, we set `userVerificationRequirement` to `UserVerificationRequirement.DISCOURAGED` as this will request the webAuthN method as second factor and not as primary method. +After updating the session, the user is **always** signed in. :warning: required as this page is a follow up for setting up a u2f method. + +### /passkey + +This page requests a webAuthN challenge for the user and updates the session afterwards. +It is invoked directly after setting up a passkey `/passkey/set` or when loggin in a user after `/loginname`. + +/passkey + +Requests to the APIs made: + +- `getBrandingSettings(org?)` +- `getSession()` +- `updateSession()` + +When updating the session for the webAuthN challenge, we set `userVerificationRequirement` to `UserVerificationRequirement.REQUIRED` as this will request the webAuthN method as primary method to login. +After updating the session, the user is **always** signed in. :warning: required as this page is a follow up for setting up a passkey + +> NOTE: This page currently does not check whether a user contains passkeys. If this method is not available, this page should not be used. + +### /mfa/set + +This page loads login settings and the authentication methods for a user and shows setup options. + +/mfa/set + +Requests to the APIs made: + +- `getBrandingSettings(org?)` +- `getLoginSettings(user.org)` :warning: context taken from session +- `getSession()` +- `listAuthenticationMethodTypes()` +- `getUserByID()` + +If a user has already setup a certain method, a checkbox is shown alongside the button and the button is disabled. +OTP Email and OTP SMS only show up if the user has verified email or phone. +If the user chooses a method he is redirected to one of `/otp/time-based/set`, `/u2f/set`, `/otp/email/set`, or `/otp/sms/set`. +At the moment, U2F methods are hidden if a method is already added on the users resource. Reasoning is that the page should only be invoked for prompts. A self service page which shows up multiple u2f factors is implemented at a later stage. + +> NOTE: The session and therefore the user factor defines which login settings are checked for available options. + +> NOTE: `listAuthenticationMethodTypes()` does not consider different domains for u2f or passkeys. The check whether a user should be redirected to one of the pages `/passkey/set` or `/u2f/set`, should be extended to use a domain filter (https://github.com/zitadel/zitadel/issues/8615) + +### /passkey/set + +This page sets a passkey method for a user. This page can be either enforced, or optional depending on the Login Settings. + + +/passkey/set + +Requests to the APIs made: + +- `getBrandingSettings(org?)` +- `getSession()` +- `createPasskeyRegistrationLink()` TODO: check if this can be used with the session token (mfa required (AUTHZ-Kl3p0)) +- `registerPasskey()` +- `verifyPasskey()` + +If the loginname decides to redirect the user to this page, a button to skip appears which will sign the user in afterwards. +After a passkey is registered, we redirect the user to `/passkey` to verify it again and sign in with the new method. The `createPasskeyRegistrationLink()` uses the token of the session which is determined by the flow. + +> NOTE: this page allows passkeys to be created only if the current session is valid (self service), or no authentication method is set (register). TODO: to be implemented. + +> NOTE: Redirecting the user to `/passkey` will not be required in future and the currently used session will be hydrated directly after registering. (https://github.com/zitadel/zitadel/issues/8611) + +### /otp/time-based/set + +This page registers a time based OTP method for a user. + +/otp/time-based/set + +Requests to the APIs made: + +- `getBrandingSettings(org?)` +- `getSession()` +- `registerTOTP()` +- `verifyTOTP()` + +After the setup is done, the user is redirected to verify the TOTP method on `/otp/time-based`. + +> NOTE: Redirecting the user to `/otp/time-based` will not be required in future and the currently used session will be hydrated directly. (https://github.com/zitadel/zitadel/issues/8611) + +### /otp/email/set /otp/sms/set + +This page registers either an Email OTP method or SMS OTP method for a user. + +Requests to the APIs made: + +- `getBrandingSettings(org?)` +- `getSession()` +- `addOTPEmail()` / `addOTPSMS()` + +This page directly calls `addOTPEmail()` or `addOTPSMS()` when invoked and shows a success message. +Right afterwards, redirects to verify the method. + +### /u2f/set + +This page registers a U2F method for a user. + +/u2f/set + +Requests to the APIs made: + +- `getBrandingSettings(org?)` +- `getSession()` +- `registerU2F()` :warning: TODO: check if this can be used with the session token (mfa required (AUTHZ-Kl3p0)) +- `verifyU2FRegistration()` + +After a u2f method is registered, we redirect the user to `/passkey` to verify it again and sign in with the new method. The `createPasskeyRegistrationLink()` uses the token of the session which is determined by the flow. + +> NOTE: Redirecting the user to `/passkey` will not be required in future and the currently used session will be hydrated directly after registering. (https://github.com/zitadel/zitadel/issues/8611) + +### /register + +This page shows a register page, which gets firstname and lastname of a user as well as the email. It offers to setup a user, using password or passkeys. + +/register + +register with password + +Requests to the APIs made: + +- `listOrganizations()` :warning: TODO: determine the default organization if no context is set +- `getLegalAndSupportSettings(org)` +- `getPasswordComplexitySettings()` +- `getBrandingSettings()` +- `addHumanUser()` +- `createSession()` +- `getSession()` + +To register a user, the organization where the resource will be created is determined first. If no context is provided via url, we fall back to the default organization of the instance. + +**PASSWORD:** If a password is set, the user is created as a resource, then a session using the password check is created immediately. After creating the session, the user is directly logged in and eventually redirected back to the application. + +**PASSKEY:** If passkey is selected, the user is created as a resource first, then a session using the userId is created immediately. This session does not yet contain a check, we therefore redirect the user to setup a passkey at `/passkey/set`. As the passkey set page verifies the passkey right afterwards, the process ends with a signed in user. + +> NOTE: https://github.com/zitadel/zitadel/issues/8616 to determine the default organization of an instance must be implemented in order to correctly use the legal-, login-, branding- and complexitysettings. + +> NOTE: TODO: check which methods are allowed in the login settings, loginSettings.allowUsernamePassword / check for passkey + +### /idp + +This page doubles as /loginname but limits it to choose from IDPs + +/idp + +Requests to the APIs made: + +- `getBrandingSettings(org?)` +- `getActiveIdentityProviders(org?)` +- `startIdentityProviderFlow()` + +### /idp/[method]/success /idp/[method]/failure + +Both /success and /failure pages are designed to intercept the responses from the IDPs and decide on how to continue with the process. + +### /verify + +This page verifies the email to be valid. It page of the login can also be invoked without an active session. +The context of the user is taken from the url and is set in the email template. + +/accounts + +Requests to the APIs made: + +- `getBrandingSettings(org?)` +- `getLoginSettings(org?)` +- `verifyEmail()` + +If the page is invoked with an active session (right after a register with password), the user is signed in or redirected to the loginname if no context is known. + +> NOTE: This page will be extended to support invitations. In such case, authentication methods of the user are loaded and if none available, shown as possible next step (`/passkey/set`, `password/set`). + +### /accounts + +This page shows an overview of all current sessions. +Sessions with invalid token show a red dot on the right side, Valid session a green dot, and its last verified date. + +/accounts + +This page is a starting point for self management, reauthentication, or can be used to clear local sessions. +This page is also shown if used with OIDC and `prompt: select_account`. + +On all pages, where the current user is shown, you can jump to this page. This way, a session can quickly be reused if valid. + +jump to accounts + +### /signedin + +This is a success page which shows a completed login flow for a user, which did navigate to the login without a OIDC auth requrest. + +/signedin + +In future, self service options to jump to are shown below, like: + +- change password +- setup passkeys +- setup mfa +- change profile +- logout + +> NOTE: This page has to be explicitly enabled or act as a fallback if no default redirect is set. + +## Currently NOT Supported + +Timebased features like the multifactor init prompt or password expiry, are not supported due to a current limitation in the API. Lockout settings which keeps track of the password retries, will also be implemented in a later stage. + +- Lockout Settings +- Password Expiry Settings +- Login Settings: multifactor init prompt +- forceMFA on login settings is not checked for IDPs +- disablePhone / disableEmail from loginSettings will be implemented right after https://github.com/zitadel/zitadel/issues/9016 is merged + +Also note that IDP logins are considered as valid MFA. An additional MFA check will be implemented in future if enforced. diff --git a/apps/login/screenshots/accounts.png b/apps/login/screenshots/accounts.png new file mode 100644 index 0000000000..a8591141c6 Binary files /dev/null and b/apps/login/screenshots/accounts.png differ diff --git a/apps/login/screenshots/accounts_jumpto.png b/apps/login/screenshots/accounts_jumpto.png new file mode 100644 index 0000000000..0fd126bf4c Binary files /dev/null and b/apps/login/screenshots/accounts_jumpto.png differ diff --git a/apps/login/screenshots/collage.png b/apps/login/screenshots/collage.png new file mode 100644 index 0000000000..9d5a9c35c8 Binary files /dev/null and b/apps/login/screenshots/collage.png differ diff --git a/apps/login/screenshots/idp.png b/apps/login/screenshots/idp.png new file mode 100644 index 0000000000..9bf58c69b0 Binary files /dev/null and b/apps/login/screenshots/idp.png differ diff --git a/apps/login/screenshots/loginname.png b/apps/login/screenshots/loginname.png new file mode 100644 index 0000000000..342e60799e Binary files /dev/null and b/apps/login/screenshots/loginname.png differ diff --git a/apps/login/screenshots/mfa.png b/apps/login/screenshots/mfa.png new file mode 100644 index 0000000000..1fd73f205c Binary files /dev/null and b/apps/login/screenshots/mfa.png differ diff --git a/apps/login/screenshots/mfaset.png b/apps/login/screenshots/mfaset.png new file mode 100644 index 0000000000..c00ee4edf5 Binary files /dev/null and b/apps/login/screenshots/mfaset.png differ diff --git a/apps/login/screenshots/otp.png b/apps/login/screenshots/otp.png new file mode 100644 index 0000000000..3818a5ad5f Binary files /dev/null and b/apps/login/screenshots/otp.png differ diff --git a/apps/login/screenshots/otpset.png b/apps/login/screenshots/otpset.png new file mode 100644 index 0000000000..f75c2154c7 Binary files /dev/null and b/apps/login/screenshots/otpset.png differ diff --git a/apps/login/screenshots/passkey.png b/apps/login/screenshots/passkey.png new file mode 100644 index 0000000000..7a5686c736 Binary files /dev/null and b/apps/login/screenshots/passkey.png differ diff --git a/apps/login/screenshots/password.png b/apps/login/screenshots/password.png new file mode 100644 index 0000000000..05cf8747bb Binary files /dev/null and b/apps/login/screenshots/password.png differ diff --git a/apps/login/screenshots/password_change.png b/apps/login/screenshots/password_change.png new file mode 100644 index 0000000000..183de6df34 Binary files /dev/null and b/apps/login/screenshots/password_change.png differ diff --git a/apps/login/screenshots/password_set.png b/apps/login/screenshots/password_set.png new file mode 100644 index 0000000000..15b5ff49ad Binary files /dev/null and b/apps/login/screenshots/password_set.png differ diff --git a/apps/login/screenshots/register.png b/apps/login/screenshots/register.png new file mode 100644 index 0000000000..ba9f6951d8 Binary files /dev/null and b/apps/login/screenshots/register.png differ diff --git a/apps/login/screenshots/register_password.png b/apps/login/screenshots/register_password.png new file mode 100644 index 0000000000..31515bda9a Binary files /dev/null and b/apps/login/screenshots/register_password.png differ diff --git a/apps/login/screenshots/signedin.png b/apps/login/screenshots/signedin.png new file mode 100644 index 0000000000..f96ea1721f Binary files /dev/null and b/apps/login/screenshots/signedin.png differ diff --git a/apps/login/screenshots/u2f.png b/apps/login/screenshots/u2f.png new file mode 100644 index 0000000000..6b8eca087d Binary files /dev/null and b/apps/login/screenshots/u2f.png differ diff --git a/apps/login/screenshots/u2fset.png b/apps/login/screenshots/u2fset.png new file mode 100644 index 0000000000..37115548a5 Binary files /dev/null and b/apps/login/screenshots/u2fset.png differ diff --git a/apps/login/screenshots/verify.png b/apps/login/screenshots/verify.png new file mode 100644 index 0000000000..c13e6a3a88 Binary files /dev/null and b/apps/login/screenshots/verify.png differ diff --git a/apps/login/src/app/(login)/accounts/page.tsx b/apps/login/src/app/(login)/accounts/page.tsx index b7b17d95b1..49fbad6202 100644 --- a/apps/login/src/app/(login)/accounts/page.tsx +++ b/apps/login/src/app/(login)/accounts/page.tsx @@ -1,9 +1,15 @@ -import { getBrandingSettings, listSessions } from "@/lib/zitadel"; -import { getAllSessionCookieIds } from "@/utils/cookies"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { SessionsList } from "@/components/sessions-list"; +import { getAllSessionCookieIds } from "@/lib/cookies"; +import { + getBrandingSettings, + getDefaultOrg, + listSessions, +} from "@/lib/zitadel"; import { UserPlusIcon } from "@heroicons/react/24/outline"; +import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; +import { getLocale, getTranslations } from "next-intl/server"; import Link from "next/link"; -import SessionsList from "@/ui/SessionsList"; -import DynamicTheme from "@/ui/DynamicTheme"; async function loadSessions() { const ids = await getAllSessionCookieIds(); @@ -19,41 +25,54 @@ async function loadSessions() { } } -export default async function Page({ - searchParams, -}: { - searchParams: Record; +export default async function Page(props: { + searchParams: Promise>; }) { + const searchParams = await props.searchParams; + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "accounts" }); + const authRequestId = searchParams?.authRequestId; const organization = searchParams?.organization; + let defaultOrganization; + if (!organization) { + const org: Organization | null = await getDefaultOrg(); + if (org) { + defaultOrganization = org.id; + } + } + let sessions = await loadSessions(); - const branding = await getBrandingSettings(organization); + const branding = await getBrandingSettings( + organization ?? defaultOrganization, + ); + + const params = new URLSearchParams(); + + if (authRequestId) { + params.append("authRequestId", authRequestId); + } + + if (organization) { + params.append("organization", organization); + } return (
-

Accounts

-

Use your ZITADEL Account

+

{t("title")}

+

{t("description")}

- +
- Add another account + {t("addAnother")}
diff --git a/apps/login/src/app/(login)/authenticator/set/page.tsx b/apps/login/src/app/(login)/authenticator/set/page.tsx new file mode 100644 index 0000000000..7203484df0 --- /dev/null +++ b/apps/login/src/app/(login)/authenticator/set/page.tsx @@ -0,0 +1,156 @@ +import { Alert } from "@/components/alert"; +import { BackButton } from "@/components/back-button"; +import { ChooseAuthenticatorToSetup } from "@/components/choose-authenticator-to-setup"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { SignInWithIdp } from "@/components/sign-in-with-idp"; +import { UserAvatar } from "@/components/user-avatar"; +import { getSessionCookieById } from "@/lib/cookies"; +import { loadMostRecentSession } from "@/lib/session"; +import { + getActiveIdentityProviders, + getBrandingSettings, + getLoginSettings, + getSession, + getUserByID, + listAuthenticationMethodTypes, +} from "@/lib/zitadel"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { getLocale, getTranslations } from "next-intl/server"; + +export default async function Page(props: { + searchParams: Promise>; +}) { + const searchParams = await props.searchParams; + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "authenticator" }); + const tError = await getTranslations({ locale, namespace: "error" }); + + const { loginName, authRequestId, organization, sessionId } = searchParams; + + const sessionWithData = sessionId + ? await loadSessionById(sessionId, organization) + : await loadSessionByLoginname(loginName, organization); + + async function getAuthMethodsAndUser(session?: Session) { + const userId = session?.factors?.user?.id; + + if (!userId) { + throw Error("Could not get user id from session"); + } + + return listAuthenticationMethodTypes(userId).then((methods) => { + return getUserByID(userId).then((user) => { + const humanUser = + user.user?.type.case === "human" ? user.user?.type.value : undefined; + + return { + factors: session?.factors, + authMethods: methods.authMethodTypes ?? [], + phoneVerified: humanUser?.phone?.isVerified ?? false, + emailVerified: humanUser?.email?.isVerified ?? false, + expirationDate: session?.expirationDate, + }; + }); + }); + } + + async function loadSessionByLoginname( + loginName?: string, + organization?: string, + ) { + return loadMostRecentSession({ + loginName, + organization, + }).then((session) => { + return getAuthMethodsAndUser(session); + }); + } + + async function loadSessionById(sessionId: string, organization?: string) { + const recent = await getSessionCookieById({ sessionId, organization }); + return getSession({ + sessionId: recent.id, + sessionToken: recent.token, + }).then((sessionResponse) => { + return getAuthMethodsAndUser(sessionResponse.session); + }); + } + + if (!sessionWithData) { + return {tError("unknownContext")}; + } + + const branding = await getBrandingSettings( + sessionWithData.factors?.user?.organizationId, + ); + + const loginSettings = await getLoginSettings( + sessionWithData.factors?.user?.organizationId, + ); + + const identityProviders = await getActiveIdentityProviders( + sessionWithData.factors?.user?.organizationId, + true, + ).then((resp) => { + return resp.identityProviders; + }); + + const params = new URLSearchParams({ + initial: "true", // defines that a code is not required and is therefore not shown in the UI + }); + + if (sessionWithData.factors?.user?.loginName) { + params.set("loginName", sessionWithData.factors?.user?.loginName); + } + + if (sessionWithData.factors?.user?.organizationId) { + params.set("organization", sessionWithData.factors?.user?.organizationId); + } + + if (authRequestId) { + params.set("authRequestId", authRequestId); + } + + return ( + +
+

{t("title")}

+ +

{t("description")}

+ + + + {loginSettings && ( + + )} + +
+

{t("linkWithIDP")}

+
+ + {loginSettings?.allowExternalIdp && identityProviders && ( + + )} + +
+ + +
+
+
+ ); +} diff --git a/apps/login/src/app/(login)/error.tsx b/apps/login/src/app/(login)/error.tsx index 47b2e505ce..bee6516a59 100644 --- a/apps/login/src/app/(login)/error.tsx +++ b/apps/login/src/app/(login)/error.tsx @@ -1,14 +1,17 @@ "use client"; -import { Boundary } from "@/ui/Boundary"; -import { Button } from "@/ui/Button"; -import React from "react"; +import { Boundary } from "@/components/boundary"; +import { Button } from "@/components/button"; +import { useTranslations } from "next-intl"; +import { useEffect } from "react"; export default function Error({ error, reset }: any) { - React.useEffect(() => { + useEffect(() => { console.log("logging error:", error); }, [error]); + const t = useTranslations("error"); + return (
@@ -16,7 +19,7 @@ export default function Error({ error, reset }: any) { Error: {error?.message}
- +
diff --git a/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx b/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx index 6ae205bcb5..a3cc0ee883 100644 --- a/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx @@ -1,44 +1,35 @@ -import { ProviderSlug } from "@/lib/demos"; -import { getBrandingSettings, PROVIDER_NAME_MAPPING } from "@/lib/zitadel"; -import DynamicTheme from "@/ui/DynamicTheme"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { getBrandingSettings } from "@/lib/zitadel"; +import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { getLocale, getTranslations } from "next-intl/server"; -export default async function Page({ - searchParams, - params, -}: { - searchParams: Record; - params: { provider: ProviderSlug }; +// This configuration shows the given name in the respective IDP button as fallback +const PROVIDER_NAME_MAPPING: { + [provider: string]: string; +} = { + [IdentityProviderType.GOOGLE]: "Google", + [IdentityProviderType.GITHUB]: "GitHub", + [IdentityProviderType.AZURE_AD]: "Microsoft", +}; + +export default async function Page(props: { + searchParams: Promise>; + params: Promise<{ provider: string }>; }) { - const { id, token, authRequestId, organization } = searchParams; - const { provider } = params; + const searchParams = await props.searchParams; + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "idp" }); + + const { organization } = searchParams; const branding = await getBrandingSettings(organization); - if (provider) { - return ( - -
-

Login failure

-
- An error signing in with{" "} - {PROVIDER_NAME_MAPPING[provider] - ? PROVIDER_NAME_MAPPING[provider] - : provider}{" "} - happened! -
- - {/* - {} - */} -
-
- ); - } else { - return ( + return ( +
-

Register

-

No id and token received!

+

{t("loginError.title")}

+

{t("loginError.description")}

- ); - } +
+ ); } diff --git a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx index 50d902007d..0de29fd1ba 100644 --- a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx @@ -1,186 +1,220 @@ -import { ProviderSlug } from "@/lib/demos"; -import { getBrandingSettings, userService } from "@/lib/zitadel"; -import Alert, { AlertType } from "@/ui/Alert"; -import DynamicTheme from "@/ui/DynamicTheme"; -import IdpSignin from "@/ui/IdpSignin"; -import { AddHumanUserRequest } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; -import { IDPInformation, IDPLink } from "@zitadel/proto/zitadel/user/v2/idp_pb"; -import { PartialMessage } from "@zitadel/client"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { IdpSignin } from "@/components/idp-signin"; +import { linkingFailed } from "@/components/idps/pages/linking-failed"; +import { linkingSuccess } from "@/components/idps/pages/linking-success"; +import { loginFailed } from "@/components/idps/pages/login-failed"; +import { loginSuccess } from "@/components/idps/pages/login-success"; +import { idpTypeToIdentityProviderType, PROVIDER_MAPPING } from "@/lib/idp"; +import { + addHuman, + addIDPLink, + getBrandingSettings, + getIDPByID, + getLoginSettings, + getOrgsByDomain, + listUsers, + retrieveIDPIntent, +} from "@/lib/zitadel"; +import { create } from "@zitadel/client"; +import { AutoLinkingOption } from "@zitadel/proto/zitadel/idp/v2/idp_pb"; +import { OrganizationSchema } from "@zitadel/proto/zitadel/object/v2/object_pb"; +import { + AddHumanUserRequest, + AddHumanUserRequestSchema, +} from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { getLocale, getTranslations } from "next-intl/server"; -const PROVIDER_MAPPING: { - [provider: string]: ( - rI: IDPInformation, - ) => PartialMessage; -} = { - [ProviderSlug.GOOGLE]: (idp: IDPInformation) => { - const rawInfo = idp.rawInformation?.toJson() as { - User: { - email: string; - name?: string; - given_name?: string; - family_name?: string; - }; - }; +const ORG_SUFFIX_REGEX = /(?<=@)(.+)/; - const idpLink: PartialMessage = { - idpId: idp.idpId, - userId: idp.userId, - userName: idp.userName, - }; - - const req: PartialMessage = { - username: idp.userName, - email: { - email: rawInfo.User?.email, - verification: { case: "isVerified", value: true }, - }, - // organisation: Organisation | undefined; - profile: { - displayName: rawInfo.User?.name ?? "", - givenName: rawInfo.User?.given_name ?? "", - familyName: rawInfo.User?.family_name ?? "", - }, - idpLinks: [idpLink], - }; - return req; - }, - [ProviderSlug.GITHUB]: (idp: IDPInformation) => { - const rawInfo = idp.rawInformation?.toJson() as { - email: string; - name: string; - }; - const idpLink: PartialMessage = { - idpId: idp.idpId, - userId: idp.userId, - userName: idp.userName, - }; - const req: PartialMessage = { - username: idp.userName, - email: { - email: rawInfo?.email, - verification: { case: "isVerified", value: true }, - }, - // organisation: Organisation | undefined; - profile: { - displayName: rawInfo?.name ?? "", - givenName: rawInfo?.name ?? "", - familyName: rawInfo?.name ?? "", - }, - idpLinks: [idpLink], - }; - return req; - }, -}; - -function retrieveIDPIntent(id: string, token: string) { - return userService.retrieveIdentityProviderIntent( - { idpIntentId: id, idpIntentToken: token }, - {}, - ); -} - -function createUser( - provider: ProviderSlug, - info: IDPInformation, -): Promise { - const userData = PROVIDER_MAPPING[provider](info); - return userService.addHumanUser(userData, {}).then((resp) => resp.userId); -} - -export default async function Page({ - searchParams, - params, -}: { - searchParams: Record; - params: { provider: ProviderSlug }; +export default async function Page(props: { + searchParams: Promise>; + params: Promise<{ provider: string }>; }) { - const { id, token, authRequestId, organization } = searchParams; + const params = await props.params; + const searchParams = await props.searchParams; + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "idp" }); + const { id, token, authRequestId, organization, link } = searchParams; const { provider } = params; const branding = await getBrandingSettings(organization); - if (provider && id && token) { - return retrieveIDPIntent(id, token) - .then((resp) => { - const { idpInformation, userId } = resp; + if (!provider || !id || !token) { + return loginFailed(branding, "IDP context missing"); + } - if (idpInformation) { - // handle login - if (userId) { - return ( - -
-

Login successful

-
You have successfully been loggedIn!
+ const intent = await retrieveIDPIntent(id, token); - -
-
- ); - } else { - // handle register - return createUser(provider, idpInformation) - .then((userId) => { - return ( - -
-

Register successful

-
You have successfully been registered!
-
-
- ); - }) - .catch((error) => { - return ( - -
-

Register failed

-
- { - - {JSON.stringify(error.message)} - - } -
-
-
- ); - }); - } - } else { - throw new Error("Could not get user information."); - } - }) - .catch((error) => { - return ( - -
-

An error occurred

-
- { - - {JSON.stringify(error.message)} - - } -
-
-
- ); - }); - } else { - return ( - -
-
-

Register

-

No id and token received!

-
-
-
+ const { idpInformation, userId } = intent; + + // sign in user. If user should be linked continue + if (userId && !link) { + // TODO: update user if idp.options.isAutoUpdate is true + + return loginSuccess( + userId, + { idpIntentId: id, idpIntentToken: token }, + authRequestId, + branding, ); } + + if (!idpInformation) { + return loginFailed(branding, "IDP information missing"); + } + + const idp = await getIDPByID(idpInformation.idpId); + const options = idp?.config?.options; + + if (!idp) { + throw new Error("IDP not found"); + } + + const providerType = idpTypeToIdentityProviderType(idp.type); + + if (link) { + if (!options?.isLinkingAllowed) { + // linking was probably disallowed since the invitation was created + return linkingFailed(branding, "Linking is no longer allowed"); + } + + let idpLink; + try { + idpLink = await addIDPLink( + { + id: idpInformation.idpId, + userId: idpInformation.userId, + userName: idpInformation.userName, + }, + userId, + ); + } catch (error) { + console.error(error); + return linkingFailed(branding); + } + + if (!idpLink) { + return linkingFailed(branding); + } else { + return linkingSuccess( + userId, + { idpIntentId: id, idpIntentToken: token }, + authRequestId, + branding, + ); + } + } + + // search for potential user via username, then link + if (options?.isLinkingAllowed) { + let foundUser; + const email = PROVIDER_MAPPING[providerType](idpInformation).email?.email; + + if (options.autoLinking === AutoLinkingOption.EMAIL && email) { + foundUser = await listUsers({ email }).then((response) => { + return response.result ? response.result[0] : null; + }); + } else if (options.autoLinking === AutoLinkingOption.USERNAME) { + foundUser = await listUsers( + options.autoLinking === AutoLinkingOption.USERNAME + ? { userName: idpInformation.userName } + : { email }, + ).then((response) => { + return response.result ? response.result[0] : null; + }); + } else { + foundUser = await listUsers({ + userName: idpInformation.userName, + email, + }).then((response) => { + return response.result ? response.result[0] : null; + }); + } + + if (foundUser) { + let idpLink; + try { + idpLink = await addIDPLink( + { + id: idpInformation.idpId, + userId: idpInformation.userId, + userName: idpInformation.userName, + }, + foundUser.userId, + ); + } catch (error) { + console.error(error); + return linkingFailed(branding); + } + + if (!idpLink) { + return linkingFailed(branding); + } else { + return linkingSuccess( + foundUser.userId, + { idpIntentId: id, idpIntentToken: token }, + authRequestId, + branding, + ); + } + } + } + + if (options?.isCreationAllowed && options.isAutoCreation) { + let orgToRegisterOn: string | undefined = organization; + + let userData: AddHumanUserRequest = + PROVIDER_MAPPING[providerType](idpInformation); + + if ( + !orgToRegisterOn && + userData.username && // username or email? + ORG_SUFFIX_REGEX.test(userData.username) + ) { + const matched = ORG_SUFFIX_REGEX.exec(userData.username); + const suffix = matched?.[1] ?? ""; + + // this just returns orgs where the suffix is set as primary domain + const orgs = await getOrgsByDomain(suffix); + const orgToCheckForDiscovery = + orgs.result && orgs.result.length === 1 ? orgs.result[0].id : undefined; + + const orgLoginSettings = await getLoginSettings(orgToCheckForDiscovery); + if (orgLoginSettings?.allowDomainDiscovery) { + orgToRegisterOn = orgToCheckForDiscovery; + } + } + + if (orgToRegisterOn) { + const organizationSchema = create(OrganizationSchema, { + org: { case: "orgId", value: orgToRegisterOn }, + }); + + userData = create(AddHumanUserRequestSchema, { + ...userData, + organization: organizationSchema, + }); + } + + const newUser = await addHuman(userData); + + if (newUser) { + return ( + +
+

{t("registerSuccess.title")}

+

{t("registerSuccess.description")}

+ +
+
+ ); + } + } + + // return login failed if no linking or creation is allowed and no user was found + return loginFailed(branding, "No user found"); } diff --git a/apps/login/src/app/(login)/idp/page.tsx b/apps/login/src/app/(login)/idp/page.tsx index 9796378e81..30d1c9fab5 100644 --- a/apps/login/src/app/(login)/idp/page.tsx +++ b/apps/login/src/app/(login)/idp/page.tsx @@ -1,11 +1,8 @@ -import { - getBrandingSettings, - getLegalAndSupportSettings, - settingsService, -} from "@/lib/zitadel"; -import DynamicTheme from "@/ui/DynamicTheme"; -import { SignInWithIDP } from "@/ui/SignInWithIDP"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { SignInWithIdp } from "@/components/sign-in-with-idp"; +import { getBrandingSettings, settingsService } from "@/lib/zitadel"; import { makeReqCtx } from "@zitadel/client/v2"; +import { getLocale, getTranslations } from "next-intl/server"; function getIdentityProviders(orgId?: string) { return settingsService @@ -15,39 +12,32 @@ function getIdentityProviders(orgId?: string) { }); } -export default async function Page({ - searchParams, -}: { - searchParams: Record; +export default async function Page(props: { + searchParams: Promise>; }) { + const searchParams = await props.searchParams; + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "idp" }); + const authRequestId = searchParams?.authRequestId; const organization = searchParams?.organization; - const legal = await getLegalAndSupportSettings(organization); - const identityProviders = await getIdentityProviders(organization); - const host = process.env.VERCEL_URL - ? `https://${process.env.VERCEL_URL}` - : "http://localhost:3000"; - const branding = await getBrandingSettings(organization); return (
-

Register

-

- Select one of the following providers to register -

+

{t("title")}

+

{t("description")}

- {legal && identityProviders && process.env.ZITADEL_API_URL && ( - + > )}
diff --git a/apps/login/src/app/(login)/invite/page.tsx b/apps/login/src/app/(login)/invite/page.tsx new file mode 100644 index 0000000000..18c60eb993 --- /dev/null +++ b/apps/login/src/app/(login)/invite/page.tsx @@ -0,0 +1,60 @@ +import { Alert, AlertType } from "@/components/alert"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { InviteForm } from "@/components/invite-form"; +import { + getBrandingSettings, + getDefaultOrg, + getLoginSettings, + getPasswordComplexitySettings, +} from "@/lib/zitadel"; +import { getLocale, getTranslations } from "next-intl/server"; + +export default async function Page(props: { + searchParams: Promise>; +}) { + const searchParams = await props.searchParams; + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "invite" }); + + let { firstname, lastname, email, organization } = searchParams; + + if (!organization) { + const org = await getDefaultOrg(); + if (!org) { + throw new Error("No default organization found"); + } + + organization = org.id; + } + + const loginSettings = await getLoginSettings(organization); + + const passwordComplexitySettings = + await getPasswordComplexitySettings(organization); + + const branding = await getBrandingSettings(organization); + + return ( + +
+

{t("title")}

+

{t("description")}

+ + {!loginSettings?.allowRegister ? ( + {t("notAllowed")} + ) : ( + {t("info")} + )} + + {passwordComplexitySettings && loginSettings?.allowRegister && ( + + )} +
+
+ ); +} diff --git a/apps/login/src/app/(login)/invite/success/page.tsx b/apps/login/src/app/(login)/invite/success/page.tsx new file mode 100644 index 0000000000..96c4984159 --- /dev/null +++ b/apps/login/src/app/(login)/invite/success/page.tsx @@ -0,0 +1,70 @@ +import { Alert, AlertType } from "@/components/alert"; +import { Button, ButtonVariants } from "@/components/button"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { UserAvatar } from "@/components/user-avatar"; +import { getBrandingSettings, getDefaultOrg, getUserByID } from "@/lib/zitadel"; +import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb"; +import { getLocale, getTranslations } from "next-intl/server"; +import Link from "next/link"; + +export default async function Page(props: { + searchParams: Promise>; +}) { + const searchParams = await props.searchParams; + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "invite" }); + + let { userId, organization } = searchParams; + + if (!organization) { + const org = await getDefaultOrg(); + if (!org) { + throw new Error("No default organization found"); + } + + organization = org.id; + } + + const branding = await getBrandingSettings(organization); + + let user: User | undefined; + let human: HumanUser | undefined; + if (userId) { + const userResponse = await getUserByID(userId); + if (userResponse) { + user = userResponse.user; + if (user?.type.case === "human") { + human = user.type.value as HumanUser; + } + } + } + + return ( + +
+

{t("success.title")}

+

{t("success.description")}

+ {user && ( + + )} + {human?.email?.isVerified ? ( + {t("success.verified")} + ) : ( + {t("success.notVerifiedYet")} + )} +
+ + + + +
+
+
+ ); +} diff --git a/apps/login/src/app/(login)/layout.tsx b/apps/login/src/app/(login)/layout.tsx new file mode 100644 index 0000000000..936c7e17e4 --- /dev/null +++ b/apps/login/src/app/(login)/layout.tsx @@ -0,0 +1,62 @@ +import "@/styles/globals.scss"; + +import { LanguageProvider } from "@/components/language-provider"; +import { LanguageSwitcher } from "@/components/language-switcher"; +import { Skeleton } from "@/components/skeleton"; +import { Theme } from "@/components/theme"; +import { ThemeProvider } from "@/components/theme-provider"; +import { Analytics } from "@vercel/analytics/react"; +import { Lato } from "next/font/google"; +import { ReactNode, Suspense } from "react"; + +const lato = Lato({ + weight: ["400", "700", "900"], + subsets: ["latin"], +}); + +export default async function RootLayout({ + children, +}: { + children: ReactNode; +}) { + return ( + + + + + +
+ +
+
+
+ +
+
+ + } + > + +
+
+ {children} +
+ + +
+
+
+
+
+
+ + + + ); +} diff --git a/apps/login/src/app/(login)/loginname/page.tsx b/apps/login/src/app/(login)/loginname/page.tsx index ee2139aff4..44601e1845 100644 --- a/apps/login/src/app/(login)/loginname/page.tsx +++ b/apps/login/src/app/(login)/loginname/page.tsx @@ -1,66 +1,70 @@ +import { DynamicTheme } from "@/components/dynamic-theme"; +import { SignInWithIdp } from "@/components/sign-in-with-idp"; +import { UsernameForm } from "@/components/username-form"; import { + getActiveIdentityProviders, getBrandingSettings, - getLegalAndSupportSettings, + getDefaultOrg, getLoginSettings, - settingsService, } from "@/lib/zitadel"; -import DynamicTheme from "@/ui/DynamicTheme"; -import { SignInWithIDP } from "@/ui/SignInWithIDP"; -import UsernameForm from "@/ui/UsernameForm"; -import { makeReqCtx } from "@zitadel/client/v2"; +import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; +import { getLocale, getTranslations } from "next-intl/server"; -function getIdentityProviders(orgId?: string) { - return settingsService - .getActiveIdentityProviders({ ctx: makeReqCtx(orgId) }, {}) - .then((resp) => { - return resp.identityProviders; - }); -} - -export default async function Page({ - searchParams, -}: { - searchParams: Record; +export default async function Page(props: { + searchParams: Promise>; }) { + const searchParams = await props.searchParams; + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "loginname" }); + const loginName = searchParams?.loginName; const authRequestId = searchParams?.authRequestId; const organization = searchParams?.organization; const submit: boolean = searchParams?.submit === "true"; - const loginSettings = await getLoginSettings(organization); - const legal = await getLegalAndSupportSettings(); + let defaultOrganization; + if (!organization) { + const org: Organization | null = await getDefaultOrg(); + if (org) { + defaultOrganization = org.id; + } + } - const identityProviders = await getIdentityProviders(organization); + const loginSettings = await getLoginSettings( + organization ?? defaultOrganization, + ); - const host = process.env.VERCEL_URL - ? `https://${process.env.VERCEL_URL}` - : "http://localhost:3000"; + const identityProviders = await getActiveIdentityProviders( + organization ?? defaultOrganization, + ).then((resp) => { + return resp.identityProviders; + }); - const branding = await getBrandingSettings(organization); + const branding = await getBrandingSettings( + organization ?? defaultOrganization, + ); return (
-

Welcome back!

-

Enter your login data.

+

{t("title")}

+

{t("description")}

- - {legal && identityProviders && process.env.ZITADEL_API_URL && ( - - )} + > + {identityProviders && ( + + )} +
); diff --git a/apps/login/src/app/(login)/mfa/create/page.tsx b/apps/login/src/app/(login)/mfa/create/page.tsx deleted file mode 100644 index 4fd798f7e7..0000000000 --- a/apps/login/src/app/(login)/mfa/create/page.tsx +++ /dev/null @@ -1,35 +0,0 @@ -"use client"; -import { Button, ButtonVariants } from "@/ui/Button"; -import { TextInput } from "@/ui/Input"; -import UserAvatar from "@/ui/UserAvatar"; -import { useRouter } from "next/navigation"; - -export default function Page() { - const router = useRouter(); - - return ( -
-

Password

-

Enter your password.

- - - -
- -
-
- - -
-
- ); -} diff --git a/apps/login/src/app/(login)/mfa/page.tsx b/apps/login/src/app/(login)/mfa/page.tsx index 75d491b856..071806db04 100644 --- a/apps/login/src/app/(login)/mfa/page.tsx +++ b/apps/login/src/app/(login)/mfa/page.tsx @@ -1,25 +1,26 @@ +import { Alert } from "@/components/alert"; +import { BackButton } from "@/components/back-button"; +import { ChooseSecondFactor } from "@/components/choose-second-factor"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { UserAvatar } from "@/components/user-avatar"; +import { getSessionCookieById } from "@/lib/cookies"; +import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings, getSession, listAuthenticationMethodTypes, } from "@/lib/zitadel"; -import Alert from "@/ui/Alert"; -import BackButton from "@/ui/BackButton"; -import ChooseSecondFactor from "@/ui/ChooseSecondFactor"; -import DynamicTheme from "@/ui/DynamicTheme"; -import UserAvatar from "@/ui/UserAvatar"; -import { - getMostRecentCookieWithLoginname, - getSessionCookieById, -} from "@/utils/cookies"; +import { getLocale, getTranslations } from "next-intl/server"; -export default async function Page({ - searchParams, -}: { - searchParams: Record; +export default async function Page(props: { + searchParams: Promise>; }) { - const { loginName, checkAfter, authRequestId, organization, sessionId } = - searchParams; + const searchParams = await props.searchParams; + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "mfa" }); + const tError = await getTranslations({ locale, namespace: "error" }); + + const { loginName, authRequestId, organization, sessionId } = searchParams; const sessionFactors = sessionId ? await loadSessionById(sessionId, organization) @@ -29,27 +30,29 @@ export default async function Page({ loginName?: string, organization?: string, ) { - const recent = await getMostRecentCookieWithLoginname( + return loadMostRecentSession({ loginName, organization, - ); - return getSession(recent.id, recent.token).then((response) => { - if (response?.session && response.session.factors?.user?.id) { - return listAuthenticationMethodTypes( - response.session.factors.user.id, - ).then((methods) => { - return { - factors: response.session?.factors, - authMethods: methods.authMethodTypes ?? [], - }; - }); + }).then((session) => { + if (session && session.factors?.user?.id) { + return listAuthenticationMethodTypes(session.factors.user.id).then( + (methods) => { + return { + factors: session?.factors, + authMethods: methods.authMethodTypes ?? [], + }; + }, + ); } }); } async function loadSessionById(sessionId: string, organization?: string) { - const recent = await getSessionCookieById(sessionId, organization); - return getSession(recent.id, recent.token).then((response) => { + const recent = await getSessionCookieById({ sessionId, organization }); + return getSession({ + sessionId: recent.id, + sessionToken: recent.token, + }).then((response) => { if (response?.session && response.session.factors?.user?.id) { return listAuthenticationMethodTypes( response.session.factors.user.id, @@ -68,9 +71,9 @@ export default async function Page({ return (
-

Verify 2-Factor

+

{t("verify.title")}

-

Choose one of the following second factors.

+

{t("verify.description")}

{sessionFactors && ( )} - {!(loginName || sessionId) && ( - Provide your active session as loginName param - )} + {!(loginName || sessionId) && {tError("unknownContext")}} {sessionFactors ? ( ) : ( - No second factors available to setup. + {t("verify.noResults")} )}
diff --git a/apps/login/src/app/(login)/mfa/set/page.tsx b/apps/login/src/app/(login)/mfa/set/page.tsx index c9f3ce8c36..dce54618dc 100644 --- a/apps/login/src/app/(login)/mfa/set/page.tsx +++ b/apps/login/src/app/(login)/mfa/set/page.tsx @@ -1,3 +1,10 @@ +import { Alert } from "@/components/alert"; +import { BackButton } from "@/components/back-button"; +import { ChooseSecondFactorToSetup } from "@/components/choose-second-factor-to-setup"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { UserAvatar } from "@/components/user-avatar"; +import { getSessionCookieById } from "@/lib/cookies"; +import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings, getLoginSettings, @@ -5,90 +12,105 @@ import { getUserByID, listAuthenticationMethodTypes, } from "@/lib/zitadel"; -import Alert from "@/ui/Alert"; -import BackButton from "@/ui/BackButton"; -import ChooseSecondFactorToSetup from "@/ui/ChooseSecondFactorToSetup"; -import DynamicTheme from "@/ui/DynamicTheme"; -import UserAvatar from "@/ui/UserAvatar"; -import { - getMostRecentCookieWithLoginname, - getSessionCookieById, -} from "@/utils/cookies"; +import { Timestamp, timestampDate } from "@zitadel/client"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { getLocale, getTranslations } from "next-intl/server"; -export default async function Page({ - searchParams, -}: { - searchParams: Record; +function isSessionValid(session: Partial): { + valid: boolean; + verifiedAt?: Timestamp; +} { + const validPassword = session?.factors?.password?.verifiedAt; + const validPasskey = session?.factors?.webAuthN?.verifiedAt; + const stillValid = session.expirationDate + ? timestampDate(session.expirationDate) > new Date() + : true; + + const verifiedAt = validPassword || validPasskey; + const valid = !!((validPassword || validPasskey) && stillValid); + + return { valid, verifiedAt }; +} + +export default async function Page(props: { + searchParams: Promise>; }) { - const { loginName, checkAfter, authRequestId, organization, sessionId } = - searchParams; + const searchParams = await props.searchParams; + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "mfa" }); + const tError = await getTranslations({ locale, namespace: "error" }); + + const { + loginName, + checkAfter, + force, + authRequestId, + organization, + sessionId, + } = searchParams; const sessionWithData = sessionId ? await loadSessionById(sessionId, organization) : await loadSessionByLoginname(loginName, organization); + async function getAuthMethodsAndUser(session?: Session) { + const userId = session?.factors?.user?.id; + + if (!userId) { + throw Error("Could not get user id from session"); + } + + return listAuthenticationMethodTypes(userId).then((methods) => { + return getUserByID(userId).then((user) => { + const humanUser = + user.user?.type.case === "human" ? user.user?.type.value : undefined; + + return { + factors: session?.factors, + authMethods: methods.authMethodTypes ?? [], + phoneVerified: humanUser?.phone?.isVerified ?? false, + emailVerified: humanUser?.email?.isVerified ?? false, + expirationDate: session?.expirationDate, + }; + }); + }); + } + async function loadSessionByLoginname( loginName?: string, organization?: string, ) { - const recent = await getMostRecentCookieWithLoginname( + return loadMostRecentSession({ loginName, organization, - ); - return getSession(recent.id, recent.token).then((response) => { - if (response?.session && response.session.factors?.user?.id) { - const userId = response.session.factors.user.id; - return listAuthenticationMethodTypes(userId).then((methods) => { - return getUserByID(userId).then((user) => { - const humanUser = - user.user?.type.case === "human" - ? user.user?.type.value - : undefined; - - return { - factors: response.session?.factors, - authMethods: methods.authMethodTypes ?? [], - phoneVerified: humanUser?.phone?.isVerified ?? false, - emailVerified: humanUser?.email?.isVerified ?? false, - }; - }); - }); - } + }).then((session) => { + return getAuthMethodsAndUser(session); }); } async function loadSessionById(sessionId: string, organization?: string) { - const recent = await getSessionCookieById(sessionId, organization); - return getSession(recent.id, recent.token).then((response) => { - if (response?.session && response.session.factors?.user?.id) { - const userId = response.session.factors.user.id; - return listAuthenticationMethodTypes(userId).then((methods) => { - return getUserByID(userId).then((user) => { - const humanUser = - user.user?.type.case === "human" - ? user.user?.type.value - : undefined; - return { - factors: response.session?.factors, - authMethods: methods.authMethodTypes ?? [], - phoneVerified: humanUser?.phone?.isVerified ?? false, - emailVerified: humanUser?.email?.isVerified ?? false, - }; - }); - }); - } + const recent = await getSessionCookieById({ sessionId, organization }); + return getSession({ + sessionId: recent.id, + sessionToken: recent.token, + }).then((sessionResponse) => { + return getAuthMethodsAndUser(sessionResponse.session); }); } const branding = await getBrandingSettings(organization); - const loginSettings = await getLoginSettings(organization); + const loginSettings = await getLoginSettings( + sessionWithData.factors?.user?.organizationId, + ); + + const { valid } = isSessionValid(sessionWithData); return (
-

Set up 2-Factor

+

{t("set.title")}

-

Choose one of the following second factors.

+

{t("set.description")}

{sessionWithData && ( )} - {!(loginName || sessionId) && ( - Provide your active session as loginName param - )} + {!(loginName || sessionId) && {tError("unknownContext")}} - {loginSettings && sessionWithData ? ( - - ) : ( - No second factors available to setup. - )} + {!valid && {tError("sessionExpired")}} + + {isSessionValid(sessionWithData).valid && + loginSettings && + sessionWithData && ( + + )}
diff --git a/apps/login/src/app/(login)/otp/[method]/page.tsx b/apps/login/src/app/(login)/otp/[method]/page.tsx index fc1641da5f..1755561238 100644 --- a/apps/login/src/app/(login)/otp/[method]/page.tsx +++ b/apps/login/src/app/(login)/otp/[method]/page.tsx @@ -1,61 +1,83 @@ +import { Alert } from "@/components/alert"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { LoginOTP } from "@/components/login-otp"; +import { UserAvatar } from "@/components/user-avatar"; +import { getSessionCookieById } from "@/lib/cookies"; +import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings, getLoginSettings, getSession, } from "@/lib/zitadel"; -import Alert from "@/ui/Alert"; -import DynamicTheme from "@/ui/DynamicTheme"; -import LoginOTP from "@/ui/LoginOTP"; -import UserAvatar from "@/ui/UserAvatar"; -import { getMostRecentCookieWithLoginname } from "@/utils/cookies"; +import { getLocale, getTranslations } from "next-intl/server"; +import { headers } from "next/headers"; -export default async function Page({ - searchParams, - params, -}: { - searchParams: Record; - params: Record; +export default async function Page(props: { + searchParams: Promise>; + params: Promise>; }) { - const { loginName, authRequestId, sessionId, organization, code, submit } = - searchParams; + const params = await props.params; + const searchParams = await props.searchParams; + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "otp" }); + const tError = await getTranslations({ locale, namespace: "error" }); + + const { + loginName, // send from password page + userId, // send from email link + authRequestId, + sessionId, + organization, + code, + submit, + } = searchParams; const { method } = params; - const { session, token } = await loadSession(loginName, organization); + const session = sessionId + ? await loadSessionById(sessionId, organization) + : await loadMostRecentSession({ loginName, organization }); - const branding = await getBrandingSettings(organization); - - async function loadSession(loginName?: string, organization?: string) { - const recent = await getMostRecentCookieWithLoginname( - loginName, - organization, - ); - - return getSession(recent.id, recent.token).then((response) => { - return { session: response?.session, token: recent.token }; + async function loadSessionById(sessionId: string, organization?: string) { + const recent = await getSessionCookieById({ sessionId, organization }); + return getSession({ + sessionId: recent.id, + sessionToken: recent.token, + }).then((response) => { + if (response?.session) { + return response.session; + } }); } + // email links do not come with organization, thus we need to use the session's organization + const branding = await getBrandingSettings( + organization ?? session?.factors?.user?.organizationId, + ); + + const loginSettings = await getLoginSettings( + organization ?? session?.factors?.user?.organizationId, + ); + + const host = (await headers()).get("host"); + return (
-

Verify 2-Factor

+

{t("verify.title")}

{method === "time-based" && ( -

Enter the code from your authenticator app.

+

{t("verify.totpDescription")}

)} {method === "sms" && ( -

Enter the code you got on your phone.

+

{t("verify.smsDescription")}

)} {method === "email" && ( -

Enter the code you got via your email.

+

{t("verify.emailDescription")}

)} {!session && (
- - Could not get the context of the user. Make sure to enter the - username first or provide a loginName as searchParam. - + {tError("unknownContext")}
)} @@ -68,13 +90,18 @@ export default async function Page({ > )} - {method && ( + {method && session && ( )}
diff --git a/apps/login/src/app/(login)/otp/[method]/set/page.tsx b/apps/login/src/app/(login)/otp/[method]/set/page.tsx index fb8ce9b545..e64b4debe0 100644 --- a/apps/login/src/app/(login)/otp/[method]/set/page.tsx +++ b/apps/login/src/app/(login)/otp/[method]/set/page.tsx @@ -1,37 +1,45 @@ +import { Alert } from "@/components/alert"; +import { BackButton } from "@/components/back-button"; +import { Button, ButtonVariants } from "@/components/button"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { TotpRegister } from "@/components/totp-register"; +import { UserAvatar } from "@/components/user-avatar"; +import { loadMostRecentSession } from "@/lib/session"; import { addOTPEmail, addOTPSMS, getBrandingSettings, - getSession, + getLoginSettings, registerTOTP, } from "@/lib/zitadel"; -import Alert from "@/ui/Alert"; -import BackButton from "@/ui/BackButton"; -import { Button, ButtonVariants } from "@/ui/Button"; -import DynamicTheme from "@/ui/DynamicTheme"; -import { Spinner } from "@/ui/Spinner"; -import TOTPRegister from "@/ui/TOTPRegister"; -import UserAvatar from "@/ui/UserAvatar"; -import { getMostRecentCookieWithLoginname } from "@/utils/cookies"; -import Link from "next/link"; import { RegisterTOTPResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { getLocale, getTranslations } from "next-intl/server"; +import Link from "next/link"; +import { redirect } from "next/navigation"; -export default async function Page({ - searchParams, - params, -}: { - searchParams: Record; - params: Record; +export default async function Page(props: { + searchParams: Promise>; + params: Promise>; }) { + const params = await props.params; + const searchParams = await props.searchParams; + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "otp" }); + const tError = await getTranslations({ locale, namespace: "error" }); + const { loginName, organization, sessionId, authRequestId, checkAfter } = searchParams; const { method } = params; const branding = await getBrandingSettings(organization); - const { session, token } = await loadSession(loginName, organization); + const loginSettings = await getLoginSettings(organization); - let totpResponse: RegisterTOTPResponse | undefined, - totpError: Error | undefined; + const session = await loadMostRecentSession({ + loginName, + organization, + }); + + let totpResponse: RegisterTOTPResponse | undefined, error: Error | undefined; if (session && session.factors?.user?.id) { if (method === "time-based") { await registerTOTP(session.factors.user.id) @@ -40,15 +48,19 @@ export default async function Page({ totpResponse = resp; } }) - .catch((error) => { - totpError = error; + .catch((err) => { + error = err; }); } else if (method === "sms") { // does not work - await addOTPSMS(session.factors.user.id); + await addOTPSMS(session.factors.user.id).catch((error) => { + error = new Error("Could not add OTP via SMS"); + }); } else if (method === "email") { // works - await addOTPEmail(session.factors.user.id); + await addOTPEmail(session.factors.user.id).catch((error) => { + error = new Error("Could not add OTP via Email"); + }); } else { throw new Error("Invalid method"); } @@ -56,61 +68,53 @@ export default async function Page({ throw new Error("No session found"); } - async function loadSession(loginName?: string, organization?: string) { - const recent = await getMostRecentCookieWithLoginname( - loginName, - organization, - ); - - return getSession(recent.id, recent.token).then((response) => { - return { session: response?.session, token: recent.token }; - }); - } - const paramsToContinue = new URLSearchParams({}); let urlToContinue = "/accounts"; - if (authRequestId && sessionId) { - if (sessionId) { - paramsToContinue.append("sessionId", sessionId); - } + if (sessionId) { + paramsToContinue.append("sessionId", sessionId); + } + if (loginName) { + paramsToContinue.append("loginName", loginName); + } + if (organization) { + paramsToContinue.append("organization", organization); + } + + if (checkAfter) { if (authRequestId) { paramsToContinue.append("authRequestId", authRequestId); } - if (organization) { - paramsToContinue.append("organization", organization); + urlToContinue = `/otp/${method}?` + paramsToContinue; + // immediately check the OTP on the next page if sms or email was set up + if (["email", "sms"].includes(method)) { + return redirect(urlToContinue); + } + } else if (authRequestId && sessionId) { + if (authRequestId) { + paramsToContinue.append("authRequest", authRequestId); } urlToContinue = `/login?` + paramsToContinue; } else if (loginName) { - if (loginName) { - paramsToContinue.append("loginName", loginName); - } if (authRequestId) { paramsToContinue.append("authRequestId", authRequestId); } - if (organization) { - paramsToContinue.append("organization", organization); - } - urlToContinue = `/signedin?` + paramsToContinue; } return (
-

Register 2-factor

+

{t("set.title")}

{!session && (
- - Could not get the context of the user. Make sure to enter the - username first or provide a loginName as searchParam. - + {tError("unknownContext")}
)} - {totpError && ( + {error && (
- {totpError?.message} + {error?.message}
)} @@ -125,13 +129,9 @@ export default async function Page({ {totpResponse && "uri" in totpResponse && "secret" in totpResponse ? ( <> -

- Scan the QR Code or navigate to the URL manually. -

+

{t("set.totpRegisterDescription")}

- {/* {auth &&
{auth.to}
} */} - - + loginSettings={loginSettings} + >
{" "} ) : ( @@ -155,19 +156,14 @@ export default async function Page({
- + +
diff --git a/apps/login/src/app/(login)/page.tsx b/apps/login/src/app/(login)/page.tsx new file mode 100644 index 0000000000..f1fce50f90 --- /dev/null +++ b/apps/login/src/app/(login)/page.tsx @@ -0,0 +1,8 @@ +import { redirect } from "next/navigation"; + +export default function Page() { + // automatically redirect to loginname + if (process.env.DEBUG !== "true") { + redirect("/loginname"); + } +} diff --git a/apps/login/src/app/(login)/passkey/add/page.tsx b/apps/login/src/app/(login)/passkey/add/page.tsx deleted file mode 100644 index ac8656342a..0000000000 --- a/apps/login/src/app/(login)/passkey/add/page.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { getBrandingSettings, getSession } from "@/lib/zitadel"; -import Alert, { AlertType } from "@/ui/Alert"; -import DynamicTheme from "@/ui/DynamicTheme"; -import RegisterPasskey from "@/ui/RegisterPasskey"; -import UserAvatar from "@/ui/UserAvatar"; -import { getMostRecentCookieWithLoginname } from "@/utils/cookies"; - -export default async function Page({ - searchParams, -}: { - searchParams: Record; -}) { - const { loginName, promptPasswordless, organization, authRequestId } = - searchParams; - - const sessionFactors = await loadSession(loginName); - - async function loadSession(loginName?: string) { - const recent = await getMostRecentCookieWithLoginname( - loginName, - organization, - ); - return getSession(recent.id, recent.token).then((response) => { - if (response?.session) { - return response.session; - } - }); - } - const title = !!promptPasswordless - ? "Authenticate with a passkey" - : "Use your passkey to confirm it's really you"; - const description = !!promptPasswordless - ? "When set up, you will be able to authenticate without a password." - : "Your device will ask for your fingerprint, face, or screen lock"; - - const branding = await getBrandingSettings(organization); - - return ( - -
-

{title}

- - {sessionFactors && ( - - )} -

{description}

- - - - A passkey is an authentication method on a device like your - fingerprint, Apple FaceID or similar. - - Passwordless Authentication - - - - - {!sessionFactors && ( -
- - Could not get the context of the user. Make sure to enter the - username first or provide a loginName as searchParam. - -
- )} - - {sessionFactors?.id && ( - - )} -
-
- ); -} diff --git a/apps/login/src/app/(login)/passkey/login/page.tsx b/apps/login/src/app/(login)/passkey/login/page.tsx deleted file mode 100644 index fe31534850..0000000000 --- a/apps/login/src/app/(login)/passkey/login/page.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { getBrandingSettings, getSession } from "@/lib/zitadel"; -import Alert from "@/ui/Alert"; -import DynamicTheme from "@/ui/DynamicTheme"; -import LoginPasskey from "@/ui/LoginPasskey"; -import UserAvatar from "@/ui/UserAvatar"; -import { - getMostRecentCookieWithLoginname, - getSessionCookieById, -} from "@/utils/cookies"; - -const title = "Authenticate with a passkey"; -const description = - "Your device will ask for your fingerprint, face, or screen lock"; - -export default async function Page({ - searchParams, -}: { - searchParams: Record; -}) { - const { loginName, altPassword, authRequestId, organization, sessionId } = - searchParams; - - const sessionFactors = sessionId - ? await loadSessionById(sessionId, organization) - : await loadSessionByLoginname(loginName, organization); - - async function loadSessionByLoginname( - loginName?: string, - organization?: string, - ) { - const recent = await getMostRecentCookieWithLoginname( - loginName, - organization, - ); - return getSession(recent.id, recent.token).then((response) => { - if (response?.session) { - return response.session; - } - }); - } - - async function loadSessionById(sessionId: string, organization?: string) { - const recent = await getSessionCookieById(sessionId, organization); - return getSession(recent.id, recent.token).then((response) => { - if (response?.session) { - return response.session; - } - }); - } - - const branding = await getBrandingSettings(organization); - - return ( - -
-

{title}

- - {sessionFactors && ( - - )} -

{description}

- - {!(loginName || sessionId) && ( - Provide your active session as loginName param - )} - - {(loginName || sessionId) && ( - - )} -
-
- ); -} diff --git a/apps/login/src/app/(login)/passkey/page.tsx b/apps/login/src/app/(login)/passkey/page.tsx new file mode 100644 index 0000000000..0804f3ce2e --- /dev/null +++ b/apps/login/src/app/(login)/passkey/page.tsx @@ -0,0 +1,75 @@ +import { Alert } from "@/components/alert"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { LoginPasskey } from "@/components/login-passkey"; +import { UserAvatar } from "@/components/user-avatar"; +import { getSessionCookieById } from "@/lib/cookies"; +import { loadMostRecentSession } from "@/lib/session"; +import { + getBrandingSettings, + getLoginSettings, + getSession, +} from "@/lib/zitadel"; +import { getLocale, getTranslations } from "next-intl/server"; + +export default async function Page(props: { + searchParams: Promise>; +}) { + const searchParams = await props.searchParams; + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "passkey" }); + const tError = await getTranslations({ locale, namespace: "error" }); + + const { loginName, altPassword, authRequestId, organization, sessionId } = + searchParams; + + const sessionFactors = sessionId + ? await loadSessionById(sessionId, organization) + : await loadMostRecentSession({ loginName, organization }); + + async function loadSessionById(sessionId: string, organization?: string) { + const recent = await getSessionCookieById({ sessionId, organization }); + return getSession({ + sessionId: recent.id, + sessionToken: recent.token, + }).then((response) => { + if (response?.session) { + return response.session; + } + }); + } + + const branding = await getBrandingSettings(organization); + + const loginSettings = await getLoginSettings(organization); + + return ( + +
+

{t("verify.title")}

+ + {sessionFactors && ( + + )} +

{t("verify.description")}

+ + {!(loginName || sessionId) && {tError("unknownContext")}} + + {(loginName || sessionId) && ( + + )} +
+
+ ); +} diff --git a/apps/login/src/app/(login)/passkey/set/page.tsx b/apps/login/src/app/(login)/passkey/set/page.tsx new file mode 100644 index 0000000000..26a2de2428 --- /dev/null +++ b/apps/login/src/app/(login)/passkey/set/page.tsx @@ -0,0 +1,72 @@ +import { Alert, AlertType } from "@/components/alert"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { RegisterPasskey } from "@/components/register-passkey"; +import { UserAvatar } from "@/components/user-avatar"; +import { loadMostRecentSession } from "@/lib/session"; +import { getBrandingSettings } from "@/lib/zitadel"; +import { getLocale, getTranslations } from "next-intl/server"; + +export default async function Page(props: { + searchParams: Promise>; +}) { + const searchParams = await props.searchParams; + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "passkey" }); + const tError = await getTranslations({ locale, namespace: "error" }); + + const { loginName, prompt, organization, authRequestId, userId } = + searchParams; + + const session = await loadMostRecentSession({ + loginName, + organization, + }); + + const branding = await getBrandingSettings(organization); + + return ( + +
+

{t("set.title")}

+ + {session && ( + + )} +

{t("set.description")}

+ + + + {t("set.info.description")} + + {t("set.info.link")} + + + + + {!session && ( +
+ {tError("unknownContext")} +
+ )} + + {session?.id && ( + + )} +
+
+ ); +} diff --git a/apps/login/src/app/(login)/password/change/page.tsx b/apps/login/src/app/(login)/password/change/page.tsx new file mode 100644 index 0000000000..bbcaea4950 --- /dev/null +++ b/apps/login/src/app/(login)/password/change/page.tsx @@ -0,0 +1,82 @@ +import { Alert } from "@/components/alert"; +import { ChangePasswordForm } from "@/components/change-password-form"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { UserAvatar } from "@/components/user-avatar"; +import { loadMostRecentSession } from "@/lib/session"; +import { + getBrandingSettings, + getLoginSettings, + getPasswordComplexitySettings, +} from "@/lib/zitadel"; +import { getLocale, getTranslations } from "next-intl/server"; + +export default async function Page(props: { + searchParams: Promise>; +}) { + const searchParams = await props.searchParams; + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "password" }); + const tError = await getTranslations({ locale, namespace: "error" }); + + const { loginName, organization, authRequestId } = searchParams; + + // also allow no session to be found (ignoreUnkownUsername) + const sessionFactors = await loadMostRecentSession({ + loginName, + organization, + }); + + const branding = await getBrandingSettings(organization); + + const passwordComplexity = await getPasswordComplexitySettings( + sessionFactors?.factors?.user?.organizationId, + ); + + const loginSettings = await getLoginSettings( + sessionFactors?.factors?.user?.organizationId, + ); + + return ( + +
+

+ {sessionFactors?.factors?.user?.displayName ?? t("change.title")} +

+

{t("change.description")}

+ + {/* show error only if usernames should be shown to be unknown */} + {(!sessionFactors || !loginName) && + !loginSettings?.ignoreUnknownUsernames && ( +
+ {tError("unknownContext")} +
+ )} + + {sessionFactors && ( + + )} + + {passwordComplexity && + loginName && + sessionFactors?.factors?.user?.id ? ( + + ) : ( +
+ {tError("failedLoading")} +
+ )} +
+
+ ); +} diff --git a/apps/login/src/app/(login)/password/page.tsx b/apps/login/src/app/(login)/password/page.tsx index c0fa08bd4a..a2c10c3238 100644 --- a/apps/login/src/app/(login)/password/page.tsx +++ b/apps/login/src/app/(login)/password/page.tsx @@ -1,53 +1,70 @@ +import { Alert } from "@/components/alert"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { PasswordForm } from "@/components/password-form"; +import { UserAvatar } from "@/components/user-avatar"; +import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings, + getDefaultOrg, getLoginSettings, - getSession, } from "@/lib/zitadel"; -import Alert from "@/ui/Alert"; -import DynamicTheme from "@/ui/DynamicTheme"; -import PasswordForm from "@/ui/PasswordForm"; -import UserAvatar from "@/ui/UserAvatar"; -import { getMostRecentCookieWithLoginname } from "@/utils/cookies"; +import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; +import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { getLocale, getTranslations } from "next-intl/server"; -export default async function Page({ - searchParams, -}: { - searchParams: Record; +export default async function Page(props: { + searchParams: Promise>; }) { - const { loginName, organization, promptPasswordless, authRequestId, alt } = - searchParams; - const sessionFactors = await loadSession(loginName, organization); + const searchParams = await props.searchParams; + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "password" }); + const tError = await getTranslations({ locale, namespace: "error" }); - async function loadSession(loginName?: string, organization?: string) { - const recent = await getMostRecentCookieWithLoginname( - loginName, - organization, - ); + let { loginName, organization, authRequestId, alt } = searchParams; - return getSession(recent.id, recent.token).then((response) => { - if (response?.session) { - return response.session; - } - }); + let defaultOrganization; + if (!organization) { + const org: Organization | null = await getDefaultOrg(); + + if (org) { + defaultOrganization = org.id; + } } - const branding = await getBrandingSettings(organization); - const loginSettings = await getLoginSettings(organization); + // also allow no session to be found (ignoreUnkownUsername) + let sessionFactors; + try { + sessionFactors = await loadMostRecentSession({ + loginName, + organization, + }); + } catch (error) { + // ignore error to continue to show the password form + console.warn(error); + } + + const branding = await getBrandingSettings( + organization ?? defaultOrganization, + ); + const loginSettings = await getLoginSettings( + organization ?? defaultOrganization, + ); return (
-

{sessionFactors?.factors?.user?.displayName ?? "Password"}

-

Enter your password.

+

+ {sessionFactors?.factors?.user?.displayName ?? t("verify.title")} +

+

{t("verify.description")}

- {!sessionFactors && ( -
- - Could not get the context of the user. Make sure to enter the - username first or provide a loginName as searchParam. - -
- )} + {/* show error only if usernames should be shown to be unknown */} + {(!sessionFactors || !loginName) && + !loginSettings?.ignoreUnknownUsernames && ( +
+ {tError("unknownContext")} +
+ )} {sessionFactors && ( )} - + {loginName && ( + + )}
); diff --git a/apps/login/src/app/(login)/password/set/page.tsx b/apps/login/src/app/(login)/password/set/page.tsx new file mode 100644 index 0000000000..d60ac7023c --- /dev/null +++ b/apps/login/src/app/(login)/password/set/page.tsx @@ -0,0 +1,106 @@ +import { Alert, AlertType } from "@/components/alert"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { SetPasswordForm } from "@/components/set-password-form"; +import { UserAvatar } from "@/components/user-avatar"; +import { loadMostRecentSession } from "@/lib/session"; +import { + getBrandingSettings, + getLoginSettings, + getPasswordComplexitySettings, + getUserByID, +} from "@/lib/zitadel"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb"; +import { getLocale, getTranslations } from "next-intl/server"; + +export default async function Page(props: { + searchParams: Promise>; +}) { + const searchParams = await props.searchParams; + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "password" }); + const tError = await getTranslations({ locale, namespace: "error" }); + + const { userId, loginName, organization, authRequestId, code, initial } = + searchParams; + + // also allow no session to be found (ignoreUnkownUsername) + let session: Session | undefined; + if (loginName) { + session = await loadMostRecentSession({ + loginName, + organization, + }); + } + + const branding = await getBrandingSettings(organization); + + const passwordComplexity = await getPasswordComplexitySettings( + session?.factors?.user?.organizationId, + ); + + const loginSettings = await getLoginSettings(organization); + + let user: User | undefined; + let displayName: string | undefined; + if (userId) { + const userResponse = await getUserByID(userId); + user = userResponse.user; + + if (user?.type.case === "human") { + displayName = (user.type.value as HumanUser).profile?.displayName; + } + } + + return ( + +
+

{session?.factors?.user?.displayName ?? t("set.title")}

+

{t("set.description")}

+ + {/* show error only if usernames should be shown to be unknown */} + {loginName && !session && !loginSettings?.ignoreUnknownUsernames && ( +
+ {tError("unknownContext")} +
+ )} + + {session ? ( + + ) : user ? ( + + ) : null} + + {!initial && {t("set.codeSent")}} + + {passwordComplexity && + (loginName ?? user?.preferredLoginName) && + (userId ?? session?.factors?.user?.id) ? ( + + ) : ( +
+ {tError("failedLoading")} +
+ )} +
+
+ ); +} diff --git a/apps/login/src/app/(login)/register/page.tsx b/apps/login/src/app/(login)/register/page.tsx index f010f229c2..2cd1c35346 100644 --- a/apps/login/src/app/(login)/register/page.tsx +++ b/apps/login/src/app/(login)/register/page.tsx @@ -1,21 +1,31 @@ +import { DynamicTheme } from "@/components/dynamic-theme"; +import { RegisterForm } from "@/components/register-form"; import { getBrandingSettings, + getDefaultOrg, getLegalAndSupportSettings, + getLoginSettings, getPasswordComplexitySettings, } from "@/lib/zitadel"; -import DynamicTheme from "@/ui/DynamicTheme"; -import RegisterFormWithoutPassword from "@/ui/RegisterFormWithoutPassword"; -import SetPasswordForm from "@/ui/SetPasswordForm"; +import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; +import { getLocale, getTranslations } from "next-intl/server"; -export default async function Page({ - searchParams, -}: { - searchParams: Record; +export default async function Page(props: { + searchParams: Promise>; }) { - const { firstname, lastname, email, organization, authRequestId } = + const searchParams = await props.searchParams; + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "register" }); + + let { firstname, lastname, email, organization, authRequestId } = searchParams; - const setPassword = !!(firstname && lastname && email); + if (!organization) { + const org: Organization | null = await getDefaultOrg(); + if (org) { + organization = org.id; + } + } const legal = await getLegalAndSupportSettings(organization); const passwordComplexitySettings = @@ -23,39 +33,35 @@ export default async function Page({ const branding = await getBrandingSettings(organization); - return setPassword ? ( + const loginSettings = await getLoginSettings(organization); + + if (!loginSettings?.allowRegister) { + return ( + +
+

{t("disabled.title")}

+

{t("disabled.description")}

+
+
+ ); + } + + return (
-

Set Password

-

Set the password for your account

+

{t("title")}

+

{t("description")}

{legal && passwordComplexitySettings && ( - - )} -
-
- ) : ( - -
-

Register

-

Create your ZITADEL account.

- - {legal && passwordComplexitySettings && ( - + loginSettings={loginSettings} + > )}
diff --git a/apps/login/src/app/(login)/register/password/page.tsx b/apps/login/src/app/(login)/register/password/page.tsx new file mode 100644 index 0000000000..48e454c312 --- /dev/null +++ b/apps/login/src/app/(login)/register/password/page.tsx @@ -0,0 +1,73 @@ +import { DynamicTheme } from "@/components/dynamic-theme"; +import { SetRegisterPasswordForm } from "@/components/set-register-password-form"; +import { + getBrandingSettings, + getDefaultOrg, + getLegalAndSupportSettings, + getLoginSettings, + getPasswordComplexitySettings, +} from "@/lib/zitadel"; +import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; +import { getLocale, getTranslations } from "next-intl/server"; + +export default async function Page(props: { + searchParams: Promise>; +}) { + const searchParams = await props.searchParams; + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "register" }); + + let { firstname, lastname, email, organization, authRequestId } = + searchParams; + + if (!organization) { + const org: Organization | null = await getDefaultOrg(); + if (org) { + organization = org.id; + } + } + + const missingData = !firstname || !lastname || !email; + + const legal = await getLegalAndSupportSettings(organization); + const passwordComplexitySettings = + await getPasswordComplexitySettings(organization); + + const branding = await getBrandingSettings(organization); + + const loginSettings = await getLoginSettings(organization); + + return missingData ? ( + +
+

{t("missingdata.title")}

+

{t("missingdata.description")}

+
+
+ ) : loginSettings?.allowRegister && loginSettings.allowUsernamePassword ? ( + +
+

{t("password.title")}

+

{t("description")}

+ + {legal && passwordComplexitySettings && ( + + )} +
+
+ ) : ( + +
+

{t("disabled.title")}

+

{t("disabled.description")}

+
+
+ ); +} diff --git a/apps/login/src/app/(login)/signedin/page.tsx b/apps/login/src/app/(login)/signedin/page.tsx index bdc6d0d698..4fc9ac8546 100644 --- a/apps/login/src/app/(login)/signedin/page.tsx +++ b/apps/login/src/app/(login)/signedin/page.tsx @@ -1,48 +1,100 @@ -import { createCallback, getBrandingSettings, getSession } from "@/lib/zitadel"; -import DynamicTheme from "@/ui/DynamicTheme"; -import UserAvatar from "@/ui/UserAvatar"; -import { getMostRecentCookieWithLoginname } from "@/utils/cookies"; +import { Button, ButtonVariants } from "@/components/button"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { SelfServiceMenu } from "@/components/self-service-menu"; +import { UserAvatar } from "@/components/user-avatar"; +import { getMostRecentCookieWithLoginname } from "@/lib/cookies"; +import { + createCallback, + getBrandingSettings, + getLoginSettings, + getSession, +} from "@/lib/zitadel"; +import { create } from "@zitadel/client"; +import { + CreateCallbackRequestSchema, + SessionSchema, +} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; +import { getLocale, getTranslations } from "next-intl/server"; +import Link from "next/link"; import { redirect } from "next/navigation"; async function loadSession(loginName: string, authRequestId?: string) { - const recent = await getMostRecentCookieWithLoginname(`${loginName}`); + const recent = await getMostRecentCookieWithLoginname({ loginName }); if (authRequestId) { - return createCallback({ - authRequestId, - callbackKind: { - case: "session", - value: { sessionId: recent.id, sessionToken: recent.token }, - }, - }).then(({ callbackUrl }) => { + return createCallback( + create(CreateCallbackRequestSchema, { + authRequestId, + callbackKind: { + case: "session", + value: create(SessionSchema, { + sessionId: recent.id, + sessionToken: recent.token, + }), + }, + }), + ).then(({ callbackUrl }) => { return redirect(callbackUrl); }); } - return getSession(recent.id, recent.token).then((response) => { - if (response?.session) { - return response.session; - } - }); + return getSession({ sessionId: recent.id, sessionToken: recent.token }).then( + (response) => { + if (response?.session) { + return response.session; + } + }, + ); } -export default async function Page({ searchParams }: { searchParams: any }) { +export default async function Page(props: { searchParams: Promise }) { + const searchParams = await props.searchParams; + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "signedin" }); + const { loginName, authRequestId, organization } = searchParams; const sessionFactors = await loadSession(loginName, authRequestId); const branding = await getBrandingSettings(organization); + let loginSettings; + if (!authRequestId) { + loginSettings = await getLoginSettings(organization); + } + return (
-

{`Welcome ${sessionFactors?.factors?.user?.displayName}`}

-

You are signed in.

+

+ {t("title", { user: sessionFactors?.factors?.user?.displayName })} +

+

{t("description")}

+ /> + + {sessionFactors?.id && ( + + )} + + {loginSettings?.defaultRedirectUri && ( +
+ + + + + +
+ )}
); diff --git a/apps/login/src/app/(login)/u2f/page.tsx b/apps/login/src/app/(login)/u2f/page.tsx index 2c8c8b19d6..e4dd2bd8d2 100644 --- a/apps/login/src/app/(login)/u2f/page.tsx +++ b/apps/login/src/app/(login)/u2f/page.tsx @@ -1,50 +1,34 @@ -import { - getBrandingSettings, - getLoginSettings, - getSession, -} from "@/lib/zitadel"; -import Alert from "@/ui/Alert"; -import DynamicTheme from "@/ui/DynamicTheme"; -import LoginPasskey from "@/ui/LoginPasskey"; -import UserAvatar from "@/ui/UserAvatar"; -import { - getMostRecentCookieWithLoginname, - getSessionCookieById, -} from "@/utils/cookies"; +import { Alert } from "@/components/alert"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { LoginPasskey } from "@/components/login-passkey"; +import { UserAvatar } from "@/components/user-avatar"; +import { getSessionCookieById } from "@/lib/cookies"; +import { loadMostRecentSession } from "@/lib/session"; +import { getBrandingSettings, getSession } from "@/lib/zitadel"; +import { getLocale, getTranslations } from "next-intl/server"; -export default async function Page({ - searchParams, - params, -}: { - searchParams: Record; - params: Record; +export default async function Page(props: { + searchParams: Promise>; }) { + const searchParams = await props.searchParams; + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "u2f" }); + const tError = await getTranslations({ locale, namespace: "error" }); + const { loginName, authRequestId, sessionId, organization } = searchParams; const branding = await getBrandingSettings(organization); const sessionFactors = sessionId ? await loadSessionById(sessionId, organization) - : await loadSessionByLoginname(loginName, organization); - - async function loadSessionByLoginname( - loginName?: string, - organization?: string, - ) { - const recent = await getMostRecentCookieWithLoginname( - loginName, - organization, - ); - return getSession(recent.id, recent.token).then((response) => { - if (response?.session) { - return response.session; - } - }); - } + : await loadMostRecentSession({ loginName, organization }); async function loadSessionById(sessionId: string, organization?: string) { - const recent = await getSessionCookieById(sessionId, organization); - return getSession(recent.id, recent.token).then((response) => { + const recent = await getSessionCookieById({ sessionId, organization }); + return getSession({ + sessionId: recent.id, + sessionToken: recent.token, + }).then((response) => { if (response?.session) { return response.session; } @@ -54,7 +38,7 @@ export default async function Page({ return (
-

Verify 2-Factor

+

{t("verify.title")}

{sessionFactors && ( )} -

- Verify your account with your device. -

+

{t("verify.description")}

- {!(loginName || sessionId) && ( - Provide your active session as loginName param - )} + {!(loginName || sessionId) && {tError("unknownContext")}} {(loginName || sessionId) && ( ; +export default async function Page(props: { + searchParams: Promise>; }) { - const { loginName, organization, authRequestId } = searchParams; + const searchParams = await props.searchParams; + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "u2f" }); + const tError = await getTranslations({ locale, namespace: "error" }); - const sessionFactors = await loadSession(loginName); + const { loginName, organization, authRequestId, checkAfter } = searchParams; - async function loadSession(loginName?: string) { - const recent = await getMostRecentCookieWithLoginname( - loginName, - organization, - ); - return getSession(recent.id, recent.token).then((response) => { - if (response?.session) { - return response.session; - } - }); - } - const title = "Use your passkey to confirm it's really you"; - const description = - "Your device will ask for your fingerprint, face, or screen lock"; + const sessionFactors = await loadMostRecentSession({ + loginName, + organization, + }); const branding = await getBrandingSettings(organization); return (
-

{title}

+

{t("set.title")}

{sessionFactors && ( )} -

{description}

+

{t("set.description")}

{!sessionFactors && (
- - Could not get the context of the user. Make sure to enter the - username first or provide a loginName as searchParam. - + {tError("unknownContext")}
)} {sessionFactors?.id && ( - )}
diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index 5bc0cb0eb6..128623963b 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -1,45 +1,154 @@ -import { getBrandingSettings } from "@/lib/zitadel"; -import DynamicTheme from "@/ui/DynamicTheme"; -import VerifyEmailForm from "@/ui/VerifyEmailForm"; -import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; +import { Alert } from "@/components/alert"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { UserAvatar } from "@/components/user-avatar"; +import { VerifyForm } from "@/components/verify-form"; +import { VerifyRedirectButton } from "@/components/verify-redirect-button"; +import { sendEmailCode } from "@/lib/server/verify"; +import { loadMostRecentSession } from "@/lib/session"; +import { + getBrandingSettings, + getUserByID, + listAuthenticationMethodTypes, +} from "@/lib/zitadel"; +import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb"; +import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { getLocale, getTranslations } from "next-intl/server"; -export default async function Page({ searchParams }: { searchParams: any }) { - const { - userId, - sessionId, - code, - submit, - organization, - authRequestId, - loginname, - passwordset, - } = searchParams; +export default async function Page(props: { searchParams: Promise }) { + const searchParams = await props.searchParams; + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "verify" }); + const tError = await getTranslations({ locale, namespace: "error" }); + + const { userId, loginName, code, organization, authRequestId, invite } = + searchParams; const branding = await getBrandingSettings(organization); + let sessionFactors; + let user: User | undefined; + let human: HumanUser | undefined; + let id: string | undefined; + + const doSend = invite !== "true"; + + if ("loginName" in searchParams) { + sessionFactors = await loadMostRecentSession({ + loginName, + organization, + }); + + if (doSend && sessionFactors?.factors?.user?.id) { + await sendEmailCode({ + userId: sessionFactors?.factors?.user?.id, + authRequestId, + }).catch((error) => { + console.error("Could not resend verification email", error); + throw Error("Failed to send verification email"); + }); + } + } else if ("userId" in searchParams && userId) { + if (doSend) { + await sendEmailCode({ + userId, + authRequestId, + }).catch((error) => { + console.error("Could not resend verification email", error); + throw Error("Failed to send verification email"); + }); + } + + const userResponse = await getUserByID(userId); + if (userResponse) { + user = userResponse.user; + if (user?.type.case === "human") { + human = user.type.value as HumanUser; + } + } + } + + id = userId ?? sessionFactors?.factors?.user?.id; + + let authMethods: AuthenticationMethodType[] | null = null; + if (human?.email?.isVerified) { + const authMethodsResponse = await listAuthenticationMethodTypes(userId); + if (authMethodsResponse.authMethodTypes) { + authMethods = authMethodsResponse.authMethodTypes; + } + } + + const params = new URLSearchParams({ + userId: userId, + initial: "true", // defines that a code is not required and is therefore not shown in the UI + }); + + if (loginName) { + params.set("loginName", loginName); + } + + if (organization) { + params.set("organization", organization); + } + + if (authRequestId) { + params.set("authRequestId", authRequestId); + } + return (
-

Verify user

-

- Enter the Code provided in the verification email. -

+

{t("verify.title")}

+

{t("verify.description")}

- {userId ? ( - - ) : ( -
- - No userId provided! -
+ {!id && ( + <> +

{t("verify.title")}

+

{t("verify.description")}

+ +
+ {tError("unknownContext")} +
+ )} + + {sessionFactors ? ( + + ) : ( + user && ( + + ) + )} + + {id && + (human?.email?.isVerified ? ( + // show page for already verified users + + ) : ( + // check if auth methods are set + + ))}
); diff --git a/apps/login/src/app/api/idp/start/route.ts b/apps/login/src/app/api/idp/start/route.ts deleted file mode 100644 index 7d7d967977..0000000000 --- a/apps/login/src/app/api/idp/start/route.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { startIdentityProviderFlow } from "@/lib/zitadel"; -import { NextRequest, NextResponse } from "next/server"; - -export async function POST(request: NextRequest) { - const body = await request.json(); - if (body) { - let { idpId, successUrl, failureUrl } = body; - - return startIdentityProviderFlow({ - idpId, - urls: { - successUrl, - failureUrl, - }, - }) - .then((resp) => { - return NextResponse.json(resp); - }) - .catch((error) => { - return NextResponse.json(error, { status: 500 }); - }); - } else { - return NextResponse.json({}, { status: 400 }); - } -} diff --git a/apps/login/src/app/api/loginname/route.ts b/apps/login/src/app/api/loginname/route.ts deleted file mode 100644 index 2b9197f6fa..0000000000 --- a/apps/login/src/app/api/loginname/route.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { ProviderSlug } from "@/lib/demos"; -import { - getActiveIdentityProviders, - getLoginSettings, - listAuthenticationMethodTypes, - listUsers, - PROVIDER_NAME_MAPPING, - startIdentityProviderFlow, -} from "@/lib/zitadel"; -import { createSessionForUserIdAndUpdateCookie } from "@/utils/session"; -import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; -import { NextRequest, NextResponse } from "next/server"; - -export async function POST(request: NextRequest) { - const body = await request.json(); - if (body) { - const { loginName, authRequestId, organization } = body; - return listUsers(loginName, organization).then(async (users) => { - if (users.details?.totalResult == BigInt(1) && users.result[0].userId) { - const userId = users.result[0].userId; - return createSessionForUserIdAndUpdateCookie( - userId, - undefined, - undefined, - authRequestId, - ) - .then((session) => { - if (session.factors?.user?.id) { - return listAuthenticationMethodTypes(session.factors?.user?.id) - .then((methods) => { - return NextResponse.json({ - authMethodTypes: methods.authMethodTypes, - sessionId: session.id, - factors: session.factors, - }); - }) - .catch((error) => { - return NextResponse.json(error, { status: 500 }); - }); - } else { - throw { details: "No user id found in session" }; - } - }) - .catch((error) => { - console.error(error); - return NextResponse.json(error, { status: 500 }); - }); - } else { - const loginSettings = await getLoginSettings(organization); - // TODO: check if allowDomainDiscovery has to be allowed too, to redirect to the register page - // user not found, check if register is enabled on organization - - if ( - loginSettings?.allowRegister && - !loginSettings?.allowUsernamePassword - ) { - // TODO redirect to loginname page with idp hint - const identityProviders = await getActiveIdentityProviders( - organization, - ).then((resp) => { - return resp.identityProviders; - }); - - if (identityProviders.length === 1) { - const host = request.nextUrl.origin; - - const identityProviderType = identityProviders[0].type; - let provider: string; - - switch (identityProviderType) { - case IdentityProviderType.GITHUB: - provider = "github"; - break; - case IdentityProviderType.GOOGLE: - provider = "google"; - break; - case IdentityProviderType.AZURE_AD: - provider = "azure"; - break; - case IdentityProviderType.SAML: - provider = "saml"; - break; - case IdentityProviderType.OIDC: - provider = "oidc"; - break; - default: - provider = "oidc"; - break; - } - - const params = new URLSearchParams(); - - if (authRequestId) { - params.set("authRequestId", authRequestId); - } - - if (organization) { - params.set("organization", organization); - } - - return startIdentityProviderFlow({ - idpId: identityProviders[0].id, - urls: { - successUrl: - `${host}/idp/${provider}/success?` + - new URLSearchParams(params), - failureUrl: - `${host}/idp/${provider}/failure?` + - new URLSearchParams(params), - }, - }).then((resp: any) => { - if (resp.authUrl) { - return NextResponse.json({ nextStep: resp.authUrl }); - } - }); - } else { - return NextResponse.json( - { message: "Could not find user" }, - { status: 404 }, - ); - } - } else if ( - loginSettings?.allowRegister && - loginSettings?.allowUsernamePassword - ) { - const params: any = { organization }; - if (authRequestId) { - params.authRequestId = authRequestId; - } - if (loginName) { - params.email = loginName; - } - - const registerUrl = new URL( - "/register?" + new URLSearchParams(params), - request.url, - ); - - return NextResponse.json({ - nextStep: registerUrl, - status: 200, - }); - } - - return NextResponse.json( - { message: "Could not find user" }, - { status: 404 }, - ); - } - }); - } else { - return NextResponse.error(); - } -} diff --git a/apps/login/src/app/api/otp/set/route.ts b/apps/login/src/app/api/otp/set/route.ts deleted file mode 100644 index 174dfff119..0000000000 --- a/apps/login/src/app/api/otp/set/route.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { - SessionCookie, - getMostRecentSessionCookie, - getSessionCookieById, - getSessionCookieByLoginName, -} from "@/utils/cookies"; -import { setSessionAndUpdateCookie } from "@/utils/session"; -import { NextRequest, NextResponse, userAgent } from "next/server"; -import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; -import { PlainMessage } from "@zitadel/client"; - -export async function POST(request: NextRequest) { - const body = await request.json(); - - if (body) { - const { loginName, sessionId, organization, authRequestId, code, method } = - body; - - const recentPromise: Promise = sessionId - ? getSessionCookieById(sessionId).catch((error) => { - return Promise.reject(error); - }) - : loginName - ? getSessionCookieByLoginName(loginName, organization).catch( - (error) => { - return Promise.reject(error); - }, - ) - : getMostRecentSessionCookie().catch((error) => { - return Promise.reject(error); - }); - - return recentPromise - .then((recent) => { - const checks: PlainMessage = {}; - - if (method === "time-based") { - checks.totp = { - code, - }; - } else if (method === "sms") { - checks.otpSms = { - code, - }; - } else if (method === "email") { - checks.otpEmail = { - code, - }; - } - - return setSessionAndUpdateCookie( - recent, - checks, - undefined, - authRequestId, - ).then((session) => { - return NextResponse.json({ - sessionId: session.id, - factors: session.factors, - challenges: session.challenges, - }); - }); - }) - .catch((error) => { - return NextResponse.json({ details: error }, { status: 500 }); - }); - } else { - return NextResponse.json( - { details: "Request body is missing" }, - { status: 400 }, - ); - } -} diff --git a/apps/login/src/app/api/passkeys/route.ts b/apps/login/src/app/api/passkeys/route.ts deleted file mode 100644 index 4489bee308..0000000000 --- a/apps/login/src/app/api/passkeys/route.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { - createPasskeyRegistrationLink, - getSession, - registerPasskey, -} from "@/lib/zitadel"; -import { getSessionCookieById } from "@/utils/cookies"; -import { NextRequest, NextResponse } from "next/server"; - -export async function POST(request: NextRequest) { - const body = await request.json(); - if (body) { - const { sessionId } = body; - - const sessionCookie = await getSessionCookieById(sessionId); - - const session = await getSession(sessionCookie.id, sessionCookie.token); - - const domain: string = request.nextUrl.hostname; - - const userId = session?.session?.factors?.user?.id; - - if (userId) { - // TODO: add org context - return createPasskeyRegistrationLink(userId) - .then((resp) => { - const code = resp.code; - if (!code) { - throw new Error("Missing code in response"); - } - return registerPasskey(userId, code, domain).then((resp) => { - return NextResponse.json(resp); - }); - }) - .catch((error) => { - console.error("error on creating passkey registration link"); - return NextResponse.json(error, { status: 500 }); - }); - } else { - return NextResponse.json( - { details: "could not get session" }, - { status: 500 }, - ); - } - } else { - return NextResponse.json({}, { status: 400 }); - } -} diff --git a/apps/login/src/app/api/passkeys/verify/route.ts b/apps/login/src/app/api/passkeys/verify/route.ts deleted file mode 100644 index a09b848c08..0000000000 --- a/apps/login/src/app/api/passkeys/verify/route.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { getSession, verifyPasskeyRegistration } from "@/lib/zitadel"; -import { getSessionCookieById } from "@/utils/cookies"; -import { NextRequest, NextResponse, userAgent } from "next/server"; - -export async function POST(request: NextRequest) { - const body = await request.json(); - if (body) { - let { passkeyId, passkeyName, publicKeyCredential, sessionId } = body; - - if (!!!passkeyName) { - const { browser, device, os } = userAgent(request); - passkeyName = `${device.vendor ?? ""} ${device.model ?? ""}${ - device.vendor || device.model ? ", " : "" - }${os.name}${os.name ? ", " : ""}${browser.name}`; - } - const sessionCookie = await getSessionCookieById(sessionId); - - const session = await getSession(sessionCookie.id, sessionCookie.token); - - const userId = session?.session?.factors?.user?.id; - - if (userId) { - return verifyPasskeyRegistration( - passkeyId, - passkeyName, - publicKeyCredential, - userId, - ) - .then((resp) => { - return NextResponse.json(resp); - }) - .catch((error) => { - return NextResponse.json(error, { status: 500 }); - }); - } else { - return NextResponse.json( - { details: "could not get session" }, - { status: 500 }, - ); - } - } else { - return NextResponse.json({}, { status: 400 }); - } -} diff --git a/apps/login/src/app/api/registeruser/route.ts b/apps/login/src/app/api/registeruser/route.ts deleted file mode 100644 index dcb3626a63..0000000000 --- a/apps/login/src/app/api/registeruser/route.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { addHumanUser } from "@/lib/zitadel"; -import { - createSessionAndUpdateCookie, - createSessionForUserIdAndUpdateCookie, -} from "@/utils/session"; -import { NextRequest, NextResponse } from "next/server"; - -export async function POST(request: NextRequest) { - const body = await request.json(); - if (body) { - const { - email, - password, - firstName, - lastName, - organization, - authRequestId, - } = body; - - return addHumanUser({ - email: email, - firstName, - lastName, - password: password ? password : undefined, - organization, - }) - .then((user) => { - return createSessionForUserIdAndUpdateCookie( - user.userId, - password, - undefined, - authRequestId, - ).then((session) => { - return NextResponse.json({ - userId: user.userId, - sessionId: session.id, - factors: session.factors, - }); - }); - }) - .catch((error) => { - return NextResponse.json(error, { status: 500 }); - }); - } else { - return NextResponse.error(); - } -} diff --git a/apps/login/src/app/api/resendverifyemail/route.ts b/apps/login/src/app/api/resendverifyemail/route.ts deleted file mode 100644 index e9a9521401..0000000000 --- a/apps/login/src/app/api/resendverifyemail/route.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { resendEmailCode } from "@/lib/zitadel"; -import { NextRequest, NextResponse } from "next/server"; - -export async function POST(request: NextRequest) { - const body = await request.json(); - if (body) { - const { userId } = body; - - // replace with resend Mail method once its implemented - return resendEmailCode(userId) - .then((resp) => { - return NextResponse.json(resp); - }) - .catch((error) => { - return NextResponse.json(error, { status: 500 }); - }); - } else { - return NextResponse.error(); - } -} diff --git a/apps/login/src/app/api/resetpassword/route.ts b/apps/login/src/app/api/resetpassword/route.ts deleted file mode 100644 index 570eb64992..0000000000 --- a/apps/login/src/app/api/resetpassword/route.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { listUsers, passwordReset } from "@/lib/zitadel"; -import { NextRequest, NextResponse } from "next/server"; - -export async function POST(request: NextRequest) { - const body = await request.json(); - if (body) { - const { loginName, organization } = body; - return listUsers(loginName, organization).then((users) => { - if ( - users.details && - Number(users.details.totalResult) == 1 && - users.result[0].userId - ) { - const userId = users.result[0].userId; - - return passwordReset(userId) - .then((resp) => { - return NextResponse.json(resp); - }) - .catch((error) => { - return NextResponse.json(error, { status: 500 }); - }); - } else { - return NextResponse.json({ error: "User not found" }, { status: 404 }); - } - }); - } -} diff --git a/apps/login/src/app/api/session/route.ts b/apps/login/src/app/api/session/route.ts deleted file mode 100644 index c0bc0d06e2..0000000000 --- a/apps/login/src/app/api/session/route.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { - deleteSession, - getSession, - getUserByID, - listAuthenticationMethodTypes, -} from "@/lib/zitadel"; -import { - SessionCookie, - getMostRecentSessionCookie, - getSessionCookieById, - getSessionCookieByLoginName, - removeSessionFromCookie, -} from "@/utils/cookies"; -import { - createSessionAndUpdateCookie, - createSessionForIdpAndUpdateCookie, - setSessionAndUpdateCookie, -} from "@/utils/session"; -import { NextRequest, NextResponse } from "next/server"; - -export async function POST(request: NextRequest) { - const body = await request.json(); - if (body) { - const { - userId, - idpIntent, - loginName, - password, - organization, - authRequestId, - } = body; - - if (userId && idpIntent) { - return createSessionForIdpAndUpdateCookie( - userId, - idpIntent, - organization, - authRequestId, - ).then((session) => { - return NextResponse.json(session); - }); - } else { - return createSessionAndUpdateCookie( - loginName, - password, - undefined, - organization, - authRequestId, - ).then((session) => { - return NextResponse.json(session); - }); - } - } else { - return NextResponse.json( - { details: "Session could not be created" }, - { status: 500 }, - ); - } -} - -/** - * - * @param request password for the most recent session - * @returns the updated most recent Session with the added password - */ -export async function PUT(request: NextRequest) { - const body = await request.json(); - - if (body) { - const { - loginName, - sessionId, - organization, - checks, - authRequestId, - challenges, - } = body; - - const recentPromise: Promise = sessionId - ? getSessionCookieById(sessionId).catch((error) => { - return Promise.reject(error); - }) - : loginName - ? getSessionCookieByLoginName(loginName, organization).catch( - (error) => { - return Promise.reject(error); - }, - ) - : getMostRecentSessionCookie().catch((error) => { - return Promise.reject(error); - }); - - const domain: string = request.nextUrl.hostname; - - if (challenges && challenges.webAuthN && !challenges.webAuthN.domain) { - challenges.webAuthN.domain = domain; - } - - return recentPromise - .then(async (recent) => { - if ( - challenges && - (challenges.otpEmail === "" || challenges.otpSms === "") - ) { - const sessionResponse = await getSession(recent.id, recent.token); - - if (sessionResponse && sessionResponse.session?.factors?.user?.id) { - const userResponse = await getUserByID( - sessionResponse.session.factors.user.id, - ); - const humanUser = - userResponse.user?.type.case === "human" - ? userResponse.user?.type.value - : undefined; - - if (challenges.otpEmail === "" && humanUser?.email?.email) { - challenges.otpEmail = humanUser?.email?.email; - } - - if (challenges.otpSms === "" && humanUser?.phone?.phone) { - challenges.otpSms = humanUser?.phone?.phone; - } - } - } - - return setSessionAndUpdateCookie( - recent, - checks, - challenges, - authRequestId, - ).then(async (session) => { - // if password, check if user has MFA methods - let authMethods; - if (checks && checks.password && session.factors?.user?.id) { - const response = await listAuthenticationMethodTypes( - session.factors?.user?.id, - ); - if (response.authMethodTypes && response.authMethodTypes.length) { - authMethods = response.authMethodTypes; - } - } - - return NextResponse.json({ - sessionId: session.id, - factors: session.factors, - challenges: session.challenges, - authMethods, - }); - }); - }) - .catch((error) => { - console.error(error); - return NextResponse.json({ details: error }, { status: 500 }); - }); - } else { - return NextResponse.json( - { details: "Request body is missing" }, - { status: 400 }, - ); - } -} - -/** - * - * @param request id of the session to be deleted - */ -export async function DELETE(request: NextRequest) { - const { searchParams } = new URL(request.url); - const id = searchParams.get("id"); - if (id) { - const session = await getSessionCookieById(id); - - return deleteSession(session.id, session.token) - .then(() => { - return removeSessionFromCookie(session) - .then(() => { - return NextResponse.json({}); - }) - .catch((error) => { - return NextResponse.json( - { details: "could not set cookie" }, - { status: 500 }, - ); - }); - }) - .catch((error) => { - return NextResponse.json( - { details: "could not delete session" }, - { status: 500 }, - ); - }); - } else { - return NextResponse.error(); - } -} diff --git a/apps/login/src/app/api/u2f/route.ts b/apps/login/src/app/api/u2f/route.ts deleted file mode 100644 index 6a1c1a82b4..0000000000 --- a/apps/login/src/app/api/u2f/route.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - createPasskeyRegistrationLink, - getSession, - registerPasskey, - registerU2F, -} from "@/lib/zitadel"; -import { getSessionCookieById } from "@/utils/cookies"; -import { NextRequest, NextResponse } from "next/server"; - -export async function POST(request: NextRequest) { - const body = await request.json(); - if (body) { - const { sessionId } = body; - - const sessionCookie = await getSessionCookieById(sessionId); - - const session = await getSession(sessionCookie.id, sessionCookie.token); - - const domain: string = request.nextUrl.hostname; - - const userId = session?.session?.factors?.user?.id; - - if (userId) { - return registerU2F(userId, domain) - .then((resp) => { - return NextResponse.json(resp); - }) - .catch((error) => { - console.error("error on creating passkey registration link"); - return NextResponse.json(error, { status: 500 }); - }); - } else { - return NextResponse.json( - { details: "could not get session" }, - { status: 500 }, - ); - } - } else { - return NextResponse.json({}, { status: 400 }); - } -} diff --git a/apps/login/src/app/api/u2f/verify/route.ts b/apps/login/src/app/api/u2f/verify/route.ts deleted file mode 100644 index eae5d13fe8..0000000000 --- a/apps/login/src/app/api/u2f/verify/route.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { getSession, verifyU2FRegistration } from "@/lib/zitadel"; -import { getSessionCookieById } from "@/utils/cookies"; -import { NextRequest, NextResponse, userAgent } from "next/server"; -import { VerifyU2FRegistrationRequest } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; -import { PlainMessage } from "@zitadel/client"; - -export async function POST(request: NextRequest) { - const body = await request.json(); - if (body) { - let { u2fId, passkeyName, publicKeyCredential, sessionId } = body; - - if (!!!passkeyName) { - const { browser, device, os } = userAgent(request); - passkeyName = `${device.vendor ?? ""} ${device.model ?? ""}${ - device.vendor || device.model ? ", " : "" - }${os.name}${os.name ? ", " : ""}${browser.name}`; - } - const sessionCookie = await getSessionCookieById(sessionId); - - const session = await getSession(sessionCookie.id, sessionCookie.token); - - const userId = session?.session?.factors?.user?.id; - - if (userId) { - const req: PlainMessage = { - publicKeyCredential, - u2fId, - userId, - tokenName: passkeyName, - }; - return verifyU2FRegistration(req) - .then((resp) => { - return NextResponse.json(resp); - }) - .catch((error) => { - return NextResponse.json(error, { status: 500 }); - }); - } else { - return NextResponse.json( - { details: "could not get session" }, - { status: 500 }, - ); - } - } else { - return NextResponse.json({}, { status: 400 }); - } -} diff --git a/apps/login/src/app/api/verifyemail/route.ts b/apps/login/src/app/api/verifyemail/route.ts deleted file mode 100644 index f8a21c467b..0000000000 --- a/apps/login/src/app/api/verifyemail/route.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { verifyEmail } from "@/lib/zitadel"; -import { NextRequest, NextResponse } from "next/server"; - -export async function POST(request: NextRequest) { - const body = await request.json(); - if (body) { - const { userId, code } = body; - - return verifyEmail(userId, code) - .then((resp) => { - return NextResponse.json(resp); - }) - .catch((error) => { - return NextResponse.json(error, { status: 500 }); - }); - } else { - return NextResponse.error(); - } -} diff --git a/apps/login/src/app/global-error.tsx b/apps/login/src/app/global-error.tsx new file mode 100644 index 0000000000..0fea5d0117 --- /dev/null +++ b/apps/login/src/app/global-error.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { Boundary } from "@/components/boundary"; +import { Button } from "@/components/button"; +import { ThemeWrapper } from "@/components/theme-wrapper"; +import { useTranslations } from "next-intl"; + +export default function GlobalError({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + const t = useTranslations("error"); + + return ( + // global-error must include html and body tags + + + + +
+
+ Error: {error?.message} +
+
+ +
+
+
+
+ + + ); +} diff --git a/apps/login/src/app/layout.tsx b/apps/login/src/app/layout.tsx deleted file mode 100644 index 5e299faf04..0000000000 --- a/apps/login/src/app/layout.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import "@/styles/globals.scss"; -import { AddressBar } from "@/ui/AddressBar"; -import { GlobalNav } from "@/ui/GlobalNav"; -import { Lato } from "next/font/google"; -import { LayoutProviders } from "@/ui/LayoutProviders"; -import { Analytics } from "@vercel/analytics/react"; -import ThemeWrapper from "@/ui/ThemeWrapper"; -import { getBrandingSettings } from "@/lib/zitadel"; -import ThemeProvider from "@/ui/ThemeProvider"; -import Theme from "@/ui/Theme"; - -const lato = Lato({ - weight: ["400", "700", "900"], - subsets: ["latin"], -}); - -export const revalidate = 60; // revalidate every minute - -export default async function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { - // later only shown with dev mode enabled - const showNav = process.env.DEBUG === "true"; - - let domain = process.env.ZITADEL_API_URL; - domain = domain ? domain.replace("https://", "") : "acme.com"; - - return ( - - - - -
- {showNav ? ( - - ) : ( -
- -
- )} - -
-
- {showNav && ( -
-
- -
-
- )} - - {children} -
-
-
- - -
- - - ); -} diff --git a/apps/login/src/app/login/route.ts b/apps/login/src/app/login/route.ts index 0393813534..e96326518f 100644 --- a/apps/login/src/app/login/route.ts +++ b/apps/login/src/app/login/route.ts @@ -1,19 +1,32 @@ +import { getAllSessions } from "@/lib/cookies"; +import { idpTypeToSlug } from "@/lib/idp"; +import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname"; import { createCallback, getActiveIdentityProviders, getAuthRequest, - getOrgByDomain, + getLoginSettings, + getOrgsByDomain, + listAuthenticationMethodTypes, listSessions, startIdentityProviderFlow, } from "@/lib/zitadel"; -import { SessionCookie, getAllSessions } from "@/utils/cookies"; -import { NextRequest, NextResponse } from "next/server"; -import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { create, timestampDate } from "@zitadel/client"; import { AuthRequest, Prompt, } from "@zitadel/proto/zitadel/oidc/v2/authorization_pb"; -import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { + CreateCallbackRequestSchema, + SessionSchema, +} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { NextRequest, NextResponse } from "next/server"; + +export const dynamic = "force-dynamic"; +export const revalidate = false; +export const fetchCache = "default-no-store"; async function loadSessions(ids: string[]): Promise { const response = await listSessions( @@ -27,23 +40,141 @@ const ORG_SCOPE_REGEX = /urn:zitadel:iam:org:id:([0-9]+)/; const ORG_DOMAIN_SCOPE_REGEX = /urn:zitadel:iam:org:domain:primary:(.+)/; // TODO: check regex for all domain character options const IDP_SCOPE_REGEX = /urn:zitadel:iam:org:idp:id:(.+)/; -function findSession( - sessions: Session[], - authRequest: AuthRequest, -): Session | undefined { - if (authRequest.hintUserId) { - console.log(`find session for hintUserId: ${authRequest.hintUserId}`); - return sessions.find((s) => s.factors?.user?.id === authRequest.hintUserId); +/** + * mfa is required, session is not valid anymore (e.g. session expired, user logged out, etc.) + * to check for mfa for automatically selected session -> const response = await listAuthenticationMethodTypes(userId); + **/ +async function isSessionValid(session: Session): Promise { + // session can't be checked without user + if (!session.factors?.user) { + console.warn("Session has no user"); + return false; } - if (authRequest.loginHint) { - console.log(`find session for loginHint: ${authRequest.loginHint}`); - return sessions.find( - (s) => s.factors?.user?.loginName === authRequest.loginHint, + + let mfaValid = true; + + const authMethodTypes = await listAuthenticationMethodTypes( + session.factors.user.id, + ); + + const authMethods = authMethodTypes.authMethodTypes; + if (authMethods && authMethods.includes(AuthenticationMethodType.TOTP)) { + mfaValid = !!session.factors.totp?.verifiedAt; + if (!mfaValid) { + console.warn( + "Session has no valid totpEmail factor", + session.factors.totp?.verifiedAt, + ); + } + } else if ( + authMethods && + authMethods.includes(AuthenticationMethodType.OTP_EMAIL) + ) { + mfaValid = !!session.factors.otpEmail?.verifiedAt; + if (!mfaValid) { + console.warn( + "Session has no valid otpEmail factor", + session.factors.otpEmail?.verifiedAt, + ); + } + } else if ( + authMethods && + authMethods.includes(AuthenticationMethodType.OTP_SMS) + ) { + mfaValid = !!session.factors.otpSms?.verifiedAt; + if (!mfaValid) { + console.warn( + "Session has no valid otpSms factor", + session.factors.otpSms?.verifiedAt, + ); + } + } else if ( + authMethods && + authMethods.includes(AuthenticationMethodType.U2F) + ) { + mfaValid = !!session.factors.webAuthN?.verifiedAt; + if (!mfaValid) { + console.warn( + "Session has no valid u2f factor", + session.factors.webAuthN?.verifiedAt, + ); + } + } else { + // only check settings if no auth methods are available, as this would require a setup + const loginSettings = await getLoginSettings( + session.factors?.user?.organizationId, + ); + if (loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly) { + const otpEmail = session.factors.otpEmail?.verifiedAt; + const otpSms = session.factors.otpSms?.verifiedAt; + const totp = session.factors.totp?.verifiedAt; + const webAuthN = session.factors.webAuthN?.verifiedAt; + const idp = session.factors.intent?.verifiedAt; // TODO: forceMFA should not consider this as valid factor + + // must have one single check + mfaValid = !!(otpEmail || otpSms || totp || webAuthN || idp); + if (!mfaValid) { + console.warn("Session has no valid multifactor", session.factors); + } + } else { + mfaValid = true; + } + } + + const validPassword = session?.factors?.password?.verifiedAt; + const validPasskey = session?.factors?.webAuthN?.verifiedAt; + const validIDP = session?.factors?.intent?.verifiedAt; + + const stillValid = session.expirationDate + ? timestampDate(session.expirationDate).getTime() > new Date().getTime() + : true; + + if (!stillValid) { + console.warn( + "Session is expired", + session.expirationDate + ? timestampDate(session.expirationDate).toDateString() + : "no expiration date", ); } - if (sessions.length) { - return sessions[0]; + + const validChecks = !!(validPassword || validPasskey || validIDP); + + return stillValid && validChecks && mfaValid; +} + +async function findValidSession( + sessions: Session[], + authRequest: AuthRequest, +): Promise { + const sessionsWithHint = sessions.filter((s) => { + if (authRequest.hintUserId) { + return s.factors?.user?.id === authRequest.hintUserId; + } + if (authRequest.loginHint) { + return s.factors?.user?.loginName === authRequest.loginHint; + } + return true; + }); + + if (sessionsWithHint.length === 0) { + return undefined; } + + // sort by change date descending + sessionsWithHint.sort((a, b) => { + const dateA = a.changeDate ? timestampDate(a.changeDate).getTime() : 0; + const dateB = b.changeDate ? timestampDate(b.changeDate).getTime() : 0; + return dateB - dateA; + }); + + // return the first valid session according to settings + for (const session of sessionsWithHint) { + if (await isSessionValid(session)) { + return session; + } + } + return undefined; } @@ -52,49 +183,115 @@ export async function GET(request: NextRequest) { const authRequestId = searchParams.get("authRequest"); const sessionId = searchParams.get("sessionId"); - const sessionCookies: SessionCookie[] = await getAllSessions(); + // TODO: find a better way to handle _rsc (react server components) requests and block them to avoid conflicts when creating oidc callback + const _rsc = searchParams.get("_rsc"); + if (_rsc) { + return NextResponse.json({ error: "No _rsc supported" }, { status: 500 }); + } + + const sessionCookies = await getAllSessions(); const ids = sessionCookies.map((s) => s.id); let sessions: Session[] = []; if (ids && ids.length) { sessions = await loadSessions(ids); } - /** - * TODO: before automatically redirecting to the callbackUrl, check if the session is still valid - * possible scenaio: - * mfa is required, session is not valid anymore (e.g. session expired, user logged out, etc.) - * to check for mfa for automatically selected session -> const response = await listAuthenticationMethodTypes(userId); - **/ - if (authRequestId && sessionId) { console.log( `Login with session: ${sessionId} and authRequest: ${authRequestId}`, ); - let selectedSession = sessions.find((s) => s.id === sessionId); + const selectedSession = sessions.find((s) => s.id === sessionId); if (selectedSession && selectedSession.id) { console.log(`Found session ${selectedSession.id}`); + + const isValid = await isSessionValid(selectedSession); + + console.log("Session is valid:", isValid); + + if (!isValid && selectedSession.factors?.user) { + // if the session is not valid anymore, we need to redirect the user to re-authenticate / + // TODO: handle IDP intent direcly if available + const command: SendLoginnameCommand = { + loginName: selectedSession.factors.user?.loginName, + organization: selectedSession.factors?.user?.organizationId, + authRequestId: authRequestId, + }; + + const res = await sendLoginname(command); + + if (res && "redirect" in res && res?.redirect) { + const absoluteUrl = new URL(res.redirect, request.url); + return NextResponse.redirect(absoluteUrl.toString()); + } + } + const cookie = sessionCookies.find( (cookie) => cookie.id === selectedSession?.id, ); if (cookie && cookie.id && cookie.token) { - console.log(`Found sessioncookie ${cookie.id}`); - const session = { sessionId: cookie?.id, sessionToken: cookie?.token, }; - const { callbackUrl } = await createCallback({ - authRequestId, - callbackKind: { - case: "session", - value: session, - }, - }); - return NextResponse.redirect(callbackUrl); + // works not with _rsc request + try { + const { callbackUrl } = await createCallback( + create(CreateCallbackRequestSchema, { + authRequestId, + callbackKind: { + case: "session", + value: create(SessionSchema, session), + }, + }), + ); + if (callbackUrl) { + return NextResponse.redirect(callbackUrl); + } else { + return NextResponse.json( + { error: "An error occurred!" }, + { status: 500 }, + ); + } + } catch (error: unknown) { + // handle already handled gracefully as these could come up if old emails with authRequestId are used (reset password, register emails etc.) + console.error(error); + if ( + error && + typeof error === "object" && + "code" in error && + error?.code === 9 + ) { + const loginSettings = await getLoginSettings( + selectedSession.factors?.user?.organizationId, + ); + + if (loginSettings?.defaultRedirectUri) { + return NextResponse.redirect(loginSettings.defaultRedirectUri); + } + + const signedinUrl = new URL("/signedin", request.url); + + if (selectedSession.factors?.user?.loginName) { + signedinUrl.searchParams.set( + "loginName", + selectedSession.factors?.user?.loginName, + ); + } + if (selectedSession.factors?.user?.organizationId) { + signedinUrl.searchParams.set( + "organization", + selectedSession.factors?.user?.organizationId, + ); + } + return NextResponse.redirect(signedinUrl); + } else { + return NextResponse.json({ error }, { status: 500 }); + } + } } } } @@ -126,8 +323,10 @@ export async function GET(request: NextRequest) { const matched = ORG_DOMAIN_SCOPE_REGEX.exec(orgDomainScope); const orgDomain = matched?.[1] ?? ""; if (orgDomain) { - const org = await getOrgByDomain(orgDomain); - organization = org?.org?.id ?? ""; + const orgs = await getOrgsByDomain(orgDomain); + if (orgs.result && orgs.result.length === 1) { + organization = orgs.result[0].id ?? ""; + } } } } @@ -145,31 +344,10 @@ export async function GET(request: NextRequest) { const idp = identityProviders.find((idp) => idp.id === idpId); if (idp) { - const host = request.nextUrl.origin; + const origin = request.nextUrl.origin; const identityProviderType = identityProviders[0].type; - let provider: string; - - switch (identityProviderType) { - case IdentityProviderType.GITHUB: - provider = "github"; - break; - case IdentityProviderType.GOOGLE: - provider = "google"; - break; - case IdentityProviderType.AZURE_AD: - provider = "azure"; - break; - case IdentityProviderType.SAML: - provider = "saml"; - break; - case IdentityProviderType.OIDC: - provider = "oidc"; - break; - default: - provider = "oidc"; - break; - } + let provider = idpTypeToSlug(identityProviderType); const params = new URLSearchParams(); @@ -185,10 +363,10 @@ export async function GET(request: NextRequest) { idpId, urls: { successUrl: - `${host}/idp/${provider}/success?` + + `${origin}/idp/${provider}/success?` + new URLSearchParams(params), failureUrl: - `${host}/idp/${provider}/failure?` + + `${origin}/idp/${provider}/failure?` + new URLSearchParams(params), }, }).then((resp) => { @@ -217,8 +395,8 @@ export async function GET(request: NextRequest) { if (authRequest && authRequest.prompt.includes(Prompt.CREATE)) { const registerUrl = new URL("/register", request.url); - if (authRequest?.id) { - registerUrl.searchParams.set("authRequestId", authRequest?.id); + if (authRequest.id) { + registerUrl.searchParams.set("authRequestId", authRequest.id); } if (organization) { registerUrl.searchParams.set("organization", organization); @@ -233,10 +411,36 @@ export async function GET(request: NextRequest) { if (authRequest.prompt.includes(Prompt.SELECT_ACCOUNT)) { return gotoAccounts(); } else if (authRequest.prompt.includes(Prompt.LOGIN)) { - // if prompt is login + /** + * The login prompt instructs the authentication server to prompt the user for re-authentication, regardless of whether the user is already authenticated + */ + + // if a hint is provided, skip loginname page and jump to the next page + if (authRequest.loginHint) { + try { + let command: SendLoginnameCommand = { + loginName: authRequest.loginHint, + authRequestId: authRequest.id, + }; + + if (organization) { + command = { ...command, organization }; + } + + const res = await sendLoginname(command); + + if (res && "redirect" in res && res?.redirect) { + const absoluteUrl = new URL(res.redirect, request.url); + return NextResponse.redirect(absoluteUrl.toString()); + } + } catch (error) { + console.error("Failed to execute sendLoginname:", error); + } + } + const loginNameUrl = new URL("/loginname", request.url); - if (authRequest?.id) { - loginNameUrl.searchParams.set("authRequestId", authRequest?.id); + if (authRequest.id) { + loginNameUrl.searchParams.set("authRequestId", authRequest.id); } if (authRequest.loginHint) { loginNameUrl.searchParams.set("loginName", authRequest.loginHint); @@ -246,75 +450,87 @@ export async function GET(request: NextRequest) { } return NextResponse.redirect(loginNameUrl); } else if (authRequest.prompt.includes(Prompt.NONE)) { - // NONE prompt - silent authentication + /** + * With an OIDC none prompt, the authentication server must not display any authentication or consent user interface pages. + * This means that the user should not be prompted to enter their password again. + * Instead, the server attempts to silently authenticate the user using an existing session or other authentication mechanisms that do not require user interaction + **/ + const selectedSession = await findValidSession(sessions, authRequest); - let selectedSession = findSession(sessions, authRequest); - - if (selectedSession && selectedSession.id) { - const cookie = sessionCookies.find( - (cookie) => cookie.id === selectedSession?.id, + if (!selectedSession || !selectedSession.id) { + return NextResponse.json( + { error: "No active session found" }, + { status: 400 }, ); + } - if (cookie && cookie.id && cookie.token) { - const session = { - sessionId: cookie?.id, - sessionToken: cookie?.token, - }; - const { callbackUrl } = await createCallback({ + const cookie = sessionCookies.find( + (cookie) => cookie.id === selectedSession.id, + ); + + if (!cookie || !cookie.id || !cookie.token) { + return NextResponse.json( + { error: "No active session found" }, + { status: 400 }, + ); + } + + const session = { + sessionId: cookie.id, + sessionToken: cookie.token, + }; + + const { callbackUrl } = await createCallback( + create(CreateCallbackRequestSchema, { + authRequestId, + callbackKind: { + case: "session", + value: create(SessionSchema, session), + }, + }), + ); + return NextResponse.redirect(callbackUrl); + } else { + // check for loginHint, userId hint and valid sessions + let selectedSession = await findValidSession(sessions, authRequest); + + if (!selectedSession || !selectedSession.id) { + return gotoAccounts(); + } + + const cookie = sessionCookies.find( + (cookie) => cookie.id === selectedSession.id, + ); + + if (!cookie || !cookie.id || !cookie.token) { + return gotoAccounts(); + } + + const session = { + sessionId: cookie.id, + sessionToken: cookie.token, + }; + + try { + const { callbackUrl } = await createCallback( + create(CreateCallbackRequestSchema, { authRequestId, callbackKind: { case: "session", - value: session, + value: create(SessionSchema, session), }, - }); + }), + ); + if (callbackUrl) { return NextResponse.redirect(callbackUrl); } else { - return NextResponse.json( - { error: "No active session found" }, - { status: 400 }, // TODO: check for correct status code + console.log( + "could not create callback, redirect user to choose other account", ); - } - } else { - return NextResponse.json( - { error: "No active session found" }, - { status: 400 }, // TODO: check for correct status code - ); - } - } else { - // check for loginHint, userId hint sessions - let selectedSession = findSession(sessions, authRequest); - - if (selectedSession && selectedSession.id) { - const cookie = sessionCookies.find( - (cookie) => cookie.id === selectedSession?.id, - ); - - if (cookie && cookie.id && cookie.token) { - const session = { - sessionId: cookie?.id, - sessionToken: cookie?.token, - }; - try { - const { callbackUrl } = await createCallback({ - authRequestId, - callbackKind: { - case: "session", - value: session, - }, - }); - if (callbackUrl) { - return NextResponse.redirect(callbackUrl); - } else { - return gotoAccounts(); - } - } catch (error) { - console.error(error); - return gotoAccounts(); - } - } else { return gotoAccounts(); } - } else { + } catch (error) { + console.error(error); return gotoAccounts(); } } diff --git a/apps/login/src/app/page.tsx b/apps/login/src/app/page.tsx deleted file mode 100644 index 7bddc8caa4..0000000000 --- a/apps/login/src/app/page.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { demos } from "@/lib/demos"; -import Link from "next/link"; - -export default function Page() { - return ( -
-
-
-

Pages

- -
- {demos.map((section) => { - return ( -
-
- {section.name} -
-
- {section.items.map((item) => { - return ( - -
{item.name}
- - {item.description ? ( -
- {item.description} -
- ) : null} - - ); - })} -
-
- ); - })} -
- -
-
- Deploy your own on Vercel -
- - Deploy with Vercel - -
-
-
-
- ); -} diff --git a/apps/login/src/app/sessions/route.ts b/apps/login/src/app/sessions/route.ts deleted file mode 100644 index 330ad15633..0000000000 --- a/apps/login/src/app/sessions/route.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { listSessions } from "@/lib/zitadel"; -import { SessionCookie, getAllSessions } from "@/utils/cookies"; -import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; -import { NextRequest, NextResponse } from "next/server"; - -async function loadSessions(ids: string[]): Promise { - const response = await listSessions( - ids.filter((id: string | undefined) => !!id), - ); - - return response?.sessions ?? []; -} - -export async function GET(request: NextRequest) { - const sessionCookies: SessionCookie[] = await getAllSessions(); - const ids = sessionCookies.map((s) => s.id); - let sessions: Session[] = []; - if (ids && ids.length) { - sessions = await loadSessions(ids); - } - - const responseHeaders = new Headers(); - responseHeaders.set("Access-Control-Allow-Origin", "*"); - responseHeaders.set("Access-Control-Allow-Headers", "*"); - - return NextResponse.json( - { sessions }, - { status: 200, headers: responseHeaders }, - ); -} diff --git a/apps/login/src/ui/AddressBar.tsx b/apps/login/src/components/address-bar.tsx similarity index 93% rename from apps/login/src/ui/AddressBar.tsx rename to apps/login/src/components/address-bar.tsx index a748488e30..7e7bda6bd0 100644 --- a/apps/login/src/ui/AddressBar.tsx +++ b/apps/login/src/components/address-bar.tsx @@ -1,7 +1,7 @@ "use client"; -import React from "react"; import { usePathname } from "next/navigation"; +import { Fragment } from "react"; type Props = { domain: string; @@ -39,7 +39,7 @@ export function AddressBar({ domain }: Props) { .filter((s) => !!s) .map((segment) => { return ( - + / - + ); })} diff --git a/apps/login/src/ui/Alert.tsx b/apps/login/src/components/alert.tsx similarity index 88% rename from apps/login/src/ui/Alert.tsx rename to apps/login/src/components/alert.tsx index b6a093bab0..417e67934e 100644 --- a/apps/login/src/ui/Alert.tsx +++ b/apps/login/src/components/alert.tsx @@ -2,10 +2,11 @@ import { ExclamationTriangleIcon, InformationCircleIcon, } from "@heroicons/react/24/outline"; -import clsx from "clsx"; +import { clsx } from "clsx"; +import { ReactNode } from "react"; type Props = { - children: React.ReactNode; + children: ReactNode; type?: AlertType; }; @@ -21,7 +22,7 @@ const red = const neutral = "border-divider-light dark:border-divider-dark bg-black/5 text-gray-600 dark:bg-white/10 dark:text-gray-200"; -export default function Alert({ children, type = AlertType.ALERT }: Props) { +export function Alert({ children, type = AlertType.ALERT }: Props) { return (
clsx( @@ -32,7 +32,7 @@ const LinkWrapper = ({ export const TOTP = (alreadyAdded: boolean, link: string) => { return ( - +
{ )} > - - - - - + timer-lock-outline + {" "} Authenticator App
@@ -93,7 +60,7 @@ C72,238.87917,85.87916,225,102.99997,225H248z" export const U2F = (alreadyAdded: boolean, link: string) => { return ( - +
{ xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" - stroke-width="1.5" + strokeWidth="1.5" stroke="currentColor" className="w-8 h-8 mr-4" > @@ -127,7 +94,7 @@ export const U2F = (alreadyAdded: boolean, link: string) => { export const EMAIL = (alreadyAdded: boolean, link: string) => { return ( - +
{ xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" - stroke-width={1.5} + strokeWidth={1.5} stroke="currentColor" > @@ -162,7 +129,7 @@ export const EMAIL = (alreadyAdded: boolean, link: string) => { export const SMS = (alreadyAdded: boolean, link: string) => { return ( - +
{ xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" - stroke-width="1.5" + strokeWidth="1.5" stroke="currentColor" > @@ -194,6 +161,68 @@ export const SMS = (alreadyAdded: boolean, link: string) => { ); }; +export const PASSKEYS = (alreadyAdded: boolean, link: string) => { + return ( + +
+ + + + Passkeys +
+ {alreadyAdded && ( + <> + + + )} +
+ ); +}; + +export const PASSWORD = (alreadyAdded: boolean, link: string) => { + return ( + +
+ + form-textbox-password + + + Password +
+ {alreadyAdded && ( + <> + + + )} +
+ ); +}; + function Setup() { return (
diff --git a/apps/login/src/components/authentication-method-radio.tsx b/apps/login/src/components/authentication-method-radio.tsx new file mode 100644 index 0000000000..1b2af2d167 --- /dev/null +++ b/apps/login/src/components/authentication-method-radio.tsx @@ -0,0 +1,95 @@ +"use client"; + +import { RadioGroup } from "@headlessui/react"; +import { useTranslations } from "next-intl"; + +export enum AuthenticationMethod { + Passkey = "passkey", + Password = "password", +} + +export const methods = [ + AuthenticationMethod.Passkey, + AuthenticationMethod.Password, +]; + +export function AuthenticationMethodRadio({ + selected, + selectionChanged, +}: { + selected: any; + selectionChanged: (value: any) => void; +}) { + const t = useTranslations("register"); + + return ( +
+
+ + Server size +
+ {methods.map((method) => ( + + `${ + active + ? "ring-2 ring-opacity-60 ring-primary-light-500 dark:ring-white/20" + : "" + } + ${ + checked + ? "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" + } + 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 }) => ( + <> +
+ {method === "passkey" && ( + + + + )} + {method === "password" && ( + + form-textbox-password + + + )} + + {t(`methods.${method}`)} + +
+ + )} +
+ ))} +
+
+
+
+ ); +} diff --git a/apps/login/src/ui/Avatar.tsx b/apps/login/src/components/avatar.tsx similarity index 86% rename from apps/login/src/ui/Avatar.tsx rename to apps/login/src/components/avatar.tsx index bfa5d229ae..3f340e09b7 100644 --- a/apps/login/src/ui/Avatar.tsx +++ b/apps/login/src/components/avatar.tsx @@ -1,7 +1,8 @@ "use client"; -import { ColorShade, getColorHash } from "@/utils/colors"; +import { ColorShade, getColorHash } from "@/helpers/colors"; import { useTheme } from "next-themes"; +import Image from "next/image"; interface AvatarProps { name: string | null | undefined; @@ -71,14 +72,17 @@ export function Avatar({ : size === "base" ? "w-[38px] h-[38px] font-bold" : size === "small" - ? "w-[32px] h-[32px] font-bold text-[13px]" - : "" + ? "!w-[32px] !h-[32px] font-bold text-[13px]" + : "w-12 h-12" }`} style={resolvedTheme === "light" ? avatarStyleLight : avatarStyleDark} > {imageUrl ? ( - avatar ) : ( diff --git a/apps/login/src/ui/BackButton.tsx b/apps/login/src/components/back-button.tsx similarity index 56% rename from apps/login/src/ui/BackButton.tsx rename to apps/login/src/components/back-button.tsx index b2f04ad584..fe348af9c4 100644 --- a/apps/login/src/ui/BackButton.tsx +++ b/apps/login/src/components/back-button.tsx @@ -1,9 +1,11 @@ "use client"; +import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; -import { Button, ButtonVariants } from "./Button"; +import { Button, ButtonVariants } from "./button"; -export default function BackButton() { +export function BackButton() { + const t = useTranslations("common"); const router = useRouter(); return ( ); } diff --git a/apps/login/src/ui/Boundary.tsx b/apps/login/src/components/boundary.tsx similarity index 93% rename from apps/login/src/ui/Boundary.tsx rename to apps/login/src/components/boundary.tsx index c7487df1a9..354d920960 100644 --- a/apps/login/src/ui/Boundary.tsx +++ b/apps/login/src/components/boundary.tsx @@ -1,12 +1,12 @@ -import clsx from "clsx"; -import React from "react"; +import { clsx } from "clsx"; +import { ReactNode } from "react"; const Label = ({ children, animateRerendering, color, }: { - children: React.ReactNode; + children: ReactNode; animateRerendering?: boolean; color?: "default" | "pink" | "blue" | "violet" | "cyan" | "orange" | "red"; }) => { @@ -34,7 +34,7 @@ export const Boundary = ({ color = "default", animateRerendering = true, }: { - children: React.ReactNode; + children: ReactNode; labels?: string[]; size?: "small" | "default"; color?: "default" | "pink" | "blue" | "violet" | "cyan" | "orange" | "red"; @@ -44,7 +44,7 @@ export const Boundary = ({
({ + mode: "onBlur", + defaultValues: { + password: "", + comfirmPassword: "", + }, + }); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + async function submitChange(values: Inputs) { + setLoading(true); + + const changeResponse = checkSessionAndSetPassword({ + sessionId, + password: values.password, + }) + .catch(() => { + setError("Could not change password"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (changeResponse && "error" in changeResponse && changeResponse.error) { + setError( + typeof changeResponse.error === "string" + ? changeResponse.error + : "Unknown error", + ); + return; + } + + if (!changeResponse) { + setError("Could not change password"); + return; + } + + await new Promise((resolve) => setTimeout(resolve, 1000)); // wait for a second, to prevent eventual consistency issues + + const passwordResponse = await sendPassword({ + loginName, + organization, + checks: create(ChecksSchema, { + password: { password: values.password }, + }), + authRequestId, + }) + .catch(() => { + setError("Could not verify password"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if ( + passwordResponse && + "error" in passwordResponse && + passwordResponse.error + ) { + setError(passwordResponse.error); + return; + } + + if ( + passwordResponse && + "redirect" in passwordResponse && + passwordResponse.redirect + ) { + return router.push(passwordResponse.redirect); + } + + return; + } + + const { errors } = formState; + + const watchPassword = watch("password", ""); + const watchConfirmPassword = watch("confirmPassword", ""); + + const hasMinLength = + passwordComplexitySettings && + watchPassword?.length >= passwordComplexitySettings.minLength; + const hasSymbol = symbolValidator(watchPassword); + const hasNumber = numberValidator(watchPassword); + const hasUppercase = upperCaseValidator(watchPassword); + const hasLowercase = lowerCaseValidator(watchPassword); + + const policyIsValid = + passwordComplexitySettings && + (passwordComplexitySettings.requiresLowercase ? hasLowercase : true) && + (passwordComplexitySettings.requiresNumber ? hasNumber : true) && + (passwordComplexitySettings.requiresUppercase ? hasUppercase : true) && + (passwordComplexitySettings.requiresSymbol ? hasSymbol : true) && + hasMinLength; + + return ( +
+
+
+ +
+
+ +
+
+ + {passwordComplexitySettings && ( + + )} + + {error && {error}} + +
+ + +
+ + ); +} diff --git a/apps/login/src/components/checkbox.tsx b/apps/login/src/components/checkbox.tsx new file mode 100644 index 0000000000..41b45aad92 --- /dev/null +++ b/apps/login/src/components/checkbox.tsx @@ -0,0 +1,62 @@ +import classNames from "clsx"; +import { + DetailedHTMLProps, + forwardRef, + InputHTMLAttributes, + useEffect, + useState, +} from "react"; + +export type CheckboxProps = DetailedHTMLProps< + InputHTMLAttributes, + HTMLInputElement +> & { + checked: boolean; + disabled?: boolean; + onChangeVal?: (checked: boolean) => void; +}; + +export const Checkbox = forwardRef( + function Checkbox( + { + className = "", + checked = false, + disabled = false, + onChangeVal, + children, + ...props + }, + ref, + ) { + const [enabled, setEnabled] = useState(checked); + + useEffect(() => { + setEnabled(checked); + }, [checked]); + + return ( +
+
+
+ { + setEnabled(event.target?.checked); + onChangeVal && onChangeVal(event.target?.checked); + }} + disabled={disabled} + type="checkbox" + 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", + className, + )} + {...props} + /> +
+
+ {children} +
+ ); + }, +); diff --git a/apps/login/src/components/choose-authenticator-to-setup.tsx b/apps/login/src/components/choose-authenticator-to-setup.tsx new file mode 100644 index 0000000000..9075e3286e --- /dev/null +++ b/apps/login/src/components/choose-authenticator-to-setup.tsx @@ -0,0 +1,44 @@ +import { + LoginSettings, + PasskeysType, +} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { useTranslations } from "next-intl"; +import { Alert, AlertType } from "./alert"; +import { PASSKEYS, PASSWORD } from "./auth-methods"; + +type Props = { + authMethods: AuthenticationMethodType[]; + params: URLSearchParams; + loginSettings: LoginSettings; +}; + +export function ChooseAuthenticatorToSetup({ + authMethods, + params, + loginSettings, +}: Props) { + const t = useTranslations("authenticator"); + + if (authMethods.length !== 0) { + return {t("allSetup")}; + } else { + return ( + <> + {loginSettings.passkeysType == PasskeysType.NOT_ALLOWED && + !loginSettings.allowUsernamePassword && ( + {t("noMethodsAvailable")} + )} + +
+ {!authMethods.includes(AuthenticationMethodType.PASSWORD) && + loginSettings.allowUsernamePassword && + PASSWORD(false, "/password/set?" + params)} + {!authMethods.includes(AuthenticationMethodType.PASSKEY) && + loginSettings.passkeysType == PasskeysType.ALLOWED && + PASSKEYS(false, "/passkey/set?" + params)} +
+ + ); + } +} diff --git a/apps/login/src/ui/ChooseSecondFactorToSetup.tsx b/apps/login/src/components/choose-second-factor-to-setup.tsx similarity index 50% rename from apps/login/src/ui/ChooseSecondFactorToSetup.tsx rename to apps/login/src/components/choose-second-factor-to-setup.tsx index f38e36037b..1502d555a7 100644 --- a/apps/login/src/ui/ChooseSecondFactorToSetup.tsx +++ b/apps/login/src/components/choose-second-factor-to-setup.tsx @@ -1,8 +1,11 @@ "use client"; -import { EMAIL, SMS, TOTP, U2F } from "./AuthMethods"; -import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { + LoginSettings, + SecondFactorType, +} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { EMAIL, SMS, TOTP, U2F } from "./auth-methods"; type Props = { loginName?: string; @@ -16,7 +19,7 @@ type Props = { emailVerified: boolean; }; -export default function ChooseSecondFactorToSetup({ +export function ChooseSecondFactorToSetup({ loginName, sessionId, authRequestId, @@ -47,28 +50,37 @@ export default function ChooseSecondFactorToSetup({ return (
- {loginSettings.secondFactors.map((factor, i) => { - return factor === 1 - ? TOTP( + {loginSettings.secondFactors.map((factor) => { + switch (factor) { + case SecondFactorType.OTP: + return TOTP( userMethods.includes(AuthenticationMethodType.TOTP), "/otp/time-based/set?" + params, - ) - : factor === 2 - ? U2F( - userMethods.includes(AuthenticationMethodType.U2F), - "/u2f/set?" + params, + ); + case SecondFactorType.U2F: + return U2F( + userMethods.includes(AuthenticationMethodType.U2F), + "/u2f/set?" + params, + ); + case SecondFactorType.OTP_EMAIL: + return ( + emailVerified && + EMAIL( + userMethods.includes(AuthenticationMethodType.OTP_EMAIL), + "/otp/email/set?" + params, ) - : factor === 3 && emailVerified - ? EMAIL( - userMethods.includes(AuthenticationMethodType.OTP_EMAIL), - "/otp/email/set?" + params, - ) - : factor === 4 && phoneVerified - ? SMS( - userMethods.includes(AuthenticationMethodType.OTP_SMS), - "/otp/sms/set?" + params, - ) - : null; + ); + case SecondFactorType.OTP_SMS: + return ( + phoneVerified && + SMS( + userMethods.includes(AuthenticationMethodType.OTP_SMS), + "/otp/sms/set?" + params, + ) + ); + default: + return null; + } })}
); diff --git a/apps/login/src/ui/ChooseSecondFactor.tsx b/apps/login/src/components/choose-second-factor.tsx similarity index 83% rename from apps/login/src/ui/ChooseSecondFactor.tsx rename to apps/login/src/components/choose-second-factor.tsx index 9d69b09cd7..3acf3e2214 100644 --- a/apps/login/src/ui/ChooseSecondFactor.tsx +++ b/apps/login/src/components/choose-second-factor.tsx @@ -1,11 +1,7 @@ "use client"; -import Link from "next/link"; -import { BadgeState, StateBadge } from "./StateBadge"; -import clsx from "clsx"; -import { CheckIcon } from "@heroicons/react/24/outline"; -import { EMAIL, SMS, TOTP, U2F } from "./AuthMethods"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { EMAIL, SMS, TOTP, U2F } from "./auth-methods"; type Props = { loginName?: string; @@ -15,7 +11,7 @@ type Props = { userMethods: AuthenticationMethodType[]; }; -export default function ChooseSecondFactor({ +export function ChooseSecondFactor({ loginName, sessionId, authRequestId, diff --git a/apps/login/src/ui/CopyToClipboard.tsx b/apps/login/src/components/copy-to-clipboard.tsx similarity index 93% rename from apps/login/src/ui/CopyToClipboard.tsx rename to apps/login/src/components/copy-to-clipboard.tsx index b77cbdd3ac..cf0dedc060 100644 --- a/apps/login/src/ui/CopyToClipboard.tsx +++ b/apps/login/src/components/copy-to-clipboard.tsx @@ -11,7 +11,7 @@ type Props = { value: string; }; -export default function CopyToClipboard({ value }: Props) { +export function CopyToClipboard({ value }: Props) { const [copied, setCopied] = useState(false); useEffect(() => { diff --git a/apps/login/src/ui/DefaultTags.tsx b/apps/login/src/components/default-tags.tsx similarity index 100% rename from apps/login/src/ui/DefaultTags.tsx rename to apps/login/src/components/default-tags.tsx diff --git a/apps/login/src/components/dynamic-theme.tsx b/apps/login/src/components/dynamic-theme.tsx new file mode 100644 index 0000000000..7d0fecb558 --- /dev/null +++ b/apps/login/src/components/dynamic-theme.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { Logo } from "@/components/logo"; +import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb"; +import { ReactNode } from "react"; +import { ThemeWrapper } from "./theme-wrapper"; + +export function DynamicTheme({ + branding, + children, +}: { + children: ReactNode; + branding?: BrandingSettings; +}) { + return ( + +
+
+
+ {branding && ( + + )} +
+ +
{children}
+
+
+
+
+ ); +} diff --git a/apps/login/src/ui/ExternalLink.tsx b/apps/login/src/components/external-link.tsx similarity index 87% rename from apps/login/src/ui/ExternalLink.tsx rename to apps/login/src/components/external-link.tsx index 0734aff95e..a52164d35d 100644 --- a/apps/login/src/ui/ExternalLink.tsx +++ b/apps/login/src/components/external-link.tsx @@ -1,10 +1,11 @@ import { ArrowRightIcon } from "@heroicons/react/24/solid"; +import { ReactNode } from "react"; export const ExternalLink = ({ children, href, }: { - children: React.ReactNode; + children: ReactNode; href: string; }) => { return ( diff --git a/apps/login/src/components/idp-signin.tsx b/apps/login/src/components/idp-signin.tsx new file mode 100644 index 0000000000..c2f3fe40b3 --- /dev/null +++ b/apps/login/src/components/idp-signin.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { createNewSessionFromIdpIntent } from "@/lib/server/idp"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import { Alert } from "./alert"; +import { Spinner } from "./spinner"; + +type Props = { + userId: string; + // organization: string; + idpIntent: { + idpIntentId: string; + idpIntentToken: string; + }; + authRequestId?: string; +}; + +export function IdpSignin({ + userId, + idpIntent: { idpIntentId, idpIntentToken }, + authRequestId, +}: Props) { + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const router = useRouter(); + + useEffect(() => { + createNewSessionFromIdpIntent({ + userId, + idpIntent: { + idpIntentId, + idpIntentToken, + }, + authRequestId, + }) + .then((response) => { + if (response && "error" in response && response?.error) { + setError(response?.error); + return; + } + + if (response && "redirect" in response && response?.redirect) { + return router.push(response.redirect); + } + }) + .catch(() => { + setError("An internal error occurred"); + return; + }) + .finally(() => { + setLoading(false); + }); + }, []); + + return ( +
+ {loading && } + {error && ( +
+ {error} +
+ )} +
+ ); +} diff --git a/apps/login/src/components/idps/base-button.tsx b/apps/login/src/components/idps/base-button.tsx new file mode 100644 index 0000000000..4f24dd17bc --- /dev/null +++ b/apps/login/src/components/idps/base-button.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { clsx } from "clsx"; +import { ButtonHTMLAttributes, DetailedHTMLProps, forwardRef } from "react"; + +export type SignInWithIdentityProviderProps = DetailedHTMLProps< + ButtonHTMLAttributes, + HTMLButtonElement +> & { + name?: string; + e2e?: string; +}; + +export const BaseButton = forwardRef< + HTMLButtonElement, + SignInWithIdentityProviderProps +>(function BaseButton(props, ref) { + return ( + - ), -); - -SignInWithGithub.displayName = "SignInWithGithub"; + + ); +}); diff --git a/apps/login/src/components/idps/sign-in-with-gitlab.test.tsx b/apps/login/src/components/idps/sign-in-with-gitlab.test.tsx new file mode 100644 index 0000000000..ab5bfda54d --- /dev/null +++ b/apps/login/src/components/idps/sign-in-with-gitlab.test.tsx @@ -0,0 +1,45 @@ +import { afterEach, describe, expect, test } from "vitest"; + +import { cleanup, render, screen } from "@testing-library/react"; +import { NextIntlClientProvider } from "next-intl"; + +import { SignInWithGitlab } from "./sign-in-with-gitlab"; + +afterEach(cleanup); + +describe("", async () => { + const messages = { + idp: { + signInWithGitlab: "Sign in with GitLab", + }, + }; + + test("renders without crashing", () => { + const { container } = render( + + + , + ); + expect(container.firstChild).toBeDefined(); + }); + + test("displays the default text", () => { + render( + + + , + ); + const signInText = screen.getByText(/Sign in with Gitlab/i); + expect(signInText).toBeInTheDocument(); + }); + + test("displays the given text", () => { + render( + + + , + ); + const signInText = screen.getByText(/Gitlab/i); + expect(signInText).toBeInTheDocument(); + }); +}); diff --git a/packages/zitadel-react/src/components/SignInWithGitlab.tsx b/apps/login/src/components/idps/sign-in-with-gitlab.tsx similarity index 62% rename from packages/zitadel-react/src/components/SignInWithGitlab.tsx rename to apps/login/src/components/idps/sign-in-with-gitlab.tsx index 62b4d04c34..8a1ed7d349 100644 --- a/packages/zitadel-react/src/components/SignInWithGitlab.tsx +++ b/apps/login/src/components/idps/sign-in-with-gitlab.tsx @@ -1,18 +1,19 @@ -import { ReactNode, forwardRef } from "react"; -import { SignInWithIdentityProviderProps } from "./SignInWith"; +"use client"; + +import { useTranslations } from "next-intl"; +import { forwardRef } from "react"; +import { BaseButton, SignInWithIdentityProviderProps } from "./base-button"; export const SignInWithGitlab = forwardRef< HTMLButtonElement, SignInWithIdentityProviderProps ->( - ({ children, className = "", name = "", ...props }, ref): ReactNode => ( - - ), -); - -SignInWithGitlab.displayName = "SignInWithGitlab"; + + ); +}); diff --git a/apps/login/src/components/idps/sign-in-with-google.test.tsx b/apps/login/src/components/idps/sign-in-with-google.test.tsx new file mode 100644 index 0000000000..953da21d94 --- /dev/null +++ b/apps/login/src/components/idps/sign-in-with-google.test.tsx @@ -0,0 +1,44 @@ +import { afterEach, describe, expect, test } from "vitest"; + +import { cleanup, render, screen } from "@testing-library/react"; +import { NextIntlClientProvider } from "next-intl"; +import { SignInWithGoogle } from "./sign-in-with-google"; + +afterEach(cleanup); + +describe("", async () => { + const messages = { + idp: { + signInWithGoogle: "Sign in with Google", + }, + }; + + test("renders without crashing", () => { + const { container } = render( + + + , + ); + expect(container.firstChild).toBeDefined(); + }); + + test("displays the default text", () => { + render( + + + , + ); + const signInText = screen.getByText(/Sign in with Google/i); + expect(signInText).toBeInTheDocument(); + }); + + test("displays the given text", () => { + render( + + + , + ); + const signInText = screen.getByText(/Google/i); + expect(signInText).toBeInTheDocument(); + }); +}); diff --git a/packages/zitadel-react/src/components/SignInWithGoogle.tsx b/apps/login/src/components/idps/sign-in-with-google.tsx similarity index 75% rename from packages/zitadel-react/src/components/SignInWithGoogle.tsx rename to apps/login/src/components/idps/sign-in-with-google.tsx index e3dc0c8596..6162ef4a96 100644 --- a/packages/zitadel-react/src/components/SignInWithGoogle.tsx +++ b/apps/login/src/components/idps/sign-in-with-google.tsx @@ -1,18 +1,19 @@ -import { ReactNode, forwardRef } from "react"; -import { SignInWithIdentityProviderProps } from "./SignInWith"; +"use client"; + +import { useTranslations } from "next-intl"; +import { forwardRef } from "react"; +import { BaseButton, SignInWithIdentityProviderProps } from "./base-button"; export const SignInWithGoogle = forwardRef< HTMLButtonElement, SignInWithIdentityProviderProps ->( - ({ children, className = "", name = "", ...props }, ref): ReactNode => ( - - ), -); - -SignInWithGoogle.displayName = "SignInWithGoogle"; + + ); +}); diff --git a/apps/login/src/ui/Input.tsx b/apps/login/src/components/input.tsx similarity index 97% rename from apps/login/src/ui/Input.tsx rename to apps/login/src/components/input.tsx index b5af102ec3..0cb9e58467 100644 --- a/apps/login/src/ui/Input.tsx +++ b/apps/login/src/components/input.tsx @@ -1,7 +1,8 @@ "use client"; + import { CheckCircleIcon } from "@heroicons/react/24/solid"; -import clsx from "clsx"; -import React, { +import { clsx } from "clsx"; +import { ChangeEvent, DetailedHTMLProps, forwardRef, @@ -64,6 +65,7 @@ export const TextInput = forwardRef( {label} {required && "*"} ({ + mode: "onBlur", + defaultValues: { + email: email ?? "", + firstName: firstname ?? "", + lastname: lastname ?? "", + }, + }); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + const router = useRouter(); + + async function submitAndContinue(values: Inputs) { + setLoading(true); + const response = await inviteUser({ + email: values.email, + firstName: values.firstname, + lastName: values.lastname, + organization: organization, + }) + .catch(() => { + setError("Could not create invitation Code"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (response && typeof response === "object" && "error" in response) { + setError(response.error); + return; + } + + if (!response) { + setError("Could not create invitation Code"); + return; + } + + const params = new URLSearchParams({}); + + if (response) { + params.append("userId", response); + } + + return router.push(`/invite/success?` + params); + } + + const { errors } = formState; + + return ( +
+
+
+ +
+
+ +
+
+ +
+
+ + {error && ( +
+ {error} +
+ )} + +
+ + +
+
+ ); +} diff --git a/apps/login/src/components/language-provider.tsx b/apps/login/src/components/language-provider.tsx new file mode 100644 index 0000000000..21a53093bb --- /dev/null +++ b/apps/login/src/components/language-provider.tsx @@ -0,0 +1,13 @@ +import { NextIntlClientProvider } from "next-intl"; +import { getMessages } from "next-intl/server"; +import { ReactNode } from "react"; + +export async function LanguageProvider({ children }: { children: ReactNode }) { + const messages = await getMessages(); + + return ( + + {children} + + ); +} diff --git a/apps/login/src/components/language-switcher.tsx b/apps/login/src/components/language-switcher.tsx new file mode 100644 index 0000000000..8d68248c4a --- /dev/null +++ b/apps/login/src/components/language-switcher.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { setLanguageCookie } from "@/lib/cookies"; +import { Lang, LANGS } from "@/lib/i18n"; +import { + Listbox, + ListboxButton, + ListboxOption, + ListboxOptions, +} from "@headlessui/react"; +import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/outline"; +import clsx from "clsx"; +import { useLocale } from "next-intl"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; + +export function LanguageSwitcher() { + const currentLocale = useLocale(); + + const [selected, setSelected] = useState( + LANGS.find((l) => l.code === currentLocale) || LANGS[0], + ); + + const router = useRouter(); + + const handleChange = async (language: Lang) => { + setSelected(language); + const newLocale = language.code; + + await setLanguageCookie(newLocale); + + router.refresh(); + }; + + return ( +
+ + + {selected.name} + + + {LANGS.map((lang, index) => ( + + +
+ {lang.name} +
+
+ ))} +
+
+
+ ); +} diff --git a/apps/login/src/ui/LayoutProviders.tsx b/apps/login/src/components/layout-providers.tsx similarity index 50% rename from apps/login/src/ui/LayoutProviders.tsx rename to apps/login/src/components/layout-providers.tsx index a9fc2dd956..fee93d015e 100644 --- a/apps/login/src/ui/LayoutProviders.tsx +++ b/apps/login/src/components/layout-providers.tsx @@ -1,9 +1,10 @@ "use client"; -import { ZitadelReactProvider } from "@zitadel/react"; + import { useTheme } from "next-themes"; +import { ReactNode } from "react"; type Props = { - children: React.ReactNode; + children: ReactNode; }; export function LayoutProviders({ children }: Props) { @@ -11,8 +12,6 @@ export function LayoutProviders({ children }: Props) { const isDark = resolvedTheme === "dark"; return ( -
- {children} -
+
{children}
); } diff --git a/apps/login/src/components/login-otp.tsx b/apps/login/src/components/login-otp.tsx new file mode 100644 index 0000000000..21d895e370 --- /dev/null +++ b/apps/login/src/components/login-otp.tsx @@ -0,0 +1,284 @@ +"use client"; + +import { getNextUrl } from "@/lib/client"; +import { updateSession } from "@/lib/server/session"; +import { create } from "@zitadel/client"; +import { RequestChallengesSchema } from "@zitadel/proto/zitadel/session/v2/challenge_pb"; +import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; +import { useEffect, useRef, useState } from "react"; +import { useForm } from "react-hook-form"; +import { Alert, AlertType } from "./alert"; +import { BackButton } from "./back-button"; +import { Button, ButtonVariants } from "./button"; +import { TextInput } from "./input"; +import { Spinner } from "./spinner"; + +// either loginName or sessionId must be provided +type Props = { + loginName?: string; + sessionId?: string; + authRequestId?: string; + organization?: string; + method: string; + code?: string; + loginSettings?: LoginSettings; + host: string | null; +}; + +type Inputs = { + code: string; +}; + +export function LoginOTP({ + loginName, + sessionId, + authRequestId, + organization, + method, + code, + loginSettings, + host, +}: Props) { + const t = useTranslations("otp"); + + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + const router = useRouter(); + + const initialized = useRef(false); + + const { register, handleSubmit, formState } = useForm({ + mode: "onBlur", + defaultValues: { + code: code ? code : "", + }, + }); + + useEffect(() => { + if (!initialized.current && ["email", "sms"].includes(method) && !code) { + initialized.current = true; + setLoading(true); + updateSessionForOTPChallenge() + .catch((error) => { + setError(error); + return; + }) + .finally(() => { + setLoading(false); + }); + } + }, []); + + async function updateSessionForOTPChallenge() { + let challenges; + + if (method === "email") { + challenges = create(RequestChallengesSchema, { + otpEmail: { + deliveryType: { + case: "sendCode", + value: host + ? { + urlTemplate: + `${host.includes("localhost") ? "http://" : "https://"}${host}/otp/${method}?code={{.Code}}&userId={{.UserID}}&sessionId={{.SessionID}}` + + (authRequestId ? `&authRequestId=${authRequestId}` : ""), + } + : {}, + }, + }, + }); + } + + if (method === "sms") { + challenges = create(RequestChallengesSchema, { + otpSms: {}, + }); + } + + setLoading(true); + const response = await updateSession({ + loginName, + sessionId, + organization, + challenges, + authRequestId, + }) + .catch(() => { + setError("Could not request OTP challenge"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (response && "error" in response && response.error) { + setError(response.error); + return; + } + + return response; + } + + async function submitCode(values: Inputs, organization?: string) { + setLoading(true); + + let body: any = { + code: values.code, + method, + }; + + if (organization) { + body.organization = organization; + } + + if (authRequestId) { + body.authRequestId = authRequestId; + } + + let checks; + + if (method === "sms") { + checks = create(ChecksSchema, { + otpSms: { code: values.code }, + }); + } + if (method === "email") { + checks = create(ChecksSchema, { + otpEmail: { code: values.code }, + }); + } + if (method === "time-based") { + checks = create(ChecksSchema, { + totp: { code: values.code }, + }); + } + + const response = await updateSession({ + loginName, + sessionId, + organization, + checks, + authRequestId, + }) + .catch(() => { + setError("Could not verify OTP code"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (response && "error" in response && response.error) { + setError(response.error); + return; + } + + return response; + } + + function setCodeAndContinue(values: Inputs, organization?: string) { + return submitCode(values, organization).then(async (response) => { + if (response && "sessionId" in response) { + setLoading(true); + // Wait for 2 seconds to avoid eventual consistency issues with an OTP code being verified in the /login endpoint + await new Promise((resolve) => setTimeout(resolve, 2000)); + + const url = + authRequestId && response.sessionId + ? await getNextUrl( + { + sessionId: response.sessionId, + authRequestId: authRequestId, + organization: response.factors?.user?.organizationId, + }, + loginSettings?.defaultRedirectUri, + ) + : response.factors?.user + ? await getNextUrl( + { + loginName: response.factors.user.loginName, + organization: response.factors?.user?.organizationId, + }, + loginSettings?.defaultRedirectUri, + ) + : null; + + setLoading(false); + if (url) { + router.push(url); + } + } + }); + } + + return ( +
+ {["email", "sms"].includes(method) && ( + +
+ + {t("verify.noCodeReceived")} + + +
+
+ )} +
+ +
+ + {error && ( +
+ {error} +
+ )} + +
+ + + +
+
+ ); +} diff --git a/apps/login/src/components/login-passkey.tsx b/apps/login/src/components/login-passkey.tsx new file mode 100644 index 0000000000..5e05cdb6a8 --- /dev/null +++ b/apps/login/src/components/login-passkey.tsx @@ -0,0 +1,286 @@ +"use client"; + +import { coerceToArrayBuffer, coerceToBase64Url } from "@/helpers/base64"; +import { sendPasskey } from "@/lib/server/passkeys"; +import { updateSession } from "@/lib/server/session"; +import { create, JsonObject } from "@zitadel/client"; +import { + RequestChallengesSchema, + UserVerificationRequirement, +} from "@zitadel/proto/zitadel/session/v2/challenge_pb"; +import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; +import { useEffect, useRef, useState } from "react"; +import { Alert } from "./alert"; +import { BackButton } from "./back-button"; +import { Button, ButtonVariants } from "./button"; +import { Spinner } from "./spinner"; + +// either loginName or sessionId must be provided +type Props = { + loginName?: string; + sessionId?: string; + authRequestId?: string; + altPassword: boolean; + login?: boolean; + organization?: string; + loginSettings?: LoginSettings; +}; + +export function LoginPasskey({ + loginName, + sessionId, + authRequestId, + altPassword, + organization, + login = true, + loginSettings, +}: Props) { + const t = useTranslations("passkey"); + + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + const router = useRouter(); + + const initialized = useRef(false); + + // TODO: move this to server side + useEffect(() => { + if (!initialized.current) { + initialized.current = true; + setLoading(true); + updateSessionForChallenge() + .then((response) => { + const pK = + response?.challenges?.webAuthN?.publicKeyCredentialRequestOptions + ?.publicKey; + + if (!pK) { + setError("Could not request passkey challenge"); + setLoading(false); + return; + } + + return submitLoginAndContinue(pK) + .catch((error) => { + setError(error); + return; + }) + .finally(() => { + setLoading(false); + }); + }) + .catch((error) => { + setError(error); + return; + }) + .finally(() => { + setLoading(false); + }); + } + }, []); + + async function updateSessionForChallenge( + userVerificationRequirement: number = login + ? UserVerificationRequirement.REQUIRED + : UserVerificationRequirement.DISCOURAGED, + ) { + setError(""); + setLoading(true); + const session = await updateSession({ + loginName, + sessionId, + organization, + challenges: create(RequestChallengesSchema, { + webAuthN: { + domain: "", + userVerificationRequirement, + }, + }), + authRequestId, + }) + .catch(() => { + setError("Could not request passkey challenge"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (session && "error" in session && session.error) { + setError(session.error); + return; + } + + return session; + } + + async function submitLogin(data: JsonObject) { + setLoading(true); + const response = await sendPasskey({ + loginName, + sessionId, + organization, + checks: { + webAuthN: { credentialAssertionData: data }, + } as Checks, + authRequestId, + }) + .catch(() => { + setError("Could not verify passkey"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (response && "error" in response && response.error) { + setError(response.error); + return; + } + + if (response && "redirect" in response && response.redirect) { + return router.push(response.redirect); + } + } + + async function submitLoginAndContinue( + publicKey: any, + ): Promise { + publicKey.challenge = coerceToArrayBuffer( + publicKey.challenge, + "publicKey.challenge", + ); + publicKey.allowCredentials.map((listItem: any) => { + listItem.id = coerceToArrayBuffer( + listItem.id, + "publicKey.allowCredentials.id", + ); + }); + + navigator.credentials + .get({ + publicKey, + }) + .then((assertedCredential: any) => { + if (!assertedCredential) { + setError("An error on retrieving passkey"); + return; + } + + const authData = new Uint8Array( + assertedCredential.response.authenticatorData, + ); + const clientDataJSON = new Uint8Array( + assertedCredential.response.clientDataJSON, + ); + const rawId = new Uint8Array(assertedCredential.rawId); + const sig = new Uint8Array(assertedCredential.response.signature); + const userHandle = new Uint8Array( + assertedCredential.response.userHandle, + ); + const data = { + id: assertedCredential.id, + rawId: coerceToBase64Url(rawId, "rawId"), + type: assertedCredential.type, + response: { + authenticatorData: coerceToBase64Url(authData, "authData"), + clientDataJSON: coerceToBase64Url(clientDataJSON, "clientDataJSON"), + signature: coerceToBase64Url(sig, "sig"), + userHandle: coerceToBase64Url(userHandle, "userHandle"), + }, + }; + + return submitLogin(data); + }) + .finally(() => { + setLoading(false); + }); + } + + return ( +
+ {error && ( +
+ {error} +
+ )} +
+ {altPassword ? ( + + ) : ( + + )} + + + +
+
+ ); +} diff --git a/apps/login/src/ui/Logo.tsx b/apps/login/src/components/logo.tsx similarity index 100% rename from apps/login/src/ui/Logo.tsx rename to apps/login/src/components/logo.tsx diff --git a/apps/login/src/ui/PasswordComplexity.test.tsx b/apps/login/src/components/password-complexity.test.tsx similarity index 94% rename from apps/login/src/ui/PasswordComplexity.test.tsx rename to apps/login/src/components/password-complexity.test.tsx index ba4b1445d7..090c95d397 100644 --- a/apps/login/src/ui/PasswordComplexity.test.tsx +++ b/apps/login/src/components/password-complexity.test.tsx @@ -1,12 +1,12 @@ -import { expect, describe, test, beforeEach, afterEach } from "vitest"; import { - render, cleanup, + render, screen, waitFor, within, } from "@testing-library/react"; -import PasswordComplexity from "./PasswordComplexity"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { PasswordComplexity } from "./password-complexity"; const matchesTitle = `Matches`; const doesntMatchTitle = `Doesn't match`; diff --git a/apps/login/src/ui/PasswordComplexity.tsx b/apps/login/src/components/password-complexity.tsx similarity index 81% rename from apps/login/src/ui/PasswordComplexity.tsx rename to apps/login/src/components/password-complexity.tsx index 3f69e5757a..40988984b6 100644 --- a/apps/login/src/ui/PasswordComplexity.tsx +++ b/apps/login/src/components/password-complexity.tsx @@ -3,7 +3,7 @@ import { numberValidator, symbolValidator, upperCaseValidator, -} from "@/utils/validators"; +} from "@/helpers/validators"; import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb"; type Props = { @@ -51,7 +51,7 @@ const cross = ( const desc = "text-14px leading-4 text-input-light-label dark:text-input-dark-label"; -export default function PasswordComplexity({ +export function PasswordComplexity({ passwordComplexitySettings, password, equals, @@ -65,7 +65,7 @@ export default function PasswordComplexity({ return (
{passwordComplexitySettings.minLength != undefined ? ( -
+
{hasMinLength ? check : cross} Password length {passwordComplexitySettings.minLength.toString()} @@ -74,23 +74,23 @@ export default function PasswordComplexity({ ) : ( )} -
+
{hasSymbol ? check : cross} has Symbol
-
+
{hasNumber ? check : cross} has Number
-
+
{hasUppercase ? check : cross} has uppercase
-
+
{hasLowercase ? check : cross} has lowercase
-
+
{equals ? check : cross} equals
diff --git a/apps/login/src/components/password-form.tsx b/apps/login/src/components/password-form.tsx new file mode 100644 index 0000000000..2d623aa09e --- /dev/null +++ b/apps/login/src/components/password-form.tsx @@ -0,0 +1,182 @@ +"use client"; + +import { resetPassword, sendPassword } from "@/lib/server/password"; +import { create } from "@zitadel/client"; +import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { Alert, AlertType } from "./alert"; +import { BackButton } from "./back-button"; +import { Button, ButtonVariants } from "./button"; +import { TextInput } from "./input"; +import { Spinner } from "./spinner"; + +type Inputs = { + password: string; +}; + +type Props = { + loginSettings: LoginSettings | undefined; + loginName: string; + organization?: string; + authRequestId?: string; + isAlternative?: boolean; // whether password was requested as alternative auth method + promptPasswordless?: boolean; +}; + +export function PasswordForm({ + loginSettings, + loginName, + organization, + authRequestId, + promptPasswordless, + isAlternative, +}: Props) { + const t = useTranslations("password"); + + const { register, handleSubmit, formState } = useForm({ + mode: "onBlur", + }); + + const [info, setInfo] = useState(""); + const [error, setError] = useState(""); + + const [loading, setLoading] = useState(false); + + const router = useRouter(); + + async function submitPassword(values: Inputs) { + setError(""); + setLoading(true); + + const response = await sendPassword({ + loginName, + organization, + checks: create(ChecksSchema, { + password: { password: values.password }, + }), + authRequestId, + }) + .catch(() => { + setError("Could not verify password"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (response && "error" in response && response.error) { + setError(response.error); + return; + } + + if (response && "redirect" in response && response.redirect) { + return router.push(response.redirect); + } + } + + async function resetPasswordAndContinue() { + setError(""); + setInfo(""); + setLoading(true); + + const response = await resetPassword({ + loginName, + organization, + authRequestId, + }) + .catch(() => { + setError("Could not reset password"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (response && "error" in response) { + setError(response.error); + return; + } + + setInfo("Password was reset. Please check your email."); + + const params = new URLSearchParams({ + loginName: loginName, + }); + + if (organization) { + params.append("organization", organization); + } + + if (authRequestId) { + params.append("authRequestId", authRequestId); + } + + return router.push("/password/set?" + params); + } + + return ( +
+
+ + {!loginSettings?.hidePasswordReset && ( + + )} + + {loginName && ( + + )} +
+ + {info && ( +
+ {info} +
+ )} + + {error && ( +
+ {error} +
+ )} + +
+ + + +
+
+ ); +} diff --git a/apps/login/src/ui/PrivacyPolicyCheckboxes.tsx b/apps/login/src/components/privacy-policy-checkboxes.tsx similarity index 89% rename from apps/login/src/ui/PrivacyPolicyCheckboxes.tsx rename to apps/login/src/components/privacy-policy-checkboxes.tsx index b09ac1a819..5ac0340bcb 100644 --- a/apps/login/src/ui/PrivacyPolicyCheckboxes.tsx +++ b/apps/login/src/components/privacy-policy-checkboxes.tsx @@ -1,8 +1,9 @@ "use client"; -import React, { useState } from "react"; -import Link from "next/link"; -import { Checkbox } from "./Checkbox"; import { LegalAndSupportSettings } from "@zitadel/proto/zitadel/settings/v2/legal_settings_pb"; +import { useTranslations } from "next-intl"; +import Link from "next/link"; +import { useState } from "react"; +import { Checkbox } from "./checkbox"; type Props = { legal: LegalAndSupportSettings; @@ -15,6 +16,7 @@ type AcceptanceState = { }; export function PrivacyPolicyCheckboxes({ legal, onChange }: Props) { + const t = useTranslations("register"); const [acceptanceState, setAcceptanceState] = useState({ tosAccepted: false, privacyPolicyAccepted: false, @@ -23,7 +25,7 @@ export function PrivacyPolicyCheckboxes({ legal, onChange }: Props) { return ( <>

- To register you must agree to the terms and conditions + {t("agreeTo")} {legal?.helpLink && ( @@ -58,13 +60,13 @@ export function PrivacyPolicyCheckboxes({ legal, onChange }: Props) { }); onChange(checked && acceptanceState.privacyPolicyAccepted); }} + data-testid="privacy-policy-checkbox" />

- Agree  - Terms of Service + {t("termsOfService")}

@@ -83,17 +85,17 @@ export function PrivacyPolicyCheckboxes({ legal, onChange }: Props) { }); onChange(checked && acceptanceState.tosAccepted); }} + data-testid="tos-checkbox" />

- Agree  - Privacy Policy + {t("privacyPolicy")}

diff --git a/apps/login/src/ui/RegisterFormWithoutPassword.tsx b/apps/login/src/components/register-form.tsx similarity index 52% rename from apps/login/src/ui/RegisterFormWithoutPassword.tsx rename to apps/login/src/components/register-form.tsx index b4a8a4c8de..c336b836c4 100644 --- a/apps/login/src/ui/RegisterFormWithoutPassword.tsx +++ b/apps/login/src/components/register-form.tsx @@ -1,19 +1,26 @@ "use client"; -import { useState } from "react"; -import { Button, ButtonVariants } from "./Button"; -import { TextInput } from "./Input"; -import { PrivacyPolicyCheckboxes } from "./PrivacyPolicyCheckboxes"; -import { FieldValues, useForm } from "react-hook-form"; -import { useRouter } from "next/navigation"; -import { Spinner } from "./Spinner"; -import AuthenticationMethodRadio, { - methods, -} from "./AuthenticationMethodRadio"; -import Alert from "./Alert"; -import BackButton from "./BackButton"; +import { registerUser } from "@/lib/server/register"; import { LegalAndSupportSettings } from "@zitadel/proto/zitadel/settings/v2/legal_settings_pb"; -import { first } from "node_modules/cypress/types/lodash"; +import { + LoginSettings, + PasskeysType, +} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { FieldValues, useForm } from "react-hook-form"; +import { Alert } from "./alert"; +import { + AuthenticationMethod, + AuthenticationMethodRadio, + methods, +} from "./authentication-method-radio"; +import { BackButton } from "./back-button"; +import { Button, ButtonVariants } from "./button"; +import { TextInput } from "./input"; +import { PrivacyPolicyCheckboxes } from "./privacy-policy-checkboxes"; +import { Spinner } from "./spinner"; type Inputs = | { @@ -30,16 +37,20 @@ type Props = { email?: string; organization?: string; authRequestId?: string; + loginSettings?: LoginSettings; }; -export default function RegisterFormWithoutPassword({ +export function RegisterForm({ legal, email, firstname, lastname, organization, authRequestId, + loginSettings, }: Props) { + const t = useTranslations("register"); + const { register, handleSubmit, formState } = useForm({ mode: "onBlur", defaultValues: { @@ -50,31 +61,38 @@ export default function RegisterFormWithoutPassword({ }); const [loading, setLoading] = useState(false); - const [selected, setSelected] = useState(methods[0]); + const [selected, setSelected] = useState(methods[0]); const [error, setError] = useState(""); const router = useRouter(); async function submitAndRegister(values: Inputs) { setLoading(true); - const res = await fetch("/api/registeruser", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - email: values.email, - firstName: values.firstname, - lastName: values.lastname, - organization: organization, - }), - }); - setLoading(false); - if (!res.ok) { - const error = await res.json(); - throw new Error(error.details); + const response = await registerUser({ + email: values.email, + firstName: values.firstname, + lastName: values.lastname, + organization: organization, + authRequestId: authRequestId, + }) + .catch(() => { + setError("Could not register user"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (response && "error" in response && response.error) { + setError(response.error); + return; } - return res.json(); + + if (response && "redirect" in response && response.redirect) { + return router.push(response.redirect); + } + + return response; } async function submitAndContinue( @@ -91,34 +109,19 @@ export default function RegisterFormWithoutPassword({ registerParams.authRequestId = authRequestId; } - return withPassword - ? router.push(`/register?` + new URLSearchParams(registerParams)) - : submitAndRegister(value) - .then((session) => { - setError(""); - - const params: any = { loginName: session.factors.user.loginName }; - - if (organization) { - params.organization = organization; - } - - if (authRequestId) { - params.authRequestId = authRequestId; - } - - return router.push(`/passkey/add?` + new URLSearchParams(params)); - }) - .catch((errorDetails: Error) => { - setLoading(false); - setError(errorDetails.message); - }); + // redirect user to /register/password if password is chosen + if (withPassword) { + return router.push( + `/register/password?` + new URLSearchParams(registerParams), + ); + } else { + return submitAndRegister(value); + } } const { errors } = formState; const [tosAndPolicyAccepted, setTosAndPolicyAccepted] = useState(false); - return (
@@ -130,6 +133,7 @@ export default function RegisterFormWithoutPassword({ {...register("firstname", { required: "This field is required" })} label="First name" error={errors.firstname?.message as string} + data-testid="firstname-text-input" />
@@ -140,6 +144,7 @@ export default function RegisterFormWithoutPassword({ {...register("lastname", { required: "This field is required" })} label="Last name" error={errors.lastname?.message as string} + data-testid="lastname-text-input" />
@@ -150,46 +155,53 @@ export default function RegisterFormWithoutPassword({ {...register("email", { required: "This field is required" })} label="E-mail" error={errors.email?.message as string} + data-testid="email-text-input" />
- {legal && ( )} - -

- Select the method you would like to authenticate -

- -
- -
+

{t("selectMethod")}

+ {/* show chooser if both methods are allowed */} + {loginSettings && + loginSettings.allowUsernamePassword && + loginSettings.passkeysType == PasskeysType.ALLOWED && ( +
+ +
+ )} {error && (
{error}
)} -
- +
diff --git a/apps/login/src/components/register-passkey.tsx b/apps/login/src/components/register-passkey.tsx new file mode 100644 index 0000000000..e737168678 --- /dev/null +++ b/apps/login/src/components/register-passkey.tsx @@ -0,0 +1,211 @@ +"use client"; + +import { coerceToArrayBuffer, coerceToBase64Url } from "@/helpers/base64"; +import { + registerPasskeyLink, + verifyPasskeyRegistration, +} from "@/lib/server/passkeys"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { Alert } from "./alert"; +import { BackButton } from "./back-button"; +import { Button, ButtonVariants } from "./button"; +import { Spinner } from "./spinner"; + +type Inputs = {}; + +type Props = { + sessionId: string; + isPrompt: boolean; + authRequestId?: string; + organization?: string; +}; + +export function RegisterPasskey({ + sessionId, + isPrompt, + organization, + authRequestId, +}: Props) { + const t = useTranslations("passkey"); + + const { handleSubmit, formState } = useForm({ + mode: "onBlur", + }); + + const [error, setError] = useState(""); + + const [loading, setLoading] = useState(false); + + const router = useRouter(); + + async function submitVerify( + passkeyId: string, + passkeyName: string, + publicKeyCredential: any, + sessionId: string, + ) { + setLoading(true); + const response = await verifyPasskeyRegistration({ + passkeyId, + passkeyName, + publicKeyCredential, + sessionId, + }) + .catch(() => { + setError("Could not verify Passkey"); + return; + }) + .finally(() => { + setLoading(false); + }); + + return response; + } + + async function submitRegisterAndContinue(): Promise { + setLoading(true); + const resp = await registerPasskeyLink({ + sessionId, + }) + .catch(() => { + setError("Could not register passkey"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (!resp) { + setError("An error on registering passkey"); + return; + } + + const passkeyId = resp.passkeyId; + const options: CredentialCreationOptions = + (resp.publicKeyCredentialCreationOptions as CredentialCreationOptions) ?? + {}; + + if (!options.publicKey) { + setError("An error on registering passkey"); + return; + } + options.publicKey.challenge = coerceToArrayBuffer( + options.publicKey.challenge, + "challenge", + ); + options.publicKey.user.id = coerceToArrayBuffer( + options.publicKey.user.id, + "userid", + ); + if (options.publicKey.excludeCredentials) { + options.publicKey.excludeCredentials.map((cred: any) => { + cred.id = coerceToArrayBuffer( + cred.id as string, + "excludeCredentials.id", + ); + return cred; + }); + } + + const credentials = await navigator.credentials.create(options); + + if ( + !credentials || + !(credentials as any).response?.attestationObject || + !(credentials as any).response?.clientDataJSON || + !(credentials as any).rawId + ) { + setError("An error on registering passkey"); + return; + } + + const attestationObject = (credentials as any).response.attestationObject; + const clientDataJSON = (credentials as any).response.clientDataJSON; + const rawId = (credentials as any).rawId; + + const data = { + id: credentials.id, + rawId: coerceToBase64Url(rawId, "rawId"), + type: credentials.type, + response: { + attestationObject: coerceToBase64Url( + attestationObject, + "attestationObject", + ), + clientDataJSON: coerceToBase64Url(clientDataJSON, "clientDataJSON"), + }, + }; + + const verificationResponse = await submitVerify( + passkeyId, + "", + data, + sessionId, + ); + + if (!verificationResponse) { + setError("Could not verify Passkey!"); + return; + } + + continueAndLogin(); + } + + function continueAndLogin() { + const params = new URLSearchParams(); + + if (organization) { + params.set("organization", organization); + } + + if (authRequestId) { + params.set("authRequestId", authRequestId); + } + + params.set("sessionId", sessionId); + + router.push("/passkey?" + params); + } + + return ( +
+ {error && ( +
+ {error} +
+ )} + +
+ {isPrompt ? ( + + ) : ( + + )} + + + +
+
+ ); +} diff --git a/apps/login/src/components/register-u2f.tsx b/apps/login/src/components/register-u2f.tsx new file mode 100644 index 0000000000..ba83de3627 --- /dev/null +++ b/apps/login/src/components/register-u2f.tsx @@ -0,0 +1,227 @@ +"use client"; + +import { coerceToArrayBuffer, coerceToBase64Url } from "@/helpers/base64"; +import { getNextUrl } from "@/lib/client"; +import { addU2F, verifyU2F } from "@/lib/server/u2f"; +import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { RegisterU2FResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { Alert } from "./alert"; +import { BackButton } from "./back-button"; +import { Button, ButtonVariants } from "./button"; +import { Spinner } from "./spinner"; + +type Props = { + loginName?: string; + sessionId: string; + authRequestId?: string; + organization?: string; + checkAfter: boolean; + loginSettings?: LoginSettings; +}; + +export function RegisterU2f({ + loginName, + sessionId, + organization, + authRequestId, + checkAfter, + loginSettings, +}: Props) { + const t = useTranslations("u2f"); + + const [error, setError] = useState(""); + + const [loading, setLoading] = useState(false); + + const router = useRouter(); + + async function submitVerify( + u2fId: string, + passkeyName: string, + publicKeyCredential: any, + sessionId: string, + ) { + setError(""); + setLoading(true); + const response = await verifyU2F({ + u2fId, + passkeyName, + publicKeyCredential, + sessionId, + }) + .catch(() => { + setError("An error on verifying passkey occurred"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (response && "error" in response && response?.error) { + setError(response?.error); + return; + } + + return response; + } + + async function submitRegisterAndContinue(): Promise { + setError(""); + setLoading(true); + const response = await addU2F({ + sessionId, + }) + .catch(() => { + setError("An error on registering passkey"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (response && "error" in response && response?.error) { + setError(response?.error); + return; + } + + if (!response || !("u2fId" in response)) { + setError("An error on registering passkey"); + return; + } + + const u2fResponse = response as unknown as RegisterU2FResponse; + + const u2fId = u2fResponse.u2fId; + const options: CredentialCreationOptions = + (u2fResponse?.publicKeyCredentialCreationOptions as CredentialCreationOptions) ?? + {}; + + if (options.publicKey) { + options.publicKey.challenge = coerceToArrayBuffer( + options.publicKey.challenge, + "challenge", + ); + options.publicKey.user.id = coerceToArrayBuffer( + options.publicKey.user.id, + "userid", + ); + if (options.publicKey.excludeCredentials) { + options.publicKey.excludeCredentials.map((cred: any) => { + cred.id = coerceToArrayBuffer( + cred.id as string, + "excludeCredentials.id", + ); + return cred; + }); + } + + const resp = await navigator.credentials.create(options); + + if ( + !resp || + !(resp as any).response.attestationObject || + !(resp as any).response.clientDataJSON || + !(resp as any).rawId + ) { + setError("An error on registering passkey"); + return; + } + + const attestationObject = (resp as any).response.attestationObject; + const clientDataJSON = (resp as any).response.clientDataJSON; + const rawId = (resp as any).rawId; + + const data = { + id: resp.id, + rawId: coerceToBase64Url(rawId, "rawId"), + type: resp.type, + response: { + attestationObject: coerceToBase64Url( + attestationObject, + "attestationObject", + ), + clientDataJSON: coerceToBase64Url(clientDataJSON, "clientDataJSON"), + }, + }; + + const submitResponse = await submitVerify(u2fId, "", data, sessionId); + + if (!submitResponse) { + setError("An error on verifying passkey"); + return; + } + + if (checkAfter) { + const paramsToContinue = new URLSearchParams({}); + + if (sessionId) { + paramsToContinue.append("sessionId", sessionId); + } + if (loginName) { + paramsToContinue.append("loginName", loginName); + } + if (organization) { + paramsToContinue.append("organization", organization); + } + if (authRequestId) { + paramsToContinue.append("authRequestId", authRequestId); + } + + return router.push(`/u2f?` + paramsToContinue); + } else { + const url = + authRequestId && sessionId + ? await getNextUrl( + { + sessionId: sessionId, + authRequestId: authRequestId, + organization: organization, + }, + loginSettings?.defaultRedirectUri, + ) + : loginName + ? await getNextUrl( + { + loginName: loginName, + organization: organization, + }, + loginSettings?.defaultRedirectUri, + ) + : null; + if (url) { + return router.push(url); + } + } + } + } + + return ( +
+ {error && ( +
+ {error} +
+ )} + +
+ + + + +
+
+ ); +} diff --git a/apps/login/src/components/self-service-menu.tsx b/apps/login/src/components/self-service-menu.tsx new file mode 100644 index 0000000000..449c1dda1f --- /dev/null +++ b/apps/login/src/components/self-service-menu.tsx @@ -0,0 +1,42 @@ +import Link from "next/link"; + +export function SelfServiceMenu({ sessionId }: { sessionId: string }) { + const list: any[] = []; + + // if (!!config.selfservice.change_password.enabled) { + // list.push({ + // link: + // `/me/change-password?` + + // new URLSearchParams({ + // sessionId: sessionId, + // }), + // name: "Change password", + // }); + // } + + return ( +
+ {list.map((menuitem, index) => { + return ( + + ); + })} +
+ ); +} + +const SelfServiceItem = ({ name, link }: { name: string; link: string }) => { + return ( + + {name} + + ); +}; diff --git a/apps/login/src/components/session-item.tsx b/apps/login/src/components/session-item.tsx new file mode 100644 index 0000000000..c3c28a03fd --- /dev/null +++ b/apps/login/src/components/session-item.tsx @@ -0,0 +1,152 @@ +"use client"; + +import { sendLoginname } from "@/lib/server/loginname"; +import { cleanupSession, continueWithSession } from "@/lib/server/session"; +import { XCircleIcon } from "@heroicons/react/24/outline"; +import { Timestamp, timestampDate } from "@zitadel/client"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import moment from "moment"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { Avatar } from "./avatar"; + +export function isSessionValid(session: Partial): { + valid: boolean; + verifiedAt?: Timestamp; +} { + const validPassword = session?.factors?.password?.verifiedAt; + const validPasskey = session?.factors?.webAuthN?.verifiedAt; + const validIDP = session?.factors?.intent?.verifiedAt; + + const stillValid = session.expirationDate + ? timestampDate(session.expirationDate) > new Date() + : true; + + const verifiedAt = validPassword || validPasskey || validIDP; + const valid = !!((validPassword || validPasskey || validIDP) && stillValid); + + return { valid, verifiedAt }; +} + +export function SessionItem({ + session, + reload, + authRequestId, +}: { + session: Session; + reload: () => void; + authRequestId?: string; +}) { + const [loading, setLoading] = useState(false); + + async function clearSession(id: string) { + setLoading(true); + const response = await cleanupSession({ + sessionId: id, + }) + .catch((error) => { + setError(error.message); + return; + }) + .finally(() => { + setLoading(false); + }); + + return response; + } + + const { valid, verifiedAt } = isSessionValid(session); + + const [error, setError] = useState(null); + + const router = useRouter(); + + return ( + + ); +} diff --git a/apps/login/src/components/sessions-list.tsx b/apps/login/src/components/sessions-list.tsx new file mode 100644 index 0000000000..09393bae72 --- /dev/null +++ b/apps/login/src/components/sessions-list.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { timestampDate } from "@zitadel/client"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { useTranslations } from "next-intl"; +import { useState } from "react"; +import { Alert } from "./alert"; +import { SessionItem } from "./session-item"; + +type Props = { + sessions: Session[]; + authRequestId?: string; +}; + +export function SessionsList({ sessions, authRequestId }: Props) { + const t = useTranslations("accounts"); + const [list, setList] = useState(sessions); + return sessions ? ( +
+ {list + .filter((session) => session?.factors?.user?.loginName) + // sort by change date descending + .sort((a, b) => { + const dateA = a.changeDate + ? timestampDate(a.changeDate).getTime() + : 0; + const dateB = b.changeDate + ? timestampDate(b.changeDate).getTime() + : 0; + return dateB - dateA; + }) + // TODO: add sorting to move invalid sessions to the bottom + .map((session, index) => { + return ( + { + setList(list.filter((s) => s.id !== session.id)); + }} + key={"session-" + index} + /> + ); + })} +
+ ) : ( + {t("noResults")} + ); +} diff --git a/apps/login/src/components/set-password-form.tsx b/apps/login/src/components/set-password-form.tsx new file mode 100644 index 0000000000..d093d1d131 --- /dev/null +++ b/apps/login/src/components/set-password-form.tsx @@ -0,0 +1,288 @@ +"use client"; + +import { + lowerCaseValidator, + numberValidator, + symbolValidator, + upperCaseValidator, +} from "@/helpers/validators"; +import { + changePassword, + resetPassword, + sendPassword, +} from "@/lib/server/password"; +import { create } from "@zitadel/client"; +import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { FieldValues, useForm } from "react-hook-form"; +import { Alert, AlertType } from "./alert"; +import { BackButton } from "./back-button"; +import { Button, ButtonVariants } from "./button"; +import { TextInput } from "./input"; +import { PasswordComplexity } from "./password-complexity"; +import { Spinner } from "./spinner"; + +type Inputs = + | { + code: string; + password: string; + confirmPassword: string; + } + | FieldValues; + +type Props = { + code?: string; + passwordComplexitySettings: PasswordComplexitySettings; + loginName: string; + userId: string; + organization?: string; + authRequestId?: string; + codeRequired: boolean; +}; + +export function SetPasswordForm({ + passwordComplexitySettings, + organization, + authRequestId, + loginName, + userId, + code, + codeRequired, +}: Props) { + const t = useTranslations("password"); + + const { register, handleSubmit, watch, formState } = useForm({ + mode: "onBlur", + defaultValues: { + code: code ?? "", + }, + }); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + const router = useRouter(); + + async function resendCode() { + setError(""); + setLoading(true); + + const response = await resetPassword({ + loginName, + organization, + authRequestId, + }) + .catch(() => { + setError("Could not reset password"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (response && "error" in response) { + setError(response.error); + return; + } + } + + async function submitPassword(values: Inputs) { + setLoading(true); + let payload: { userId: string; password: string; code?: string } = { + userId: userId, + password: values.password, + }; + + // this is not required for initial password setup + if (codeRequired) { + payload = { ...payload, code: values.code }; + } + + const changeResponse = await changePassword(payload) + .catch(() => { + setError("Could not set password"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (changeResponse && "error" in changeResponse) { + setError(changeResponse.error); + return; + } + + if (!changeResponse) { + setError("Could not set password"); + return; + } + + const params = new URLSearchParams({}); + + if (loginName) { + params.append("loginName", loginName); + } + if (organization) { + params.append("organization", organization); + } + + await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait for a second to avoid eventual consistency issues with an initial password being set + + const passwordResponse = await sendPassword({ + loginName, + organization, + checks: create(ChecksSchema, { + password: { password: values.password }, + }), + authRequestId, + }) + .catch(() => { + setError("Could not verify password"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if ( + passwordResponse && + "error" in passwordResponse && + passwordResponse.error + ) { + setError(passwordResponse.error); + return; + } + + if ( + passwordResponse && + "redirect" in passwordResponse && + passwordResponse.redirect + ) { + return router.push(passwordResponse.redirect); + } + + return; + } + + const { errors } = formState; + + const watchPassword = watch("password", ""); + const watchConfirmPassword = watch("confirmPassword", ""); + + const hasMinLength = + passwordComplexitySettings && + watchPassword?.length >= passwordComplexitySettings.minLength; + const hasSymbol = symbolValidator(watchPassword); + const hasNumber = numberValidator(watchPassword); + const hasUppercase = upperCaseValidator(watchPassword); + const hasLowercase = lowerCaseValidator(watchPassword); + + const policyIsValid = + passwordComplexitySettings && + (passwordComplexitySettings.requiresLowercase ? hasLowercase : true) && + (passwordComplexitySettings.requiresNumber ? hasNumber : true) && + (passwordComplexitySettings.requiresUppercase ? hasUppercase : true) && + (passwordComplexitySettings.requiresSymbol ? hasSymbol : true) && + hasMinLength; + + return ( +
+
+ {codeRequired && ( + +
+ + {t("set.noCodeReceived")} + + +
+
+ )} + {codeRequired && ( +
+ +
+ )} +
+ +
+
+ +
+
+ + {passwordComplexitySettings && ( + + )} + + {error && {error}} + +
+ + +
+ + ); +} diff --git a/apps/login/src/ui/SetPasswordForm.tsx b/apps/login/src/components/set-register-password-form.tsx similarity index 68% rename from apps/login/src/ui/SetPasswordForm.tsx rename to apps/login/src/components/set-register-password-form.tsx index 9f2a82c3ff..19bab38e10 100644 --- a/apps/login/src/ui/SetPasswordForm.tsx +++ b/apps/login/src/components/set-register-password-form.tsx @@ -1,20 +1,23 @@ "use client"; -import PasswordComplexity from "./PasswordComplexity"; -import { useState } from "react"; -import { Button, ButtonVariants } from "./Button"; -import { TextInput } from "./Input"; -import { FieldValues, useForm } from "react-hook-form"; import { lowerCaseValidator, numberValidator, symbolValidator, upperCaseValidator, -} from "@/utils/validators"; -import { useRouter } from "next/navigation"; -import { Spinner } from "./Spinner"; -import Alert from "./Alert"; +} from "@/helpers/validators"; +import { registerUser } from "@/lib/server/register"; import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { FieldValues, useForm } from "react-hook-form"; +import { Alert } from "./alert"; +import { BackButton } from "./back-button"; +import { Button, ButtonVariants } from "./button"; +import { TextInput } from "./input"; +import { PasswordComplexity } from "./password-complexity"; +import { Spinner } from "./spinner"; type Inputs = | { @@ -32,7 +35,7 @@ type Props = { authRequestId?: string; }; -export default function SetPasswordForm({ +export function SetRegisterPasswordForm({ passwordComplexitySettings, email, firstname, @@ -40,6 +43,8 @@ export default function SetPasswordForm({ organization, authRequestId, }: Props) { + const t = useTranslations("register"); + const { register, handleSubmit, watch, formState } = useForm({ mode: "onBlur", defaultValues: { @@ -56,52 +61,30 @@ export default function SetPasswordForm({ async function submitRegister(values: Inputs) { setLoading(true); - const res = await fetch("/api/registeruser", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - email: email, - firstName: firstname, - lastName: lastname, - organization: organization, - authRequestId: authRequestId, - password: values.password, - }), - }); - setLoading(false); - if (!res.ok) { - const error = await res.json(); - throw new Error(error.details); - } - return res.json(); - } - - function submitAndLink(value: Inputs): Promise { - return submitRegister(value) - .then((registerResponse) => { - setError(""); - - setLoading(false); - const params: any = { userId: registerResponse.userId }; - - if (authRequestId) { - params.authRequestId = authRequestId; - } - if (organization) { - params.organization = organization; - } - if (registerResponse && registerResponse.sessionId) { - params.sessionId = registerResponse.sessionId; - } - - return router.push(`/verify?` + new URLSearchParams(params)); + const response = await registerUser({ + email: email, + firstName: firstname, + lastName: lastname, + organization: organization, + authRequestId: authRequestId, + password: values.password, + }) + .catch(() => { + setError("Could not register user"); + return; }) - .catch((errorDetails: Error) => { + .finally(() => { setLoading(false); - setError(errorDetails.message); }); + + if (response && "error" in response && response.error) { + setError(response.error); + return; + } + + if (response && "redirect" in response && response.redirect) { + return router.push(response.redirect); + } } const { errors } = formState; @@ -138,6 +121,7 @@ export default function SetPasswordForm({ })} label="Password" error={errors.password?.message as string} + data-testid="password-text-input" />
@@ -150,6 +134,7 @@ export default function SetPasswordForm({ })} label="Confirm Password" error={errors.confirmPassword?.message as string} + data-testid="password-confirm-text-input" />
@@ -165,9 +150,7 @@ export default function SetPasswordForm({ {error && {error}}
- +
diff --git a/apps/login/src/components/sign-in-with-idp.tsx b/apps/login/src/components/sign-in-with-idp.tsx new file mode 100644 index 0000000000..972f501cb1 --- /dev/null +++ b/apps/login/src/components/sign-in-with-idp.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { idpTypeToSlug } from "@/lib/idp"; +import { startIDPFlow } from "@/lib/server/idp"; +import { + IdentityProvider, + IdentityProviderType, +} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { useRouter } from "next/navigation"; +import { ReactNode, useCallback, useState } from "react"; +import { Alert } from "./alert"; +import { SignInWithIdentityProviderProps } from "./idps/base-button"; +import { SignInWithApple } from "./idps/sign-in-with-apple"; +import { SignInWithAzureAd } from "./idps/sign-in-with-azure-ad"; +import { SignInWithGeneric } from "./idps/sign-in-with-generic"; +import { SignInWithGithub } from "./idps/sign-in-with-github"; +import { SignInWithGitlab } from "./idps/sign-in-with-gitlab"; +import { SignInWithGoogle } from "./idps/sign-in-with-google"; + +export interface SignInWithIDPProps { + children?: ReactNode; + identityProviders: IdentityProvider[]; + authRequestId?: string; + organization?: string; + linkOnly?: boolean; +} + +export function SignInWithIdp({ + identityProviders, + authRequestId, + organization, + linkOnly, +}: Readonly) { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const router = useRouter(); + + const startFlow = useCallback( + async (idpId: string, provider: string) => { + setLoading(true); + const params = new URLSearchParams(); + if (linkOnly) params.set("link", "true"); + if (authRequestId) params.set("authRequestId", authRequestId); + if (organization) params.set("organization", organization); + + try { + const response = await startIDPFlow({ + idpId, + successUrl: `/idp/${provider}/success?` + params.toString(), + failureUrl: `/idp/${provider}/failure?` + params.toString(), + }); + + if (response && "error" in response && response?.error) { + setError(response.error); + return; + } + + if (response && "redirect" in response && response?.redirect) { + return router.push(response.redirect); + } + } catch { + setError("Could not start IDP flow"); + } finally { + setLoading(false); + } + }, + [authRequestId, organization, linkOnly, router], + ); + + const renderIDPButton = (idp: IdentityProvider) => { + const { id, name, type } = idp; + const onClick = () => startFlow(id, idpTypeToSlug(type)); + + const components: Partial< + Record< + IdentityProviderType, + (props: SignInWithIdentityProviderProps) => ReactNode + > + > = { + [IdentityProviderType.APPLE]: SignInWithApple, + [IdentityProviderType.OAUTH]: SignInWithGeneric, + [IdentityProviderType.OIDC]: SignInWithGeneric, + [IdentityProviderType.GITHUB]: SignInWithGithub, + [IdentityProviderType.GITHUB_ES]: SignInWithGithub, + [IdentityProviderType.AZURE_AD]: SignInWithAzureAd, + [IdentityProviderType.GOOGLE]: (props) => ( + + ), + [IdentityProviderType.GITLAB]: SignInWithGitlab, + [IdentityProviderType.GITLAB_SELF_HOSTED]: SignInWithGitlab, + }; + + const Component = components[type]; + return Component ? ( + + ) : null; + }; + + return ( +
+ {identityProviders?.map(renderIDPButton)} + {error && ( +
+ {error} +
+ )} +
+ ); +} + +SignInWithIdp.displayName = "SignInWithIDP"; diff --git a/apps/login/src/ui/SkeletonCard.tsx b/apps/login/src/components/skeleton-card.tsx similarity index 95% rename from apps/login/src/ui/SkeletonCard.tsx rename to apps/login/src/components/skeleton-card.tsx index fe40f40830..80b3793e8f 100644 --- a/apps/login/src/ui/SkeletonCard.tsx +++ b/apps/login/src/components/skeleton-card.tsx @@ -1,4 +1,4 @@ -import clsx from "clsx"; +import { clsx } from "clsx"; export const SkeletonCard = ({ isLoading }: { isLoading?: boolean }) => (
+ {children} +
+ ); +} diff --git a/apps/login/src/ui/Spinner.tsx b/apps/login/src/components/spinner.tsx similarity index 100% rename from apps/login/src/ui/Spinner.tsx rename to apps/login/src/components/spinner.tsx diff --git a/apps/login/src/ui/StateBadge.tsx b/apps/login/src/components/state-badge.tsx similarity index 98% rename from apps/login/src/ui/StateBadge.tsx rename to apps/login/src/components/state-badge.tsx index d5b862bfa8..00151390bf 100644 --- a/apps/login/src/ui/StateBadge.tsx +++ b/apps/login/src/components/state-badge.tsx @@ -1,4 +1,4 @@ -import clsx from "clsx"; +import { clsx } from "clsx"; import { ReactNode } from "react"; export enum BadgeState { diff --git a/apps/login/src/ui/TabGroup.tsx b/apps/login/src/components/tab-group.tsx similarity index 89% rename from apps/login/src/ui/TabGroup.tsx rename to apps/login/src/components/tab-group.tsx index 9afd6927f6..afa625d345 100644 --- a/apps/login/src/ui/TabGroup.tsx +++ b/apps/login/src/components/tab-group.tsx @@ -1,4 +1,4 @@ -import { Tab } from "@/ui/Tab"; +import { Tab } from "@/components/tab"; export type Item = { text: string; diff --git a/apps/login/src/ui/Tab.tsx b/apps/login/src/components/tab.tsx similarity index 90% rename from apps/login/src/ui/Tab.tsx rename to apps/login/src/components/tab.tsx index 48980cb28f..bd82931b5a 100644 --- a/apps/login/src/ui/Tab.tsx +++ b/apps/login/src/components/tab.tsx @@ -1,7 +1,7 @@ "use client"; -import type { Item } from "@/ui/TabGroup"; -import clsx from "clsx"; +import type { Item } from "@/components/tab-group"; +import { clsx } from "clsx"; import Link from "next/link"; import { useSelectedLayoutSegment } from "next/navigation"; diff --git a/apps/login/src/ui/ThemeProvider.tsx b/apps/login/src/components/theme-provider.tsx similarity index 78% rename from apps/login/src/ui/ThemeProvider.tsx rename to apps/login/src/components/theme-provider.tsx index ab5eeea64f..a8a72f86a6 100644 --- a/apps/login/src/ui/ThemeProvider.tsx +++ b/apps/login/src/components/theme-provider.tsx @@ -2,7 +2,7 @@ import { ThemeProvider as ThemeP } from "next-themes"; import { ReactNode } from "react"; -export default function ThemeProvider({ children }: { children: ReactNode }) { +export function ThemeProvider({ children }: { children: ReactNode }) { return ( { + useEffect(() => { + setTheme(document, branding); + }, [branding]); + + return
{children}
; +}; diff --git a/apps/login/src/ui/Theme.tsx b/apps/login/src/components/theme.tsx similarity index 84% rename from apps/login/src/ui/Theme.tsx rename to apps/login/src/components/theme.tsx index 126d9403ff..86d39476ff 100644 --- a/apps/login/src/ui/Theme.tsx +++ b/apps/login/src/components/theme.tsx @@ -1,10 +1,10 @@ "use client"; -import React, { useEffect, useState } from "react"; -import { useTheme } from "next-themes"; import { MoonIcon, SunIcon } from "@heroicons/react/24/outline"; +import { useTheme } from "next-themes"; +import { useEffect, useState } from "react"; -function Theme() { +export function Theme() { const { resolvedTheme, setTheme } = useTheme(); const [mounted, setMounted] = useState(false); @@ -21,7 +21,7 @@ function Theme() { return (
@@ -136,9 +144,10 @@ export default function TOTPRegister({ variant={ButtonVariants.Primary} disabled={loading || !formState.isValid} onClick={handleSubmit(continueWithCode)} + data-testid="submit-button" > {loading && } - continue + {t("set.submit")}
diff --git a/apps/login/src/ui/UserAvatar.tsx b/apps/login/src/components/user-avatar.tsx similarity index 87% rename from apps/login/src/ui/UserAvatar.tsx rename to apps/login/src/components/user-avatar.tsx index 61e9a84df7..b7644310eb 100644 --- a/apps/login/src/ui/UserAvatar.tsx +++ b/apps/login/src/components/user-avatar.tsx @@ -1,4 +1,4 @@ -import { Avatar } from "@/ui/Avatar"; +import { Avatar } from "@/components/avatar"; import { ChevronDownIcon } from "@heroicons/react/24/outline"; import Link from "next/link"; @@ -9,7 +9,7 @@ type Props = { searchParams?: Record; }; -export default function UserAvatar({ +export function UserAvatar({ loginName, displayName, showDropdown, @@ -42,7 +42,9 @@ export default function UserAvatar({ loginName={loginName ?? ""} />
- {loginName} + + {loginName} + {showDropdown && ( ({ + mode: "onBlur", + defaultValues: { + loginName: loginName ? loginName : "", + }, + }); + + const router = useRouter(); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + async function submitLoginName(values: Inputs, organization?: string) { + setLoading(true); + + const res = await sendLoginname({ + loginName: values.loginName, + organization, + authRequestId, + }) + .catch(() => { + setError("An internal error occurred"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (res && "redirect" in res && res.redirect) { + return router.push(res.redirect); + } + + if (res && "error" in res && res.error) { + setError(res.error); + return; + } + + return res; + } + + useEffect(() => { + if (submit && loginName) { + // When we navigate to this page, we always want to be redirected if submit is true and the parameters are valid. + submitLoginName({ loginName }, organization); + } + }, []); + + return ( +
+
+ + {allowRegister && ( + + )} +
+ + {error && ( +
+ {error} +
+ )} + +
{children}
+ +
+ + + +
+
+ ); +} diff --git a/apps/login/src/components/verify-form.tsx b/apps/login/src/components/verify-form.tsx new file mode 100644 index 0000000000..6b6189297e --- /dev/null +++ b/apps/login/src/components/verify-form.tsx @@ -0,0 +1,163 @@ +"use client"; + +import { Alert, AlertType } from "@/components/alert"; +import { resendVerification, sendVerification } from "@/lib/server/verify"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; +import { useCallback, useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { BackButton } from "./back-button"; +import { Button, ButtonVariants } from "./button"; +import { TextInput } from "./input"; +import { Spinner } from "./spinner"; + +type Inputs = { + code: string; +}; + +type Props = { + userId: string; + loginName?: string; + organization?: string; + code?: string; + isInvite: boolean; + authRequestId?: string; +}; + +export function VerifyForm({ + userId, + loginName, + organization, + authRequestId, + code, + isInvite, +}: Props) { + const t = useTranslations("verify"); + + const router = useRouter(); + + const { register, handleSubmit, formState } = useForm({ + mode: "onBlur", + defaultValues: { + code: code ?? "", + }, + }); + + const [error, setError] = useState(""); + + const [loading, setLoading] = useState(false); + + async function resendCode() { + setError(""); + setLoading(true); + + const response = await resendVerification({ + userId, + isInvite: isInvite, + }) + .catch(() => { + setError("Could not resend email"); + return; + }) + .finally(() => { + setLoading(false); + }); + + return response; + } + + const fcn = useCallback( + async function submitCodeAndContinue( + value: Inputs, + ): Promise { + setLoading(true); + + const response = await sendVerification({ + code: value.code, + userId, + isInvite: isInvite, + loginName: loginName, + organization: organization, + authRequestId: authRequestId, + }) + .catch(() => { + setError("Could not verify user"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (response && "error" in response && response?.error) { + setError(response.error); + return; + } + + if (response && "redirect" in response && response?.redirect) { + return router.push(response?.redirect); + } + }, + [isInvite, userId], + ); + + useEffect(() => { + if (code) { + fcn({ code }); + } + }, [code, fcn]); + + return ( + <> +
+ +
+ + {t("verify.noCodeReceived")} + + +
+
+
+ +
+ + {error && ( +
+ {error} +
+ )} + +
+ + + +
+
+ + ); +} diff --git a/apps/login/src/components/verify-redirect-button.tsx b/apps/login/src/components/verify-redirect-button.tsx new file mode 100644 index 0000000000..552e787ebc --- /dev/null +++ b/apps/login/src/components/verify-redirect-button.tsx @@ -0,0 +1,90 @@ +"use client"; + +import { + sendVerificationRedirectWithoutCheck, + SendVerificationRedirectWithoutCheckCommand, +} from "@/lib/server/verify"; +import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { useTranslations } from "next-intl"; +import { useState } from "react"; +import { Alert, AlertType } from "./alert"; +import { BackButton } from "./back-button"; +import { Button, ButtonVariants } from "./button"; +import { Spinner } from "./spinner"; + +export function VerifyRedirectButton({ + userId, + loginName, + authRequestId, + authMethods, + organization, +}: { + userId?: string; + loginName?: string; + authRequestId: string; + authMethods: AuthenticationMethodType[] | null; + organization?: string; +}) { + const t = useTranslations("verify"); + const [error, setError] = useState(""); + + const [loading, setLoading] = useState(false); + + async function submitAndContinue(): Promise { + setLoading(true); + + let command = { + organization, + authRequestId, + } as SendVerificationRedirectWithoutCheckCommand; + + if (userId) { + command = { + ...command, + userId, + } as SendVerificationRedirectWithoutCheckCommand; + } else if (loginName) { + command = { + ...command, + loginName, + } as SendVerificationRedirectWithoutCheckCommand; + } + + await sendVerificationRedirectWithoutCheck(command) + .catch(() => { + setError("Could not verify"); + return; + }) + .finally(() => { + setLoading(false); + }); + } + + return ( + <> + {t("success")} + + {error && ( +
+ {error} +
+ )} + +
+ + + {authMethods?.length === 0 && ( + + )} +
+ + ); +} diff --git a/apps/login/src/ui/ZitadelLogoDark.tsx b/apps/login/src/components/zitadel-logo-dark.tsx similarity index 100% rename from apps/login/src/ui/ZitadelLogoDark.tsx rename to apps/login/src/components/zitadel-logo-dark.tsx diff --git a/apps/login/src/ui/ZitadelLogoLight.tsx b/apps/login/src/components/zitadel-logo-light.tsx similarity index 100% rename from apps/login/src/ui/ZitadelLogoLight.tsx rename to apps/login/src/components/zitadel-logo-light.tsx diff --git a/apps/login/src/ui/ZitadelLogo.tsx b/apps/login/src/components/zitadel-logo.tsx similarity index 86% rename from apps/login/src/ui/ZitadelLogo.tsx rename to apps/login/src/components/zitadel-logo.tsx index a260424e93..105665fbba 100644 --- a/apps/login/src/ui/ZitadelLogo.tsx +++ b/apps/login/src/components/zitadel-logo.tsx @@ -1,6 +1,4 @@ import Image from "next/image"; -import { ZitadelLogoDark } from "./ZitadelLogoDark"; -import { ZitadelLogoLight } from "./ZitadelLogoLight"; type Props = { height?: number; width?: number; diff --git a/apps/login/src/utils/base64.ts b/apps/login/src/helpers/base64.ts similarity index 100% rename from apps/login/src/utils/base64.ts rename to apps/login/src/helpers/base64.ts diff --git a/apps/login/src/utils/colors.ts b/apps/login/src/helpers/colors.ts similarity index 98% rename from apps/login/src/utils/colors.ts rename to apps/login/src/helpers/colors.ts index 5d075fe130..bdb07cecfd 100644 --- a/apps/login/src/utils/colors.ts +++ b/apps/login/src/helpers/colors.ts @@ -1,6 +1,5 @@ -import tinycolor from "tinycolor2"; import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb"; -import { PartialMessage } from "@zitadel/client"; +import tinycolor from "tinycolor2"; export interface Color { name: string; @@ -69,10 +68,7 @@ type BrandingColors = { }; }; -export function setTheme( - document: any, - policy?: PartialMessage, -) { +export function setTheme(document: any, policy?: BrandingSettings) { const lP: BrandingColors = { lightTheme: { backgroundColor: policy?.lightTheme?.backgroundColor || BACKGROUND, diff --git a/apps/login/src/utils/validators.ts b/apps/login/src/helpers/validators.ts similarity index 100% rename from apps/login/src/utils/validators.ts rename to apps/login/src/helpers/validators.ts diff --git a/apps/login/src/i18n/request.ts b/apps/login/src/i18n/request.ts new file mode 100644 index 0000000000..59c9da42cc --- /dev/null +++ b/apps/login/src/i18n/request.ts @@ -0,0 +1,35 @@ +import { LANGS, LANGUAGE_COOKIE_NAME, LANGUAGE_HEADER_NAME } from "@/lib/i18n"; +import deepmerge from "deepmerge"; +import { getRequestConfig } from "next-intl/server"; +import { cookies, headers } from "next/headers"; + +export default getRequestConfig(async () => { + const fallback = "en"; + const cookiesList = await cookies(); + + let locale: string = fallback; + + const languageHeader = await (await headers()).get(LANGUAGE_HEADER_NAME); + if (languageHeader) { + const headerLocale = languageHeader.split(",")[0].split("-")[0]; // Extract the language code + if (LANGS.map((l) => l.code).includes(headerLocale)) { + locale = headerLocale; + } + } + + const languageCookie = cookiesList?.get(LANGUAGE_COOKIE_NAME); + if (languageCookie && languageCookie.value) { + if (LANGS.map((l) => l.code).includes(languageCookie.value)) { + locale = languageCookie.value; + } + } + + const userMessages = (await import(`../../locales/${locale}.json`)).default; + const fallbackMessages = (await import(`../../locales/${fallback}.json`)) + .default; + + return { + locale, + messages: deepmerge(fallbackMessages, userMessages), + }; +}); diff --git a/apps/login/src/lib/client.ts b/apps/login/src/lib/client.ts new file mode 100644 index 0000000000..37d22dc83d --- /dev/null +++ b/apps/login/src/lib/client.ts @@ -0,0 +1,43 @@ +type FinishFlowCommand = + | { + sessionId: string; + authRequestId: string; + } + | { loginName: string }; + +/** + * for client: redirects user back to OIDC application or to a success page when using authRequestId, check if a default redirect and redirect to it, or just redirect to a success page with the loginName + * @param command + * @returns + */ +export async function getNextUrl( + command: FinishFlowCommand & { organization?: string }, + defaultRedirectUri?: string, +): Promise { + if ("sessionId" in command && "authRequestId" in command) { + const params = new URLSearchParams({ + sessionId: command.sessionId, + authRequest: command.authRequestId, + }); + + if (command.organization) { + params.append("organization", command.organization); + } + + return `/login?` + params; + } + + if (defaultRedirectUri) { + return defaultRedirectUri; + } + + const params = new URLSearchParams({ + loginName: command.loginName, + }); + + if (command.organization) { + params.append("organization", command.organization); + } + + return `/signedin?` + params; +} diff --git a/apps/login/src/utils/cookies.ts b/apps/login/src/lib/cookies.ts similarity index 53% rename from apps/login/src/utils/cookies.ts rename to apps/login/src/lib/cookies.ts index 526e5de4f1..4d29b9e7d4 100644 --- a/apps/login/src/utils/cookies.ts +++ b/apps/login/src/lib/cookies.ts @@ -1,21 +1,28 @@ "use server"; +import { timestampDate, timestampFromMs } from "@zitadel/client"; import { cookies } from "next/headers"; +import { LANGUAGE_COOKIE_NAME } from "./i18n"; -export type SessionCookie = { +// TODO: improve this to handle overflow +const MAX_COOKIE_SIZE = 2048; + +export type Cookie = { id: string; token: string; loginName: string; organization?: string; - creationDate: string; - expirationDate: string; - changeDate: string; + creationTs: string; + expirationTs: string; + changeTs: string; authRequestId?: string; // if its linked to an OIDC flow }; -function setSessionHttpOnlyCookie(sessions: SessionCookie[]) { - const cookiesList = cookies(); - // @ts-ignore +type SessionCookie = Cookie & T; + +async function setSessionHttpOnlyCookie(sessions: SessionCookie[]) { + const cookiesList = await cookies(); + return cookiesList.set({ name: "sessions", value: JSON.stringify(sessions), @@ -24,14 +31,25 @@ function setSessionHttpOnlyCookie(sessions: SessionCookie[]) { }); } -export async function addSessionToCookie( - session: SessionCookie, +export async function setLanguageCookie(language: string) { + const cookiesList = await cookies(); + + await cookiesList.set({ + name: LANGUAGE_COOKIE_NAME, + value: language, + httpOnly: true, + path: "/", + }); +} + +export async function addSessionToCookie( + session: SessionCookie, cleanup: boolean = false, ): Promise { - const cookiesList = cookies(); + const cookiesList = await cookies(); const stringifiedCookie = cookiesList.get("sessions"); - let currentSessions: SessionCookie[] = stringifiedCookie?.value + let currentSessions: SessionCookie[] = stringifiedCookie?.value ? JSON.parse(stringifiedCookie?.value) : []; @@ -42,13 +60,24 @@ export async function addSessionToCookie( if (index > -1) { currentSessions[index] = session; } else { - currentSessions = [...currentSessions, session]; + const temp = [...currentSessions, session]; + + if (JSON.stringify(temp).length >= MAX_COOKIE_SIZE) { + console.log("WARNING COOKIE OVERFLOW"); + // TODO: improve cookie handling + // this replaces the first session (oldest) with the new one + currentSessions = [session].concat(currentSessions.slice(1)); + } else { + currentSessions = [session].concat(currentSessions); + } } if (cleanup) { const now = new Date(); const filteredSessions = currentSessions.filter((session) => - session.expirationDate ? new Date(session.expirationDate) > now : true, + session.expirationTs + ? timestampDate(timestampFromMs(Number(session.expirationTs))) > now + : true, ); return setSessionHttpOnlyCookie(filteredSessions); } else { @@ -56,15 +85,15 @@ export async function addSessionToCookie( } } -export async function updateSessionCookie( +export async function updateSessionCookie( id: string, - session: SessionCookie, + session: SessionCookie, cleanup: boolean = false, ): Promise { - const cookiesList = cookies(); + const cookiesList = await cookies(); const stringifiedCookie = cookiesList.get("sessions"); - const sessions: SessionCookie[] = stringifiedCookie?.value + const sessions: SessionCookie[] = stringifiedCookie?.value ? JSON.parse(stringifiedCookie?.value) : [session]; @@ -75,25 +104,27 @@ export async function updateSessionCookie( if (cleanup) { const now = new Date(); const filteredSessions = sessions.filter((session) => - session.expirationDate ? new Date(session.expirationDate) > now : true, + session.expirationTs + ? timestampDate(timestampFromMs(Number(session.expirationTs))) > now + : true, ); return setSessionHttpOnlyCookie(filteredSessions); } else { return setSessionHttpOnlyCookie(sessions); } } else { - throw "updateSessionCookie: session id now found"; + throw "updateSessionCookie: session id now found"; } } -export async function removeSessionFromCookie( - session: SessionCookie, +export async function removeSessionFromCookie( + session: SessionCookie, cleanup: boolean = false, ): Promise { - const cookiesList = cookies(); + const cookiesList = await cookies(); const stringifiedCookie = cookiesList.get("sessions"); - const sessions: SessionCookie[] = stringifiedCookie?.value + const sessions: SessionCookie[] = stringifiedCookie?.value ? JSON.parse(stringifiedCookie?.value) : [session]; @@ -101,7 +132,9 @@ export async function removeSessionFromCookie( if (cleanup) { const now = new Date(); const filteredSessions = reducedSessions.filter((session) => - session.expirationDate ? new Date(session.expirationDate) > now : true, + session.expirationTs + ? timestampDate(timestampFromMs(Number(session.expirationTs))) > now + : true, ); return setSessionHttpOnlyCookie(filteredSessions); } else { @@ -109,18 +142,15 @@ export async function removeSessionFromCookie( } } -export async function getMostRecentSessionCookie(): Promise { - const cookiesList = cookies(); +export async function getMostRecentSessionCookie(): Promise { + const cookiesList = await cookies(); const stringifiedCookie = cookiesList.get("sessions"); if (stringifiedCookie?.value) { - const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value); + const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value); const latest = sessions.reduce((prev, current) => { - return new Date(prev.changeDate).getTime() > - new Date(current.changeDate).getTime() - ? prev - : current; + return prev.changeTs > current.changeTs ? prev : current; }); return latest; @@ -129,20 +159,23 @@ export async function getMostRecentSessionCookie(): Promise { } } -export async function getSessionCookieById( - id: string, - organization?: string, -): Promise { - const cookiesList = cookies(); +export async function getSessionCookieById({ + sessionId, + organization, +}: { + sessionId: string; + organization?: string; +}): Promise> { + const cookiesList = await cookies(); const stringifiedCookie = cookiesList.get("sessions"); if (stringifiedCookie?.value) { - const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value); + const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value); const found = sessions.find((s) => organization - ? s.organization === organization && s.id === id - : s.id === id, + ? s.organization === organization && s.id === sessionId + : s.id === sessionId, ); if (found) { return found; @@ -154,15 +187,18 @@ export async function getSessionCookieById( } } -export async function getSessionCookieByLoginName( - loginName: string, - organization?: string, -): Promise { - const cookiesList = cookies(); +export async function getSessionCookieByLoginName({ + loginName, + organization, +}: { + loginName?: string; + organization?: string; +}): Promise> { + const cookiesList = await cookies(); const stringifiedCookie = cookiesList.get("sessions"); if (stringifiedCookie?.value) { - const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value); + const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value); const found = sessions.find((s) => organization ? s.organization === organization && s.loginName === loginName @@ -183,21 +219,21 @@ export async function getSessionCookieByLoginName( * @param cleanup when true, removes all expired sessions, default true * @returns Session Cookies */ -export async function getAllSessionCookieIds( +export async function getAllSessionCookieIds( cleanup: boolean = false, ): Promise { - const cookiesList = cookies(); + const cookiesList = await cookies(); const stringifiedCookie = cookiesList.get("sessions"); if (stringifiedCookie?.value) { - const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value); + const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value); if (cleanup) { const now = new Date(); return sessions .filter((session) => - session.expirationDate - ? new Date(session.expirationDate) > now + session.expirationTs + ? timestampDate(timestampFromMs(Number(session.expirationTs))) > now : true, ) .map((session) => session.id); @@ -214,19 +250,21 @@ export async function getAllSessionCookieIds( * @param cleanup when true, removes all expired sessions, default true * @returns Session Cookies */ -export async function getAllSessions( +export async function getAllSessions( cleanup: boolean = false, -): Promise { - const cookiesList = cookies(); +): Promise[]> { + const cookiesList = await cookies(); const stringifiedCookie = cookiesList.get("sessions"); if (stringifiedCookie?.value) { - const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value); + const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value); if (cleanup) { const now = new Date(); return sessions.filter((session) => - session.expirationDate ? new Date(session.expirationDate) > now : true, + session.expirationTs + ? timestampDate(timestampFromMs(Number(session.expirationTs))) > now + : true, ); } else { return sessions; @@ -238,18 +276,22 @@ export async function getAllSessions( /** * Returns most recent session filtered by optinal loginName - * @param loginName + * @param loginName optional loginName to filter cookies, if non provided, returns most recent session + * @param organization optional organization to filter cookies * @returns most recent session */ -export async function getMostRecentCookieWithLoginname( - loginName?: string, - organization?: string, -): Promise { - const cookiesList = cookies(); +export async function getMostRecentCookieWithLoginname({ + loginName, + organization, +}: { + loginName?: string; + organization?: string; +}): Promise { + const cookiesList = await cookies(); const stringifiedCookie = cookiesList.get("sessions"); if (stringifiedCookie?.value) { - const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value); + const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value); let filtered = sessions.filter((cookie) => { return !!loginName ? cookie.loginName === loginName : true; }); @@ -263,22 +305,16 @@ export async function getMostRecentCookieWithLoginname( const latest = filtered && filtered.length ? filtered.reduce((prev, current) => { - return new Date(prev.changeDate).getTime() > - new Date(current.changeDate).getTime() - ? prev - : current; + return prev.changeTs > current.changeTs ? prev : current; }) : undefined; if (latest) { return latest; } else { - console.error("sessions", sessions, loginName, organization); return Promise.reject("Could not get the context or retrieve a session"); } } else { return Promise.reject("Could not read session cookie"); } } - -export async function clearSessions() {} diff --git a/apps/login/src/lib/demos.ts b/apps/login/src/lib/demos.ts index 464bb57889..38912e50e5 100644 --- a/apps/login/src/lib/demos.ts +++ b/apps/login/src/lib/demos.ts @@ -4,11 +4,6 @@ export type Item = { description?: string; }; -export enum ProviderSlug { - GOOGLE = "google", - GITHUB = "github", -} - export const demos: { name: string; items: Item[] }[] = [ { name: "Login", diff --git a/apps/login/src/lib/i18n.ts b/apps/login/src/lib/i18n.ts new file mode 100644 index 0000000000..bb088a35b5 --- /dev/null +++ b/apps/login/src/lib/i18n.ts @@ -0,0 +1,30 @@ +export interface Lang { + name: string; + code: string; +} + +export const LANGS: Lang[] = [ + { + name: "English", + code: "en", + }, + { + name: "Deutsch", + code: "de", + }, + { + name: "Italiano", + code: "it", + }, + { + name: "Español", + code: "es", + }, + { + name: "简体中文", + code: "zh", + }, +]; + +export const LANGUAGE_COOKIE_NAME = "NEXT_LOCALE"; +export const LANGUAGE_HEADER_NAME = "accept-language"; diff --git a/apps/login/src/lib/idp.ts b/apps/login/src/lib/idp.ts new file mode 100644 index 0000000000..1c021cbfe0 --- /dev/null +++ b/apps/login/src/lib/idp.ts @@ -0,0 +1,262 @@ +import { create } from "@zitadel/client"; +import { IDPType } from "@zitadel/proto/zitadel/idp/v2/idp_pb"; +import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { IDPInformation } from "@zitadel/proto/zitadel/user/v2/idp_pb"; +import { + AddHumanUserRequest, + AddHumanUserRequestSchema, +} from "@zitadel/proto/zitadel/user/v2/user_service_pb"; + +// This maps the IdentityProviderType to a slug which is used in the /success and /failure routes +export function idpTypeToSlug(idpType: IdentityProviderType) { + switch (idpType) { + case IdentityProviderType.GITHUB: + return "github"; + case IdentityProviderType.GITHUB_ES: + return "github_es"; + case IdentityProviderType.GITLAB: + return "gitlab"; + case IdentityProviderType.GITLAB_SELF_HOSTED: + return "gitlab_es"; + case IdentityProviderType.APPLE: + return "apple"; + case IdentityProviderType.GOOGLE: + return "google"; + case IdentityProviderType.AZURE_AD: + return "azure"; + case IdentityProviderType.SAML: + return "saml"; + case IdentityProviderType.OAUTH: + return "oauth"; + case IdentityProviderType.OIDC: + return "oidc"; + default: + throw new Error("Unknown identity provider type"); + } +} + +// TODO: this is ugly but needed atm as the getIDPByID returns a IDPType and not a IdentityProviderType +export function idpTypeToIdentityProviderType( + idpType: IDPType, +): IdentityProviderType { + switch (idpType) { + case IDPType.IDP_TYPE_GITHUB: + return IdentityProviderType.GITHUB; + + case IDPType.IDP_TYPE_GITHUB_ES: + return IdentityProviderType.GITHUB_ES; + + case IDPType.IDP_TYPE_GITLAB: + return IdentityProviderType.GITLAB; + + case IDPType.IDP_TYPE_GITLAB_SELF_HOSTED: + return IdentityProviderType.GITLAB_SELF_HOSTED; + + case IDPType.IDP_TYPE_APPLE: + return IdentityProviderType.APPLE; + + case IDPType.IDP_TYPE_GOOGLE: + return IdentityProviderType.GOOGLE; + + case IDPType.IDP_TYPE_AZURE_AD: + return IdentityProviderType.AZURE_AD; + + case IDPType.IDP_TYPE_SAML: + return IdentityProviderType.SAML; + + case IDPType.IDP_TYPE_OAUTH: + return IdentityProviderType.OAUTH; + + case IDPType.IDP_TYPE_OIDC: + return IdentityProviderType.OIDC; + + default: + throw new Error("Unknown identity provider type"); + } +} +// this maps the IDPInformation to the AddHumanUserRequest which is used when creating a user or linking a user (email) +// TODO: extend this object from a other file which can be overwritten by customers like map = { ...PROVIDER_MAPPING, ...customerMap } +export type OIDC_USER = { + User: { + email: string; + name?: string; + given_name?: string; + family_name?: string; + }; +}; + +const GITLAB_MAPPING = (idp: IDPInformation) => { + const rawInfo = idp.rawInformation as { + name: string; + email: string; + email_verified: boolean; + }; + + return create(AddHumanUserRequestSchema, { + username: idp.userName, + email: { + email: rawInfo.email, + verification: { case: "isVerified", value: rawInfo.email_verified }, + }, + profile: { + displayName: rawInfo.name || idp.userName || "", + givenName: "", + familyName: "", + }, + idpLinks: [ + { + idpId: idp.idpId, + userId: idp.userId, + userName: idp.userName, + }, + ], + }); +}; + +const OIDC_MAPPING = (idp: IDPInformation) => { + const rawInfo = idp.rawInformation as OIDC_USER; + + return create(AddHumanUserRequestSchema, { + username: idp.userName, + email: { + email: rawInfo.User?.email, + verification: { case: "isVerified", value: true }, + }, + profile: { + displayName: rawInfo.User?.name ?? "", + givenName: rawInfo.User?.given_name ?? "", + familyName: rawInfo.User?.family_name ?? "", + }, + idpLinks: [ + { + idpId: idp.idpId, + userId: idp.userId, + userName: idp.userName, + }, + ], + }); +}; + +const GITHUB_MAPPING = (idp: IDPInformation) => { + const rawInfo = idp.rawInformation as { + email: string; + name: string; + }; + + return create(AddHumanUserRequestSchema, { + username: idp.userName, + email: { + email: rawInfo.email, + verification: { case: "isVerified", value: true }, + }, + profile: { + displayName: rawInfo.name ?? "", + givenName: rawInfo.name ?? "", + familyName: rawInfo.name ?? "", + }, + idpLinks: [ + { + idpId: idp.idpId, + userId: idp.userId, + userName: idp.userName, + }, + ], + }); +}; + +export const PROVIDER_MAPPING: { + [provider: number]: (rI: IDPInformation) => AddHumanUserRequest; +} = { + [IdentityProviderType.GOOGLE]: (idp: IDPInformation) => { + const rawInfo = idp.rawInformation as OIDC_USER; + + return create(AddHumanUserRequestSchema, { + username: idp.userName, + email: { + email: rawInfo.User?.email, + verification: { case: "isVerified", value: true }, + }, + profile: { + displayName: rawInfo.User?.name ?? "", + givenName: rawInfo.User?.given_name ?? "", + familyName: rawInfo.User?.family_name ?? "", + }, + idpLinks: [ + { + idpId: idp.idpId, + userId: idp.userId, + userName: idp.userName, + }, + ], + }); + }, + [IdentityProviderType.GITLAB]: GITLAB_MAPPING, + [IdentityProviderType.GITLAB_SELF_HOSTED]: GITLAB_MAPPING, + [IdentityProviderType.OIDC]: OIDC_MAPPING, + // check + [IdentityProviderType.OAUTH]: OIDC_MAPPING, + [IdentityProviderType.AZURE_AD]: (idp: IDPInformation) => { + const rawInfo = idp.rawInformation as { + jobTitle: string; + mail: string; + mobilePhone: string; + preferredLanguage: string; + id: string; + displayName?: string; + givenName?: string; + surname?: string; + officeLocation?: string; + userPrincipalName: string; + }; + + return create(AddHumanUserRequestSchema, { + username: idp.userName, + email: { + email: rawInfo.mail || rawInfo.userPrincipalName || "", + verification: { case: "isVerified", value: true }, + }, + profile: { + displayName: rawInfo.displayName ?? "", + givenName: rawInfo.givenName ?? "", + familyName: rawInfo.surname ?? "", + }, + idpLinks: [ + { + idpId: idp.idpId, + userId: idp.userId, + userName: idp.userName, + }, + ], + }); + }, + [IdentityProviderType.GITHUB]: GITHUB_MAPPING, + [IdentityProviderType.GITHUB_ES]: GITHUB_MAPPING, + [IdentityProviderType.APPLE]: (idp: IDPInformation) => { + const rawInfo = idp.rawInformation as { + name?: string; + firstName?: string; + lastName?: string; + email?: string; + }; + + return create(AddHumanUserRequestSchema, { + username: idp.userName, + email: { + email: rawInfo.email ?? "", + verification: { case: "isVerified", value: true }, + }, + profile: { + displayName: rawInfo.name ?? "", + givenName: rawInfo.firstName ?? "", + familyName: rawInfo.lastName ?? "", + }, + idpLinks: [ + { + idpId: idp.idpId, + userId: idp.userId, + userName: idp.userName, + }, + ], + }); + }, +}; diff --git a/apps/login/src/lib/self.ts b/apps/login/src/lib/self.ts new file mode 100644 index 0000000000..0328adfaff --- /dev/null +++ b/apps/login/src/lib/self.ts @@ -0,0 +1,56 @@ +"use server"; + +import { createServerTransport } from "@zitadel/client/node"; +import { createUserServiceClient } from "@zitadel/client/v2"; +import { getSessionCookieById } from "./cookies"; +import { getSession } from "./zitadel"; + +const transport = (token: string) => + createServerTransport(token, { + baseUrl: process.env.ZITADEL_API_URL!, + }); + +const myUserService = (sessionToken: string) => { + return createUserServiceClient(transport(sessionToken)); +}; + +export async function setMyPassword({ + sessionId, + password, +}: { + sessionId: string; + password: string; +}) { + const sessionCookie = await getSessionCookieById({ sessionId }); + + const { session } = await getSession({ + sessionId: sessionCookie.id, + sessionToken: sessionCookie.token, + }); + + if (!session) { + return { error: "Could not load session" }; + } + + const service = await myUserService(`${sessionCookie.token}`); + + if (!session?.factors?.user?.id) { + return { error: "No user id found in session" }; + } + + return service + .setPassword( + { + userId: session.factors.user.id, + newPassword: { password, changeRequired: false }, + }, + {}, + ) + .catch((error) => { + console.log(error); + if (error.code === 7) { + return { error: "Session is not valid." }; + } + throw error; + }); +} diff --git a/apps/login/src/lib/server-actions.ts b/apps/login/src/lib/server-actions.ts index 612f5f059c..ce0726075f 100644 --- a/apps/login/src/lib/server-actions.ts +++ b/apps/login/src/lib/server-actions.ts @@ -1,24 +1,21 @@ "use server"; -import { getMostRecentCookieWithLoginname } from "@/utils/cookies"; -import { getSession, verifyTOTPRegistration } from "./zitadel"; +import { loadMostRecentSession } from "./session"; +import { verifyTOTPRegistration } from "./zitadel"; export async function verifyTOTP( code: string, loginName?: string, organization?: string, ) { - return getMostRecentCookieWithLoginname(loginName, organization) - .then((recent) => { - return getSession(recent.id, recent.token).then((response) => { - return { session: response?.session, token: recent.token }; - }); - }) - .then(({ session, token }) => { - if (session?.factors?.user?.id) { - return verifyTOTPRegistration(code, session.factors.user.id, token); - } else { - throw Error("No user id found in session."); - } - }); + return loadMostRecentSession({ + loginName, + organization, + }).then((session) => { + if (session?.factors?.user?.id) { + return verifyTOTPRegistration(code, session.factors.user.id); + } else { + throw Error("No user id found in session."); + } + }); } diff --git a/apps/login/src/lib/server/cookie.ts b/apps/login/src/lib/server/cookie.ts new file mode 100644 index 0000000000..91447174f6 --- /dev/null +++ b/apps/login/src/lib/server/cookie.ts @@ -0,0 +1,204 @@ +"use server"; + +import { addSessionToCookie, updateSessionCookie } from "@/lib/cookies"; +import { + createSessionForUserIdAndIdpIntent, + createSessionFromChecks, + getSession, + setSession, +} from "@/lib/zitadel"; +import { Duration, timestampMs } from "@zitadel/client"; +import { + Challenges, + RequestChallenges, +} from "@zitadel/proto/zitadel/session/v2/challenge_pb"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; + +type CustomCookieData = { + id: string; + token: string; + loginName: string; + organization?: string; + creationTs: string; + expirationTs: string; + changeTs: string; + authRequestId?: string; // if its linked to an OIDC flow +}; + +export async function createSessionAndUpdateCookie( + checks: Checks, + challenges: RequestChallenges | undefined, + authRequestId: string | undefined, + lifetime?: Duration, +): Promise { + const createdSession = await createSessionFromChecks(checks, challenges); + + if (createdSession) { + return getSession({ + sessionId: createdSession.sessionId, + sessionToken: createdSession.sessionToken, + }).then((response) => { + if (response?.session && response.session?.factors?.user?.loginName) { + const sessionCookie: CustomCookieData = { + id: createdSession.sessionId, + token: createdSession.sessionToken, + creationTs: response.session.creationDate + ? `${timestampMs(response.session.creationDate)}` + : "", + expirationTs: response.session.expirationDate + ? `${timestampMs(response.session.expirationDate)}` + : "", + changeTs: response.session.changeDate + ? `${timestampMs(response.session.changeDate)}` + : "", + loginName: response.session.factors.user.loginName ?? "", + }; + + if (authRequestId) { + sessionCookie.authRequestId = authRequestId; + } + + if (response.session.factors.user.organizationId) { + sessionCookie.organization = + response.session.factors.user.organizationId; + } + + return addSessionToCookie(sessionCookie).then(() => { + return response.session as Session; + }); + } else { + throw "could not get session or session does not have loginName"; + } + }); + } else { + throw "Could not create session"; + } +} + +export async function createSessionForIdpAndUpdateCookie( + userId: string, + idpIntent: { + idpIntentId?: string | undefined; + idpIntentToken?: string | undefined; + }, + authRequestId: string | undefined, + lifetime?: Duration, +): Promise { + const createdSession = await createSessionForUserIdAndIdpIntent( + userId, + idpIntent, + lifetime, + ); + + if (!createdSession) { + throw "Could not create session"; + } + + const { session } = await getSession({ + sessionId: createdSession.sessionId, + sessionToken: createdSession.sessionToken, + }); + + if (!session || !session.factors?.user?.loginName) { + throw "Could not retrieve session"; + } + + const sessionCookie: CustomCookieData = { + id: createdSession.sessionId, + token: createdSession.sessionToken, + creationTs: session.creationDate + ? `${timestampMs(session.creationDate)}` + : "", + expirationTs: session.expirationDate + ? `${timestampMs(session.expirationDate)}` + : "", + changeTs: session.changeDate ? `${timestampMs(session.changeDate)}` : "", + loginName: session.factors.user.loginName ?? "", + organization: session.factors.user.organizationId ?? "", + }; + + if (authRequestId) { + sessionCookie.authRequestId = authRequestId; + } + + if (session.factors.user.organizationId) { + sessionCookie.organization = session.factors.user.organizationId; + } + + return addSessionToCookie(sessionCookie).then(() => { + return session as Session; + }); +} + +export type SessionWithChallenges = Session & { + challenges: Challenges | undefined; +}; + +export async function setSessionAndUpdateCookie( + recentCookie: CustomCookieData, + checks?: Checks, + challenges?: RequestChallenges, + authRequestId?: string, + lifetime?: Duration, +) { + return setSession( + recentCookie.id, + recentCookie.token, + challenges, + checks, + lifetime, + ).then((updatedSession) => { + if (updatedSession) { + const sessionCookie: CustomCookieData = { + id: recentCookie.id, + token: updatedSession.sessionToken, + creationTs: recentCookie.creationTs, + expirationTs: recentCookie.expirationTs, + // just overwrite the changeDate with the new one + changeTs: updatedSession.details?.changeDate + ? `${timestampMs(updatedSession.details.changeDate)}` + : "", + loginName: recentCookie.loginName, + organization: recentCookie.organization, + }; + + if (authRequestId) { + sessionCookie.authRequestId = authRequestId; + } + + return getSession({ + sessionId: sessionCookie.id, + sessionToken: sessionCookie.token, + }).then((response) => { + if (response?.session && response.session.factors?.user?.loginName) { + const { session } = response; + const newCookie: CustomCookieData = { + id: sessionCookie.id, + token: updatedSession.sessionToken, + creationTs: sessionCookie.creationTs, + expirationTs: sessionCookie.expirationTs, + // just overwrite the changeDate with the new one + changeTs: updatedSession.details?.changeDate + ? `${timestampMs(updatedSession.details.changeDate)}` + : "", + loginName: session.factors?.user?.loginName ?? "", + organization: session.factors?.user?.organizationId ?? "", + }; + + if (sessionCookie.authRequestId) { + newCookie.authRequestId = sessionCookie.authRequestId; + } + + return updateSessionCookie(sessionCookie.id, newCookie).then(() => { + return { challenges: updatedSession.challenges, ...session }; + }); + } else { + throw "could not get session or session does not have loginName"; + } + }); + } else { + throw "Session not be set"; + } + }); +} diff --git a/apps/login/src/lib/server/idp.ts b/apps/login/src/lib/server/idp.ts new file mode 100644 index 0000000000..b48f796160 --- /dev/null +++ b/apps/login/src/lib/server/idp.ts @@ -0,0 +1,123 @@ +"use server"; + +import { + getLoginSettings, + getUserByID, + startIdentityProviderFlow, +} from "@/lib/zitadel"; +import { headers } from "next/headers"; +import { getNextUrl } from "../client"; +import { checkEmailVerification } from "../verify-helper"; +import { createSessionForIdpAndUpdateCookie } from "./cookie"; + +export type StartIDPFlowCommand = { + idpId: string; + successUrl: string; + failureUrl: string; +}; + +export async function startIDPFlow(command: StartIDPFlowCommand) { + const host = (await headers()).get("host"); + + if (!host) { + return { error: "Could not get host" }; + } + + return startIdentityProviderFlow({ + idpId: command.idpId, + urls: { + successUrl: `${host.includes("localhost") ? "http://" : "https://"}${host}${command.successUrl}`, + failureUrl: `${host.includes("localhost") ? "http://" : "https://"}${host}${command.failureUrl}`, + }, + }).then((response) => { + if ( + response && + response.nextStep.case === "authUrl" && + response?.nextStep.value + ) { + return { redirect: response.nextStep.value }; + } + }); +} + +type CreateNewSessionCommand = { + userId: string; + idpIntent: { + idpIntentId: string; + idpIntentToken: string; + }; + loginName?: string; + password?: string; + organization?: string; + authRequestId?: string; +}; + +export async function createNewSessionFromIdpIntent( + command: CreateNewSessionCommand, +) { + if (!command.userId || !command.idpIntent) { + throw new Error("No userId or loginName provided"); + } + + const userResponse = await getUserByID(command.userId); + + if (!userResponse || !userResponse.user) { + return { error: "User not found in the system" }; + } + + const loginSettings = await getLoginSettings( + userResponse.user.details?.resourceOwner, + ); + + const session = await createSessionForIdpAndUpdateCookie( + command.userId, + command.idpIntent, + command.authRequestId, + loginSettings?.externalLoginCheckLifetime, + ); + + if (!session || !session.factors?.user) { + return { error: "Could not create session" }; + } + + const humanUser = + userResponse.user.type.case === "human" + ? userResponse.user.type.value + : undefined; + + // check to see if user was verified + const emailVerificationCheck = checkEmailVerification( + session, + humanUser, + command.organization, + command.authRequestId, + ); + + if (emailVerificationCheck?.redirect) { + return emailVerificationCheck; + } + + // TODO: check if user has MFA methods + // const mfaFactorCheck = checkMFAFactors(session, loginSettings, authMethods, organization, authRequestId); + // if (mfaFactorCheck?.redirect) { + // return mfaFactorCheck; + // } + + const url = await getNextUrl( + command.authRequestId && session.id + ? { + sessionId: session.id, + authRequestId: command.authRequestId, + organization: session.factors.user.organizationId, + } + : { + loginName: session.factors.user.loginName, + organization: session.factors.user.organizationId, + }, + loginSettings?.defaultRedirectUri, + ); + + if (url) { + return { redirect: url }; + } +} diff --git a/apps/login/src/lib/server/invite.ts b/apps/login/src/lib/server/invite.ts new file mode 100644 index 0000000000..3c68587898 --- /dev/null +++ b/apps/login/src/lib/server/invite.ts @@ -0,0 +1,44 @@ +"use server"; + +import { addHumanUser, createInviteCode } from "@/lib/zitadel"; +import { Factors } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { headers } from "next/headers"; + +type InviteUserCommand = { + email: string; + firstName: string; + lastName: string; + password?: string; + organization?: string; + authRequestId?: string; +}; + +export type RegisterUserResponse = { + userId: string; + sessionId: string; + factors: Factors | undefined; +}; + +export async function inviteUser(command: InviteUserCommand) { + const host = (await headers()).get("host"); + + const human = await addHumanUser({ + email: command.email, + firstName: command.firstName, + lastName: command.lastName, + password: command.password ? command.password : undefined, + organization: command.organization, + }); + + if (!human) { + return { error: "Could not create user" }; + } + + const codeResponse = await createInviteCode(human.userId, host); + + if (!codeResponse || !human) { + return { error: "Could not create invite code" }; + } + + return human.userId; +} diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts new file mode 100644 index 0000000000..f33e3577d0 --- /dev/null +++ b/apps/login/src/lib/server/loginname.ts @@ -0,0 +1,375 @@ +"use server"; + +import { create } from "@zitadel/client"; +import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { headers } from "next/headers"; +import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp"; + +import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; +import { checkInvite } from "../verify-helper"; +import { + getActiveIdentityProviders, + getIDPByID, + getLoginSettings, + getOrgsByDomain, + listAuthenticationMethodTypes, + listIDPLinks, + listUsers, + startIdentityProviderFlow, +} from "../zitadel"; +import { createSessionAndUpdateCookie } from "./cookie"; + +export type SendLoginnameCommand = { + loginName: string; + authRequestId?: string; + organization?: string; +}; + +const ORG_SUFFIX_REGEX = /(?<=@)(.+)/; + +export async function sendLoginname(command: SendLoginnameCommand) { + const users = await listUsers({ + loginName: command.loginName, + organizationId: command.organization, + }); + + const loginSettings = await getLoginSettings(command.organization); + + const potentialUsers = users.result.filter((u) => { + const human = u.type.case === "human" ? u.type.value : undefined; + return loginSettings?.disableLoginWithEmail + ? human?.email?.isVerified && human?.email?.email !== command.loginName + : loginSettings?.disableLoginWithPhone + ? human?.phone?.isVerified && human?.phone?.phone !== command.loginName + : true; + }); + + const redirectUserToSingleIDPIfAvailable = async () => { + const identityProviders = await getActiveIdentityProviders( + command.organization, + ).then((resp) => { + return resp.identityProviders; + }); + + if (identityProviders.length === 1) { + const host = (await headers()).get("host"); + + if (!host) { + return { error: "Could not get host" }; + } + + const identityProviderType = identityProviders[0].type; + + const provider = idpTypeToSlug(identityProviderType); + + const params = new URLSearchParams(); + + if (command.authRequestId) { + params.set("authRequestId", command.authRequestId); + } + + if (command.organization) { + params.set("organization", command.organization); + } + + const resp = await startIdentityProviderFlow({ + idpId: identityProviders[0].id, + urls: { + successUrl: + `${host.includes("localhost") ? "http://" : "https://"}${host}/idp/${provider}/success?` + + new URLSearchParams(params), + failureUrl: + `${host.includes("localhost") ? "http://" : "https://"}${host}/idp/${provider}/failure?` + + new URLSearchParams(params), + }, + }); + + if (resp?.nextStep.case === "authUrl") { + return { redirect: resp.nextStep.value }; + } + } + }; + + const redirectUserToIDP = async (userId: string) => { + const identityProviders = await listIDPLinks(userId).then((resp) => { + return resp.result; + }); + + if (identityProviders.length === 1) { + const host = (await headers()).get("host"); + + if (!host) { + return { error: "Could not get host" }; + } + + const identityProviderId = identityProviders[0].idpId; + + const idp = await getIDPByID(identityProviderId); + + const idpType = idp?.type; + + if (!idp || !idpType) { + throw new Error("Could not find identity provider"); + } + + const identityProviderType = idpTypeToIdentityProviderType(idpType); + const provider = idpTypeToSlug(identityProviderType); + + const params = new URLSearchParams(); + + if (command.authRequestId) { + params.set("authRequestId", command.authRequestId); + } + + if (command.organization) { + params.set("organization", command.organization); + } + + const resp = await startIdentityProviderFlow({ + idpId: idp.id, + urls: { + successUrl: + `${host.includes("localhost") ? "http://" : "https://"}${host}/idp/${provider}/success?` + + new URLSearchParams(params), + failureUrl: + `${host.includes("localhost") ? "http://" : "https://"}${host}/idp/${provider}/failure?` + + new URLSearchParams(params), + }, + }); + + if (resp?.nextStep.case === "authUrl") { + return { redirect: resp.nextStep.value }; + } + } + }; + + if (potentialUsers.length == 1 && potentialUsers[0].userId) { + const userId = potentialUsers[0].userId; + + const checks = create(ChecksSchema, { + user: { search: { case: "userId", value: userId } }, + }); + + const session = await createSessionAndUpdateCookie( + checks, + undefined, + command.authRequestId, + ); + + if (!session.factors?.user?.id) { + return { error: "Could not create session for user" }; + } + + // TODO: check if handling of userstate INITIAL is needed + if (potentialUsers[0].state === UserState.INITIAL) { + return { error: "Initial User not supported" }; + } + + const methods = await listAuthenticationMethodTypes( + session.factors?.user?.id, + ); + + // this can be expected to be an invite as users created in console have a password set. + if (!methods.authMethodTypes || !methods.authMethodTypes.length) { + const humanUser = + potentialUsers[0].type.case === "human" + ? potentialUsers[0].type.value + : undefined; + + // redirect to /verify invite if no auth method is set and email is not verified + const inviteCheck = checkInvite( + session, + humanUser, + session.factors.user.organizationId, + command.authRequestId, + ); + + if (inviteCheck?.redirect) { + return inviteCheck; + } + + const paramsAuthenticatorSetup = new URLSearchParams({ + loginName: session.factors?.user?.loginName, + userId: session.factors?.user?.id, // verify needs user id + }); + + if (command.organization || session.factors?.user?.organizationId) { + paramsAuthenticatorSetup.append( + "organization", + command.organization ?? session.factors?.user?.organizationId, + ); + } + + if (command.authRequestId) { + paramsAuthenticatorSetup.append("authRequestId", command.authRequestId); + } + + return { redirect: "/authenticator/set?" + paramsAuthenticatorSetup }; + } + + if (methods.authMethodTypes.length == 1) { + const method = methods.authMethodTypes[0]; + switch (method) { + case AuthenticationMethodType.PASSWORD: // user has only password as auth method + if (!loginSettings?.allowUsernamePassword) { + return { + error: + "Username Password not allowed! Contact your administrator for more information.", + }; + } + + const paramsPassword: any = { + loginName: session.factors?.user?.loginName, + }; + + // TODO: does this have to be checked in loginSettings.allowDomainDiscovery + + if (command.organization || session.factors?.user?.organizationId) { + paramsPassword.organization = + command.organization ?? session.factors?.user?.organizationId; + } + + if (command.authRequestId) { + paramsPassword.authRequestId = command.authRequestId; + } + + return { + redirect: "/password?" + new URLSearchParams(paramsPassword), + }; + + case AuthenticationMethodType.PASSKEY: // AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSKEY + if (loginSettings?.passkeysType === PasskeysType.NOT_ALLOWED) { + return { + error: + "Passkeys not allowed! Contact your administrator for more information.", + }; + } + + const paramsPasskey: any = { loginName: command.loginName }; + if (command.authRequestId) { + paramsPasskey.authRequestId = command.authRequestId; + } + + if (command.organization || session.factors?.user?.organizationId) { + paramsPasskey.organization = + command.organization ?? session.factors?.user?.organizationId; + } + + return { redirect: "/passkey?" + new URLSearchParams(paramsPasskey) }; + } + } else { + // prefer passkey in favor of other methods + if (methods.authMethodTypes.includes(AuthenticationMethodType.PASSKEY)) { + const passkeyParams: any = { + loginName: command.loginName, + altPassword: `${methods.authMethodTypes.includes(1)}`, // show alternative password option + }; + + if (command.authRequestId) { + passkeyParams.authRequestId = command.authRequestId; + } + + if (command.organization || session.factors?.user?.organizationId) { + passkeyParams.organization = + command.organization ?? session.factors?.user?.organizationId; + } + + return { redirect: "/passkey?" + new URLSearchParams(passkeyParams) }; + } else if ( + methods.authMethodTypes.includes(AuthenticationMethodType.IDP) + ) { + return redirectUserToIDP(userId); + } else if ( + methods.authMethodTypes.includes(AuthenticationMethodType.PASSWORD) + ) { + // user has no passkey setup and login settings allow passkeys + const paramsPasswordDefault: any = { loginName: command.loginName }; + + if (command.authRequestId) { + paramsPasswordDefault.authRequestId = command.authRequestId; + } + + if (command.organization || session.factors?.user?.organizationId) { + paramsPasswordDefault.organization = + command.organization ?? session.factors?.user?.organizationId; + } + + return { + redirect: "/password?" + new URLSearchParams(paramsPasswordDefault), + }; + } + } + } + + // user not found, check if register is enabled on organization + if (loginSettings?.allowRegister && !loginSettings?.allowUsernamePassword) { + // TODO: do we need to handle login hints for IDPs here? + const resp = await redirectUserToSingleIDPIfAvailable(); + if (resp) { + return resp; + } + return { error: "User not found in the system" }; + } else if ( + loginSettings?.allowRegister && + loginSettings?.allowUsernamePassword + ) { + let orgToRegisterOn: string | undefined = command.organization; + + if ( + !loginSettings?.ignoreUnknownUsernames && + !orgToRegisterOn && + command.loginName && + ORG_SUFFIX_REGEX.test(command.loginName) + ) { + const matched = ORG_SUFFIX_REGEX.exec(command.loginName); + const suffix = matched?.[1] ?? ""; + + // this just returns orgs where the suffix is set as primary domain + const orgs = await getOrgsByDomain(suffix); + const orgToCheckForDiscovery = + orgs.result && orgs.result.length === 1 ? orgs.result[0].id : undefined; + + const orgLoginSettings = await getLoginSettings(orgToCheckForDiscovery); + if (orgLoginSettings?.allowDomainDiscovery) { + orgToRegisterOn = orgToCheckForDiscovery; + } + } + + // do not register user if ignoreUnknownUsernames is set + if (orgToRegisterOn && !loginSettings?.ignoreUnknownUsernames) { + const params = new URLSearchParams({ organization: orgToRegisterOn }); + + if (command.authRequestId) { + params.set("authRequestId", command.authRequestId); + } + + if (command.loginName) { + params.set("email", command.loginName); + } + + return { redirect: "/register?" + params }; + } + } + + if (loginSettings?.ignoreUnknownUsernames) { + const paramsPasswordDefault = new URLSearchParams({ + loginName: command.loginName, + }); + + if (command.authRequestId) { + paramsPasswordDefault.append("authRequestId", command.authRequestId); + } + + if (command.organization) { + paramsPasswordDefault.append("organization", command.organization); + } + + return { redirect: "/password?" + paramsPasswordDefault }; + } + + // fallbackToPassword + + return { error: "User not found in the system" }; +} diff --git a/apps/login/src/lib/server/otp.ts b/apps/login/src/lib/server/otp.ts new file mode 100644 index 0000000000..b91d8eac7c --- /dev/null +++ b/apps/login/src/lib/server/otp.ts @@ -0,0 +1,75 @@ +"use server"; + +import { setSessionAndUpdateCookie } from "@/lib/server/cookie"; +import { create } from "@zitadel/client"; +import { + CheckOTPSchema, + ChecksSchema, + CheckTOTPSchema, +} from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { + getMostRecentSessionCookie, + getSessionCookieById, + getSessionCookieByLoginName, +} from "../cookies"; +import { getLoginSettings } from "../zitadel"; + +export type SetOTPCommand = { + loginName?: string; + sessionId?: string; + organization?: string; + authRequestId?: string; + code: string; + method: string; +}; + +export async function setOTP(command: SetOTPCommand) { + const recentSession = command.sessionId + ? await getSessionCookieById({ sessionId: command.sessionId }).catch( + (error) => { + return Promise.reject(error); + }, + ) + : command.loginName + ? await getSessionCookieByLoginName({ + loginName: command.loginName, + organization: command.organization, + }).catch((error) => { + return Promise.reject(error); + }) + : await getMostRecentSessionCookie().catch((error) => { + return Promise.reject(error); + }); + + const checks = create(ChecksSchema, {}); + + if (command.method === "time-based") { + checks.totp = create(CheckTOTPSchema, { + code: command.code, + }); + } else if (command.method === "sms") { + checks.otpSms = create(CheckOTPSchema, { + code: command.code, + }); + } else if (command.method === "email") { + checks.otpEmail = create(CheckOTPSchema, { + code: command.code, + }); + } + + const loginSettings = await getLoginSettings(command.organization); + + return setSessionAndUpdateCookie( + recentSession, + checks, + undefined, + command.authRequestId, + loginSettings?.secondFactorCheckLifetime, + ).then((session) => { + return { + sessionId: session.id, + factors: session.factors, + challenges: session.challenges, + }; + }); +} diff --git a/apps/login/src/lib/server/passkeys.ts b/apps/login/src/lib/server/passkeys.ts new file mode 100644 index 0000000000..c21076265c --- /dev/null +++ b/apps/login/src/lib/server/passkeys.ts @@ -0,0 +1,210 @@ +"use server"; + +import { + createPasskeyRegistrationLink, + getLoginSettings, + getSession, + getUserByID, + registerPasskey, + verifyPasskeyRegistration as zitadelVerifyPasskeyRegistration, +} from "@/lib/zitadel"; +import { create, Duration } from "@zitadel/client"; +import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { + RegisterPasskeyResponse, + VerifyPasskeyRegistrationRequestSchema, +} from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { headers } from "next/headers"; +import { userAgent } from "next/server"; +import { getNextUrl } from "../client"; +import { + getMostRecentSessionCookie, + getSessionCookieById, + getSessionCookieByLoginName, +} from "../cookies"; +import { checkEmailVerification } from "../verify-helper"; +import { setSessionAndUpdateCookie } from "./cookie"; + +type VerifyPasskeyCommand = { + passkeyId: string; + passkeyName?: string; + publicKeyCredential: any; + sessionId: string; +}; + +type RegisterPasskeyCommand = { + sessionId: string; +}; + +export async function registerPasskeyLink( + command: RegisterPasskeyCommand, +): Promise { + const { sessionId } = command; + + const sessionCookie = await getSessionCookieById({ sessionId }); + const session = await getSession({ + sessionId: sessionCookie.id, + sessionToken: sessionCookie.token, + }); + + const host = (await headers()).get("host"); + + if (!host) { + throw new Error("Could not get domain"); + } + + const [hostname, port] = host.split(":"); + + if (!hostname) { + throw new Error("Could not get hostname"); + } + + const userId = session?.session?.factors?.user?.id; + + if (!userId) { + throw new Error("Could not get session"); + } + // TODO: add org context + + // use session token to add the passkey + const registerLink = await createPasskeyRegistrationLink( + userId, + // sessionCookie.token, + ); + + if (!registerLink.code) { + throw new Error("Missing code in response"); + } + + return registerPasskey(userId, registerLink.code, hostname); +} + +export async function verifyPasskeyRegistration(command: VerifyPasskeyCommand) { + // if no name is provided, try to generate one from the user agent + let passkeyName = command.passkeyName; + if (!!!passkeyName) { + const headersList = await headers(); + const userAgentStructure = { headers: headersList }; + const { browser, device, os } = userAgent(userAgentStructure); + + passkeyName = `${device.vendor ?? ""} ${device.model ?? ""}${ + device.vendor || device.model ? ", " : "" + }${os.name}${os.name ? ", " : ""}${browser.name}`; + } + + const sessionCookie = await getSessionCookieById({ + sessionId: command.sessionId, + }); + const session = await getSession({ + sessionId: sessionCookie.id, + sessionToken: sessionCookie.token, + }); + const userId = session?.session?.factors?.user?.id; + + if (!userId) { + throw new Error("Could not get session"); + } + + return zitadelVerifyPasskeyRegistration( + create(VerifyPasskeyRegistrationRequestSchema, { + passkeyId: command.passkeyId, + publicKeyCredential: command.publicKeyCredential, + passkeyName, + userId, + }), + ); +} + +type SendPasskeyCommand = { + loginName?: string; + sessionId?: string; + organization?: string; + checks?: Checks; + authRequestId?: string; + lifetime?: Duration; +}; + +export async function sendPasskey(command: SendPasskeyCommand) { + let { loginName, sessionId, organization, checks, authRequestId } = command; + const recentSession = sessionId + ? await getSessionCookieById({ sessionId }) + : loginName + ? await getSessionCookieByLoginName({ loginName, organization }) + : await getMostRecentSessionCookie(); + + if (!recentSession) { + return { + error: "Could not find session", + }; + } + + const host = (await headers()).get("host"); + + if (!host) { + return { error: "Could not get host" }; + } + + const loginSettings = await getLoginSettings(organization); + + const lifetime = checks?.webAuthN + ? loginSettings?.multiFactorCheckLifetime // TODO different lifetime for webauthn u2f/passkey + : checks?.otpEmail || checks?.otpSms + ? loginSettings?.secondFactorCheckLifetime + : undefined; + + const session = await setSessionAndUpdateCookie( + recentSession, + checks, + undefined, + authRequestId, + lifetime, + ); + + if (!session || !session?.factors?.user?.id) { + return { error: "Could not update session" }; + } + + const userResponse = await getUserByID(session?.factors?.user?.id); + + if (!userResponse.user) { + return { error: "User not found in the system" }; + } + + const humanUser = + userResponse.user.type.case === "human" + ? userResponse.user.type.value + : undefined; + + const emailVerificationCheck = checkEmailVerification( + session, + humanUser, + organization, + authRequestId, + ); + + if (emailVerificationCheck?.redirect) { + return emailVerificationCheck; + } + + const url = + authRequestId && session.id + ? await getNextUrl( + { + sessionId: session.id, + authRequestId: authRequestId, + organization: organization, + }, + loginSettings?.defaultRedirectUri, + ) + : session?.factors?.user?.loginName + ? await getNextUrl( + { + loginName: session.factors.user.loginName, + organization: organization, + }, + loginSettings?.defaultRedirectUri, + ) + : null; + + return { redirect: url }; +} diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts new file mode 100644 index 0000000000..3b7a24a718 --- /dev/null +++ b/apps/login/src/lib/server/password.ts @@ -0,0 +1,333 @@ +"use server"; + +import { + createSessionAndUpdateCookie, + setSessionAndUpdateCookie, +} from "@/lib/server/cookie"; +import { + getLoginSettings, + getSession, + getUserByID, + listAuthenticationMethodTypes, + listUsers, + passwordReset, + setPassword, + setUserPassword, +} from "@/lib/zitadel"; +import { create } from "@zitadel/client"; +import { createServerTransport } from "@zitadel/client/node"; +import { createUserServiceClient } from "@zitadel/client/v2"; +import { + Checks, + ChecksSchema, +} from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { User, UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; +import { + AuthenticationMethodType, + SetPasswordRequestSchema, +} from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { headers } from "next/headers"; +import { getNextUrl } from "../client"; +import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies"; +import { + checkEmailVerification, + checkMFAFactors, + checkPasswordChangeRequired, +} from "../verify-helper"; + +type ResetPasswordCommand = { + loginName: string; + organization?: string; + authRequestId?: string; +}; + +export async function resetPassword(command: ResetPasswordCommand) { + const host = (await headers()).get("host"); + + const users = await listUsers({ + loginName: command.loginName, + organizationId: command.organization, + }); + + if ( + !users.details || + users.details.totalResult !== BigInt(1) || + !users.result[0].userId + ) { + return { error: "Could not send Password Reset Link" }; + } + const userId = users.result[0].userId; + + return passwordReset(userId, host, command.authRequestId); +} + +export type UpdateSessionCommand = { + loginName: string; + organization?: string; + checks: Checks; + authRequestId?: string; +}; + +export async function sendPassword(command: UpdateSessionCommand) { + let sessionCookie = await getSessionCookieByLoginName({ + loginName: command.loginName, + organization: command.organization, + }).catch((error) => { + console.warn("Ignored error:", error); + }); + + let session; + let user: User; + let loginSettings: LoginSettings | undefined; + + if (!sessionCookie) { + const users = await listUsers({ + loginName: command.loginName, + organizationId: command.organization, + }); + + if (users.details?.totalResult == BigInt(1) && users.result[0].userId) { + user = users.result[0]; + + const checks = create(ChecksSchema, { + user: { search: { case: "userId", value: users.result[0].userId } }, + password: { password: command.checks.password?.password }, + }); + + loginSettings = await getLoginSettings(command.organization); + + session = await createSessionAndUpdateCookie( + checks, + undefined, + command.authRequestId, + loginSettings?.passwordCheckLifetime, + ); + } + + // this is a fake error message to hide that the user does not even exist + return { error: "Could not verify password" }; + } else { + session = await setSessionAndUpdateCookie( + sessionCookie, + command.checks, + undefined, + command.authRequestId, + loginSettings?.passwordCheckLifetime, + ); + + if (!session?.factors?.user?.id) { + return { error: "Could not create session for user" }; + } + + const userResponse = await getUserByID(session?.factors?.user?.id); + + if (!userResponse.user) { + return { error: "User not found in the system" }; + } + + user = userResponse.user; + } + + if (!loginSettings) { + loginSettings = await getLoginSettings( + command.organization ?? session.factors?.user?.organizationId, + ); + } + + if (!session?.factors?.user?.id || !sessionCookie) { + return { error: "Could not create session for user" }; + } + + const humanUser = user.type.case === "human" ? user.type.value : undefined; + + // check if the user has to change password first + const passwordChangedCheck = checkPasswordChangeRequired( + session, + humanUser, + command.organization, + command.authRequestId, + ); + + if (passwordChangedCheck?.redirect) { + return passwordChangedCheck; + } + + // throw error if user is in initial state here and do not continue + if (user.state === UserState.INITIAL) { + return { error: "Initial User not supported" }; + } + + // check to see if user was verified + const emailVerificationCheck = checkEmailVerification( + session, + humanUser, + command.organization, + command.authRequestId, + ); + + if (emailVerificationCheck?.redirect) { + return emailVerificationCheck; + } + + // if password, check if user has MFA methods + let authMethods; + if (command.checks && command.checks.password && session.factors?.user?.id) { + const response = await listAuthenticationMethodTypes( + session.factors.user.id, + ); + if (response.authMethodTypes && response.authMethodTypes.length) { + authMethods = response.authMethodTypes; + } + } + + if (!authMethods) { + return { error: "Could not verify password!" }; + } + + const mfaFactorCheck = checkMFAFactors( + session, + loginSettings, + authMethods, + command.organization, + command.authRequestId, + ); + + if (mfaFactorCheck?.redirect) { + return mfaFactorCheck; + } + + if (command.authRequestId && session.id) { + const nextUrl = await getNextUrl( + { + sessionId: session.id, + authRequestId: command.authRequestId, + organization: + command.organization ?? session.factors?.user?.organizationId, + }, + loginSettings?.defaultRedirectUri, + ); + + return { redirect: nextUrl }; + } + + const url = await getNextUrl( + { + loginName: session.factors.user.loginName, + organization: session.factors?.user?.organizationId, + }, + loginSettings?.defaultRedirectUri, + ); + + return { redirect: url }; +} + +export async function changePassword(command: { + code?: string; + userId: string; + password: string; +}) { + // check for init state + const { user } = await getUserByID(command.userId); + + if (!user || user.userId !== command.userId) { + return { error: "Could not send Password Reset Link" }; + } + const userId = user.userId; + + return setUserPassword(userId, command.password, user, command.code); +} + +type CheckSessionAndSetPasswordCommand = { + sessionId: string; + password: string; +}; + +export async function checkSessionAndSetPassword({ + sessionId, + password, +}: CheckSessionAndSetPasswordCommand) { + const sessionCookie = await getSessionCookieById({ sessionId }); + + const { session } = await getSession({ + sessionId: sessionCookie.id, + sessionToken: sessionCookie.token, + }); + + if (!session || !session.factors?.user?.id) { + return { error: "Could not load session" }; + } + + const payload = create(SetPasswordRequestSchema, { + userId: session.factors.user.id, + newPassword: { + password, + }, + }); + + // check if the user has no password set in order to set a password + const authmethods = await listAuthenticationMethodTypes( + session.factors.user.id, + ); + + if (!authmethods) { + return { error: "Could not load auth methods" }; + } + + const requiredAuthMethodsForForceMFA = [ + AuthenticationMethodType.OTP_EMAIL, + AuthenticationMethodType.OTP_SMS, + AuthenticationMethodType.TOTP, + AuthenticationMethodType.U2F, + ]; + + const hasNoMFAMethods = requiredAuthMethodsForForceMFA.every( + (method) => !authmethods.authMethodTypes.includes(method), + ); + + const loginSettings = await getLoginSettings( + session.factors.user.organizationId, + ); + + const forceMfa = !!( + loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly + ); + + // if the user has no MFA but MFA is enforced, we can set a password otherwise we use the token of the user + if (forceMfa && hasNoMFAMethods) { + return setPassword(payload).catch((error) => { + // throw error if failed precondition (ex. User is not yet initialized) + if (error.code === 9 && error.message) { + return { error: "Failed precondition" }; + } else { + throw error; + } + }); + } else { + const myUserService = (sessionToken: string) => { + return createUserServiceClient( + createServerTransport(sessionToken, { + baseUrl: process.env.ZITADEL_API_URL!, + }), + ); + }; + + const selfService = await myUserService(`${sessionCookie.token}`); + + return selfService + .setPassword( + { + userId: session.factors.user.id, + newPassword: { password, changeRequired: false }, + }, + {}, + ) + .catch((error) => { + console.log(error); + if (error.code === 7) { + return { error: "Session is not valid." }; + } + throw error; + }); + } +} diff --git a/apps/login/src/lib/server/register.ts b/apps/login/src/lib/server/register.ts new file mode 100644 index 0000000000..284689523a --- /dev/null +++ b/apps/login/src/lib/server/register.ts @@ -0,0 +1,117 @@ +"use server"; + +import { createSessionAndUpdateCookie } from "@/lib/server/cookie"; +import { addHumanUser, getLoginSettings, getUserByID } from "@/lib/zitadel"; +import { create } from "@zitadel/client"; +import { Factors } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { + ChecksJson, + ChecksSchema, +} from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { getNextUrl } from "../client"; +import { checkEmailVerification } from "../verify-helper"; + +type RegisterUserCommand = { + email: string; + firstName: string; + lastName: string; + password?: string; + organization?: string; + authRequestId?: string; +}; + +export type RegisterUserResponse = { + userId: string; + sessionId: string; + factors: Factors | undefined; +}; +export async function registerUser(command: RegisterUserCommand) { + const addResponse = await addHumanUser({ + email: command.email, + firstName: command.firstName, + lastName: command.lastName, + password: command.password ? command.password : undefined, + organization: command.organization, + }); + + if (!addResponse) { + return { error: "Could not create user" }; + } + + const loginSettings = await getLoginSettings(command.organization); + + let checkPayload: any = { + user: { search: { case: "userId", value: addResponse.userId } }, + }; + + if (command.password) { + checkPayload = { + ...checkPayload, + password: { password: command.password }, + } as ChecksJson; + } + + const checks = create(ChecksSchema, checkPayload); + + const session = await createSessionAndUpdateCookie( + checks, + undefined, + command.authRequestId, + command.password ? loginSettings?.passwordCheckLifetime : undefined, + ); + + if (!session || !session.factors?.user) { + return { error: "Could not create session" }; + } + + if (!command.password) { + const params = new URLSearchParams({ + loginName: session.factors.user.loginName, + organization: session.factors.user.organizationId, + }); + + if (command.authRequestId) { + params.append("authRequestId", command.authRequestId); + } + + return { redirect: "/passkey/set?" + params }; + } else { + const userResponse = await getUserByID(session?.factors?.user?.id); + + if (!userResponse.user) { + return { error: "User not found in the system" }; + } + + const humanUser = + userResponse.user.type.case === "human" + ? userResponse.user.type.value + : undefined; + + const emailVerificationCheck = checkEmailVerification( + session, + humanUser, + session.factors.user.organizationId, + command.authRequestId, + ); + + if (emailVerificationCheck?.redirect) { + return emailVerificationCheck; + } + + const url = await getNextUrl( + command.authRequestId && session.id + ? { + sessionId: session.id, + authRequestId: command.authRequestId, + organization: session.factors.user.organizationId, + } + : { + loginName: session.factors.user.loginName, + organization: session.factors.user.organizationId, + }, + loginSettings?.defaultRedirectUri, + ); + + return { redirect: url }; + } +} diff --git a/apps/login/src/lib/server/session.ts b/apps/login/src/lib/server/session.ts new file mode 100644 index 0000000000..70bc18f6d5 --- /dev/null +++ b/apps/login/src/lib/server/session.ts @@ -0,0 +1,174 @@ +"use server"; + +import { setSessionAndUpdateCookie } from "@/lib/server/cookie"; +import { + deleteSession, + getLoginSettings, + listAuthenticationMethodTypes, +} from "@/lib/zitadel"; +import { Duration } from "@zitadel/client"; +import { RequestChallenges } from "@zitadel/proto/zitadel/session/v2/challenge_pb"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { headers } from "next/headers"; +import { getNextUrl } from "../client"; +import { + getMostRecentSessionCookie, + getSessionCookieById, + getSessionCookieByLoginName, + removeSessionFromCookie, +} from "../cookies"; + +export async function continueWithSession({ + authRequestId, + ...session +}: Session & { authRequestId?: string }) { + const loginSettings = await getLoginSettings( + session.factors?.user?.organizationId, + ); + + const url = + authRequestId && session.id && session.factors?.user + ? await getNextUrl( + { + sessionId: session.id, + authRequestId: authRequestId, + organization: session.factors.user.organizationId, + }, + loginSettings?.defaultRedirectUri, + ) + : session.factors?.user + ? await getNextUrl( + { + loginName: session.factors.user.loginName, + organization: session.factors.user.organizationId, + }, + loginSettings?.defaultRedirectUri, + ) + : null; + if (url) { + return { redirect: url }; + } +} + +export type UpdateSessionCommand = { + loginName?: string; + sessionId?: string; + organization?: string; + checks?: Checks; + authRequestId?: string; + challenges?: RequestChallenges; + lifetime?: Duration; +}; + +export async function updateSession(options: UpdateSessionCommand) { + let { + loginName, + sessionId, + organization, + checks, + authRequestId, + challenges, + } = options; + const recentSession = sessionId + ? await getSessionCookieById({ sessionId }) + : loginName + ? await getSessionCookieByLoginName({ loginName, organization }) + : await getMostRecentSessionCookie(); + + if (!recentSession) { + return { + error: "Could not find session", + }; + } + + const host = (await headers()).get("host"); + + if (!host) { + return { error: "Could not get host" }; + } + + if ( + host && + challenges && + challenges.webAuthN && + !challenges.webAuthN.domain + ) { + const [hostname, port] = host.split(":"); + + challenges.webAuthN.domain = hostname; + } + + const loginSettings = await getLoginSettings(organization); + + const lifetime = checks?.webAuthN + ? loginSettings?.multiFactorCheckLifetime // TODO different lifetime for webauthn u2f/passkey + : checks?.otpEmail || checks?.otpSms + ? loginSettings?.secondFactorCheckLifetime + : undefined; + + const session = await setSessionAndUpdateCookie( + recentSession, + checks, + challenges, + authRequestId, + lifetime, + ); + + if (!session) { + return { error: "Could not update session" }; + } + + // if password, check if user has MFA methods + let authMethods; + if (checks && checks.password && session.factors?.user?.id) { + const response = await listAuthenticationMethodTypes( + session.factors.user.id, + ); + if (response.authMethodTypes && response.authMethodTypes.length) { + authMethods = response.authMethodTypes; + } + } + + return { + sessionId: session.id, + factors: session.factors, + challenges: session.challenges, + authMethods, + }; +} + +type ClearSessionOptions = { + sessionId: string; +}; + +export async function clearSession(options: ClearSessionOptions) { + const { sessionId } = options; + + const session = await getSessionCookieById({ sessionId }); + + const deletedSession = await deleteSession(session.id, session.token); + + if (deletedSession) { + return removeSessionFromCookie(session); + } +} + +type CleanupSessionCommand = { + sessionId: string; +}; + +export async function cleanupSession({ sessionId }: CleanupSessionCommand) { + const sessionCookie = await getSessionCookieById({ sessionId }); + + const deleteResponse = await deleteSession( + sessionCookie.id, + sessionCookie.token, + ); + + if (!deleteResponse) { + throw new Error("Could not delete session"); + } + + return removeSessionFromCookie(sessionCookie); +} diff --git a/apps/login/src/lib/server/u2f.ts b/apps/login/src/lib/server/u2f.ts new file mode 100644 index 0000000000..5cbd80611b --- /dev/null +++ b/apps/login/src/lib/server/u2f.ts @@ -0,0 +1,90 @@ +"use server"; + +import { getSession, registerU2F, verifyU2FRegistration } from "@/lib/zitadel"; +import { create } from "@zitadel/client"; +import { VerifyU2FRegistrationRequestSchema } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { headers } from "next/headers"; +import { userAgent } from "next/server"; +import { getSessionCookieById } from "../cookies"; + +type RegisterU2FCommand = { + sessionId: string; +}; + +type VerifyU2FCommand = { + u2fId: string; + passkeyName?: string; + publicKeyCredential: any; + sessionId: string; +}; + +export async function addU2F(command: RegisterU2FCommand) { + const sessionCookie = await getSessionCookieById({ + sessionId: command.sessionId, + }); + + if (!sessionCookie) { + return { error: "Could not get session" }; + } + + const session = await getSession({ + sessionId: sessionCookie.id, + sessionToken: sessionCookie.token, + }); + + const host = (await headers()).get("host"); + + if (!host) { + return { error: "Could not get domain" }; + } + + const [hostname, port] = host.split(":"); + + if (!hostname) { + throw new Error("Could not get hostname"); + } + + const userId = session?.session?.factors?.user?.id; + + if (!session || !userId) { + return { error: "Could not get session" }; + } + + return registerU2F(userId, hostname); +} + +export async function verifyU2F(command: VerifyU2FCommand) { + let passkeyName = command.passkeyName; + if (!!!passkeyName) { + const headersList = await headers(); + const userAgentStructure = { headers: headersList }; + const { browser, device, os } = userAgent(userAgentStructure); + + passkeyName = `${device.vendor ?? ""} ${device.model ?? ""}${ + device.vendor || device.model ? ", " : "" + }${os.name}${os.name ? ", " : ""}${browser.name}`; + } + const sessionCookie = await getSessionCookieById({ + sessionId: command.sessionId, + }); + + const session = await getSession({ + sessionId: sessionCookie.id, + sessionToken: sessionCookie.token, + }); + + const userId = session?.session?.factors?.user?.id; + + if (!userId) { + return { error: "Could not get session" }; + } + + const req = create(VerifyU2FRegistrationRequestSchema, { + u2fId: command.u2fId, + publicKeyCredential: command.publicKeyCredential, + tokenName: passkeyName, + userId, + }); + + return verifyU2FRegistration(req); +} diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts new file mode 100644 index 0000000000..2c0c78272a --- /dev/null +++ b/apps/login/src/lib/server/verify.ts @@ -0,0 +1,357 @@ +"use server"; + +import { + getLoginSettings, + getSession, + getUserByID, + listAuthenticationMethodTypes, + resendEmailCode, + resendInviteCode, + verifyEmail, + verifyInviteCode, + sendEmailCode as zitadelSendEmailCode, +} from "@/lib/zitadel"; +import { create } from "@zitadel/client"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { User } from "@zitadel/proto/zitadel/user/v2/user_pb"; +import { headers } from "next/headers"; +import { getNextUrl } from "../client"; +import { getSessionCookieByLoginName } from "../cookies"; +import { checkMFAFactors } from "../verify-helper"; +import { createSessionAndUpdateCookie } from "./cookie"; + +type VerifyUserByEmailCommand = { + userId: string; + loginName?: string; // to determine already existing session + organization?: string; + code: string; + isInvite: boolean; + authRequestId?: string; +}; + +export async function sendVerification(command: VerifyUserByEmailCommand) { + const verifyResponse = command.isInvite + ? await verifyInviteCode(command.userId, command.code).catch(() => { + return { error: "Could not verify invite" }; + }) + : await verifyEmail(command.userId, command.code).catch(() => { + return { error: "Could not verify email" }; + }); + + if ("error" in verifyResponse) { + return verifyResponse; + } + + if (!verifyResponse) { + return { error: "Could not verify" }; + } + + let session: Session | undefined; + let user: User | undefined; + + if ("loginName" in command) { + const sessionCookie = await getSessionCookieByLoginName({ + loginName: command.loginName, + organization: command.organization, + }).catch((error) => { + console.warn("Ignored error:", error); + }); + + if (!sessionCookie) { + return { error: "Could not load session cookie" }; + } + + session = await getSession({ + sessionId: sessionCookie.id, + sessionToken: sessionCookie.token, + }).then((response) => { + if (response?.session) { + return response.session; + } + }); + + if (!session?.factors?.user?.id) { + return { error: "Could not create session for user" }; + } + + const userResponse = await getUserByID(session?.factors?.user?.id); + + if (!userResponse?.user) { + return { error: "Could not load user" }; + } + + user = userResponse.user; + } else { + const userResponse = await getUserByID(command.userId); + + if (!userResponse || !userResponse.user) { + return { error: "Could not load user" }; + } + + user = userResponse.user; + + const checks = create(ChecksSchema, { + user: { + search: { + case: "loginName", + value: userResponse.user.preferredLoginName, + }, + }, + }); + + session = await createSessionAndUpdateCookie( + checks, + undefined, + command.authRequestId, + ); + } + + if (!session?.factors?.user?.id) { + return { error: "Could not create session for user" }; + } + + if (!session?.factors?.user?.id) { + return { error: "Could not create session for user" }; + } + + if (!user) { + return { error: "Could not load user" }; + } + + const loginSettings = await getLoginSettings(user.details?.resourceOwner); + + const authMethodResponse = await listAuthenticationMethodTypes(user.userId); + + if (!authMethodResponse || !authMethodResponse.authMethodTypes) { + return { error: "Could not load possible authenticators" }; + } + + // if no authmethods are found on the user, redirect to set one up + if ( + authMethodResponse && + authMethodResponse.authMethodTypes && + authMethodResponse.authMethodTypes.length == 0 + ) { + const params = new URLSearchParams({ + sessionId: session.id, + }); + + if (session.factors?.user?.loginName) { + params.set("loginName", session.factors?.user?.loginName); + } + return { redirect: `/authenticator/set?${params}` }; + } + + // redirect to mfa factor if user has one, or redirect to set one up + const mfaFactorCheck = checkMFAFactors( + session, + loginSettings, + authMethodResponse.authMethodTypes, + command.organization, + command.authRequestId, + ); + + if (mfaFactorCheck?.redirect) { + return mfaFactorCheck; + } + + // login user if no additional steps are required + if (command.authRequestId && session.id) { + const nextUrl = await getNextUrl( + { + sessionId: session.id, + authRequestId: command.authRequestId, + organization: + command.organization ?? session.factors?.user?.organizationId, + }, + loginSettings?.defaultRedirectUri, + ); + + return { redirect: nextUrl }; + } + + const url = await getNextUrl( + { + loginName: session.factors.user.loginName, + organization: session.factors?.user?.organizationId, + }, + loginSettings?.defaultRedirectUri, + ); + + return { redirect: url }; +} + +type resendVerifyEmailCommand = { + userId: string; + isInvite: boolean; + authRequestId?: string; +}; + +export async function resendVerification(command: resendVerifyEmailCommand) { + const host = (await headers()).get("host"); + + return command.isInvite + ? resendInviteCode(command.userId) + : resendEmailCode(command.userId, host, command.authRequestId); +} + +type sendEmailCommand = { + userId: string; + authRequestId?: string; +}; + +export async function sendEmailCode(command: sendEmailCommand) { + const host = (await headers()).get("host"); + return zitadelSendEmailCode(command.userId, host, command.authRequestId); +} + +export type SendVerificationRedirectWithoutCheckCommand = { + organization?: string; + authRequestId?: string; +} & ( + | { userId: string; loginName?: never } + | { userId?: never; loginName: string } +); + +export async function sendVerificationRedirectWithoutCheck( + command: SendVerificationRedirectWithoutCheckCommand, +) { + if (!("loginName" in command || "userId" in command)) { + return { error: "No userId, nor loginname provided" }; + } + + let session: Session | undefined; + let user: User | undefined; + + if ("loginName" in command) { + const sessionCookie = await getSessionCookieByLoginName({ + loginName: command.loginName, + organization: command.organization, + }).catch((error) => { + console.warn("Ignored error:", error); + }); + + if (!sessionCookie) { + return { error: "Could not load session cookie" }; + } + + session = await getSession({ + sessionId: sessionCookie.id, + sessionToken: sessionCookie.token, + }).then((response) => { + if (response?.session) { + return response.session; + } + }); + + if (!session?.factors?.user?.id) { + return { error: "Could not create session for user" }; + } + + const userResponse = await getUserByID(session?.factors?.user?.id); + + if (!userResponse?.user) { + return { error: "Could not load user" }; + } + + user = userResponse.user; + } else if ("userId" in command) { + const userResponse = await getUserByID(command.userId); + + if (!userResponse?.user) { + return { error: "Could not load user" }; + } + + user = userResponse.user; + + const checks = create(ChecksSchema, { + user: { + search: { + case: "loginName", + value: userResponse.user.preferredLoginName, + }, + }, + }); + + session = await createSessionAndUpdateCookie( + checks, + undefined, + command.authRequestId, + ); + } + + if (!session?.factors?.user?.id) { + return { error: "Could not create session for user" }; + } + + if (!session?.factors?.user?.id) { + return { error: "Could not create session for user" }; + } + + if (!user) { + return { error: "Could not load user" }; + } + + const authMethodResponse = await listAuthenticationMethodTypes(user.userId); + + if (!authMethodResponse || !authMethodResponse.authMethodTypes) { + return { error: "Could not load possible authenticators" }; + } + + // if no authmethods are found on the user, redirect to set one up + if ( + authMethodResponse && + authMethodResponse.authMethodTypes && + authMethodResponse.authMethodTypes.length == 0 + ) { + const params = new URLSearchParams({ + sessionId: session.id, + }); + + if (session.factors?.user?.loginName) { + params.set("loginName", session.factors?.user?.loginName); + } + return { redirect: `/authenticator/set?${params}` }; + } + + const loginSettings = await getLoginSettings(user.details?.resourceOwner); + + // redirect to mfa factor if user has one, or redirect to set one up + const mfaFactorCheck = checkMFAFactors( + session, + loginSettings, + authMethodResponse.authMethodTypes, + command.organization, + command.authRequestId, + ); + + if (mfaFactorCheck?.redirect) { + return mfaFactorCheck; + } + + // login user if no additional steps are required + if (command.authRequestId && session.id) { + const nextUrl = await getNextUrl( + { + sessionId: session.id, + authRequestId: command.authRequestId, + organization: + command.organization ?? session.factors?.user?.organizationId, + }, + loginSettings?.defaultRedirectUri, + ); + + return { redirect: nextUrl }; + } + + const url = await getNextUrl( + { + loginName: session.factors.user.loginName, + organization: session.factors?.user?.organizationId, + }, + loginSettings?.defaultRedirectUri, + ); + + return { redirect: url }; +} diff --git a/apps/login/src/lib/session.ts b/apps/login/src/lib/session.ts new file mode 100644 index 0000000000..29ceb3764b --- /dev/null +++ b/apps/login/src/lib/session.ts @@ -0,0 +1,17 @@ +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { GetSessionResponse } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { getMostRecentCookieWithLoginname } from "./cookies"; +import { sessionService } from "./zitadel"; + +export async function loadMostRecentSession(sessionParams: { + loginName?: string; + organization?: string; +}): Promise { + const recent = await getMostRecentCookieWithLoginname({ + loginName: sessionParams.loginName, + organization: sessionParams.organization, + }); + return sessionService + .getSession({ sessionId: recent.id, sessionToken: recent.token }, {}) + .then((resp: GetSessionResponse) => resp.session); +} diff --git a/apps/login/src/lib/verify-helper.ts b/apps/login/src/lib/verify-helper.ts new file mode 100644 index 0000000000..b37287a959 --- /dev/null +++ b/apps/login/src/lib/verify-helper.ts @@ -0,0 +1,204 @@ +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { HumanUser } from "@zitadel/proto/zitadel/user/v2/user_pb"; +import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; + +export function checkPasswordChangeRequired( + session: Session, + humanUser: HumanUser | undefined, + organization?: string, + authRequestId?: string, +) { + if (humanUser?.passwordChangeRequired) { + const params = new URLSearchParams({ + loginName: session.factors?.user?.loginName as string, + }); + + if (organization || session.factors?.user?.organizationId) { + params.append( + "organization", + session.factors?.user?.organizationId as string, + ); + } + + if (authRequestId) { + params.append("authRequestId", authRequestId); + } + + return { redirect: "/password/change?" + params }; + } +} + +export function checkInvite( + session: Session, + humanUser?: HumanUser, + organization?: string, + authRequestId?: string, +) { + if (!humanUser?.email?.isVerified) { + const paramsVerify = new URLSearchParams({ + loginName: session.factors?.user?.loginName as string, + userId: session.factors?.user?.id as string, // verify needs user id + invite: "true", // TODO: check - set this to true as we dont expect old email verification method here + }); + + if (organization || session.factors?.user?.organizationId) { + paramsVerify.append( + "organization", + organization ?? (session.factors?.user?.organizationId as string), + ); + } + + if (authRequestId) { + paramsVerify.append("authRequestId", authRequestId); + } + + return { redirect: "/verify?" + paramsVerify }; + } +} + +export function checkEmailVerification( + session: Session, + humanUser?: HumanUser, + organization?: string, + authRequestId?: string, +) { + if ( + !humanUser?.email?.isVerified && + process.env.EMAIL_VERIFICATION === "true" + ) { + const params = new URLSearchParams({ + loginName: session.factors?.user?.loginName as string, + }); + + if (authRequestId) { + params.append("authRequestId", authRequestId); + } + + if (organization || session.factors?.user?.organizationId) { + params.append( + "organization", + organization ?? (session.factors?.user?.organizationId as string), + ); + } + + return { redirect: `/verify?` + params }; + } +} + +export function checkMFAFactors( + session: Session, + loginSettings: LoginSettings | undefined, + authMethods: AuthenticationMethodType[], + organization?: string, + authRequestId?: string, +) { + const availableMultiFactors = authMethods?.filter( + (m: AuthenticationMethodType) => + m !== AuthenticationMethodType.PASSWORD && + m !== AuthenticationMethodType.PASSKEY, + ); + + const hasAuthenticatedWithPasskey = + session.factors?.webAuthN?.verifiedAt && + session.factors?.webAuthN?.userVerified; + + // escape further checks if user has authenticated with passkey + if (hasAuthenticatedWithPasskey) { + return; + } + + // if user has not authenticated with passkey and has only one additional mfa factor, redirect to that + if (availableMultiFactors?.length == 1) { + const params = new URLSearchParams({ + loginName: session.factors?.user?.loginName as string, + }); + + if (authRequestId) { + params.append("authRequestId", authRequestId); + } + + if (organization || session.factors?.user?.organizationId) { + params.append( + "organization", + organization ?? (session.factors?.user?.organizationId as string), + ); + } + + const factor = availableMultiFactors[0]; + // if passwordless is other method, but user selected password as alternative, perform a login + if (factor === AuthenticationMethodType.TOTP) { + return { redirect: `/otp/time-based?` + params }; + } else if (factor === AuthenticationMethodType.OTP_SMS) { + return { redirect: `/otp/sms?` + params }; + } else if (factor === AuthenticationMethodType.OTP_EMAIL) { + return { redirect: `/otp/email?` + params }; + } else if (factor === AuthenticationMethodType.U2F) { + return { redirect: `/u2f?` + params }; + } + } else if (availableMultiFactors?.length > 1) { + const params = new URLSearchParams({ + loginName: session.factors?.user?.loginName as string, + }); + + if (authRequestId) { + params.append("authRequestId", authRequestId); + } + + if (organization || session.factors?.user?.organizationId) { + params.append( + "organization", + organization ?? (session.factors?.user?.organizationId as string), + ); + } + + return { redirect: `/mfa?` + params }; + } else if ( + (loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly) && + !availableMultiFactors.length + ) { + const params = new URLSearchParams({ + loginName: session.factors?.user?.loginName as string, + force: "true", // this defines if the mfa is forced in the settings + checkAfter: "true", // this defines if the check is directly made after the setup + }); + + if (authRequestId) { + params.append("authRequestId", authRequestId); + } + + if (organization || session.factors?.user?.organizationId) { + params.append( + "organization", + organization ?? (session.factors?.user?.organizationId as string), + ); + } + + // TODO: provide a way to setup passkeys on mfa page? + return { redirect: `/mfa/set?` + params }; + } + + // TODO: implement passkey setup + + // else if ( + // submitted.factors && + // !submitted.factors.webAuthN && // if session was not verified with a passkey + // promptPasswordless && // if explicitly prompted due policy + // !isAlternative // escaped if password was used as an alternative method + // ) { + // const params = new URLSearchParams({ + // loginName: submitted.factors.user.loginName, + // prompt: "true", + // }); + + // if (authRequestId) { + // params.append("authRequestId", authRequestId); + // } + + // if (organization) { + // params.append("organization", organization); + // } + + // return router.push(`/passkey/set?` + params); + // } +} diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index 3aee979c1e..f415c8fb5c 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -1,52 +1,92 @@ +import { createServerTransport } from "@zitadel/client/node"; import { + createIdpServiceClient, createOIDCServiceClient, + createOrganizationServiceClient, createSessionServiceClient, createSettingsServiceClient, createUserServiceClient, makeReqCtx, } from "@zitadel/client/v2"; -import { createManagementServiceClient } from "@zitadel/client/v1"; -import { createServerTransport } from "@zitadel/node"; -import { GetActiveIdentityProvidersRequest } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb"; -import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { RequestChallenges } from "@zitadel/proto/zitadel/session/v2/challenge_pb"; +import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { + AddHumanUserRequest, + ResendEmailCodeRequest, + ResendEmailCodeRequestSchema, RetrieveIdentityProviderIntentRequest, + SendEmailCodeRequestSchema, + SetPasswordRequest, + SetPasswordRequestSchema, + VerifyPasskeyRegistrationRequest, VerifyU2FRegistrationRequest, } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; -import { CreateCallbackRequest } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; +import { create, Duration } from "@zitadel/client"; import { TextQueryMethod } from "@zitadel/proto/zitadel/object/v2/object_pb"; -import type { RedirectURLs } from "@zitadel/proto/zitadel/user/v2/idp_pb"; -import { ProviderSlug } from "./demos"; -import { PlainMessage } from "@zitadel/client"; - -const SESSION_LIFETIME_S = 3000; +import { CreateCallbackRequest } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; +import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; +import { SendEmailVerificationCodeSchema } from "@zitadel/proto/zitadel/user/v2/email_pb"; +import type { RedirectURLsJson } from "@zitadel/proto/zitadel/user/v2/idp_pb"; +import { + NotificationType, + SendPasswordResetLinkSchema, +} from "@zitadel/proto/zitadel/user/v2/password_pb"; +import { + SearchQuery, + SearchQuerySchema, +} from "@zitadel/proto/zitadel/user/v2/query_pb"; +import { + SendInviteCodeSchema, + User, + UserState, +} from "@zitadel/proto/zitadel/user/v2/user_pb"; +import { unstable_cacheLife as cacheLife } from "next/cache"; const transport = createServerTransport( process.env.ZITADEL_SERVICE_USER_TOKEN!, - { - baseUrl: process.env.ZITADEL_API_URL!, - httpVersion: "2", - }, + { baseUrl: process.env.ZITADEL_API_URL! }, ); export const sessionService = createSessionServiceClient(transport); -export const managementService = createManagementServiceClient(transport); export const userService = createUserServiceClient(transport); export const oidcService = createOIDCServiceClient(transport); +export const idpService = createIdpServiceClient(transport); +export const orgService = createOrganizationServiceClient(transport); export const settingsService = createSettingsServiceClient(transport); +const useCache = process.env.DEBUG !== "true"; + +async function cacheWrapper(callback: Promise) { + "use cache"; + cacheLife("hours"); + + return callback; +} + export async function getBrandingSettings(organization?: string) { - return settingsService + const callback = settingsService .getBrandingSettings({ ctx: makeReqCtx(organization) }, {}) - .then((resp) => resp.settings); + .then((resp) => (resp.settings ? resp.settings : undefined)); + + return useCache ? cacheWrapper(callback) : callback; } export async function getLoginSettings(orgId?: string) { - return settingsService + const callback = settingsService .getLoginSettings({ ctx: makeReqCtx(orgId) }, {}) - .then((resp) => resp.settings); + .then((resp) => (resp.settings ? resp.settings : undefined)); + + return useCache ? cacheWrapper(callback) : callback; +} + +export async function listIDPLinks(userId: string) { + return userService.listIDPLinks( + { + userId, + }, + {}, + ); } export async function addOTPEmail(userId: string) { @@ -58,74 +98,48 @@ export async function addOTPEmail(userId: string) { ); } -export async function addOTPSMS(userId: string, token?: string) { - // TODO: Follow up here, I do not understand the branching - // let userService; - // if (token) { - // const authConfig: ZitadelServerOptions = { - // name: "zitadel login", - // apiUrl: process.env.ZITADEL_API_URL ?? "", - // token: token, - // }; - // const sessionUser = initializeServer(authConfig); - // userService = user.getUser(sessionUser); - // } else { - // userService = user.getUser(server); - // } - +export async function addOTPSMS(userId: string) { return userService.addOTPSMS({ userId }, {}); } -export async function registerTOTP(userId: string, token?: string) { - // TODO: Follow up here, I do not understand the branching - // let userService; - // if (token) { - // const authConfig: ZitadelServerOptions = { - // name: "zitadel login", - // apiUrl: process.env.ZITADEL_API_URL ?? "", - // token: token, - // }; - // - // const sessionUser = initializeServer(authConfig); - // userService = user.getUser(sessionUser); - // } else { - // userService = user.getUser(server); - // } +export async function registerTOTP(userId: string) { return userService.registerTOTP({ userId }, {}); } export async function getGeneralSettings() { - return settingsService + const callback = settingsService .getGeneralSettings({}, {}) .then((resp) => resp.supportedLanguages); + + return useCache ? cacheWrapper(callback) : callback; } export async function getLegalAndSupportSettings(organization?: string) { - return settingsService + const callback = settingsService .getLegalAndSupportSettings({ ctx: makeReqCtx(organization) }, {}) - .then((resp) => { - return resp.settings; - }); + .then((resp) => (resp.settings ? resp.settings : undefined)); + + return useCache ? cacheWrapper(callback) : callback; } export async function getPasswordComplexitySettings(organization?: string) { - return settingsService + const callback = settingsService .getPasswordComplexitySettings({ ctx: makeReqCtx(organization) }) - .then((resp) => resp.settings); + .then((resp) => (resp.settings ? resp.settings : undefined)); + + return useCache ? cacheWrapper(callback) : callback; } export async function createSessionFromChecks( - checks: PlainMessage, - challenges: PlainMessage | undefined, + checks: Checks, + challenges: RequestChallenges | undefined, + lifetime?: Duration, ) { return sessionService.createSession( { checks: checks, challenges, - lifetime: { - seconds: BigInt(SESSION_LIFETIME_S), - nanos: 0, - }, + lifetime, }, {}, ); @@ -137,6 +151,7 @@ export async function createSessionForUserIdAndIdpIntent( idpIntentId?: string | undefined; idpIntentToken?: string | undefined; }, + lifetime?: Duration, ) { return sessionService.createSession({ checks: { @@ -148,10 +163,7 @@ export async function createSessionForUserIdAndIdpIntent( }, idpIntent, }, - // lifetime: { - // seconds: 300, - // nanos: 0, - // }, + lifetime, }); } @@ -159,7 +171,8 @@ export async function setSession( sessionId: string, sessionToken: string, challenges: RequestChallenges | undefined, - checks?: PlainMessage, + checks?: Checks, + lifetime?: Duration, ) { return sessionService.setSession( { @@ -168,12 +181,19 @@ export async function setSession( challenges, checks: checks ? checks : {}, metadata: {}, + lifetime, }, {}, ); } -export async function getSession(sessionId: string, sessionToken: string) { +export async function getSession({ + sessionId, + sessionToken, +}: { + sessionId: string; + sessionToken: string; +}) { return sessionService.getSession({ sessionId, sessionToken }, {}); } @@ -213,7 +233,13 @@ export async function addHumanUser({ organization, }: AddHumanUserData) { return userService.addHumanUser({ - email: { email }, + email: { + email, + verification: { + case: "isVerified", + value: false, + }, + }, username: email, profile: { givenName: firstName, familyName: lastName }, organization: organization @@ -225,24 +251,11 @@ export async function addHumanUser({ }); } -export async function verifyTOTPRegistration( - code: string, - userId: string, - token?: string, -) { - // let userService; - // if (token) { - // const authConfig: ZitadelServerOptions = { - // name: "zitadel login", - // apiUrl: process.env.ZITADEL_API_URL ?? "", - // token: token, - // }; - // - // const sessionUser = initializeServer(authConfig); - // userService = user.getUser(sessionUser); - // } else { - // userService = user.getUser(server); - // } +export async function addHuman(request: AddHumanUserRequest) { + return userService.addHumanUser(request); +} + +export async function verifyTOTPRegistration(code: string, userId: string) { return userService.verifyTOTPRegistration({ code, userId }, {}); } @@ -250,62 +263,177 @@ export async function getUserByID(userId: string) { return userService.getUserByID({ userId }, {}); } -export async function listUsers(userName: string, organizationId: string) { - return userService.listUsers( +export async function verifyInviteCode( + userId: string, + verificationCode: string, +) { + return userService.verifyInviteCode({ userId, verificationCode }, {}); +} + +export async function resendInviteCode(userId: string) { + return userService.resendInviteCode({ userId }, {}); +} + +export async function sendEmailCode( + userId: string, + host: string | null, + authRequestId?: string, +) { + let medium = create(SendEmailCodeRequestSchema, { + userId, + }); + + if (host) { + medium = create(SendEmailCodeRequestSchema, { + ...medium, + verification: { + case: "sendCode", + value: create(SendEmailVerificationCodeSchema, { + urlTemplate: + `${host.includes("localhost") ? "http://" : "https://"}${host}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` + + (authRequestId ? `&authRequestId=${authRequestId}` : ""), + }), + }, + }); + } + + return userService.sendEmailCode(medium, {}); +} + +export async function createInviteCode(userId: string, host: string | null) { + let medium = create(SendInviteCodeSchema, { + applicationName: "Typescript Login", + }); + + if (host) { + medium = { + ...medium, + urlTemplate: `${host.includes("localhost") ? "http://" : "https://"}${host}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true`, + }; + } + + return userService.createInviteCode( { - queries: organizationId - ? [ - { - query: { - case: "userNameQuery", - value: { - userName, - method: TextQueryMethod.EQUALS, - }, - }, - }, - { - query: { - case: "organizationIdQuery", - value: { - organizationId, - }, - }, - }, - ] - : [ - { - query: { - case: "userNameQuery", - value: { - userName, - method: TextQueryMethod.EQUALS, - }, - }, - }, - ], + userId, + verification: { + case: "sendCode", + value: medium, + }, }, {}, ); } -export async function getOrgByDomain(domain: string) { - return managementService.getOrgByDomainGlobal({ domain }, {}); +export async function listUsers({ + loginName, + userName, + email, + organizationId, +}: { + loginName?: string; + userName?: string; + email?: string; + organizationId?: string; +}) { + const queries: SearchQuery[] = []; + + if (loginName) { + queries.push( + create(SearchQuerySchema, { + query: { + case: "loginNameQuery", + value: { + loginName: loginName, + method: TextQueryMethod.EQUALS, + }, + }, + }), + ); + } + + if (userName) { + queries.push( + create(SearchQuerySchema, { + query: { + case: "userNameQuery", + value: { + userName: userName, + method: TextQueryMethod.EQUALS, + }, + }, + }), + ); + } + + if (organizationId) { + queries.push( + create(SearchQuerySchema, { + query: { + case: "organizationIdQuery", + value: { + organizationId, + }, + }, + }), + ); + } + + if (email) { + queries.push( + create(SearchQuerySchema, { + query: { + case: "emailQuery", + value: { + emailAddress: email, + }, + }, + }), + ); + } + + return userService.listUsers({ queries: queries }); } -export const PROVIDER_NAME_MAPPING: { - [provider: string]: string; -} = { - [ProviderSlug.GOOGLE]: "Google", - [ProviderSlug.GITHUB]: "GitHub", -}; +export async function getDefaultOrg(): Promise { + return orgService + .listOrganizations( + { + queries: [ + { + query: { + case: "defaultQuery", + value: {}, + }, + }, + ], + }, + {}, + ) + .then((resp) => (resp?.result && resp.result[0] ? resp.result[0] : null)); +} + +export async function getOrgsByDomain(domain: string) { + return orgService.listOrganizations( + { + queries: [ + { + query: { + case: "domainQuery", + value: { domain, method: TextQueryMethod.EQUALS }, + }, + }, + ], + }, + {}, + ); +} export async function startIdentityProviderFlow({ idpId, urls, }: { idpId: string; - urls: PlainMessage; + urls: RedirectURLsJson; }) { return userService.startIdentityProviderIntent({ idpId, @@ -336,7 +464,7 @@ export async function getAuthRequest({ }); } -export async function createCallback(req: PlainMessage) { +export async function createCallback(req: CreateCallbackRequest) { return oidcService.createCallback(req); } @@ -350,14 +478,54 @@ export async function verifyEmail(userId: string, verificationCode: string) { ); } -/** - * - * @param userId the id of the user where the email should be set - * @returns the newly set email - */ -export async function resendEmailCode(userId: string) { - return userService.resendEmailCode( +export async function resendEmailCode( + userId: string, + host: string | null, + authRequestId?: string, +) { + let request: ResendEmailCodeRequest = create(ResendEmailCodeRequestSchema, { + userId, + }); + + if (host) { + const medium = create(SendEmailVerificationCodeSchema, { + urlTemplate: + `${host.includes("localhost") ? "http://" : "https://"}${host}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + + (authRequestId ? `&authRequestId=${authRequestId}` : ""), + }); + + request = { ...request, verification: { case: "sendCode", value: medium } }; + } + + return userService.resendEmailCode(request, {}); +} + +export function retrieveIDPIntent(id: string, token: string) { + return userService.retrieveIdentityProviderIntent( + { idpIntentId: id, idpIntentToken: token }, + {}, + ); +} + +export function getIDPByID(id: string) { + return idpService.getIDPByID({ id }, {}).then((resp) => resp.idp); +} + +export function addIDPLink( + idp: { + id: string; + userId: string; + userName: string; + }, + userId: string, +) { + return userService.addIDPLink( { + idpLink: { + userId: idp.userId, + idpId: idp.id, + userName: idp.userName, + }, userId, }, {}, @@ -369,39 +537,110 @@ export async function resendEmailCode(userId: string) { * @param userId the id of the user where the email should be set * @returns the newly set email */ -export async function passwordReset(userId: string): Promise { +export async function passwordReset( + userId: string, + host: string | null, + authRequestId?: string, +) { + let medium = create(SendPasswordResetLinkSchema, { + notificationType: NotificationType.Email, + }); + + if (host) { + medium = { + ...medium, + urlTemplate: + `${host.includes("localhost") ? "http://" : "https://"}${host}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + + (authRequestId ? `&authRequestId=${authRequestId}` : ""), + }; + } + return userService.passwordReset( { userId, + medium: { + case: "sendLink", + value: medium, + }, }, {}, ); } +/** + * + * @param userId userId of the user to set the password for + * @param password the new password + * @param code optional if the password should be set with a code (reset), no code for initial setup of password + * @returns + */ +export async function setUserPassword( + userId: string, + password: string, + user: User, + code?: string, +) { + let payload = create(SetPasswordRequestSchema, { + userId, + newPassword: { + password, + }, + }); + + // check if the user has no password set in order to set a password + if (!code) { + const authmethods = await listAuthenticationMethodTypes(userId); + + // if the user has no authmethods set, we can set a password otherwise we need a code + if ( + !(authmethods.authMethodTypes.length === 0) && + user.state !== UserState.INITIAL + ) { + return { error: "Provide a code to set a password" }; + } + } + + if (code) { + payload = { + ...payload, + verification: { + case: "verificationCode", + value: code, + }, + }; + } + + return userService.setPassword(payload, {}).catch((error) => { + // throw error if failed precondition (ex. User is not yet initialized) + if (error.code === 9 && error.message) { + return { error: error.message }; + } else { + throw error; + } + }); +} + +export async function setPassword(payload: SetPasswordRequest) { + return userService.setPassword(payload, {}); +} + /** * * @param server * @param userId the id of the user where the email should be set * @returns the newly set email */ + +// TODO check for token requirements! export async function createPasskeyRegistrationLink( userId: string, - token?: string, + // token: string, ) { - // let userService; - // if (token) { - // const authConfig: ZitadelServerOptions = { - // name: "zitadel login", - // apiUrl: process.env.ZITADEL_API_URL ?? "", - // token: token, - // }; - // - // const sessionUser = initializeServer(authConfig); - // userService = user.getUser(sessionUser); - // } else { - // userService = user.getUser(server); - // } + // const transport = createServerTransport(token, { + // baseUrl: process.env.ZITADEL_API_URL!, + // }); + // const service = createUserServiceClient(transport); return userService.createPasskeyRegistrationLink({ userId, medium: { @@ -417,6 +656,7 @@ export async function createPasskeyRegistrationLink( * @param domain the domain on which the factor is registered * @returns the newly set email */ + export async function registerU2F(userId: string, domain: string) { return userService.registerU2F({ userId, @@ -431,16 +671,20 @@ export async function registerU2F(userId: string, domain: string) { * @returns the newly set email */ export async function verifyU2FRegistration( - request: PlainMessage, + request: VerifyU2FRegistrationRequest, ) { return userService.verifyU2FRegistration(request, {}); } -export async function getActiveIdentityProviders(orgId?: string) { - return settingsService.getActiveIdentityProviders( - { ctx: makeReqCtx(orgId) }, - {}, - ); +export async function getActiveIdentityProviders( + orgId?: string, + linking_allowed?: boolean, +) { + const props: any = { ctx: makeReqCtx(orgId) }; + if (linking_allowed) { + props.linkingAllowed = linking_allowed; + } + return settingsService.getActiveIdentityProviders(props, {}); } /** @@ -449,24 +693,9 @@ export async function getActiveIdentityProviders(orgId?: string) { * @returns the newly set email */ export async function verifyPasskeyRegistration( - passkeyId: string, - passkeyName: string, - publicKeyCredential: - | { - [key: string]: any; - } - | undefined, - userId: string, + request: VerifyPasskeyRegistrationRequest, ) { - return userService.verifyPasskeyRegistration( - { - passkeyId, - passkeyName, - publicKeyCredential, - userId, - }, - {}, - ); + return userService.verifyPasskeyRegistration(request, {}); } /** @@ -483,7 +712,6 @@ export async function registerPasskey( userId, code, domain, - // authenticator: }); } diff --git a/apps/login/src/middleware.ts b/apps/login/src/middleware.ts index ab98c0f05f..93cb65581c 100644 --- a/apps/login/src/middleware.ts +++ b/apps/login/src/middleware.ts @@ -1,5 +1,4 @@ -import { NextResponse } from "next/server"; -import type { NextRequest } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; export const config = { matcher: [ @@ -18,7 +17,14 @@ export function middleware(request: NextRequest) { requestHeaders.set("x-zitadel-login-client", SERVICE_USER_ID); // this is a workaround for the next.js server not forwarding the host header - requestHeaders.set("x-zitadel-forwarded", `host="${request.nextUrl.host}"`); + // requestHeaders.set("x-zitadel-forwarded", `host="${request.nextUrl.host}"`); + requestHeaders.set("x-zitadel-public-host", `${request.nextUrl.host}`); + + // this is a workaround for the next.js server not forwarding the host header + requestHeaders.set( + "x-zitadel-instance-host", + `${INSTANCE}`.replace(/^https?:\/\//, ""), + ); const responseHeaders = new Headers(); responseHeaders.set("Access-Control-Allow-Origin", "*"); diff --git a/apps/login/src/styles/globals.scss b/apps/login/src/styles/globals.scss index bb028d1db6..cfce853bc7 100755 --- a/apps/login/src/styles/globals.scss +++ b/apps/login/src/styles/globals.scss @@ -1,6 +1,5 @@ // include styles from the ui package -@import "@zitadel/react/styles.css"; -@import "./vars.scss"; +@use "./vars.scss"; @tailwind base; @tailwind components; @@ -13,7 +12,7 @@ } .ztdl-p { - @apply text-sm text-text-light-secondary-500 dark:text-text-dark-secondary-500; + @apply text-sm text-center text-text-light-secondary-500 dark:text-text-dark-secondary-500 text-center; } } @@ -22,56 +21,45 @@ html { --dark-background-color: #000000; } -// $types: "background", "primary", "warn"; -// $shades: 50, 100, 200, 300, 400, 500, 600, 700, 800, 900; +.form-checkbox:checked { + background-image: url("/checkbox.svg"); +} -// @each $type in $types { -// @each $shade in $shades { -// .bg-#{$type}-light-#{$shade} { -// background-color: var(--theme-light-#{$type}-#{$shade}) !important; -// } -// .dark .dark\:bg-#{$type}-dark-#{$shade} { -// background-color: var(--theme-dark-#{$type}-#{$shade}) !important; -// } +.skeleton { + --accents-2: var(--theme-light-background-400); + --accents-1: var(--theme-light-background-500); -// .text-#{$type}-light-#{$shade} { -// color: var(--theme-light-#{$type}-#{$shade}) !important; -// } -// .dark .dark\:text-#{$type}-dark-#{$shade} { -// color: var(--theme-dark-#{$type}-#{$shade}) !important; -// } + background-image: linear-gradient( + 270deg, + var(--accents-1), + var(--accents-2), + var(--accents-2), + var(--accents-1) + ); + background-size: 400% 100%; + animation: skeleton_loading 8s ease-in-out infinite; +} -// .text-#{$type}-light-contrast-#{$shade} { -// color: var(--theme-light-#{$type}-contrast-#{$shade}) !important; -// } -// .dark .dark\:text-#{$type}-dark-contrast-#{$shade} { -// color: var(--theme-dark-#{$type}-contrast-#{$shade}) !important; -// } -// } -// } +.dark .skeleton { + --accents-2: var(--theme-dark-background-400); + --accents-1: var(--theme-dark-background-500); -// $alphatypes: "text", "link"; + background-image: linear-gradient( + 270deg, + var(--accents-1), + var(--accents-2), + var(--accents-2), + var(--accents-1) + ); + background-size: 400% 100%; + animation: skeleton_loading 8s ease-in-out infinite; +} -// @each $alphatype in $alphatypes { -// .text-#{$alphatype}-light, -// .hover\:text-#{$alphatype}-light { -// color: var(--theme-light-#{$alphatype}) !important; -// } -// .text-#{$alphatype}-light-contrast { -// color: var(--theme-light-#{$alphatype}-contrast) !important; -// } -// .text-#{$alphatype}-light-secondary { -// color: var(--theme-light-#{$alphatype}-secondary) !important; -// } - -// .dark .dark\:text-#{$alphatype}-dark, -// .dark .hover\:text-#{$alphatype}-dark { -// color: var(--theme-dark-#{$alphatype}) !important; -// } -// .dark .dark\:text-#{$alphatype}-dark-contrast { -// color: var(--theme-dark-#{$alphatype}-contrast) !important; -// } -// .dark .dark\:text-#{$alphatype}-dark-secondary { -// color: var(--theme-dark-#{$alphatype}-secondary) !important; -// } -// } +@keyframes skeleton_loading { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} diff --git a/apps/login/src/ui/AuthenticationMethodRadio.tsx b/apps/login/src/ui/AuthenticationMethodRadio.tsx deleted file mode 100644 index 1eb12ce668..0000000000 --- a/apps/login/src/ui/AuthenticationMethodRadio.tsx +++ /dev/null @@ -1,105 +0,0 @@ -"use client"; - -import { RadioGroup } from "@headlessui/react"; - -export const methods = [ - { - name: "Passkeys", - description: "Authenticate with your device.", - }, - { - name: "Password", - description: "Authenticate with a password", - }, -]; - -export default function AuthenticationMethodRadio({ - selected, - selectionChanged, -}: { - selected: any; - selectionChanged: (value: any) => void; -}) { - return ( -
-
- - Server size -
- {methods.map((method) => ( - - `${ - active - ? "h-full ring-2 ring-opacity-60 ring-primary-light-500 dark:ring-white/20" - : "h-full " - } - ${ - checked - ? "bg-background-light-400 dark:bg-background-dark-400" - : "bg-background-light-400 dark:bg-background-dark-400" - } - 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 }) => ( - <> -
-
-
- - {method.name} - - - {method.description} - {" "} - -
-
- {checked && ( -
- -
- )} -
- - )} -
- ))} -
-
-
-
- ); -} - -function CheckIcon(props: any) { - return ( - - - - - ); -} diff --git a/apps/login/src/ui/Checkbox.tsx b/apps/login/src/ui/Checkbox.tsx deleted file mode 100644 index c879e1aa9a..0000000000 --- a/apps/login/src/ui/Checkbox.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import classNames from "clsx"; -import React, { - DetailedHTMLProps, - forwardRef, - InputHTMLAttributes, - useEffect, - useState, -} from "react"; - -export type CheckboxProps = DetailedHTMLProps< - InputHTMLAttributes, - HTMLInputElement -> & { - checked: boolean; - disabled?: boolean; - onChangeVal?: (checked: boolean) => void; -}; - -export const Checkbox = forwardRef( - function Checkbox( - { - className = "", - checked = false, - disabled = false, - onChangeVal, - children, - ...props - }, - ref, - ) { - const [enabled, setEnabled] = useState(checked); - - useEffect(() => { - setEnabled(checked); - }, [checked]); - - return ( -
-
- { - setEnabled(event.target?.checked); - onChangeVal && onChangeVal(event.target?.checked); - }} - disabled={disabled} - type="checkbox" - className={classNames( - enabled - ? "border-none text-primary-light-500 dark:text-primary-dark-500 bg-primary-light-500 active:bg-primary-light-500 dark:bg-primary-dark-500 active:dark:bg-primary-dark-500" - : "border-2 border-gray-500 dark:border-white bg-transparent dark:bg-transparent", - "focus:border-gray-500 focus:dark:border-white focus:ring-opacity-40 focus:dark:ring-opacity-40 focus:ring-offset-0 focus:ring-2 dark:focus:ring-offset-0 dark:focus:ring-2 focus:ring-gray-500 focus:dark:ring-white", - "h-4 w-4 rounded-sm ring-0 outline-0 checked:ring-0 checked:dark:ring-0 active:border-none active:ring-0", - "disabled:bg-gray-500 disabled:text-gray-500 disabled:border-gray-200 disabled:cursor-not-allowed", - className, - )} - {...props} - /> -
- {children} -
- ); - }, -); diff --git a/apps/login/src/ui/DynamicTheme.tsx b/apps/login/src/ui/DynamicTheme.tsx deleted file mode 100644 index 7ab210437f..0000000000 --- a/apps/login/src/ui/DynamicTheme.tsx +++ /dev/null @@ -1,50 +0,0 @@ -"use client"; - -import React from "react"; -import { Logo } from "@/ui/Logo"; -import ThemeWrapper from "./ThemeWrapper"; -import { LayoutProviders } from "./LayoutProviders"; -import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb"; - -export default function DynamicTheme({ - branding, - children, -}: { - children: React.ReactNode; - branding?: BrandingSettings; -}) { - let partial: Partial | undefined; - if (branding) { - partial = { - lightTheme: branding?.lightTheme, - darkTheme: branding?.darkTheme, - }; - } - return ( - - {/* */} - -
-
-
-
- {branding && ( - - )} -
- -
{children}
-
-
-
-
-
- {/*
*/} -
- ); -} diff --git a/apps/login/src/ui/GlobalNav.tsx b/apps/login/src/ui/GlobalNav.tsx deleted file mode 100644 index 6a35d0fd2d..0000000000 --- a/apps/login/src/ui/GlobalNav.tsx +++ /dev/null @@ -1,120 +0,0 @@ -"use client"; - -import { demos, type Item } from "@/lib/demos"; -import { ZitadelLogo } from "@/ui/ZitadelLogo"; -import Link from "next/link"; -import { useSelectedLayoutSegment, usePathname } from "next/navigation"; -import clsx from "clsx"; -import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/solid"; -import { useState } from "react"; -import Theme from "./Theme"; - -export function GlobalNav() { - const [isOpen, setIsOpen] = useState(false); - const close = () => setIsOpen(false); - - return ( -
-
- -
- -
- -

- Login -

- -
- -
- - -
- -
- - -
- -
-
-
- ); -} - -function GlobalNavItem({ - item, - close, -}: { - item: Item; - close: () => false | void; -}) { - const segment = useSelectedLayoutSegment(); - const pathname = usePathname(); - - const isActive = `/${item.slug}` === pathname; - - return ( - - {item.name} - - ); -} diff --git a/apps/login/src/ui/IdpSignin.tsx b/apps/login/src/ui/IdpSignin.tsx deleted file mode 100644 index 72391d1b11..0000000000 --- a/apps/login/src/ui/IdpSignin.tsx +++ /dev/null @@ -1,90 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { Spinner } from "./Spinner"; -import Alert from "./Alert"; -import { useRouter } from "next/navigation"; - -type Props = { - userId: string; - // organization: string; - idpIntent: { - idpIntentId: string; - idpIntentToken: string; - }; - authRequestId?: string; -}; - -export default function IdpSignin(props: Props) { - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const router = useRouter(); - - async function createSessionForIdp() { - setLoading(true); - const res = await fetch("/api/session", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - userId: props.userId, - idpIntent: props.idpIntent, - authRequestId: props.authRequestId, - // organization: props.organization, - }), - }); - - if (!res.ok) { - const error = await res.json(); - throw error.details.details; - } - return res.json(); - } - - useEffect(() => { - createSessionForIdp() - .then((session) => { - setLoading(false); - if (props.authRequestId && session && session.sessionId) { - return router.push( - `/login?` + - new URLSearchParams({ - sessionId: session.sessionId, - authRequest: props.authRequestId, - }), - ); - } else { - return router.push( - `/signedin?` + - new URLSearchParams( - props.authRequestId - ? { - loginName: session.factors.user.loginName, - authRequestId: props.authRequestId, - } - : { - loginName: session.factors.user.loginName, - }, - ), - ); - } - }) - .catch((error) => { - setLoading(false); - setError(error.message); - }); - }, []); - - return ( -
- {loading && } - {error && ( -
- {error} -
- )} -
- ); -} diff --git a/apps/login/src/ui/LoginOTP.tsx b/apps/login/src/ui/LoginOTP.tsx deleted file mode 100644 index 2925982710..0000000000 --- a/apps/login/src/ui/LoginOTP.tsx +++ /dev/null @@ -1,247 +0,0 @@ -"use client"; - -import { useEffect, useRef, useState } from "react"; -import { useRouter } from "next/navigation"; -import { Button, ButtonVariants } from "./Button"; -import Alert, { AlertType } from "./Alert"; -import { Spinner } from "./Spinner"; -import { useForm } from "react-hook-form"; -import { TextInput } from "./Input"; -import BackButton from "./BackButton"; -import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; -import { PlainMessage } from "@zitadel/client"; -import { Challenges } from "@zitadel/proto/zitadel/session/v2/challenge_pb"; - -// either loginName or sessionId must be provided -type Props = { - loginName?: string; - sessionId?: string; - authRequestId?: string; - organization?: string; - method: string; - code?: string; -}; - -type Inputs = { - code: string; -}; - -export default function LoginOTP({ - loginName, - sessionId, - authRequestId, - organization, - method, - code, -}: Props) { - const [error, setError] = useState(""); - const [loading, setLoading] = useState(false); - - const router = useRouter(); - - const initialized = useRef(false); - - const { register, handleSubmit, formState } = useForm({ - mode: "onBlur", - defaultValues: { - code: code ? code : "", - }, - }); - - useEffect(() => { - if (!initialized.current && ["email", "sms"].includes(method)) { - initialized.current = true; - setLoading(true); - updateSessionForOTPChallenge() - .then((response) => { - setLoading(false); - }) - .catch((error) => { - setError(error); - setLoading(false); - }); - } - }, []); - - async function updateSessionForOTPChallenge() { - const challenges: PlainMessage = {}; - - if (method === "email") { - challenges.otpEmail = ""; - } - - if (method === "sms") { - challenges.otpSms = ""; - } - setLoading(true); - const res = await fetch("/api/session", { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - loginName, - sessionId, - organization, - challenges, - authRequestId, - }), - }); - - setLoading(false); - if (!res.ok) { - const error = await res.json(); - throw error.details.details; - } - return res.json(); - } - - async function submitCode(values: Inputs, organization?: string) { - setLoading(true); - - let body: any = { - code: values.code, - method, - }; - - if (organization) { - body.organization = organization; - } - - if (authRequestId) { - body.authRequestId = authRequestId; - } - - const checks: PlainMessage = {}; - if (method === "sms") { - checks.otpSms = { code: values.code }; - } - if (method === "email") { - checks.otpEmail = { code: values.code }; - } - if (method === "time-based") { - checks.totp = { code: values.code }; - } - - const res = await fetch("/api/session", { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - loginName, - sessionId, - organization, - checks, - authRequestId, - }), - }); - - setLoading(false); - if (!res.ok) { - const response = await res.json(); - - setError(response.details.details ?? "An internal error occurred"); - return Promise.reject( - response.details.details ?? "An internal error occurred", - ); - } - return res.json(); - } - - function setCodeAndContinue(values: Inputs, organization?: string) { - return submitCode(values, organization).then((response) => { - if (authRequestId && response && response.sessionId) { - const params = new URLSearchParams({ - sessionId: response.sessionId, - authRequest: authRequestId, - }); - - if (organization) { - params.append("organization", organization); - } - - return router.push(`/login?` + params); - } else { - const params = new URLSearchParams( - authRequestId - ? { - loginName: response.factors.user.loginName, - authRequestId, - } - : { - loginName: response.factors.user.loginName, - }, - ); - - if (organization) { - params.append("organization", organization); - } - - return router.push(`/signedin?` + params); - } - }); - } - - const { errors } = formState; - - return ( -
- {["email", "sms"].includes(method) && ( - -
- - Did not get the Code? - - -
-
- )} -
- -
- - {error && ( -
- {error} -
- )} - -
- - - -
-
- ); -} diff --git a/apps/login/src/ui/LoginPasskey.tsx b/apps/login/src/ui/LoginPasskey.tsx deleted file mode 100644 index fce8ecd8ec..0000000000 --- a/apps/login/src/ui/LoginPasskey.tsx +++ /dev/null @@ -1,271 +0,0 @@ -"use client"; - -import { useEffect, useRef, useState } from "react"; -import { useRouter } from "next/navigation"; -import { coerceToArrayBuffer, coerceToBase64Url } from "@/utils/base64"; -import { Button, ButtonVariants } from "./Button"; -import Alert from "./Alert"; -import { Spinner } from "./Spinner"; -import BackButton from "./BackButton"; -import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; - -// either loginName or sessionId must be provided -type Props = { - loginName?: string; - sessionId?: string; - authRequestId?: string; - altPassword: boolean; - login?: boolean; - organization?: string; -}; - -export default function LoginPasskey({ - loginName, - sessionId, - authRequestId, - altPassword, - organization, - login = true, -}: Props) { - const [error, setError] = useState(""); - const [loading, setLoading] = useState(false); - - const router = useRouter(); - - const initialized = useRef(false); - - // TODO: move this to server side - useEffect(() => { - if (!initialized.current) { - initialized.current = true; - setLoading(true); - updateSessionForChallenge() - .then((response) => { - const pK = - response.challenges.webAuthN.publicKeyCredentialRequestOptions - .publicKey; - if (pK) { - submitLoginAndContinue(pK) - .then(() => { - setLoading(false); - }) - .catch((error) => { - setError(error); - setLoading(false); - }); - } else { - setError("Could not request passkey challenge"); - setLoading(false); - } - }) - .catch((error) => { - setError(error); - setLoading(false); - }); - } - }, []); - - async function updateSessionForChallenge( - userVerificationRequirement: number = login ? 1 : 3, - ) { - setLoading(true); - const res = await fetch("/api/session", { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - loginName, - sessionId, - organization, - challenges: { - webAuthN: { - domain: "", - // USER_VERIFICATION_REQUIREMENT_UNSPECIFIED = 0; - // USER_VERIFICATION_REQUIREMENT_REQUIRED = 1; - passkey login - // USER_VERIFICATION_REQUIREMENT_PREFERRED = 2; - // USER_VERIFICATION_REQUIREMENT_DISCOURAGED = 3; - mfa - userVerificationRequirement: userVerificationRequirement, - }, - }, - authRequestId, - }), - }); - - setLoading(false); - if (!res.ok) { - const error = await res.json(); - throw error.details.details; - } - return res.json(); - } - - async function submitLogin(data: any) { - setLoading(true); - const res = await fetch("/api/session", { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - loginName, - sessionId, - organization, - checks: { - webAuthN: { credentialAssertionData: data }, - } as Checks, - authRequestId, - }), - }); - - const response = await res.json(); - - setLoading(false); - if (!res.ok) { - setError(response.details); - return Promise.reject(response.details); - } - return response; - } - - async function submitLoginAndContinue( - publicKey: any, - ): Promise { - publicKey.challenge = coerceToArrayBuffer( - publicKey.challenge, - "publicKey.challenge", - ); - publicKey.allowCredentials.map((listItem: any) => { - listItem.id = coerceToArrayBuffer( - listItem.id, - "publicKey.allowCredentials.id", - ); - }); - - navigator.credentials - .get({ - publicKey, - }) - .then((assertedCredential: any) => { - if (assertedCredential) { - const authData = new Uint8Array( - assertedCredential.response.authenticatorData, - ); - const clientDataJSON = new Uint8Array( - assertedCredential.response.clientDataJSON, - ); - const rawId = new Uint8Array(assertedCredential.rawId); - const sig = new Uint8Array(assertedCredential.response.signature); - const userHandle = new Uint8Array( - assertedCredential.response.userHandle, - ); - const data = { - id: assertedCredential.id, - rawId: coerceToBase64Url(rawId, "rawId"), - type: assertedCredential.type, - response: { - authenticatorData: coerceToBase64Url(authData, "authData"), - clientDataJSON: coerceToBase64Url( - clientDataJSON, - "clientDataJSON", - ), - signature: coerceToBase64Url(sig, "sig"), - userHandle: coerceToBase64Url(userHandle, "userHandle"), - }, - }; - return submitLogin(data).then((resp) => { - if (authRequestId && resp && resp.sessionId) { - return router.push( - `/login?` + - new URLSearchParams({ - sessionId: resp.sessionId, - authRequest: authRequestId, - }), - ); - } else { - return router.push( - `/signedin?` + - new URLSearchParams( - authRequestId - ? { - loginName: resp.factors.user.loginName, - authRequestId, - } - : { - loginName: resp.factors.user.loginName, - }, - ), - ); - } - }); - } else { - setLoading(false); - setError("An error on retrieving passkey"); - return null; - } - }) - .catch((error) => { - console.error(error); - setLoading(false); - // setError(error); - return null; - }); - } - - return ( -
- {error && ( -
- {error} -
- )} -
- {altPassword ? ( - - ) : ( - - )} - - - -
-
- ); -} diff --git a/apps/login/src/ui/MobileNavToggle.tsx b/apps/login/src/ui/MobileNavToggle.tsx deleted file mode 100644 index fe9290b9e3..0000000000 --- a/apps/login/src/ui/MobileNavToggle.tsx +++ /dev/null @@ -1,64 +0,0 @@ -"use client"; - -import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/solid"; -import clsx from "clsx"; -import React from "react"; - -const MobileNavContext = React.createContext< - [boolean, React.Dispatch>] | undefined ->(undefined); - -export function MobileNavContextProvider({ - children, -}: { - children: React.ReactNode; -}) { - const [isOpen, setIsOpen] = React.useState(false); - return ( - - {children} - - ); -} - -export function useMobileNavToggle() { - const context = React.useContext(MobileNavContext); - if (context === undefined) { - throw new Error( - "useMobileNavToggle must be used within a MobileNavContextProvider", - ); - } - return context; -} - -export function MobileNavToggle({ children }: { children: React.ReactNode }) { - const [isOpen, setIsOpen] = useMobileNavToggle(); - - return ( - <> - - -
- {children} -
- - ); -} diff --git a/apps/login/src/ui/PasswordForm.tsx b/apps/login/src/ui/PasswordForm.tsx deleted file mode 100644 index d4d94cd34b..0000000000 --- a/apps/login/src/ui/PasswordForm.tsx +++ /dev/null @@ -1,267 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { Button, ButtonVariants } from "./Button"; -import { TextInput } from "./Input"; -import { useForm } from "react-hook-form"; -import { useRouter } from "next/navigation"; -import { Spinner } from "./Spinner"; -import Alert from "./Alert"; -import BackButton from "./BackButton"; -import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; -import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; -import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; - -type Inputs = { - password: string; -}; - -type Props = { - loginSettings: LoginSettings | undefined; - loginName?: string; - organization?: string; - authRequestId?: string; - isAlternative?: boolean; // whether password was requested as alternative auth method - promptPasswordless?: boolean; -}; - -export default function PasswordForm({ - loginSettings, - loginName, - organization, - authRequestId, - promptPasswordless, - isAlternative, -}: Props) { - const { register, handleSubmit, formState } = useForm({ - mode: "onBlur", - }); - - const [error, setError] = useState(""); - - const [loading, setLoading] = useState(false); - - const router = useRouter(); - - async function submitPassword(values: Inputs) { - setError(""); - setLoading(true); - - const res = await fetch("/api/session", { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - loginName, - organization, - checks: { - password: { password: values.password }, - } as Checks, - authRequestId, - }), - }); - - const response = await res.json(); - - setLoading(false); - if (!res.ok) { - setError(response.details?.details ?? "Could not verify password"); - return Promise.reject(response.details); - } - return response; - } - - async function resetPassword() { - setError(""); - setLoading(true); - - const res = await fetch("/api/resetpassword", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - loginName, - organization, - authRequestId, - }), - }); - - const response = await res.json(); - - setLoading(false); - if (!res.ok) { - console.log(response.details.details); - setError(response.details?.details ?? "Could not verify password"); - return Promise.reject(response.details); - } - return response; - } - - function submitPasswordAndContinue(value: Inputs): Promise { - return submitPassword(value).then((resp) => { - // if user has mfa -> /otp/[method] or /u2f - // if mfa is forced and user has no mfa -> /mfa/set - // if no passwordless -> /passkey/add - - // exclude password and passwordless - const availableSecondFactors = resp.authMethods?.filter( - (m: AuthenticationMethodType) => - m !== AuthenticationMethodType.PASSWORD && - m !== AuthenticationMethodType.PASSKEY, - ); - - if (availableSecondFactors.length == 1) { - const params = new URLSearchParams({ - loginName: resp.factors.user.loginName, - }); - - if (authRequestId) { - params.append("authRequestId", authRequestId); - } - - if (organization) { - params.append("organization", organization); - } - - const factor = availableSecondFactors[0]; - // if passwordless is other method, but user selected password as alternative, perform a login - if (factor === AuthenticationMethodType.TOTP) { - return router.push(`/otp/time-based?` + params); - } else if (factor === AuthenticationMethodType.OTP_SMS) { - return router.push(`/otp/sms?` + params); - } else if (factor === AuthenticationMethodType.OTP_EMAIL) { - return router.push(`/otp/email?` + params); - } else if (factor === AuthenticationMethodType.U2F) { - return router.push(`/u2f?` + params); - } - } else if (availableSecondFactors.length >= 1) { - const params = new URLSearchParams({ - loginName: resp.factors.user.loginName, - }); - - if (authRequestId) { - params.append("authRequestId", authRequestId); - } - - if (organization) { - params.append("organization", organization); - } - - return router.push(`/mfa?` + params); - } else if ( - resp.factors && - !resp.factors.passwordless && // if session was not verified with a passkey - promptPasswordless && // if explicitly prompted due policy - !isAlternative // escaped if password was used as an alternative method - ) { - const params = new URLSearchParams({ - loginName: resp.factors.user.loginName, - promptPasswordless: "true", - }); - - if (authRequestId) { - params.append("authRequestId", authRequestId); - } - - if (organization) { - params.append("organization", organization); - } - - return router.push(`/passkey/add?` + params); - } else if (loginSettings?.forceMfa && !availableSecondFactors.length) { - const params = new URLSearchParams({ - loginName: resp.factors.user.loginName, - checkAfter: "true", // this defines if the check is directly made after the setup - }); - - if (authRequestId) { - params.append("authRequestId", authRequestId); - } - - if (organization) { - params.append("organization", organization); - } - - return router.push(`/mfa/set?` + params); - } else if (authRequestId && resp.sessionId) { - const params = new URLSearchParams({ - sessionId: resp.sessionId, - authRequest: authRequestId, - }); - - if (organization) { - params.append("organization", organization); - } - - return router.push(`/login?` + params); - } else { - // without OIDC flow - const params = new URLSearchParams( - authRequestId - ? { - loginName: resp.factors.user.loginName, - authRequestId, - } - : { - loginName: resp.factors.user.loginName, - }, - ); - - if (organization) { - params.append("organization", organization); - } - - return router.push(`/signedin?` + params); - } - }); - } - - return ( -
-
- - - - {loginName && ( - - )} -
- - {error && ( -
- {error} -
- )} - -
- - - -
-
- ); -} diff --git a/apps/login/src/ui/RegisterPasskey.tsx b/apps/login/src/ui/RegisterPasskey.tsx deleted file mode 100644 index 4ad72b583b..0000000000 --- a/apps/login/src/ui/RegisterPasskey.tsx +++ /dev/null @@ -1,232 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { Button, ButtonVariants } from "./Button"; -import { useForm } from "react-hook-form"; -import { useRouter } from "next/navigation"; -import { Spinner } from "./Spinner"; -import Alert from "./Alert"; -import { coerceToArrayBuffer, coerceToBase64Url } from "@/utils/base64"; -import BackButton from "./BackButton"; -import { RegisterPasskeyResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; - -type Inputs = {}; - -type Props = { - sessionId: string; - isPrompt: boolean; - authRequestId?: string; - organization?: string; -}; - -export default function RegisterPasskey({ - sessionId, - isPrompt, - organization, - authRequestId, -}: Props) { - const { register, handleSubmit, formState } = useForm({ - mode: "onBlur", - }); - - const [error, setError] = useState(""); - - const [loading, setLoading] = useState(false); - - const router = useRouter(); - - async function submitRegister() { - setError(""); - setLoading(true); - const res = await fetch("/api/passkeys", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - sessionId, - }), - }); - - const response = await res.json(); - - setLoading(false); - if (!res.ok) { - setError(response.details); - return Promise.reject(response.details); - } - return response; - } - - async function submitVerify( - passkeyId: string, - passkeyName: string, - publicKeyCredential: any, - sessionId: string, - ) { - setLoading(true); - const res = await fetch("/api/passkeys/verify", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - passkeyId, - passkeyName, - publicKeyCredential, - sessionId, - }), - }); - - const response = await res.json(); - - setLoading(false); - if (!res.ok) { - setError(response.details); - return Promise.reject(response.details); - } - return response; - } - - function submitRegisterAndContinue(value: Inputs): Promise { - return submitRegister().then((resp: RegisterPasskeyResponse) => { - const passkeyId = resp.passkeyId; - const options: CredentialCreationOptions = - (resp.publicKeyCredentialCreationOptions?.toJson() as CredentialCreationOptions) ?? - {}; - - if (options?.publicKey) { - options.publicKey.challenge = coerceToArrayBuffer( - options.publicKey.challenge, - "challenge", - ); - options.publicKey.user.id = coerceToArrayBuffer( - options.publicKey.user.id, - "userid", - ); - if (options.publicKey.excludeCredentials) { - options.publicKey.excludeCredentials.map((cred: any) => { - cred.id = coerceToArrayBuffer( - cred.id as string, - "excludeCredentials.id", - ); - return cred; - }); - } - - navigator.credentials - .create(options) - .then((resp) => { - if ( - resp && - (resp as any).response.attestationObject && - (resp as any).response.clientDataJSON && - (resp as any).rawId - ) { - const attestationObject = (resp as any).response - .attestationObject; - const clientDataJSON = (resp as any).response.clientDataJSON; - const rawId = (resp as any).rawId; - - const data = { - id: resp.id, - rawId: coerceToBase64Url(rawId, "rawId"), - type: resp.type, - response: { - attestationObject: coerceToBase64Url( - attestationObject, - "attestationObject", - ), - clientDataJSON: coerceToBase64Url( - clientDataJSON, - "clientDataJSON", - ), - }, - }; - return submitVerify(passkeyId, "", data, sessionId).then(() => { - const params = new URLSearchParams(); - - if (organization) { - params.set("organization", organization); - } - - if (authRequestId) { - params.set("authRequestId", authRequestId); - params.set("sessionId", sessionId); - // params.set("altPassword", ${false}); // without setting altPassword this does not allow password - // params.set("loginName", resp.loginName); - - router.push("/passkey/login?" + params); - } else { - router.push("/accounts?" + params); - } - }); - } else { - setLoading(false); - setError("An error on registering passkey"); - return null; - } - }) - .catch((error) => { - console.error(error); - setLoading(false); - setError(error); - - return null; - }); - } - }); - } - - const { errors } = formState; - - return ( -
- {error && ( -
- {error} -
- )} - -
- {isPrompt ? ( - - ) : ( - - )} - - - -
-
- ); -} diff --git a/apps/login/src/ui/RegisterU2F.tsx b/apps/login/src/ui/RegisterU2F.tsx deleted file mode 100644 index eebbbe87b2..0000000000 --- a/apps/login/src/ui/RegisterU2F.tsx +++ /dev/null @@ -1,206 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { Button, ButtonVariants } from "./Button"; -import { useForm } from "react-hook-form"; -import { useRouter } from "next/navigation"; -import { Spinner } from "./Spinner"; -import Alert from "./Alert"; -import { coerceToArrayBuffer, coerceToBase64Url } from "@/utils/base64"; -import BackButton from "./BackButton"; -import { RegisterU2FResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; - -type Inputs = {}; - -type Props = { - sessionId: string; - authRequestId?: string; - organization?: string; -}; - -export default function RegisterU2F({ - sessionId, - organization, - authRequestId, -}: Props) { - const { register, handleSubmit, formState } = useForm({ - mode: "onBlur", - }); - - const [error, setError] = useState(""); - - const [loading, setLoading] = useState(false); - - const router = useRouter(); - - async function submitRegister() { - setError(""); - setLoading(true); - const res = await fetch("/api/u2f", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - sessionId, - }), - }); - - const response = await res.json(); - - setLoading(false); - if (!res.ok) { - setError(response.details); - return Promise.reject(response.details); - } - return response; - } - - async function submitVerify( - u2fId: string, - passkeyName: string, - publicKeyCredential: any, - sessionId: string, - ) { - setLoading(true); - const res = await fetch("/api/u2f/verify", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - u2fId, - passkeyName, - publicKeyCredential, - sessionId, - }), - }); - - const response = await res.json(); - - setLoading(false); - if (!res.ok) { - setError(response.details); - return Promise.reject(response.details); - } - return response; - } - - function submitRegisterAndContinue(value: Inputs): Promise { - return submitRegister().then((resp: RegisterU2FResponse) => { - const u2fId = resp.u2fId; - const options: CredentialCreationOptions = - (resp.publicKeyCredentialCreationOptions?.toJson() as CredentialCreationOptions) ?? - {}; - - if (options.publicKey) { - options.publicKey.challenge = coerceToArrayBuffer( - options.publicKey.challenge, - "challenge", - ); - options.publicKey.user.id = coerceToArrayBuffer( - options.publicKey.user.id, - "userid", - ); - if (options.publicKey.excludeCredentials) { - options.publicKey.excludeCredentials.map((cred: any) => { - cred.id = coerceToArrayBuffer( - cred.id as string, - "excludeCredentials.id", - ); - return cred; - }); - } - - navigator.credentials - .create(options) - .then((resp) => { - if ( - resp && - (resp as any).response.attestationObject && - (resp as any).response.clientDataJSON && - (resp as any).rawId - ) { - const attestationObject = (resp as any).response - .attestationObject; - const clientDataJSON = (resp as any).response.clientDataJSON; - const rawId = (resp as any).rawId; - - const data = { - id: resp.id, - rawId: coerceToBase64Url(rawId, "rawId"), - type: resp.type, - response: { - attestationObject: coerceToBase64Url( - attestationObject, - "attestationObject", - ), - clientDataJSON: coerceToBase64Url( - clientDataJSON, - "clientDataJSON", - ), - }, - }; - return submitVerify(u2fId, "", data, sessionId).then(() => { - const params = new URLSearchParams(); - - if (organization) { - params.set("organization", organization); - } - - if (authRequestId) { - params.set("authRequestId", authRequestId); - params.set("sessionId", sessionId); - // params.set("altPassword", ${false}); // without setting altPassword this does not allow password - // params.set("loginName", resp.loginName); - - router.push("/u2f?" + params); - } else { - router.push("/accounts?" + params); - } - }); - } else { - setLoading(false); - setError("An error on registering passkey"); - return null; - } - }) - .catch((error) => { - console.error(error); - setLoading(false); - setError(error); - - return null; - }); - } - }); - } - - const { errors } = formState; - - return ( -
- {error && ( -
- {error} -
- )} - -
- - - - -
-
- ); -} diff --git a/apps/login/src/ui/SessionItem.tsx b/apps/login/src/ui/SessionItem.tsx deleted file mode 100644 index f9c2b60c00..0000000000 --- a/apps/login/src/ui/SessionItem.tsx +++ /dev/null @@ -1,127 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { useState } from "react"; -import { Avatar } from "./Avatar"; -import moment from "moment"; -import { XCircleIcon } from "@heroicons/react/24/outline"; -import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; - -export default function SessionItem({ - session, - reload, - authRequestId, -}: { - session: Session; - reload: () => void; - authRequestId?: string; -}) { - // TODO: remove casting when bufbuild/protobuf-es@v2 is released - session = Session.fromJson(session as any); - const [loading, setLoading] = useState(false); - - async function clearSession(id: string) { - setLoading(true); - const res = await fetch("/api/session?" + new URLSearchParams({ id }), { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - id: id, - }), - }); - - const response = await res.json(); - - setLoading(false); - if (!res.ok) { - // setError(response.details); - return Promise.reject(response); - } else { - return response; - } - } - - const validPassword = session?.factors?.password?.verifiedAt; - const validPasskey = session?.factors?.webAuthN?.verifiedAt; - const stillValid = session.expirationDate - ? session.expirationDate.toDate() > new Date() - : true; - - const validDate = validPassword || validPasskey; - const validUser = (validPassword || validPasskey) && stillValid; - - return ( - -
- -
- -
- {session.factors?.user?.displayName} - - {session.factors?.user?.loginName} - - {validUser && ( - - {validDate && moment(validDate.toDate()).fromNow()} - - )} -
- - -
- {validUser ? ( -
- ) : ( -
- )} - - { - event.preventDefault(); - clearSession(session.id).then(() => { - reload(); - }); - }} - /> -
- - ); -} diff --git a/apps/login/src/ui/SessionsList.tsx b/apps/login/src/ui/SessionsList.tsx deleted file mode 100644 index 8a7c794440..0000000000 --- a/apps/login/src/ui/SessionsList.tsx +++ /dev/null @@ -1,35 +0,0 @@ -"use client"; - -import SessionItem from "./SessionItem"; -import Alert from "./Alert"; -import { useEffect, useState } from "react"; -import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; - -type Props = { - sessions: Session[]; - authRequestId?: string; -}; - -export default function SessionsList({ sessions, authRequestId }: Props) { - const [list, setList] = useState(sessions); - return sessions ? ( -
- {list - .filter((session) => session?.factors?.user?.loginName) - .map((session, index) => { - return ( - { - setList(list.filter((s) => s.id !== session.id)); - }} - key={"session-" + index} - /> - ); - })} -
- ) : ( - No Sessions available! - ); -} diff --git a/apps/login/src/ui/SignInWithIDP.tsx b/apps/login/src/ui/SignInWithIDP.tsx deleted file mode 100644 index eee49e2572..0000000000 --- a/apps/login/src/ui/SignInWithIDP.tsx +++ /dev/null @@ -1,154 +0,0 @@ -"use client"; -import { ReactNode, useState } from "react"; - -import { - SignInWithGitlab, - SignInWithAzureAD, - SignInWithGoogle, - SignInWithGithub, -} from "@zitadel/react"; -import { useRouter } from "next/navigation"; -import { ProviderSlug } from "@/lib/demos"; -import Alert from "./Alert"; -import BackButton from "./BackButton"; -import { IdentityProvider } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; - -export interface SignInWithIDPProps { - children?: ReactNode; - host: string; - identityProviders: IdentityProvider[]; - authRequestId?: string; - organization?: string; -} - -export function SignInWithIDP({ - host, - identityProviders, - authRequestId, - organization, -}: SignInWithIDPProps) { - // TODO: remove casting when bufbuild/protobuf-es@v2 is released - identityProviders = identityProviders.map((idp) => - IdentityProvider.fromJson(idp as any), - ); - - const [loading, setLoading] = useState(false); - const [error, setError] = useState(""); - const router = useRouter(); - - async function startFlow(idpId: string, provider: ProviderSlug) { - setLoading(true); - - const params = new URLSearchParams(); - - if (authRequestId) { - params.set("authRequestId", authRequestId); - } - - if (organization) { - params.set("organization", organization); - } - - const res = await fetch("/api/idp/start", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - idpId, - successUrl: - `${host}/idp/${provider}/success?` + new URLSearchParams(params), - failureUrl: - `${host}/idp/${provider}/failure?` + new URLSearchParams(params), - }), - }); - - const response = await res.json(); - - setLoading(false); - if (!res.ok) { - setError(response.details); - return Promise.reject(response.details); - } - return response; - } - - return ( -
- {identityProviders && - identityProviders.map((idp, i) => { - switch (idp.type) { - case 6: // IdentityProviderType.IDENTITY_PROVIDER_TYPE_GITHUB: - return ( - - startFlow(idp.id, ProviderSlug.GITHUB).then( - ({ authUrl }) => { - router.push(authUrl); - }, - ) - } - > - ); - case 7: // IdentityProviderType.IDENTITY_PROVIDER_TYPE_GITHUB_ES: - return ( - alert("TODO: unimplemented")} - > - ); - case 5: // IdentityProviderType.IDENTITY_PROVIDER_TYPE_AZURE_AD: - return ( - alert("TODO: unimplemented")} - > - ); - case 10: // IdentityProviderType.IDENTITY_PROVIDER_TYPE_GOOGLE: - return ( - - startFlow(idp.id, ProviderSlug.GOOGLE).then( - ({ authUrl }) => { - router.push(authUrl); - }, - ) - } - > - ); - case 8: // IdentityProviderType.IDENTITY_PROVIDER_TYPE_GITLAB: - return ( - alert("TODO: unimplemented")} - > - ); - case 9: //IdentityProviderType.IDENTITY_PROVIDER_TYPE_GITLAB_SELF_HOSTED: - return ( - alert("TODO: unimplemented")} - > - ); - default: - return null; - } - })} - {error && ( -
- {error} -
- )} -
- - -
-
- ); -} - -SignInWithIDP.displayName = "SignInWithIDP"; diff --git a/apps/login/src/ui/ThemeWrapper.tsx b/apps/login/src/ui/ThemeWrapper.tsx deleted file mode 100644 index c5b99b1ac0..0000000000 --- a/apps/login/src/ui/ThemeWrapper.tsx +++ /dev/null @@ -1,23 +0,0 @@ -"use client"; - -import { setTheme } from "@/utils/colors"; -import { useEffect } from "react"; -import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb"; -import { PartialMessage } from "@zitadel/client"; - -type Props = { - branding: PartialMessage | undefined; - children: React.ReactNode; -}; - -const ThemeWrapper = ({ children, branding }: Props) => { - useEffect(() => { - setTheme(document, branding); - }, []); - - const defaultClasses = "bg-background-light-600 dark:bg-background-dark-600"; - - return
{children}
; -}; - -export default ThemeWrapper; diff --git a/apps/login/src/ui/UsernameForm.tsx b/apps/login/src/ui/UsernameForm.tsx deleted file mode 100644 index a470d77aec..0000000000 --- a/apps/login/src/ui/UsernameForm.tsx +++ /dev/null @@ -1,258 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { Button, ButtonVariants } from "./Button"; -import { TextInput } from "./Input"; -import { useForm } from "react-hook-form"; -import { useRouter } from "next/navigation"; -import { Spinner } from "./Spinner"; -import Alert from "./Alert"; -import { - LoginSettings, - PasskeysType, -} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; - -type Inputs = { - loginName: string; -}; - -type Props = { - loginSettings: LoginSettings | undefined; - loginName: string | undefined; - authRequestId: string | undefined; - organization?: string; - submit: boolean; - allowRegister: boolean; -}; - -export default function UsernameForm({ - loginSettings, - loginName, - authRequestId, - organization, - submit, - allowRegister, -}: Props) { - const { register, handleSubmit, formState } = useForm({ - mode: "onBlur", - defaultValues: { - loginName: loginName ? loginName : "", - }, - }); - - const router = useRouter(); - - const [loading, setLoading] = useState(false); - const [error, setError] = useState(""); - - async function submitLoginName(values: Inputs, organization?: string) { - setLoading(true); - - let body: any = { - loginName: values.loginName, - }; - - if (organization) { - body.organization = organization; - } - - if (authRequestId) { - body.authRequestId = authRequestId; - } - - const res = await fetch("/api/loginname", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(body), - }); - - setLoading(false); - if (!res.ok) { - const response = await res.json(); - - setError(response.message ?? "An internal error occurred"); - return Promise.reject(response.message ?? "An internal error occurred"); - } - return res.json(); - } - - function setLoginNameAndGetAuthMethods( - values: Inputs, - organization?: string, - ) { - return submitLoginName(values, organization).then((response) => { - if (response.nextStep) { - return router.push(response.nextStep); - } else if (response.authMethodTypes.length == 1) { - const method = response.authMethodTypes[0]; - switch (method) { - case 1: // user has only password as auth method - const paramsPassword: any = { - loginName: response.factors.user.loginName, - }; - - // TODO: does this have to be checked in loginSettings.allowDomainDiscovery - - if (organization || response.factors.user.organizationId) { - paramsPassword.organization = - organization ?? response.factors.user.organizationId; - } - - if ( - loginSettings?.passkeysType && - (loginSettings?.passkeysType === PasskeysType.ALLOWED || - (loginSettings.passkeysType as string) === - "PASSKEYS_TYPE_ALLOWED") - ) { - paramsPassword.promptPasswordless = `true`; - } - - if (authRequestId) { - paramsPassword.authRequestId = authRequestId; - } - - return router.push( - "/password?" + new URLSearchParams(paramsPassword), - ); - case 2: // AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSKEY - const paramsPasskey: any = { loginName: values.loginName }; - if (authRequestId) { - paramsPasskey.authRequestId = authRequestId; - } - - if (organization || response.factors.user.organizationId) { - paramsPasskey.organization = - organization ?? response.factors.user.organizationId; - } - - return router.push( - "/passkey/login?" + new URLSearchParams(paramsPasskey), - ); - default: - const paramsPasskeyDefault: any = { loginName: values.loginName }; - - if (loginSettings?.passkeysType === 1) { - paramsPasskeyDefault.promptPasswordless = `true`; // PasskeysType.PASSKEYS_TYPE_ALLOWED, - } - - if (authRequestId) { - paramsPasskeyDefault.authRequestId = authRequestId; - } - - if (organization || response.factors.user.organizationId) { - paramsPasskeyDefault.organization = - organization ?? response.factors.user.organizationId; - } - - return router.push( - "/password?" + new URLSearchParams(paramsPasskeyDefault), - ); - } - } else if ( - response.authMethodTypes && - response.authMethodTypes.length === 0 - ) { - setError( - "User has no available authentication methods. Contact your administrator to setup authentication for the requested user.", - ); - } else { - // prefer passkey in favor of other methods - if (response.authMethodTypes.includes(2)) { - const passkeyParams: any = { - loginName: values.loginName, - altPassword: `${response.authMethodTypes.includes(1)}`, // show alternative password option - }; - - if (authRequestId) { - passkeyParams.authRequestId = authRequestId; - } - - if (organization || response.factors.user.organizationId) { - passkeyParams.organization = - organization ?? response.factors.user.organizationId; - } - - return router.push( - "/passkey/login?" + new URLSearchParams(passkeyParams), - ); - } else { - // user has no passkey setup and login settings allow passkeys - const paramsPasswordDefault: any = { loginName: values.loginName }; - - if (loginSettings?.passkeysType === 1) { - paramsPasswordDefault.promptPasswordless = `true`; // PasskeysType.PASSKEYS_TYPE_ALLOWED, - } - - if (authRequestId) { - paramsPasswordDefault.authRequestId = authRequestId; - } - - if (organization || response.factors.user.organizationId) { - paramsPasswordDefault.organization = - organization ?? response.factors.user.organizationId; - } - - return router.push( - "/password?" + new URLSearchParams(paramsPasswordDefault), - ); - } - } - }); - } - - const { errors } = formState; - - useEffect(() => { - if (submit && loginName) { - // When we navigate to this page, we always want to be redirected if submit is true and the parameters are valid. - setLoginNameAndGetAuthMethods({ loginName }, organization); - } - }, []); - - return ( -
-
- -
- - {error && ( -
- {error} -
- )} - -
- {allowRegister && ( - - )} - - -
-
- ); -} diff --git a/apps/login/src/ui/VerifyEmailForm.tsx b/apps/login/src/ui/VerifyEmailForm.tsx deleted file mode 100644 index 13ef01dca5..0000000000 --- a/apps/login/src/ui/VerifyEmailForm.tsx +++ /dev/null @@ -1,160 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { Button, ButtonVariants } from "./Button"; -import { TextInput } from "./Input"; -import { useForm } from "react-hook-form"; -import { useRouter } from "next/navigation"; -import { Spinner } from "./Spinner"; -import Alert from "@/ui/Alert"; - -type Inputs = { - code: string; -}; - -type Props = { - userId: string; - code: string; - submit: boolean; - organization?: string; - authRequestId?: string; - sessionId?: string; -}; - -export default function VerifyEmailForm({ - userId, - code, - submit, - organization, - authRequestId, - sessionId, -}: Props) { - const { register, handleSubmit, formState } = useForm({ - mode: "onBlur", - defaultValues: { - code: code ?? "", - }, - }); - - useEffect(() => { - if (submit && code && userId) { - // When we navigate to this page, we always want to be redirected if submit is true and the parameters are valid. - // For programmatic verification, the /verifyemail API should be used. - submitCodeAndContinue({ code }); - } - }, []); - - const [error, setError] = useState(""); - - const [loading, setLoading] = useState(false); - - const router = useRouter(); - - async function submitCode(values: Inputs) { - setLoading(true); - const res = await fetch("/api/verifyemail", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - code: values.code, - userId, - organization, - }), - }); - - const response = await res.json(); - - setLoading(false); - if (!res.ok) { - setError(response.rawMessage); - return Promise.reject(response); - } else { - return response; - } - } - - async function resendCode() { - setLoading(true); - const res = await fetch("/api/resendverifyemail", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - userId, - }), - }); - - const response = await res.json(); - - if (!res.ok) { - setLoading(false); - setError(response.details); - return Promise.reject(response); - } else { - setLoading(false); - return response; - } - } - - function submitCodeAndContinue(value: Inputs): Promise { - return submitCode(value).then((resp: any) => { - const params = new URLSearchParams({}); - - if (organization) { - params.set("organization", organization); - } - - if (authRequestId && sessionId) { - params.set("authRequest", authRequestId); - params.set("sessionId", sessionId); - return router.push(`/login?` + params); - } else { - return router.push(`/loginname?` + params); - } - }); - } - - return ( -
-
- -
- - {error && ( -
- {error} -
- )} - -
- - - -
-
- ); -} diff --git a/apps/login/src/utils/session.ts b/apps/login/src/utils/session.ts deleted file mode 100644 index a9e83d4604..0000000000 --- a/apps/login/src/utils/session.ts +++ /dev/null @@ -1,240 +0,0 @@ -"use server"; - -import { - createSessionFromChecks, - createSessionForUserIdAndIdpIntent, - getSession, - setSession, -} from "@/lib/zitadel"; -import { - SessionCookie, - addSessionToCookie, - updateSessionCookie, -} from "./cookies"; -import { - Challenges, - RequestChallenges, -} from "@zitadel/proto/zitadel/session/v2/challenge_pb"; -import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; -import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; -import { PlainMessage } from "@zitadel/client"; - -export async function createSessionAndUpdateCookie( - loginName: string, - password: string | undefined, - challenges: RequestChallenges | undefined, - organization?: string, - authRequestId?: string, -) { - const createdSession = await createSessionFromChecks( - password - ? { - user: { search: { case: "loginName", value: loginName } }, - password: { password }, - // totp: { code: totpCode }, - } - : { user: { search: { case: "loginName", value: loginName } } }, - challenges, - ); - - if (createdSession) { - return getSession( - createdSession.sessionId, - createdSession.sessionToken, - ).then((response) => { - if (response?.session && response.session?.factors?.user?.loginName) { - const sessionCookie: SessionCookie = { - id: createdSession.sessionId, - token: createdSession.sessionToken, - creationDate: `${response.session.creationDate?.toDate().getTime() ?? ""}`, - expirationDate: `${response.session.expirationDate?.toDate().getTime() ?? ""}`, - changeDate: `${response.session.changeDate?.toDate().getTime() ?? ""}`, - loginName: response.session.factors.user.loginName ?? "", - organization: response.session.factors.user.organizationId ?? "", - }; - - if (authRequestId) { - sessionCookie.authRequestId = authRequestId; - } - - if (organization) { - sessionCookie.organization = organization; - } - - return addSessionToCookie(sessionCookie).then(() => { - return response.session as Session; - }); - } else { - throw "could not get session or session does not have loginName"; - } - }); - } else { - throw "Could not create session"; - } -} - -export async function createSessionForUserIdAndUpdateCookie( - userId: string, - password: string | undefined, - challenges: RequestChallenges | undefined, - authRequestId: string | undefined, -): Promise { - const createdSession = await createSessionFromChecks( - password - ? { - user: { search: { case: "userId", value: userId } }, - password: { password }, - // totp: { code: totpCode }, - } - : { user: { search: { case: "userId", value: userId } } }, - challenges, - ); - - if (createdSession) { - return getSession( - createdSession.sessionId, - createdSession.sessionToken, - ).then((response) => { - if (response?.session && response.session?.factors?.user?.loginName) { - const sessionCookie: SessionCookie = { - id: createdSession.sessionId, - token: createdSession.sessionToken, - creationDate: `${response.session.creationDate?.toDate().getTime() ?? ""}`, - expirationDate: `${response.session.expirationDate?.toDate().getTime() ?? ""}`, - changeDate: `${response.session.changeDate?.toDate().getTime() ?? ""}`, - loginName: response.session.factors.user.loginName ?? "", - }; - - if (authRequestId) { - sessionCookie.authRequestId = authRequestId; - } - - if (response.session.factors.user.organizationId) { - sessionCookie.organization = - response.session.factors.user.organizationId; - } - - return addSessionToCookie(sessionCookie).then(() => { - return response.session as Session; - }); - } else { - throw "could not get session or session does not have loginName"; - } - }); - } else { - throw "Could not create session"; - } -} - -export async function createSessionForIdpAndUpdateCookie( - userId: string, - idpIntent: { - idpIntentId?: string | undefined; - idpIntentToken?: string | undefined; - }, - organization: string | undefined, - authRequestId: string | undefined, -): Promise { - const createdSession = await createSessionForUserIdAndIdpIntent( - userId, - idpIntent, - ); - - if (createdSession) { - return getSession( - createdSession.sessionId, - createdSession.sessionToken, - ).then((response) => { - if (response?.session && response.session?.factors?.user?.loginName) { - const sessionCookie: SessionCookie = { - id: createdSession.sessionId, - token: createdSession.sessionToken, - creationDate: `${response.session.creationDate?.toDate().getTime() ?? ""}`, - expirationDate: `${response.session.expirationDate?.toDate().getTime() ?? ""}`, - changeDate: `${response.session.changeDate?.toDate().getTime() ?? ""}`, - loginName: response.session.factors.user.loginName ?? "", - organization: response.session.factors.user.organizationId ?? "", - }; - - if (authRequestId) { - sessionCookie.authRequestId = authRequestId; - } - - if (organization) { - sessionCookie.organization = organization; - } - - return addSessionToCookie(sessionCookie).then(() => { - return response.session as Session; - }); - } else { - throw "could not get session or session does not have loginName"; - } - }); - } else { - throw "Could not create session"; - } -} - -export type SessionWithChallenges = Session & { - challenges: Challenges | undefined; -}; - -export async function setSessionAndUpdateCookie( - recentCookie: SessionCookie, - checks: PlainMessage, - challenges: RequestChallenges | undefined, - authRequestId: string | undefined, -) { - return setSession( - recentCookie.id, - recentCookie.token, - challenges, - checks, - ).then((updatedSession) => { - if (updatedSession) { - const sessionCookie: SessionCookie = { - id: recentCookie.id, - token: updatedSession.sessionToken, - creationDate: recentCookie.creationDate, - expirationDate: recentCookie.expirationDate, - changeDate: `${updatedSession.details?.changeDate?.toDate().getTime() ?? ""}`, - loginName: recentCookie.loginName, - organization: recentCookie.organization, - }; - - if (authRequestId) { - sessionCookie.authRequestId = authRequestId; - } - - return getSession(sessionCookie.id, sessionCookie.token).then( - (response) => { - if (response?.session && response.session.factors?.user?.loginName) { - const { session } = response; - const newCookie: SessionCookie = { - id: sessionCookie.id, - token: updatedSession.sessionToken, - creationDate: sessionCookie.creationDate, - expirationDate: sessionCookie.expirationDate, - changeDate: `${session.changeDate?.toDate().getTime() ?? ""}`, - loginName: session.factors?.user?.loginName ?? "", - organization: session.factors?.user?.organizationId ?? "", - }; - - if (sessionCookie.authRequestId) { - newCookie.authRequestId = sessionCookie.authRequestId; - } - - return updateSessionCookie(sessionCookie.id, newCookie).then(() => { - return { challenges: updatedSession.challenges, ...session }; - }); - } else { - throw "could not get session or session does not have loginName"; - } - }, - ); - } else { - throw "Session not be set"; - } - }); -} diff --git a/apps/login/tailwind.config.js b/apps/login/tailwind.config.mjs similarity index 96% rename from apps/login/tailwind.config.js rename to apps/login/tailwind.config.mjs index 66c1fc7dc2..8acc7da458 100644 --- a/apps/login/tailwind.config.js +++ b/apps/login/tailwind.config.mjs @@ -1,4 +1,4 @@ -const sharedConfig = require("zitadel-tailwind-config/tailwind.config.js"); +import sharedConfig from "zitadel-tailwind-config/tailwind.config.mjs"; let colors = { background: { light: { contrast: {} }, dark: { contrast: {} } }, @@ -35,7 +35,7 @@ types.forEach((type) => { }); /** @type {import('tailwindcss').Config} */ -module.exports = { +export default { presets: [sharedConfig], darkMode: "class", content: ["./src/**/*.{js,ts,jsx,tsx}"], diff --git a/apps/login/tsconfig.json b/apps/login/tsconfig.json index bf3a62f57d..a1efe752c5 100755 --- a/apps/login/tsconfig.json +++ b/apps/login/tsconfig.json @@ -12,6 +12,12 @@ } ] }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "custom-config.js" + ], "exclude": ["node_modules"] } diff --git a/apps/login/turbo.json b/apps/login/turbo.json index 0e271d4003..e8a243feaf 100644 --- a/apps/login/turbo.json +++ b/apps/login/turbo.json @@ -6,32 +6,16 @@ "dependsOn": ["^build"] }, "test": { - "dependsOn": [ - "@zitadel/node#build", - "@zitadel/client#build", - "@zitadel/react#build" - ] + "dependsOn": ["@zitadel/client#build"] }, "test:integration": { - "dependsOn": [ - "@zitadel/node#build", - "@zitadel/client#build", - "@zitadel/react#build" - ] + "dependsOn": ["@zitadel/client#build"] }, "test:unit": { - "dependsOn": [ - "@zitadel/node#build", - "@zitadel/client#build", - "@zitadel/react#build" - ] + "dependsOn": ["@zitadel/client#build"] }, "test:watch": { - "dependsOn": [ - "@zitadel/node#build", - "@zitadel/client#build", - "@zitadel/react#build" - ] + "dependsOn": ["@zitadel/client#build"] } } } diff --git a/apps/login/vitest.config.mts b/apps/login/vitest.config.mts index 161c2d14fc..238c5b8b93 100644 --- a/apps/login/vitest.config.mts +++ b/apps/login/vitest.config.mts @@ -1,6 +1,6 @@ -import { defineConfig } from "vitest/config"; import react from "@vitejs/plugin-react"; import tsconfigPaths from "vite-tsconfig-paths"; +import { defineConfig } from "vitest/config"; export default defineConfig({ plugins: [tsconfigPaths(), react()], diff --git a/package.json b/package.json old mode 100755 new mode 100644 index 77d30bda43..6538d7b90e --- a/package.json +++ b/package.json @@ -5,35 +5,55 @@ "scripts": { "generate": "turbo run generate", "build": "turbo run build", + "build:packages": "turbo run build --filter=./packages/*", + "build:apps": "turbo run build --filter=./apps/*", "test": "turbo run test", + "start": "turbo run start", + "start:built": "turbo run start:built", "test:unit": "turbo run test:unit -- --passWithNoTests", "test:integration": "turbo run test:integration", + "test:acceptance": "pnpm exec playwright test", "test:watch": "turbo run test:watch", "dev": "turbo run dev --no-cache --continue", "lint": "turbo run lint", "lint:fix": "turbo run lint:fix", "clean": "turbo run clean && rm -rf node_modules", - "format": "prettier --write \"**/*.{ts,tsx,md}\"", + "format:fix": "prettier --write \"**/*.{ts,tsx,md}\"", + "format": "prettier --check \"**/*.{ts,tsx,md}\"", "changeset": "changeset", "version-packages": "changeset version", - "release": "turbo run build --filter=login^... && changeset publish" + "release": "turbo run build --filter=login^... && changeset publish", + "run-zitadel": "docker compose -f ./acceptance/docker-compose.yaml run setup", + "run-sink": "docker compose -f ./acceptance/docker-compose.yaml up -d sink", + "stop": "docker compose -f ./acceptance/docker-compose.yaml stop" }, "pnpm": { "overrides": { - "@typescript-eslint/parser": "^7.9.0" + "@typescript-eslint/parser": "^7.9.0", + "@types/react": "npm:types-react@19.0.0-rc.1", + "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1" } }, "devDependencies": { - "@changesets/cli": "^2.22.0", - "@vitejs/plugin-react": "^4.2.1", - "eslint": "^8.57.0", + "@otplib/core": "^12.0.0", + "@otplib/plugin-thirty-two": "^12.0.0", + "@otplib/plugin-crypto": "^12.0.0", + "@faker-js/faker": "^9.2.0", + "@changesets/cli": "^2.27.9", + "@playwright/test": "^1.48.2", + "@types/node": "^22.9.0", + "@vitejs/plugin-react": "^4.3.3", + "@zitadel/prettier-config": "workspace:*", + "axios": "^1.7.7", + "dotenv": "^16.4.5", + "eslint": "8.57.1", "eslint-config-zitadel": "workspace:*", "prettier": "^3.2.5", - "prettier-plugin-organize-imports": "^4.0.0", - "tsup": "^8.0.2", - "turbo": "2.0.9", - "typescript": "^5.4.5", - "vite-tsconfig-paths": "^4.3.2", - "vitest": "^1.6.0" + "prettier-plugin-organize-imports": "^4.1.0", + "tsup": "^8.3.5", + "turbo": "2.2.3", + "typescript": "^5.6.3", + "vite-tsconfig-paths": "^5.1.2", + "vitest": "^2.1.4" } } diff --git a/packages/eslint-config-zitadel/index.js b/packages/eslint-config-zitadel/index.js index c9523f13cd..6a53b2a5e6 100644 --- a/packages/eslint-config-zitadel/index.js +++ b/packages/eslint-config-zitadel/index.js @@ -1,11 +1,13 @@ module.exports = { + parser: "@babel/eslint-parser", extends: ["next", "turbo", "prettier"], rules: { "@next/next/no-html-link-for-pages": "off", }, parserOptions: { + requireConfigFile: false, babelOptions: { - presets: [require.resolve("next/babel")], + presets: ["next/babel"], }, }, }; diff --git a/packages/eslint-config-zitadel/package.json b/packages/eslint-config-zitadel/package.json index 261744ce00..b733284b2b 100644 --- a/packages/eslint-config-zitadel/package.json +++ b/packages/eslint-config-zitadel/package.json @@ -7,10 +7,11 @@ "access": "public" }, "dependencies": { - "eslint-config-next": "^14.2.3", "@typescript-eslint/parser": "^7.9.0", + "eslint-config-next": "^14.2.18", "eslint-config-prettier": "^9.1.0", + "eslint-config-turbo": "^2.0.9", "eslint-plugin-react": "^7.34.1", - "eslint-config-turbo": "^2.0.9" + "@babel/eslint-parser": "^7.25.9" } } diff --git a/packages/zitadel-client/package.json b/packages/zitadel-client/package.json index c7dda285bb..1ed9f5ff73 100644 --- a/packages/zitadel-client/package.json +++ b/packages/zitadel-client/package.json @@ -9,24 +9,29 @@ "type": "module", "exports": { ".": { + "types": "./dist/index.d.ts", "import": "./dist/index.js", - "require": "./dist/index.cjs", - "types": "./dist/index.d.ts" + "require": "./dist/index.cjs" }, "./v1": { + "types": "./dist/v1.d.ts", "import": "./dist/v1.js", - "require": "./dist/v1.cjs", - "types": "./dist/v1.d.ts" + "require": "./dist/v1.cjs" }, "./v2": { + "types": "./dist/v2.d.ts", "import": "./dist/v2.js", - "require": "./dist/v2.cjs", - "types": "./dist/v2.d.ts" + "require": "./dist/v2.cjs" }, "./v3alpha": { + "types": "./dist/v3alpha.d.ts", "import": "./dist/v3alpha.js", - "require": "./dist/v3alpha.cjs", - "types": "./dist/v3alpha.d.ts" + "require": "./dist/v3alpha.cjs" + }, + "./node": { + "types": "./dist/node.d.ts", + "import": "./dist/node.js", + "require": "./dist/node.cjs" } }, "files": [ @@ -44,11 +49,15 @@ "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" }, "dependencies": { - "@zitadel/proto": "workspace:*", - "@bufbuild/protobuf": "^1.10.0", - "@connectrpc/connect": "^1.4.0" + "@bufbuild/protobuf": "^2.2.2", + "@connectrpc/connect": "^2.0.0", + "@connectrpc/connect-node": "^2.0.0", + "@connectrpc/connect-web": "^2.0.0", + "jose": "^5.3.0", + "@zitadel/proto": "workspace:*" }, "devDependencies": { + "@bufbuild/protocompile": "^0.0.1", "@zitadel/tsconfig": "workspace:*", "eslint-config-zitadel": "workspace:*" } diff --git a/packages/zitadel-client/src/helpers.ts b/packages/zitadel-client/src/helpers.ts index fbae0ed0a9..637cadf538 100644 --- a/packages/zitadel-client/src/helpers.ts +++ b/packages/zitadel-client/src/helpers.ts @@ -1,8 +1,11 @@ -import { createPromiseClient, Transport } from "@connectrpc/connect"; -import type { ServiceType } from "@bufbuild/protobuf"; +import type { DescService } from "@bufbuild/protobuf"; +import { Timestamp, timestampDate } from "@bufbuild/protobuf/wkt"; +import { createClient, Transport } from "@connectrpc/connect"; -export function createClientFor( - service: TService, -) { - return (transport: Transport) => createPromiseClient(service, transport); +export function createClientFor(service: TService) { + return (transport: Transport) => createClient(service, transport); +} + +export function toDate(timestamp: Timestamp | undefined): Date | undefined { + return timestamp ? timestampDate(timestamp) : undefined; } diff --git a/packages/zitadel-client/src/index.ts b/packages/zitadel-client/src/index.ts index b1dcfa8fe8..64c3af5050 100644 --- a/packages/zitadel-client/src/index.ts +++ b/packages/zitadel-client/src/index.ts @@ -1,2 +1,8 @@ +export { toDate } from "./helpers"; export { NewAuthorizationBearerInterceptor } from "./interceptors"; -export type { PartialMessage, PlainMessage } from "@bufbuild/protobuf"; + +// TODO: Move this to `./protobuf.ts` and export it from there +export { create, fromJson, toJson } from "@bufbuild/protobuf"; +export type { JsonObject } from "@bufbuild/protobuf"; +export { TimestampSchema, timestampDate, timestampFromDate, timestampFromMs, timestampMs } from "@bufbuild/protobuf/wkt"; +export type { Duration, Timestamp } from "@bufbuild/protobuf/wkt"; diff --git a/packages/zitadel-client/src/interceptors.test.ts b/packages/zitadel-client/src/interceptors.test.ts index dd70a2c3f2..2d80a7f5b8 100644 --- a/packages/zitadel-client/src/interceptors.test.ts +++ b/packages/zitadel-client/src/interceptors.test.ts @@ -1,19 +1,22 @@ -import { describe, expect, test, vitest } from "vitest"; -import { Int32Value, MethodKind, StringValue } from "@bufbuild/protobuf"; +import { Int32Value } from "@bufbuild/protobuf/wkt"; +import { compileService } from "@bufbuild/protocompile"; import { createRouterTransport, HandlerContext } from "@connectrpc/connect"; +import { describe, expect, test, vitest } from "vitest"; import { NewAuthorizationBearerInterceptor } from "./interceptors"; -const TestService = { - typeName: "handwritten.TestService", - methods: { - unary: { - name: "Unary", - I: Int32Value, - O: StringValue, - kind: MethodKind.Unary, - }, - }, -} as const; +const TestService = compileService(` + syntax = "proto3"; + package handwritten; + service TestService { + rpc Unary(Int32Value) returns (StringValue); + } + message Int32Value { + int32 value = 1; + } + message StringValue { + string value = 1; + } +`); describe("NewAuthorizationBearerInterceptor", () => { const transport = { @@ -21,51 +24,37 @@ describe("NewAuthorizationBearerInterceptor", () => { }; test("injects the authorization token", async () => { - const handler = vitest.fn( - (request: Int32Value, context: HandlerContext) => { - return { value: request.value.toString() }; - }, - ); + const handler = vitest.fn((request: Int32Value, context: HandlerContext) => { + return { value: request.value.toString() }; + }); const service = createRouterTransport( - ({ service }) => { - service(TestService, { unary: handler }); + ({ rpc }) => { + rpc(TestService.method.unary, handler); }, { transport }, ); - await service.unary( - TestService, - TestService.methods.unary, - undefined, - undefined, - {}, - { value: 9001 }, - ); + await service.unary(TestService.method.unary, undefined, undefined, {}, { value: 9001 }); expect(handler).toBeCalled(); - expect(handler.mock.calls[0][1].requestHeader.get("Authorization")).toBe( - "Bearer mytoken", - ); + expect(handler.mock.calls[0][1].requestHeader.get("Authorization")).toBe("Bearer mytoken"); }); test("do not overwrite the previous authorization token", async () => { - const handler = vitest.fn( - (request: Int32Value, context: HandlerContext) => { - return { value: request.value.toString() }; - }, - ); + const handler = vitest.fn((request: Int32Value, context: HandlerContext) => { + return { value: request.value.toString() }; + }); const service = createRouterTransport( - ({ service }) => { - service(TestService, { unary: handler }); + ({ rpc }) => { + rpc(TestService.method.unary, handler); }, { transport }, ); await service.unary( - TestService, - TestService.methods.unary, + TestService.method.unary, undefined, undefined, { Authorization: "Bearer somethingelse" }, @@ -73,8 +62,6 @@ describe("NewAuthorizationBearerInterceptor", () => { ); expect(handler).toBeCalled(); - expect(handler.mock.calls[0][1].requestHeader.get("Authorization")).toBe( - "Bearer somethingelse", - ); + expect(handler.mock.calls[0][1].requestHeader.get("Authorization")).toBe("Bearer somethingelse"); }); }); diff --git a/packages/zitadel-node/src/index.ts b/packages/zitadel-client/src/node.ts similarity index 63% rename from packages/zitadel-node/src/index.ts rename to packages/zitadel-client/src/node.ts index 4867870d0d..c5900bb592 100644 --- a/packages/zitadel-node/src/index.ts +++ b/packages/zitadel-client/src/node.ts @@ -1,25 +1,16 @@ -import { NewAuthorizationBearerInterceptor } from "@zitadel/client"; -import { - createGrpcTransport, - GrpcTransportOptions, -} from "@connectrpc/connect-node"; +import { createGrpcTransport, GrpcTransportOptions } from "@connectrpc/connect-node"; import { importPKCS8, SignJWT } from "jose"; +import { NewAuthorizationBearerInterceptor } from "./interceptors"; /** * Create a server transport with the given token and configuration options. * @param token * @param opts */ -export function createServerTransport( - token: string, - opts: GrpcTransportOptions, -) { +export function createServerTransport(token: string, opts: GrpcTransportOptions) { return createGrpcTransport({ ...opts, - interceptors: [ - ...(opts.interceptors || []), - NewAuthorizationBearerInterceptor(token), - ], + interceptors: [...(opts.interceptors || []), NewAuthorizationBearerInterceptor(token)], }); } diff --git a/packages/zitadel-client/src/v1.ts b/packages/zitadel-client/src/v1.ts index 5aef450755..718e2ca3ce 100644 --- a/packages/zitadel-client/src/v1.ts +++ b/packages/zitadel-client/src/v1.ts @@ -1,9 +1,9 @@ import { createClientFor } from "./helpers"; -import { AdminService } from "@zitadel/proto/zitadel/admin_connect"; -import { AuthService } from "@zitadel/proto/zitadel/auth_connect"; -import { ManagementService } from "@zitadel/proto/zitadel/management_connect"; -import { SystemService } from "@zitadel/proto/zitadel/system_connect"; +import { AdminService } from "@zitadel/proto/zitadel/admin_pb"; +import { AuthService } from "@zitadel/proto/zitadel/auth_pb"; +import { ManagementService } from "@zitadel/proto/zitadel/management_pb"; +import { SystemService } from "@zitadel/proto/zitadel/system_pb"; export const createAdminServiceClient = createClientFor(AdminService); export const createAuthServiceClient = createClientFor(AuthService); diff --git a/packages/zitadel-client/src/v2.ts b/packages/zitadel-client/src/v2.ts index 2146149894..532a722f49 100644 --- a/packages/zitadel-client/src/v2.ts +++ b/packages/zitadel-client/src/v2.ts @@ -1,10 +1,13 @@ -import { FeatureService } from "@zitadel/proto/zitadel/feature/v2/feature_service_connect"; -import { RequestContext } from "@zitadel/proto/zitadel/object/v2/object_pb"; -import { OIDCService } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_connect"; -import { OrganizationService } from "@zitadel/proto/zitadel/org/v2/org_service_connect"; -import { SessionService } from "@zitadel/proto/zitadel/session/v2/session_service_connect"; -import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_connect"; -import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_connect"; +import { create } from "@bufbuild/protobuf"; +import { FeatureService } from "@zitadel/proto/zitadel/feature/v2/feature_service_pb"; +import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_service_pb"; +import { RequestContextSchema } from "@zitadel/proto/zitadel/object/v2/object_pb"; +import { OIDCService } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; +import { OrganizationService } from "@zitadel/proto/zitadel/org/v2/org_service_pb"; +import { SessionService } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb"; +import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; + import { createClientFor } from "./helpers"; export const createUserServiceClient = createClientFor(UserService); @@ -13,9 +16,10 @@ export const createSessionServiceClient = createClientFor(SessionService); export const createOIDCServiceClient = createClientFor(OIDCService); export const createOrganizationServiceClient = createClientFor(OrganizationService); export const createFeatureServiceClient = createClientFor(FeatureService); +export const createIdpServiceClient = createClientFor(IdentityProviderService); -export function makeReqCtx(orgId: string | undefined): Partial { - return { +export function makeReqCtx(orgId: string | undefined) { + return create(RequestContextSchema, { resourceOwner: orgId ? { case: "orgId", value: orgId } : { case: "instance", value: true }, - }; + }); } diff --git a/packages/zitadel-client/src/v3alpha.ts b/packages/zitadel-client/src/v3alpha.ts index 52b3b4c8ff..166fcb2b6e 100644 --- a/packages/zitadel-client/src/v3alpha.ts +++ b/packages/zitadel-client/src/v3alpha.ts @@ -1,6 +1,6 @@ -import { UserSchemaService } from "@zitadel/proto/zitadel/user/schema/v3alpha/user_schema_service_connect"; -import { UserService } from "@zitadel/proto/zitadel/user/v3alpha/user_service_connect"; +import { ZITADELUsers } from "@zitadel/proto/zitadel/resources/user/v3alpha/user_service_pb"; +import { ZITADELUserSchemas } from "@zitadel/proto/zitadel/resources/userschema/v3alpha/user_schema_service_pb"; import { createClientFor } from "./helpers"; -export const createUserSchemaServiceClient = createClientFor(UserSchemaService); -export const createUserServiceClient = createClientFor(UserService); +export const createUserSchemaServiceClient = createClientFor(ZITADELUserSchemas); +export const createUserServiceClient = createClientFor(ZITADELUsers); diff --git a/packages/zitadel-client/tsup.config.ts b/packages/zitadel-client/tsup.config.ts index 0293f4f242..bb1644b766 100644 --- a/packages/zitadel-client/tsup.config.ts +++ b/packages/zitadel-client/tsup.config.ts @@ -1,7 +1,7 @@ import { defineConfig, Options } from "tsup"; export default defineConfig((options: Options) => ({ - entry: ["src/index.ts", "src/v1.ts", "src/v2.ts", "src/v3alpha.ts"], + entry: ["src/index.ts", "src/v1.ts", "src/v2.ts", "src/v3alpha.ts", "src/node.ts"], format: ["esm", "cjs"], treeshake: false, splitting: true, diff --git a/packages/zitadel-next/.eslintrc.js b/packages/zitadel-next/.eslintrc.js deleted file mode 100644 index 8e247ab3c2..0000000000 --- a/packages/zitadel-next/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - root: true, - extends: ["zitadel"], -}; diff --git a/packages/zitadel-next/package.json b/packages/zitadel-next/package.json deleted file mode 100644 index 9d1da0d519..0000000000 --- a/packages/zitadel-next/package.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "@zitadel/next", - "version": "0.0.0", - "main": "./dist/index.js", - "module": "./dist/index.mjs", - "types": "./dist/index.d.ts", - "sideEffects": false, - "license": "MIT", - "files": [ - "dist/**" - ], - "publishConfig": { - "access": "public" - }, - "scripts": { - "build": "tsup", - "test": "pnpm test:unit", - "test:watch": "pnpm test:unit:watch", - "test:unit": "vitest", - "test:unit:watch": "vitest --watch", - "dev": "tsup --watch", - "lint": "eslint \"src/**/*.ts*\"", - "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" - }, - "peerDependencies": { - "@zitadel/node": "workspace:*", - "@zitadel/react": "workspace:*", - "next": "^14.2.3", - "react": "18.2.0" - }, - "dependencies": { - "next": "^14.2.5" - }, - "devDependencies": { - "@types/react": "^17.0.80", - "@zitadel/tsconfig": "workspace:*", - "eslint-config-zitadel": "workspace:*", - "postcss": "8.4.21", - "tailwindcss": "3.2.4", - "zitadel-tailwind-config": "workspace:*" - } -} diff --git a/packages/zitadel-next/postcss.config.js b/packages/zitadel-next/postcss.config.js deleted file mode 100644 index 07aa434b2b..0000000000 --- a/packages/zitadel-next/postcss.config.js +++ /dev/null @@ -1,9 +0,0 @@ -// If you want to use other PostCSS plugins, see the following: -// https://tailwindcss.com/docs/using-with-preprocessors - -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; diff --git a/packages/zitadel-next/src/components/ZitadelNextProvider.tsx b/packages/zitadel-next/src/components/ZitadelNextProvider.tsx deleted file mode 100644 index b0df5b4726..0000000000 --- a/packages/zitadel-next/src/components/ZitadelNextProvider.tsx +++ /dev/null @@ -1,10 +0,0 @@ -export type ZitadelNextProps = { - dark: boolean; - children: React.ReactNode; -}; - -export function ZitadelNextProvider({ dark, children }: ZitadelNextProps) { - return ( -
{children}
- ); -} diff --git a/packages/zitadel-next/src/index.tsx b/packages/zitadel-next/src/index.tsx deleted file mode 100644 index 1754b0ee48..0000000000 --- a/packages/zitadel-next/src/index.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import "./styles.css"; - -export { - ZitadelNextProvider, - type ZitadelNextProps, -} from "./components/ZitadelNextProvider"; diff --git a/packages/zitadel-next/src/styles.css b/packages/zitadel-next/src/styles.css deleted file mode 100644 index b5c61c9567..0000000000 --- a/packages/zitadel-next/src/styles.css +++ /dev/null @@ -1,3 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/packages/zitadel-next/tailwind.config.js b/packages/zitadel-next/tailwind.config.js deleted file mode 100644 index 2bf2c389f6..0000000000 --- a/packages/zitadel-next/tailwind.config.js +++ /dev/null @@ -1,9 +0,0 @@ -const sharedConfig = require("zitadel-tailwind-config/tailwind.config.js"); - -/** @type {import('tailwindcss').Config} */ -module.exports = { - presets: [sharedConfig], - prefix: "ztdl-next-", - darkMode: "class", - content: [`src/**/*.{js,ts,jsx,tsx}`], -}; diff --git a/packages/zitadel-next/tsconfig.json b/packages/zitadel-next/tsconfig.json deleted file mode 100644 index 460eef71f5..0000000000 --- a/packages/zitadel-next/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "@zitadel/tsconfig/react-library.json", - "include": ["."], - "exclude": ["dist", "build", "node_modules"] -} diff --git a/packages/zitadel-next/tsup.config.ts b/packages/zitadel-next/tsup.config.ts deleted file mode 100644 index 8a2f312e51..0000000000 --- a/packages/zitadel-next/tsup.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { defineConfig, Options } from "tsup"; - -export default defineConfig((options: Options) => ({ - treeshake: true, - splitting: true, - publicDir: true, - entry: ["src/**/*.tsx"], - format: ["esm"], - dts: true, - minify: true, - clean: true, - external: ["react"], - ...options, -})); diff --git a/packages/zitadel-next/turbo.json b/packages/zitadel-next/turbo.json deleted file mode 100644 index 277108232b..0000000000 --- a/packages/zitadel-next/turbo.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "extends": [ - "//" - ], - "tasks": { - "dev": { - "dependsOn": [ - "@zitadel/react#build" - ] - }, - "build": { - "outputs": [ - "dist/**" - ], - "dependsOn": [ - "@zitadel/react#build" - ] - } - } -} diff --git a/packages/zitadel-node/.eslintrc.cjs b/packages/zitadel-node/.eslintrc.cjs deleted file mode 100644 index 51720b7c3a..0000000000 --- a/packages/zitadel-node/.eslintrc.cjs +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - root: true, - // TODO: React is not used in the server package - extends: ["zitadel"], -}; diff --git a/packages/zitadel-node/package.json b/packages/zitadel-node/package.json deleted file mode 100644 index 95f0df8dbf..0000000000 --- a/packages/zitadel-node/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "@zitadel/node", - "version": "0.0.0", - "type": "module", - "sideEffects": false, - "license": "MIT", - "exports": { - ".": { - "import": "./dist/index.js", - "require": "./dist/index.cjs", - "types": "./dist/index.d.ts" - } - }, - "files": [ - "dist/**" - ], - "publishConfig": { - "access": "public" - }, - "scripts": { - "build": "tsup", - "test": "pnpm test:unit", - "test:watch": "pnpm test:unit:watch", - "test:unit": "vitest", - "test:unit:watch": "vitest --watch", - "dev": "tsup --watch", - "lint": "eslint \"src/**/*.ts*\"", - "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist && rm -rf src/proto" - }, - "peerDependencies": { - "@zitadel/client": "workspace:*" - }, - "dependencies": { - "@connectrpc/connect-node": "^1.4.0", - "@connectrpc/connect-web": "^1.4.0", - "jose": "^5.3.0" - }, - "devDependencies": { - "@types/node": "^20.14.2", - "@zitadel/client": "workspace:*", - "@zitadel/tsconfig": "workspace:*", - "eslint-config-zitadel": "workspace:*" - } -} diff --git a/packages/zitadel-node/tsconfig.json b/packages/zitadel-node/tsconfig.json deleted file mode 100644 index 5f0ea69110..0000000000 --- a/packages/zitadel-node/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "@zitadel/tsconfig/tsup.json", - "include": ["./src/**/*"], - "exclude": ["dist", "build", "node_modules"] -} diff --git a/packages/zitadel-node/tsup.config.ts b/packages/zitadel-node/tsup.config.ts deleted file mode 100644 index b978211b1e..0000000000 --- a/packages/zitadel-node/tsup.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineConfig, Options } from "tsup"; - -export default defineConfig((options: Options) => ({ - treeshake: false, - splitting: true, - entry: ["src/index.ts"], - format: ["esm", "cjs"], - dts: true, - minify: false, - clean: true, - sourcemap: true, - ...options, -})); diff --git a/packages/zitadel-node/turbo.json b/packages/zitadel-node/turbo.json deleted file mode 100644 index ba7648a89d..0000000000 --- a/packages/zitadel-node/turbo.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": [ - "//" - ], - "tasks": { - "build": { - "outputs": [ - "dist/**" - ], - "dependsOn": [ - "@zitadel/client#build" - ] - } - } -} diff --git a/packages/zitadel-prettier-config/index.js b/packages/zitadel-prettier-config/index.js index 4654e85e40..31d8c455e8 100644 --- a/packages/zitadel-prettier-config/index.js +++ b/packages/zitadel-prettier-config/index.js @@ -7,4 +7,5 @@ export default { trailingComma: 'all', bracketSpacing: true, arrowParens: 'always', + plugins: ["prettier-plugin-organize-imports"] }; diff --git a/packages/zitadel-prettier-config/package.json b/packages/zitadel-prettier-config/package.json index 568bb4a681..d6f15c91c6 100644 --- a/packages/zitadel-prettier-config/package.json +++ b/packages/zitadel-prettier-config/package.json @@ -2,8 +2,8 @@ "name": "@zitadel/prettier-config", "version": "0.1.0", "description": "Prettier configuration", - "private": true, "type": "module", + "private": true, "exports": { ".": "./index.js" } diff --git a/packages/zitadel-proto/.gitignore b/packages/zitadel-proto/.gitignore index d7f47cfd83..93276fc105 100644 --- a/packages/zitadel-proto/.gitignore +++ b/packages/zitadel-proto/.gitignore @@ -1 +1,4 @@ -zitadel \ No newline at end of file +zitadel +google +protoc-gen-openapiv2 +validate diff --git a/packages/zitadel-proto/buf.gen.yaml b/packages/zitadel-proto/buf.gen.yaml index 99cc0b5114..1827d5c355 100644 --- a/packages/zitadel-proto/buf.gen.yaml +++ b/packages/zitadel-proto/buf.gen.yaml @@ -2,7 +2,8 @@ version: v2 managed: enabled: true plugins: - - remote: buf.build/connectrpc/es:v1.4.0 - out: . - - remote: buf.build/bufbuild/es:v1.7.2 + - remote: buf.build/bufbuild/es:v2.2.0 out: . + include_imports: true + opt: + - json_types=true diff --git a/packages/zitadel-proto/package.json b/packages/zitadel-proto/package.json index 1f05e8a5ab..6d2df46f40 100644 --- a/packages/zitadel-proto/package.json +++ b/packages/zitadel-proto/package.json @@ -12,12 +12,12 @@ "sideEffects": false, "scripts": { "generate": "buf generate https://github.com/zitadel/zitadel.git --path ./proto/zitadel", - "clean": "rm -rf zitadel && rm -rf .turbo && rm -rf node_modules" + "clean": "rm -rf zitadel .turbo node_modules google protoc-gen-openapiv2 validate" }, "dependencies": { - "@bufbuild/protobuf": "^1.10.0" + "@bufbuild/protobuf": "^2.2.2" }, "devDependencies": { - "@bufbuild/buf": "^1.35.1" + "@bufbuild/buf": "^1.47.2" } } diff --git a/packages/zitadel-react/.eslintrc.js b/packages/zitadel-react/.eslintrc.js deleted file mode 100644 index 8e247ab3c2..0000000000 --- a/packages/zitadel-react/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - root: true, - extends: ["zitadel"], -}; diff --git a/packages/zitadel-react/README.md b/packages/zitadel-react/README.md deleted file mode 100644 index 0d2b62c8d6..0000000000 --- a/packages/zitadel-react/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# How to use - -### Install - -```sh -npm install @zitadel/react -``` - -or - -```sh -yarn add @zitadel/react -``` - -### Import styles file - -To get the styles, import them in `_app.tsx` or global styling file - -``` -import "@zitadel/react/styles.css"; -``` - -### Setup Dark mode - -to set dark theme, wrap your components in a `ui-dark` class. - -### Use components - -```tsx -import { SignInWithGoogle } from "@zitadel/react"; - -export default function IdentityProviders() { - return ( -
- -
- ); -} -``` diff --git a/packages/zitadel-react/package.json b/packages/zitadel-react/package.json deleted file mode 100644 index eb8592a532..0000000000 --- a/packages/zitadel-react/package.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "@zitadel/react", - "version": "0.0.0", - "types": "./dist/index.d.ts", - "sideEffects": [ - "**/*.css" - ], - "license": "MIT", - "exports": { - ".": { - "import": "./dist/index.mjs", - "require": "./dist/index.cjs" - }, - "./styles.css": "./dist/index.css", - "./assets/*": "./dist/assets/*" - }, - "publishConfig": { - "access": "public" - }, - "scripts": { - "build": "tsup", - "test": "pnpm test:unit", - "test:watch": "pnpm test:unit:watch", - "test:unit": "vitest", - "test:unit:watch": "vitest --watch", - "dev": "tsup --watch", - "lint": "eslint \"src/**/*.ts*\"", - "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist", - "copy-files": "cp -R ./src/public/ ./dist/" - }, - "peerDependencies": { - "react": "^18.2.0" - }, - "devDependencies": { - "@testing-library/jest-dom": "^6.4.5", - "@testing-library/react": "^14.0.0", - "@types/react": "^18.2.17", - "@types/react-dom": "^18.2.7", - "@zitadel/tsconfig": "workspace:*", - "autoprefixer": "10.4.13", - "eslint-config-zitadel": "workspace:*", - "jsdom": "^24.0.0", - "postcss": "8.4.21", - "sass": "^1.77.1", - "tailwindcss": "3.2.4", - "zitadel-tailwind-config": "workspace:*" - } -} diff --git a/packages/zitadel-react/postcss.config.js b/packages/zitadel-react/postcss.config.js deleted file mode 100644 index 07aa434b2b..0000000000 --- a/packages/zitadel-react/postcss.config.js +++ /dev/null @@ -1,9 +0,0 @@ -// If you want to use other PostCSS plugins, see the following: -// https://tailwindcss.com/docs/using-with-preprocessors - -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; diff --git a/packages/zitadel-react/src/components/SignInWith.tsx b/packages/zitadel-react/src/components/SignInWith.tsx deleted file mode 100644 index 933d8577a5..0000000000 --- a/packages/zitadel-react/src/components/SignInWith.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { ButtonHTMLAttributes, DetailedHTMLProps } from "react"; - -export type SignInWithIdentityProviderProps = DetailedHTMLProps< - ButtonHTMLAttributes, - HTMLButtonElement -> & { - name?: string; - e2e?: string; -}; diff --git a/packages/zitadel-react/src/components/SignInWithAzureAD.tsx b/packages/zitadel-react/src/components/SignInWithAzureAD.tsx deleted file mode 100644 index d9f7859bcf..0000000000 --- a/packages/zitadel-react/src/components/SignInWithAzureAD.tsx +++ /dev/null @@ -1,42 +0,0 @@ -"use client"; - -import { ReactNode, forwardRef } from "react"; -import { SignInWithIdentityProviderProps } from "./SignInWith"; - -export const SignInWithAzureAD = forwardRef< - HTMLButtonElement, - SignInWithIdentityProviderProps ->( - ({ children, className = "", name = "", ...props }, ref): ReactNode => ( - - ), -); - -SignInWithAzureAD.displayName = "SignInWithAzureAD"; diff --git a/packages/zitadel-react/src/components/SignInWithGitlab.test.tsx b/packages/zitadel-react/src/components/SignInWithGitlab.test.tsx deleted file mode 100644 index c24caa84f3..0000000000 --- a/packages/zitadel-react/src/components/SignInWithGitlab.test.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { afterEach, describe, expect, test } from "vitest"; - -import { cleanup, render, screen } from "@testing-library/react"; -import { SignInWithGitlab } from "./SignInWithGitlab"; - -afterEach(cleanup); - -describe("", () => { - test("renders without crashing", () => { - const { container } = render(); - expect(container.firstChild).toBeDefined(); - }); - - test("displays the default text", () => { - render(); - const signInText = screen.getByText(/Sign in with Gitlab/i); - expect(signInText).toBeInTheDocument(); - }); - - test("displays the given text", () => { - render(); - const signInText = screen.getByText(/Gitlab/i); - expect(signInText).toBeInTheDocument(); - }); -}); diff --git a/packages/zitadel-react/src/components/SignInWithGoogle.test.tsx b/packages/zitadel-react/src/components/SignInWithGoogle.test.tsx deleted file mode 100644 index d485a1e838..0000000000 --- a/packages/zitadel-react/src/components/SignInWithGoogle.test.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { afterEach, describe, expect, test } from "vitest"; - -import { cleanup, render, screen } from "@testing-library/react"; -import { SignInWithGoogle } from "./SignInWithGoogle"; - -afterEach(cleanup); - -describe("", () => { - test("renders without crashing", () => { - const { container } = render(); - expect(container.firstChild).toBeDefined(); - }); - - test("displays the default text", () => { - render(); - const signInText = screen.getByText(/Sign in with Google/i); - expect(signInText).toBeInTheDocument(); - }); - - test("displays the given text", () => { - render(); - const signInText = screen.getByText(/Google/i); - expect(signInText).toBeInTheDocument(); - }); -}); diff --git a/packages/zitadel-react/src/components/SignInWithIDP.tsx b/packages/zitadel-react/src/components/SignInWithIDP.tsx deleted file mode 100644 index eddcda928c..0000000000 --- a/packages/zitadel-react/src/components/SignInWithIDP.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import * as React from "react"; - -export interface SignInWithIDPProps { - children?: React.ReactNode; - orgId?: string; -} - -export function SignInWithIDP(props: SignInWithIDPProps) { - return ( -
-
- {props.children} -
- ); -} - -SignInWithIDP.displayName = "SignInWithIDP"; diff --git a/packages/zitadel-react/src/components/ZitadelReactProvider.tsx b/packages/zitadel-react/src/components/ZitadelReactProvider.tsx deleted file mode 100644 index 7ffe40a735..0000000000 --- a/packages/zitadel-react/src/components/ZitadelReactProvider.tsx +++ /dev/null @@ -1,10 +0,0 @@ -export type ZitadelReactProps = { - dark: boolean; - children: React.ReactNode; -}; - -export function ZitadelReactProvider({ dark, children }: ZitadelReactProps) { - return ( -
{children}
- ); -} diff --git a/packages/zitadel-react/src/index.tsx b/packages/zitadel-react/src/index.tsx deleted file mode 100644 index 4ba8a3ea0f..0000000000 --- a/packages/zitadel-react/src/index.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import "./styles.css"; - -export { SignInWithGoogle } from "./components/SignInWithGoogle"; - -export { SignInWithGitlab } from "./components/SignInWithGitlab"; - -export { SignInWithAzureAD } from "./components/SignInWithAzureAD"; - -export { SignInWithGithub } from "./components/SignInWithGithub"; - -export { - ZitadelReactProvider, - type ZitadelReactProps, -} from "./components/ZitadelReactProvider"; - -export { - SignInWithIDP, - type SignInWithIDPProps, -} from "./components/SignInWithIDP"; diff --git a/packages/zitadel-react/src/styles.css b/packages/zitadel-react/src/styles.css deleted file mode 100644 index b5c61c9567..0000000000 --- a/packages/zitadel-react/src/styles.css +++ /dev/null @@ -1,3 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/packages/zitadel-react/tailwind.config.js b/packages/zitadel-react/tailwind.config.js deleted file mode 100644 index db20760020..0000000000 --- a/packages/zitadel-react/tailwind.config.js +++ /dev/null @@ -1,9 +0,0 @@ -const sharedConfig = require("zitadel-tailwind-config/tailwind.config.js"); - -/** @type {import('tailwindcss').Config} */ -module.exports = { - presets: [sharedConfig], - prefix: "ztdl-", - darkMode: "class", - content: [`src/**/*.{js,ts,jsx,tsx}`], -}; diff --git a/packages/zitadel-react/tsconfig.json b/packages/zitadel-react/tsconfig.json deleted file mode 100644 index 2200a65a4b..0000000000 --- a/packages/zitadel-react/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "@zitadel/tsconfig/react-library.json", - "include": ["."], - "exclude": ["dist", "build", "node_modules"], - "compilerOptions": { - "types": ["@testing-library/jest-dom"] - } -} diff --git a/packages/zitadel-react/tsup.config.ts b/packages/zitadel-react/tsup.config.ts deleted file mode 100644 index 730147e6c1..0000000000 --- a/packages/zitadel-react/tsup.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig, Options } from "tsup"; - -export default defineConfig((options: Options) => ({ - entry: ["src/index.tsx"], - format: ["esm", "cjs"], - dts: true, - external: ["react"], - ...options, -})); diff --git a/packages/zitadel-react/turbo.json b/packages/zitadel-react/turbo.json deleted file mode 100644 index 52e8c763f0..0000000000 --- a/packages/zitadel-react/turbo.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": [ - "//" - ], - "tasks": { - "build": { - "outputs": [ - "dist/**" - ] - } - } -} diff --git a/packages/zitadel-react/vitest.config.mts b/packages/zitadel-react/vitest.config.mts deleted file mode 100644 index a0ee71ace8..0000000000 --- a/packages/zitadel-react/vitest.config.mts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineConfig } from "vitest/config"; -import react from "@vitejs/plugin-react"; - -export default defineConfig({ - plugins: [react()], - test: { - include: ['src/**/*.test.ts', 'src/**/*.test.tsx'], - environment: "jsdom", - setupFiles: ["@testing-library/jest-dom/vitest"], - }, -}); diff --git a/packages/zitadel-tailwind-config/package.json b/packages/zitadel-tailwind-config/package.json index fcb92a7b8a..2d0f81075e 100644 --- a/packages/zitadel-tailwind-config/package.json +++ b/packages/zitadel-tailwind-config/package.json @@ -4,7 +4,7 @@ "private": true, "main": "index.js", "devDependencies": { - "tailwindcss": "^3.2.4", + "tailwindcss": "^3.4.14", "@tailwindcss/forms": "0.5.3" } } diff --git a/packages/zitadel-tailwind-config/tailwind.config.js b/packages/zitadel-tailwind-config/tailwind.config.mjs similarity index 90% rename from packages/zitadel-tailwind-config/tailwind.config.js rename to packages/zitadel-tailwind-config/tailwind.config.mjs index f1a2eccf99..4a9a437cb7 100644 --- a/packages/zitadel-tailwind-config/tailwind.config.js +++ b/packages/zitadel-tailwind-config/tailwind.config.mjs @@ -1,12 +1,8 @@ -const colors = require("tailwindcss/colors"); +import colors from "tailwindcss/colors"; /** @type {import('tailwindcss').Config} */ -module.exports = { - content: [ - "./app/**/*.{js,ts,jsx,tsx}", - "./page/**/*.{js,ts,jsx,tsx}", - "./ui/**/*.{js,ts,jsx,tsx}", - ], +export default { + content: ["./app/**/*.{js,ts,jsx,tsx}", "./page/**/*.{js,ts,jsx,tsx}", "./ui/**/*.{js,ts,jsx,tsx}"], future: { hoverOnlyWhenSupported: true, }, @@ -48,10 +44,10 @@ module.exports = { }, backgroundImage: ({ theme }) => ({ "dark-vc-border-gradient": `radial-gradient(at left top, ${theme( - "colors.gray.800" + "colors.gray.800", )}, 50px, ${theme("colors.gray.800")} 50%)`, "vc-border-gradient": `radial-gradient(at left top, ${theme( - "colors.gray.200" + "colors.gray.200", )}, 50px, ${theme("colors.gray.300")} 50%)`, }), keyframes: ({ theme }) => ({ diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000000..342a302461 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,82 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./acceptance/tests", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: "http://localhost:3000", + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + /* + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + }, + TODO: webkit fails. Is this a bug? + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + */ + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run local dev server before starting the tests */ + + webServer: { + command: "pnpm start:built", + url: "http://127.0.0.1:3000", + reuseExistingServer: !process.env.CI, + timeout: 5 * 60_000, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28da5fff99..7e617f8e78 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,20 +6,49 @@ settings: overrides: '@typescript-eslint/parser': ^7.9.0 + '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 importers: .: devDependencies: '@changesets/cli': - specifier: ^2.22.0 - version: 2.27.7 + specifier: ^2.27.9 + version: 2.27.9 + '@faker-js/faker': + specifier: ^9.2.0 + version: 9.2.0 + '@otplib/core': + specifier: ^12.0.0 + version: 12.0.1 + '@otplib/plugin-crypto': + specifier: ^12.0.0 + version: 12.0.1 + '@otplib/plugin-thirty-two': + specifier: ^12.0.0 + version: 12.0.1 + '@playwright/test': + specifier: ^1.48.2 + version: 1.48.2 + '@types/node': + specifier: ^22.9.0 + version: 22.9.0 '@vitejs/plugin-react': - specifier: ^4.2.1 - version: 4.3.1(vite@5.3.5(@types/node@20.14.13)(sass@1.77.8)) + specifier: ^4.3.3 + version: 4.3.3(vite@5.4.11(@types/node@22.9.0)(sass@1.80.7)) + '@zitadel/prettier-config': + specifier: workspace:* + version: link:packages/zitadel-prettier-config + axios: + specifier: ^1.7.7 + version: 1.7.7(debug@4.3.7) + dotenv: + specifier: ^16.4.5 + version: 16.4.5 eslint: - specifier: ^8.57.0 - version: 8.57.0 + specifier: 8.57.1 + version: 8.57.1 eslint-config-zitadel: specifier: workspace:* version: link:packages/eslint-config-zitadel @@ -27,114 +56,114 @@ importers: specifier: ^3.2.5 version: 3.3.3 prettier-plugin-organize-imports: - specifier: ^4.0.0 - version: 4.0.0(prettier@3.3.3)(typescript@5.5.4) + specifier: ^4.1.0 + version: 4.1.0(prettier@3.3.3)(typescript@5.6.3) tsup: - specifier: ^8.0.2 - version: 8.2.3(postcss@8.4.40)(typescript@5.5.4)(yaml@2.5.0) + specifier: ^8.3.5 + version: 8.3.5(jiti@1.21.6)(postcss@8.4.49)(typescript@5.6.3)(yaml@2.5.0) turbo: - specifier: 2.0.9 - version: 2.0.9 + specifier: 2.2.3 + version: 2.2.3 typescript: - specifier: ^5.4.5 - version: 5.5.4 + specifier: ^5.6.3 + version: 5.6.3 vite-tsconfig-paths: - specifier: ^4.3.2 - version: 4.3.2(typescript@5.5.4)(vite@5.3.5(@types/node@20.14.13)(sass@1.77.8)) + specifier: ^5.1.2 + version: 5.1.2(typescript@5.6.3)(vite@5.4.11(@types/node@22.9.0)(sass@1.80.7)) vitest: - specifier: ^1.6.0 - version: 1.6.0(@types/node@20.14.13)(jsdom@24.1.1)(sass@1.77.8) + specifier: ^2.1.4 + version: 2.1.4(@types/node@22.9.0)(jsdom@25.0.1)(sass@1.80.7) apps/login: dependencies: '@headlessui/react': - specifier: ^1.7.18 - version: 1.7.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^2.1.9 + version: 2.1.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@heroicons/react': specifier: 2.1.3 - version: 2.1.3(react@18.3.1) + version: 2.1.3(react@19.0.0-rc-66855b96-20241106) '@tailwindcss/forms': specifier: 0.5.7 - version: 0.5.7(tailwindcss@3.2.4(postcss@8.4.21)) + version: 0.5.7(tailwindcss@3.4.14) '@vercel/analytics': specifier: ^1.2.2 - version: 1.3.1(next@14.2.3(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1) + version: 1.3.1(next@15.0.4-canary.23(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(sass@1.80.7))(react@19.0.0-rc-66855b96-20241106) '@zitadel/client': specifier: workspace:* version: link:../../packages/zitadel-client - '@zitadel/node': - specifier: workspace:* - version: link:../../packages/zitadel-node '@zitadel/proto': specifier: workspace:* version: link:../../packages/zitadel-proto - '@zitadel/react': - specifier: workspace:* - version: link:../../packages/zitadel-react clsx: specifier: 1.2.1 version: 1.2.1 copy-to-clipboard: specifier: ^3.3.3 version: 3.3.3 + deepmerge: + specifier: ^4.3.1 + version: 4.3.1 moment: specifier: ^2.29.4 version: 2.30.1 next: - specifier: 14.2.3 - version: 14.2.3(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8) + specifier: 15.0.4-canary.23 + version: 15.0.4-canary.23(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(sass@1.80.7) + next-intl: + specifier: ^3.25.1 + version: 3.25.1(next@15.0.4-canary.23(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(sass@1.80.7))(react@19.0.0-rc-66855b96-20241106) next-themes: specifier: ^0.2.1 - version: 0.2.1(next@14.2.3(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 0.2.1(next@15.0.4-canary.23(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(sass@1.80.7))(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) nice-grpc: specifier: 2.0.1 version: 2.0.1 qrcode.react: specifier: ^3.1.0 - version: 3.1.0(react@18.3.1) + version: 3.1.0(react@19.0.0-rc-66855b96-20241106) react: - specifier: 18.3.1 - version: 18.3.1 + specifier: 19.0.0-rc-66855b96-20241106 + version: 19.0.0-rc-66855b96-20241106 react-dom: - specifier: 18.3.1 - version: 18.3.1(react@18.3.1) + specifier: 19.0.0-rc-66855b96-20241106 + version: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) react-hook-form: specifier: 7.39.5 - version: 7.39.5(react@18.3.1) + version: 7.39.5(react@19.0.0-rc-66855b96-20241106) swr: specifier: ^2.2.0 - version: 2.2.5(react@18.3.1) + version: 2.2.5(react@19.0.0-rc-66855b96-20241106) tinycolor2: specifier: 1.4.2 version: 1.4.2 devDependencies: '@bufbuild/buf': - specifier: ^1.35.1 - version: 1.35.1 + specifier: ^1.46.0 + version: 1.46.0 '@testing-library/jest-dom': - specifier: ^6.4.5 - version: 6.4.8 + specifier: ^6.6.3 + version: 6.6.3 '@testing-library/react': - specifier: ^15.0.7 - version: 15.0.7(@types/react@18.2.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^16.0.1 + version: 16.0.1(@testing-library/dom@10.4.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) '@types/ms': - specifier: 0.7.31 - version: 0.7.31 + specifier: 0.7.34 + version: 0.7.34 '@types/node': - specifier: 18.11.9 - version: 18.11.9 + specifier: 22.9.0 + version: 22.9.0 '@types/react': - specifier: 18.2.8 - version: 18.2.8 + specifier: npm:types-react@19.0.0-rc.1 + version: types-react@19.0.0-rc.1 '@types/react-dom': - specifier: 18.0.9 - version: 18.0.9 + specifier: npm:types-react-dom@19.0.0-rc.1 + version: types-react-dom@19.0.0-rc.1 '@types/tinycolor2': specifier: 1.4.3 version: 1.4.3 '@types/uuid': - specifier: ^9.0.1 - version: 9.0.8 + specifier: ^10.0.0 + version: 10.0.0 '@vercel/git-hooks': specifier: 1.0.0 version: 1.0.0 @@ -145,17 +174,17 @@ importers: specifier: workspace:* version: link:../../packages/zitadel-tsconfig autoprefixer: - specifier: 10.4.13 - version: 10.4.13(postcss@8.4.21) + specifier: 10.4.20 + version: 10.4.20(postcss@8.4.49) concurrently: - specifier: ^8.1.0 - version: 8.2.2 + specifier: ^9.1.0 + version: 9.1.0 cypress: - specifier: ^13.9.0 - version: 13.13.1 + specifier: ^13.15.2 + version: 13.15.2 del-cli: - specifier: 5.0.0 - version: 5.0.0 + specifier: 6.0.0 + version: 6.0.0 env-cmd: specifier: ^10.1.0 version: 10.1.0 @@ -163,131 +192,90 @@ importers: specifier: workspace:* version: link:../../packages/eslint-config-zitadel grpc-tools: - specifier: 1.11.3 - version: 1.11.3 + specifier: 1.12.4 + version: 1.12.4 + jsdom: + specifier: ^25.0.1 + version: 25.0.1 lint-staged: - specifier: 13.0.3 - version: 13.0.3(enquirer@2.4.1) + specifier: 15.2.10 + version: 15.2.10 make-dir-cli: - specifier: 3.0.0 - version: 3.0.0 + specifier: 4.0.0 + version: 4.0.0 nodemon: - specifier: ^2.0.22 - version: 2.0.22 + specifier: ^3.1.7 + version: 3.1.7 postcss: - specifier: 8.4.21 - version: 8.4.21 + specifier: 8.4.49 + version: 8.4.49 prettier-plugin-tailwindcss: - specifier: 0.1.13 - version: 0.1.13(prettier@3.3.3) + specifier: 0.6.8 + version: 0.6.8(prettier-plugin-organize-imports@4.1.0(prettier@3.3.3)(typescript@5.6.3))(prettier@3.3.3) sass: - specifier: ^1.77.1 - version: 1.77.8 + specifier: ^1.80.7 + version: 1.80.7 start-server-and-test: - specifier: ^2.0.0 - version: 2.0.5 + specifier: ^2.0.8 + version: 2.0.8 tailwindcss: - specifier: 3.2.4 - version: 3.2.4(postcss@8.4.21) + specifier: 3.4.14 + version: 3.4.14 ts-proto: - specifier: ^1.139.0 - version: 1.181.1 + specifier: ^2.2.7 + version: 2.2.7 typescript: - specifier: ^5.4.5 - version: 5.5.4 + specifier: ^5.6.3 + version: 5.6.3 zitadel-tailwind-config: specifier: workspace:* version: link:../../packages/zitadel-tailwind-config packages/eslint-config-zitadel: dependencies: + '@babel/eslint-parser': + specifier: ^7.25.9 + version: 7.25.9(@babel/core@7.26.0)(eslint@8.57.1) '@typescript-eslint/parser': specifier: ^7.9.0 - version: 7.18.0(eslint@8.57.0)(typescript@5.5.4) + version: 7.18.0(eslint@8.57.1)(typescript@5.6.3) eslint-config-next: - specifier: ^14.2.3 - version: 14.2.5(eslint@8.57.0)(typescript@5.5.4) + specifier: ^14.2.18 + version: 14.2.18(eslint@8.57.1)(typescript@5.6.3) eslint-config-prettier: specifier: ^9.1.0 - version: 9.1.0(eslint@8.57.0) + version: 9.1.0(eslint@8.57.1) eslint-config-turbo: specifier: ^2.0.9 - version: 2.0.9(eslint@8.57.0) + version: 2.1.0(eslint@8.57.1) eslint-plugin-react: specifier: ^7.34.1 - version: 7.35.0(eslint@8.57.0) + version: 7.35.0(eslint@8.57.1) packages/zitadel-client: dependencies: '@bufbuild/protobuf': - specifier: ^1.10.0 - version: 1.10.0 + specifier: ^2.2.2 + version: 2.2.2 '@connectrpc/connect': - specifier: ^1.4.0 - version: 1.4.0(@bufbuild/protobuf@1.10.0) + specifier: ^2.0.0 + version: 2.0.0(@bufbuild/protobuf@2.2.2) + '@connectrpc/connect-node': + specifier: ^2.0.0 + version: 2.0.0(@bufbuild/protobuf@2.2.2)(@connectrpc/connect@2.0.0(@bufbuild/protobuf@2.2.2)) + '@connectrpc/connect-web': + specifier: ^2.0.0 + version: 2.0.0(@bufbuild/protobuf@2.2.2)(@connectrpc/connect@2.0.0(@bufbuild/protobuf@2.2.2)) '@zitadel/proto': specifier: workspace:* version: link:../zitadel-proto - devDependencies: - '@zitadel/tsconfig': - specifier: workspace:* - version: link:../zitadel-tsconfig - eslint-config-zitadel: - specifier: workspace:* - version: link:../eslint-config-zitadel - - packages/zitadel-next: - dependencies: - '@zitadel/node': - specifier: workspace:* - version: link:../zitadel-node - '@zitadel/react': - specifier: workspace:* - version: link:../zitadel-react - next: - specifier: ^14.2.5 - version: 14.2.5(@babel/core@7.25.2)(react-dom@18.3.1(react@18.2.0))(react@18.2.0)(sass@1.77.8) - react: - specifier: 18.2.0 - version: 18.2.0 - devDependencies: - '@types/react': - specifier: ^17.0.80 - version: 17.0.80 - '@zitadel/tsconfig': - specifier: workspace:* - version: link:../zitadel-tsconfig - eslint-config-zitadel: - specifier: workspace:* - version: link:../eslint-config-zitadel - postcss: - specifier: 8.4.21 - version: 8.4.21 - tailwindcss: - specifier: 3.2.4 - version: 3.2.4(postcss@8.4.21) - zitadel-tailwind-config: - specifier: workspace:* - version: link:../zitadel-tailwind-config - - packages/zitadel-node: - dependencies: - '@connectrpc/connect-node': - specifier: ^1.4.0 - version: 1.4.0(@bufbuild/protobuf@1.10.0)(@connectrpc/connect@1.4.0(@bufbuild/protobuf@1.10.0)) - '@connectrpc/connect-web': - specifier: ^1.4.0 - version: 1.4.0(@bufbuild/protobuf@1.10.0)(@connectrpc/connect@1.4.0(@bufbuild/protobuf@1.10.0)) jose: specifier: ^5.3.0 - version: 5.6.3 + version: 5.8.0 devDependencies: - '@types/node': - specifier: ^20.14.2 - version: 20.14.13 - '@zitadel/client': - specifier: workspace:* - version: link:../zitadel-client + '@bufbuild/protocompile': + specifier: ^0.0.1 + version: 0.0.1(@bufbuild/buf@1.47.2) '@zitadel/tsconfig': specifier: workspace:* version: link:../zitadel-tsconfig @@ -300,64 +288,21 @@ importers: packages/zitadel-proto: dependencies: '@bufbuild/protobuf': - specifier: ^1.10.0 - version: 1.10.0 + specifier: ^2.2.2 + version: 2.2.2 devDependencies: '@bufbuild/buf': - specifier: ^1.35.1 - version: 1.35.1 - - packages/zitadel-react: - dependencies: - react: - specifier: ^18.2.0 - version: 18.3.1 - devDependencies: - '@testing-library/jest-dom': - specifier: ^6.4.5 - version: 6.4.8 - '@testing-library/react': - specifier: ^14.0.0 - version: 14.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@types/react': - specifier: ^18.2.17 - version: 18.3.3 - '@types/react-dom': - specifier: ^18.2.7 - version: 18.3.0 - '@zitadel/tsconfig': - specifier: workspace:* - version: link:../zitadel-tsconfig - autoprefixer: - specifier: 10.4.13 - version: 10.4.13(postcss@8.4.21) - eslint-config-zitadel: - specifier: workspace:* - version: link:../eslint-config-zitadel - jsdom: - specifier: ^24.0.0 - version: 24.1.1 - postcss: - specifier: 8.4.21 - version: 8.4.21 - sass: - specifier: ^1.77.1 - version: 1.77.8 - tailwindcss: - specifier: 3.2.4 - version: 3.2.4(postcss@8.4.21) - zitadel-tailwind-config: - specifier: workspace:* - version: link:../zitadel-tailwind-config + specifier: ^1.47.2 + version: 1.47.2 packages/zitadel-tailwind-config: devDependencies: '@tailwindcss/forms': specifier: 0.5.3 - version: 0.5.3(tailwindcss@3.2.4(postcss@8.4.40)) + version: 0.5.3(tailwindcss@3.4.14) tailwindcss: - specifier: ^3.2.4 - version: 3.2.4(postcss@8.4.40) + specifier: ^3.4.14 + version: 3.4.14 packages/zitadel-tsconfig: {} @@ -366,190 +311,252 @@ packages: '@adobe/css-tools@4.4.0': resolution: {integrity: sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==} + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} - '@babel/code-frame@7.24.7': - resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.25.2': - resolution: {integrity: sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==} + '@babel/compat-data@7.26.2': + resolution: {integrity: sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==} engines: {node: '>=6.9.0'} - '@babel/core@7.25.2': - resolution: {integrity: sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==} + '@babel/core@7.26.0': + resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} engines: {node: '>=6.9.0'} - '@babel/generator@7.25.0': - resolution: {integrity: sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==} + '@babel/eslint-parser@7.25.9': + resolution: {integrity: sha512-5UXfgpK0j0Xr/xIdgdLEhOFxaDZ0bRPWJJchRpqOSur/3rZoPbqqki5mm0p4NE2cs28krBEiSM2MB7//afRSQQ==} + engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} + peerDependencies: + '@babel/core': ^7.11.0 + eslint: ^7.5.0 || ^8.0.0 || ^9.0.0 + + '@babel/generator@7.26.2': + resolution: {integrity: sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==} engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.25.2': - resolution: {integrity: sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==} + '@babel/helper-compilation-targets@7.25.9': + resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==} engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.24.7': - resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==} + '@babel/helper-module-imports@7.25.9': + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.25.2': - resolution: {integrity: sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==} + '@babel/helper-module-transforms@7.26.0': + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-plugin-utils@7.24.8': - resolution: {integrity: sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==} + '@babel/helper-plugin-utils@7.25.9': + resolution: {integrity: sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==} engines: {node: '>=6.9.0'} - '@babel/helper-simple-access@7.24.7': - resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==} + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.24.8': - resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==} + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.24.7': - resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} + '@babel/helper-validator-option@7.25.9': + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-option@7.24.8': - resolution: {integrity: sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==} + '@babel/helpers@7.26.0': + resolution: {integrity: sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.25.0': - resolution: {integrity: sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==} - engines: {node: '>=6.9.0'} - - '@babel/highlight@7.24.7': - resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.25.0': - resolution: {integrity: sha512-CzdIU9jdP0dg7HdyB+bHvDJGagUv+qtzZt5rYCWwW6tITNqV9odjp6Qu41gkG0ca5UfdDUWrKkiAnHHdGRnOrA==} + '@babel/parser@7.26.2': + resolution: {integrity: sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/plugin-transform-react-jsx-self@7.24.7': - resolution: {integrity: sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==} + '@babel/plugin-transform-react-jsx-self@7.25.9': + resolution: {integrity: sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-jsx-source@7.24.7': - resolution: {integrity: sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==} + '@babel/plugin-transform-react-jsx-source@7.25.9': + resolution: {integrity: sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.25.0': - resolution: {integrity: sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==} + '@babel/runtime@7.25.6': + resolution: {integrity: sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==} engines: {node: '>=6.9.0'} - '@babel/template@7.25.0': - resolution: {integrity: sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==} + '@babel/runtime@7.26.0': + resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.25.2': - resolution: {integrity: sha512-s4/r+a7xTnny2O6FcZzqgT6nE4/GHEdcqj4qAeglbUOh0TeglEfmNJFAd/OLoVtGd6ZhAO8GCVvCNUO5t/VJVQ==} + '@babel/template@7.25.9': + resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} engines: {node: '>=6.9.0'} - '@babel/types@7.25.2': - resolution: {integrity: sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==} + '@babel/traverse@7.25.9': + resolution: {integrity: sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==} engines: {node: '>=6.9.0'} - '@bufbuild/buf-darwin-arm64@1.35.1': - resolution: {integrity: sha512-Yy+sk+8sg3LDvMSZLGUIoMCkZajkQSZkdxO96mpqJagKlEYPLGTtakVFCVNX9KgK/sv1bd9sU55iMGXE3+eIYw==} + '@babel/types@7.26.0': + resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} + engines: {node: '>=6.9.0'} + + '@bufbuild/buf-darwin-arm64@1.46.0': + resolution: {integrity: sha512-lSmTKyRhg+71acXp9QeX/wm+vjkf0J3n38wph7KOwMfCEeK4A2AkqsGOkoXSiaIvidA2pRU9RJRQYfryzCA9Pg==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] - '@bufbuild/buf-darwin-x64@1.35.1': - resolution: {integrity: sha512-LcscoNTCHFeb5y9sitw4w6HWZtJ4Ja/MDBCUU9A8/OGHJSESV0JjhbvVHGNOIsKUbPq5p/SVjYA/Ab/wlmmpaA==} + '@bufbuild/buf-darwin-arm64@1.47.2': + resolution: {integrity: sha512-74WerFn06y+azgVfsnzhfbI5wla/OLPDnIvaNJBWHaqya/3bfascJkDylW2GVNHmwG1K/cscpmcc/RJPaO7ntQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@bufbuild/buf-darwin-x64@1.46.0': + resolution: {integrity: sha512-Oa9XTLJshsEjzowyt2mH9XrXW38DRFdz7ml+IYKXVQPotNLr04ix7QES7A1eOBJtxLwuTiri4ScXuBLQGNX8+A==} engines: {node: '>=12'} cpu: [x64] os: [darwin] - '@bufbuild/buf-linux-aarch64@1.35.1': - resolution: {integrity: sha512-bPeiSURl8WFxCdawtJjAjUOMqknVTw763NLIDcbYSH1/wTiUbM5QeXCORRlHKXtMGM89SYU5AatcY9UhQ+sn9g==} + '@bufbuild/buf-darwin-x64@1.47.2': + resolution: {integrity: sha512-adAiOacOQe8Ym/YXPCEiq9mrPeKRmDtF2TgqPWTcDy6mF7TqR7hMJINkEEuMd1EeACmXnzMOnXlm9ICtvdYgPg==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@bufbuild/buf-linux-aarch64@1.46.0': + resolution: {integrity: sha512-CbxbLH5sQCRjEKVEcWJySvCKyAPAUhX0vCTifT/eQyZ70FUsqCJKJ6+dKl6Ajk0CgUHqf8jkU/wX/+aQFYXyaA==} engines: {node: '>=12'} cpu: [arm64] os: [linux] - '@bufbuild/buf-linux-x64@1.35.1': - resolution: {integrity: sha512-n6ziazYjNH9H1JjHiacGi20rIyZuKnsHjF8qWisO8KGajhnS/7tpq0VzYdorqqWyJ1TcnLBWHj+dWYuGay9Nag==} + '@bufbuild/buf-linux-aarch64@1.47.2': + resolution: {integrity: sha512-52vY+Owffr5diw2PyfQJqH+Fld6zW6NhNZak4zojvc2MjZKubWM0TfNyM9jXz2YrwyB+cyxkabE60nBI80m37w==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@bufbuild/buf-linux-armv7@1.47.2': + resolution: {integrity: sha512-g9KtpObDeHZ/VG/0b5ZCieOao7L/WYZ0fPqFSs4N07D3APgEDhJG6vLyUcDgJMDgyLcgkNjNz0+XdYQb/tXyQw==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@bufbuild/buf-linux-x64@1.46.0': + resolution: {integrity: sha512-bMqp+Q+16KPbuwX34/OLDeiimnwt5sfvHqyeMeRz4LLwLshbmM3m+8dGCSHZRo3Lr+4gW1PfunrfaEmcGqPHLQ==} engines: {node: '>=12'} cpu: [x64] os: [linux] - '@bufbuild/buf-win32-arm64@1.35.1': - resolution: {integrity: sha512-3B65+iA1i/LDjJBseEpAvrkEI7VJqrvW39PyYVkIXSHHT917O+n95g74pn38A0XkggN5lEibLEkipBMDUfwMew==} + '@bufbuild/buf-linux-x64@1.47.2': + resolution: {integrity: sha512-MODCK2BzD1Mgoyr+5Sp8xA8qMNdytj8hYheyhA5NnCGTkQf8sfqAjpBSAAmKk6Zar8HOlVXML6tzE/ioDFFGwQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@bufbuild/buf-win32-arm64@1.46.0': + resolution: {integrity: sha512-geVYXp1PWJiAAFpwhgP8Cnct0+Rdr89BF/WZoIh5WwFGYITGiu5Hb1Ui9DTrEYwDzahPCyPxgIVwzzW6kPWSag==} engines: {node: '>=12'} cpu: [arm64] os: [win32] - '@bufbuild/buf-win32-x64@1.35.1': - resolution: {integrity: sha512-iafrcs+1FMlD+3ZjI1kVBHGOluT6YcoAUETrGMbQjRha6dL5s2Ldr0G7zCKLIT13yEKG5QTyP8z8gVEpk8C8wg==} + '@bufbuild/buf-win32-arm64@1.47.2': + resolution: {integrity: sha512-563YKYWJl3LrCY3G3+zuhb8HwOs6DzWslwGPFkKV2hwHyWyvd1DR1JjiLvw9zX64IKNctQ0HempSqc3kcboaqQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@bufbuild/buf-win32-x64@1.46.0': + resolution: {integrity: sha512-6nsxkzj5a1L41NOJFKjli8j6GB/NkPHLIr0T/b27Y3GfprVYQawOComYD5HfojvBLuAiE2cD/kEQIWKK1YRcng==} engines: {node: '>=12'} cpu: [x64] os: [win32] - '@bufbuild/buf@1.35.1': - resolution: {integrity: sha512-POtbb4wRhvgCmmClnuaQTpkHL4ukhFItuS/AaD7QDY0kamn4ExNJz4XlHG5jeJODaQ1Wq3f9qn7UIgUr6CUODw==} + '@bufbuild/buf-win32-x64@1.47.2': + resolution: {integrity: sha512-Sqcdv7La2xBDh3bTdEYb2f4UTMMqCcYe/D0RELhvQ5wDn6I35V3/2YT1OF5fRuf0BZLCo0OdO37S9L47uHSz2g==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@bufbuild/buf@1.46.0': + resolution: {integrity: sha512-uN3NKuAKvcQcZc1hn9+njSCusL7NAILqQI7mlkDDa4kRy7cTsiw53ggddfAs0YXbQ8zZnmudwwWjKRDIUaRqXQ==} engines: {node: '>=12'} hasBin: true - '@bufbuild/protobuf@1.10.0': - resolution: {integrity: sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==} + '@bufbuild/buf@1.47.2': + resolution: {integrity: sha512-glY5kCAoO4+a7HvDb+BLOdoHSdCk4mdXdkp53H8JFz7maOnkxCiHHXgRX+taFyEu25N8ybn7NjZFrZSdRwq2sA==} + engines: {node: '>=12'} + hasBin: true - '@changesets/apply-release-plan@7.0.4': - resolution: {integrity: sha512-HLFwhKWayKinWAul0Vj+76jVx1Pc2v55MGPVjZ924Y/ROeSsBMFutv9heHmCUj48lJyRfOTJG5+ar+29FUky/A==} + '@bufbuild/protobuf@2.2.0': + resolution: {integrity: sha512-+imAQkHf7U/Rwvu0wk1XWgsP3WnpCWmK7B48f0XqSNzgk64+grljTKC7pnO/xBiEMUziF7vKRfbBnOQhg126qQ==} - '@changesets/assemble-release-plan@6.0.3': - resolution: {integrity: sha512-bLNh9/Lgl1VwkjWZTq8JmRqH+hj7/Yzfz0jsQ/zJJ+FTmVqmqPj3szeKOri8O/hEM8JmHW019vh2gTO9iq5Cuw==} + '@bufbuild/protobuf@2.2.2': + resolution: {integrity: sha512-UNtPCbrwrenpmrXuRwn9jYpPoweNXj8X5sMvYgsqYyaH8jQ6LfUJSk3dJLnBK+6sfYPrF4iAIo5sd5HQ+tg75A==} + + '@bufbuild/protocompile@0.0.1': + resolution: {integrity: sha512-cOTMtjcWLcbjF17dPYgeMtVC5jZyS0bSjz3jy8kDPjOgjgSYMD2u2It7w8aCc2z23hTPIKl/2SNdMnz0Jzu3Xg==} + peerDependencies: + '@bufbuild/buf': ^1.22.0 + + '@changesets/apply-release-plan@7.0.5': + resolution: {integrity: sha512-1cWCk+ZshEkSVEZrm2fSj1Gz8sYvxgUL4Q78+1ZZqeqfuevPTPk033/yUZ3df8BKMohkqqHfzj0HOOrG0KtXTw==} + + '@changesets/assemble-release-plan@6.0.4': + resolution: {integrity: sha512-nqICnvmrwWj4w2x0fOhVj2QEGdlUuwVAwESrUo5HLzWMI1rE5SWfsr9ln+rDqWB6RQ2ZyaMZHUcU7/IRaUJS+Q==} '@changesets/changelog-git@0.2.0': resolution: {integrity: sha512-bHOx97iFI4OClIT35Lok3sJAwM31VbUM++gnMBV16fdbtBhgYu4dxsphBF/0AZZsyAHMrnM0yFcj5gZM1py6uQ==} - '@changesets/cli@2.27.7': - resolution: {integrity: sha512-6lr8JltiiXPIjDeYg4iM2MeePP6VN/JkmqBsVA5XRiy01hGS3y629LtSDvKcycj/w/5Eur1rEwby/MjcYS+e2A==} + '@changesets/cli@2.27.9': + resolution: {integrity: sha512-q42a/ZbDnxPpCb5Wkm6tMVIxgeI9C/bexntzTeCFBrQEdpisQqk8kCHllYZMDjYtEc1ZzumbMJAG8H0Z4rdvjg==} hasBin: true - '@changesets/config@3.0.2': - resolution: {integrity: sha512-cdEhS4t8woKCX2M8AotcV2BOWnBp09sqICxKapgLHf9m5KdENpWjyrFNMjkLqGJtUys9U+w93OxWT0czorVDfw==} + '@changesets/config@3.0.3': + resolution: {integrity: sha512-vqgQZMyIcuIpw9nqFIpTSNyc/wgm/Lu1zKN5vECy74u95Qx/Wa9g27HdgO4NkVAaq+BGA8wUc/qvbvVNs93n6A==} '@changesets/errors@0.2.0': resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} - '@changesets/get-dependents-graph@2.1.1': - resolution: {integrity: sha512-LRFjjvigBSzfnPU2n/AhFsuWR5DK++1x47aq6qZ8dzYsPtS/I5mNhIGAS68IAxh1xjO9BTtz55FwefhANZ+FCA==} + '@changesets/get-dependents-graph@2.1.2': + resolution: {integrity: sha512-sgcHRkiBY9i4zWYBwlVyAjEM9sAzs4wYVwJUdnbDLnVG3QwAaia1Mk5P8M7kraTOZN+vBET7n8KyB0YXCbFRLQ==} - '@changesets/get-release-plan@4.0.3': - resolution: {integrity: sha512-6PLgvOIwTSdJPTtpdcr3sLtGatT+Jr22+cQwEBJBy6wP0rjB4yJ9lv583J9fVpn1bfQlBkDa8JxbS2g/n9lIyA==} + '@changesets/get-release-plan@4.0.4': + resolution: {integrity: sha512-SicG/S67JmPTrdcc9Vpu0wSQt7IiuN0dc8iR5VScnnTVPfIaLvKmEGRvIaF0kcn8u5ZqLbormZNTO77bCEvyWw==} '@changesets/get-version-range-type@0.4.0': resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} - '@changesets/git@3.0.0': - resolution: {integrity: sha512-vvhnZDHe2eiBNRFHEgMiGd2CT+164dfYyrJDhwwxTVD/OW0FUD6G7+4DIx1dNwkwjHyzisxGAU96q0sVNBns0w==} + '@changesets/git@3.0.1': + resolution: {integrity: sha512-pdgHcYBLCPcLd82aRcuO0kxCDbw/yISlOtkmwmE8Odo1L6hSiZrBOsRl84eYG7DRCab/iHnOkWqExqc4wxk2LQ==} - '@changesets/logger@0.1.0': - resolution: {integrity: sha512-pBrJm4CQm9VqFVwWnSqKEfsS2ESnwqwH+xR7jETxIErZcfd1u2zBSqrHbRHR7xjhSgep9x2PSKFKY//FAshA3g==} + '@changesets/logger@0.1.1': + resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} '@changesets/parse@0.4.0': resolution: {integrity: sha512-TS/9KG2CdGXS27S+QxbZXgr8uPsP4yNJYb4BC2/NeFUj80Rni3TeD2qwWmabymxmrLo7JEsytXH1FbpKTbvivw==} - '@changesets/pre@2.0.0': - resolution: {integrity: sha512-HLTNYX/A4jZxc+Sq8D1AMBsv+1qD6rmmJtjsCJa/9MSRybdxh0mjbTvE6JYZQ/ZiQ0mMlDOlGPXTm9KLTU3jyw==} + '@changesets/pre@2.0.1': + resolution: {integrity: sha512-vvBJ/If4jKM4tPz9JdY2kGOgWmCowUYOi5Ycv8dyLnEE8FgpYYUo1mgJZxcdtGGP3aG8rAQulGLyyXGSLkIMTQ==} - '@changesets/read@0.6.0': - resolution: {integrity: sha512-ZypqX8+/im1Fm98K4YcZtmLKgjs1kDQ5zHpc2U1qdtNBmZZfo/IBiG162RoP0CUF05tvp2y4IspH11PLnPxuuw==} + '@changesets/read@0.6.1': + resolution: {integrity: sha512-jYMbyXQk3nwP25nRzQQGa1nKLY0KfoOV7VLgwucI0bUO8t8ZLCr6LZmgjXsiKuRDc+5A6doKPr9w2d+FEJ55zQ==} - '@changesets/should-skip-package@0.1.0': - resolution: {integrity: sha512-FxG6Mhjw7yFStlSM7Z0Gmg3RiyQ98d/9VpQAZ3Fzr59dCOM9G6ZdYbjiSAt0XtFr9JR5U2tBaJWPjrkGGc618g==} + '@changesets/should-skip-package@0.1.1': + resolution: {integrity: sha512-H9LjLbF6mMHLtJIc/eHR9Na+MifJ3VxtgP/Y+XLn4BF7tDTEN1HNYtH6QMcjP1uxp9sjaFYmW8xqloaCi/ckTg==} '@changesets/types@4.1.0': resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} @@ -557,46 +564,49 @@ packages: '@changesets/types@6.0.0': resolution: {integrity: sha512-b1UkfNulgKoWfqyHtzKS5fOZYSJO+77adgL7DLRDr+/7jhChN+QcHnbjiQVOz/U+Ts3PGNySq7diAItzDgugfQ==} - '@changesets/write@0.3.1': - resolution: {integrity: sha512-SyGtMXzH3qFqlHKcvFY2eX+6b0NGiFcNav8AFsYwy5l8hejOeoeTDemu5Yjmke2V5jpzY+pBvM0vCCQ3gdZpfw==} + '@changesets/write@0.3.2': + resolution: {integrity: sha512-kDxDrPNpUgsjDbWBvUo27PzKX4gqeKOlhibaOXDJA6kuBisGqNHv/HwGJrAu8U/dSf8ZEFIeHIPtvSlZI1kULw==} '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} - '@connectrpc/connect-node@1.4.0': - resolution: {integrity: sha512-0ANnrr6SvsjevsWEgdzHy7BaHkluZyS6s4xNoVt7RBHFR5V/kT9lPokoIbYUOU9JHzdRgTaS3x5595mwUsu15g==} - engines: {node: '>=16.0.0'} + '@connectrpc/connect-node@2.0.0': + resolution: {integrity: sha512-DoI5T+SUvlS/8QBsxt2iDoUg15dSxqhckegrgZpWOtADtmGohBIVbx1UjtWmjLBrP4RdD0FeBw+XyRUSbpKnJQ==} + engines: {node: '>=18.14.1'} peerDependencies: - '@bufbuild/protobuf': ^1.4.2 - '@connectrpc/connect': 1.4.0 + '@bufbuild/protobuf': ^2.2.0 + '@connectrpc/connect': 2.0.0 - '@connectrpc/connect-web@1.4.0': - resolution: {integrity: sha512-13aO4psFbbm7rdOFGV0De2Za64DY/acMspgloDlcOKzLPPs0yZkhp1OOzAQeiAIr7BM/VOHIA3p8mF0inxCYTA==} + '@connectrpc/connect-web@2.0.0': + resolution: {integrity: sha512-oeCxqHXLXlWJdmcvp9L3scgAuK+FjNSn+twyhUxc8yvDbTumnt5Io+LnBzSYxAdUdYqTw5yHfTSCJ4hj0QID0g==} peerDependencies: - '@bufbuild/protobuf': ^1.4.2 - '@connectrpc/connect': 1.4.0 + '@bufbuild/protobuf': ^2.2.0 + '@connectrpc/connect': 2.0.0 - '@connectrpc/connect@1.4.0': - resolution: {integrity: sha512-vZeOkKaAjyV4+RH3+rJZIfDFJAfr+7fyYr6sLDKbYX3uuTVszhFe9/YKf5DNqrDb5cKdKVlYkGn6DTDqMitAnA==} + '@connectrpc/connect@2.0.0': + resolution: {integrity: sha512-Usm8jgaaULANJU8vVnhWssSA6nrZ4DJEAbkNtXSoZay2YD5fDyMukCxu8NEhCvFzfHvrhxhcjttvgpyhOM7xAQ==} peerDependencies: - '@bufbuild/protobuf': ^1.4.2 + '@bufbuild/protobuf': ^2.2.0 - '@cypress/request@3.0.1': - resolution: {integrity: sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==} + '@cypress/request@3.0.6': + resolution: {integrity: sha512-fi0eVdCOtKu5Ed6+E8mYxUF6ZTFJDZvHogCBelM0xVXmrDEkyM22gRArQzq1YcHPm1V47Vf/iAD+WgVdUlJCGg==} engines: {node: '>= 6'} '@cypress/xvfb@1.2.4': resolution: {integrity: sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==} + '@emnapi/runtime@1.3.1': + resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.23.0': - resolution: {integrity: sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==} + '@esbuild/aix-ppc64@0.24.0': + resolution: {integrity: sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] @@ -607,8 +617,8 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.23.0': - resolution: {integrity: sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==} + '@esbuild/android-arm64@0.24.0': + resolution: {integrity: sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==} engines: {node: '>=18'} cpu: [arm64] os: [android] @@ -619,8 +629,8 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.23.0': - resolution: {integrity: sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==} + '@esbuild/android-arm@0.24.0': + resolution: {integrity: sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==} engines: {node: '>=18'} cpu: [arm] os: [android] @@ -631,8 +641,8 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.23.0': - resolution: {integrity: sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==} + '@esbuild/android-x64@0.24.0': + resolution: {integrity: sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==} engines: {node: '>=18'} cpu: [x64] os: [android] @@ -643,8 +653,8 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.23.0': - resolution: {integrity: sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==} + '@esbuild/darwin-arm64@0.24.0': + resolution: {integrity: sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] @@ -655,8 +665,8 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.23.0': - resolution: {integrity: sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==} + '@esbuild/darwin-x64@0.24.0': + resolution: {integrity: sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] @@ -667,8 +677,8 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.23.0': - resolution: {integrity: sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==} + '@esbuild/freebsd-arm64@0.24.0': + resolution: {integrity: sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] @@ -679,8 +689,8 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.23.0': - resolution: {integrity: sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==} + '@esbuild/freebsd-x64@0.24.0': + resolution: {integrity: sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] @@ -691,8 +701,8 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.23.0': - resolution: {integrity: sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==} + '@esbuild/linux-arm64@0.24.0': + resolution: {integrity: sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] @@ -703,8 +713,8 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.23.0': - resolution: {integrity: sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==} + '@esbuild/linux-arm@0.24.0': + resolution: {integrity: sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==} engines: {node: '>=18'} cpu: [arm] os: [linux] @@ -715,8 +725,8 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.23.0': - resolution: {integrity: sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==} + '@esbuild/linux-ia32@0.24.0': + resolution: {integrity: sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] @@ -727,8 +737,8 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.23.0': - resolution: {integrity: sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==} + '@esbuild/linux-loong64@0.24.0': + resolution: {integrity: sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==} engines: {node: '>=18'} cpu: [loong64] os: [linux] @@ -739,8 +749,8 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.23.0': - resolution: {integrity: sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==} + '@esbuild/linux-mips64el@0.24.0': + resolution: {integrity: sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] @@ -751,8 +761,8 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.23.0': - resolution: {integrity: sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==} + '@esbuild/linux-ppc64@0.24.0': + resolution: {integrity: sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] @@ -763,8 +773,8 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.23.0': - resolution: {integrity: sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==} + '@esbuild/linux-riscv64@0.24.0': + resolution: {integrity: sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] @@ -775,8 +785,8 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.23.0': - resolution: {integrity: sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==} + '@esbuild/linux-s390x@0.24.0': + resolution: {integrity: sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==} engines: {node: '>=18'} cpu: [s390x] os: [linux] @@ -787,8 +797,8 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.23.0': - resolution: {integrity: sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==} + '@esbuild/linux-x64@0.24.0': + resolution: {integrity: sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==} engines: {node: '>=18'} cpu: [x64] os: [linux] @@ -799,14 +809,14 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.23.0': - resolution: {integrity: sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==} + '@esbuild/netbsd-x64@0.24.0': + resolution: {integrity: sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.23.0': - resolution: {integrity: sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==} + '@esbuild/openbsd-arm64@0.24.0': + resolution: {integrity: sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] @@ -817,8 +827,8 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.23.0': - resolution: {integrity: sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==} + '@esbuild/openbsd-x64@0.24.0': + resolution: {integrity: sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] @@ -829,8 +839,8 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.23.0': - resolution: {integrity: sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==} + '@esbuild/sunos-x64@0.24.0': + resolution: {integrity: sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] @@ -841,8 +851,8 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.23.0': - resolution: {integrity: sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==} + '@esbuild/win32-arm64@0.24.0': + resolution: {integrity: sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] @@ -853,8 +863,8 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.23.0': - resolution: {integrity: sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==} + '@esbuild/win32-ia32@0.24.0': + resolution: {integrity: sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==} engines: {node: '>=18'} cpu: [ia32] os: [win32] @@ -865,8 +875,8 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.23.0': - resolution: {integrity: sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==} + '@esbuild/win32-x64@0.24.0': + resolution: {integrity: sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -877,21 +887,70 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.11.0': - resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==} + '@eslint-community/eslint-utils@4.4.1': + resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.11.1': + resolution: {integrity: sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} '@eslint/eslintrc@2.1.4': resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@eslint/js@8.57.0': - resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@fastify/busboy@2.1.1': - resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} - engines: {node: '>=14'} + '@faker-js/faker@9.2.0': + resolution: {integrity: sha512-ulqQu4KMr1/sTFIYvqSdegHT8NIkt66tFAkugGnHA+1WAfEn6hMzNR+svjXGFRVLnapxvej67Z/LwchFrnLBUg==} + engines: {node: '>=18.0.0', npm: '>=9.0.0'} + + '@floating-ui/core@1.6.8': + resolution: {integrity: sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==} + + '@floating-ui/dom@1.6.11': + resolution: {integrity: sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ==} + + '@floating-ui/react-dom@2.1.2': + resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/react@0.26.24': + resolution: {integrity: sha512-2ly0pCkZIGEQUq5H8bBK0XJmc1xIK/RM3tvVzY3GBER7IOD1UgmC2Y2tjj4AuS+TC+vTE1KJv2053290jua0Sw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.8': + resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==} + + '@formatjs/ecma402-abstract@2.2.4': + resolution: {integrity: sha512-lFyiQDVvSbQOpU+WFd//ILolGj4UgA/qXrKeZxdV14uKiAUiPAtX6XAn7WBCRi7Mx6I7EybM9E5yYn4BIpZWYg==} + + '@formatjs/fast-memoize@2.2.3': + resolution: {integrity: sha512-3jeJ+HyOfu8osl3GNSL4vVHUuWFXR03Iz9jjgI7RwjG6ysu/Ymdr0JRCPHfF5yGbTE6JCrd63EpvX1/WybYRbA==} + + '@formatjs/icu-messageformat-parser@2.9.4': + resolution: {integrity: sha512-Tbvp5a9IWuxUcpWNIW6GlMQYEc4rwNHR259uUFoKWNN1jM9obf9Ul0e+7r7MvFOBNcN+13K7NuKCKqQiAn1QEg==} + + '@formatjs/icu-skeleton-parser@1.8.8': + resolution: {integrity: sha512-vHwK3piXwamFcx5YQdCdJxUQ1WdTl6ANclt5xba5zLGDv5Bsur7qz8AD7BevaKxITwpgDeU0u8My3AIibW9ywA==} + + '@formatjs/intl-localematcher@0.5.4': + resolution: {integrity: sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==} + + '@formatjs/intl-localematcher@0.5.8': + resolution: {integrity: sha512-I+WDNWWJFZie+jkfkiK5Mp4hEDyRSEvmyfYadflOno/mmKJKcB17fEpEH0oJu/OWhhCJ8kJBDz2YMd/6cDl7Mg==} '@grpc/grpc-js@1.11.1': resolution: {integrity: sha512-gyt/WayZrVPH2w/UTLansS7F9Nwld472JxxaETamrM8HNlsa+jSLNyKAZmhxI2Me4c3mQHFiS1wWHDY1g1Kthw==} @@ -908,20 +967,20 @@ packages: '@hapi/topo@5.1.0': resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} - '@headlessui/react@1.7.19': - resolution: {integrity: sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw==} + '@headlessui/react@2.1.9': + resolution: {integrity: sha512-ckWw7vlKtnoa1fL2X0fx1a3t/Li9MIKDVXn3SgG65YlxvDAsNrY39PPCxVM7sQRA7go2fJsuHSSauKFNaJHH7A==} engines: {node: '>=10'} peerDependencies: - react: ^16 || ^17 || ^18 - react-dom: ^16 || ^17 || ^18 + react: ^18 + react-dom: ^18 '@heroicons/react@2.1.3': resolution: {integrity: sha512-fEcPfo4oN345SoqdlCDdSa4ivjaKbk0jTd+oubcgNxnNgAfzysfwWfQUr+51wigiWHQQRiZNd1Ao0M5Y3M2EGg==} peerDependencies: react: '>= 16' - '@humanwhocodes/config-array@0.11.14': - resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} engines: {node: '>=10.10.0'} deprecated: Use @eslint/config-array instead @@ -933,14 +992,115 @@ packages: resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} deprecated: Use @eslint/object-schema instead + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} - '@jest/schemas@29.6.3': - resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jridgewell/gen-mapping@0.3.5': resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} @@ -972,122 +1132,62 @@ packages: resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} hasBin: true - '@next/env@14.2.3': - resolution: {integrity: sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA==} + '@next/env@15.0.4-canary.23': + resolution: {integrity: sha512-NfBMRPa10yaEzQ693kGEsgHL58Y27jSbGCDbyXy14dx3z6UeQZQfEVRAwJ4iG1V6gND9+CzzugtiXvJZfSlC9A==} - '@next/env@14.2.5': - resolution: {integrity: sha512-/zZGkrTOsraVfYjGP8uM0p6r0BDT6xWpkjdVbcz66PJVSpwXX3yNiRycxAuDfBKGWBrZBXRuK/YVlkNgxHGwmA==} + '@next/eslint-plugin-next@14.2.18': + resolution: {integrity: sha512-KyYTbZ3GQwWOjX3Vi1YcQbekyGP0gdammb7pbmmi25HBUCINzDReyrzCMOJIeZisK1Q3U6DT5Rlc4nm2/pQeXA==} - '@next/eslint-plugin-next@14.2.5': - resolution: {integrity: sha512-LY3btOpPh+OTIpviNojDpUdIbHW9j0JBYBjsIp8IxtDFfYFyORvw3yNq6N231FVqQA7n7lwaf7xHbVJlA1ED7g==} - - '@next/swc-darwin-arm64@14.2.3': - resolution: {integrity: sha512-3pEYo/RaGqPP0YzwnlmPN2puaF2WMLM3apt5jLW2fFdXD9+pqcoTzRk+iZsf8ta7+quAe4Q6Ms0nR0SFGFdS1A==} + '@next/swc-darwin-arm64@15.0.4-canary.23': + resolution: {integrity: sha512-sX3MaDUiFiMT14KSx5mJz6B+IH9k7+buNniNrDP7iz4YG28jssm9e8uHbiWXsbn9jnkQUJu8PHoUOLhgjZgtsQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-arm64@14.2.5': - resolution: {integrity: sha512-/9zVxJ+K9lrzSGli1///ujyRfon/ZneeZ+v4ptpiPoOU+GKZnm8Wj8ELWU1Pm7GHltYRBklmXMTUqM/DqQ99FQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - '@next/swc-darwin-x64@14.2.3': - resolution: {integrity: sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==} + '@next/swc-darwin-x64@15.0.4-canary.23': + resolution: {integrity: sha512-KJRSDVvEPuvjRKe9IY3YMAv9KMOmB/U5+7g0c3OTT/50x1KL0XOlgnc+Af2GdZKIrkKiAdTFG54AHaSD584yHg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-darwin-x64@14.2.5': - resolution: {integrity: sha512-vXHOPCwfDe9qLDuq7U1OYM2wUY+KQ4Ex6ozwsKxp26BlJ6XXbHleOUldenM67JRyBfVjv371oneEvYd3H2gNSA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - - '@next/swc-linux-arm64-gnu@14.2.3': - resolution: {integrity: sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==} + '@next/swc-linux-arm64-gnu@15.0.4-canary.23': + resolution: {integrity: sha512-0EqeqGdlG0MPDYGE/cPtTvBLtBiWDAd7fSRgRhIga6CkuaRVFKuTeRrsjTa0v+51C2OawjQp2N3ww1zBLuBhcg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-gnu@14.2.5': - resolution: {integrity: sha512-vlhB8wI+lj8q1ExFW8lbWutA4M2ZazQNvMWuEDqZcuJJc78iUnLdPPunBPX8rC4IgT6lIx/adB+Cwrl99MzNaA==} + '@next/swc-linux-arm64-musl@15.0.4-canary.23': + resolution: {integrity: sha512-O06Gw8HU0z9f1b4TiGb0u1o87hgLa0yEW1odyLPE1d3+JKwhkh4L1Ug9uLpeqEUnxCoIrwVomEUyQBPGNQtq0Q==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@14.2.3': - resolution: {integrity: sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@next/swc-linux-arm64-musl@14.2.5': - resolution: {integrity: sha512-NpDB9NUR2t0hXzJJwQSGu1IAOYybsfeB+LxpGsXrRIb7QOrYmidJz3shzY8cM6+rO4Aojuef0N/PEaX18pi9OA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@next/swc-linux-x64-gnu@14.2.3': - resolution: {integrity: sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==} + '@next/swc-linux-x64-gnu@15.0.4-canary.23': + resolution: {integrity: sha512-BvERc3hri6eyUHnasZgwcRCdR8WpfCdKKe/M12Q+ZAkTeJeVkLXNakznaZbBWdlCc77F/NeHz/OWoQWUTpKm3g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-gnu@14.2.5': - resolution: {integrity: sha512-8XFikMSxWleYNryWIjiCX+gU201YS+erTUidKdyOVYi5qUQo/gRxv/3N1oZFCgqpesN6FPeqGM72Zve+nReVXQ==} + '@next/swc-linux-x64-musl@15.0.4-canary.23': + resolution: {integrity: sha512-FF5LNTdra/tHxdHjRR3lb+UxFgRVT+v3EMruueQg6BpOqpciodyCkkYQFrx2DitpADojQ6bBBFBDs6KIb8jB5w==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@14.2.3': - resolution: {integrity: sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@next/swc-linux-x64-musl@14.2.5': - resolution: {integrity: sha512-6QLwi7RaYiQDcRDSU/os40r5o06b5ue7Jsk5JgdRBGGp8l37RZEh9JsLSM8QF0YDsgcosSeHjglgqi25+m04IQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@next/swc-win32-arm64-msvc@14.2.3': - resolution: {integrity: sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==} + '@next/swc-win32-arm64-msvc@15.0.4-canary.23': + resolution: {integrity: sha512-XnHD7fqQYZR1XCCuAf8+yAdkMpzAFz2pWmny2K6g5C7BalrwNuxWLsM5LycW1PTMzSqkzLJeXCG6AZu099u7/w==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-arm64-msvc@14.2.5': - resolution: {integrity: sha512-1GpG2VhbspO+aYoMOQPQiqc/tG3LzmsdBH0LhnDS3JrtDx2QmzXe0B6mSZZiN3Bq7IOMXxv1nlsjzoS1+9mzZw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - - '@next/swc-win32-ia32-msvc@14.2.3': - resolution: {integrity: sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==} - engines: {node: '>= 10'} - cpu: [ia32] - os: [win32] - - '@next/swc-win32-ia32-msvc@14.2.5': - resolution: {integrity: sha512-Igh9ZlxwvCDsu6438FXlQTHlRno4gFpJzqPjSIBZooD22tKeI4fE/YMRoHVJHmrQ2P5YL1DoZ0qaOKkbeFWeMg==} - engines: {node: '>= 10'} - cpu: [ia32] - os: [win32] - - '@next/swc-win32-x64-msvc@14.2.3': - resolution: {integrity: sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==} + '@next/swc-win32-x64-msvc@15.0.4-canary.23': + resolution: {integrity: sha512-HGoW8LjYxbUhkND+vJ/21dWQ7sdv4SIUQDv2r/FpcdHFMzb5M/jgQVqcMFkqg2ibH65ZAcVBM0ICcUnTLlX7PQ==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@next/swc-win32-x64-msvc@14.2.5': - resolution: {integrity: sha512-tEQ7oinq1/CjSG9uSTerca3v4AZ+dFa+4Yu6ihaG8Ud8ddqLQgFGcnwYls13H5X5CPDPZJdYxyeMui6muOLd4g==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] + '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': + resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -1101,10 +1201,110 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + + '@otplib/core@12.0.1': + resolution: {integrity: sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==} + + '@otplib/plugin-crypto@12.0.1': + resolution: {integrity: sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==} + + '@otplib/plugin-thirty-two@12.0.1': + resolution: {integrity: sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==} + + '@parcel/watcher-android-arm64@2.5.0': + resolution: {integrity: sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.0': + resolution: {integrity: sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.0': + resolution: {integrity: sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.0': + resolution: {integrity: sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.0': + resolution: {integrity: sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.0': + resolution: {integrity: sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.0': + resolution: {integrity: sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.0': + resolution: {integrity: sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.0': + resolution: {integrity: sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.0': + resolution: {integrity: sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.0': + resolution: {integrity: sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.0': + resolution: {integrity: sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.0': + resolution: {integrity: sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.0': + resolution: {integrity: sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==} + engines: {node: '>= 10.0.0'} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@playwright/test@1.48.2': + resolution: {integrity: sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==} + engines: {node: '>=18'} + hasBin: true + '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -1135,83 +1335,124 @@ packages: '@protobufjs/utf8@1.1.0': resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} - '@rollup/rollup-android-arm-eabi@4.19.1': - resolution: {integrity: sha512-XzqSg714++M+FXhHfXpS1tDnNZNpgxxuGZWlRG/jSj+VEPmZ0yg6jV4E0AL3uyBKxO8mO3xtOsP5mQ+XLfrlww==} + '@react-aria/focus@3.18.3': + resolution: {integrity: sha512-WKUElg+5zS0D3xlVn8MntNnkzJql2J6MuzAMP8Sv5WTgFDse/XGR842dsxPTIyKKdrWVCRegCuwa4m3n/GzgJw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + + '@react-aria/interactions@3.22.3': + resolution: {integrity: sha512-RRUb/aG+P0IKTIWikY/SylB6bIbLZeztnZY2vbe7RAG5MgVaCgn5HQ45SI15GlTmhsFG8CnF6slJsUFJiNHpbQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + + '@react-aria/ssr@3.9.6': + resolution: {integrity: sha512-iLo82l82ilMiVGy342SELjshuWottlb5+VefO3jOQqQRNYnJBFpUSadswDPbRimSgJUZuFwIEYs6AabkP038fA==} + engines: {node: '>= 12'} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + + '@react-aria/utils@3.25.3': + resolution: {integrity: sha512-PR5H/2vaD8fSq0H/UB9inNbc8KDcVmW6fYAfSWkkn+OAdhTTMVKqXXrZuZBWyFfSD5Ze7VN6acr4hrOQm2bmrA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + + '@react-stately/utils@3.10.4': + resolution: {integrity: sha512-gBEQEIMRh5f60KCm7QKQ2WfvhB2gLUr9b72sqUdIZ2EG+xuPgaIlCBeSicvjmjBvYZwOjoOEnmIkcx2GHp/HWw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + + '@react-types/shared@3.25.0': + resolution: {integrity: sha512-OZSyhzU6vTdW3eV/mz5i6hQwQUhkRs7xwY2d1aqPvTdMe0+2cY7Fwp45PAiwYLEj73i9ro2FxF9qC4DvHGSCgQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + + '@rollup/rollup-android-arm-eabi@4.25.0': + resolution: {integrity: sha512-CC/ZqFZwlAIbU1wUPisHyV/XRc5RydFrNLtgl3dGYskdwPZdt4HERtKm50a/+DtTlKeCq9IXFEWR+P6blwjqBA==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.19.1': - resolution: {integrity: sha512-thFUbkHteM20BGShD6P08aungq4irbIZKUNbG70LN8RkO7YztcGPiKTTGZS7Kw+x5h8hOXs0i4OaHwFxlpQN6A==} + '@rollup/rollup-android-arm64@4.25.0': + resolution: {integrity: sha512-/Y76tmLGUJqVBXXCfVS8Q8FJqYGhgH4wl4qTA24E9v/IJM0XvJCGQVSW1QZ4J+VURO9h8YCa28sTFacZXwK7Rg==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.19.1': - resolution: {integrity: sha512-8o6eqeFZzVLia2hKPUZk4jdE3zW7LCcZr+MD18tXkgBBid3lssGVAYuox8x6YHoEPDdDa9ixTaStcmx88lio5Q==} + '@rollup/rollup-darwin-arm64@4.25.0': + resolution: {integrity: sha512-YVT6L3UrKTlC0FpCZd0MGA7NVdp7YNaEqkENbWQ7AOVOqd/7VzyHpgIpc1mIaxRAo1ZsJRH45fq8j4N63I/vvg==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.19.1': - resolution: {integrity: sha512-4T42heKsnbjkn7ovYiAdDVRRWZLU9Kmhdt6HafZxFcUdpjlBlxj4wDrt1yFWLk7G4+E+8p2C9tcmSu0KA6auGA==} + '@rollup/rollup-darwin-x64@4.25.0': + resolution: {integrity: sha512-ZRL+gexs3+ZmmWmGKEU43Bdn67kWnMeWXLFhcVv5Un8FQcx38yulHBA7XR2+KQdYIOtD0yZDWBCudmfj6lQJoA==} cpu: [x64] os: [darwin] - '@rollup/rollup-linux-arm-gnueabihf@4.19.1': - resolution: {integrity: sha512-MXg1xp+e5GhZ3Vit1gGEyoC+dyQUBy2JgVQ+3hUrD9wZMkUw/ywgkpK7oZgnB6kPpGrxJ41clkPPnsknuD6M2Q==} + '@rollup/rollup-freebsd-arm64@4.25.0': + resolution: {integrity: sha512-xpEIXhiP27EAylEpreCozozsxWQ2TJbOLSivGfXhU4G1TBVEYtUPi2pOZBnvGXHyOdLAUUhPnJzH3ah5cqF01g==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.25.0': + resolution: {integrity: sha512-sC5FsmZGlJv5dOcURrsnIK7ngc3Kirnx3as2XU9uER+zjfyqIjdcMVgzy4cOawhsssqzoAX19qmxgJ8a14Qrqw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.25.0': + resolution: {integrity: sha512-uD/dbLSs1BEPzg564TpRAQ/YvTnCds2XxyOndAO8nJhaQcqQGFgv/DAVko/ZHap3boCvxnzYMa3mTkV/B/3SWA==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.19.1': - resolution: {integrity: sha512-DZNLwIY4ftPSRVkJEaxYkq7u2zel7aah57HESuNkUnz+3bZHxwkCUkrfS2IWC1sxK6F2QNIR0Qr/YXw7nkF3Pw==} + '@rollup/rollup-linux-arm-musleabihf@4.25.0': + resolution: {integrity: sha512-ZVt/XkrDlQWegDWrwyC3l0OfAF7yeJUF4fq5RMS07YM72BlSfn2fQQ6lPyBNjt+YbczMguPiJoCfaQC2dnflpQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.19.1': - resolution: {integrity: sha512-C7evongnjyxdngSDRRSQv5GvyfISizgtk9RM+z2biV5kY6S/NF/wta7K+DanmktC5DkuaJQgoKGf7KUDmA7RUw==} + '@rollup/rollup-linux-arm64-gnu@4.25.0': + resolution: {integrity: sha512-qboZ+T0gHAW2kkSDPHxu7quaFaaBlynODXpBVnPxUgvWYaE84xgCKAPEYE+fSMd3Zv5PyFZR+L0tCdYCMAtG0A==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.19.1': - resolution: {integrity: sha512-89tFWqxfxLLHkAthAcrTs9etAoBFRduNfWdl2xUs/yLV+7XDrJ5yuXMHptNqf1Zw0UCA3cAutkAiAokYCkaPtw==} + '@rollup/rollup-linux-arm64-musl@4.25.0': + resolution: {integrity: sha512-ndWTSEmAaKr88dBuogGH2NZaxe7u2rDoArsejNslugHZ+r44NfWiwjzizVS1nUOHo+n1Z6qV3X60rqE/HlISgw==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.19.1': - resolution: {integrity: sha512-PromGeV50sq+YfaisG8W3fd+Cl6mnOOiNv2qKKqKCpiiEke2KiKVyDqG/Mb9GWKbYMHj5a01fq/qlUR28PFhCQ==} + '@rollup/rollup-linux-powerpc64le-gnu@4.25.0': + resolution: {integrity: sha512-BVSQvVa2v5hKwJSy6X7W1fjDex6yZnNKy3Kx1JGimccHft6HV0THTwNtC2zawtNXKUu+S5CjXslilYdKBAadzA==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.19.1': - resolution: {integrity: sha512-/1BmHYh+iz0cNCP0oHCuF8CSiNj0JOGf0jRlSo3L/FAyZyG2rGBuKpkZVH9YF+x58r1jgWxvm1aRg3DHrLDt6A==} + '@rollup/rollup-linux-riscv64-gnu@4.25.0': + resolution: {integrity: sha512-G4hTREQrIdeV0PE2JruzI+vXdRnaK1pg64hemHq2v5fhv8C7WjVaeXc9P5i4Q5UC06d/L+zA0mszYIKl+wY8oA==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.19.1': - resolution: {integrity: sha512-0cYP5rGkQWRZKy9/HtsWVStLXzCF3cCBTRI+qRL8Z+wkYlqN7zrSYm6FuY5Kd5ysS5aH0q5lVgb/WbG4jqXN1Q==} + '@rollup/rollup-linux-s390x-gnu@4.25.0': + resolution: {integrity: sha512-9T/w0kQ+upxdkFL9zPVB6zy9vWW1deA3g8IauJxojN4bnz5FwSsUAD034KpXIVX5j5p/rn6XqumBMxfRkcHapQ==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.19.1': - resolution: {integrity: sha512-XUXeI9eM8rMP8aGvii/aOOiMvTs7xlCosq9xCjcqI9+5hBxtjDpD+7Abm1ZhVIFE1J2h2VIg0t2DX/gjespC2Q==} + '@rollup/rollup-linux-x64-gnu@4.25.0': + resolution: {integrity: sha512-ThcnU0EcMDn+J4B9LD++OgBYxZusuA7iemIIiz5yzEcFg04VZFzdFjuwPdlURmYPZw+fgVrFzj4CA64jSTG4Ig==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.19.1': - resolution: {integrity: sha512-V7cBw/cKXMfEVhpSvVZhC+iGifD6U1zJ4tbibjjN+Xi3blSXaj/rJynAkCFFQfoG6VZrAiP7uGVzL440Q6Me2Q==} + '@rollup/rollup-linux-x64-musl@4.25.0': + resolution: {integrity: sha512-zx71aY2oQxGxAT1JShfhNG79PnjYhMC6voAjzpu/xmMjDnKNf6Nl/xv7YaB/9SIa9jDYf8RBPWEnjcdlhlv1rQ==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.19.1': - resolution: {integrity: sha512-88brja2vldW/76jWATlBqHEoGjJLRnP0WOEKAUbMcXaAZnemNhlAHSyj4jIwMoP2T750LE9lblvD4e2jXleZsA==} + '@rollup/rollup-win32-arm64-msvc@4.25.0': + resolution: {integrity: sha512-JT8tcjNocMs4CylWY/CxVLnv8e1lE7ff1fi6kbGocWwxDq9pj30IJ28Peb+Y8yiPNSF28oad42ApJB8oUkwGww==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.19.1': - resolution: {integrity: sha512-LdxxcqRVSXi6k6JUrTah1rHuaupoeuiv38du8Mt4r4IPer3kwlTo+RuvfE8KzZ/tL6BhaPlzJ3835i6CxrFIRQ==} + '@rollup/rollup-win32-ia32-msvc@4.25.0': + resolution: {integrity: sha512-dRLjLsO3dNOfSN6tjyVlG+Msm4IiZnGkuZ7G5NmpzwF9oOc582FZG05+UdfTbz5Jd4buK/wMb6UeHFhG18+OEg==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.19.1': - resolution: {integrity: sha512-2bIrL28PcK3YCqD9anGxDxamxdiJAxA+l7fWIwM5o8UqNy1t3d1NdAweO2XhA0KTDJ5aH1FsuiT5+7VhtHliXg==} + '@rollup/rollup-win32-x64-msvc@4.25.0': + resolution: {integrity: sha512-/RqrIFtLB926frMhZD0a5oDa4eFIbyNEwLLloMTEjmqfwZWXywwVVOVmwTsuyhC9HKkVEZcOOi+KV4U9wmOdlg==} cpu: [x64] os: [win32] @@ -1227,12 +1468,16 @@ packages: '@sideway/pinpoint@2.0.0': resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} - '@sinclair/typebox@0.27.8': - resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + '@sindresorhus/merge-streams@2.3.0': + resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} + engines: {node: '>=18'} '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + '@swc/helpers@0.5.13': + resolution: {integrity: sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==} + '@swc/helpers@0.5.5': resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} @@ -1246,44 +1491,37 @@ packages: peerDependencies: tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1' - '@tanstack/react-virtual@3.8.4': - resolution: {integrity: sha512-Dq0VQr3QlTS2qL35g360QaJWBt7tCn/0xw4uZ0dHXPLO1Ak4Z4nVX4vuj1Npg1b/jqNMDToRtR5OIxM2NXRBWg==} + '@tanstack/react-virtual@3.10.6': + resolution: {integrity: sha512-xaSy6uUxB92O8mngHZ6CvbhGuqxQ5lIZWCBy+FjhrbHmOwc6BnOnKkYm2FsB1/BpKw/+FVctlMbEtI+F6I1aJg==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - '@tanstack/virtual-core@3.8.4': - resolution: {integrity: sha512-iO5Ujgw3O1yIxWDe9FgUPNkGjyT657b1WNX52u+Wv1DyBFEpdCdGkuVaky0M3hHFqNWjAmHWTn4wgj9rTr7ZQg==} + '@tanstack/virtual-core@3.10.6': + resolution: {integrity: sha512-1giLc4dzgEKLMx5pgKjL6HlG5fjZMgCjzlKAlpr7yoUtetVPELgER1NtephAI910nMwfPTHNyWKSFmJdHkz2Cw==} '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} engines: {node: '>=18'} - '@testing-library/dom@9.3.4': - resolution: {integrity: sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==} - engines: {node: '>=14'} - - '@testing-library/jest-dom@6.4.8': - resolution: {integrity: sha512-JD0G+Zc38f5MBHA4NgxQMR5XtO5Jx9g86jqturNTt2WUfRmLDIY7iKkWHDCCTiDuFMre6nxAD5wHw9W5kI4rGw==} + '@testing-library/jest-dom@6.6.3': + resolution: {integrity: sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} - '@testing-library/react@14.3.1': - resolution: {integrity: sha512-H99XjUhWQw0lTgyMN05W3xQG1Nh4lq574D8keFf1dDoNTJgp66VbJozRaczoF+wsiaPJNt/TcnfpLGufGxSrZQ==} - engines: {node: '>=14'} - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - - '@testing-library/react@15.0.7': - resolution: {integrity: sha512-cg0RvEdD1TIhhkm1IeYMQxrzy0MtUNfa3minv4MjbgcYzJAZ7yD0i0lwoPOTPr+INtiXFezt2o8xMSnyHhEn2Q==} + '@testing-library/react@16.0.1': + resolution: {integrity: sha512-dSmwJVtJXmku+iocRhWOUFbrERC76TX2Mnf0ATODz8brzAZrMBbzLwQixlBSanZxR6LddK3eiwpSFZgDET1URg==} engines: {node: '>=18'} peerDependencies: - '@types/react': ^18.0.0 + '@testing-library/dom': ^10.0.0 + '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 react: ^18.0.0 react-dom: ^18.0.0 peerDependenciesMeta: '@types/react': optional: true + '@types/react-dom': + optional: true '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -1300,56 +1538,26 @@ packages: '@types/babel__traverse@7.20.6': resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} - '@types/estree@1.0.5': - resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/minimist@1.2.5': - resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} - - '@types/ms@0.7.31': - resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} + '@types/ms@0.7.34': + resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - '@types/node@18.11.9': - resolution: {integrity: sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==} - - '@types/node@20.14.13': - resolution: {integrity: sha512-+bHoGiZb8UiQ0+WEtmph2IWQCjIqg8MDZMAV+ppRRhUZnquF5mQkP/9vpSwJClEiSM/C7fZZExPzfU0vJTyp8w==} - - '@types/normalize-package-data@2.4.4': - resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/node@22.9.0': + resolution: {integrity: sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==} '@types/prop-types@15.7.12': resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} - '@types/react-dom@18.0.9': - resolution: {integrity: sha512-qnVvHxASt/H7i+XG1U1xMiY5t+IHcPGUK7TDMDzom08xa7e86eCeKOiLZezwCKVxJn6NEiiy2ekgX8aQssjIKg==} - - '@types/react-dom@18.3.0': - resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} - - '@types/react@17.0.80': - resolution: {integrity: sha512-LrgHIu2lEtIo8M7d1FcI3BdwXWoRQwMoXOZ7+dPTW0lYREjmlHl3P0U1VD0i/9tppOuv8/sam7sOjx34TxSFbA==} - - '@types/react@18.2.8': - resolution: {integrity: sha512-lTyWUNrd8ntVkqycEEplasWy2OxNlShj3zqS0LuB1ENUGis5HodmhM7DtCoUGbxj3VW/WsGA0DUhpG6XrM7gPA==} - - '@types/react@18.3.3': - resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==} - - '@types/scheduler@0.16.8': - resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} - - '@types/scheduler@0.23.0': - resolution: {integrity: sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==} - - '@types/semver@7.5.8': - resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} + '@types/react@18.3.12': + resolution: {integrity: sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==} '@types/sinonjs__fake-timers@8.1.1': resolution: {integrity: sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==} @@ -1360,12 +1568,23 @@ packages: '@types/tinycolor2@1.4.3': resolution: {integrity: sha512-Kf1w9NE5HEgGxCRyIcRXR/ZYtDv0V8FVPtYHwLxl0O+maGX0erE77pQlD0gpP+/KByMZ87mOA79SjifhSB3PjQ==} - '@types/uuid@9.0.8': - resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + '@typescript-eslint/eslint-plugin@8.15.0': + resolution: {integrity: sha512-+zkm9AR1Ds9uLWN3fkoeXgFppaQ+uEVtfOV62dDmsy9QCNqlRHWNEck4yarvRNrvRcHQLGfqBNui3cimoz8XAg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^7.9.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + '@typescript-eslint/parser@7.18.0': resolution: {integrity: sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==} engines: {node: ^18.18.0 || >=20.0.0} @@ -1380,10 +1599,28 @@ packages: resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/scope-manager@8.15.0': + resolution: {integrity: sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/type-utils@8.15.0': + resolution: {integrity: sha512-UU6uwXDoI3JGSXmcdnP5d8Fffa2KayOhUUqr/AiBnG1Gl7+7ut/oyagVeSkh7bxQ0zSXV9ptRh/4N15nkCqnpw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + '@typescript-eslint/types@7.18.0': resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/types@8.15.0': + resolution: {integrity: sha512-n3Gt8Y/KyJNe0S3yDCD2RVKrHBC4gTUcLTebVBXacPy091E6tNspFLKRXlk3hwT4G55nfr1n2AdFqi/XMxzmPQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@7.18.0': resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -1393,10 +1630,33 @@ packages: typescript: optional: true + '@typescript-eslint/typescript-estree@8.15.0': + resolution: {integrity: sha512-1eMp2JgNec/niZsR7ioFBlsh/Fk0oJbhaqO0jRyQBMgkz7RrFfkqF9lYYmBoGBaSiLnu8TAPQTwoTUiSTUW9dg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@8.15.0': + resolution: {integrity: sha512-k82RI9yGhr0QM3Dnq+egEpz9qB6Un+WLYhmoNcvl8ltMEededhh7otBVVIDDsEEttauwdY/hQoSsOv13lxrFzQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + '@typescript-eslint/visitor-keys@7.18.0': resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/visitor-keys@8.15.0': + resolution: {integrity: sha512-h8vYOulWec9LhpwfAdZf2bjr8xIp0KNKnpgqSz0qqYYKAW/QZKw3ktRndbiAtUz4acH4QLQavwZBYCc0wulA/Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} @@ -1414,26 +1674,40 @@ packages: '@vercel/git-hooks@1.0.0': resolution: {integrity: sha512-OxDFAAdyiJ/H0b8zR9rFCu3BIb78LekBXOphOYG3snV4ULhKFX387pBPpqZ9HLiRTejBWBxYEahkw79tuIgdAA==} - '@vitejs/plugin-react@4.3.1': - resolution: {integrity: sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==} + '@vitejs/plugin-react@4.3.3': + resolution: {integrity: sha512-NooDe9GpHGqNns1i8XDERg0Vsg5SSYRhRxxyTGogUdkdNt47jal+fbuYi+Yfq6pzRCKXyoPcWisfxE6RIM3GKA==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: vite: ^4.2.0 || ^5.0.0 - '@vitest/expect@1.6.0': - resolution: {integrity: sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==} + '@vitest/expect@2.1.4': + resolution: {integrity: sha512-DOETT0Oh1avie/D/o2sgMHGrzYUFFo3zqESB2Hn70z6QB1HrS2IQ9z5DfyTqU8sg4Bpu13zZe9V4+UTNQlUeQA==} - '@vitest/runner@1.6.0': - resolution: {integrity: sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==} + '@vitest/mocker@2.1.4': + resolution: {integrity: sha512-Ky/O1Lc0QBbutJdW0rqLeFNbuLEyS+mIPiNdlVlp2/yhJ0SbyYqObS5IHdhferJud8MbbwMnexg4jordE5cCoQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true - '@vitest/snapshot@1.6.0': - resolution: {integrity: sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==} + '@vitest/pretty-format@2.1.4': + resolution: {integrity: sha512-L95zIAkEuTDbUX1IsjRl+vyBSLh3PwLLgKpghl37aCK9Jvw0iP+wKwIFhfjdUtA2myLgjrG6VU6JCFLv8q/3Ww==} - '@vitest/spy@1.6.0': - resolution: {integrity: sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==} + '@vitest/runner@2.1.4': + resolution: {integrity: sha512-sKRautINI9XICAMl2bjxQM8VfCMTB0EbsBc/EDFA57V6UQevEKY/TOPOF5nzcvCALltiLfXWbq4MaAwWx/YxIA==} - '@vitest/utils@1.6.0': - resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==} + '@vitest/snapshot@2.1.4': + resolution: {integrity: sha512-3Kab14fn/5QZRog5BPj6Rs8dc4B+mim27XaKWFWHWA87R56AKjHTGcBFKpvZKDzC4u5Wd0w/qKsUIio3KzWW4Q==} + + '@vitest/spy@2.1.4': + resolution: {integrity: sha512-4JOxa+UAizJgpZfaCPKK2smq9d8mmjZVPMt2kOsg/R8QkoRzydHH1qHxIYNvr1zlEaFj4SXiaaJWxq/LPLKaLg==} + + '@vitest/utils@2.1.4': + resolution: {integrity: sha512-MXDnZn0Awl2S86PSNIim5PWXgIAx8CIkzu35mBdSApUip6RFOGXBCf3YFyeEu8n1IHk4bWD46DeYFu9mQlFIRg==} abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} @@ -1446,22 +1720,6 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn-node@1.8.2: - resolution: {integrity: sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==} - - acorn-walk@7.2.0: - resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==} - engines: {node: '>=0.4.0'} - - acorn-walk@8.3.3: - resolution: {integrity: sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==} - engines: {node: '>=0.4.0'} - - acorn@7.4.1: - resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} - engines: {node: '>=0.4.0'} - hasBin: true - acorn@8.12.1: resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} engines: {node: '>=0.4.0'} @@ -1479,10 +1737,6 @@ packages: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} - aggregate-error@4.0.1: - resolution: {integrity: sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==} - engines: {node: '>=12'} - ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -1494,18 +1748,18 @@ packages: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} + ansi-escapes@7.0.0: + resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} + engines: {node: '>=18'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-regex@6.0.1: - resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} engines: {node: '>=12'} - ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} - ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -1587,10 +1841,6 @@ packages: resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} engines: {node: '>= 0.4'} - arrify@1.0.1: - resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} - engines: {node: '>=0.10.0'} - asn1@0.2.6: resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} @@ -1598,8 +1848,9 @@ packages: resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} engines: {node: '>=0.8'} - assertion-error@1.1.0: - resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} @@ -1608,8 +1859,8 @@ packages: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} - async@3.2.5: - resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -1618,8 +1869,8 @@ packages: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} - autoprefixer@10.4.13: - resolution: {integrity: sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==} + autoprefixer@10.4.20: + resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: @@ -1632,15 +1883,15 @@ packages: aws-sign2@0.7.0: resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} - aws4@1.13.0: - resolution: {integrity: sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g==} + aws4@1.13.2: + resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} axe-core@4.10.0: resolution: {integrity: sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g==} engines: {node: '>=4'} - axios@1.7.2: - resolution: {integrity: sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==} + axios@1.7.7: + resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} axobject-query@3.1.1: resolution: {integrity: sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==} @@ -1678,8 +1929,13 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.23.2: - resolution: {integrity: sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==} + browserslist@4.23.3: + resolution: {integrity: sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + browserslist@4.24.2: + resolution: {integrity: sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -1719,16 +1975,11 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - camelcase-keys@7.0.2: - resolution: {integrity: sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==} - engines: {node: '>=12'} + caniuse-lite@1.0.30001654: + resolution: {integrity: sha512-wLJc602fW0OdrUR+PqsBUH3dgrjDcT+mWs/Kw86zPvgjiqOiI2TXMkBFK4KihYzZclmJxrFwgYhZDSEogFai/g==} - camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} - - caniuse-lite@1.0.30001644: - resolution: {integrity: sha512-YGvlOZB4QhZuiis+ETS0VXR+MExbFf4fZYYeMTEE0aTQd/RdIjkTyZjLrbYVKnHzppDvnOhritRVv+i7Go6mHw==} + caniuse-lite@1.0.30001680: + resolution: {integrity: sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==} case-anything@2.1.13: resolution: {integrity: sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==} @@ -1737,13 +1988,9 @@ packages: caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} - chai@4.5.0: - resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} - engines: {node: '>=4'} - - chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} + chai@5.1.2: + resolution: {integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==} + engines: {node: '>=12'} chalk@3.0.0: resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} @@ -1753,11 +2000,16 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} - check-error@1.0.3: - resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} check-more-types@2.24.0: resolution: {integrity: sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==} @@ -1767,6 +2019,10 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chokidar@4.0.1: + resolution: {integrity: sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==} + engines: {node: '>= 14.16.0'} + chownr@2.0.0: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} @@ -1775,18 +2031,22 @@ packages: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} + ci-info@4.1.0: + resolution: {integrity: sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==} + engines: {node: '>=8'} + clean-stack@2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} - clean-stack@4.2.0: - resolution: {integrity: sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==} - engines: {node: '>=12'} - cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + cli-table3@0.6.5: resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} engines: {node: 10.* || >= 12.*} @@ -1795,9 +2055,9 @@ packages: resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} engines: {node: '>=8'} - cli-truncate@3.1.0: - resolution: {integrity: sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + cli-truncate@4.0.0: + resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} + engines: {node: '>=18'} client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} @@ -1810,23 +2070,28 @@ packages: resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} engines: {node: '>=6'} - color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} - color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + color-support@1.1.3: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} hasBin: true + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -1834,6 +2099,10 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -1842,10 +2111,6 @@ packages: resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} engines: {node: '>= 6'} - commander@9.5.0: - resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} - engines: {node: ^12.20.0 || >=14} - common-tags@1.8.2: resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} engines: {node: '>=4.0.0'} @@ -1853,14 +2118,11 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - concurrently@8.2.2: - resolution: {integrity: sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==} - engines: {node: ^14.13.0 || >=16.0.0} + concurrently@9.1.0: + resolution: {integrity: sha512-VxkzwMAn4LP7WyMnJNbHN5mKV9L2IbyDjpzemKr99sXNR3GqRNMMHdm7prV1ws9wg7ETj6WUkNOigZVsptwbgg==} + engines: {node: '>=18'} hasBin: true - confbox@0.1.7: - resolution: {integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==} - consola@3.2.3: resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} engines: {node: ^14.18.0 || >=16.10.0} @@ -1884,6 +2146,10 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} + cross-spawn@7.0.5: + resolution: {integrity: sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==} + engines: {node: '>= 8'} + css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} @@ -1892,15 +2158,15 @@ packages: engines: {node: '>=4'} hasBin: true - cssstyle@4.0.1: - resolution: {integrity: sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==} + cssstyle@4.1.0: + resolution: {integrity: sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==} engines: {node: '>=18'} csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - cypress@13.13.1: - resolution: {integrity: sha512-8F9UjL5MDUdgC/S5hr8CGLHbS5gGht5UOV184qc2pFny43fnkoaKxlzH/U6//zmGu/xRTaKimNfjknLT8+UDFg==} + cypress@13.15.2: + resolution: {integrity: sha512-ARbnUorjcCM3XiPwgHKuqsyr5W9Qn+pIIBPaoilnoBkLdSC2oLQjV1BUpnmc7KR+b7Avah3Ly2RMFnfxr96E/A==} engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0} hasBin: true @@ -1927,12 +2193,8 @@ packages: resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} engines: {node: '>= 0.4'} - date-fns@2.30.0: - resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} - engines: {node: '>=0.11'} - - dayjs@1.11.12: - resolution: {integrity: sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg==} + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} @@ -1951,23 +2213,20 @@ packages: supports-color: optional: true - decamelize-keys@1.1.1: - resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} - engines: {node: '>=0.10.0'} - - decamelize@1.2.0: - resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} - engines: {node: '>=0.10.0'} - - decamelize@5.0.1: - resolution: {integrity: sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==} - engines: {node: '>=10'} + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true decimal.js@10.4.3: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} - deep-eql@4.1.4: - resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} deep-equal@2.2.3: @@ -1977,6 +2236,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -1985,17 +2248,14 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} - defined@1.0.1: - resolution: {integrity: sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==} - - del-cli@5.0.0: - resolution: {integrity: sha512-rENFhUaYcjoMODwFhhlON+ogN7DoG+4+GFN+bsA1XeDt4w2OKQnQadFP1thHSAlK9FAtl88qgP66wOV+eFZZiQ==} - engines: {node: '>=14.16'} + del-cli@6.0.0: + resolution: {integrity: sha512-9nitGV2W6KLFyya4qYt4+9AKQFL+c0Ehj5K7V7IwlxTc6RMCfQUGY9E9pLG6e8TQjtwXpuiWIGGZb3mfVxyZkw==} + engines: {node: '>=18'} hasBin: true - del@7.1.0: - resolution: {integrity: sha512-v2KyNk7efxhlyHpjEvfyxaAihKKK0nWCuf6ZtqZcFFpQRG0bJ12Qsr0RpvsICMjAAZ8DOVCxrlqpxISlMHC4Kg==} - engines: {node: '>=14.16'} + del@8.0.0: + resolution: {integrity: sha512-R6ep6JJ+eOBZsBr9esiNN1gxFbZE4Q2cULkUSFumGYecAiS6qodDvcPx/sFuWHMNul7DWmrtoEOpYSm7o6tbSA==} + engines: {node: '>=18'} delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} @@ -2021,18 +2281,9 @@ packages: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} - detective@5.2.1: - resolution: {integrity: sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==} - engines: {node: '>=0.8.0'} - hasBin: true - didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} - diff-sequences@29.6.3: - resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -2058,6 +2309,10 @@ packages: resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} engines: {node: '>=12'} + dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + dprint-node@1.0.8: resolution: {integrity: sha512-iVKnUtYfGrYcW1ZAlfR/F59cUVL8QIhWoBJoSjkkdua/dkWIgjZfiLMeTjiB06X0ZLkQ0M2C1VbUj/CxkIf1zg==} @@ -2070,8 +2325,14 @@ packages: ecc-jsbn@0.1.2: resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} - electron-to-chromium@1.5.3: - resolution: {integrity: sha512-QNdYSS5i8D9axWp/6XIezRObRHqaav/ur9z1VzCDUCH1XIFOr9WQk5xmgunhsTpjjgDy3oLxO/WMOVZlpUQrlA==} + electron-to-chromium@1.5.13: + resolution: {integrity: sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==} + + electron-to-chromium@1.5.57: + resolution: {integrity: sha512-xS65H/tqgOwUBa5UmOuNSLuslDo7zho0y/lgQw35pnrqiZh7UOWHCeL/Bt6noJATbA6tpQJGCifsFsIRZj1Fqg==} + + emoji-regex@10.4.0: + resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -2099,8 +2360,9 @@ packages: engines: {node: '>=8.0.0'} hasBin: true - error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} es-abstract@1.23.3: resolution: {integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==} @@ -2141,13 +2403,13 @@ packages: engines: {node: '>=12'} hasBin: true - esbuild@0.23.0: - resolution: {integrity: sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==} + esbuild@0.24.0: + resolution: {integrity: sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==} engines: {node: '>=18'} hasBin: true - escalade@3.1.2: - resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} escape-string-regexp@1.0.5: @@ -2158,12 +2420,8 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - escape-string-regexp@5.0.0: - resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} - engines: {node: '>=12'} - - eslint-config-next@14.2.5: - resolution: {integrity: sha512-zogs9zlOiZ7ka+wgUnmcM0KBEDjo4Jis7kxN1jvC0N4wynQ2MIx/KBkg4mVF63J5EK4W0QMCn7xO3vNisjaAoA==} + eslint-config-next@14.2.18: + resolution: {integrity: sha512-SuDRcpJY5VHBkhz5DijJ4iA4bVnBA0n48Rb+YSJSCDr+h7kKAcb1mZHusLbW+WA8LDB6edSolomXA55eG3eOVA==} peerDependencies: eslint: ^7.23.0 || ^8.0.0 typescript: '>=3.3.1' @@ -2177,23 +2435,29 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-config-turbo@2.0.9: - resolution: {integrity: sha512-FoIMElI8md/dR5DxjB5Om52KJfi7Qf7RInXeE+PGU6lN388rumppwyqEJsZ7vnR5GhGa9cLPt0vNZwEK9iXtKg==} + eslint-config-turbo@2.1.0: + resolution: {integrity: sha512-3SeE2OCWnkA/84adGJXABm++966LNGxRdXtXKBcplJdIe4PmERkov1z6Kzp2PrPKT13wGu/bwoLV5h1rm7v9ug==} peerDependencies: eslint: '>6.6.0' eslint-import-resolver-node@0.3.9: resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} - eslint-import-resolver-typescript@3.6.1: - resolution: {integrity: sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==} + eslint-import-resolver-typescript@3.6.3: + resolution: {integrity: sha512-ud9aw4szY9cCT1EWWdGv1L1XR6hh2PaRWif0j2QjQ0pgTY/69iw+W0Z4qZv5wHahOl8isEr+k/JnyAqNQkLkIA==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: eslint: '*' eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true - eslint-module-utils@2.8.1: - resolution: {integrity: sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==} + eslint-module-utils@2.8.2: + resolution: {integrity: sha512-3XnC5fDyc8M4J2E8pt8pmSVRX2M+5yWMCfI/kDZwauQeFgzQOuhcRBFKjTeJagqgk4sFKxe1mvNVnaWwImx/Tg==} engines: {node: '>=4'} peerDependencies: '@typescript-eslint/parser': '*' @@ -2241,22 +2505,35 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - eslint-plugin-turbo@2.0.9: - resolution: {integrity: sha512-q4s4mg6JcXzz5zK4LC3c6FcWehGAWjGj7kIM76ZvG0KiR9Ks0znzjnAKW0NoiDP4s/gt3r4YPOpI357qWt167Q==} + eslint-plugin-turbo@2.1.0: + resolution: {integrity: sha512-+CWVY29y7Qa+gvrKSzP+TOYrHAlNLCh/97K5VtDdnpH54h/JFmnd3U0aSG6WANe0HgAK8NHQfeWFDdRzfDqbKA==} peerDependencies: eslint: '>6.6.0' + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + eslint-scope@7.2.2: resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-visitor-keys@2.1.0: + resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} + engines: {node: '>=10'} + eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint@8.57.0: - resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} + eslint-visitor-keys@4.2.0: + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true espree@9.6.1: @@ -2276,6 +2553,10 @@ packages: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + estraverse@5.3.0: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} @@ -2293,6 +2574,9 @@ packages: eventemitter2@6.4.7: resolution: {integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==} + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + execa@4.1.0: resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} engines: {node: '>=10'} @@ -2301,10 +2585,6 @@ packages: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} - execa@6.1.0: - resolution: {integrity: sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - execa@8.0.1: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} @@ -2313,6 +2593,10 @@ packages: resolution: {integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==} engines: {node: '>=4'} + expect-type@1.1.0: + resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} + engines: {node: '>=12.0.0'} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -2351,6 +2635,17 @@ packages: fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + fdir@6.4.2: + resolution: {integrity: sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -2371,9 +2666,6 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} - find-yarn-workspace-root2@1.2.16: - resolution: {integrity: sha512-hr6hb1w8ePMpPVUK39S4RlwJzi+xPLuVuG8XlwXU3KD5Yn3qgBWVfy3AzNlDhWvE1EORCE65/Qm26rFQt3VLVA==} - flat-cache@3.2.0: resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} engines: {node: ^10.12.0 || >=12.0.0} @@ -2393,17 +2685,13 @@ packages: for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} - foreground-child@3.2.1: - resolution: {integrity: sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==} + foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} forever-agent@0.6.1: resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} - form-data@2.3.3: - resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} - engines: {node: '>= 0.12'} - form-data@4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} @@ -2433,6 +2721,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2461,8 +2754,9 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-func-name@2.0.2: - resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + get-east-asian-width@1.2.0: + resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==} + engines: {node: '>=18'} get-intrinsic@1.2.4: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} @@ -2484,8 +2778,8 @@ packages: resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} engines: {node: '>= 0.4'} - get-tsconfig@4.7.6: - resolution: {integrity: sha512-ZAqrLlu18NbDdRaHq+AKXzAmqIUPswPWKUchfytdAjiRFnCe5ojG2bstg6mRiZabkKfCoL/e98pbBELIV/YCeA==} + get-tsconfig@4.8.0: + resolution: {integrity: sha512-Pgba6TExTZ0FJAn1qkJAjIeKoDJ3CsI2ChuLohJnZl/tTU8MVrq3b+2t5UOPfRa4RMsorClBjJALkJUMjG1PAw==} getos@3.2.1: resolution: {integrity: sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==} @@ -2534,9 +2828,9 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} - globby@13.2.2: - resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + globby@14.0.2: + resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==} + engines: {node: '>=18'} globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} @@ -2550,14 +2844,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - grpc-tools@1.11.3: - resolution: {integrity: sha512-cRSK2uhDKHtZ9hLRM35HxaMAMxyh/L7C96Ojt58DhQBdwTOQlV1VIJHSK6X/pDeSQKhaQqWMFfebt8tIcvRfjQ==} + grpc-tools@1.12.4: + resolution: {integrity: sha512-5+mLAJJma3BjnW/KQp6JBjUMgvu7Mu3dBvBPd1dcbNIb+qiR0817zDpgPjS7gRb+l/8EVNIa3cB02xI9JLToKg==} hasBin: true - hard-rejection@2.1.0: - resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} - engines: {node: '>=6'} - has-bigints@1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} @@ -2591,10 +2881,6 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - hosted-git-info@4.1.0: - resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} - engines: {node: '>=10'} - html-encoding-sniffer@4.0.0: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} @@ -2603,8 +2889,8 @@ packages: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} - http-signature@1.3.6: - resolution: {integrity: sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==} + http-signature@1.4.0: + resolution: {integrity: sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==} engines: {node: '>=0.10'} https-proxy-agent@5.0.1: @@ -2626,10 +2912,6 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} - human-signals@3.0.1: - resolution: {integrity: sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==} - engines: {node: '>=12.20.0'} - human-signals@5.0.0: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} @@ -2648,12 +2930,12 @@ packages: ignore-by-default@1.0.1: resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} - ignore@5.3.1: - resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - immutable@4.3.7: - resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==} + immutable@5.0.2: + resolution: {integrity: sha512-1NU7hWZDkV7hJ4PJ9dur9gTNQ4ePNPN4k9/0YhwjzykTi/+3Q5pF93YU5QoVj8BuOnhLgaY8gs0U2pj4kSYVcw==} import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} @@ -2667,10 +2949,6 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} - indent-string@5.0.0: - resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} - engines: {node: '>=12'} - inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -2686,6 +2964,9 @@ packages: resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} engines: {node: '>= 0.4'} + intl-messageformat@10.7.7: + resolution: {integrity: sha512-F134jIoeYMro/3I0h08D0Yt4N9o9pjddU/4IIxMMURqbAtI2wu70X8hvG1V48W49zXHXv3RKSF/po+0fDfsGjA==} + is-arguments@1.1.1: resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} engines: {node: '>= 0.4'} @@ -2694,8 +2975,8 @@ packages: resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} engines: {node: '>= 0.4'} - is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} is-async-function@2.0.0: resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==} @@ -2712,16 +2993,15 @@ packages: resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} engines: {node: '>= 0.4'} + is-bun-module@1.1.0: + resolution: {integrity: sha512-4mTAVPlrXpaN3jtF0lsnPCMGnq4+qZjVIKq0HCpfcqf8OC1SM5oATCIAPM5V5FN05qp2NNnFndphmdZS9CV3hA==} + is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} - is-ci@3.0.1: - resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} - hasBin: true - - is-core-module@2.15.0: - resolution: {integrity: sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==} + is-core-module@2.15.1: + resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} engines: {node: '>= 0.4'} is-data-view@1.0.1: @@ -2747,6 +3027,10 @@ packages: resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} engines: {node: '>=12'} + is-fullwidth-code-point@5.0.0: + resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} + engines: {node: '>=18'} + is-generator-function@1.0.10: resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} engines: {node: '>= 0.4'} @@ -2787,10 +3071,6 @@ packages: resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} engines: {node: '>=12'} - is-plain-obj@1.1.0: - resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} - engines: {node: '>=0.10.0'} - is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -2871,11 +3151,15 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jiti@1.21.6: + resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} + hasBin: true + joi@17.13.3: resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} - jose@5.6.3: - resolution: {integrity: sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g==} + jose@5.8.0: + resolution: {integrity: sha512-E7CqYpL/t7MMnfGnK/eg416OsFCVUrU/Y3Vwe7QjKhu/BkS1Ms455+2xsqZQVN57/U2MHMBvEb5SrmAZWAIntA==} joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} @@ -2884,9 +3168,6 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-tokens@9.0.0: - resolution: {integrity: sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==} - js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true @@ -2898,8 +3179,8 @@ packages: jsbn@0.1.1: resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} - jsdom@24.1.1: - resolution: {integrity: sha512-5O1wWV99Jhq4DV7rCLIoZ/UIhyQeDR7wHVyZAHAshbrvZsLs+Xzz7gtwnlJTJDjleiTKh54F4dXrX70vJQTyJQ==} + jsdom@25.0.1: + resolution: {integrity: sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==} engines: {node: '>=18'} peerDependencies: canvas: ^2.11.2 @@ -2907,17 +3188,14 @@ packages: canvas: optional: true - jsesc@2.5.2: - resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} - engines: {node: '>=4'} + jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} hasBin: true json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -2956,10 +3234,6 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - kind-of@6.0.3: - resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} - engines: {node: '>=0.10.0'} - language-subtag-registry@0.3.23: resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} @@ -2975,10 +3249,6 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - lilconfig@2.0.5: - resolution: {integrity: sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==} - engines: {node: '>=10'} - lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} @@ -2990,9 +3260,9 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - lint-staged@13.0.3: - resolution: {integrity: sha512-9hmrwSCFroTSYLjflGI8Uk+GWAwMB4OlpU4bMJEAT5d/llQwtYKoim4bLOyLCuWFAhWEupE0vkIFqtw/WIsPug==} - engines: {node: ^14.13.1 || >=16.0.0} + lint-staged@15.2.10: + resolution: {integrity: sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg==} + engines: {node: '>=18.12.0'} hasBin: true listr2@3.14.0: @@ -3004,27 +3274,14 @@ packages: enquirer: optional: true - listr2@4.0.5: - resolution: {integrity: sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==} - engines: {node: '>=12'} - peerDependencies: - enquirer: '>= 2.3.0 < 3' - peerDependenciesMeta: - enquirer: - optional: true + listr2@8.2.4: + resolution: {integrity: sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==} + engines: {node: '>=18.0.0'} load-tsconfig@0.2.5: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - load-yaml-file@0.2.0: - resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==} - engines: {node: '>=6'} - - local-pkg@0.5.0: - resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} - engines: {node: '>=14'} - locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -3059,6 +3316,10 @@ packages: resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} engines: {node: '>=10'} + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + long@5.2.3: resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} @@ -3066,8 +3327,8 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - loupe@2.3.7: - resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + loupe@3.1.2: + resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -3078,40 +3339,32 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true - magic-string@0.30.11: - resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} + magic-string@0.30.12: + resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==} - make-dir-cli@3.0.0: - resolution: {integrity: sha512-8yCjIAOQ8tezWRJWUG3tbvN2I19hiVr8K5DPDVl8fECS3qz0ZbeL194ZGRdf8K3LgvbjDCTadge6NrN/I4XrNw==} - engines: {node: '>=12.17'} + make-dir-cli@4.0.0: + resolution: {integrity: sha512-9BBC2CaGH0hUAx+tQthgxqYypwkTs+7oXmPdiWyDpHGo4mGB3kdudUKQGivK59C1aJroo4QLlXF7Chu/kdhYiw==} + engines: {node: '>=18'} hasBin: true make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} - map-obj@1.0.1: - resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} - engines: {node: '>=0.10.0'} - - map-obj@4.3.0: - resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} - engines: {node: '>=8'} + make-dir@5.0.0: + resolution: {integrity: sha512-G0yBotnlWVonPClw+tq+xi4K7DZC9n96HjGTBDdHkstAVsDkfZhi1sTvZypXLpyQTbISBkDtK0E5XlUqDsShQg==} + engines: {node: '>=18'} map-stream@0.1.0: resolution: {integrity: sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==} - meow@10.1.5: - resolution: {integrity: sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + meow@13.2.0: + resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} + engines: {node: '>=18'} merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -3120,8 +3373,8 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - micromatch@4.0.7: - resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} mime-db@1.52.0: @@ -3140,6 +3393,10 @@ packages: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -3155,10 +3412,6 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} - minimist-options@4.1.0: - resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} - engines: {node: '>= 6'} - minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -3183,9 +3436,6 @@ packages: engines: {node: '>=10'} hasBin: true - mlly@1.7.1: - resolution: {integrity: sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==} - moment@2.30.1: resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} @@ -3210,6 +3460,16 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + next-intl@3.25.1: + resolution: {integrity: sha512-Z2dJWn5f/b1sb8EmuJcuDhbQTIp4RG1KBFAILgRt/y27W0ifU7Ll/os3liphUY4InyRH89uShTAk7ItAlpr0uA==} + peerDependencies: + next: ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 + next-themes@0.2.1: resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==} peerDependencies: @@ -3217,38 +3477,23 @@ packages: react: '*' react-dom: '*' - next@14.2.3: - resolution: {integrity: sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==} - engines: {node: '>=18.17.0'} + next@15.0.4-canary.23: + resolution: {integrity: sha512-xCjjBx4csWdG4MP9tKV/C25OIDbN0o+zovMC5zd4yvE4lrd43Y5tt+w171IGUueb6VbPLTSlDaXvqOtrxKJXzQ==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 '@playwright/test': ^1.41.2 - react: ^18.2.0 - react-dom: ^18.2.0 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-380f5d67-20241113 + react-dom: ^18.2.0 || 19.0.0-rc-380f5d67-20241113 sass: ^1.3.0 peerDependenciesMeta: '@opentelemetry/api': optional: true '@playwright/test': optional: true - sass: - optional: true - - next@14.2.5: - resolution: {integrity: sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==} - engines: {node: '>=18.17.0'} - hasBin: true - peerDependencies: - '@opentelemetry/api': ^1.1.0 - '@playwright/test': ^1.41.2 - react: ^18.2.0 - react-dom: ^18.2.0 - sass: ^1.3.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - '@playwright/test': + babel-plugin-react-compiler: optional: true sass: optional: true @@ -3259,6 +3504,9 @@ packages: nice-grpc@2.0.1: resolution: {integrity: sha512-Q5CGXO08STsv+HAkXeFgRayANT62X1LnIDhNXdCf+LP0XaP7EiHM0Cr3QefnoFjDZAx/Kxq+qiQfY66BrtKcNQ==} + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -3271,9 +3519,9 @@ packages: node-releases@2.0.18: resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} - nodemon@2.0.22: - resolution: {integrity: sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==} - engines: {node: '>=8.10.0'} + nodemon@3.1.7: + resolution: {integrity: sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==} + engines: {node: '>=10'} hasBin: true nopt@5.0.0: @@ -3281,10 +3529,6 @@ packages: engines: {node: '>=6'} hasBin: true - normalize-package-data@3.0.3: - resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} - engines: {node: '>=10'} - normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -3359,6 +3603,10 @@ packages: resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} engines: {node: '>=12'} + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -3385,10 +3633,6 @@ packages: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} - p-limit@5.0.0: - resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} - engines: {node: '>=18'} - p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -3405,25 +3649,24 @@ packages: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} engines: {node: '>=10'} - p-map@5.5.0: - resolution: {integrity: sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg==} - engines: {node: '>=12'} + p-map@7.0.2: + resolution: {integrity: sha512-z4cYYMMdKHzw4O5UkWJImbZynVIo0lSGTXc7bzB1e/rrDqkgGUNysK/o4bTr+0+xKvvLoTyGqYC4Fgljy9qe1Q==} + engines: {node: '>=18'} p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} - package-json-from-dist@1.0.0: - resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + package-manager-detector@0.2.2: + resolution: {integrity: sha512-VgXbyrSNsml4eHWIvxxG/nTL4wgybMTXCV2Un/+yEc3aDKKU6nQBZjbeP3Pl3qm9Qg92X/1ng4ffvCeD/zwHgg==} parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} - parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} - parse5@7.1.2: resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} @@ -3454,11 +3697,16 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + path-type@5.0.0: + resolution: {integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==} + engines: {node: '>=12'} + pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} - pathval@1.1.1: - resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + pathval@2.0.0: + resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + engines: {node: '>= 14.16'} pause-stream@0.0.11: resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} @@ -3472,10 +3720,17 @@ packages: picocolors@1.0.1: resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + pidtree@0.6.0: resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} engines: {node: '>=0.10'} @@ -3493,20 +3748,23 @@ packages: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} - pkg-dir@4.2.0: - resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} - engines: {node: '>=8'} + playwright-core@1.48.2: + resolution: {integrity: sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==} + engines: {node: '>=18'} + hasBin: true - pkg-types@1.1.3: - resolution: {integrity: sha512-+JrgthZG6m3ckicaOB74TwQ+tBWsFl3qVQg7mN8ulwSOElJ7gBhKzj2VkCPnZ4NlF6kEquYU+RIYNVAvzd54UA==} + playwright@1.48.2: + resolution: {integrity: sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==} + engines: {node: '>=18'} + hasBin: true possible-typed-array-names@1.0.0: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} - postcss-import@14.1.0: - resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==} - engines: {node: '>=10.0.0'} + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} peerDependencies: postcss: ^8.0.0 @@ -3516,9 +3774,9 @@ packages: peerDependencies: postcss: ^8.4.21 - postcss-load-config@3.1.4: - resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} - engines: {node: '>= 10'} + postcss-load-config@4.0.2: + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} peerDependencies: postcss: '>=8.0.9' ts-node: '>=9.0.0' @@ -3546,57 +3804,95 @@ packages: yaml: optional: true - postcss-nested@6.0.0: - resolution: {integrity: sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==} + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} engines: {node: '>=12.0'} peerDependencies: postcss: ^8.2.14 - postcss-selector-parser@6.1.1: - resolution: {integrity: sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==} + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.4.21: - resolution: {integrity: sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.4.31: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.4.40: - resolution: {integrity: sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==} + postcss@8.4.49: + resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} engines: {node: ^10 || ^12 || >=14} - preferred-pm@3.1.4: - resolution: {integrity: sha512-lEHd+yEm22jXdCphDrkvIJQU66EuLojPPtvZkpKIkiD+l0DMThF/niqZKJSoU8Vl7iuvtmzyMhir9LdVy5WMnA==} - engines: {node: '>=10'} - prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-plugin-organize-imports@4.0.0: - resolution: {integrity: sha512-vnKSdgv9aOlqKeEFGhf9SCBsTyzDSyScy1k7E0R1Uo4L0cTcOV7c1XQaT7jfXIOc/p08WLBfN2QUQA9zDSZMxA==} + prettier-plugin-organize-imports@4.1.0: + resolution: {integrity: sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==} peerDependencies: - '@vue/language-plugin-pug': ^2.0.24 prettier: '>=2.0' typescript: '>=2.9' - vue-tsc: ^2.0.24 + vue-tsc: ^2.1.0 peerDependenciesMeta: - '@vue/language-plugin-pug': - optional: true vue-tsc: optional: true - prettier-plugin-tailwindcss@0.1.13: - resolution: {integrity: sha512-/EKQURUrxLu66CMUg4+1LwGdxnz8of7IDvrSLqEtDqhLH61SAlNNUSr90UTvZaemujgl3OH/VHg+fyGltrNixw==} - engines: {node: '>=12.17.0'} + prettier-plugin-tailwindcss@0.6.8: + resolution: {integrity: sha512-dGu3kdm7SXPkiW4nzeWKCl3uoImdd5CTZEJGxyypEPL37Wj0HT2pLqjrvSei1nTeuQfO4PUfjeW5cTUNRLZ4sA==} + engines: {node: '>=14.21.3'} peerDependencies: - prettier: '>=2.2.0' + '@ianvs/prettier-plugin-sort-imports': '*' + '@prettier/plugin-pug': '*' + '@shopify/prettier-plugin-liquid': '*' + '@trivago/prettier-plugin-sort-imports': '*' + '@zackad/prettier-plugin-twig-melody': '*' + prettier: ^3.0 + prettier-plugin-astro: '*' + prettier-plugin-css-order: '*' + prettier-plugin-import-sort: '*' + prettier-plugin-jsdoc: '*' + prettier-plugin-marko: '*' + prettier-plugin-multiline-arrays: '*' + prettier-plugin-organize-attributes: '*' + prettier-plugin-organize-imports: '*' + prettier-plugin-sort-imports: '*' + prettier-plugin-style-order: '*' + prettier-plugin-svelte: '*' + peerDependenciesMeta: + '@ianvs/prettier-plugin-sort-imports': + optional: true + '@prettier/plugin-pug': + optional: true + '@shopify/prettier-plugin-liquid': + optional: true + '@trivago/prettier-plugin-sort-imports': + optional: true + '@zackad/prettier-plugin-twig-melody': + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-css-order: + optional: true + prettier-plugin-import-sort: + optional: true + prettier-plugin-jsdoc: + optional: true + prettier-plugin-marko: + optional: true + prettier-plugin-multiline-arrays: + optional: true + prettier-plugin-organize-attributes: + optional: true + prettier-plugin-organize-imports: + optional: true + prettier-plugin-sort-imports: + optional: true + prettier-plugin-style-order: + optional: true + prettier-plugin-svelte: + optional: true prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} @@ -3616,10 +3912,6 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - pretty-format@29.7.0: - resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} @@ -3627,8 +3919,8 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - protobufjs@7.3.2: - resolution: {integrity: sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==} + protobufjs@7.4.0: + resolution: {integrity: sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==} engines: {node: '>=12.0.0'} proxy-from-env@1.0.0: @@ -3645,9 +3937,6 @@ packages: pseudomap@1.0.2: resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} - psl@1.9.0: - resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} - pstree.remy@1.1.8: resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} @@ -3663,24 +3952,17 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 - qs@6.10.4: - resolution: {integrity: sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==} + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} - querystringify@2.2.0: - resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} - queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - quick-lru@5.1.1: - resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} - engines: {node: '>=10'} - - react-dom@18.3.1: - resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + react-dom@19.0.0-rc-66855b96-20241106: + resolution: {integrity: sha512-D25vdaytZ1wFIRiwNU98NPQ/upS2P8Co4/oNoa02PzHbh8deWdepjm5qwZM/46OdSiGv4WSWwxP55RO9obqJEQ==} peerDependencies: - react: ^18.3.1 + react: 19.0.0-rc-66855b96-20241106 react-hook-form@7.39.5: resolution: {integrity: sha512-OE0HKyz5IPc6svN2wd+e+evidZrw4O4WZWAWYzQVZuHi+hYnHFSLnxOq0ddjbdmaLIsLHut/ab7j72y2QT3+KA==} @@ -3694,32 +3976,17 @@ packages: react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} - react-is@18.3.1: - resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - react-refresh@0.14.2: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} - react@18.2.0: - resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} - engines: {node: '>=0.10.0'} - - react@18.3.1: - resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + react@19.0.0-rc-66855b96-20241106: + resolution: {integrity: sha512-klH7xkT71SxRCx4hb1hly5FJB21Hz0ACyxbXYAECEqssUjtJeFUAaI2U1DgJAzkGEnvEm3DkxuBchMC/9K4ipg==} engines: {node: '>=0.10.0'} read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} - read-pkg-up@8.0.0: - resolution: {integrity: sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==} - engines: {node: '>=12'} - - read-pkg@6.0.0: - resolution: {integrity: sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==} - engines: {node: '>=12'} - read-yaml-file@1.1.0: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} @@ -3732,14 +3999,14 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + readdirp@4.0.2: + resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==} + engines: {node: '>= 14.16.0'} + redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} - redent@4.0.0: - resolution: {integrity: sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==} - engines: {node: '>=12'} - reflect.getprototypeof@1.0.6: resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} engines: {node: '>= 0.4'} @@ -3758,9 +4025,6 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} - requires-port@1.0.0: - resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -3784,6 +4048,10 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -3796,14 +4064,11 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rollup@4.19.1: - resolution: {integrity: sha512-K5vziVlg7hTpYfFBI+91zHBEMo6jafYXpkMlqZjg7/zhIG9iHqazBf4xz9AVdjS9BruRn280ROqLI7G3OFRIlw==} + rollup@4.25.0: + resolution: {integrity: sha512-uVbClXmR6wvx5R1M3Od4utyLUxrmOcEm3pAtMphn73Apq19PDtHpgZoEvqH2YnnaNUuvKmg2DgRd2Sqv+odyqg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - rrweb-cssom@0.6.0: - resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==} - rrweb-cssom@0.7.1: resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} @@ -3827,8 +4092,8 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sass@1.77.8: - resolution: {integrity: sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==} + sass@1.80.7: + resolution: {integrity: sha512-MVWvN0u5meytrSjsU7AWsbhoXi1sc58zADXFllfZzbsBT1GHjjar6JwBINYPRrkx/zqnQ6uqbQuHgE95O+C+eQ==} engines: {node: '>=14.0.0'} hasBin: true @@ -3836,21 +4101,13 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} - scheduler@0.23.2: - resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} - - semver@5.7.2: - resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} - hasBin: true + scheduler@0.25.0-rc-66855b96-20241106: + resolution: {integrity: sha512-HQXp/Mnp/MMRSXMQF7urNFla+gmtXW/Gr1KliuR0iboTit4KvZRY8KYaq5ccCTAOJiUqQh2rE2F3wgUekmgdlA==} semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.0.0: - resolution: {integrity: sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==} - hasBin: true - semver@7.6.3: resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} engines: {node: '>=10'} @@ -3870,6 +4127,10 @@ packages: resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} engines: {node: '>= 0.4'} + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@1.2.0: resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} engines: {node: '>=0.10.0'} @@ -3903,17 +4164,20 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - simple-update-notifier@1.1.0: - resolution: {integrity: sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==} - engines: {node: '>=8.10.0'} + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - slash@4.0.0: - resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} - engines: {node: '>=12'} + slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} slice-ansi@3.0.0: resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} @@ -3927,32 +4191,21 @@ packages: resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} engines: {node: '>=12'} - source-map-js@1.2.0: - resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} + slice-ansi@7.1.0: + resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} + engines: {node: '>=18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} - spawn-command@0.0.2: - resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==} - spawndamnit@2.0.0: resolution: {integrity: sha512-j4JKEcncSjFlqIwU5L/rp2N5SIPsdxaRsIv678+TZxZ0SRDJTm8JrxJMjE/XuiEZNEir3S8l0Fa3Ke339WI4qA==} - spdx-correct@3.2.0: - resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} - - spdx-exceptions@2.5.0: - resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} - - spdx-expression-parse@3.0.1: - resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} - - spdx-license-ids@3.0.18: - resolution: {integrity: sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==} - split@0.3.3: resolution: {integrity: sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==} @@ -3967,13 +4220,13 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - start-server-and-test@2.0.5: - resolution: {integrity: sha512-2CV4pz69NJVJKQmJeSr+O+SPtOreu0yxvhPmSXclzmAKkPREuMabyMh+Txpzemjx0RDzXOcG2XkhiUuxjztSQw==} + start-server-and-test@2.0.8: + resolution: {integrity: sha512-v2fV6NV2F7tL1ocwfI4Wpait+IKjRbT5l3ZZ+ZikXdMLmxYsS8ynGAsCQAUVXkVyGyS+UibsRnvgHkMvJIvCsw==} engines: {node: '>=16'} hasBin: true - std-env@3.7.0: - resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + std-env@3.8.0: + resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} stop-iteration-iterator@1.0.0: resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} @@ -3998,6 +4251,10 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + string.prototype.includes@2.0.0: resolution: {integrity: sha512-E34CkBgyeqNDcrbU76cDjL5JLcVrtSdYq0MEh/B10r17pRP4ciHLwTgnuLV8Ay6cgEMLkcBkFCKyFZ43YldYzg==} @@ -4046,24 +4303,17 @@ packages: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} - strip-indent@4.0.0: - resolution: {integrity: sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==} - engines: {node: '>=12'} - strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - strip-literal@2.1.0: - resolution: {integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==} - - styled-jsx@5.1.1: - resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} peerDependencies: '@babel/core': '*' babel-plugin-macros: '*' - react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' peerDependenciesMeta: '@babel/core': optional: true @@ -4099,12 +4349,13 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - tailwindcss@3.2.4: - resolution: {integrity: sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==} - engines: {node: '>=12.13.0'} + tabbable@6.2.0: + resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} + + tailwindcss@3.4.14: + resolution: {integrity: sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==} + engines: {node: '>=14.0.0'} hasBin: true - peerDependencies: - postcss: ^8.0.9 tapable@2.2.1: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} @@ -4128,26 +4379,48 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + thirty-two@1.0.2: + resolution: {integrity: sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==} + engines: {node: '>=0.2.6'} + throttleit@1.0.1: resolution: {integrity: sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==} through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - tinybench@2.8.0: - resolution: {integrity: sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} tinycolor2@1.4.2: resolution: {integrity: sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==} - tinypool@0.8.4: - resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} + tinyexec@0.3.1: + resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} + + tinyglobby@0.2.10: + resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==} + engines: {node: '>=12.0.0'} + + tinypool@1.0.1: + resolution: {integrity: sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} engines: {node: '>=14.0.0'} - tinyspy@2.2.1: - resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} + tldts-core@6.1.60: + resolution: {integrity: sha512-XHjoxak8SFQnHnmYHb3PcnW5TZ+9ErLZemZei3azuIRhQLw4IExsVbL3VZJdHcLeNaXq6NqawgpDPpjBOg4B5g==} + + tldts@6.1.60: + resolution: {integrity: sha512-TYVHm7G9NCnhgqOsFalbX6MG1Po5F4efF+tLfoeiOGQq48Oqgwcgz8upY2R1BHWa4aDrj28RYx0dkYJ63qCFMg==} + hasBin: true + tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -4156,10 +4429,6 @@ packages: resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} engines: {node: '>=14.14'} - to-fast-properties@2.0.0: - resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} - engines: {node: '>=4'} - to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -4171,9 +4440,9 @@ packages: resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} hasBin: true - tough-cookie@4.1.4: - resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} - engines: {node: '>=6'} + tough-cookie@5.0.0: + resolution: {integrity: sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==} + engines: {node: '>=16'} tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -4189,16 +4458,18 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - trim-newlines@4.1.1: - resolution: {integrity: sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==} - engines: {node: '>=12'} - ts-api-utils@1.3.0: resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} engines: {node: '>=16'} peerDependencies: typescript: '>=4.2.0' + ts-api-utils@1.4.1: + resolution: {integrity: sha512-5RU2/lxTA3YUZxju61HO2U6EoZLvBLtmV2mbTvqyu4a/7s7RmJPT+1YekhMVsQhznRWk/czIwDUg+V8Q9ZuG4w==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + ts-error@1.0.6: resolution: {integrity: sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA==} @@ -4208,15 +4479,15 @@ packages: ts-poet@6.9.0: resolution: {integrity: sha512-roe6W6MeZmCjRmppyfOURklO5tQFQ6Sg7swURKkwYJvV7dbGCrK28um5+51iW3twdPRKtwarqFAVMU6G1mvnuQ==} - ts-proto-descriptors@1.16.0: - resolution: {integrity: sha512-3yKuzMLpltdpcyQji1PJZRfoo4OJjNieKTYkQY8pF7xGKsYz/RHe3aEe4KiRxcinoBmnEhmuI+yJTxLb922ULA==} + ts-proto-descriptors@2.0.0: + resolution: {integrity: sha512-wHcTH3xIv11jxgkX5OyCSFfw27agpInAd6yh89hKG6zqIXnjW9SYqSER2CVQxdPj4czeOhGagNvZBEbJPy7qkw==} - ts-proto@1.181.1: - resolution: {integrity: sha512-lNmd/KEgqWtwDG9mIM3EpcxBx+URRVHkDP/EEJBgQJaQwmZFTk6VjHg56HNQswd114yXGfF+8pKQvJ2iH9KfWw==} + ts-proto@2.2.7: + resolution: {integrity: sha512-O3LcgGzx/lVfjp6mPm15mYZH6tHIX6PXReXKEQqs1B2yWI8oquUxzpgxA98sa1XrdOjm/n+BYyei3NruXZyiFg==} hasBin: true - tsconfck@3.1.1: - resolution: {integrity: sha512-00eoI6WY57SvZEVjm13stEVE90VkEdJAFGgpFLTsZbJyW/LwFQ7uQxJHWpZ2hzSWgCPKc9AnBnNP+0X7o3hAmQ==} + tsconfck@3.1.4: + resolution: {integrity: sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==} engines: {node: ^18 || >=20} hasBin: true peerDependencies: @@ -4228,11 +4499,14 @@ packages: tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} - tslib@2.6.3: - resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} + tslib@2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} - tsup@8.2.3: - resolution: {integrity: sha512-6YNT44oUfXRbZuSMNmN36GzwPPIlD2wBccY7looM2fkTcxkf2NEmwr3OZuDZoySklnrIG4hoEtzy8yUXYOqNcg==} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsup@8.3.5: + resolution: {integrity: sha512-Tunf6r6m6tnZsG9GYWndg0z8dEV7fD733VBFzFJ5Vcm1FtlXB8xBD/rtrBi2a3YKEV7hHtxiZtW5EAVADoe1pA==} engines: {node: '>=18'} hasBin: true peerDependencies: @@ -4253,38 +4527,38 @@ packages: tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} - turbo-darwin-64@2.0.9: - resolution: {integrity: sha512-owlGsOaExuVGBUfrnJwjkL1BWlvefjSKczEAcpLx4BI7Oh6ttakOi+JyomkPkFlYElRpjbvlR2gP8WIn6M/+xQ==} + turbo-darwin-64@2.2.3: + resolution: {integrity: sha512-Rcm10CuMKQGcdIBS3R/9PMeuYnv6beYIHqfZFeKWVYEWH69sauj4INs83zKMTUiZJ3/hWGZ4jet9AOwhsssLyg==} cpu: [x64] os: [darwin] - turbo-darwin-arm64@2.0.9: - resolution: {integrity: sha512-XAXkKkePth5ZPPE/9G9tTnPQx0C8UTkGWmNGYkpmGgRr8NedW+HrPsi9N0HcjzzIH9A4TpNYvtiV+WcwdaEjKA==} + turbo-darwin-arm64@2.2.3: + resolution: {integrity: sha512-+EIMHkuLFqUdJYsA3roj66t9+9IciCajgj+DVek+QezEdOJKcRxlvDOS2BUaeN8kEzVSsNiAGnoysFWYw4K0HA==} cpu: [arm64] os: [darwin] - turbo-linux-64@2.0.9: - resolution: {integrity: sha512-l9wSgEjrCFM1aG16zItBsZ206ZlhSSx1owB8Cgskfv0XyIXRGHRkluihiaxkp+UeU5WoEfz4EN5toc+ICA0q0w==} + turbo-linux-64@2.2.3: + resolution: {integrity: sha512-UBhJCYnqtaeOBQLmLo8BAisWbc9v9daL9G8upLR+XGj6vuN/Nz6qUAhverN4Pyej1g4Nt1BhROnj6GLOPYyqxQ==} cpu: [x64] os: [linux] - turbo-linux-arm64@2.0.9: - resolution: {integrity: sha512-gRnjxXRne18B27SwxXMqL3fJu7jw/8kBrOBTBNRSmZZiG1Uu3nbnP7b4lgrA/bCku6C0Wligwqurvtpq6+nFHA==} + turbo-linux-arm64@2.2.3: + resolution: {integrity: sha512-hJYT9dN06XCQ3jBka/EWvvAETnHRs3xuO/rb5bESmDfG+d9yQjeTMlhRXKrr4eyIMt6cLDt1LBfyi+6CQ+VAwQ==} cpu: [arm64] os: [linux] - turbo-windows-64@2.0.9: - resolution: {integrity: sha512-ZVo0apxUvaRq4Vm1qhsfqKKhtRgReYlBVf9MQvVU1O9AoyydEQvLDO1ryqpXDZWpcHoFxHAQc9msjAMtE5K2lA==} + turbo-windows-64@2.2.3: + resolution: {integrity: sha512-NPrjacrZypMBF31b4HE4ROg4P3nhMBPHKS5WTpMwf7wydZ8uvdEHpESVNMOtqhlp857zbnKYgP+yJF30H3N2dQ==} cpu: [x64] os: [win32] - turbo-windows-arm64@2.0.9: - resolution: {integrity: sha512-sGRz7c5Pey6y7y9OKi8ypbWNuIRPF9y8xcMqL56OZifSUSo+X2EOsOleR9MKxQXVaqHPGOUKWsE6y8hxBi9pag==} + turbo-windows-arm64@2.2.3: + resolution: {integrity: sha512-fnNrYBCqn6zgKPKLHu4sOkihBI/+0oYFr075duRxqUZ+1aLWTAGfHZLgjVeLh3zR37CVzuerGIPWAEkNhkWEIw==} cpu: [arm64] os: [win32] - turbo@2.0.9: - resolution: {integrity: sha512-QaLaUL1CqblSKKPgLrFW3lZWkWG4pGBQNW+q1ScJB5v1D/nFWtsrD/yZljW/bdawg90ihi4/ftQJ3h6fz1FamA==} + turbo@2.2.3: + resolution: {integrity: sha512-5lDvSqIxCYJ/BAd6rQGK/AzFRhBkbu4JHVMLmGh/hCb7U3CqSnr5Tjwfy9vc+/5wG2DJ6wttgAaA7MoCgvBKZQ==} hasBin: true tweetnacl@0.14.5: @@ -4294,10 +4568,6 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - type-detect@4.1.0: - resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} - engines: {node: '>=4'} - type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} @@ -4306,10 +4576,6 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} - type-fest@1.4.0: - resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} - engines: {node: '>=10'} - typed-array-buffer@1.0.2: resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} engines: {node: '>= 0.4'} @@ -4326,35 +4592,34 @@ packages: resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} engines: {node: '>= 0.4'} - typescript@5.5.4: - resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} + types-react-dom@19.0.0-rc.1: + resolution: {integrity: sha512-VSLZJl8VXCD0fAWp7DUTFUDCcZ8DVXOQmjhJMD03odgeFmu14ZQJHCXeETm3BEAhJqfgJaFkLnGkQv88sRx0fQ==} + + types-react@19.0.0-rc.1: + resolution: {integrity: sha512-RshndUfqTW6K3STLPis8BtAYCGOkMbtvYsi90gmVNDZBXUyUc5juf2PE9LfS/JmOlUIRO8cWTS/1MTnmhjDqyQ==} + + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} engines: {node: '>=14.17'} hasBin: true - ufo@1.5.4: - resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} - unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} undefsafe@2.0.5: resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} - undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} - undici@5.28.4: - resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} - engines: {node: '>=14.0'} + unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} - universalify@0.2.0: - resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} - engines: {node: '>= 4.0.0'} - universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -4369,11 +4634,19 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + update-browserslist-db@1.1.1: + resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - url-parse@1.5.10: - resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + use-intl@3.25.1: + resolution: {integrity: sha512-Xeyl0+BjlBf6fJr2h5W/CESZ2IQAH7jzXYK4c/ao+qR26jNPW3FXBLjg7eLRxdeI6QaLcYGLtH3WYhC9I0+6Yg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 use-sync-external-store@1.2.2: resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} @@ -4387,28 +4660,25 @@ packages: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true - validate-npm-package-license@3.0.4: - resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} - verror@1.10.0: resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} engines: {'0': node >=0.6.0} - vite-node@1.6.0: - resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==} + vite-node@2.1.4: + resolution: {integrity: sha512-kqa9v+oi4HwkG6g8ufRnb5AeplcRw8jUF6/7/Qz1qRQOXHImG8YnLbB+LLszENwFnoBl9xIf9nVdCFzNd7GQEg==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true - vite-tsconfig-paths@4.3.2: - resolution: {integrity: sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==} + vite-tsconfig-paths@5.1.2: + resolution: {integrity: sha512-gEIbKfJzSEv0yR3XS2QEocKetONoWkbROj6hGx0FHM18qKUojhvcokQsxQx5nMkelZq2n37zbSGCJn+FSODSjA==} peerDependencies: vite: '*' peerDependenciesMeta: vite: optional: true - vite@5.3.5: - resolution: {integrity: sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==} + vite@5.4.11: + resolution: {integrity: sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -4416,6 +4686,7 @@ packages: less: '*' lightningcss: ^1.21.0 sass: '*' + sass-embedded: '*' stylus: '*' sugarss: '*' terser: ^5.4.0 @@ -4428,6 +4699,8 @@ packages: optional: true sass: optional: true + sass-embedded: + optional: true stylus: optional: true sugarss: @@ -4435,15 +4708,15 @@ packages: terser: optional: true - vitest@1.6.0: - resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==} + vitest@2.1.4: + resolution: {integrity: sha512-eDjxbVAJw1UJJCHr5xr/xM86Zx+YxIEXGAR+bmnEID7z9qWfoxpHw0zdobz+TQAFOLT+nEXz3+gx6nUJ7RgmlQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 1.6.0 - '@vitest/ui': 1.6.0 + '@vitest/browser': 2.1.4 + '@vitest/ui': 2.1.4 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -4464,8 +4737,8 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} - wait-on@7.2.0: - resolution: {integrity: sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==} + wait-on@8.0.1: + resolution: {integrity: sha512-1wWQOyR2LVVtaqrcIL2+OM+x7bkpmzVROa0Nf6FryXkS+er5Sa1kzFGjzZRqLnHa3n1rACFLeTwUqE1ETL9Mig==} engines: {node: '>=12.0.0'} hasBin: true @@ -4508,10 +4781,6 @@ packages: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} - which-pm@2.2.0: - resolution: {integrity: sha512-MOiaDbA5ZZgUjkeMWM5EkJp4loW5ZRoa5bc3/aeMox/PJelMhE6t7S/mLuiY43DBupyxH+S0U1bTui9kWUlmsw==} - engines: {node: '>=8.15'} - which-typed-array@1.1.15: resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} engines: {node: '>= 0.4'} @@ -4549,6 +4818,10 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + wrap-ansi@9.0.0: + resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + engines: {node: '>=18'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -4571,10 +4844,6 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} - xtend@4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} - y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -4588,19 +4857,11 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yaml@1.10.2: - resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} - engines: {node: '>= 6'} - yaml@2.5.0: resolution: {integrity: sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==} engines: {node: '>= 14'} hasBin: true - yargs-parser@20.2.9: - resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} - engines: {node: '>=10'} - yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -4616,183 +4877,217 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - yocto-queue@1.1.1: - resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==} - engines: {node: '>=12.20'} - snapshots: '@adobe/css-tools@4.4.0': {} + '@alloc/quick-lru@5.2.0': {} + '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 - '@babel/code-frame@7.24.7': + '@babel/code-frame@7.26.2': dependencies: - '@babel/highlight': 7.24.7 - picocolors: 1.0.1 + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 - '@babel/compat-data@7.25.2': {} + '@babel/compat-data@7.26.2': {} - '@babel/core@7.25.2': + '@babel/core@7.26.0': dependencies: '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.24.7 - '@babel/generator': 7.25.0 - '@babel/helper-compilation-targets': 7.25.2 - '@babel/helper-module-transforms': 7.25.2(@babel/core@7.25.2) - '@babel/helpers': 7.25.0 - '@babel/parser': 7.25.0 - '@babel/template': 7.25.0 - '@babel/traverse': 7.25.2 - '@babel/types': 7.25.2 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helpers': 7.26.0 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 convert-source-map: 2.0.0 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/generator@7.25.0': + '@babel/eslint-parser@7.25.9(@babel/core@7.26.0)(eslint@8.57.1)': dependencies: - '@babel/types': 7.25.2 + '@babel/core': 7.26.0 + '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 + eslint: 8.57.1 + eslint-visitor-keys: 2.1.0 + semver: 6.3.1 + + '@babel/generator@7.26.2': + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 - jsesc: 2.5.2 + jsesc: 3.0.2 - '@babel/helper-compilation-targets@7.25.2': + '@babel/helper-compilation-targets@7.25.9': dependencies: - '@babel/compat-data': 7.25.2 - '@babel/helper-validator-option': 7.24.8 - browserslist: 4.23.2 + '@babel/compat-data': 7.26.2 + '@babel/helper-validator-option': 7.25.9 + browserslist: 4.24.2 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-module-imports@7.24.7': + '@babel/helper-module-imports@7.25.9': dependencies: - '@babel/traverse': 7.25.2 - '@babel/types': 7.25.2 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.25.2(@babel/core@7.25.2)': + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.2 - '@babel/helper-module-imports': 7.24.7 - '@babel/helper-simple-access': 7.24.7 - '@babel/helper-validator-identifier': 7.24.7 - '@babel/traverse': 7.25.2 + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/helper-plugin-utils@7.24.8': {} + '@babel/helper-plugin-utils@7.25.9': {} - '@babel/helper-simple-access@7.24.7': + '@babel/helper-string-parser@7.25.9': {} + + '@babel/helper-validator-identifier@7.25.9': {} + + '@babel/helper-validator-option@7.25.9': {} + + '@babel/helpers@7.26.0': dependencies: - '@babel/traverse': 7.25.2 - '@babel/types': 7.25.2 - transitivePeerDependencies: - - supports-color + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 - '@babel/helper-string-parser@7.24.8': {} - - '@babel/helper-validator-identifier@7.24.7': {} - - '@babel/helper-validator-option@7.24.8': {} - - '@babel/helpers@7.25.0': + '@babel/parser@7.26.2': dependencies: - '@babel/template': 7.25.0 - '@babel/types': 7.25.2 + '@babel/types': 7.26.0 - '@babel/highlight@7.24.7': + '@babel/plugin-transform-react-jsx-self@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/helper-validator-identifier': 7.24.7 - chalk: 2.4.2 - js-tokens: 4.0.0 - picocolors: 1.0.1 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/parser@7.25.0': + '@babel/plugin-transform-react-jsx-source@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/types': 7.25.2 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-react-jsx-self@7.24.7(@babel/core@7.25.2)': - dependencies: - '@babel/core': 7.25.2 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-transform-react-jsx-source@7.24.7(@babel/core@7.25.2)': - dependencies: - '@babel/core': 7.25.2 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/runtime@7.25.0': + '@babel/runtime@7.25.6': dependencies: regenerator-runtime: 0.14.1 - '@babel/template@7.25.0': + '@babel/runtime@7.26.0': dependencies: - '@babel/code-frame': 7.24.7 - '@babel/parser': 7.25.0 - '@babel/types': 7.25.2 + regenerator-runtime: 0.14.1 - '@babel/traverse@7.25.2': + '@babel/template@7.25.9': dependencies: - '@babel/code-frame': 7.24.7 - '@babel/generator': 7.25.0 - '@babel/parser': 7.25.0 - '@babel/template': 7.25.0 - '@babel/types': 7.25.2 - debug: 4.3.6(supports-color@8.1.1) + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + + '@babel/traverse@7.25.9': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 + debug: 4.3.7(supports-color@5.5.0) globals: 11.12.0 transitivePeerDependencies: - supports-color - '@babel/types@7.25.2': + '@babel/types@7.26.0': dependencies: - '@babel/helper-string-parser': 7.24.8 - '@babel/helper-validator-identifier': 7.24.7 - to-fast-properties: 2.0.0 + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 - '@bufbuild/buf-darwin-arm64@1.35.1': + '@bufbuild/buf-darwin-arm64@1.46.0': optional: true - '@bufbuild/buf-darwin-x64@1.35.1': + '@bufbuild/buf-darwin-arm64@1.47.2': optional: true - '@bufbuild/buf-linux-aarch64@1.35.1': + '@bufbuild/buf-darwin-x64@1.46.0': optional: true - '@bufbuild/buf-linux-x64@1.35.1': + '@bufbuild/buf-darwin-x64@1.47.2': optional: true - '@bufbuild/buf-win32-arm64@1.35.1': + '@bufbuild/buf-linux-aarch64@1.46.0': optional: true - '@bufbuild/buf-win32-x64@1.35.1': + '@bufbuild/buf-linux-aarch64@1.47.2': optional: true - '@bufbuild/buf@1.35.1': + '@bufbuild/buf-linux-armv7@1.47.2': + optional: true + + '@bufbuild/buf-linux-x64@1.46.0': + optional: true + + '@bufbuild/buf-linux-x64@1.47.2': + optional: true + + '@bufbuild/buf-win32-arm64@1.46.0': + optional: true + + '@bufbuild/buf-win32-arm64@1.47.2': + optional: true + + '@bufbuild/buf-win32-x64@1.46.0': + optional: true + + '@bufbuild/buf-win32-x64@1.47.2': + optional: true + + '@bufbuild/buf@1.46.0': optionalDependencies: - '@bufbuild/buf-darwin-arm64': 1.35.1 - '@bufbuild/buf-darwin-x64': 1.35.1 - '@bufbuild/buf-linux-aarch64': 1.35.1 - '@bufbuild/buf-linux-x64': 1.35.1 - '@bufbuild/buf-win32-arm64': 1.35.1 - '@bufbuild/buf-win32-x64': 1.35.1 + '@bufbuild/buf-darwin-arm64': 1.46.0 + '@bufbuild/buf-darwin-x64': 1.46.0 + '@bufbuild/buf-linux-aarch64': 1.46.0 + '@bufbuild/buf-linux-x64': 1.46.0 + '@bufbuild/buf-win32-arm64': 1.46.0 + '@bufbuild/buf-win32-x64': 1.46.0 - '@bufbuild/protobuf@1.10.0': {} + '@bufbuild/buf@1.47.2': + optionalDependencies: + '@bufbuild/buf-darwin-arm64': 1.47.2 + '@bufbuild/buf-darwin-x64': 1.47.2 + '@bufbuild/buf-linux-aarch64': 1.47.2 + '@bufbuild/buf-linux-armv7': 1.47.2 + '@bufbuild/buf-linux-x64': 1.47.2 + '@bufbuild/buf-win32-arm64': 1.47.2 + '@bufbuild/buf-win32-x64': 1.47.2 - '@changesets/apply-release-plan@7.0.4': + '@bufbuild/protobuf@2.2.0': {} + + '@bufbuild/protobuf@2.2.2': {} + + '@bufbuild/protocompile@0.0.1(@bufbuild/buf@1.47.2)': dependencies: - '@babel/runtime': 7.25.0 - '@changesets/config': 3.0.2 + '@bufbuild/buf': 1.47.2 + '@bufbuild/protobuf': 2.2.2 + fflate: 0.8.2 + + '@changesets/apply-release-plan@7.0.5': + dependencies: + '@changesets/config': 3.0.3 '@changesets/get-version-range-type': 0.4.0 - '@changesets/git': 3.0.0 - '@changesets/should-skip-package': 0.1.0 + '@changesets/git': 3.0.1 + '@changesets/should-skip-package': 0.1.1 '@changesets/types': 6.0.0 '@manypkg/get-packages': 1.1.3 detect-indent: 6.1.0 @@ -4803,12 +5098,11 @@ snapshots: resolve-from: 5.0.0 semver: 7.6.3 - '@changesets/assemble-release-plan@6.0.3': + '@changesets/assemble-release-plan@6.0.4': dependencies: - '@babel/runtime': 7.25.0 '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.1 - '@changesets/should-skip-package': 0.1.0 + '@changesets/get-dependents-graph': 2.1.2 + '@changesets/should-skip-package': 0.1.1 '@changesets/types': 6.0.0 '@manypkg/get-packages': 1.1.3 semver: 7.6.3 @@ -4817,116 +5111,105 @@ snapshots: dependencies: '@changesets/types': 6.0.0 - '@changesets/cli@2.27.7': + '@changesets/cli@2.27.9': dependencies: - '@babel/runtime': 7.25.0 - '@changesets/apply-release-plan': 7.0.4 - '@changesets/assemble-release-plan': 6.0.3 + '@changesets/apply-release-plan': 7.0.5 + '@changesets/assemble-release-plan': 6.0.4 '@changesets/changelog-git': 0.2.0 - '@changesets/config': 3.0.2 + '@changesets/config': 3.0.3 '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.1 - '@changesets/get-release-plan': 4.0.3 - '@changesets/git': 3.0.0 - '@changesets/logger': 0.1.0 - '@changesets/pre': 2.0.0 - '@changesets/read': 0.6.0 - '@changesets/should-skip-package': 0.1.0 + '@changesets/get-dependents-graph': 2.1.2 + '@changesets/get-release-plan': 4.0.4 + '@changesets/git': 3.0.1 + '@changesets/logger': 0.1.1 + '@changesets/pre': 2.0.1 + '@changesets/read': 0.6.1 + '@changesets/should-skip-package': 0.1.1 '@changesets/types': 6.0.0 - '@changesets/write': 0.3.1 + '@changesets/write': 0.3.2 '@manypkg/get-packages': 1.1.3 - '@types/semver': 7.5.8 ansi-colors: 4.1.3 - chalk: 2.4.2 ci-info: 3.9.0 enquirer: 2.4.1 external-editor: 3.1.0 fs-extra: 7.0.1 - human-id: 1.0.2 mri: 1.2.0 - outdent: 0.5.0 p-limit: 2.3.0 - preferred-pm: 3.1.4 + package-manager-detector: 0.2.2 + picocolors: 1.1.1 resolve-from: 5.0.0 semver: 7.6.3 spawndamnit: 2.0.0 term-size: 2.2.1 - '@changesets/config@3.0.2': + '@changesets/config@3.0.3': dependencies: '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.1 - '@changesets/logger': 0.1.0 + '@changesets/get-dependents-graph': 2.1.2 + '@changesets/logger': 0.1.1 '@changesets/types': 6.0.0 '@manypkg/get-packages': 1.1.3 fs-extra: 7.0.1 - micromatch: 4.0.7 + micromatch: 4.0.8 '@changesets/errors@0.2.0': dependencies: extendable-error: 0.1.7 - '@changesets/get-dependents-graph@2.1.1': + '@changesets/get-dependents-graph@2.1.2': dependencies: '@changesets/types': 6.0.0 '@manypkg/get-packages': 1.1.3 - chalk: 2.4.2 - fs-extra: 7.0.1 + picocolors: 1.1.1 semver: 7.6.3 - '@changesets/get-release-plan@4.0.3': + '@changesets/get-release-plan@4.0.4': dependencies: - '@babel/runtime': 7.25.0 - '@changesets/assemble-release-plan': 6.0.3 - '@changesets/config': 3.0.2 - '@changesets/pre': 2.0.0 - '@changesets/read': 0.6.0 + '@changesets/assemble-release-plan': 6.0.4 + '@changesets/config': 3.0.3 + '@changesets/pre': 2.0.1 + '@changesets/read': 0.6.1 '@changesets/types': 6.0.0 '@manypkg/get-packages': 1.1.3 '@changesets/get-version-range-type@0.4.0': {} - '@changesets/git@3.0.0': + '@changesets/git@3.0.1': dependencies: - '@babel/runtime': 7.25.0 '@changesets/errors': 0.2.0 - '@changesets/types': 6.0.0 '@manypkg/get-packages': 1.1.3 is-subdir: 1.2.0 - micromatch: 4.0.7 + micromatch: 4.0.8 spawndamnit: 2.0.0 - '@changesets/logger@0.1.0': + '@changesets/logger@0.1.1': dependencies: - chalk: 2.4.2 + picocolors: 1.1.1 '@changesets/parse@0.4.0': dependencies: '@changesets/types': 6.0.0 js-yaml: 3.14.1 - '@changesets/pre@2.0.0': + '@changesets/pre@2.0.1': dependencies: - '@babel/runtime': 7.25.0 '@changesets/errors': 0.2.0 '@changesets/types': 6.0.0 '@manypkg/get-packages': 1.1.3 fs-extra: 7.0.1 - '@changesets/read@0.6.0': + '@changesets/read@0.6.1': dependencies: - '@babel/runtime': 7.25.0 - '@changesets/git': 3.0.0 - '@changesets/logger': 0.1.0 + '@changesets/git': 3.0.1 + '@changesets/logger': 0.1.1 '@changesets/parse': 0.4.0 '@changesets/types': 6.0.0 - chalk: 2.4.2 fs-extra: 7.0.1 p-filter: 2.1.0 + picocolors: 1.1.1 - '@changesets/should-skip-package@0.1.0': + '@changesets/should-skip-package@0.1.1': dependencies: - '@babel/runtime': 7.25.0 '@changesets/types': 6.0.0 '@manypkg/get-packages': 1.1.3 @@ -4934,9 +5217,8 @@ snapshots: '@changesets/types@6.0.0': {} - '@changesets/write@0.3.1': + '@changesets/write@0.3.2': dependencies: - '@babel/runtime': 7.25.0 '@changesets/types': 6.0.0 fs-extra: 7.0.1 human-id: 1.0.2 @@ -4945,39 +5227,38 @@ snapshots: '@colors/colors@1.5.0': optional: true - '@connectrpc/connect-node@1.4.0(@bufbuild/protobuf@1.10.0)(@connectrpc/connect@1.4.0(@bufbuild/protobuf@1.10.0))': + '@connectrpc/connect-node@2.0.0(@bufbuild/protobuf@2.2.2)(@connectrpc/connect@2.0.0(@bufbuild/protobuf@2.2.2))': dependencies: - '@bufbuild/protobuf': 1.10.0 - '@connectrpc/connect': 1.4.0(@bufbuild/protobuf@1.10.0) - undici: 5.28.4 + '@bufbuild/protobuf': 2.2.2 + '@connectrpc/connect': 2.0.0(@bufbuild/protobuf@2.2.2) - '@connectrpc/connect-web@1.4.0(@bufbuild/protobuf@1.10.0)(@connectrpc/connect@1.4.0(@bufbuild/protobuf@1.10.0))': + '@connectrpc/connect-web@2.0.0(@bufbuild/protobuf@2.2.2)(@connectrpc/connect@2.0.0(@bufbuild/protobuf@2.2.2))': dependencies: - '@bufbuild/protobuf': 1.10.0 - '@connectrpc/connect': 1.4.0(@bufbuild/protobuf@1.10.0) + '@bufbuild/protobuf': 2.2.2 + '@connectrpc/connect': 2.0.0(@bufbuild/protobuf@2.2.2) - '@connectrpc/connect@1.4.0(@bufbuild/protobuf@1.10.0)': + '@connectrpc/connect@2.0.0(@bufbuild/protobuf@2.2.2)': dependencies: - '@bufbuild/protobuf': 1.10.0 + '@bufbuild/protobuf': 2.2.2 - '@cypress/request@3.0.1': + '@cypress/request@3.0.6': dependencies: aws-sign2: 0.7.0 - aws4: 1.13.0 + aws4: 1.13.2 caseless: 0.12.0 combined-stream: 1.0.8 extend: 3.0.2 forever-agent: 0.6.1 - form-data: 2.3.3 - http-signature: 1.3.6 + form-data: 4.0.0 + http-signature: 1.4.0 is-typedarray: 1.0.0 isstream: 0.1.2 json-stringify-safe: 5.0.1 mime-types: 2.1.35 performance-now: 2.1.0 - qs: 6.10.4 + qs: 6.13.0 safe-buffer: 5.2.1 - tough-cookie: 4.1.4 + tough-cookie: 5.0.0 tunnel-agent: 0.6.0 uuid: 8.3.2 @@ -4988,161 +5269,173 @@ snapshots: transitivePeerDependencies: - supports-color + '@emnapi/runtime@1.3.1': + dependencies: + tslib: 2.7.0 + optional: true + '@esbuild/aix-ppc64@0.21.5': optional: true - '@esbuild/aix-ppc64@0.23.0': + '@esbuild/aix-ppc64@0.24.0': optional: true '@esbuild/android-arm64@0.21.5': optional: true - '@esbuild/android-arm64@0.23.0': + '@esbuild/android-arm64@0.24.0': optional: true '@esbuild/android-arm@0.21.5': optional: true - '@esbuild/android-arm@0.23.0': + '@esbuild/android-arm@0.24.0': optional: true '@esbuild/android-x64@0.21.5': optional: true - '@esbuild/android-x64@0.23.0': + '@esbuild/android-x64@0.24.0': optional: true '@esbuild/darwin-arm64@0.21.5': optional: true - '@esbuild/darwin-arm64@0.23.0': + '@esbuild/darwin-arm64@0.24.0': optional: true '@esbuild/darwin-x64@0.21.5': optional: true - '@esbuild/darwin-x64@0.23.0': + '@esbuild/darwin-x64@0.24.0': optional: true '@esbuild/freebsd-arm64@0.21.5': optional: true - '@esbuild/freebsd-arm64@0.23.0': + '@esbuild/freebsd-arm64@0.24.0': optional: true '@esbuild/freebsd-x64@0.21.5': optional: true - '@esbuild/freebsd-x64@0.23.0': + '@esbuild/freebsd-x64@0.24.0': optional: true '@esbuild/linux-arm64@0.21.5': optional: true - '@esbuild/linux-arm64@0.23.0': + '@esbuild/linux-arm64@0.24.0': optional: true '@esbuild/linux-arm@0.21.5': optional: true - '@esbuild/linux-arm@0.23.0': + '@esbuild/linux-arm@0.24.0': optional: true '@esbuild/linux-ia32@0.21.5': optional: true - '@esbuild/linux-ia32@0.23.0': + '@esbuild/linux-ia32@0.24.0': optional: true '@esbuild/linux-loong64@0.21.5': optional: true - '@esbuild/linux-loong64@0.23.0': + '@esbuild/linux-loong64@0.24.0': optional: true '@esbuild/linux-mips64el@0.21.5': optional: true - '@esbuild/linux-mips64el@0.23.0': + '@esbuild/linux-mips64el@0.24.0': optional: true '@esbuild/linux-ppc64@0.21.5': optional: true - '@esbuild/linux-ppc64@0.23.0': + '@esbuild/linux-ppc64@0.24.0': optional: true '@esbuild/linux-riscv64@0.21.5': optional: true - '@esbuild/linux-riscv64@0.23.0': + '@esbuild/linux-riscv64@0.24.0': optional: true '@esbuild/linux-s390x@0.21.5': optional: true - '@esbuild/linux-s390x@0.23.0': + '@esbuild/linux-s390x@0.24.0': optional: true '@esbuild/linux-x64@0.21.5': optional: true - '@esbuild/linux-x64@0.23.0': + '@esbuild/linux-x64@0.24.0': optional: true '@esbuild/netbsd-x64@0.21.5': optional: true - '@esbuild/netbsd-x64@0.23.0': + '@esbuild/netbsd-x64@0.24.0': optional: true - '@esbuild/openbsd-arm64@0.23.0': + '@esbuild/openbsd-arm64@0.24.0': optional: true '@esbuild/openbsd-x64@0.21.5': optional: true - '@esbuild/openbsd-x64@0.23.0': + '@esbuild/openbsd-x64@0.24.0': optional: true '@esbuild/sunos-x64@0.21.5': optional: true - '@esbuild/sunos-x64@0.23.0': + '@esbuild/sunos-x64@0.24.0': optional: true '@esbuild/win32-arm64@0.21.5': optional: true - '@esbuild/win32-arm64@0.23.0': + '@esbuild/win32-arm64@0.24.0': optional: true '@esbuild/win32-ia32@0.21.5': optional: true - '@esbuild/win32-ia32@0.23.0': + '@esbuild/win32-ia32@0.24.0': optional: true '@esbuild/win32-x64@0.21.5': optional: true - '@esbuild/win32-x64@0.23.0': + '@esbuild/win32-x64@0.24.0': optional: true - '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)': + '@eslint-community/eslint-utils@4.4.0(eslint@8.57.1)': dependencies: - eslint: 8.57.0 + eslint: 8.57.1 eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.11.0': {} + '@eslint-community/eslint-utils@4.4.1(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.11.1': {} + + '@eslint-community/regexpp@4.12.1': {} '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7(supports-color@5.5.0) espree: 9.6.1 globals: 13.24.0 - ignore: 5.3.1 + ignore: 5.3.2 import-fresh: 3.3.0 js-yaml: 4.1.0 minimatch: 3.1.2 @@ -5150,9 +5443,63 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@8.57.0': {} + '@eslint/js@8.57.1': {} - '@fastify/busboy@2.1.1': {} + '@faker-js/faker@9.2.0': {} + + '@floating-ui/core@1.6.8': + dependencies: + '@floating-ui/utils': 0.2.8 + + '@floating-ui/dom@1.6.11': + dependencies: + '@floating-ui/core': 1.6.8 + '@floating-ui/utils': 0.2.8 + + '@floating-ui/react-dom@2.1.2(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@floating-ui/dom': 1.6.11 + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + + '@floating-ui/react@0.26.24(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@floating-ui/react-dom': 2.1.2(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@floating-ui/utils': 0.2.8 + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + tabbable: 6.2.0 + + '@floating-ui/utils@0.2.8': {} + + '@formatjs/ecma402-abstract@2.2.4': + dependencies: + '@formatjs/fast-memoize': 2.2.3 + '@formatjs/intl-localematcher': 0.5.8 + tslib: 2.8.1 + + '@formatjs/fast-memoize@2.2.3': + dependencies: + tslib: 2.8.1 + + '@formatjs/icu-messageformat-parser@2.9.4': + dependencies: + '@formatjs/ecma402-abstract': 2.2.4 + '@formatjs/icu-skeleton-parser': 1.8.8 + tslib: 2.8.1 + + '@formatjs/icu-skeleton-parser@1.8.8': + dependencies: + '@formatjs/ecma402-abstract': 2.2.4 + tslib: 2.8.1 + + '@formatjs/intl-localematcher@0.5.4': + dependencies: + tslib: 2.7.0 + + '@formatjs/intl-localematcher@0.5.8': + dependencies: + tslib: 2.8.1 '@grpc/grpc-js@1.11.1': dependencies: @@ -5163,7 +5510,7 @@ snapshots: dependencies: lodash.camelcase: 4.3.0 long: 5.2.3 - protobufjs: 7.3.2 + protobufjs: 7.4.0 yargs: 17.7.2 '@hapi/hoek@9.3.0': {} @@ -5172,21 +5519,23 @@ snapshots: dependencies: '@hapi/hoek': 9.3.0 - '@headlessui/react@1.7.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@headlessui/react@2.1.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: - '@tanstack/react-virtual': 3.8.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - client-only: 0.0.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + '@floating-ui/react': 0.26.24(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@react-aria/focus': 3.18.3(react@19.0.0-rc-66855b96-20241106) + '@react-aria/interactions': 3.22.3(react@19.0.0-rc-66855b96-20241106) + '@tanstack/react-virtual': 3.10.6(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) - '@heroicons/react@2.1.3(react@18.3.1)': + '@heroicons/react@2.1.3(react@19.0.0-rc-66855b96-20241106)': dependencies: - react: 18.3.1 + react: 19.0.0-rc-66855b96-20241106 - '@humanwhocodes/config-array@0.11.14': + '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7(supports-color@5.5.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -5195,6 +5544,81 @@ snapshots: '@humanwhocodes/object-schema@2.0.3': {} + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.3.1 + optional: true + + '@img/sharp-win32-ia32@0.33.5': + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -5204,10 +5628,6 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 - '@jest/schemas@29.6.3': - dependencies: - '@sinclair/typebox': 0.27.8 - '@jridgewell/gen-mapping@0.3.5': dependencies: '@jridgewell/set-array': 1.2.1 @@ -5229,14 +5649,14 @@ snapshots: '@manypkg/find-root@1.1.0': dependencies: - '@babel/runtime': 7.25.0 + '@babel/runtime': 7.26.0 '@types/node': 12.20.55 find-up: 4.1.0 fs-extra: 8.1.0 '@manypkg/get-packages@1.1.3': dependencies: - '@babel/runtime': 7.25.0 + '@babel/runtime': 7.26.0 '@changesets/types': 4.1.0 '@manypkg/find-root': 1.1.0 fs-extra: 8.1.0 @@ -5258,67 +5678,39 @@ snapshots: - encoding - supports-color - '@next/env@14.2.3': {} + '@next/env@15.0.4-canary.23': {} - '@next/env@14.2.5': {} - - '@next/eslint-plugin-next@14.2.5': + '@next/eslint-plugin-next@14.2.18': dependencies: glob: 10.3.10 - '@next/swc-darwin-arm64@14.2.3': + '@next/swc-darwin-arm64@15.0.4-canary.23': optional: true - '@next/swc-darwin-arm64@14.2.5': + '@next/swc-darwin-x64@15.0.4-canary.23': optional: true - '@next/swc-darwin-x64@14.2.3': + '@next/swc-linux-arm64-gnu@15.0.4-canary.23': optional: true - '@next/swc-darwin-x64@14.2.5': + '@next/swc-linux-arm64-musl@15.0.4-canary.23': optional: true - '@next/swc-linux-arm64-gnu@14.2.3': + '@next/swc-linux-x64-gnu@15.0.4-canary.23': optional: true - '@next/swc-linux-arm64-gnu@14.2.5': + '@next/swc-linux-x64-musl@15.0.4-canary.23': optional: true - '@next/swc-linux-arm64-musl@14.2.3': + '@next/swc-win32-arm64-msvc@15.0.4-canary.23': optional: true - '@next/swc-linux-arm64-musl@14.2.5': + '@next/swc-win32-x64-msvc@15.0.4-canary.23': optional: true - '@next/swc-linux-x64-gnu@14.2.3': - optional: true - - '@next/swc-linux-x64-gnu@14.2.5': - optional: true - - '@next/swc-linux-x64-musl@14.2.3': - optional: true - - '@next/swc-linux-x64-musl@14.2.5': - optional: true - - '@next/swc-win32-arm64-msvc@14.2.3': - optional: true - - '@next/swc-win32-arm64-msvc@14.2.5': - optional: true - - '@next/swc-win32-ia32-msvc@14.2.3': - optional: true - - '@next/swc-win32-ia32-msvc@14.2.5': - optional: true - - '@next/swc-win32-x64-msvc@14.2.3': - optional: true - - '@next/swc-win32-x64-msvc@14.2.5': - optional: true + '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': + dependencies: + eslint-scope: 5.1.1 '@nodelib/fs.scandir@2.1.5': dependencies: @@ -5332,9 +5724,87 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 + '@nolyfill/is-core-module@1.0.39': {} + + '@otplib/core@12.0.1': {} + + '@otplib/plugin-crypto@12.0.1': + dependencies: + '@otplib/core': 12.0.1 + + '@otplib/plugin-thirty-two@12.0.1': + dependencies: + '@otplib/core': 12.0.1 + thirty-two: 1.0.2 + + '@parcel/watcher-android-arm64@2.5.0': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.0': + optional: true + + '@parcel/watcher-darwin-x64@2.5.0': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.0': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.0': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.0': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.0': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.0': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.0': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.0': + optional: true + + '@parcel/watcher-win32-arm64@2.5.0': + optional: true + + '@parcel/watcher-win32-ia32@2.5.0': + optional: true + + '@parcel/watcher-win32-x64@2.5.0': + optional: true + + '@parcel/watcher@2.5.0': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.0 + '@parcel/watcher-darwin-arm64': 2.5.0 + '@parcel/watcher-darwin-x64': 2.5.0 + '@parcel/watcher-freebsd-x64': 2.5.0 + '@parcel/watcher-linux-arm-glibc': 2.5.0 + '@parcel/watcher-linux-arm-musl': 2.5.0 + '@parcel/watcher-linux-arm64-glibc': 2.5.0 + '@parcel/watcher-linux-arm64-musl': 2.5.0 + '@parcel/watcher-linux-x64-glibc': 2.5.0 + '@parcel/watcher-linux-x64-musl': 2.5.0 + '@parcel/watcher-win32-arm64': 2.5.0 + '@parcel/watcher-win32-ia32': 2.5.0 + '@parcel/watcher-win32-x64': 2.5.0 + optional: true + '@pkgjs/parseargs@0.11.0': optional: true + '@playwright/test@1.48.2': + dependencies: + playwright: 1.48.2 + '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -5358,52 +5828,98 @@ snapshots: '@protobufjs/utf8@1.1.0': {} - '@rollup/rollup-android-arm-eabi@4.19.1': + '@react-aria/focus@3.18.3(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@react-aria/interactions': 3.22.3(react@19.0.0-rc-66855b96-20241106) + '@react-aria/utils': 3.25.3(react@19.0.0-rc-66855b96-20241106) + '@react-types/shared': 3.25.0(react@19.0.0-rc-66855b96-20241106) + '@swc/helpers': 0.5.5 + clsx: 2.1.1 + react: 19.0.0-rc-66855b96-20241106 + + '@react-aria/interactions@3.22.3(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@react-aria/ssr': 3.9.6(react@19.0.0-rc-66855b96-20241106) + '@react-aria/utils': 3.25.3(react@19.0.0-rc-66855b96-20241106) + '@react-types/shared': 3.25.0(react@19.0.0-rc-66855b96-20241106) + '@swc/helpers': 0.5.5 + react: 19.0.0-rc-66855b96-20241106 + + '@react-aria/ssr@3.9.6(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@swc/helpers': 0.5.5 + react: 19.0.0-rc-66855b96-20241106 + + '@react-aria/utils@3.25.3(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@react-aria/ssr': 3.9.6(react@19.0.0-rc-66855b96-20241106) + '@react-stately/utils': 3.10.4(react@19.0.0-rc-66855b96-20241106) + '@react-types/shared': 3.25.0(react@19.0.0-rc-66855b96-20241106) + '@swc/helpers': 0.5.5 + clsx: 2.1.1 + react: 19.0.0-rc-66855b96-20241106 + + '@react-stately/utils@3.10.4(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@swc/helpers': 0.5.13 + react: 19.0.0-rc-66855b96-20241106 + + '@react-types/shared@3.25.0(react@19.0.0-rc-66855b96-20241106)': + dependencies: + react: 19.0.0-rc-66855b96-20241106 + + '@rollup/rollup-android-arm-eabi@4.25.0': optional: true - '@rollup/rollup-android-arm64@4.19.1': + '@rollup/rollup-android-arm64@4.25.0': optional: true - '@rollup/rollup-darwin-arm64@4.19.1': + '@rollup/rollup-darwin-arm64@4.25.0': optional: true - '@rollup/rollup-darwin-x64@4.19.1': + '@rollup/rollup-darwin-x64@4.25.0': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.19.1': + '@rollup/rollup-freebsd-arm64@4.25.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.19.1': + '@rollup/rollup-freebsd-x64@4.25.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.19.1': + '@rollup/rollup-linux-arm-gnueabihf@4.25.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.19.1': + '@rollup/rollup-linux-arm-musleabihf@4.25.0': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.19.1': + '@rollup/rollup-linux-arm64-gnu@4.25.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.19.1': + '@rollup/rollup-linux-arm64-musl@4.25.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.19.1': + '@rollup/rollup-linux-powerpc64le-gnu@4.25.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.19.1': + '@rollup/rollup-linux-riscv64-gnu@4.25.0': optional: true - '@rollup/rollup-linux-x64-musl@4.19.1': + '@rollup/rollup-linux-s390x-gnu@4.25.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.19.1': + '@rollup/rollup-linux-x64-gnu@4.25.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.19.1': + '@rollup/rollup-linux-x64-musl@4.25.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.19.1': + '@rollup/rollup-win32-arm64-msvc@4.25.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.25.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.25.0': optional: true '@rushstack/eslint-patch@1.10.4': {} @@ -5416,37 +5932,41 @@ snapshots: '@sideway/pinpoint@2.0.0': {} - '@sinclair/typebox@0.27.8': {} + '@sindresorhus/merge-streams@2.3.0': {} '@swc/counter@0.1.3': {} + '@swc/helpers@0.5.13': + dependencies: + tslib: 2.7.0 + '@swc/helpers@0.5.5': dependencies: '@swc/counter': 0.1.3 - tslib: 2.6.3 + tslib: 2.7.0 - '@tailwindcss/forms@0.5.3(tailwindcss@3.2.4(postcss@8.4.40))': + '@tailwindcss/forms@0.5.3(tailwindcss@3.4.14)': dependencies: mini-svg-data-uri: 1.4.4 - tailwindcss: 3.2.4(postcss@8.4.40) + tailwindcss: 3.4.14 - '@tailwindcss/forms@0.5.7(tailwindcss@3.2.4(postcss@8.4.21))': + '@tailwindcss/forms@0.5.7(tailwindcss@3.4.14)': dependencies: mini-svg-data-uri: 1.4.4 - tailwindcss: 3.2.4(postcss@8.4.21) + tailwindcss: 3.4.14 - '@tanstack/react-virtual@3.8.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@tanstack/react-virtual@3.10.6(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: - '@tanstack/virtual-core': 3.8.4 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + '@tanstack/virtual-core': 3.10.6 + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) - '@tanstack/virtual-core@3.8.4': {} + '@tanstack/virtual-core@3.10.6': {} '@testing-library/dom@10.4.0': dependencies: - '@babel/code-frame': 7.24.7 - '@babel/runtime': 7.25.0 + '@babel/code-frame': 7.26.2 + '@babel/runtime': 7.26.0 '@types/aria-query': 5.0.4 aria-query: 5.3.0 chalk: 4.1.2 @@ -5454,21 +5974,9 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 - '@testing-library/dom@9.3.4': - dependencies: - '@babel/code-frame': 7.24.7 - '@babel/runtime': 7.25.0 - '@types/aria-query': 5.0.4 - aria-query: 5.1.3 - chalk: 4.1.2 - dom-accessibility-api: 0.5.16 - lz-string: 1.5.0 - pretty-format: 27.5.1 - - '@testing-library/jest-dom@6.4.8': + '@testing-library/jest-dom@6.6.3': dependencies: '@adobe/css-tools': 4.4.0 - '@babel/runtime': 7.25.0 aria-query: 5.3.0 chalk: 3.0.0 css.escape: 1.5.1 @@ -5476,121 +5984,99 @@ snapshots: lodash: 4.17.21 redent: 3.0.0 - '@testing-library/react@14.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@testing-library/react@16.0.1(@testing-library/dom@10.4.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)': dependencies: - '@babel/runtime': 7.25.0 - '@testing-library/dom': 9.3.4 - '@types/react-dom': 18.3.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - - '@testing-library/react@15.0.7(@types/react@18.2.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@babel/runtime': 7.25.0 + '@babel/runtime': 7.25.6 '@testing-library/dom': 10.4.0 - '@types/react-dom': 18.3.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) optionalDependencies: - '@types/react': 18.2.8 + '@types/react': types-react@19.0.0-rc.1 + '@types/react-dom': types-react-dom@19.0.0-rc.1 '@types/aria-query@5.0.4': {} '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.25.0 - '@babel/types': 7.25.2 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 '@types/babel__generator': 7.6.8 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.20.6 '@types/babel__generator@7.6.8': dependencies: - '@babel/types': 7.25.2 + '@babel/types': 7.26.0 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.25.0 - '@babel/types': 7.25.2 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 '@types/babel__traverse@7.20.6': dependencies: - '@babel/types': 7.25.2 + '@babel/types': 7.26.0 - '@types/estree@1.0.5': {} + '@types/estree@1.0.6': {} '@types/json5@0.0.29': {} - '@types/minimist@1.2.5': {} - - '@types/ms@0.7.31': {} + '@types/ms@0.7.34': {} '@types/node@12.20.55': {} - '@types/node@18.11.9': {} - - '@types/node@20.14.13': + '@types/node@22.9.0': dependencies: - undici-types: 5.26.5 - - '@types/normalize-package-data@2.4.4': {} + undici-types: 6.19.8 '@types/prop-types@15.7.12': {} - '@types/react-dom@18.0.9': - dependencies: - '@types/react': 18.3.3 - - '@types/react-dom@18.3.0': - dependencies: - '@types/react': 18.3.3 - - '@types/react@17.0.80': - dependencies: - '@types/prop-types': 15.7.12 - '@types/scheduler': 0.16.8 - csstype: 3.1.3 - - '@types/react@18.2.8': - dependencies: - '@types/prop-types': 15.7.12 - '@types/scheduler': 0.23.0 - csstype: 3.1.3 - - '@types/react@18.3.3': + '@types/react@18.3.12': dependencies: '@types/prop-types': 15.7.12 csstype: 3.1.3 - '@types/scheduler@0.16.8': {} - - '@types/scheduler@0.23.0': {} - - '@types/semver@7.5.8': {} - '@types/sinonjs__fake-timers@8.1.1': {} '@types/sizzle@2.3.8': {} '@types/tinycolor2@1.4.3': {} - '@types/uuid@9.0.8': {} + '@types/uuid@10.0.0': {} '@types/yauzl@2.10.3': dependencies: - '@types/node': 20.14.13 + '@types/node': 22.9.0 optional: true - '@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4)': + '@typescript-eslint/eslint-plugin@8.15.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/scope-manager': 8.15.0 + '@typescript-eslint/type-utils': 8.15.0(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/utils': 8.15.0(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.15.0 + eslint: 8.57.1 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.4.1(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3)': dependencies: '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.5.4) + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.3) '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.3.6(supports-color@8.1.1) - eslint: 8.57.0 + debug: 4.3.6 + eslint: 8.57.1 optionalDependencies: - typescript: 5.5.4 + typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -5599,20 +6085,66 @@ snapshots: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 + '@typescript-eslint/scope-manager@8.15.0': + dependencies: + '@typescript-eslint/types': 8.15.0 + '@typescript-eslint/visitor-keys': 8.15.0 + + '@typescript-eslint/type-utils@8.15.0(eslint@8.57.1)(typescript@5.6.3)': + dependencies: + '@typescript-eslint/typescript-estree': 8.15.0(typescript@5.6.3) + '@typescript-eslint/utils': 8.15.0(eslint@8.57.1)(typescript@5.6.3) + debug: 4.3.7(supports-color@5.5.0) + eslint: 8.57.1 + ts-api-utils: 1.4.1(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/types@7.18.0': {} - '@typescript-eslint/typescript-estree@7.18.0(typescript@5.5.4)': + '@typescript-eslint/types@8.15.0': {} + + '@typescript-eslint/typescript-estree@7.18.0(typescript@5.6.3)': dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.6.3 - ts-api-utils: 1.3.0(typescript@5.5.4) + ts-api-utils: 1.3.0(typescript@5.6.3) optionalDependencies: - typescript: 5.5.4 + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@8.15.0(typescript@5.6.3)': + dependencies: + '@typescript-eslint/types': 8.15.0 + '@typescript-eslint/visitor-keys': 8.15.0 + debug: 4.3.7(supports-color@5.5.0) + fast-glob: 3.3.2 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 1.4.1(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.15.0(eslint@8.57.1)(typescript@5.6.3)': + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) + '@typescript-eslint/scope-manager': 8.15.0 + '@typescript-eslint/types': 8.15.0 + '@typescript-eslint/typescript-estree': 8.15.0(typescript@5.6.3) + eslint: 8.57.1 + optionalDependencies: + typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -5621,56 +6153,72 @@ snapshots: '@typescript-eslint/types': 7.18.0 eslint-visitor-keys: 3.4.3 + '@typescript-eslint/visitor-keys@8.15.0': + dependencies: + '@typescript-eslint/types': 8.15.0 + eslint-visitor-keys: 4.2.0 + '@ungap/structured-clone@1.2.0': {} - '@vercel/analytics@1.3.1(next@14.2.3(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1)': + '@vercel/analytics@1.3.1(next@15.0.4-canary.23(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(sass@1.80.7))(react@19.0.0-rc-66855b96-20241106)': dependencies: server-only: 0.0.1 optionalDependencies: - next: 14.2.3(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8) - react: 18.3.1 + next: 15.0.4-canary.23(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(sass@1.80.7) + react: 19.0.0-rc-66855b96-20241106 '@vercel/git-hooks@1.0.0': {} - '@vitejs/plugin-react@4.3.1(vite@5.3.5(@types/node@20.14.13)(sass@1.77.8))': + '@vitejs/plugin-react@4.3.3(vite@5.4.11(@types/node@22.9.0)(sass@1.80.7))': dependencies: - '@babel/core': 7.25.2 - '@babel/plugin-transform-react-jsx-self': 7.24.7(@babel/core@7.25.2) - '@babel/plugin-transform-react-jsx-source': 7.24.7(@babel/core@7.25.2) + '@babel/core': 7.26.0 + '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 5.3.5(@types/node@20.14.13)(sass@1.77.8) + vite: 5.4.11(@types/node@22.9.0)(sass@1.80.7) transitivePeerDependencies: - supports-color - '@vitest/expect@1.6.0': + '@vitest/expect@2.1.4': dependencies: - '@vitest/spy': 1.6.0 - '@vitest/utils': 1.6.0 - chai: 4.5.0 + '@vitest/spy': 2.1.4 + '@vitest/utils': 2.1.4 + chai: 5.1.2 + tinyrainbow: 1.2.0 - '@vitest/runner@1.6.0': + '@vitest/mocker@2.1.4(vite@5.4.11(@types/node@22.9.0)(sass@1.80.7))': dependencies: - '@vitest/utils': 1.6.0 - p-limit: 5.0.0 - pathe: 1.1.2 - - '@vitest/snapshot@1.6.0': - dependencies: - magic-string: 0.30.11 - pathe: 1.1.2 - pretty-format: 29.7.0 - - '@vitest/spy@1.6.0': - dependencies: - tinyspy: 2.2.1 - - '@vitest/utils@1.6.0': - dependencies: - diff-sequences: 29.6.3 + '@vitest/spy': 2.1.4 estree-walker: 3.0.3 - loupe: 2.3.7 - pretty-format: 29.7.0 + magic-string: 0.30.12 + optionalDependencies: + vite: 5.4.11(@types/node@22.9.0)(sass@1.80.7) + + '@vitest/pretty-format@2.1.4': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.4': + dependencies: + '@vitest/utils': 2.1.4 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.4': + dependencies: + '@vitest/pretty-format': 2.1.4 + magic-string: 0.30.12 + pathe: 1.1.2 + + '@vitest/spy@2.1.4': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.4': + dependencies: + '@vitest/pretty-format': 2.1.4 + loupe: 3.1.2 + tinyrainbow: 1.2.0 abbrev@1.1.1: {} @@ -5680,31 +6228,17 @@ snapshots: dependencies: acorn: 8.12.1 - acorn-node@1.8.2: - dependencies: - acorn: 7.4.1 - acorn-walk: 7.2.0 - xtend: 4.0.2 - - acorn-walk@7.2.0: {} - - acorn-walk@8.3.3: - dependencies: - acorn: 8.12.1 - - acorn@7.4.1: {} - acorn@8.12.1: {} agent-base@6.0.2: dependencies: - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7(supports-color@5.5.0) transitivePeerDependencies: - supports-color agent-base@7.1.1: dependencies: - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -5713,11 +6247,6 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 - aggregate-error@4.0.1: - dependencies: - clean-stack: 4.2.0 - indent-string: 5.0.0 - ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -5731,13 +6260,13 @@ snapshots: dependencies: type-fest: 0.21.3 + ansi-escapes@7.0.0: + dependencies: + environment: 1.1.0 + ansi-regex@5.0.1: {} - ansi-regex@6.0.1: {} - - ansi-styles@3.2.1: - dependencies: - color-convert: 1.9.3 + ansi-regex@6.1.0: {} ansi-styles@4.3.0: dependencies: @@ -5846,34 +6375,32 @@ snapshots: is-array-buffer: 3.0.4 is-shared-array-buffer: 1.0.3 - arrify@1.0.1: {} - asn1@0.2.6: dependencies: safer-buffer: 2.1.2 assert-plus@1.0.0: {} - assertion-error@1.1.0: {} + assertion-error@2.0.1: {} ast-types-flow@0.0.8: {} astral-regex@2.0.0: {} - async@3.2.5: {} + async@3.2.6: {} asynckit@0.4.0: {} at-least-node@1.0.0: {} - autoprefixer@10.4.13(postcss@8.4.21): + autoprefixer@10.4.20(postcss@8.4.49): dependencies: - browserslist: 4.23.2 - caniuse-lite: 1.0.30001644 + browserslist: 4.23.3 + caniuse-lite: 1.0.30001654 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.0.1 - postcss: 8.4.21 + postcss: 8.4.49 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: @@ -5882,13 +6409,13 @@ snapshots: aws-sign2@0.7.0: {} - aws4@1.13.0: {} + aws4@1.13.2: {} axe-core@4.10.0: {} - axios@1.7.2(debug@4.3.6): + axios@1.7.7(debug@4.3.7): dependencies: - follow-redirects: 1.15.6(debug@4.3.6) + follow-redirects: 1.15.6(debug@4.3.7) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -5929,12 +6456,19 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.23.2: + browserslist@4.23.3: dependencies: - caniuse-lite: 1.0.30001644 - electron-to-chromium: 1.5.3 + caniuse-lite: 1.0.30001654 + electron-to-chromium: 1.5.13 node-releases: 2.0.18 - update-browserslist-db: 1.1.0(browserslist@4.23.2) + update-browserslist-db: 1.1.0(browserslist@4.23.3) + + browserslist@4.24.2: + dependencies: + caniuse-lite: 1.0.30001680 + electron-to-chromium: 1.5.57 + node-releases: 2.0.18 + update-browserslist-db: 1.1.1(browserslist@4.24.2) buffer-crc32@0.2.13: {} @@ -5943,9 +6477,9 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 - bundle-require@5.0.0(esbuild@0.23.0): + bundle-require@5.0.0(esbuild@0.24.0): dependencies: - esbuild: 0.23.0 + esbuild: 0.24.0 load-tsconfig: 0.2.5 busboy@1.6.0: @@ -5968,36 +6502,21 @@ snapshots: camelcase-css@2.0.1: {} - camelcase-keys@7.0.2: - dependencies: - camelcase: 6.3.0 - map-obj: 4.3.0 - quick-lru: 5.1.1 - type-fest: 1.4.0 + caniuse-lite@1.0.30001654: {} - camelcase@6.3.0: {} - - caniuse-lite@1.0.30001644: {} + caniuse-lite@1.0.30001680: {} case-anything@2.1.13: {} caseless@0.12.0: {} - chai@4.5.0: + chai@5.1.2: dependencies: - assertion-error: 1.1.0 - check-error: 1.0.3 - deep-eql: 4.1.4 - get-func-name: 2.0.2 - loupe: 2.3.7 - pathval: 1.1.1 - type-detect: 4.1.0 - - chalk@2.4.2: - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.2 + pathval: 2.0.0 chalk@3.0.0: dependencies: @@ -6009,11 +6528,11 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chalk@5.3.0: {} + chardet@0.7.0: {} - check-error@1.0.3: - dependencies: - get-func-name: 2.0.2 + check-error@2.1.1: {} check-more-types@2.24.0: {} @@ -6029,20 +6548,26 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chokidar@4.0.1: + dependencies: + readdirp: 4.0.2 + chownr@2.0.0: {} ci-info@3.9.0: {} - clean-stack@2.2.0: {} + ci-info@4.1.0: {} - clean-stack@4.2.0: - dependencies: - escape-string-regexp: 5.0.0 + clean-stack@2.2.0: {} cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + cli-table3@0.6.5: dependencies: string-width: 4.2.3 @@ -6054,10 +6579,10 @@ snapshots: slice-ansi: 3.0.0 string-width: 4.2.3 - cli-truncate@3.1.0: + cli-truncate@4.0.0: dependencies: slice-ansi: 5.0.0 - string-width: 5.1.2 + string-width: 7.2.0 client-only@0.0.1: {} @@ -6069,50 +6594,54 @@ snapshots: clsx@1.2.1: {} - color-convert@1.9.3: - dependencies: - color-name: 1.1.3 + clsx@2.1.1: {} color-convert@2.0.1: dependencies: color-name: 1.1.4 - color-name@1.1.3: {} - color-name@1.1.4: {} + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + optional: true + color-support@1.1.3: {} + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + optional: true + colorette@2.0.20: {} combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 + commander@12.1.0: {} + commander@4.1.1: {} commander@6.2.1: {} - commander@9.5.0: {} - common-tags@1.8.2: {} concat-map@0.0.1: {} - concurrently@8.2.2: + concurrently@9.1.0: dependencies: chalk: 4.1.2 - date-fns: 2.30.0 lodash: 4.17.21 rxjs: 7.8.1 shell-quote: 1.8.1 - spawn-command: 0.0.2 supports-color: 8.1.1 tree-kill: 1.2.2 yargs: 17.7.2 - confbox@0.1.7: {} - consola@3.2.3: {} console-control-strings@1.1.0: {} @@ -6137,19 +6666,25 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + cross-spawn@7.0.5: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + css.escape@1.5.1: {} cssesc@3.0.0: {} - cssstyle@4.0.1: + cssstyle@4.1.0: dependencies: - rrweb-cssom: 0.6.0 + rrweb-cssom: 0.7.1 csstype@3.1.3: {} - cypress@13.13.1: + cypress@13.15.2: dependencies: - '@cypress/request': 3.0.1 + '@cypress/request': 3.0.6 '@cypress/xvfb': 1.2.4(supports-color@8.1.1) '@types/sinonjs__fake-timers': 8.1.1 '@types/sizzle': 2.3.8 @@ -6160,12 +6695,13 @@ snapshots: cachedir: 2.4.0 chalk: 4.1.2 check-more-types: 2.24.0 + ci-info: 4.1.0 cli-cursor: 3.1.0 cli-table3: 0.6.5 commander: 6.2.1 common-tags: 1.8.2 - dayjs: 1.11.12 - debug: 4.3.6(supports-color@8.1.1) + dayjs: 1.11.13 + debug: 4.3.7(supports-color@8.1.1) enquirer: 2.4.1 eventemitter2: 6.4.7 execa: 4.1.0 @@ -6174,7 +6710,6 @@ snapshots: figures: 3.2.0 fs-extra: 9.1.0 getos: 3.2.1 - is-ci: 3.0.1 is-installed-globally: 0.4.0 lazy-ass: 1.6.0 listr2: 3.14.0(enquirer@2.4.1) @@ -6189,6 +6724,7 @@ snapshots: semver: 7.6.3 supports-color: 8.1.1 tmp: 0.2.3 + tree-kill: 1.2.2 untildify: 4.0.0 yauzl: 2.10.0 @@ -6221,17 +6757,7 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.1 - date-fns@2.30.0: - dependencies: - '@babel/runtime': 7.25.0 - - dayjs@1.11.12: {} - - debug@3.2.7(supports-color@5.5.0): - dependencies: - ms: 2.1.3 - optionalDependencies: - supports-color: 5.5.0 + dayjs@1.11.13: {} debug@3.2.7(supports-color@8.1.1): dependencies: @@ -6239,26 +6765,25 @@ snapshots: optionalDependencies: supports-color: 8.1.1 - debug@4.3.6(supports-color@8.1.1): + debug@4.3.6: dependencies: ms: 2.1.2 + + debug@4.3.7(supports-color@5.5.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 5.5.0 + + debug@4.3.7(supports-color@8.1.1): + dependencies: + ms: 2.1.3 optionalDependencies: supports-color: 8.1.1 - decamelize-keys@1.1.1: - dependencies: - decamelize: 1.2.0 - map-obj: 1.0.1 - - decamelize@1.2.0: {} - - decamelize@5.0.1: {} - decimal.js@10.4.3: {} - deep-eql@4.1.4: - dependencies: - type-detect: 4.1.0 + deep-eql@5.0.2: {} deep-equal@2.2.3: dependencies: @@ -6283,6 +6808,8 @@ snapshots: deep-is@0.1.4: {} + deepmerge@4.3.1: {} + define-data-property@1.1.4: dependencies: es-define-property: 1.0.0 @@ -6295,23 +6822,19 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 - defined@1.0.1: {} - - del-cli@5.0.0: + del-cli@6.0.0: dependencies: - del: 7.1.0 - meow: 10.1.5 + del: 8.0.0 + meow: 13.2.0 - del@7.1.0: + del@8.0.0: dependencies: - globby: 13.2.2 - graceful-fs: 4.2.11 + globby: 14.0.2 is-glob: 4.0.3 is-path-cwd: 3.0.0 is-path-inside: 4.0.0 - p-map: 5.5.0 - rimraf: 3.0.2 - slash: 4.0.0 + p-map: 7.0.2 + slash: 5.1.0 delayed-stream@1.0.0: {} @@ -6325,16 +6848,8 @@ snapshots: detect-libc@2.0.3: {} - detective@5.2.1: - dependencies: - acorn-node: 1.8.2 - defined: 1.0.1 - minimist: 1.2.8 - didyoumean@1.2.2: {} - diff-sequences@29.6.3: {} - dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -6355,6 +6870,8 @@ snapshots: dotenv@16.0.3: {} + dotenv@16.4.5: {} + dprint-node@1.0.8: dependencies: detect-libc: 1.0.3 @@ -6368,7 +6885,11 @@ snapshots: jsbn: 0.1.1 safer-buffer: 2.1.2 - electron-to-chromium@1.5.3: {} + electron-to-chromium@1.5.13: {} + + electron-to-chromium@1.5.57: {} + + emoji-regex@10.4.0: {} emoji-regex@8.0.0: {} @@ -6395,9 +6916,7 @@ snapshots: commander: 4.1.1 cross-spawn: 7.0.3 - error-ex@1.3.2: - dependencies: - is-arrayish: 0.2.1 + environment@1.1.0: {} es-abstract@1.23.3: dependencies: @@ -6529,117 +7048,119 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 - esbuild@0.23.0: + esbuild@0.24.0: optionalDependencies: - '@esbuild/aix-ppc64': 0.23.0 - '@esbuild/android-arm': 0.23.0 - '@esbuild/android-arm64': 0.23.0 - '@esbuild/android-x64': 0.23.0 - '@esbuild/darwin-arm64': 0.23.0 - '@esbuild/darwin-x64': 0.23.0 - '@esbuild/freebsd-arm64': 0.23.0 - '@esbuild/freebsd-x64': 0.23.0 - '@esbuild/linux-arm': 0.23.0 - '@esbuild/linux-arm64': 0.23.0 - '@esbuild/linux-ia32': 0.23.0 - '@esbuild/linux-loong64': 0.23.0 - '@esbuild/linux-mips64el': 0.23.0 - '@esbuild/linux-ppc64': 0.23.0 - '@esbuild/linux-riscv64': 0.23.0 - '@esbuild/linux-s390x': 0.23.0 - '@esbuild/linux-x64': 0.23.0 - '@esbuild/netbsd-x64': 0.23.0 - '@esbuild/openbsd-arm64': 0.23.0 - '@esbuild/openbsd-x64': 0.23.0 - '@esbuild/sunos-x64': 0.23.0 - '@esbuild/win32-arm64': 0.23.0 - '@esbuild/win32-ia32': 0.23.0 - '@esbuild/win32-x64': 0.23.0 + '@esbuild/aix-ppc64': 0.24.0 + '@esbuild/android-arm': 0.24.0 + '@esbuild/android-arm64': 0.24.0 + '@esbuild/android-x64': 0.24.0 + '@esbuild/darwin-arm64': 0.24.0 + '@esbuild/darwin-x64': 0.24.0 + '@esbuild/freebsd-arm64': 0.24.0 + '@esbuild/freebsd-x64': 0.24.0 + '@esbuild/linux-arm': 0.24.0 + '@esbuild/linux-arm64': 0.24.0 + '@esbuild/linux-ia32': 0.24.0 + '@esbuild/linux-loong64': 0.24.0 + '@esbuild/linux-mips64el': 0.24.0 + '@esbuild/linux-ppc64': 0.24.0 + '@esbuild/linux-riscv64': 0.24.0 + '@esbuild/linux-s390x': 0.24.0 + '@esbuild/linux-x64': 0.24.0 + '@esbuild/netbsd-x64': 0.24.0 + '@esbuild/openbsd-arm64': 0.24.0 + '@esbuild/openbsd-x64': 0.24.0 + '@esbuild/sunos-x64': 0.24.0 + '@esbuild/win32-arm64': 0.24.0 + '@esbuild/win32-ia32': 0.24.0 + '@esbuild/win32-x64': 0.24.0 - escalade@3.1.2: {} + escalade@3.2.0: {} escape-string-regexp@1.0.5: {} escape-string-regexp@4.0.0: {} - escape-string-regexp@5.0.0: {} - - eslint-config-next@14.2.5(eslint@8.57.0)(typescript@5.5.4): + eslint-config-next@14.2.18(eslint@8.57.1)(typescript@5.6.3): dependencies: - '@next/eslint-plugin-next': 14.2.5 + '@next/eslint-plugin-next': 14.2.18 '@rushstack/eslint-patch': 1.10.4 - '@typescript-eslint/parser': 7.18.0(eslint@8.57.0)(typescript@5.5.4) - eslint: 8.57.0 + '@typescript-eslint/eslint-plugin': 8.15.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.6.3) + eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) - eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) - eslint-plugin-react: 7.35.0(eslint@8.57.0) - eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.1) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) + eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.1) + eslint-plugin-react: 7.35.0(eslint@8.57.1) + eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1) optionalDependencies: - typescript: 5.5.4 + typescript: 5.6.3 transitivePeerDependencies: - eslint-import-resolver-webpack + - eslint-plugin-import-x - supports-color - eslint-config-prettier@9.1.0(eslint@8.57.0): + eslint-config-prettier@9.1.0(eslint@8.57.1): dependencies: - eslint: 8.57.0 + eslint: 8.57.1 - eslint-config-turbo@2.0.9(eslint@8.57.0): + eslint-config-turbo@2.1.0(eslint@8.57.1): dependencies: - eslint: 8.57.0 - eslint-plugin-turbo: 2.0.9(eslint@8.57.0) + eslint: 8.57.1 + eslint-plugin-turbo: 2.1.0(eslint@8.57.1) eslint-import-resolver-node@0.3.9: dependencies: - debug: 3.2.7(supports-color@5.5.0) - is-core-module: 2.15.0 + debug: 3.2.7(supports-color@8.1.1) + is-core-module: 2.15.1 resolve: 1.22.8 transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0): + eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.1): dependencies: - debug: 4.3.6(supports-color@8.1.1) + '@nolyfill/is-core-module': 1.0.39 + debug: 4.3.7(supports-color@5.5.0) enhanced-resolve: 5.17.1 - eslint: 8.57.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint: 8.57.1 + eslint-module-utils: 2.8.2(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) fast-glob: 3.3.2 - get-tsconfig: 4.7.6 - is-core-module: 2.15.0 + get-tsconfig: 4.8.0 + is-bun-module: 1.1.0 is-glob: 4.0.3 + optionalDependencies: + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.8.2(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) optionalDependencies: - '@typescript-eslint/parser': 7.18.0(eslint@8.57.0)(typescript@5.5.4) - eslint: 8.57.0 + '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.6.3) + eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 array.prototype.flat: 1.3.2 array.prototype.flatmap: 1.3.2 - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) doctrine: 2.1.0 - eslint: 8.57.0 + eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.2(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) hasown: 2.0.2 - is-core-module: 2.15.0 + is-core-module: 2.15.1 is-glob: 4.0.3 minimatch: 3.1.2 object.fromentries: 2.0.8 @@ -6648,13 +7169,13 @@ snapshots: semver: 6.3.1 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 7.18.0(eslint@8.57.0)(typescript@5.5.4) + '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.6.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.9.0(eslint@8.57.0): + eslint-plugin-jsx-a11y@6.9.0(eslint@8.57.1): dependencies: aria-query: 5.1.3 array-includes: 3.1.8 @@ -6665,7 +7186,7 @@ snapshots: damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 es-iterator-helpers: 1.0.19 - eslint: 8.57.0 + eslint: 8.57.1 hasown: 2.0.2 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -6674,11 +7195,11 @@ snapshots: safe-regex-test: 1.0.3 string.prototype.includes: 2.0.0 - eslint-plugin-react-hooks@4.6.2(eslint@8.57.0): + eslint-plugin-react-hooks@4.6.2(eslint@8.57.1): dependencies: - eslint: 8.57.0 + eslint: 8.57.1 - eslint-plugin-react@7.35.0(eslint@8.57.0): + eslint-plugin-react@7.35.0(eslint@8.57.1): dependencies: array-includes: 3.1.8 array.prototype.findlast: 1.2.5 @@ -6686,7 +7207,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.0.19 - eslint: 8.57.0 + eslint: 8.57.1 estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -6700,32 +7221,41 @@ snapshots: string.prototype.matchall: 4.0.11 string.prototype.repeat: 1.0.0 - eslint-plugin-turbo@2.0.9(eslint@8.57.0): + eslint-plugin-turbo@2.1.0(eslint@8.57.1): dependencies: dotenv: 16.0.3 - eslint: 8.57.0 + eslint: 8.57.1 + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 eslint-scope@7.2.2: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 + eslint-visitor-keys@2.1.0: {} + eslint-visitor-keys@3.4.3: {} - eslint@8.57.0: + eslint-visitor-keys@4.2.0: {} + + eslint@8.57.1: dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - '@eslint-community/regexpp': 4.11.0 + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.1) + '@eslint-community/regexpp': 4.11.1 '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.57.0 - '@humanwhocodes/config-array': 0.11.14 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 '@humanwhocodes/module-importer': 1.0.1 '@nodelib/fs.walk': 1.2.8 '@ungap/structured-clone': 1.2.0 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7(supports-color@5.5.0) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -6739,7 +7269,7 @@ snapshots: glob-parent: 6.0.2 globals: 13.24.0 graphemer: 1.4.0 - ignore: 5.3.1 + ignore: 5.3.2 imurmurhash: 0.1.4 is-glob: 4.0.3 is-path-inside: 3.0.3 @@ -6771,11 +7301,13 @@ snapshots: dependencies: estraverse: 5.3.0 + estraverse@4.3.0: {} + estraverse@5.3.0: {} estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 esutils@2.0.3: {} @@ -6791,9 +7323,11 @@ snapshots: eventemitter2@6.4.7: {} + eventemitter3@5.0.1: {} + execa@4.1.0: dependencies: - cross-spawn: 7.0.3 + cross-spawn: 7.0.5 get-stream: 5.2.0 human-signals: 1.1.1 is-stream: 2.0.1 @@ -6815,18 +7349,6 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 2.0.0 - execa@6.1.0: - dependencies: - cross-spawn: 7.0.3 - get-stream: 6.0.1 - human-signals: 3.0.1 - is-stream: 3.0.0 - merge-stream: 2.0.0 - npm-run-path: 5.3.0 - onetime: 6.0.0 - signal-exit: 3.0.7 - strip-final-newline: 3.0.0 - execa@8.0.1: dependencies: cross-spawn: 7.0.3 @@ -6843,6 +7365,8 @@ snapshots: dependencies: pify: 2.3.0 + expect-type@1.1.0: {} + extend@3.0.2: {} extendable-error@0.1.7: {} @@ -6855,7 +7379,7 @@ snapshots: extract-zip@2.0.1(supports-color@8.1.1): dependencies: - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7(supports-color@8.1.1) get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -6873,7 +7397,7 @@ snapshots: '@nodelib/fs.walk': 1.2.8 glob-parent: 5.1.2 merge2: 1.4.1 - micromatch: 4.0.7 + micromatch: 4.0.8 fast-json-stable-stringify@2.1.0: {} @@ -6887,6 +7411,12 @@ snapshots: dependencies: pend: 1.2.0 + fdir@6.4.2(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + fflate@0.8.2: {} + figures@3.2.0: dependencies: escape-string-regexp: 1.0.5 @@ -6909,11 +7439,6 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 - find-yarn-workspace-root2@1.2.16: - dependencies: - micromatch: 4.0.7 - pkg-dir: 4.2.0 - flat-cache@3.2.0: dependencies: flatted: 3.3.1 @@ -6922,27 +7447,21 @@ snapshots: flatted@3.3.1: {} - follow-redirects@1.15.6(debug@4.3.6): + follow-redirects@1.15.6(debug@4.3.7): optionalDependencies: - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7(supports-color@5.5.0) for-each@0.3.3: dependencies: is-callable: 1.2.7 - foreground-child@3.2.1: + foreground-child@3.3.0: dependencies: - cross-spawn: 7.0.3 + cross-spawn: 7.0.5 signal-exit: 4.1.0 forever-agent@0.6.1: {} - form-data@2.3.3: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - mime-types: 2.1.35 - form-data@4.0.0: dependencies: asynckit: 0.4.0 @@ -6978,6 +7497,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -7008,7 +7530,7 @@ snapshots: get-caller-file@2.0.5: {} - get-func-name@2.0.2: {} + get-east-asian-width@1.2.0: {} get-intrinsic@1.2.4: dependencies: @@ -7032,13 +7554,13 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.2.4 - get-tsconfig@4.7.6: + get-tsconfig@4.8.0: dependencies: resolve-pkg-maps: 1.0.0 getos@3.2.1: dependencies: - async: 3.2.5 + async: 3.2.6 getpass@0.1.7: dependencies: @@ -7054,7 +7576,7 @@ snapshots: glob@10.3.10: dependencies: - foreground-child: 3.2.1 + foreground-child: 3.3.0 jackspeak: 2.3.6 minimatch: 9.0.5 minipass: 7.1.2 @@ -7062,11 +7584,11 @@ snapshots: glob@10.4.5: dependencies: - foreground-child: 3.2.1 + foreground-child: 3.3.0 jackspeak: 3.4.3 minimatch: 9.0.5 minipass: 7.1.2 - package-json-from-dist: 1.0.0 + package-json-from-dist: 1.0.1 path-scurry: 1.11.1 glob@7.2.3: @@ -7098,17 +7620,18 @@ snapshots: array-union: 2.1.0 dir-glob: 3.0.1 fast-glob: 3.3.2 - ignore: 5.3.1 + ignore: 5.3.2 merge2: 1.4.1 slash: 3.0.0 - globby@13.2.2: + globby@14.0.2: dependencies: - dir-glob: 3.0.1 + '@sindresorhus/merge-streams': 2.3.0 fast-glob: 3.3.2 - ignore: 5.3.1 - merge2: 1.4.1 - slash: 4.0.0 + ignore: 5.3.2 + path-type: 5.0.0 + slash: 5.1.0 + unicorn-magic: 0.1.0 globrex@0.1.2: {} @@ -7120,15 +7643,13 @@ snapshots: graphemer@1.4.0: {} - grpc-tools@1.11.3: + grpc-tools@1.12.4: dependencies: '@mapbox/node-pre-gyp': 1.0.11 transitivePeerDependencies: - encoding - supports-color - hard-rejection@2.1.0: {} - has-bigints@1.0.2: {} has-flag@3.0.0: {} @@ -7153,10 +7674,6 @@ snapshots: dependencies: function-bind: 1.1.2 - hosted-git-info@4.1.0: - dependencies: - lru-cache: 6.0.0 - html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 @@ -7164,11 +7681,11 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.1 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7(supports-color@5.5.0) transitivePeerDependencies: - supports-color - http-signature@1.3.6: + http-signature@1.4.0: dependencies: assert-plus: 1.0.0 jsprim: 2.0.2 @@ -7177,14 +7694,14 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7(supports-color@5.5.0) transitivePeerDependencies: - supports-color https-proxy-agent@7.0.5: dependencies: agent-base: 7.1.1 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -7194,8 +7711,6 @@ snapshots: human-signals@2.1.0: {} - human-signals@3.0.1: {} - human-signals@5.0.0: {} iconv-lite@0.4.24: @@ -7210,9 +7725,9 @@ snapshots: ignore-by-default@1.0.1: {} - ignore@5.3.1: {} + ignore@5.3.2: {} - immutable@4.3.7: {} + immutable@5.0.2: {} import-fresh@3.3.0: dependencies: @@ -7223,8 +7738,6 @@ snapshots: indent-string@4.0.0: {} - indent-string@5.0.0: {} - inflight@1.0.6: dependencies: once: 1.4.0 @@ -7240,6 +7753,13 @@ snapshots: hasown: 2.0.2 side-channel: 1.0.6 + intl-messageformat@10.7.7: + dependencies: + '@formatjs/ecma402-abstract': 2.2.4 + '@formatjs/fast-memoize': 2.2.3 + '@formatjs/icu-messageformat-parser': 2.9.4 + tslib: 2.8.1 + is-arguments@1.1.1: dependencies: call-bind: 1.0.7 @@ -7250,7 +7770,8 @@ snapshots: call-bind: 1.0.7 get-intrinsic: 1.2.4 - is-arrayish@0.2.1: {} + is-arrayish@0.3.2: + optional: true is-async-function@2.0.0: dependencies: @@ -7269,13 +7790,13 @@ snapshots: call-bind: 1.0.7 has-tostringtag: 1.0.2 + is-bun-module@1.1.0: + dependencies: + semver: 7.6.3 + is-callable@1.2.7: {} - is-ci@3.0.1: - dependencies: - ci-info: 3.9.0 - - is-core-module@2.15.0: + is-core-module@2.15.1: dependencies: hasown: 2.0.2 @@ -7297,6 +7818,10 @@ snapshots: is-fullwidth-code-point@4.0.0: {} + is-fullwidth-code-point@5.0.0: + dependencies: + get-east-asian-width: 1.2.0 + is-generator-function@1.0.10: dependencies: has-tostringtag: 1.0.2 @@ -7326,8 +7851,6 @@ snapshots: is-path-inside@4.0.0: {} - is-plain-obj@1.1.0: {} - is-potential-custom-element-name@1.0.1: {} is-regex@1.1.4: @@ -7404,6 +7927,8 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jiti@1.21.6: {} + joi@17.13.3: dependencies: '@hapi/hoek': 9.3.0 @@ -7412,14 +7937,12 @@ snapshots: '@sideway/formula': 3.0.1 '@sideway/pinpoint': 2.0.0 - jose@5.6.3: {} + jose@5.8.0: {} joycon@3.1.1: {} js-tokens@4.0.0: {} - js-tokens@9.0.0: {} - js-yaml@3.14.1: dependencies: argparse: 1.0.10 @@ -7431,9 +7954,9 @@ snapshots: jsbn@0.1.1: {} - jsdom@24.1.1: + jsdom@25.0.1: dependencies: - cssstyle: 4.0.1 + cssstyle: 4.1.0 data-urls: 5.0.0 decimal.js: 10.4.3 form-data: 4.0.0 @@ -7446,7 +7969,7 @@ snapshots: rrweb-cssom: 0.7.1 saxes: 6.0.0 symbol-tree: 3.2.4 - tough-cookie: 4.1.4 + tough-cookie: 5.0.0 w3c-xmlserializer: 5.0.0 webidl-conversions: 7.0.0 whatwg-encoding: 3.1.1 @@ -7459,12 +7982,10 @@ snapshots: - supports-color - utf-8-validate - jsesc@2.5.2: {} + jsesc@3.0.2: {} json-buffer@3.0.1: {} - json-parse-even-better-errors@2.3.1: {} - json-schema-traverse@0.4.1: {} json-schema@0.4.0: {} @@ -7507,8 +8028,6 @@ snapshots: dependencies: json-buffer: 3.0.1 - kind-of@6.0.3: {} - language-subtag-registry@0.3.23: {} language-tags@1.0.9: @@ -7522,31 +8041,25 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - lilconfig@2.0.5: {} - lilconfig@2.1.0: {} lilconfig@3.1.2: {} lines-and-columns@1.2.4: {} - lint-staged@13.0.3(enquirer@2.4.1): + lint-staged@15.2.10: dependencies: - cli-truncate: 3.1.0 - colorette: 2.0.20 - commander: 9.5.0 - debug: 4.3.6(supports-color@8.1.1) - execa: 6.1.0 - lilconfig: 2.0.5 - listr2: 4.0.5(enquirer@2.4.1) - micromatch: 4.0.7 - normalize-path: 3.0.0 - object-inspect: 1.13.2 + chalk: 5.3.0 + commander: 12.1.0 + debug: 4.3.7(supports-color@5.5.0) + execa: 8.0.1 + lilconfig: 3.1.2 + listr2: 8.2.4 + micromatch: 4.0.8 pidtree: 0.6.0 string-argv: 0.3.2 yaml: 2.5.0 transitivePeerDependencies: - - enquirer - supports-color listr2@3.14.0(enquirer@2.4.1): @@ -7562,33 +8075,17 @@ snapshots: optionalDependencies: enquirer: 2.4.1 - listr2@4.0.5(enquirer@2.4.1): + listr2@8.2.4: dependencies: - cli-truncate: 2.1.0 + cli-truncate: 4.0.0 colorette: 2.0.20 - log-update: 4.0.0 - p-map: 4.0.0 + eventemitter3: 5.0.1 + log-update: 6.1.0 rfdc: 1.4.1 - rxjs: 7.8.1 - through: 2.3.8 - wrap-ansi: 7.0.0 - optionalDependencies: - enquirer: 2.4.1 + wrap-ansi: 9.0.0 load-tsconfig@0.2.5: {} - load-yaml-file@0.2.0: - dependencies: - graceful-fs: 4.2.11 - js-yaml: 3.14.1 - pify: 4.0.1 - strip-bom: 3.0.0 - - local-pkg@0.5.0: - dependencies: - mlly: 1.7.1 - pkg-types: 1.1.3 - locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -7621,15 +8118,21 @@ snapshots: slice-ansi: 4.0.0 wrap-ansi: 6.2.0 + log-update@6.1.0: + dependencies: + ansi-escapes: 7.0.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.0 + strip-ansi: 7.1.0 + wrap-ansi: 9.0.0 + long@5.2.3: {} loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 - loupe@2.3.7: - dependencies: - get-func-name: 2.0.2 + loupe@3.1.2: {} lru-cache@10.4.3: {} @@ -7642,51 +8145,32 @@ snapshots: dependencies: yallist: 3.1.1 - lru-cache@6.0.0: - dependencies: - yallist: 4.0.0 - lz-string@1.5.0: {} - magic-string@0.30.11: + magic-string@0.30.12: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 - make-dir-cli@3.0.0: + make-dir-cli@4.0.0: dependencies: - make-dir: 3.1.0 - meow: 10.1.5 + make-dir: 5.0.0 + meow: 13.2.0 make-dir@3.1.0: dependencies: semver: 6.3.1 - map-obj@1.0.1: {} - - map-obj@4.3.0: {} + make-dir@5.0.0: {} map-stream@0.1.0: {} - meow@10.1.5: - dependencies: - '@types/minimist': 1.2.5 - camelcase-keys: 7.0.2 - decamelize: 5.0.1 - decamelize-keys: 1.1.1 - hard-rejection: 2.1.0 - minimist-options: 4.1.0 - normalize-package-data: 3.0.3 - read-pkg-up: 8.0.0 - redent: 4.0.0 - trim-newlines: 4.1.1 - type-fest: 1.4.0 - yargs-parser: 20.2.9 + meow@13.2.0: {} merge-stream@2.0.0: {} merge2@1.4.1: {} - micromatch@4.0.7: + micromatch@4.0.8: dependencies: braces: 3.0.3 picomatch: 2.3.1 @@ -7701,6 +8185,8 @@ snapshots: mimic-fn@4.0.0: {} + mimic-function@5.0.1: {} + min-indent@1.0.1: {} mini-svg-data-uri@1.4.4: {} @@ -7713,12 +8199,6 @@ snapshots: dependencies: brace-expansion: 2.0.1 - minimist-options@4.1.0: - dependencies: - arrify: 1.0.1 - is-plain-obj: 1.1.0 - kind-of: 6.0.3 - minimist@1.2.8: {} minipass@3.3.6: @@ -7736,13 +8216,6 @@ snapshots: mkdirp@1.0.4: {} - mlly@1.7.1: - dependencies: - acorn: 8.12.1 - pathe: 1.1.2 - pkg-types: 1.1.3 - ufo: 1.5.4 - moment@2.30.1: {} mri@1.2.0: {} @@ -7761,60 +8234,45 @@ snapshots: natural-compare@1.4.0: {} - next-themes@0.2.1(next@14.2.3(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): - dependencies: - next: 14.2.3(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + negotiator@1.0.0: {} - next@14.2.3(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8): + next-intl@3.25.1(next@15.0.4-canary.23(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(sass@1.80.7))(react@19.0.0-rc-66855b96-20241106): dependencies: - '@next/env': 14.2.3 - '@swc/helpers': 0.5.5 - busboy: 1.6.0 - caniuse-lite: 1.0.30001644 - graceful-fs: 4.2.11 - postcss: 8.4.31 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.1(@babel/core@7.25.2)(react@18.3.1) - optionalDependencies: - '@next/swc-darwin-arm64': 14.2.3 - '@next/swc-darwin-x64': 14.2.3 - '@next/swc-linux-arm64-gnu': 14.2.3 - '@next/swc-linux-arm64-musl': 14.2.3 - '@next/swc-linux-x64-gnu': 14.2.3 - '@next/swc-linux-x64-musl': 14.2.3 - '@next/swc-win32-arm64-msvc': 14.2.3 - '@next/swc-win32-ia32-msvc': 14.2.3 - '@next/swc-win32-x64-msvc': 14.2.3 - sass: 1.77.8 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros + '@formatjs/intl-localematcher': 0.5.4 + negotiator: 1.0.0 + next: 15.0.4-canary.23(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(sass@1.80.7) + react: 19.0.0-rc-66855b96-20241106 + use-intl: 3.25.1(react@19.0.0-rc-66855b96-20241106) - next@14.2.5(@babel/core@7.25.2)(react-dom@18.3.1(react@18.2.0))(react@18.2.0)(sass@1.77.8): + next-themes@0.2.1(next@15.0.4-canary.23(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(sass@1.80.7))(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106): dependencies: - '@next/env': 14.2.5 - '@swc/helpers': 0.5.5 + next: 15.0.4-canary.23(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(sass@1.80.7) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + + next@15.0.4-canary.23(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(sass@1.80.7): + dependencies: + '@next/env': 15.0.4-canary.23 + '@swc/counter': 0.1.3 + '@swc/helpers': 0.5.13 busboy: 1.6.0 - caniuse-lite: 1.0.30001644 - graceful-fs: 4.2.11 + caniuse-lite: 1.0.30001680 postcss: 8.4.31 - react: 18.2.0 - react-dom: 18.3.1(react@18.2.0) - styled-jsx: 5.1.1(@babel/core@7.25.2)(react@18.2.0) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + styled-jsx: 5.1.6(@babel/core@7.26.0)(react@19.0.0-rc-66855b96-20241106) optionalDependencies: - '@next/swc-darwin-arm64': 14.2.5 - '@next/swc-darwin-x64': 14.2.5 - '@next/swc-linux-arm64-gnu': 14.2.5 - '@next/swc-linux-arm64-musl': 14.2.5 - '@next/swc-linux-x64-gnu': 14.2.5 - '@next/swc-linux-x64-musl': 14.2.5 - '@next/swc-win32-arm64-msvc': 14.2.5 - '@next/swc-win32-ia32-msvc': 14.2.5 - '@next/swc-win32-x64-msvc': 14.2.5 - sass: 1.77.8 + '@next/swc-darwin-arm64': 15.0.4-canary.23 + '@next/swc-darwin-x64': 15.0.4-canary.23 + '@next/swc-linux-arm64-gnu': 15.0.4-canary.23 + '@next/swc-linux-arm64-musl': 15.0.4-canary.23 + '@next/swc-linux-x64-gnu': 15.0.4-canary.23 + '@next/swc-linux-x64-musl': 15.0.4-canary.23 + '@next/swc-win32-arm64-msvc': 15.0.4-canary.23 + '@next/swc-win32-x64-msvc': 15.0.4-canary.23 + '@playwright/test': 1.48.2 + sass: 1.80.7 + sharp: 0.33.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -7829,21 +8287,24 @@ snapshots: abort-controller-x: 0.4.3 nice-grpc-common: 2.0.2 + node-addon-api@7.1.1: + optional: true + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 node-releases@2.0.18: {} - nodemon@2.0.22: + nodemon@3.1.7: dependencies: chokidar: 3.6.0 - debug: 3.2.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@5.5.0) ignore-by-default: 1.0.1 minimatch: 3.1.2 pstree.remy: 1.1.8 - semver: 5.7.2 - simple-update-notifier: 1.1.0 + semver: 7.6.3 + simple-update-notifier: 2.0.0 supports-color: 5.5.0 touch: 3.1.1 undefsafe: 2.0.5 @@ -7852,13 +8313,6 @@ snapshots: dependencies: abbrev: 1.1.1 - normalize-package-data@3.0.3: - dependencies: - hosted-git-info: 4.1.0 - is-core-module: 2.15.0 - semver: 7.6.3 - validate-npm-package-license: 3.0.4 - normalize-path@3.0.0: {} normalize-range@0.1.2: {} @@ -7937,6 +8391,10 @@ snapshots: dependencies: mimic-fn: 4.0.0 + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -7964,10 +8422,6 @@ snapshots: dependencies: yocto-queue: 0.1.0 - p-limit@5.0.0: - dependencies: - yocto-queue: 1.1.1 - p-locate@4.1.0: dependencies: p-limit: 2.3.0 @@ -7982,25 +8436,18 @@ snapshots: dependencies: aggregate-error: 3.1.0 - p-map@5.5.0: - dependencies: - aggregate-error: 4.0.1 + p-map@7.0.2: {} p-try@2.2.0: {} - package-json-from-dist@1.0.0: {} + package-json-from-dist@1.0.1: {} + + package-manager-detector@0.2.2: {} parent-module@1.0.1: dependencies: callsites: 3.1.0 - parse-json@5.2.0: - dependencies: - '@babel/code-frame': 7.24.7 - error-ex: 1.3.2 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 - parse5@7.1.2: dependencies: entities: 4.5.0 @@ -8022,9 +8469,11 @@ snapshots: path-type@4.0.0: {} + path-type@5.0.0: {} + pathe@1.1.2: {} - pathval@1.1.1: {} + pathval@2.0.0: {} pause-stream@0.0.11: dependencies: @@ -8036,8 +8485,12 @@ snapshots: picocolors@1.0.1: {} + picocolors@1.1.1: {} + picomatch@2.3.1: {} + picomatch@4.0.2: {} + pidtree@0.6.0: {} pify@2.3.0: {} @@ -8046,115 +8499,79 @@ snapshots: pirates@4.0.6: {} - pkg-dir@4.2.0: - dependencies: - find-up: 4.1.0 + playwright-core@1.48.2: {} - pkg-types@1.1.3: + playwright@1.48.2: dependencies: - confbox: 0.1.7 - mlly: 1.7.1 - pathe: 1.1.2 + playwright-core: 1.48.2 + optionalDependencies: + fsevents: 2.3.2 possible-typed-array-names@1.0.0: {} - postcss-import@14.1.0(postcss@8.4.21): + postcss-import@15.1.0(postcss@8.4.49): dependencies: - postcss: 8.4.21 + postcss: 8.4.49 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.8 - postcss-import@14.1.0(postcss@8.4.40): - dependencies: - postcss: 8.4.40 - postcss-value-parser: 4.2.0 - read-cache: 1.0.0 - resolve: 1.22.8 - - postcss-js@4.0.1(postcss@8.4.21): + postcss-js@4.0.1(postcss@8.4.49): dependencies: camelcase-css: 2.0.1 - postcss: 8.4.21 + postcss: 8.4.49 - postcss-js@4.0.1(postcss@8.4.40): + postcss-load-config@4.0.2(postcss@8.4.49): dependencies: - camelcase-css: 2.0.1 - postcss: 8.4.40 - - postcss-load-config@3.1.4(postcss@8.4.21): - dependencies: - lilconfig: 2.1.0 - yaml: 1.10.2 + lilconfig: 3.1.2 + yaml: 2.5.0 optionalDependencies: - postcss: 8.4.21 + postcss: 8.4.49 - postcss-load-config@3.1.4(postcss@8.4.40): - dependencies: - lilconfig: 2.1.0 - yaml: 1.10.2 - optionalDependencies: - postcss: 8.4.40 - - postcss-load-config@6.0.1(postcss@8.4.40)(yaml@2.5.0): + postcss-load-config@6.0.1(jiti@1.21.6)(postcss@8.4.49)(yaml@2.5.0): dependencies: lilconfig: 3.1.2 optionalDependencies: - postcss: 8.4.40 + jiti: 1.21.6 + postcss: 8.4.49 yaml: 2.5.0 - postcss-nested@6.0.0(postcss@8.4.21): + postcss-nested@6.2.0(postcss@8.4.49): dependencies: - postcss: 8.4.21 - postcss-selector-parser: 6.1.1 + postcss: 8.4.49 + postcss-selector-parser: 6.1.2 - postcss-nested@6.0.0(postcss@8.4.40): - dependencies: - postcss: 8.4.40 - postcss-selector-parser: 6.1.1 - - postcss-selector-parser@6.1.1: + postcss-selector-parser@6.1.2: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 postcss-value-parser@4.2.0: {} - postcss@8.4.21: - dependencies: - nanoid: 3.3.7 - picocolors: 1.0.1 - source-map-js: 1.2.0 - postcss@8.4.31: dependencies: nanoid: 3.3.7 - picocolors: 1.0.1 - source-map-js: 1.2.0 + picocolors: 1.1.1 + source-map-js: 1.2.1 - postcss@8.4.40: + postcss@8.4.49: dependencies: nanoid: 3.3.7 - picocolors: 1.0.1 - source-map-js: 1.2.0 - - preferred-pm@3.1.4: - dependencies: - find-up: 5.0.0 - find-yarn-workspace-root2: 1.2.16 - path-exists: 4.0.0 - which-pm: 2.2.0 + picocolors: 1.1.1 + source-map-js: 1.2.1 prelude-ls@1.2.1: {} - prettier-plugin-organize-imports@4.0.0(prettier@3.3.3)(typescript@5.5.4): + prettier-plugin-organize-imports@4.1.0(prettier@3.3.3)(typescript@5.6.3): dependencies: prettier: 3.3.3 - typescript: 5.5.4 + typescript: 5.6.3 - prettier-plugin-tailwindcss@0.1.13(prettier@3.3.3): + prettier-plugin-tailwindcss@0.6.8(prettier-plugin-organize-imports@4.1.0(prettier@3.3.3)(typescript@5.6.3))(prettier@3.3.3): dependencies: prettier: 3.3.3 + optionalDependencies: + prettier-plugin-organize-imports: 4.1.0(prettier@3.3.3)(typescript@5.6.3) prettier@2.8.8: {} @@ -8168,12 +8585,6 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 - pretty-format@29.7.0: - dependencies: - '@jest/schemas': 29.6.3 - ansi-styles: 5.2.0 - react-is: 18.3.1 - process@0.11.10: {} prop-types@15.8.1: @@ -8182,7 +8593,7 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 - protobufjs@7.3.2: + protobufjs@7.4.0: dependencies: '@protobufjs/aspromise': 1.1.2 '@protobufjs/base64': 1.1.2 @@ -8194,7 +8605,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 20.14.13 + '@types/node': 22.9.0 long: 5.2.3 proxy-from-env@1.0.0: {} @@ -8207,8 +8618,6 @@ snapshots: pseudomap@1.0.2: {} - psl@1.9.0: {} - pstree.remy@1.1.8: {} pump@3.0.0: @@ -8218,69 +8627,37 @@ snapshots: punycode@2.3.1: {} - qrcode.react@3.1.0(react@18.3.1): + qrcode.react@3.1.0(react@19.0.0-rc-66855b96-20241106): dependencies: - react: 18.3.1 + react: 19.0.0-rc-66855b96-20241106 - qs@6.10.4: + qs@6.13.0: dependencies: side-channel: 1.0.6 - querystringify@2.2.0: {} - queue-microtask@1.2.3: {} - quick-lru@5.1.1: {} - - react-dom@18.3.1(react@18.2.0): + react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106): dependencies: - loose-envify: 1.4.0 - react: 18.2.0 - scheduler: 0.23.2 + react: 19.0.0-rc-66855b96-20241106 + scheduler: 0.25.0-rc-66855b96-20241106 - react-dom@18.3.1(react@18.3.1): + react-hook-form@7.39.5(react@19.0.0-rc-66855b96-20241106): dependencies: - loose-envify: 1.4.0 - react: 18.3.1 - scheduler: 0.23.2 - - react-hook-form@7.39.5(react@18.3.1): - dependencies: - react: 18.3.1 + react: 19.0.0-rc-66855b96-20241106 react-is@16.13.1: {} react-is@17.0.2: {} - react-is@18.3.1: {} - react-refresh@0.14.2: {} - react@18.2.0: - dependencies: - loose-envify: 1.4.0 - - react@18.3.1: - dependencies: - loose-envify: 1.4.0 + react@19.0.0-rc-66855b96-20241106: {} read-cache@1.0.0: dependencies: pify: 2.3.0 - read-pkg-up@8.0.0: - dependencies: - find-up: 5.0.0 - read-pkg: 6.0.0 - type-fest: 1.4.0 - - read-pkg@6.0.0: - dependencies: - '@types/normalize-package-data': 2.4.4 - normalize-package-data: 3.0.3 - parse-json: 5.2.0 - type-fest: 1.4.0 - read-yaml-file@1.1.0: dependencies: graceful-fs: 4.2.11 @@ -8298,16 +8675,13 @@ snapshots: dependencies: picomatch: 2.3.1 + readdirp@4.0.2: {} + redent@3.0.0: dependencies: indent-string: 4.0.0 strip-indent: 3.0.0 - redent@4.0.0: - dependencies: - indent-string: 5.0.0 - strip-indent: 4.0.0 - reflect.getprototypeof@1.0.6: dependencies: call-bind: 1.0.7 @@ -8333,8 +8707,6 @@ snapshots: require-directory@2.1.1: {} - requires-port@1.0.0: {} - resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -8343,13 +8715,13 @@ snapshots: resolve@1.22.8: dependencies: - is-core-module: 2.15.0 + is-core-module: 2.15.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 resolve@2.0.0-next.5: dependencies: - is-core-module: 2.15.0 + is-core-module: 2.15.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 @@ -8358,6 +8730,11 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + reusify@1.0.4: {} rfdc@1.4.1: {} @@ -8366,30 +8743,30 @@ snapshots: dependencies: glob: 7.2.3 - rollup@4.19.1: + rollup@4.25.0: dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.19.1 - '@rollup/rollup-android-arm64': 4.19.1 - '@rollup/rollup-darwin-arm64': 4.19.1 - '@rollup/rollup-darwin-x64': 4.19.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.19.1 - '@rollup/rollup-linux-arm-musleabihf': 4.19.1 - '@rollup/rollup-linux-arm64-gnu': 4.19.1 - '@rollup/rollup-linux-arm64-musl': 4.19.1 - '@rollup/rollup-linux-powerpc64le-gnu': 4.19.1 - '@rollup/rollup-linux-riscv64-gnu': 4.19.1 - '@rollup/rollup-linux-s390x-gnu': 4.19.1 - '@rollup/rollup-linux-x64-gnu': 4.19.1 - '@rollup/rollup-linux-x64-musl': 4.19.1 - '@rollup/rollup-win32-arm64-msvc': 4.19.1 - '@rollup/rollup-win32-ia32-msvc': 4.19.1 - '@rollup/rollup-win32-x64-msvc': 4.19.1 + '@rollup/rollup-android-arm-eabi': 4.25.0 + '@rollup/rollup-android-arm64': 4.25.0 + '@rollup/rollup-darwin-arm64': 4.25.0 + '@rollup/rollup-darwin-x64': 4.25.0 + '@rollup/rollup-freebsd-arm64': 4.25.0 + '@rollup/rollup-freebsd-x64': 4.25.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.25.0 + '@rollup/rollup-linux-arm-musleabihf': 4.25.0 + '@rollup/rollup-linux-arm64-gnu': 4.25.0 + '@rollup/rollup-linux-arm64-musl': 4.25.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.25.0 + '@rollup/rollup-linux-riscv64-gnu': 4.25.0 + '@rollup/rollup-linux-s390x-gnu': 4.25.0 + '@rollup/rollup-linux-x64-gnu': 4.25.0 + '@rollup/rollup-linux-x64-musl': 4.25.0 + '@rollup/rollup-win32-arm64-msvc': 4.25.0 + '@rollup/rollup-win32-ia32-msvc': 4.25.0 + '@rollup/rollup-win32-x64-msvc': 4.25.0 fsevents: 2.3.3 - rrweb-cssom@0.6.0: {} - rrweb-cssom@0.7.1: {} run-parallel@1.2.0: @@ -8398,7 +8775,7 @@ snapshots: rxjs@7.8.1: dependencies: - tslib: 2.6.3 + tslib: 2.7.0 safe-array-concat@1.1.2: dependencies: @@ -8417,26 +8794,22 @@ snapshots: safer-buffer@2.1.2: {} - sass@1.77.8: + sass@1.80.7: dependencies: - chokidar: 3.6.0 - immutable: 4.3.7 - source-map-js: 1.2.0 + chokidar: 4.0.1 + immutable: 5.0.2 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.0 saxes@6.0.0: dependencies: xmlchars: 2.2.0 - scheduler@0.23.2: - dependencies: - loose-envify: 1.4.0 - - semver@5.7.2: {} + scheduler@0.25.0-rc-66855b96-20241106: {} semver@6.3.1: {} - semver@7.0.0: {} - semver@7.6.3: {} server-only@0.0.1: {} @@ -8459,6 +8832,33 @@ snapshots: functions-have-names: 1.2.3 has-property-descriptors: 1.0.2 + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.0.3 + semver: 7.6.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + optional: true + shebang-command@1.2.0: dependencies: shebang-regex: 1.0.0 @@ -8486,13 +8886,18 @@ snapshots: signal-exit@4.1.0: {} - simple-update-notifier@1.1.0: + simple-swizzle@0.2.2: dependencies: - semver: 7.0.0 + is-arrayish: 0.3.2 + optional: true + + simple-update-notifier@2.0.0: + dependencies: + semver: 7.6.3 slash@3.0.0: {} - slash@4.0.0: {} + slash@5.1.0: {} slice-ansi@3.0.0: dependencies: @@ -8511,33 +8916,22 @@ snapshots: ansi-styles: 6.2.1 is-fullwidth-code-point: 4.0.0 - source-map-js@1.2.0: {} + slice-ansi@7.1.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 5.0.0 + + source-map-js@1.2.1: {} source-map@0.8.0-beta.0: dependencies: whatwg-url: 7.1.0 - spawn-command@0.0.2: {} - spawndamnit@2.0.0: dependencies: cross-spawn: 5.1.0 signal-exit: 3.0.7 - spdx-correct@3.2.0: - dependencies: - spdx-expression-parse: 3.0.1 - spdx-license-ids: 3.0.18 - - spdx-exceptions@2.5.0: {} - - spdx-expression-parse@3.0.1: - dependencies: - spdx-exceptions: 2.5.0 - spdx-license-ids: 3.0.18 - - spdx-license-ids@3.0.18: {} - split@0.3.3: dependencies: through: 2.3.8 @@ -8558,20 +8952,20 @@ snapshots: stackback@0.0.2: {} - start-server-and-test@2.0.5: + start-server-and-test@2.0.8: dependencies: arg: 5.0.2 bluebird: 3.7.2 check-more-types: 2.24.0 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7(supports-color@5.5.0) execa: 5.1.1 lazy-ass: 1.6.0 ps-tree: 1.2.0 - wait-on: 7.2.0(debug@4.3.6) + wait-on: 8.0.1(debug@4.3.7) transitivePeerDependencies: - supports-color - std-env@3.7.0: {} + std-env@3.8.0: {} stop-iteration-iterator@1.0.0: dependencies: @@ -8597,6 +8991,12 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.0 + string-width@7.2.0: + dependencies: + emoji-regex: 10.4.0 + get-east-asian-width: 1.2.0 + strip-ansi: 7.1.0 + string.prototype.includes@2.0.0: dependencies: define-properties: 1.2.1 @@ -8651,7 +9051,7 @@ snapshots: strip-ansi@7.1.0: dependencies: - ansi-regex: 6.0.1 + ansi-regex: 6.1.0 strip-bom@3.0.0: {} @@ -8663,29 +9063,14 @@ snapshots: dependencies: min-indent: 1.0.1 - strip-indent@4.0.0: - dependencies: - min-indent: 1.0.1 - strip-json-comments@3.1.1: {} - strip-literal@2.1.0: - dependencies: - js-tokens: 9.0.0 - - styled-jsx@5.1.1(@babel/core@7.25.2)(react@18.2.0): + styled-jsx@5.1.6(@babel/core@7.26.0)(react@19.0.0-rc-66855b96-20241106): dependencies: client-only: 0.0.1 - react: 18.2.0 + react: 19.0.0-rc-66855b96-20241106 optionalDependencies: - '@babel/core': 7.25.2 - - styled-jsx@5.1.1(@babel/core@7.25.2)(react@18.3.1): - dependencies: - client-only: 0.0.1 - react: 18.3.1 - optionalDependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.26.0 sucrase@3.35.0: dependencies: @@ -8711,67 +9096,40 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - swr@2.2.5(react@18.3.1): + swr@2.2.5(react@19.0.0-rc-66855b96-20241106): dependencies: client-only: 0.0.1 - react: 18.3.1 - use-sync-external-store: 1.2.2(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + use-sync-external-store: 1.2.2(react@19.0.0-rc-66855b96-20241106) symbol-tree@3.2.4: {} - tailwindcss@3.2.4(postcss@8.4.21): - dependencies: - arg: 5.0.2 - chokidar: 3.6.0 - color-name: 1.1.4 - detective: 5.2.1 - didyoumean: 1.2.2 - dlv: 1.1.3 - fast-glob: 3.3.2 - glob-parent: 6.0.2 - is-glob: 4.0.3 - lilconfig: 2.1.0 - micromatch: 4.0.7 - normalize-path: 3.0.0 - object-hash: 3.0.0 - picocolors: 1.0.1 - postcss: 8.4.21 - postcss-import: 14.1.0(postcss@8.4.21) - postcss-js: 4.0.1(postcss@8.4.21) - postcss-load-config: 3.1.4(postcss@8.4.21) - postcss-nested: 6.0.0(postcss@8.4.21) - postcss-selector-parser: 6.1.1 - postcss-value-parser: 4.2.0 - quick-lru: 5.1.1 - resolve: 1.22.8 - transitivePeerDependencies: - - ts-node + tabbable@6.2.0: {} - tailwindcss@3.2.4(postcss@8.4.40): + tailwindcss@3.4.14: dependencies: + '@alloc/quick-lru': 5.2.0 arg: 5.0.2 chokidar: 3.6.0 - color-name: 1.1.4 - detective: 5.2.1 didyoumean: 1.2.2 dlv: 1.1.3 fast-glob: 3.3.2 glob-parent: 6.0.2 is-glob: 4.0.3 + jiti: 1.21.6 lilconfig: 2.1.0 - micromatch: 4.0.7 + micromatch: 4.0.8 normalize-path: 3.0.0 object-hash: 3.0.0 - picocolors: 1.0.1 - postcss: 8.4.40 - postcss-import: 14.1.0(postcss@8.4.40) - postcss-js: 4.0.1(postcss@8.4.40) - postcss-load-config: 3.1.4(postcss@8.4.40) - postcss-nested: 6.0.0(postcss@8.4.40) - postcss-selector-parser: 6.1.1 - postcss-value-parser: 4.2.0 - quick-lru: 5.1.1 + picocolors: 1.1.1 + postcss: 8.4.49 + postcss-import: 15.1.0(postcss@8.4.49) + postcss-js: 4.0.1(postcss@8.4.49) + postcss-load-config: 4.0.2(postcss@8.4.49) + postcss-nested: 6.2.0(postcss@8.4.49) + postcss-selector-parser: 6.1.2 resolve: 1.22.8 + sucrase: 3.35.0 transitivePeerDependencies: - ts-node @@ -8798,17 +9156,34 @@ snapshots: dependencies: any-promise: 1.3.0 + thirty-two@1.0.2: {} + throttleit@1.0.1: {} through@2.3.8: {} - tinybench@2.8.0: {} + tinybench@2.9.0: {} tinycolor2@1.4.2: {} - tinypool@0.8.4: {} + tinyexec@0.3.1: {} - tinyspy@2.2.1: {} + tinyglobby@0.2.10: + dependencies: + fdir: 6.4.2(picomatch@4.0.2) + picomatch: 4.0.2 + + tinypool@1.0.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + + tldts-core@6.1.60: {} + + tldts@6.1.60: + dependencies: + tldts-core: 6.1.60 tmp@0.0.33: dependencies: @@ -8816,8 +9191,6 @@ snapshots: tmp@0.2.3: {} - to-fast-properties@2.0.0: {} - to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -8826,12 +9199,9 @@ snapshots: touch@3.1.1: {} - tough-cookie@4.1.4: + tough-cookie@5.0.0: dependencies: - psl: 1.9.0 - punycode: 2.3.1 - universalify: 0.2.0 - url-parse: 1.5.10 + tldts: 6.1.60 tr46@0.0.3: {} @@ -8845,11 +9215,13 @@ snapshots: tree-kill@1.2.2: {} - trim-newlines@4.1.1: {} - - ts-api-utils@1.3.0(typescript@5.5.4): + ts-api-utils@1.3.0(typescript@5.6.3): dependencies: - typescript: 5.5.4 + typescript: 5.6.3 + + ts-api-utils@1.4.1(typescript@5.6.3): + dependencies: + typescript: 5.6.3 ts-error@1.0.6: {} @@ -8859,21 +9231,20 @@ snapshots: dependencies: dprint-node: 1.0.8 - ts-proto-descriptors@1.16.0: + ts-proto-descriptors@2.0.0: dependencies: - long: 5.2.3 - protobufjs: 7.3.2 + '@bufbuild/protobuf': 2.2.2 - ts-proto@1.181.1: + ts-proto@2.2.7: dependencies: + '@bufbuild/protobuf': 2.2.0 case-anything: 2.1.13 - protobufjs: 7.3.2 ts-poet: 6.9.0 - ts-proto-descriptors: 1.16.0 + ts-proto-descriptors: 2.0.0 - tsconfck@3.1.1(typescript@5.5.4): + tsconfck@3.1.4(typescript@5.6.3): optionalDependencies: - typescript: 5.5.4 + typescript: 5.6.3 tsconfig-paths@3.15.0: dependencies: @@ -8882,29 +9253,31 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 - tslib@2.6.3: {} + tslib@2.7.0: {} - tsup@8.2.3(postcss@8.4.40)(typescript@5.5.4)(yaml@2.5.0): + tslib@2.8.1: {} + + tsup@8.3.5(jiti@1.21.6)(postcss@8.4.49)(typescript@5.6.3)(yaml@2.5.0): dependencies: - bundle-require: 5.0.0(esbuild@0.23.0) + bundle-require: 5.0.0(esbuild@0.24.0) cac: 6.7.14 - chokidar: 3.6.0 + chokidar: 4.0.1 consola: 3.2.3 - debug: 4.3.6(supports-color@8.1.1) - esbuild: 0.23.0 - execa: 5.1.1 - globby: 11.1.0 + debug: 4.3.7(supports-color@5.5.0) + esbuild: 0.24.0 joycon: 3.1.1 - picocolors: 1.0.1 - postcss-load-config: 6.0.1(postcss@8.4.40)(yaml@2.5.0) + picocolors: 1.1.1 + postcss-load-config: 6.0.1(jiti@1.21.6)(postcss@8.4.49)(yaml@2.5.0) resolve-from: 5.0.0 - rollup: 4.19.1 + rollup: 4.25.0 source-map: 0.8.0-beta.0 sucrase: 3.35.0 + tinyexec: 0.3.1 + tinyglobby: 0.2.10 tree-kill: 1.2.2 optionalDependencies: - postcss: 8.4.40 - typescript: 5.5.4 + postcss: 8.4.49 + typescript: 5.6.3 transitivePeerDependencies: - jiti - supports-color @@ -8915,32 +9288,32 @@ snapshots: dependencies: safe-buffer: 5.2.1 - turbo-darwin-64@2.0.9: + turbo-darwin-64@2.2.3: optional: true - turbo-darwin-arm64@2.0.9: + turbo-darwin-arm64@2.2.3: optional: true - turbo-linux-64@2.0.9: + turbo-linux-64@2.2.3: optional: true - turbo-linux-arm64@2.0.9: + turbo-linux-arm64@2.2.3: optional: true - turbo-windows-64@2.0.9: + turbo-windows-64@2.2.3: optional: true - turbo-windows-arm64@2.0.9: + turbo-windows-arm64@2.2.3: optional: true - turbo@2.0.9: + turbo@2.2.3: optionalDependencies: - turbo-darwin-64: 2.0.9 - turbo-darwin-arm64: 2.0.9 - turbo-linux-64: 2.0.9 - turbo-linux-arm64: 2.0.9 - turbo-windows-64: 2.0.9 - turbo-windows-arm64: 2.0.9 + turbo-darwin-64: 2.2.3 + turbo-darwin-arm64: 2.2.3 + turbo-linux-64: 2.2.3 + turbo-linux-arm64: 2.2.3 + turbo-windows-64: 2.2.3 + turbo-windows-arm64: 2.2.3 tweetnacl@0.14.5: {} @@ -8948,14 +9321,10 @@ snapshots: dependencies: prelude-ls: 1.2.1 - type-detect@4.1.0: {} - type-fest@0.20.2: {} type-fest@0.21.3: {} - type-fest@1.4.0: {} - typed-array-buffer@1.0.2: dependencies: call-bind: 1.0.7 @@ -8988,9 +9357,15 @@ snapshots: is-typed-array: 1.1.13 possible-typed-array-names: 1.0.0 - typescript@5.5.4: {} + types-react-dom@19.0.0-rc.1: + dependencies: + '@types/react': 18.3.12 - ufo@1.5.4: {} + types-react@19.0.0-rc.1: + dependencies: + csstype: 3.1.3 + + typescript@5.6.3: {} unbox-primitive@1.0.2: dependencies: @@ -9001,121 +9376,121 @@ snapshots: undefsafe@2.0.5: {} - undici-types@5.26.5: {} + undici-types@6.19.8: {} - undici@5.28.4: - dependencies: - '@fastify/busboy': 2.1.1 + unicorn-magic@0.1.0: {} universalify@0.1.2: {} - universalify@0.2.0: {} - universalify@2.0.1: {} untildify@4.0.0: {} - update-browserslist-db@1.1.0(browserslist@4.23.2): + update-browserslist-db@1.1.0(browserslist@4.23.3): dependencies: - browserslist: 4.23.2 - escalade: 3.1.2 - picocolors: 1.0.1 + browserslist: 4.23.3 + escalade: 3.2.0 + picocolors: 1.1.1 + + update-browserslist-db@1.1.1(browserslist@4.24.2): + dependencies: + browserslist: 4.24.2 + escalade: 3.2.0 + picocolors: 1.1.1 uri-js@4.4.1: dependencies: punycode: 2.3.1 - url-parse@1.5.10: + use-intl@3.25.1(react@19.0.0-rc-66855b96-20241106): dependencies: - querystringify: 2.2.0 - requires-port: 1.0.0 + '@formatjs/fast-memoize': 2.2.3 + intl-messageformat: 10.7.7 + react: 19.0.0-rc-66855b96-20241106 - use-sync-external-store@1.2.2(react@18.3.1): + use-sync-external-store@1.2.2(react@19.0.0-rc-66855b96-20241106): dependencies: - react: 18.3.1 + react: 19.0.0-rc-66855b96-20241106 util-deprecate@1.0.2: {} uuid@8.3.2: {} - validate-npm-package-license@3.0.4: - dependencies: - spdx-correct: 3.2.0 - spdx-expression-parse: 3.0.1 - verror@1.10.0: dependencies: assert-plus: 1.0.0 core-util-is: 1.0.2 extsprintf: 1.3.0 - vite-node@1.6.0(@types/node@20.14.13)(sass@1.77.8): + vite-node@2.1.4(@types/node@22.9.0)(sass@1.80.7): dependencies: cac: 6.7.14 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7(supports-color@5.5.0) pathe: 1.1.2 - picocolors: 1.0.1 - vite: 5.3.5(@types/node@20.14.13)(sass@1.77.8) + vite: 5.4.11(@types/node@22.9.0)(sass@1.80.7) transitivePeerDependencies: - '@types/node' - less - lightningcss - sass + - sass-embedded - stylus - sugarss - supports-color - terser - vite-tsconfig-paths@4.3.2(typescript@5.5.4)(vite@5.3.5(@types/node@20.14.13)(sass@1.77.8)): + vite-tsconfig-paths@5.1.2(typescript@5.6.3)(vite@5.4.11(@types/node@22.9.0)(sass@1.80.7)): dependencies: - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7(supports-color@5.5.0) globrex: 0.1.2 - tsconfck: 3.1.1(typescript@5.5.4) + tsconfck: 3.1.4(typescript@5.6.3) optionalDependencies: - vite: 5.3.5(@types/node@20.14.13)(sass@1.77.8) + vite: 5.4.11(@types/node@22.9.0)(sass@1.80.7) transitivePeerDependencies: - supports-color - typescript - vite@5.3.5(@types/node@20.14.13)(sass@1.77.8): + vite@5.4.11(@types/node@22.9.0)(sass@1.80.7): dependencies: esbuild: 0.21.5 - postcss: 8.4.40 - rollup: 4.19.1 + postcss: 8.4.49 + rollup: 4.25.0 optionalDependencies: - '@types/node': 20.14.13 + '@types/node': 22.9.0 fsevents: 2.3.3 - sass: 1.77.8 + sass: 1.80.7 - vitest@1.6.0(@types/node@20.14.13)(jsdom@24.1.1)(sass@1.77.8): + vitest@2.1.4(@types/node@22.9.0)(jsdom@25.0.1)(sass@1.80.7): dependencies: - '@vitest/expect': 1.6.0 - '@vitest/runner': 1.6.0 - '@vitest/snapshot': 1.6.0 - '@vitest/spy': 1.6.0 - '@vitest/utils': 1.6.0 - acorn-walk: 8.3.3 - chai: 4.5.0 - debug: 4.3.6(supports-color@8.1.1) - execa: 8.0.1 - local-pkg: 0.5.0 - magic-string: 0.30.11 + '@vitest/expect': 2.1.4 + '@vitest/mocker': 2.1.4(vite@5.4.11(@types/node@22.9.0)(sass@1.80.7)) + '@vitest/pretty-format': 2.1.4 + '@vitest/runner': 2.1.4 + '@vitest/snapshot': 2.1.4 + '@vitest/spy': 2.1.4 + '@vitest/utils': 2.1.4 + chai: 5.1.2 + debug: 4.3.7(supports-color@5.5.0) + expect-type: 1.1.0 + magic-string: 0.30.12 pathe: 1.1.2 - picocolors: 1.0.1 - std-env: 3.7.0 - strip-literal: 2.1.0 - tinybench: 2.8.0 - tinypool: 0.8.4 - vite: 5.3.5(@types/node@20.14.13)(sass@1.77.8) - vite-node: 1.6.0(@types/node@20.14.13)(sass@1.77.8) + std-env: 3.8.0 + tinybench: 2.9.0 + tinyexec: 0.3.1 + tinypool: 1.0.1 + tinyrainbow: 1.2.0 + vite: 5.4.11(@types/node@22.9.0)(sass@1.80.7) + vite-node: 2.1.4(@types/node@22.9.0)(sass@1.80.7) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 20.14.13 - jsdom: 24.1.1 + '@types/node': 22.9.0 + jsdom: 25.0.1 transitivePeerDependencies: - less - lightningcss + - msw - sass + - sass-embedded - stylus - sugarss - supports-color @@ -9125,9 +9500,9 @@ snapshots: dependencies: xml-name-validator: 5.0.0 - wait-on@7.2.0(debug@4.3.6): + wait-on@8.0.1(debug@4.3.7): dependencies: - axios: 1.7.2(debug@4.3.6) + axios: 1.7.7(debug@4.3.7) joi: 17.13.3 lodash: 4.17.21 minimist: 1.2.8 @@ -9193,11 +9568,6 @@ snapshots: is-weakmap: 2.0.2 is-weakset: 2.0.3 - which-pm@2.2.0: - dependencies: - load-yaml-file: 0.2.0 - path-exists: 4.0.0 - which-typed-array@1.1.15: dependencies: available-typed-arrays: 1.0.7 @@ -9243,6 +9613,12 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.0 + wrap-ansi@9.0.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 7.2.0 + strip-ansi: 7.1.0 + wrappy@1.0.2: {} ws@8.18.0: {} @@ -9251,8 +9627,6 @@ snapshots: xmlchars@2.2.0: {} - xtend@4.0.2: {} - y18n@5.0.8: {} yallist@2.1.2: {} @@ -9261,18 +9635,14 @@ snapshots: yallist@4.0.0: {} - yaml@1.10.2: {} - yaml@2.5.0: {} - yargs-parser@20.2.9: {} - yargs-parser@21.1.1: {} yargs@17.7.2: dependencies: cliui: 8.0.1 - escalade: 3.1.2 + escalade: 3.2.0 get-caller-file: 2.0.5 require-directory: 2.1.1 string-width: 4.2.3 @@ -9285,5 +9655,3 @@ snapshots: fd-slicer: 1.1.0 yocto-queue@0.1.0: {} - - yocto-queue@1.1.1: {} diff --git a/turbo.json b/turbo.json index 609e4bec6c..2817c8c157 100644 --- a/turbo.json +++ b/turbo.json @@ -1,9 +1,7 @@ { "$schema": "https://turbo.build/schema.json", "ui": "tui", - "globalDependencies": [ - "**/.env.*local" - ], + "globalDependencies": ["**/.env.*local"], "globalEnv": [ "DEBUG", "ZITADEL_API_URL", @@ -14,6 +12,7 @@ "ZITADEL_SYSTEM_API_KEY", "ZITADEL_ISSUER", "ZITADEL_ADMIN_TOKEN", + "EMAIL_VERIFICATION", "VERCEL_URL" ], "tasks": { @@ -22,6 +21,8 @@ }, "build": {}, "test": {}, + "start": {}, + "start:built": {}, "test:unit": {}, "test:integration": {}, "test:watch": {