pull login

This commit is contained in:
Elio Bischof
2025-06-24 10:49:45 +02:00
103 changed files with 808 additions and 571 deletions

View File

@@ -46,17 +46,23 @@ The application is now available at `http://localhost:3000`
Configure apps/login/.env.local to target the Zitadel instance of your choice. Configure apps/login/.env.local to target the Zitadel instance of your choice.
The login app live-reloads on changes, so you can start developing right away. The login app live-reloads on changes, so you can start developing right away.
<!-- Console doesn't load
### Developing Against Your Local ZITADEL Instance ### Developing Against Your Local ZITADEL Instance
The following command uses Docker to run a local ZITADEL instance and the login application in live-reloading dev mode. The following command uses Docker to run a local ZITADEL instance and the login application in live-reloading dev mode.
Additionally, it runs a Traefik reverse proxy that exposes the login at https://localhost with a self-signed certificate. Additionally, it runs a Traefik reverse proxy that exposes the login with a self-signed certificate at https://127.0.0.1.sslip.io
127.0.0.1.sslip.io is a special domain that resolves to your localhost, so it's safe to allow your browser to proceed with loading the page.
```sh ```sh
# This command runs all dependencies and overwrites the file ./apps/login/.env.test.local.
pnpm test:acceptance:setup pnpm test:acceptance:setup
# As soon as the setup container completed successfully, you are ready to run the login application in live-reloading dev mode
pnpm dev
``` ```
-->
Log in at https://127.0.0.1.sslip.io/ui/v2/login/loginname and use the following credentials:
**Loginname**: *zitadel-admin@zitadel.127.0.0.1.sslip.io*
**Password**: *Password1!*.
### Quality Assurance ### Quality Assurance

View File

@@ -26,7 +26,7 @@ export LOGIN_TEST_ACCEPTANCE_SAMLSP_TAG := login-test-acceptance-samlsp:${DOCKER
export LOGIN_TEST_ACCEPTANCE_SAMLIDP_TAG := login-test-acceptance-samlidp:${DOCKER_METADATA_OUTPUT_VERSION} export LOGIN_TEST_ACCEPTANCE_SAMLIDP_TAG := login-test-acceptance-samlidp:${DOCKER_METADATA_OUTPUT_VERSION}
export POSTGRES_TAG := postgres:17.0-alpine3.19 export POSTGRES_TAG := postgres:17.0-alpine3.19
export GOLANG_TAG := golang:1.24-alpine export GOLANG_TAG := golang:1.24-alpine
export ZITADEL_TAG ?= ghcr.io/zitadel/zitadel:v3.3.0 export ZITADEL_TAG ?= ghcr.io/zitadel/zitadel:02617cf17fdde849378c1a6b5254bbfb2745b164
export CORE_MOCK_TAG := login-core-mock:${DOCKER_METADATA_OUTPUT_VERSION} export CORE_MOCK_TAG := login-core-mock:${DOCKER_METADATA_OUTPUT_VERSION}
.PHONY: login-help .PHONY: login-help
@@ -52,10 +52,10 @@ login-test-unit:
$(LOGIN_BAKE_CLI_WITH_COMMON_ARGS) login-test-unit $(LOGIN_BAKE_CLI_WITH_COMMON_ARGS) login-test-unit
login-test-integration-build: login-test-integration-build:
$(LOGIN_BAKE_CLI_WITH_COMMON_ARGS) login-core-mock login-test-integration login-standalone $(LOGIN_BAKE_CLI_WITH_COMMON_ARGS) core-mock login-test-integration login-standalone
login-test-integration-dev: login-test-integration-cleanup login-test-integration-dev: login-test-integration-cleanup
$(LOGIN_BAKE_CLI_WITH_COMMON_ARGS) login-core-mock && docker compose --file $(LOGIN_DIR)apps/login-test-integration/docker-compose.yaml run --service-ports --rm login-core-mock $(LOGIN_BAKE_CLI_WITH_COMMON_ARGS) core-mock && docker compose --file $(LOGIN_DIR)apps/login-test-integration/docker-compose.yaml run --service-ports --rm core-mock
login-test-integration-run: login-test-integration-cleanup login-test-integration-run: login-test-integration-cleanup
docker compose --file $(LOGIN_DIR)apps/login-test-integration/docker-compose.yaml run --rm integration docker compose --file $(LOGIN_DIR)apps/login-test-integration/docker-compose.yaml run --rm integration
@@ -79,8 +79,11 @@ login-test-acceptance-build-compose:
login-test-acceptance-build: login-test-acceptance-build-compose login-test-acceptance-build-bake login-test-acceptance-build: login-test-acceptance-build-compose login-test-acceptance-build-bake
@: @:
login-test-acceptance-dev: login-test-acceptance-build-compose login-test-acceptance-cleanup login-test-acceptance-env: login-test-acceptance-build-compose login-test-acceptance-cleanup
docker compose --file $(LOGIN_DIR)apps/login-test-acceptance/docker-compose.yaml up zitadel setup traefik setup sink docker compose --file $(LOGIN_DIR)apps/login-test-acceptance/docker-compose.yaml run setup
login-test-acceptance-dev:
docker compose --file $(LOGIN_DIR)apps/login-test-acceptance/docker-compose.yaml up zitadel traefik sink
login-test-acceptance-run: login-test-acceptance-cleanup login-test-acceptance-run: login-test-acceptance-cleanup
docker compose --file $(LOGIN_DIR)apps/login-test-acceptance/docker-compose.yaml --file $(LOGIN_DIR)apps/login-test-acceptance/docker-compose-ci.yaml run --rm --service-ports acceptance docker compose --file $(LOGIN_DIR)apps/login-test-acceptance/docker-compose.yaml --file $(LOGIN_DIR)apps/login-test-acceptance/docker-compose-ci.yaml run --rm --service-ports acceptance

View File

@@ -9,7 +9,6 @@ services:
setup: setup:
environment: environment:
WRITE_ENVIRONMENT_FILE: /login-env/.env
ZITADEL_API_DOMAIN: traefik ZITADEL_API_DOMAIN: traefik
ZITADEL_API_URL: https://traefik ZITADEL_API_URL: https://traefik
LOGIN_BASE_URL: https://traefik/ui/v2/login/ LOGIN_BASE_URL: https://traefik/ui/v2/login/
@@ -38,6 +37,8 @@ services:
- CI - CI
- LOGIN_BASE_URL=https://traefik/ui/v2/login/ - LOGIN_BASE_URL=https://traefik/ui/v2/login/
- NODE_TLS_REJECT_UNAUTHORIZED=0 - NODE_TLS_REJECT_UNAUTHORIZED=0
volumes:
- ../login/.env.test.local:/builder/apps/login/.env.test.local
ports: ports:
- 9323:9323 - 9323:9323
ipc: "host" ipc: "host"

View File

@@ -5,7 +5,7 @@ services:
image: "${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:v3.3.0}" image: "${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:v3.3.0}"
container_name: acceptance-zitadel container_name: acceptance-zitadel
pull_policy: always pull_policy: always
command: 'start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled --config /zitadel.yaml --steps /zitadel.yaml' command: 'start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --config /zitadel.yaml --steps /zitadel.yaml'
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.http.routers.zitadel.rule=!PathPrefix(`/ui/v2/login`)" - "traefik.http.routers.zitadel.rule=!PathPrefix(`/ui/v2/login`)"
@@ -55,7 +55,7 @@ services:
- "traefik.http.routers.login.rule=PathPrefix(`/ui/v2/login`)" - "traefik.http.routers.login.rule=PathPrefix(`/ui/v2/login`)"
- "traefik.http.services.login-service.loadbalancer.server.url=http://host.docker.internal:3000" - "traefik.http.services.login-service.loadbalancer.server.url=http://host.docker.internal:3000"
command: command:
- "--log.level=DEBUG" # - "--log.level=DEBUG"
- "--ping" - "--ping"
- "--api.insecure=true" - "--api.insecure=true"
- "--providers.docker=true" - "--providers.docker=true"
@@ -70,6 +70,7 @@ services:
start_period: "20s" start_period: "20s"
ports: ports:
- "443:443" - "443:443"
- "8090:8080"
volumes: volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro" - "/var/run/docker.sock:/var/run/docker.sock:ro"
extra_hosts: extra_hosts:
@@ -87,18 +88,16 @@ services:
environment: environment:
PAT_FILE: /pat/zitadel-admin-sa.pat PAT_FILE: /pat/zitadel-admin-sa.pat
ZITADEL_API_INTERNAL_URL: http://zitadel:8080 ZITADEL_API_INTERNAL_URL: http://zitadel:8080
WRITE_ENVIRONMENT_FILE: /login-env/.env.local WRITE_ENVIRONMENT_FILE: /login-env/.env.test.local
WRITE_TEST_ENVIRONMENT_FILE: /acceptance-env/.env
SINK_EMAIL_INTERNAL_URL: http://sink:3333/email SINK_EMAIL_INTERNAL_URL: http://sink:3333/email
SINK_SMS_INTERNAL_URL: http://sink:3333/sms SINK_SMS_INTERNAL_URL: http://sink:3333/sms
SINK_NOTIFICATION_URL: http://localhost:3333/notification SINK_NOTIFICATION_URL: http://localhost:3333/notification
LOGIN_BASE_URL: https://localhost/ui/v2/login/ LOGIN_BASE_URL: https://127.0.0.1.sslip.io/ui/v2/login/
ZITADEL_API_URL: https://localhost ZITADEL_API_URL: https://127.0.0.1.sslip.io
ZITADEL_API_DOMAIN: localhost ZITADEL_API_DOMAIN: 127.0.0.1.sslip.io
ZITADEL_ADMIN_USER: zitadel-admin@zitadel.localhost ZITADEL_ADMIN_USER: zitadel-admin@zitadel.127.0.0.1.sslip.io
volumes: volumes:
- ./pat:/pat # Read the PAT file from zitadels setup - ./pat:/pat # Read the PAT file from zitadels setup
- ./env:/acceptance-env # Write the environment variables file for the tests
- ../login:/login-env # Write the environment variables file for the login - ../login:/login-env # Write the environment variables file for the login
depends_on: depends_on:
traefik: traefik:

View File

@@ -1,2 +0,0 @@
*
!.gitkeep

View File

@@ -3,6 +3,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"test:acceptance": "pnpm exec playwright", "test:acceptance": "pnpm exec playwright",
"test:acceptance:env": "cd ../.. && make login-test-acceptance-env",
"test:acceptance:setup": "cd ../.. && make login-test-acceptance-dev" "test:acceptance:setup": "cd ../.. && make login-test-acceptance-dev"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -2,7 +2,7 @@ import { defineConfig, devices } from "@playwright/test";
import dotenv from "dotenv"; import dotenv from "dotenv";
import path from "path"; import path from "path";
dotenv.config({ path: path.resolve(__dirname, "./env/.env") }); dotenv.config({ path: path.resolve(__dirname, "../login/.env.test.local") });
/** /**
* See https://playwright.dev/docs/test-configuration. * See https://playwright.dev/docs/test-configuration.

View File

@@ -12,8 +12,7 @@ ZITADEL_API_INTERNAL_URL="${ZITADEL_API_INTERNAL_URL:-${ZITADEL_API_URL}}"
SINK_EMAIL_INTERNAL_URL="${SINK_EMAIL_INTERNAL_URL:-"http://sink:3333/email"}" 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_SMS_INTERNAL_URL="${SINK_SMS_INTERNAL_URL:-"http://sink:3333/sms"}"
SINK_NOTIFICATION_URL="${SINK_NOTIFICATION_URL:-"http://localhost:3333/notification"}" SINK_NOTIFICATION_URL="${SINK_NOTIFICATION_URL:-"http://localhost:3333/notification"}"
WRITE_ENVIRONMENT_FILE=${WRITE_ENVIRONMENT_FILE:-$(dirname "$0")/../apps/login/.env.local} WRITE_ENVIRONMENT_FILE=${WRITE_ENVIRONMENT_FILE:-$(dirname "$0")/../apps/login/.env.test.local}
WRITE_TEST_ENVIRONMENT_FILE=${WRITE_TEST_ENVIRONMENT_FILE:-$(dirname "$0")/../apps/login-test-acceptance/tests/.env.local}
if [ -z "${PAT}" ]; then if [ -z "${PAT}" ]; then
echo "Reading PAT from file ${PAT_FILE}" echo "Reading PAT from file ${PAT_FILE}"
@@ -59,7 +58,6 @@ echo "Received ServiceAccount Token: ${SA_PAT}"
################################################################# #################################################################
echo "Writing environment file ${WRITE_ENVIRONMENT_FILE}." echo "Writing environment file ${WRITE_ENVIRONMENT_FILE}."
echo "Writing environment file ${WRITE_TEST_ENVIRONMENT_FILE}."
echo "ZITADEL_API_URL=${ZITADEL_API_URL} echo "ZITADEL_API_URL=${ZITADEL_API_URL}
ZITADEL_SERVICE_USER_TOKEN=${SA_PAT} ZITADEL_SERVICE_USER_TOKEN=${SA_PAT}
@@ -71,14 +69,11 @@ LOGIN_BASE_URL=${LOGIN_BASE_URL}
NODE_TLS_REJECT_UNAUTHORIZED=0 NODE_TLS_REJECT_UNAUTHORIZED=0
ZITADEL_ADMIN_USER=${ZITADEL_ADMIN_USER:-"zitadel-admin@zitadel.localhost"} ZITADEL_ADMIN_USER=${ZITADEL_ADMIN_USER:-"zitadel-admin@zitadel.localhost"}
NEXT_PUBLIC_BASE_PATH=/ui/v2/login NEXT_PUBLIC_BASE_PATH=/ui/v2/login
" | tee "${WRITE_ENVIRONMENT_FILE}" "${WRITE_TEST_ENVIRONMENT_FILE}" > /dev/null " > ${WRITE_ENVIRONMENT_FILE}
echo "Wrote environment file ${WRITE_ENVIRONMENT_FILE}" echo "Wrote environment file ${WRITE_ENVIRONMENT_FILE}"
cat ${WRITE_ENVIRONMENT_FILE} cat ${WRITE_ENVIRONMENT_FILE}
echo "Wrote environment file ${WRITE_TEST_ENVIRONMENT_FILE}"
cat ${WRITE_TEST_ENVIRONMENT_FILE}
################################################################# #################################################################
# SMS provider with HTTP # SMS provider with HTTP
################################################################# #################################################################

View File

@@ -9,7 +9,7 @@ import { getCodeFromSink } from "./sink";
import { PasswordUser } from "./user"; import { PasswordUser } from "./user";
// Read from ".env" file. // Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, "../env/.env") }); dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
const test = base.extend<{ user: PasswordUser }>({ const test = base.extend<{ user: PasswordUser }>({
user: async ({ page }, use) => { user: async ({ page }, use) => {

View File

@@ -7,7 +7,7 @@ import { registerWithPasskey, registerWithPassword } from "./register";
import { removeUserByUsername } from "./zitadel"; import { removeUserByUsername } from "./zitadel";
// Read from ".env" file. // Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, "../env/.env") }); dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
test("register with password", async ({ page }) => { test("register with password", async ({ page }) => {
const username = faker.internet.email(); const username = faker.internet.email();

View File

@@ -6,7 +6,7 @@ import { loginScreenExpect, loginWithPasskey } from "./login";
import { PasskeyUser } from "./user"; import { PasskeyUser } from "./user";
// Read from ".env" file. // Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, "../env/.env") }); dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
const test = base.extend<{ user: PasskeyUser }>({ const test = base.extend<{ user: PasskeyUser }>({
user: async ({ page }, use) => { user: async ({ page }, use) => {

View File

@@ -7,7 +7,7 @@ import { changePassword } from "./password";
import { PasswordUser } from "./user"; import { PasswordUser } from "./user";
// Read from ".env" file. // Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, "../env/.env") }); dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
const test = base.extend<{ user: PasswordUser }>({ const test = base.extend<{ user: PasswordUser }>({
user: async ({ page }, use) => { user: async ({ page }, use) => {

View File

@@ -8,7 +8,7 @@ import { changePasswordScreen, changePasswordScreenExpect } from "./password-scr
import { PasswordUser } from "./user"; import { PasswordUser } from "./user";
// Read from ".env" file. // Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, "../env/.env") }); dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
const test = base.extend<{ user: PasswordUser }>({ const test = base.extend<{ user: PasswordUser }>({
user: async ({ page }, use) => { user: async ({ page }, use) => {

View File

@@ -8,7 +8,7 @@ import { loginScreenExpect, loginWithPassword, loginWithPasswordAndEmailOTP } fr
import { OtpType, PasswordUserWithOTP } from "./user"; import { OtpType, PasswordUserWithOTP } from "./user";
// Read from ".env" file. // Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, "../env/.env") }); dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
const test = base.extend<{ user: PasswordUserWithOTP; sink: any }>({ const test = base.extend<{ user: PasswordUserWithOTP; sink: any }>({
user: async ({ page }, use) => { user: async ({ page }, use) => {

View File

@@ -8,7 +8,7 @@ import { loginScreenExpect, loginWithPassword, loginWithPasswordAndPhoneOTP } fr
import { OtpType, PasswordUserWithOTP } from "./user"; import { OtpType, PasswordUserWithOTP } from "./user";
// Read from ".env" file. // Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, "../env/.env") }); dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
const test = base.extend<{ user: PasswordUserWithOTP; sink: any }>({ const test = base.extend<{ user: PasswordUserWithOTP; sink: any }>({
user: async ({ page }, use) => { user: async ({ page }, use) => {

View File

@@ -9,7 +9,7 @@ import { resetPasswordScreen, resetPasswordScreenExpect } from "./password-scree
import { PasswordUser } from "./user"; import { PasswordUser } from "./user";
// Read from ".env" file. // Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, "../env/.env") }); dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
const test = base.extend<{ user: PasswordUser }>({ const test = base.extend<{ user: PasswordUser }>({
user: async ({ page }, use) => { user: async ({ page }, use) => {

View File

@@ -8,7 +8,7 @@ import { loginScreenExpect, loginWithPassword, loginWithPasswordAndTOTP } from "
import { PasswordUserWithTOTP } from "./user"; import { PasswordUserWithTOTP } from "./user";
// Read from ".env" file. // Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, "../env/.env") }); dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
const test = base.extend<{ user: PasswordUserWithTOTP; sink: any }>({ const test = base.extend<{ user: PasswordUserWithTOTP; sink: any }>({
user: async ({ page }, use) => { user: async ({ page }, use) => {

View File

@@ -10,7 +10,7 @@ import { passwordScreenExpect } from "./password-screen";
import { PasswordUser } from "./user"; import { PasswordUser } from "./user";
// Read from ".env" file. // Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, "../env/.env") }); dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
const test = base.extend<{ user: PasswordUser }>({ const test = base.extend<{ user: PasswordUser }>({
user: async ({ page }, use) => { user: async ({ page }, use) => {

View File

@@ -7,7 +7,7 @@ import { request } from "gaxios";
import path from "path"; import path from "path";
import { OtpType, userProps } from "./user"; import { OtpType, userProps } from "./user";
dotenv.config({ path: path.resolve(__dirname, "../env/.env") }); dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
export async function addUser(props: userProps) { export async function addUser(props: userProps) {
const body = { const body = {

View File

@@ -1,6 +1,7 @@
ExternalDomain: localhost ExternalDomain: 127.0.0.1.sslip.io
ExternalSecure: true ExternalSecure: true
ExternalPort: 443 ExternalPort: 443
TLS.Enabled: false
FirstInstance: FirstInstance:
PatPath: /pat/zitadel-admin-sa.pat PatPath: /pat/zitadel-admin-sa.pat

View File

@@ -1,5 +1,5 @@
NEXT_PUBLIC_BASE_PATH=""
ZITADEL_API_URL=http://localhost:22222 ZITADEL_API_URL=http://localhost:22222
ZITADEL_SERVICE_USER_TOKEN="yolo" ZITADEL_SERVICE_USER_TOKEN="yolo"
EMAIL_VERIFICATION=true EMAIL_VERIFICATION=true
DEBUG=true DEBUG=true
NEXT_PUBLIC_BASE_PATH=""

View File

@@ -1,3 +1,2 @@
custom-config.js custom-config.js
.env.local .env*.local
.env.acceptance

View File

@@ -22,7 +22,8 @@
"loginname": { "loginname": {
"title": "Willkommen zurück!", "title": "Willkommen zurück!",
"description": "Geben Sie Ihre Anmeldedaten ein.", "description": "Geben Sie Ihre Anmeldedaten ein.",
"register": "Neuen Benutzer registrieren" "register": "Neuen Benutzer registrieren",
"submit": "Weiter"
}, },
"password": { "password": {
"verify": { "verify": {

View File

@@ -22,7 +22,8 @@
"loginname": { "loginname": {
"title": "Welcome back!", "title": "Welcome back!",
"description": "Enter your login data.", "description": "Enter your login data.",
"register": "Register new user" "register": "Register new user",
"submit": "Continue"
}, },
"password": { "password": {
"verify": { "verify": {

View File

@@ -22,7 +22,8 @@
"loginname": { "loginname": {
"title": "¡Bienvenido de nuevo!", "title": "¡Bienvenido de nuevo!",
"description": "Introduce tus datos de acceso.", "description": "Introduce tus datos de acceso.",
"register": "Registrar nuevo usuario" "register": "Registrar nuevo usuario",
"submit": "Continuar"
}, },
"password": { "password": {
"verify": { "verify": {

View File

@@ -22,7 +22,8 @@
"loginname": { "loginname": {
"title": "Bentornato!", "title": "Bentornato!",
"description": "Inserisci i tuoi dati di accesso.", "description": "Inserisci i tuoi dati di accesso.",
"register": "Registrati come nuovo utente" "register": "Registrati come nuovo utente",
"submit": "Continua"
}, },
"password": { "password": {
"verify": { "verify": {

View File

@@ -22,7 +22,8 @@
"loginname": { "loginname": {
"title": "Witamy ponownie!", "title": "Witamy ponownie!",
"description": "Wprowadź dane logowania.", "description": "Wprowadź dane logowania.",
"register": "Zarejestruj nowego użytkownika" "register": "Zarejestruj nowego użytkownika",
"submit": "Kontynuuj"
}, },
"password": { "password": {
"verify": { "verify": {

View File

@@ -22,7 +22,8 @@
"loginname": { "loginname": {
"title": "С возвращением!", "title": "С возвращением!",
"description": "Введите свои данные для входа.", "description": "Введите свои данные для входа.",
"register": "Зарегистрировать нового пользователя" "register": "Зарегистрировать нового пользователя",
"submit": "Продолжить"
}, },
"password": { "password": {
"verify": { "verify": {

View File

@@ -22,7 +22,8 @@
"loginname": { "loginname": {
"title": "欢迎回来!", "title": "欢迎回来!",
"description": "请输入您的登录信息。", "description": "请输入您的登录信息。",
"register": "注册新用户" "register": "注册新用户",
"submit": "继续"
}, },
"password": { "password": {
"verify": { "verify": {

View File

@@ -26,8 +26,6 @@ const secureHeaders = [
key: "X-XSS-Protection", key: "X-XSS-Protection",
value: "1; mode=block", value: "1; mode=block",
}, },
// img-src vercel.com needed for deploy button,
// script-src va.vercel-scripts.com for analytics/vercel scripts
{ {
key: "Content-Security-Policy", key: "Content-Security-Policy",
value: `${DEFAULT_CSP} frame-ancestors 'none'`, value: `${DEFAULT_CSP} frame-ancestors 'none'`,

View File

@@ -3,8 +3,8 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "pnpm exec next dev --turbopack", "dev": "pnpm next dev --turbopack",
"test:unit": "pnpm exec vitest", "test:unit": "pnpm vitest",
"test:unit:watch": "pnpm test:unit --watch", "test:unit:watch": "pnpm test:unit --watch",
"lint": "pnpm exec next lint && pnpm exec prettier --check .", "lint": "pnpm exec next lint && pnpm exec prettier --check .",
"lint:fix": "pnpm exec prettier --write .", "lint:fix": "pnpm exec prettier --write .",
@@ -31,10 +31,9 @@
"clsx": "1.2.1", "clsx": "1.2.1",
"copy-to-clipboard": "^3.3.3", "copy-to-clipboard": "^3.3.3",
"deepmerge": "^4.3.1", "deepmerge": "^4.3.1",
"jose": "^5.3.0",
"lucide-react": "0.469.0", "lucide-react": "0.469.0",
"moment": "^2.29.4", "moment": "^2.29.4",
"next": "15.4.0-canary.3", "next": "15.4.0-canary.86",
"next-intl": "^3.25.1", "next-intl": "^3.25.1",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"nice-grpc": "2.0.1", "nice-grpc": "2.0.1",
@@ -42,7 +41,6 @@
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-hook-form": "7.39.5", "react-hook-form": "7.39.5",
"swr": "^2.2.0",
"tinycolor2": "1.4.2", "tinycolor2": "1.4.2",
"uuid": "^11.1.0" "uuid": "^11.1.0"
}, },

View File

@@ -1,5 +1,6 @@
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { SessionsList } from "@/components/sessions-list"; import { SessionsList } from "@/components/sessions-list";
import { Translated } from "@/components/translated";
import { getAllSessionCookieIds } from "@/lib/cookies"; import { getAllSessionCookieIds } from "@/lib/cookies";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { import {
@@ -9,7 +10,7 @@ import {
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { UserPlusIcon } from "@heroicons/react/24/outline"; import { UserPlusIcon } from "@heroicons/react/24/outline";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { getLocale, getTranslations } from "next-intl/server"; import { getLocale } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
import Link from "next/link"; import Link from "next/link";
@@ -33,7 +34,6 @@ export default async function Page(props: {
}) { }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale(); const locale = getLocale();
const t = await getTranslations({ locale, namespace: "accounts" });
const requestId = searchParams?.requestId; const requestId = searchParams?.requestId;
const organization = searchParams?.organization; const organization = searchParams?.organization;
@@ -71,8 +71,12 @@ export default async function Page(props: {
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("title")}</h1> <h1>
<p className="ztdl-p mb-6 block">{t("description")}</p> <Translated i18nKey="title" namespace="accounts" />
</h1>
<p className="ztdl-p mb-6 block">
<Translated i18nKey="description" namespace="accounts" />
</p>
<div className="flex flex-col w-full space-y-2"> <div className="flex flex-col w-full space-y-2">
<SessionsList sessions={sessions} requestId={requestId} /> <SessionsList sessions={sessions} requestId={requestId} />
@@ -81,7 +85,9 @@ export default async function Page(props: {
<div className="w-8 h-8 mr-4 flex flex-row justify-center items-center rounded-full bg-black/5 dark:bg-white/5"> <div className="w-8 h-8 mr-4 flex flex-row justify-center items-center rounded-full bg-black/5 dark:bg-white/5">
<UserPlusIcon className="h-5 w-5" /> <UserPlusIcon className="h-5 w-5" />
</div> </div>
<span className="text-sm">{t("addAnother")}</span> <span className="text-sm">
<Translated i18nKey="addAnother" namespace="accounts" />
</span>
</div> </div>
</Link> </Link>
</div> </div>

View File

@@ -3,6 +3,7 @@ import { BackButton } from "@/components/back-button";
import { ChooseAuthenticatorToSetup } from "@/components/choose-authenticator-to-setup"; import { ChooseAuthenticatorToSetup } from "@/components/choose-authenticator-to-setup";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { SignInWithIdp } from "@/components/sign-in-with-idp"; import { SignInWithIdp } from "@/components/sign-in-with-idp";
import { Translated } from "@/components/translated";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getSessionCookieById } from "@/lib/cookies"; import { getSessionCookieById } from "@/lib/cookies";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
@@ -17,7 +18,7 @@ import {
listAuthenticationMethodTypes, listAuthenticationMethodTypes,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { getLocale, getTranslations } from "next-intl/server"; import { getLocale } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
@@ -26,8 +27,6 @@ export default async function Page(props: {
}) { }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale(); const locale = getLocale();
const t = await getTranslations({ locale, namespace: "authenticator" });
const tError = await getTranslations({ locale, namespace: "error" });
const { loginName, requestId, organization, sessionId } = searchParams; const { loginName, requestId, organization, sessionId } = searchParams;
@@ -99,7 +98,11 @@ export default async function Page(props: {
!sessionWithData.factors || !sessionWithData.factors ||
!sessionWithData.factors.user !sessionWithData.factors.user
) { ) {
return <Alert>{tError("unknownContext")}</Alert>; return (
<Alert>
<Translated i18nKey="unknownContext" namespace="error" />
</Alert>
);
} }
const branding = await getBrandingSettings({ const branding = await getBrandingSettings({
@@ -165,9 +168,13 @@ export default async function Page(props: {
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("title")}</h1> <h1>
<Translated i18nKey="title" namespace="authenticator" />
</h1>
<p className="ztdl-p">{t("description")}</p> <p className="ztdl-p">
<Translated i18nKey="description" namespace="authenticator" />
</p>
<UserAvatar <UserAvatar
loginName={sessionWithData.factors?.user?.loginName} loginName={sessionWithData.factors?.user?.loginName}
@@ -187,7 +194,9 @@ export default async function Page(props: {
{loginSettings?.allowExternalIdp && !!identityProviders.length && ( {loginSettings?.allowExternalIdp && !!identityProviders.length && (
<> <>
<div className="py-3 flex flex-col"> <div className="py-3 flex flex-col">
<p className="ztdl-p text-center">{t("linkWithIDP")}</p> <p className="ztdl-p text-center">
<Translated i18nKey="linkWithIDP" namespace="authenticator" />
</p>
</div> </div>
<SignInWithIdp <SignInWithIdp

View File

@@ -1,5 +1,6 @@
import { ConsentScreen } from "@/components/consent"; import { ConsentScreen } from "@/components/consent";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { Translated } from "@/components/translated";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { import {
getBrandingSettings, getBrandingSettings,
@@ -7,22 +8,23 @@ import {
getDeviceAuthorizationRequest, getDeviceAuthorizationRequest,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
export default async function Page(props: { export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>; searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) { }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale });
const userCode = searchParams?.user_code; const userCode = searchParams?.user_code;
const requestId = searchParams?.requestId; const requestId = searchParams?.requestId;
const organization = searchParams?.organization; const organization = searchParams?.organization;
if (!userCode || !requestId) { if (!userCode || !requestId) {
return <div>{t("error.noUserCode")}</div>; return (
<div>
<Translated i18nKey="noUserCode" namespace="error" />
</div>
);
} }
const _headers = await headers(); const _headers = await headers();
@@ -34,7 +36,11 @@ export default async function Page(props: {
}); });
if (!deviceAuthorizationRequest) { if (!deviceAuthorizationRequest) {
return <div>{t("error.noDeviceRequest")}</div>; return (
<div>
<Translated i18nKey="noDeviceRequest" namespace="error" />
</div>
);
} }
let defaultOrganization; let defaultOrganization;
@@ -66,15 +72,19 @@ export default async function Page(props: {
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1> <h1>
{t("device.request.title", { <Translated
appName: deviceAuthorizationRequest?.appName, i18nKey="request.title"
})} namespace="device"
data={{ appName: deviceAuthorizationRequest?.appName }}
/>
</h1> </h1>
<p className="ztdl-p"> <p className="ztdl-p">
{t("device.request.description", { <Translated
appName: deviceAuthorizationRequest?.appName, i18nKey="request.description"
})} namespace="device"
data={{ appName: deviceAuthorizationRequest?.appName }}
/>
</p> </p>
<ConsentScreen <ConsentScreen

View File

@@ -1,17 +1,15 @@
import { DeviceCodeForm } from "@/components/device-code-form"; import { DeviceCodeForm } from "@/components/device-code-form";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { Translated } from "@/components/translated";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { getBrandingSettings, getDefaultOrg } from "@/lib/zitadel"; import { getBrandingSettings, getDefaultOrg } from "@/lib/zitadel";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
export default async function Page(props: { export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>; searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) { }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "device" });
const userCode = searchParams?.user_code; const userCode = searchParams?.user_code;
const organization = searchParams?.organization; const organization = searchParams?.organization;
@@ -37,8 +35,12 @@ export default async function Page(props: {
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("usercode.title")}</h1> <h1>
<p className="ztdl-p">{t("usercode.description")}</p> <Translated i18nKey="usercode.title" namespace="device" />
</h1>
<p className="ztdl-p">
<Translated i18nKey="usercode.description" namespace="device" />
</p>
<DeviceCodeForm userCode={userCode}></DeviceCodeForm> <DeviceCodeForm userCode={userCode}></DeviceCodeForm>
</div> </div>
</DynamicTheme> </DynamicTheme>

View File

@@ -2,7 +2,7 @@
import { Boundary } from "@/components/boundary"; import { Boundary } from "@/components/boundary";
import { Button } from "@/components/button"; import { Button } from "@/components/button";
import { useTranslations } from "next-intl"; import { Translated } from "@/components/translated";
import { useEffect } from "react"; import { useEffect } from "react";
export default function Error({ error, reset }: any) { export default function Error({ error, reset }: any) {
@@ -10,8 +10,6 @@ export default function Error({ error, reset }: any) {
console.log("logging error:", error); console.log("logging error:", error);
}, [error]); }, [error]);
const t = useTranslations("error");
return ( return (
<Boundary labels={["Login Error"]} color="red"> <Boundary labels={["Login Error"]} color="red">
<div className="space-y-4"> <div className="space-y-4">
@@ -19,7 +17,9 @@ export default function Error({ error, reset }: any) {
<strong className="font-bold">Error:</strong> {error?.message} <strong className="font-bold">Error:</strong> {error?.message}
</div> </div>
<div> <div>
<Button onClick={() => reset()}>{t("tryagain")}</Button> <Button data-i18n-key="error.tryagain" onClick={() => reset()}>
<Translated i18nKey="tryagain" namespace="error" />
</Button>
</div> </div>
</div> </div>
</Boundary> </Boundary>

View File

@@ -1,6 +1,7 @@
import { Alert, AlertType } from "@/components/alert"; import { Alert, AlertType } from "@/components/alert";
import { ChooseAuthenticatorToLogin } from "@/components/choose-authenticator-to-login"; import { ChooseAuthenticatorToLogin } from "@/components/choose-authenticator-to-login";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { Translated } from "@/components/translated";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { import {
@@ -11,7 +12,6 @@ import {
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
export default async function Page(props: { export default async function Page(props: {
@@ -19,8 +19,6 @@ export default async function Page(props: {
params: Promise<{ provider: string }>; params: Promise<{ provider: string }>;
}) { }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "idp" });
const { organization, userId } = searchParams; const { organization, userId } = searchParams;
@@ -77,8 +75,12 @@ export default async function Page(props: {
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("loginError.title")}</h1> <h1>
<Alert type={AlertType.ALERT}>{t("loginError.description")}</Alert> <Translated i18nKey="loginError.title" namespace="idp" />
</h1>
<Alert type={AlertType.ALERT}>
<Translated i18nKey="loginError.description" namespace="idp" />
</Alert>
{userId && authMethods.length && ( {userId && authMethods.length && (
<> <>

View File

@@ -5,6 +5,7 @@ import { linkingFailed } from "@/components/idps/pages/linking-failed";
import { linkingSuccess } from "@/components/idps/pages/linking-success"; import { linkingSuccess } from "@/components/idps/pages/linking-success";
import { loginFailed } from "@/components/idps/pages/login-failed"; import { loginFailed } from "@/components/idps/pages/login-failed";
import { loginSuccess } from "@/components/idps/pages/login-success"; import { loginSuccess } from "@/components/idps/pages/login-success";
import { Translated } from "@/components/translated";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { import {
addHuman, addHuman,
@@ -27,7 +28,6 @@ import {
AddHumanUserRequestSchema, AddHumanUserRequestSchema,
UpdateHumanUserRequestSchema, UpdateHumanUserRequestSchema,
} from "@zitadel/proto/zitadel/user/v2/user_service_pb"; } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
const ORG_SUFFIX_REGEX = /(?<=@)(.+)/; const ORG_SUFFIX_REGEX = /(?<=@)(.+)/;
@@ -73,8 +73,6 @@ export default async function Page(props: {
}) { }) {
const params = await props.params; const params = await props.params;
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "idp" });
let { id, token, requestId, organization, link } = searchParams; let { id, token, requestId, organization, link } = searchParams;
const { provider } = params; const { provider } = params;
@@ -321,8 +319,12 @@ export default async function Page(props: {
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("registerSuccess.title")}</h1> <h1>
<p className="ztdl-p">{t("registerSuccess.description")}</p> <Translated i18nKey="registerSuccess.title" namespace="idp" />
</h1>
<p className="ztdl-p">
<Translated i18nKey="registerSuccess.description" namespace="idp" />
</p>
<IdpSignin <IdpSignin
userId={newUser.userId} userId={newUser.userId}
idpIntent={{ idpIntentId: id, idpIntentToken: token }} idpIntent={{ idpIntentId: id, idpIntentToken: token }}

View File

@@ -1,16 +1,14 @@
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { SignInWithIdp } from "@/components/sign-in-with-idp"; import { SignInWithIdp } from "@/components/sign-in-with-idp";
import { Translated } from "@/components/translated";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { getActiveIdentityProviders, getBrandingSettings } from "@/lib/zitadel"; import { getActiveIdentityProviders, getBrandingSettings } from "@/lib/zitadel";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
export default async function Page(props: { export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>; searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) { }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "idp" });
const requestId = searchParams?.requestId; const requestId = searchParams?.requestId;
const organization = searchParams?.organization; const organization = searchParams?.organization;
@@ -33,8 +31,12 @@ export default async function Page(props: {
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("title")}</h1> <h1>
<p className="ztdl-p">{t("description")}</p> <Translated i18nKey="title" namespace="idp" />
</h1>
<p className="ztdl-p">
<Translated i18nKey="description" namespace="idp" />
</p>
{identityProviders && ( {identityProviders && (
<SignInWithIdp <SignInWithIdp

View File

@@ -1,5 +1,6 @@
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { SignInWithIdp } from "@/components/sign-in-with-idp"; import { SignInWithIdp } from "@/components/sign-in-with-idp";
import { Translated } from "@/components/translated";
import { UsernameForm } from "@/components/username-form"; import { UsernameForm } from "@/components/username-form";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { import {
@@ -9,15 +10,12 @@ import {
getLoginSettings, getLoginSettings,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
export default async function Page(props: { export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>; searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) { }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "loginname" });
const loginName = searchParams?.loginName; const loginName = searchParams?.loginName;
const requestId = searchParams?.requestId; const requestId = searchParams?.requestId;
@@ -63,8 +61,12 @@ export default async function Page(props: {
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("title")}</h1> <h1 data-i18n-key="error.tryagain">
<p className="ztdl-p">{t("description")}</p> <Translated i18nKey="title" namespace="loginname" />
</h1>
<p className="ztdl-p">
<Translated i18nKey="description" namespace="loginname" />
</p>
<UsernameForm <UsernameForm
loginName={loginName} loginName={loginName}

View File

@@ -1,5 +1,6 @@
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { SessionsClearList } from "@/components/sessions-clear-list"; import { SessionsClearList } from "@/components/sessions-clear-list";
import { Translated } from "@/components/translated";
import { getAllSessionCookieIds } from "@/lib/cookies"; import { getAllSessionCookieIds } from "@/lib/cookies";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { import {
@@ -8,7 +9,6 @@ import {
listSessions, listSessions,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
async function loadSessions({ serviceUrl }: { serviceUrl: string }) { async function loadSessions({ serviceUrl }: { serviceUrl: string }) {
@@ -30,8 +30,6 @@ export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>; searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) { }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "logout" });
const organization = searchParams?.organization; const organization = searchParams?.organization;
const postLogoutRedirectUri = searchParams?.post_logout_redirect_uri; const postLogoutRedirectUri = searchParams?.post_logout_redirect_uri;
@@ -67,8 +65,12 @@ export default async function Page(props: {
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("title")}</h1> <h1>
<p className="ztdl-p mb-6 block">{t("description")}</p> <Translated i18nKey="title" namespace="logout" />
</h1>
<p className="ztdl-p mb-6 block">
<Translated i18nKey="description" namespace="logout" />
</p>
<div className="flex flex-col w-full space-y-2"> <div className="flex flex-col w-full space-y-2">
<SessionsClearList <SessionsClearList

View File

@@ -1,14 +1,12 @@
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { Translated } from "@/components/translated";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { getBrandingSettings, getDefaultOrg } from "@/lib/zitadel"; import { getBrandingSettings, getDefaultOrg } from "@/lib/zitadel";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
export default async function Page(props: { searchParams: Promise<any> }) { export default async function Page(props: { searchParams: Promise<any> }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "logout" });
const _headers = await headers(); const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers); const { serviceUrl } = getServiceUrlFromHeaders(_headers);
@@ -33,8 +31,12 @@ export default async function Page(props: { searchParams: Promise<any> }) {
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("success.title")}</h1> <h1>
<p className="ztdl-p mb-6 block">{t("success.description")}</p> <Translated i18nKey="success.title" namespace="logout" />
</h1>
<p className="ztdl-p mb-6 block">
<Translated i18nKey="success.description" namespace="logout" />
</p>
</div> </div>
</DynamicTheme> </DynamicTheme>
); );

View File

@@ -2,6 +2,7 @@ import { Alert } from "@/components/alert";
import { BackButton } from "@/components/back-button"; import { BackButton } from "@/components/back-button";
import { ChooseSecondFactor } from "@/components/choose-second-factor"; import { ChooseSecondFactor } from "@/components/choose-second-factor";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { Translated } from "@/components/translated";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getSessionCookieById } from "@/lib/cookies"; import { getSessionCookieById } from "@/lib/cookies";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
@@ -11,16 +12,12 @@ import {
getSession, getSession,
listAuthenticationMethodTypes, listAuthenticationMethodTypes,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
export default async function Page(props: { export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>; searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) { }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "mfa" });
const tError = await getTranslations({ locale, namespace: "error" });
const { loginName, requestId, organization, sessionId } = searchParams; const { loginName, requestId, organization, sessionId } = searchParams;
@@ -90,9 +87,13 @@ export default async function Page(props: {
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("verify.title")}</h1> <h1>
<Translated i18nKey="verify.title" namespace="mfa" />
</h1>
<p className="ztdl-p">{t("verify.description")}</p> <p className="ztdl-p">
<Translated i18nKey="verify.description" namespace="mfa" />
</p>
{sessionFactors && ( {sessionFactors && (
<UserAvatar <UserAvatar
@@ -103,7 +104,11 @@ export default async function Page(props: {
></UserAvatar> ></UserAvatar>
)} )}
{!(loginName || sessionId) && <Alert>{tError("unknownContext")}</Alert>} {!(loginName || sessionId) && (
<Alert>
<Translated i18nKey="unknownContext" namespace="error" />
</Alert>
)}
{sessionFactors ? ( {sessionFactors ? (
<ChooseSecondFactor <ChooseSecondFactor
@@ -114,7 +119,9 @@ export default async function Page(props: {
userMethods={sessionFactors.authMethods ?? []} userMethods={sessionFactors.authMethods ?? []}
></ChooseSecondFactor> ></ChooseSecondFactor>
) : ( ) : (
<Alert>{t("verify.noResults")}</Alert> <Alert>
<Translated i18nKey="verify.noResults" namespace="mfa" />
</Alert>
)} )}
<div className="mt-8 flex w-full flex-row items-center"> <div className="mt-8 flex w-full flex-row items-center">

View File

@@ -2,6 +2,7 @@ import { Alert } from "@/components/alert";
import { BackButton } from "@/components/back-button"; import { BackButton } from "@/components/back-button";
import { ChooseSecondFactorToSetup } from "@/components/choose-second-factor-to-setup"; import { ChooseSecondFactorToSetup } from "@/components/choose-second-factor-to-setup";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { Translated } from "@/components/translated";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getSessionCookieById } from "@/lib/cookies"; import { getSessionCookieById } from "@/lib/cookies";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
@@ -15,7 +16,6 @@ import {
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { Timestamp, timestampDate } from "@zitadel/client"; import { Timestamp, timestampDate } from "@zitadel/client";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
function isSessionValid(session: Partial<Session>): { function isSessionValid(session: Partial<Session>): {
@@ -38,9 +38,6 @@ export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>; searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) { }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "mfa" });
const tError = await getTranslations({ locale, namespace: "error" });
const { loginName, checkAfter, force, requestId, organization, sessionId } = const { loginName, checkAfter, force, requestId, organization, sessionId } =
searchParams; searchParams;
@@ -119,9 +116,13 @@ export default async function Page(props: {
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("set.title")}</h1> <h1>
<Translated i18nKey="set.title" namespace="mfa" />
</h1>
<p className="ztdl-p">{t("set.description")}</p> <p className="ztdl-p">
<Translated i18nKey="set.description" namespace="mfa" />
</p>
{sessionWithData && ( {sessionWithData && (
<UserAvatar <UserAvatar
@@ -132,9 +133,17 @@ export default async function Page(props: {
></UserAvatar> ></UserAvatar>
)} )}
{!(loginName || sessionId) && <Alert>{tError("unknownContext")}</Alert>} {!(loginName || sessionId) && (
<Alert>
<Translated i18nKey="unknownContext" namespace="error" />
</Alert>
)}
{!valid && <Alert>{tError("sessionExpired")}</Alert>} {!valid && (
<Alert>
<Translated i18nKey="sessionExpired" namespace="error" />
</Alert>
)}
{isSessionValid(sessionWithData).valid && {isSessionValid(sessionWithData).valid &&
loginSettings && loginSettings &&

View File

@@ -1,6 +1,7 @@
import { Alert } from "@/components/alert"; import { Alert } from "@/components/alert";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { LoginOTP } from "@/components/login-otp"; import { LoginOTP } from "@/components/login-otp";
import { Translated } from "@/components/translated";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getSessionCookieById } from "@/lib/cookies"; import { getSessionCookieById } from "@/lib/cookies";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
@@ -10,7 +11,7 @@ import {
getLoginSettings, getLoginSettings,
getSession, getSession,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { getLocale, getTranslations } from "next-intl/server"; import { getLocale } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
export default async function Page(props: { export default async function Page(props: {
@@ -20,8 +21,6 @@ export default async function Page(props: {
const params = await props.params; const params = await props.params;
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale(); const locale = getLocale();
const t = await getTranslations({ locale, namespace: "otp" });
const tError = await getTranslations({ locale, namespace: "error" });
const _headers = await headers(); const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers); const { serviceUrl } = getServiceUrlFromHeaders(_headers);
@@ -81,20 +80,30 @@ export default async function Page(props: {
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("verify.title")}</h1> <h1>
<Translated i18nKey="verify.title" namespace="otp" />
</h1>
{method === "time-based" && ( {method === "time-based" && (
<p className="ztdl-p">{t("verify.totpDescription")}</p> <p className="ztdl-p">
<Translated i18nKey="verify.totpDescription" namespace="otp" />
</p>
)} )}
{method === "sms" && ( {method === "sms" && (
<p className="ztdl-p">{t("verify.smsDescription")}</p> <p className="ztdl-p">
<Translated i18nKey="verify.smsDescription" namespace="otp" />
</p>
)} )}
{method === "email" && ( {method === "email" && (
<p className="ztdl-p">{t("verify.emailDescription")}</p> <p className="ztdl-p">
<Translated i18nKey="verify.emailDescription" namespace="otp" />
</p>
)} )}
{!session && ( {!session && (
<div className="py-4"> <div className="py-4">
<Alert>{tError("unknownContext")}</Alert> <Alert>
<Translated i18nKey="unknownContext" namespace="error" />
</Alert>
</div> </div>
)} )}

View File

@@ -3,6 +3,7 @@ import { BackButton } from "@/components/back-button";
import { Button, ButtonVariants } from "@/components/button"; import { Button, ButtonVariants } from "@/components/button";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { TotpRegister } from "@/components/totp-register"; import { TotpRegister } from "@/components/totp-register";
import { Translated } from "@/components/translated";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
@@ -14,7 +15,6 @@ import {
registerTOTP, registerTOTP,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { RegisterTOTPResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { RegisterTOTPResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
@@ -25,9 +25,6 @@ export default async function Page(props: {
}) { }) {
const params = await props.params; const params = await props.params;
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "otp" });
const tError = await getTranslations({ locale, namespace: "error" });
const { loginName, organization, sessionId, requestId, checkAfter } = const { loginName, organization, sessionId, requestId, checkAfter } =
searchParams; searchParams;
@@ -128,10 +125,14 @@ export default async function Page(props: {
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("set.title")}</h1> <h1>
<Translated i18nKey="set.title" namespace="otp" />
</h1>
{!session && ( {!session && (
<div className="py-4"> <div className="py-4">
<Alert>{tError("unknownContext")}</Alert> <Alert>
<Translated i18nKey="unknownContext" namespace="error" />
</Alert>
</div> </div>
)} )}
@@ -152,7 +153,12 @@ export default async function Page(props: {
{totpResponse && "uri" in totpResponse && "secret" in totpResponse ? ( {totpResponse && "uri" in totpResponse && "secret" in totpResponse ? (
<> <>
<p className="ztdl-p">{t("set.totpRegisterDescription")}</p> <p className="ztdl-p">
<Translated
i18nKey="set.totpRegisterDescription"
namespace="otp"
/>
</p>
<div> <div>
<TotpRegister <TotpRegister
uri={totpResponse.uri as string} uri={totpResponse.uri as string}
@@ -186,7 +192,7 @@ export default async function Page(props: {
className="self-end" className="self-end"
variant={ButtonVariants.Primary} variant={ButtonVariants.Primary}
> >
{t("set.submit")} <Translated i18nKey="set.submit" namespace="otp" />
</Button> </Button>
</Link> </Link>
</div> </div>

View File

@@ -1,21 +1,18 @@
import { Alert } from "@/components/alert"; import { Alert } from "@/components/alert";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { LoginPasskey } from "@/components/login-passkey"; import { LoginPasskey } from "@/components/login-passkey";
import { Translated } from "@/components/translated";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getSessionCookieById } from "@/lib/cookies"; import { getSessionCookieById } from "@/lib/cookies";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings, getSession } from "@/lib/zitadel"; import { getBrandingSettings, getSession } from "@/lib/zitadel";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
export default async function Page(props: { export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>; searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) { }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "passkey" });
const tError = await getTranslations({ locale, namespace: "error" });
const { loginName, altPassword, requestId, organization, sessionId } = const { loginName, altPassword, requestId, organization, sessionId } =
searchParams; searchParams;
@@ -55,7 +52,9 @@ export default async function Page(props: {
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("verify.title")}</h1> <h1>
<Translated i18nKey="verify.title" namespace="passkey" />
</h1>
{sessionFactors && ( {sessionFactors && (
<UserAvatar <UserAvatar
@@ -65,9 +64,15 @@ export default async function Page(props: {
searchParams={searchParams} searchParams={searchParams}
></UserAvatar> ></UserAvatar>
)} )}
<p className="ztdl-p mb-6 block">{t("verify.description")}</p> <p className="ztdl-p mb-6 block">
<Translated i18nKey="verify.description" namespace="passkey" />
</p>
{!(loginName || sessionId) && <Alert>{tError("unknownContext")}</Alert>} {!(loginName || sessionId) && (
<Alert>
<Translated i18nKey="unknownContext" namespace="error" />
</Alert>
)}
{(loginName || sessionId) && ( {(loginName || sessionId) && (
<LoginPasskey <LoginPasskey

View File

@@ -1,20 +1,17 @@
import { Alert, AlertType } from "@/components/alert"; import { Alert, AlertType } from "@/components/alert";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { RegisterPasskey } from "@/components/register-passkey"; import { RegisterPasskey } from "@/components/register-passkey";
import { Translated } from "@/components/translated";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings } from "@/lib/zitadel"; import { getBrandingSettings } from "@/lib/zitadel";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
export default async function Page(props: { export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>; searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) { }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "passkey" });
const tError = await getTranslations({ locale, namespace: "error" });
const { loginName, prompt, organization, requestId, userId } = searchParams; const { loginName, prompt, organization, requestId, userId } = searchParams;
@@ -37,7 +34,9 @@ export default async function Page(props: {
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("set.title")}</h1> <h1>
<Translated i18nKey="set.title" namespace="passkey" />
</h1>
{session && ( {session && (
<UserAvatar <UserAvatar
@@ -47,24 +46,28 @@ export default async function Page(props: {
searchParams={searchParams} searchParams={searchParams}
></UserAvatar> ></UserAvatar>
)} )}
<p className="ztdl-p mb-6 block">{t("set.description")}</p> <p className="ztdl-p mb-6 block">
<Translated i18nKey="set.description" namespace="passkey" />
</p>
<Alert type={AlertType.INFO}> <Alert type={AlertType.INFO}>
<span> <span>
{t("set.info.description")} <Translated i18nKey="set.info.description" namespace="passkey" />
<a <a
className="text-primary-light-500 dark:text-primary-dark-500 hover:text-primary-light-300 hover:dark:text-primary-dark-300" className="text-primary-light-500 dark:text-primary-dark-500 hover:text-primary-light-300 hover:dark:text-primary-dark-300"
target="_blank" target="_blank"
href="https://zitadel.com/docs/guides/manage/user/reg-create-user#with-passwordless" href="https://zitadel.com/docs/guides/manage/user/reg-create-user#with-passwordless"
> >
{t("set.info.link")} <Translated i18nKey="set.info.link" namespace="passkey" />
</a> </a>
</span> </span>
</Alert> </Alert>
{!session && ( {!session && (
<div className="py-4"> <div className="py-4">
<Alert>{tError("unknownContext")}</Alert> <Alert>
<Translated i18nKey="unknownContext" namespace="error" />
</Alert>
</div> </div>
)} )}

View File

@@ -1,6 +1,7 @@
import { Alert } from "@/components/alert"; import { Alert } from "@/components/alert";
import { ChangePasswordForm } from "@/components/change-password-form"; import { ChangePasswordForm } from "@/components/change-password-form";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { Translated } from "@/components/translated";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
@@ -9,7 +10,6 @@ import {
getLoginSettings, getLoginSettings,
getPasswordComplexitySettings, getPasswordComplexitySettings,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
export default async function Page(props: { export default async function Page(props: {
@@ -19,9 +19,6 @@ export default async function Page(props: {
const { serviceUrl } = getServiceUrlFromHeaders(_headers); const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const searchParams = await props.searchParams; 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, requestId } = searchParams; const { loginName, organization, requestId } = searchParams;
@@ -53,15 +50,21 @@ export default async function Page(props: {
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1> <h1>
{sessionFactors?.factors?.user?.displayName ?? t("change.title")} {sessionFactors?.factors?.user?.displayName ?? (
<Translated i18nKey="change.title" namespace="password" />
)}
</h1> </h1>
<p className="ztdl-p mb-6 block">{t("change.description")}</p> <p className="ztdl-p mb-6 block">
<Translated i18nKey="change.description" namespace="u2f" />
</p>
{/* show error only if usernames should be shown to be unknown */} {/* show error only if usernames should be shown to be unknown */}
{(!sessionFactors || !loginName) && {(!sessionFactors || !loginName) &&
!loginSettings?.ignoreUnknownUsernames && ( !loginSettings?.ignoreUnknownUsernames && (
<div className="py-4"> <div className="py-4">
<Alert>{tError("unknownContext")}</Alert> <Alert>
<Translated i18nKey="unknownContext" namespace="error" />
</Alert>
</div> </div>
)} )}
@@ -86,7 +89,9 @@ export default async function Page(props: {
/> />
) : ( ) : (
<div className="py-4"> <div className="py-4">
<Alert>{tError("failedLoading")}</Alert> <Alert>
<Translated i18nKey="failedLoading" namespace="error" />
</Alert>
</div> </div>
)} )}
</div> </div>

View File

@@ -1,6 +1,7 @@
import { Alert } from "@/components/alert"; import { Alert } from "@/components/alert";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { PasswordForm } from "@/components/password-form"; import { PasswordForm } from "@/components/password-form";
import { Translated } from "@/components/translated";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
@@ -11,17 +12,12 @@ import {
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
export default async function Page(props: { export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>; searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) { }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "password" });
const tError = await getTranslations({ locale, namespace: "error" });
let { loginName, organization, requestId, alt } = searchParams; let { loginName, organization, requestId, alt } = searchParams;
const _headers = await headers(); const _headers = await headers();
@@ -66,15 +62,21 @@ export default async function Page(props: {
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1> <h1>
{sessionFactors?.factors?.user?.displayName ?? t("verify.title")} {sessionFactors?.factors?.user?.displayName ?? (
<Translated i18nKey="verify.title" namespace="password" />
)}
</h1> </h1>
<p className="ztdl-p mb-6 block">{t("verify.description")}</p> <p className="ztdl-p mb-6 block">
<Translated i18nKey="verify.description" namespace="password" />
</p>
{/* show error only if usernames should be shown to be unknown */} {/* show error only if usernames should be shown to be unknown */}
{(!sessionFactors || !loginName) && {(!sessionFactors || !loginName) &&
!loginSettings?.ignoreUnknownUsernames && ( !loginSettings?.ignoreUnknownUsernames && (
<div className="py-4"> <div className="py-4">
<Alert>{tError("unknownContext")}</Alert> <Alert>
<Translated i18nKey="unknownContext" namespace="error" />
</Alert>
</div> </div>
)} )}

View File

@@ -1,6 +1,7 @@
import { Alert, AlertType } from "@/components/alert"; import { Alert, AlertType } from "@/components/alert";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { SetPasswordForm } from "@/components/set-password-form"; import { SetPasswordForm } from "@/components/set-password-form";
import { Translated } from "@/components/translated";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
@@ -12,7 +13,7 @@ import {
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { getLocale, getTranslations } from "next-intl/server"; import { getLocale } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
export default async function Page(props: { export default async function Page(props: {
@@ -20,8 +21,6 @@ export default async function Page(props: {
}) { }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale(); const locale = getLocale();
const t = await getTranslations({ locale, namespace: "password" });
const tError = await getTranslations({ locale, namespace: "error" });
const { userId, loginName, organization, requestId, code, initial } = const { userId, loginName, organization, requestId, code, initial } =
searchParams; searchParams;
@@ -73,13 +72,21 @@ export default async function Page(props: {
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{session?.factors?.user?.displayName ?? t("set.title")}</h1> <h1>
<p className="ztdl-p mb-6 block">{t("set.description")}</p> {session?.factors?.user?.displayName ?? (
<Translated i18nKey="set.title" namespace="password" />
)}
</h1>
<p className="ztdl-p mb-6 block">
<Translated i18nKey="set.description" namespace="password" />
</p>
{/* show error only if usernames should be shown to be unknown */} {/* show error only if usernames should be shown to be unknown */}
{loginName && !session && !loginSettings?.ignoreUnknownUsernames && ( {loginName && !session && !loginSettings?.ignoreUnknownUsernames && (
<div className="py-4"> <div className="py-4">
<Alert>{tError("unknownContext")}</Alert> <Alert>
<Translated i18nKey="unknownContext" namespace="error" />
</Alert>
</div> </div>
)} )}
@@ -99,7 +106,11 @@ export default async function Page(props: {
></UserAvatar> ></UserAvatar>
) : null} ) : null}
{!initial && <Alert type={AlertType.INFO}>{t("set.codeSent")}</Alert>} {!initial && (
<Alert type={AlertType.INFO}>
<Translated i18nKey="set.codeSent" namespace="password" />
</Alert>
)}
{passwordComplexity && {passwordComplexity &&
(loginName ?? user?.preferredLoginName) && (loginName ?? user?.preferredLoginName) &&
@@ -115,7 +126,9 @@ export default async function Page(props: {
/> />
) : ( ) : (
<div className="py-4"> <div className="py-4">
<Alert>{tError("failedLoading")}</Alert> <Alert>
<Translated i18nKey="failedLoading" namespace="error" />
</Alert>
</div> </div>
)} )}
</div> </div>

View File

@@ -2,6 +2,7 @@ import { Alert } from "@/components/alert";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { RegisterForm } from "@/components/register-form"; import { RegisterForm } from "@/components/register-form";
import { SignInWithIdp } from "@/components/sign-in-with-idp"; import { SignInWithIdp } from "@/components/sign-in-with-idp";
import { Translated } from "@/components/translated";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { import {
getActiveIdentityProviders, getActiveIdentityProviders,
@@ -13,7 +14,7 @@ import {
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { getLocale, getTranslations } from "next-intl/server"; import { getLocale } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
export default async function Page(props: { export default async function Page(props: {
@@ -21,8 +22,6 @@ export default async function Page(props: {
}) { }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale(); const locale = getLocale();
const t = await getTranslations({ locale, namespace: "register" });
const tError = await getTranslations({ locale, namespace: "error" });
let { firstname, lastname, email, organization, requestId } = searchParams; let { firstname, lastname, email, organization, requestId } = searchParams;
@@ -70,8 +69,12 @@ export default async function Page(props: {
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("disabled.title")}</h1> <h1>
<p className="ztdl-p">{t("disabled.description")}</p> <Translated i18nKey="disabled.title" namespace="register" />
</h1>
<p className="ztdl-p">
<Translated i18nKey="disabled.description" namespace="register" />
</p>
</div> </div>
</DynamicTheme> </DynamicTheme>
); );
@@ -80,10 +83,18 @@ export default async function Page(props: {
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("title")}</h1> <h1>
<p className="ztdl-p">{t("description")}</p> <Translated i18nKey="title" namespace="register" />
</h1>
<p className="ztdl-p">
<Translated i18nKey="description" namespace="register" />
</p>
{!organization && <Alert>{tError("unknownContext")}</Alert>} {!organization && (
<Alert>
<Translated i18nKey="unknownContext" namespace="error" />
</Alert>
)}
{legal && {legal &&
passwordComplexitySettings && passwordComplexitySettings &&
@@ -107,7 +118,9 @@ export default async function Page(props: {
{loginSettings?.allowExternalIdp && !!identityProviders.length && ( {loginSettings?.allowExternalIdp && !!identityProviders.length && (
<> <>
<div className="py-3 flex flex-col items-center"> <div className="py-3 flex flex-col items-center">
<p className="ztdl-p text-center">{t("orUseIDP")}</p> <p className="ztdl-p text-center">
<Translated i18nKey="orUseIDP" namespace="register" />
</p>
</div> </div>
<SignInWithIdp <SignInWithIdp

View File

@@ -1,5 +1,6 @@
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { SetRegisterPasswordForm } from "@/components/set-register-password-form"; import { SetRegisterPasswordForm } from "@/components/set-register-password-form";
import { Translated } from "@/components/translated";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { import {
getBrandingSettings, getBrandingSettings,
@@ -9,15 +10,12 @@ import {
getPasswordComplexitySettings, getPasswordComplexitySettings,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
export default async function Page(props: { export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>; searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) { }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "register" });
let { firstname, lastname, email, organization, requestId } = searchParams; let { firstname, lastname, email, organization, requestId } = searchParams;
@@ -57,15 +55,23 @@ export default async function Page(props: {
return missingData ? ( return missingData ? (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("missingdata.title")}</h1> <h1>
<p className="ztdl-p">{t("missingdata.description")}</p> <Translated i18nKey="missingdata.title" namespace="register" />
</h1>
<p className="ztdl-p">
<Translated i18nKey="missingdata.description" namespace="register" />
</p>
</div> </div>
</DynamicTheme> </DynamicTheme>
) : loginSettings?.allowRegister && loginSettings.allowUsernamePassword ? ( ) : loginSettings?.allowRegister && loginSettings.allowUsernamePassword ? (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("password.title")}</h1> <h1>
<p className="ztdl-p">{t("description")}</p> <Translated i18nKey="password.title" namespace="register" />
</h1>
<p className="ztdl-p">
<Translated i18nKey="description" namespace="register" />
</p>
{legal && passwordComplexitySettings && ( {legal && passwordComplexitySettings && (
<SetRegisterPasswordForm <SetRegisterPasswordForm
@@ -82,8 +88,12 @@ export default async function Page(props: {
) : ( ) : (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("disabled.title")}</h1> <h1>
<p className="ztdl-p">{t("disabled.description")}</p> <Translated i18nKey="disabled.title" namespace="register" />
</h1>
<p className="ztdl-p">
<Translated i18nKey="disabled.description" namespace="register" />
</p>
</div> </div>
</DynamicTheme> </DynamicTheme>
); );

View File

@@ -1,6 +1,7 @@
import { Alert, AlertType } from "@/components/alert"; import { Alert, AlertType } from "@/components/alert";
import { Button, ButtonVariants } from "@/components/button"; import { Button, ButtonVariants } from "@/components/button";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { Translated } from "@/components/translated";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { import {
getMostRecentCookieWithLoginname, getMostRecentCookieWithLoginname,
@@ -14,7 +15,6 @@ import {
getLoginSettings, getLoginSettings,
getSession, getSession,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
import Link from "next/link"; import Link from "next/link";
@@ -37,8 +37,6 @@ async function loadSessionById(
export default async function Page(props: { searchParams: Promise<any> }) { export default async function Page(props: { searchParams: Promise<any> }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "signedin" });
const _headers = await headers(); const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers); const { serviceUrl } = getServiceUrlFromHeaders(_headers);
@@ -66,8 +64,12 @@ export default async function Page(props: { searchParams: Promise<any> }) {
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("error.title")}</h1> <h1>
<p className="ztdl-p mb-6 block">{t("error.description")}</p> <Translated i18nKey="error.title" namespace="signedin" />
</h1>
<p className="ztdl-p mb-6 block">
<Translated i18nKey="error.description" namespace="signedin" />
</p>
<Alert>{err.message}</Alert> <Alert>{err.message}</Alert>
</div> </div>
</DynamicTheme> </DynamicTheme>
@@ -94,9 +96,15 @@ export default async function Page(props: { searchParams: Promise<any> }) {
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1> <h1>
{t("title", { user: sessionFactors?.factors?.user?.displayName })} <Translated
i18nKey="title"
namespace="signedin"
data={{ user: sessionFactors?.factors?.user?.displayName }}
/>
</h1> </h1>
<p className="ztdl-p mb-6 block">{t("description")}</p> <p className="ztdl-p mb-6 block">
<Translated i18nKey="description" namespace="signedin" />
</p>
<UserAvatar <UserAvatar
loginName={loginName ?? sessionFactors?.factors?.user?.loginName} loginName={loginName ?? sessionFactors?.factors?.user?.loginName}
@@ -122,7 +130,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
className="self-end" className="self-end"
variant={ButtonVariants.Primary} variant={ButtonVariants.Primary}
> >
{t("continue")} <Translated i18nKey="continue" namespace="signedin" />
</Button> </Button>
</Link> </Link>
</div> </div>

View File

@@ -1,12 +1,13 @@
import { Alert } from "@/components/alert"; import { Alert } from "@/components/alert";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { LoginPasskey } from "@/components/login-passkey"; import { LoginPasskey } from "@/components/login-passkey";
import { Translated } from "@/components/translated";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getSessionCookieById } from "@/lib/cookies"; import { getSessionCookieById } from "@/lib/cookies";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings, getSession } from "@/lib/zitadel"; import { getBrandingSettings, getSession } from "@/lib/zitadel";
import { getLocale, getTranslations } from "next-intl/server"; import { getLocale } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
export default async function Page(props: { export default async function Page(props: {
@@ -14,8 +15,6 @@ export default async function Page(props: {
}) { }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale(); const locale = getLocale();
const t = await getTranslations({ locale, namespace: "u2f" });
const tError = await getTranslations({ locale, namespace: "error" });
const { loginName, requestId, sessionId, organization } = searchParams; const { loginName, requestId, sessionId, organization } = searchParams;
@@ -59,7 +58,9 @@ export default async function Page(props: {
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("verify.title")}</h1> <h1>
<Translated i18nKey="verify.title" namespace="u2f" />
</h1>
{sessionFactors && ( {sessionFactors && (
<UserAvatar <UserAvatar
@@ -69,9 +70,15 @@ export default async function Page(props: {
searchParams={searchParams} searchParams={searchParams}
></UserAvatar> ></UserAvatar>
)} )}
<p className="ztdl-p mb-6 block">{t("verify.description")}</p> <p className="ztdl-p mb-6 block">
<Translated i18nKey="verify.description" namespace="u2f" />
</p>
{!(loginName || sessionId) && <Alert>{tError("unknownContext")}</Alert>} {!(loginName || sessionId) && (
<Alert>
<Translated i18nKey="unknownContext" namespace="error" />
</Alert>
)}
{(loginName || sessionId) && ( {(loginName || sessionId) && (
<LoginPasskey <LoginPasskey

View File

@@ -1,11 +1,12 @@
import { Alert } from "@/components/alert"; import { Alert } from "@/components/alert";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { RegisterU2f } from "@/components/register-u2f"; import { RegisterU2f } from "@/components/register-u2f";
import { Translated } from "@/components/translated";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings } from "@/lib/zitadel"; import { getBrandingSettings } from "@/lib/zitadel";
import { getLocale, getTranslations } from "next-intl/server"; import { getLocale } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
export default async function Page(props: { export default async function Page(props: {
@@ -13,8 +14,6 @@ export default async function Page(props: {
}) { }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale(); const locale = getLocale();
const t = await getTranslations({ locale, namespace: "u2f" });
const tError = await getTranslations({ locale, namespace: "error" });
const { loginName, organization, requestId, checkAfter } = searchParams; const { loginName, organization, requestId, checkAfter } = searchParams;
@@ -37,7 +36,9 @@ export default async function Page(props: {
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("set.title")}</h1> <h1>
<Translated i18nKey="set.title" namespace="u2f" />
</h1>
{sessionFactors && ( {sessionFactors && (
<UserAvatar <UserAvatar
@@ -47,11 +48,16 @@ export default async function Page(props: {
searchParams={searchParams} searchParams={searchParams}
></UserAvatar> ></UserAvatar>
)} )}
<p className="ztdl-p mb-6 block">{t("set.description")}</p> <p className="ztdl-p mb-6 block">
{" "}
<Translated i18nKey="set.description" namespace="u2f" />
</p>
{!sessionFactors && ( {!sessionFactors && (
<div className="py-4"> <div className="py-4">
<Alert>{tError("unknownContext")}</Alert> <Alert>
<Translated i18nKey="unknownContext" namespace="error" />
</Alert>
</div> </div>
)} )}

View File

@@ -1,5 +1,6 @@
import { Alert, AlertType } from "@/components/alert"; import { Alert, AlertType } from "@/components/alert";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { Translated } from "@/components/translated";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { VerifyForm } from "@/components/verify-form"; import { VerifyForm } from "@/components/verify-form";
import { sendEmailCode, sendInviteEmailCode } from "@/lib/server/verify"; import { sendEmailCode, sendInviteEmailCode } from "@/lib/server/verify";
@@ -7,14 +8,12 @@ import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings, getUserByID } from "@/lib/zitadel"; import { getBrandingSettings, getUserByID } from "@/lib/zitadel";
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { getLocale, getTranslations } from "next-intl/server"; import { getLocale } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
export default async function Page(props: { searchParams: Promise<any> }) { export default async function Page(props: { searchParams: Promise<any> }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale(); const locale = getLocale();
const t = await getTranslations({ locale, namespace: "verify" });
const tError = await getTranslations({ locale, namespace: "error" });
const { userId, loginName, code, organization, requestId, invite, send } = const { userId, loginName, code, organization, requestId, invite, send } =
searchParams; searchParams;
@@ -121,23 +120,26 @@ export default async function Page(props: { searchParams: Promise<any> }) {
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("verify.title")}</h1> <h1>
<p className="ztdl-p mb-6 block">{t("verify.description")}</p> <Translated i18nKey="verify.title" namespace="verify" />
</h1>
<p className="ztdl-p mb-6 block">
<Translated i18nKey="verify.description" namespace="verify" />
</p>
{!id && ( {!id && (
<>
<h1>{t("verify.title")}</h1>
<p className="ztdl-p mb-6 block">{t("verify.description")}</p>
<div className="py-4"> <div className="py-4">
<Alert>{tError("unknownContext")}</Alert> <Alert>
<Translated i18nKey="unknownContext" namespace="error" />
</Alert>
</div> </div>
</>
)} )}
{id && send && ( {id && send && (
<div className="py-4 w-full"> <div className="py-4 w-full">
<Alert type={AlertType.INFO}>{t("verify.codeSent")}</Alert> <Alert type={AlertType.INFO}>
<Translated i18nKey="verify.codeSent" namespace="verify" />
</Alert>
</div> </div>
)} )}

View File

@@ -1,4 +1,5 @@
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { Translated } from "@/components/translated";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
@@ -8,13 +9,10 @@ import {
getUserByID, getUserByID,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
export default async function Page(props: { searchParams: Promise<any> }) { export default async function Page(props: { searchParams: Promise<any> }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "verify" });
const _headers = await headers(); const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers); const { serviceUrl } = getServiceUrlFromHeaders(_headers);
@@ -65,8 +63,12 @@ export default async function Page(props: { searchParams: Promise<any> }) {
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("successTitle")}</h1> <h1>
<p className="ztdl-p mb-6 block">{t("successDescription")}</p> <Translated i18nKey="successTitle" namespace="verify" />
</h1>
<p className="ztdl-p mb-6 block">
<Translated i18nKey="successDescription" namespace="verify" />
</p>
{sessionFactors ? ( {sessionFactors ? (
<UserAvatar <UserAvatar

View File

@@ -3,7 +3,7 @@
import { Boundary } from "@/components/boundary"; import { Boundary } from "@/components/boundary";
import { Button } from "@/components/button"; import { Button } from "@/components/button";
import { ThemeWrapper } from "@/components/theme-wrapper"; import { ThemeWrapper } from "@/components/theme-wrapper";
import { useTranslations } from "next-intl"; import { Translated } from "@/components/translated";
export default function GlobalError({ export default function GlobalError({
error, error,
@@ -12,8 +12,6 @@ export default function GlobalError({
error: Error & { digest?: string }; error: Error & { digest?: string };
reset: () => void; reset: () => void;
}) { }) {
const t = useTranslations("error");
return ( return (
// global-error must include html and body tags // global-error must include html and body tags
<html> <html>
@@ -25,7 +23,9 @@ export default function GlobalError({
<span className="font-bold">Error:</span> {error?.message} <span className="font-bold">Error:</span> {error?.message}
</div> </div>
<div> <div>
<Button onClick={() => reset()}>{t("tryagain")}</Button> <Button data-i18n-key="error.tryagain" onClick={() => reset()}>
<Translated i18nKey="tryagain" namespace="error" />
</Button>
</div> </div>
</div> </div>
</Boundary> </Boundary>

View File

@@ -1,5 +1,5 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
export async function GET() { export async function GET() {
return NextResponse.json({}, { status: 200 }); return NextResponse.json({token: process.env.ZITADEL_SERVICE_USER_TOKEN}, { status: 200 });
} }

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { RadioGroup } from "@headlessui/react"; import { RadioGroup } from "@headlessui/react";
import { useTranslations } from "next-intl"; import { Translated } from "./translated";
export enum AuthenticationMethod { export enum AuthenticationMethod {
Passkey = "passkey", Passkey = "passkey",
@@ -20,8 +20,6 @@ export function AuthenticationMethodRadio({
selected: any; selected: any;
selectionChanged: (value: any) => void; selectionChanged: (value: any) => void;
}) { }) {
const t = useTranslations("register");
return ( return (
<div className="w-full"> <div className="w-full">
<div className="mx-auto w-full max-w-md"> <div className="mx-auto w-full max-w-md">
@@ -80,7 +78,18 @@ export function AuthenticationMethodRadio({
as="p" as="p"
className={`font-medium ${checked ? "" : ""}`} className={`font-medium ${checked ? "" : ""}`}
> >
{t(`methods.${method}`)} {method === AuthenticationMethod.Passkey && (
<Translated
i18nKey="methods.passkey"
namespace="register"
/>
)}
{method === AuthenticationMethod.Password && (
<Translated
i18nKey="methods.password"
namespace="register"
/>
)}
</RadioGroup.Label> </RadioGroup.Label>
</div> </div>
</> </>

View File

@@ -1,11 +1,10 @@
"use client"; "use client";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Button, ButtonVariants } from "./button"; import { Button, ButtonVariants } from "./button";
import { Translated } from "./translated";
export function BackButton() { export function BackButton() {
const t = useTranslations("common");
const router = useRouter(); const router = useRouter();
return ( return (
<Button <Button
@@ -13,7 +12,7 @@ export function BackButton() {
type="button" type="button"
variant={ButtonVariants.Secondary} variant={ButtonVariants.Secondary}
> >
{t("back")} <Translated i18nKey="back" namespace="common" />
</Button> </Button>
); );
} }

View File

@@ -13,7 +13,6 @@ import {
import { create } from "@zitadel/client"; import { create } from "@zitadel/client";
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb"; import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import { FieldValues, useForm } from "react-hook-form"; import { FieldValues, useForm } from "react-hook-form";
@@ -23,6 +22,7 @@ import { Button, ButtonVariants } from "./button";
import { TextInput } from "./input"; import { TextInput } from "./input";
import { PasswordComplexity } from "./password-complexity"; import { PasswordComplexity } from "./password-complexity";
import { Spinner } from "./spinner"; import { Spinner } from "./spinner";
import { Translated } from "./translated";
type Inputs = type Inputs =
| { | {
@@ -46,7 +46,6 @@ export function ChangePasswordForm({
requestId, requestId,
organization, organization,
}: Props) { }: Props) {
const t = useTranslations("password");
const router = useRouter(); const router = useRouter();
const { register, handleSubmit, watch, formState } = useForm<Inputs>({ const { register, handleSubmit, watch, formState } = useForm<Inputs>({
@@ -203,8 +202,8 @@ export function ChangePasswordForm({
onClick={handleSubmit(submitChange)} onClick={handleSubmit(submitChange)}
data-testid="submit-button" data-testid="submit-button"
> >
{loading && <Spinner className="h-5 w-5 mr-2" />} {loading && <Spinner className="h-5 w-5 mr-2" />}{" "}
{t("change.submit")} <Translated i18nKey="change.submit" namespace="password" />
</Button> </Button>
</div> </div>
</form> </form>

View File

@@ -3,8 +3,8 @@ import {
PasskeysType, PasskeysType,
} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { useTranslations } from "next-intl";
import { PASSKEYS, PASSWORD } from "./auth-methods"; import { PASSKEYS, PASSWORD } from "./auth-methods";
import { Translated } from "./translated";
type Props = { type Props = {
authMethods: AuthenticationMethodType[]; authMethods: AuthenticationMethodType[];
@@ -17,13 +17,13 @@ export function ChooseAuthenticatorToLogin({
params, params,
loginSettings, loginSettings,
}: Props) { }: Props) {
const t = useTranslations("idp");
return ( return (
<> <>
{authMethods.includes(AuthenticationMethodType.PASSWORD) && {authMethods.includes(AuthenticationMethodType.PASSWORD) &&
loginSettings?.allowUsernamePassword && ( loginSettings?.allowUsernamePassword && (
<div className="ztdl-p">Choose an alternative method to login </div> <div className="ztdl-p">
<Translated i18nKey="chooseAlternativeMethod" namespace="idp" />
</div>
)} )}
<div className="grid grid-cols-1 gap-5 w-full pt-4"> <div className="grid grid-cols-1 gap-5 w-full pt-4">
{authMethods.includes(AuthenticationMethodType.PASSWORD) && {authMethods.includes(AuthenticationMethodType.PASSWORD) &&

View File

@@ -3,9 +3,9 @@ import {
PasskeysType, PasskeysType,
} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { useTranslations } from "next-intl";
import { Alert, AlertType } from "./alert"; import { Alert, AlertType } from "./alert";
import { PASSKEYS, PASSWORD } from "./auth-methods"; import { PASSKEYS, PASSWORD } from "./auth-methods";
import { Translated } from "./translated";
type Props = { type Props = {
authMethods: AuthenticationMethodType[]; authMethods: AuthenticationMethodType[];
@@ -18,16 +18,23 @@ export function ChooseAuthenticatorToSetup({
params, params,
loginSettings, loginSettings,
}: Props) { }: Props) {
const t = useTranslations("authenticator");
if (authMethods.length !== 0) { if (authMethods.length !== 0) {
return <Alert type={AlertType.ALERT}>{t("allSetup")}</Alert>; return (
<Alert type={AlertType.ALERT}>
<Translated i18nKey="allSetup" namespace="authenticator" />
</Alert>
);
} else { } else {
return ( return (
<> <>
{loginSettings.passkeysType == PasskeysType.NOT_ALLOWED && {loginSettings.passkeysType == PasskeysType.NOT_ALLOWED &&
!loginSettings.allowUsernamePassword && ( !loginSettings.allowUsernamePassword && (
<Alert type={AlertType.ALERT}>{t("noMethodsAvailable")}</Alert> <Alert type={AlertType.ALERT}>
<Translated
i18nKey="noMethodsAvailable"
namespace="authenticator"
/>
</Alert>
)} )}
<div className="grid grid-cols-1 gap-5 w-full pt-4"> <div className="grid grid-cols-1 gap-5 w-full pt-4">

View File

@@ -6,9 +6,9 @@ import {
SecondFactorType, SecondFactorType,
} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { EMAIL, SMS, TOTP, U2F } from "./auth-methods"; import { EMAIL, SMS, TOTP, U2F } from "./auth-methods";
import { Translated } from "./translated";
type Props = { type Props = {
userId: string; userId: string;
@@ -37,7 +37,6 @@ export function ChooseSecondFactorToSetup({
emailVerified, emailVerified,
force, force,
}: Props) { }: Props) {
const t = useTranslations("mfa");
const router = useRouter(); const router = useRouter();
const params = new URLSearchParams({}); const params = new URLSearchParams({});
@@ -112,7 +111,7 @@ export function ChooseSecondFactorToSetup({
type="button" type="button"
data-testid="reset-button" data-testid="reset-button"
> >
{t("set.skip")} <Translated i18nKey="set.skip" namespace="mfa" />
</button> </button>
)} )}
</> </>

View File

@@ -8,6 +8,7 @@ import { useState } from "react";
import { Alert } from "./alert"; import { Alert } from "./alert";
import { Button, ButtonVariants } from "./button"; import { Button, ButtonVariants } from "./button";
import { Spinner } from "./spinner"; import { Spinner } from "./spinner";
import { Translated } from "./translated";
export function ConsentScreen({ export function ConsentScreen({
scope, scope,
@@ -50,7 +51,7 @@ export function ConsentScreen({
<ul className="list-disc space-y-2 w-full"> <ul className="list-disc space-y-2 w-full">
{scopes?.length === 0 && ( {scopes?.length === 0 && (
<span className="w-full text-sm flex flex-row items-center bg-background-light-400 dark:bg-background-dark-400 border border-divider-light py-2 px-4 rounded-md transition-all"> <span className="w-full text-sm flex flex-row items-center bg-background-light-400 dark:bg-background-dark-400 border border-divider-light py-2 px-4 rounded-md transition-all">
{t("device.scope.openid")} <Translated i18nKey="device.scope.openid" namespace="device" />
</span> </span>
)} )}
{scopes?.map((s) => { {scopes?.map((s) => {
@@ -73,7 +74,11 @@ export function ConsentScreen({
</ul> </ul>
<p className="ztdl-p text-xs text-left"> <p className="ztdl-p text-xs text-left">
{t("device.request.disclaimer", { appName: appName })} <Translated
i18nKey="request.disclaimer"
namespace="device"
data={{ appName: appName }}
/>
</p> </p>
{error && ( {error && (
@@ -91,7 +96,7 @@ export function ConsentScreen({
data-testid="deny-button" data-testid="deny-button"
> >
{loading && <Spinner className="h-5 w-5 mr-2" />} {loading && <Spinner className="h-5 w-5 mr-2" />}
{t("device.request.deny")} <Translated i18nKey="device.request.deny" namespace="device" />
</Button> </Button>
<span className="flex-grow"></span> <span className="flex-grow"></span>
@@ -102,7 +107,7 @@ export function ConsentScreen({
className="self-end" className="self-end"
variant={ButtonVariants.Primary} variant={ButtonVariants.Primary}
> >
{t("device.request.submit")} <Translated i18nKey="device.request.submit" namespace="device" />
</Button> </Button>
</Link> </Link>
</div> </div>

View File

@@ -2,7 +2,6 @@
import { Alert } from "@/components/alert"; import { Alert } from "@/components/alert";
import { getDeviceAuthorizationRequest } from "@/lib/server/oidc"; import { getDeviceAuthorizationRequest } from "@/lib/server/oidc";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@@ -10,14 +9,13 @@ import { BackButton } from "./back-button";
import { Button, ButtonVariants } from "./button"; import { Button, ButtonVariants } from "./button";
import { TextInput } from "./input"; import { TextInput } from "./input";
import { Spinner } from "./spinner"; import { Spinner } from "./spinner";
import { Translated } from "./translated";
type Inputs = { type Inputs = {
userCode: string; userCode: string;
}; };
export function DeviceCodeForm({ userCode }: { userCode?: string }) { export function DeviceCodeForm({ userCode }: { userCode?: string }) {
const t = useTranslations("verify");
const router = useRouter(); const router = useRouter();
const { register, handleSubmit, formState } = useForm<Inputs>({ const { register, handleSubmit, formState } = useForm<Inputs>({
@@ -87,8 +85,8 @@ export function DeviceCodeForm({ userCode }: { userCode?: string }) {
onClick={handleSubmit(submitCodeAndContinue)} onClick={handleSubmit(submitCodeAndContinue)}
data-testid="submit-button" data-testid="submit-button"
> >
{loading && <Spinner className="h-5 w-5 mr-2" />} {loading && <Spinner className="h-5 w-5 mr-2" />}{" "}
{t("verify.submit")} <Translated i18nKey="verify.submit" namespace="verify" />
</Button> </Button>
</div> </div>
</form> </form>

View File

@@ -1,8 +1,8 @@
import { RegisterFormIDPIncomplete } from "@/components/register-form-idp-incomplete"; import { RegisterFormIDPIncomplete } from "@/components/register-form-idp-incomplete";
import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb"; import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb";
import { AddHumanUserRequest } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { AddHumanUserRequest } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { DynamicTheme } from "../../dynamic-theme"; import { DynamicTheme } from "../../dynamic-theme";
import { Translated } from "../../translated";
export async function completeIDP({ export async function completeIDP({
idpUserId, idpUserId,
@@ -26,14 +26,15 @@ export async function completeIDP({
idpIntentToken: string; idpIntentToken: string;
}; };
}) { }) {
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "idp" });
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("completeRegister.title")}</h1> <h1>
<p className="ztdl-p">{t("completeRegister.description")}</p> <Translated i18nKey="completeRegister.title" namespace="idp" />
</h1>
<p className="ztdl-p">
<Translated i18nKey="completeRegister.description" namespace="idp" />
</p>
<RegisterFormIDPIncomplete <RegisterFormIDPIncomplete
idpUserId={idpUserId} idpUserId={idpUserId}

View File

@@ -1,20 +1,21 @@
import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb"; import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { Alert, AlertType } from "../../alert"; import { Alert, AlertType } from "../../alert";
import { DynamicTheme } from "../../dynamic-theme"; import { DynamicTheme } from "../../dynamic-theme";
import { Translated } from "../../translated";
export async function linkingFailed( export async function linkingFailed(
branding?: BrandingSettings, branding?: BrandingSettings,
error?: string, error?: string,
) { ) {
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "idp" });
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("linkingError.title")}</h1> <h1>
<p className="ztdl-p">{t("linkingError.description")}</p> <Translated i18nKey="linkingError.title" namespace="idp" />
</h1>
<p className="ztdl-p">
<Translated i18nKey="linkingError.description" namespace="idp" />
</p>
{error && ( {error && (
<div className="w-full"> <div className="w-full">
{<Alert type={AlertType.ALERT}>{error}</Alert>} {<Alert type={AlertType.ALERT}>{error}</Alert>}

View File

@@ -1,7 +1,7 @@
import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb"; import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { DynamicTheme } from "../../dynamic-theme"; import { DynamicTheme } from "../../dynamic-theme";
import { IdpSignin } from "../../idp-signin"; import { IdpSignin } from "../../idp-signin";
import { Translated } from "../../translated";
export async function linkingSuccess( export async function linkingSuccess(
userId: string, userId: string,
@@ -9,14 +9,15 @@ export async function linkingSuccess(
requestId?: string, requestId?: string,
branding?: BrandingSettings, branding?: BrandingSettings,
) { ) {
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "idp" });
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("linkingSuccess.title")}</h1> <h1>
<p className="ztdl-p">{t("linkingSuccess.description")}</p> <Translated i18nKey="linkingSuccess.title" namespace="idp" />
</h1>
<p className="ztdl-p">
<Translated i18nKey="linkingSuccess.description" namespace="idp" />
</p>
<IdpSignin <IdpSignin
userId={userId} userId={userId}

View File

@@ -1,17 +1,18 @@
import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb"; import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { Alert, AlertType } from "../../alert"; import { Alert, AlertType } from "../../alert";
import { DynamicTheme } from "../../dynamic-theme"; import { DynamicTheme } from "../../dynamic-theme";
import { Translated } from "../../translated";
export async function loginFailed(branding?: BrandingSettings, error?: string) { export async function loginFailed(branding?: BrandingSettings, error?: string) {
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "idp" });
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("loginError.title")}</h1> <h1>
<p className="ztdl-p">{t("loginError.description")}</p> <Translated i18nKey="loginError.title" namespace="idp" />
</h1>
<p className="ztdl-p">
<Translated i18nKey="loginError.description" namespace="idp" />
</p>
{error && ( {error && (
<div className="w-full"> <div className="w-full">
{<Alert type={AlertType.ALERT}>{error}</Alert>} {<Alert type={AlertType.ALERT}>{error}</Alert>}

View File

@@ -1,7 +1,7 @@
import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb"; import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { DynamicTheme } from "../../dynamic-theme"; import { DynamicTheme } from "../../dynamic-theme";
import { IdpSignin } from "../../idp-signin"; import { IdpSignin } from "../../idp-signin";
import { Translated } from "../../translated";
export async function loginSuccess( export async function loginSuccess(
userId: string, userId: string,
@@ -9,14 +9,15 @@ export async function loginSuccess(
requestId?: string, requestId?: string,
branding?: BrandingSettings, branding?: BrandingSettings,
) { ) {
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "idp" });
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("loginSuccess.title")}</h1> <h1>
<p className="ztdl-p">{t("loginSuccess.description")}</p> <Translated i18nKey="loginSuccess.title" namespace="idp" />
</h1>
<p className="ztdl-p">
<Translated i18nKey="loginSuccess.description" namespace="idp" />
</p>
<IdpSignin <IdpSignin
userId={userId} userId={userId}

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { useTranslations } from "next-intl";
import { forwardRef } from "react"; import { forwardRef } from "react";
import { Translated } from "../translated";
import { BaseButton, SignInWithIdentityProviderProps } from "./base-button"; import { BaseButton, SignInWithIdentityProviderProps } from "./base-button";
export const SignInWithApple = forwardRef< export const SignInWithApple = forwardRef<
@@ -9,7 +9,6 @@ export const SignInWithApple = forwardRef<
SignInWithIdentityProviderProps SignInWithIdentityProviderProps
>(function SignInWithApple(props, ref) { >(function SignInWithApple(props, ref) {
const { children, name, ...restProps } = props; const { children, name, ...restProps } = props;
const t = useTranslations("idp");
return ( return (
<BaseButton {...restProps} ref={ref}> <BaseButton {...restProps} ref={ref}>
@@ -24,7 +23,13 @@ export const SignInWithApple = forwardRef<
{children ? ( {children ? (
children children
) : ( ) : (
<span className="ml-4">{name ? name : t("signInWithApple")}</span> <span className="ml-4">
{name ? (
name
) : (
<Translated i18nKey="signInWithApple" namespace="idp" />
)}
</span>
)} )}
</BaseButton> </BaseButton>
); );

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { useTranslations } from "next-intl";
import { forwardRef } from "react"; import { forwardRef } from "react";
import { Translated } from "../translated";
import { BaseButton, SignInWithIdentityProviderProps } from "./base-button"; import { BaseButton, SignInWithIdentityProviderProps } from "./base-button";
export const SignInWithAzureAd = forwardRef< export const SignInWithAzureAd = forwardRef<
@@ -9,7 +9,6 @@ export const SignInWithAzureAd = forwardRef<
SignInWithIdentityProviderProps SignInWithIdentityProviderProps
>(function SignInWithAzureAd(props, ref) { >(function SignInWithAzureAd(props, ref) {
const { children, name, ...restProps } = props; const { children, name, ...restProps } = props;
const t = useTranslations("idp");
return ( return (
<BaseButton {...restProps} ref={ref}> <BaseButton {...restProps} ref={ref}>
@@ -30,7 +29,13 @@ export const SignInWithAzureAd = forwardRef<
{children ? ( {children ? (
children children
) : ( ) : (
<span className="ml-4">{name ? name : t("signInWithAzureAD")}</span> <span className="ml-4">
{name ? (
name
) : (
<Translated i18nKey="signInWithAzureAD" namespace="idp" />
)}
</span>
)} )}
</BaseButton> </BaseButton>
); );

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { useTranslations } from "next-intl";
import { forwardRef } from "react"; import { forwardRef } from "react";
import { Translated } from "../translated";
import { BaseButton, SignInWithIdentityProviderProps } from "./base-button"; import { BaseButton, SignInWithIdentityProviderProps } from "./base-button";
function GitHubLogo() { function GitHubLogo() {
@@ -42,7 +42,6 @@ export const SignInWithGithub = forwardRef<
SignInWithIdentityProviderProps SignInWithIdentityProviderProps
>(function SignInWithGithub(props, ref) { >(function SignInWithGithub(props, ref) {
const { children, name, ...restProps } = props; const { children, name, ...restProps } = props;
const t = useTranslations("idp");
return ( return (
<BaseButton {...restProps} ref={ref}> <BaseButton {...restProps} ref={ref}>
@@ -52,7 +51,13 @@ export const SignInWithGithub = forwardRef<
{children ? ( {children ? (
children children
) : ( ) : (
<span className="ml-4">{name ? name : t("signInWithGithub")}</span> <span className="ml-4">
{name ? (
name
) : (
<Translated i18nKey="signInWithGithub" namespace="idp" />
)}
</span>
)} )}
</BaseButton> </BaseButton>
); );

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { useTranslations } from "next-intl";
import { forwardRef } from "react"; import { forwardRef } from "react";
import { Translated } from "../translated";
import { BaseButton, SignInWithIdentityProviderProps } from "./base-button"; import { BaseButton, SignInWithIdentityProviderProps } from "./base-button";
export const SignInWithGitlab = forwardRef< export const SignInWithGitlab = forwardRef<
@@ -9,7 +9,6 @@ export const SignInWithGitlab = forwardRef<
SignInWithIdentityProviderProps SignInWithIdentityProviderProps
>(function SignInWithGitlab(props, ref) { >(function SignInWithGitlab(props, ref) {
const { children, name, ...restProps } = props; const { children, name, ...restProps } = props;
const t = useTranslations("idp");
return ( return (
<BaseButton {...restProps} ref={ref}> <BaseButton {...restProps} ref={ref}>
@@ -41,7 +40,13 @@ export const SignInWithGitlab = forwardRef<
{children ? ( {children ? (
children children
) : ( ) : (
<span className="ml-4">{name ? name : t("signInWithGitlab")}</span> <span className="ml-4">
{name ? (
name
) : (
<Translated i18nKey="signInWithGitlab" namespace="idp" />
)}
</span>
)} )}
</BaseButton> </BaseButton>
); );

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { useTranslations } from "next-intl";
import { forwardRef } from "react"; import { forwardRef } from "react";
import { Translated } from "../translated";
import { BaseButton, SignInWithIdentityProviderProps } from "./base-button"; import { BaseButton, SignInWithIdentityProviderProps } from "./base-button";
export const SignInWithGoogle = forwardRef< export const SignInWithGoogle = forwardRef<
@@ -9,7 +9,6 @@ export const SignInWithGoogle = forwardRef<
SignInWithIdentityProviderProps SignInWithIdentityProviderProps
>(function SignInWithGoogle(props, ref) { >(function SignInWithGoogle(props, ref) {
const { children, name, ...restProps } = props; const { children, name, ...restProps } = props;
const t = useTranslations("idp");
return ( return (
<BaseButton {...restProps} ref={ref}> <BaseButton {...restProps} ref={ref}>
@@ -54,7 +53,13 @@ export const SignInWithGoogle = forwardRef<
{children ? ( {children ? (
children children
) : ( ) : (
<span className="ml-4">{name ? name : t("signInWithGoogle")}</span> <span className="ml-4">
{name ? (
name
) : (
<Translated i18nKey="signInWithGoogle" namespace="idp" />
)}
</span>
)} )}
</BaseButton> </BaseButton>
); );

View File

@@ -6,7 +6,6 @@ import { create } from "@zitadel/client";
import { RequestChallengesSchema } from "@zitadel/proto/zitadel/session/v2/challenge_pb"; import { RequestChallengesSchema } from "@zitadel/proto/zitadel/session/v2/challenge_pb";
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@@ -15,6 +14,7 @@ import { BackButton } from "./back-button";
import { Button, ButtonVariants } from "./button"; import { Button, ButtonVariants } from "./button";
import { TextInput } from "./input"; import { TextInput } from "./input";
import { Spinner } from "./spinner"; import { Spinner } from "./spinner";
import { Translated } from "./translated";
// either loginName or sessionId must be provided // either loginName or sessionId must be provided
type Props = { type Props = {
@@ -42,8 +42,6 @@ export function LoginOTP({
code, code,
loginSettings, loginSettings,
}: Props) { }: Props) {
const t = useTranslations("otp");
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
@@ -223,7 +221,7 @@ export function LoginOTP({
<Alert type={AlertType.INFO}> <Alert type={AlertType.INFO}>
<div className="flex flex-row"> <div className="flex flex-row">
<span className="flex-1 mr-auto text-left"> <span className="flex-1 mr-auto text-left">
{t("verify.noCodeReceived")} <Translated i18nKey="verify.noCodeReceived" namespace="otp" />
</span> </span>
<button <button
aria-label="Resend OTP Code" aria-label="Resend OTP Code"
@@ -243,7 +241,7 @@ export function LoginOTP({
}} }}
data-testid="resend-button" data-testid="resend-button"
> >
{t("verify.resendCode")} <Translated i18nKey="verify.resendCode" namespace="otp" />
</button> </button>
</div> </div>
</Alert> </Alert>
@@ -277,8 +275,8 @@ export function LoginOTP({
})} })}
data-testid="submit-button" data-testid="submit-button"
> >
{loading && <Spinner className="h-5 w-5 mr-2" />} {loading && <Spinner className="h-5 w-5 mr-2" />}{" "}
{t("verify.submit")} <Translated i18nKey="verify.submit" namespace="otp" />
</Button> </Button>
</div> </div>
</form> </form>

View File

@@ -9,13 +9,13 @@ import {
UserVerificationRequirement, UserVerificationRequirement,
} from "@zitadel/proto/zitadel/session/v2/challenge_pb"; } from "@zitadel/proto/zitadel/session/v2/challenge_pb";
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { Alert } from "./alert"; import { Alert } from "./alert";
import { BackButton } from "./back-button"; import { BackButton } from "./back-button";
import { Button, ButtonVariants } from "./button"; import { Button, ButtonVariants } from "./button";
import { Spinner } from "./spinner"; import { Spinner } from "./spinner";
import { Translated } from "./translated";
// either loginName or sessionId must be provided // either loginName or sessionId must be provided
type Props = { type Props = {
@@ -35,8 +35,6 @@ export function LoginPasskey({
organization, organization,
login = true, login = true,
}: Props) { }: Props) {
const t = useTranslations("passkey");
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
@@ -234,7 +232,7 @@ export function LoginPasskey({
}} }}
data-testid="password-button" data-testid="password-button"
> >
{t("verify.usePassword")} <Translated i18nKey="verify.usePassword" namespace="passkey" />
</Button> </Button>
) : ( ) : (
<BackButton /> <BackButton />
@@ -273,8 +271,8 @@ export function LoginPasskey({
}} }}
data-testid="submit-button" data-testid="submit-button"
> >
{loading && <Spinner className="h-5 w-5 mr-2" />} {loading && <Spinner className="h-5 w-5 mr-2" />}{" "}
{t("verify.submit")} <Translated i18nKey="verify.submit" namespace="passkey" />
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -4,7 +4,6 @@ import { resetPassword, sendPassword } from "@/lib/server/password";
import { create } from "@zitadel/client"; import { create } from "@zitadel/client";
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@@ -13,6 +12,7 @@ import { BackButton } from "./back-button";
import { Button, ButtonVariants } from "./button"; import { Button, ButtonVariants } from "./button";
import { TextInput } from "./input"; import { TextInput } from "./input";
import { Spinner } from "./spinner"; import { Spinner } from "./spinner";
import { Translated } from "./translated";
type Inputs = { type Inputs = {
password: string; password: string;
@@ -35,8 +35,6 @@ export function PasswordForm({
promptPasswordless, promptPasswordless,
isAlternative, isAlternative,
}: Props) { }: Props) {
const t = useTranslations("password");
const { register, handleSubmit, formState } = useForm<Inputs>({ const { register, handleSubmit, formState } = useForm<Inputs>({
mode: "onBlur", mode: "onBlur",
}); });
@@ -136,7 +134,7 @@ export function PasswordForm({
disabled={loading} disabled={loading}
data-testid="reset-button" data-testid="reset-button"
> >
{t("verify.resetPassword")} <Translated i18nKey="verify.resetPassword" namespace="password" />
</button> </button>
)} )}
@@ -173,8 +171,8 @@ export function PasswordForm({
onClick={handleSubmit(submitPassword)} onClick={handleSubmit(submitPassword)}
data-testid="submit-button" data-testid="submit-button"
> >
{loading && <Spinner className="h-5 w-5 mr-2" />} {loading && <Spinner className="h-5 w-5 mr-2" />}{" "}
{t("verify.submit")} <Translated i18nKey="verify.submit" namespace="password" />
</Button> </Button>
</div> </div>
</form> </form>

View File

@@ -1,9 +1,9 @@
"use client"; "use client";
import { LegalAndSupportSettings } from "@zitadel/proto/zitadel/settings/v2/legal_settings_pb"; import { LegalAndSupportSettings } from "@zitadel/proto/zitadel/settings/v2/legal_settings_pb";
import { useTranslations } from "next-intl";
import Link from "next/link"; import Link from "next/link";
import { useState } from "react"; import { useState } from "react";
import { Checkbox } from "./checkbox"; import { Checkbox } from "./checkbox";
import { Translated } from "./translated";
type Props = { type Props = {
legal: LegalAndSupportSettings; legal: LegalAndSupportSettings;
@@ -16,7 +16,6 @@ type AcceptanceState = {
}; };
export function PrivacyPolicyCheckboxes({ legal, onChange }: Props) { export function PrivacyPolicyCheckboxes({ legal, onChange }: Props) {
const t = useTranslations("register");
const [acceptanceState, setAcceptanceState] = useState<AcceptanceState>({ const [acceptanceState, setAcceptanceState] = useState<AcceptanceState>({
tosAccepted: false, tosAccepted: false,
privacyPolicyAccepted: false, privacyPolicyAccepted: false,
@@ -25,7 +24,7 @@ export function PrivacyPolicyCheckboxes({ legal, onChange }: Props) {
return ( return (
<> <>
<p className="flex flex-row items-center text-text-light-secondary-500 dark:text-text-dark-secondary-500 mt-4 text-sm"> <p className="flex flex-row items-center text-text-light-secondary-500 dark:text-text-dark-secondary-500 mt-4 text-sm">
{t("agreeTo")} <Translated i18nKey="agreeTo" namespace="register" />
{legal?.helpLink && ( {legal?.helpLink && (
<span> <span>
<Link href={legal.helpLink} target="_blank"> <Link href={legal.helpLink} target="_blank">
@@ -66,7 +65,7 @@ export function PrivacyPolicyCheckboxes({ legal, onChange }: Props) {
<div className="mr-4 w-[28rem]"> <div className="mr-4 w-[28rem]">
<p className="text-sm text-text-light-500 dark:text-text-dark-500"> <p className="text-sm text-text-light-500 dark:text-text-dark-500">
<Link href={legal.tosLink} className="underline" target="_blank"> <Link href={legal.tosLink} className="underline" target="_blank">
{t("termsOfService")} <Translated i18nKey="termsOfService" namespace="register" />
</Link> </Link>
</p> </p>
</div> </div>
@@ -95,7 +94,7 @@ export function PrivacyPolicyCheckboxes({ legal, onChange }: Props) {
className="underline" className="underline"
target="_blank" target="_blank"
> >
{t("privacyPolicy")} <Translated i18nKey="privacyPolicy" namespace="register" />
</Link> </Link>
</p> </p>
</div> </div>

View File

@@ -1,7 +1,6 @@
"use client"; "use client";
import { registerUserAndLinkToIDP } from "@/lib/server/register"; import { registerUserAndLinkToIDP } from "@/lib/server/register";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import { FieldValues, useForm } from "react-hook-form"; import { FieldValues, useForm } from "react-hook-form";
@@ -10,6 +9,7 @@ import { BackButton } from "./back-button";
import { Button, ButtonVariants } from "./button"; import { Button, ButtonVariants } from "./button";
import { TextInput } from "./input"; import { TextInput } from "./input";
import { Spinner } from "./spinner"; import { Spinner } from "./spinner";
import { Translated } from "./translated";
type Inputs = type Inputs =
| { | {
@@ -45,8 +45,6 @@ export function RegisterFormIDPIncomplete({
idpId, idpId,
idpUserName, idpUserName,
}: Props) { }: Props) {
const t = useTranslations("register");
const { register, handleSubmit, formState } = useForm<Inputs>({ const { register, handleSubmit, formState } = useForm<Inputs>({
mode: "onBlur", mode: "onBlur",
defaultValues: { defaultValues: {
@@ -149,8 +147,8 @@ export function RegisterFormIDPIncomplete({
onClick={handleSubmit(submitAndRegister)} onClick={handleSubmit(submitAndRegister)}
data-testid="submit-button" data-testid="submit-button"
> >
{loading && <Spinner className="h-5 w-5 mr-2" />} {loading && <Spinner className="h-5 w-5 mr-2" />}{" "}
{t("submit")} <Translated i18nKey="submit" namespace="register" />
</Button> </Button>
</div> </div>
</form> </form>

View File

@@ -6,7 +6,6 @@ import {
LoginSettings, LoginSettings,
PasskeysType, PasskeysType,
} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import { FieldValues, useForm } from "react-hook-form"; import { FieldValues, useForm } from "react-hook-form";
@@ -21,6 +20,7 @@ import { Button, ButtonVariants } from "./button";
import { TextInput } from "./input"; import { TextInput } from "./input";
import { PrivacyPolicyCheckboxes } from "./privacy-policy-checkboxes"; import { PrivacyPolicyCheckboxes } from "./privacy-policy-checkboxes";
import { Spinner } from "./spinner"; import { Spinner } from "./spinner";
import { Translated } from "./translated";
type Inputs = type Inputs =
| { | {
@@ -51,8 +51,6 @@ export function RegisterForm({
loginSettings, loginSettings,
idpCount = 0, idpCount = 0,
}: Props) { }: Props) {
const t = useTranslations("register");
const { register, handleSubmit, formState } = useForm<Inputs>({ const { register, handleSubmit, formState } = useForm<Inputs>({
mode: "onBlur", mode: "onBlur",
defaultValues: { defaultValues: {
@@ -173,7 +171,7 @@ export function RegisterForm({
loginSettings.passkeysType == PasskeysType.ALLOWED && ( loginSettings.passkeysType == PasskeysType.ALLOWED && (
<> <>
<p className="mt-4 ztdl-p mb-6 block text-left"> <p className="mt-4 ztdl-p mb-6 block text-left">
{t("selectMethod")} <Translated i18nKey="selectMethod" namespace="register" />
</p> </p>
<div className="pb-4"> <div className="pb-4">
@@ -186,10 +184,15 @@ export function RegisterForm({
)} )}
{!loginSettings?.allowUsernamePassword && {!loginSettings?.allowUsernamePassword &&
loginSettings?.passkeysType != PasskeysType.ALLOWED && loginSettings?.passkeysType !== PasskeysType.ALLOWED &&
(!loginSettings?.allowExternalIdp || !idpCount) && ( (!loginSettings?.allowExternalIdp || !idpCount) && (
<div className="py-4"> <div className="py-4">
<Alert type={AlertType.INFO}>{t("noMethodAvailableWarning")}</Alert> <Alert type={AlertType.INFO}>
<Translated
i18nKey="noMethodAvailableWarning"
namespace="register"
/>
</Alert>
</div> </div>
)} )}
@@ -217,7 +220,7 @@ export function RegisterForm({
data-testid="submit-button" data-testid="submit-button"
> >
{loading && <Spinner className="h-5 w-5 mr-2" />} {loading && <Spinner className="h-5 w-5 mr-2" />}
{t("submit")} <Translated i18nKey="submit" namespace="register" />
</Button> </Button>
</div> </div>
</form> </form>

View File

@@ -5,7 +5,6 @@ import {
registerPasskeyLink, registerPasskeyLink,
verifyPasskeyRegistration, verifyPasskeyRegistration,
} from "@/lib/server/passkeys"; } from "@/lib/server/passkeys";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@@ -13,6 +12,7 @@ import { Alert } from "./alert";
import { BackButton } from "./back-button"; import { BackButton } from "./back-button";
import { Button, ButtonVariants } from "./button"; import { Button, ButtonVariants } from "./button";
import { Spinner } from "./spinner"; import { Spinner } from "./spinner";
import { Translated } from "./translated";
type Inputs = {}; type Inputs = {};
@@ -29,8 +29,6 @@ export function RegisterPasskey({
organization, organization,
requestId, requestId,
}: Props) { }: Props) {
const t = useTranslations("passkey");
const { handleSubmit, formState } = useForm<Inputs>({ const { handleSubmit, formState } = useForm<Inputs>({
mode: "onBlur", mode: "onBlur",
}); });
@@ -198,7 +196,7 @@ export function RegisterPasskey({
continueAndLogin(); continueAndLogin();
}} }}
> >
{t("set.skip")} <Translated i18nKey="set.skip" namespace="passkey" />
</Button> </Button>
) : ( ) : (
<BackButton /> <BackButton />
@@ -213,8 +211,8 @@ export function RegisterPasskey({
onClick={handleSubmit(submitRegisterAndContinue)} onClick={handleSubmit(submitRegisterAndContinue)}
data-testid="submit-button" data-testid="submit-button"
> >
{loading && <Spinner className="h-5 w-5 mr-2" />} {loading && <Spinner className="h-5 w-5 mr-2" />}{" "}
{t("set.submit")} <Translated i18nKey="set.submit" namespace="passkey" />
</Button> </Button>
</div> </div>
</form> </form>

View File

@@ -5,13 +5,13 @@ import { getNextUrl } from "@/lib/client";
import { addU2F, verifyU2F } from "@/lib/server/u2f"; import { addU2F, verifyU2F } from "@/lib/server/u2f";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { RegisterU2FResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { RegisterU2FResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import { Alert } from "./alert"; import { Alert } from "./alert";
import { BackButton } from "./back-button"; import { BackButton } from "./back-button";
import { Button, ButtonVariants } from "./button"; import { Button, ButtonVariants } from "./button";
import { Spinner } from "./spinner"; import { Spinner } from "./spinner";
import { Translated } from "./translated";
type Props = { type Props = {
loginName?: string; loginName?: string;
@@ -30,8 +30,6 @@ export function RegisterU2f({
checkAfter, checkAfter,
loginSettings, loginSettings,
}: Props) { }: Props) {
const t = useTranslations("u2f");
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
@@ -218,8 +216,8 @@ export function RegisterU2f({
onClick={submitRegisterAndContinue} onClick={submitRegisterAndContinue}
data-testid="submit-button" data-testid="submit-button"
> >
{loading && <Spinner className="h-5 w-5 mr-2" />} {loading && <Spinner className="h-5 w-5 mr-2" />}{" "}
{t("set.submit")} <Translated i18nKey="set.submit" namespace="u2f" />
</Button> </Button>
</div> </div>
</form> </form>

View File

@@ -4,11 +4,12 @@ import { clearSession } from "@/lib/server/session";
import { timestampDate } from "@zitadel/client"; import { timestampDate } from "@zitadel/client";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import moment from "moment"; import moment from "moment";
import { useLocale, useTranslations } from "next-intl"; import { useLocale } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import { Avatar } from "./avatar"; import { Avatar } from "./avatar";
import { isSessionValid } from "./session-item"; import { isSessionValid } from "./session-item";
import { Translated } from "./translated";
export function SessionClearItem({ export function SessionClearItem({
session, session,
@@ -17,8 +18,6 @@ export function SessionClearItem({
session: Session; session: Session;
reload: () => void; reload: () => void;
}) { }) {
const t = useTranslations("logout");
const currentLocale = useLocale(); const currentLocale = useLocale();
moment.locale(currentLocale === "zh" ? "zh-cn" : currentLocale); moment.locale(currentLocale === "zh" ? "zh-cn" : currentLocale);
@@ -70,10 +69,13 @@ export function SessionClearItem({
</span> </span>
{valid ? ( {valid ? (
<span className="text-xs opacity-80 text-ellipsis"> <span className="text-xs opacity-80 text-ellipsis">
{verifiedAt && {verifiedAt && (
t("verfiedAt", { <Translated
time: moment(timestampDate(verifiedAt)).fromNow(), i18nKey="verfiedAt"
})} namespace="logout"
data={{ time: moment(timestampDate(verifiedAt)).fromNow() }}
/>
)}
</span> </span>
) : ( ) : (
verifiedAt && ( verifiedAt && (
@@ -89,7 +91,7 @@ export function SessionClearItem({
<span className="flex-grow"></span> <span className="flex-grow"></span>
<div className="relative flex flex-row items-center"> <div className="relative flex flex-row items-center">
<div className="mr-6 px-2 py-[2px] text-xs hidden group-hover:block transition-all text-warn-light-500 dark:text-warn-dark-500 bg-[#ff0000]/10 dark:bg-[#ff0000]/10 rounded-full flex items-center justify-center"> <div className="mr-6 px-2 py-[2px] text-xs hidden group-hover:block transition-all text-warn-light-500 dark:text-warn-dark-500 bg-[#ff0000]/10 dark:bg-[#ff0000]/10 rounded-full flex items-center justify-center">
{t("clear")} <Translated i18nKey="clear" namespace="logout" />
</div> </div>
{valid ? ( {valid ? (

View File

@@ -3,11 +3,11 @@
import { clearSession } from "@/lib/server/session"; import { clearSession } from "@/lib/server/session";
import { timestampDate } from "@zitadel/client"; import { timestampDate } from "@zitadel/client";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { useTranslations } from "next-intl";
import { redirect, useRouter } from "next/navigation"; import { redirect, useRouter } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Alert, AlertType } from "./alert"; import { Alert, AlertType } from "./alert";
import { SessionClearItem } from "./session-clear-item"; import { SessionClearItem } from "./session-clear-item";
import { Translated } from "./translated";
type Props = { type Props = {
sessions: Session[]; sessions: Session[];
@@ -22,7 +22,6 @@ export function SessionsClearList({
postLogoutRedirectUri, postLogoutRedirectUri,
organization, organization,
}: Props) { }: Props) {
const t = useTranslations("logout");
const [list, setList] = useState<Session[]>(sessions); const [list, setList] = useState<Session[]>(sessions);
const router = useRouter(); const router = useRouter();
@@ -97,10 +96,14 @@ export function SessionsClearList({
); );
})} })}
{list.length === 0 && ( {list.length === 0 && (
<Alert type={AlertType.INFO}>{t("noResults")}</Alert> <Alert type={AlertType.INFO}>
<Translated i18nKey="noResults" namespace="logout" />
</Alert>
)} )}
</div> </div>
) : ( ) : (
<Alert>{t("noResults")}</Alert> <Alert>
<Translated i18nKey="noResults" namespace="logout" />
</Alert>
); );
} }

View File

@@ -2,10 +2,10 @@
import { timestampDate } from "@zitadel/client"; import { timestampDate } from "@zitadel/client";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { useTranslations } from "next-intl";
import { useState } from "react"; import { useState } from "react";
import { Alert } from "./alert"; import { Alert } from "./alert";
import { SessionItem } from "./session-item"; import { SessionItem } from "./session-item";
import { Translated } from "./translated";
type Props = { type Props = {
sessions: Session[]; sessions: Session[];
@@ -13,7 +13,6 @@ type Props = {
}; };
export function SessionsList({ sessions, requestId }: Props) { export function SessionsList({ sessions, requestId }: Props) {
const t = useTranslations("accounts");
const [list, setList] = useState<Session[]>(sessions); const [list, setList] = useState<Session[]>(sessions);
return sessions ? ( return sessions ? (
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
@@ -44,6 +43,8 @@ export function SessionsList({ sessions, requestId }: Props) {
})} })}
</div> </div>
) : ( ) : (
<Alert>{t("noResults")}</Alert> <Alert>
<Translated i18nKey="noResults" namespace="accounts" />
</Alert>
); );
} }

View File

@@ -14,7 +14,6 @@ import {
import { create } from "@zitadel/client"; import { create } from "@zitadel/client";
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb"; import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import { FieldValues, useForm } from "react-hook-form"; import { FieldValues, useForm } from "react-hook-form";
@@ -24,6 +23,7 @@ import { Button, ButtonVariants } from "./button";
import { TextInput } from "./input"; import { TextInput } from "./input";
import { PasswordComplexity } from "./password-complexity"; import { PasswordComplexity } from "./password-complexity";
import { Spinner } from "./spinner"; import { Spinner } from "./spinner";
import { Translated } from "./translated";
type Inputs = type Inputs =
| { | {
@@ -52,8 +52,6 @@ export function SetPasswordForm({
code, code,
codeRequired, codeRequired,
}: Props) { }: Props) {
const t = useTranslations("password");
const { register, handleSubmit, watch, formState } = useForm<Inputs>({ const { register, handleSubmit, watch, formState } = useForm<Inputs>({
mode: "onBlur", mode: "onBlur",
defaultValues: { defaultValues: {
@@ -195,7 +193,7 @@ export function SetPasswordForm({
<Alert type={AlertType.INFO}> <Alert type={AlertType.INFO}>
<div className="flex flex-row"> <div className="flex flex-row">
<span className="flex-1 mr-auto text-left"> <span className="flex-1 mr-auto text-left">
{t("set.noCodeReceived")} <Translated i18nKey="set.noCodeReceived" namespace="password" />
</span> </span>
<button <button
aria-label="Resend OTP Code" aria-label="Resend OTP Code"
@@ -207,7 +205,7 @@ export function SetPasswordForm({
}} }}
data-testid="resend-button" data-testid="resend-button"
> >
{t("set.resend")} <Translated i18nKey="set.resend" namespace="password" />
</button> </button>
</div> </div>
</Alert> </Alert>
@@ -279,8 +277,8 @@ export function SetPasswordForm({
onClick={handleSubmit(submitPassword)} onClick={handleSubmit(submitPassword)}
data-testid="submit-button" data-testid="submit-button"
> >
{loading && <Spinner className="h-5 w-5 mr-2" />} {loading && <Spinner className="h-5 w-5 mr-2" />}{" "}
{t("set.submit")} <Translated i18nKey="set.submit" namespace="password" />
</Button> </Button>
</div> </div>
</form> </form>

View File

@@ -8,7 +8,6 @@ import {
} from "@/helpers/validators"; } from "@/helpers/validators";
import { registerUser } from "@/lib/server/register"; import { registerUser } from "@/lib/server/register";
import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb"; import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import { FieldValues, useForm } from "react-hook-form"; import { FieldValues, useForm } from "react-hook-form";
@@ -18,6 +17,7 @@ import { Button, ButtonVariants } from "./button";
import { TextInput } from "./input"; import { TextInput } from "./input";
import { PasswordComplexity } from "./password-complexity"; import { PasswordComplexity } from "./password-complexity";
import { Spinner } from "./spinner"; import { Spinner } from "./spinner";
import { Translated } from "./translated";
type Inputs = type Inputs =
| { | {
@@ -43,8 +43,6 @@ export function SetRegisterPasswordForm({
organization, organization,
requestId, requestId,
}: Props) { }: Props) {
const t = useTranslations("register");
const { register, handleSubmit, watch, formState } = useForm<Inputs>({ const { register, handleSubmit, watch, formState } = useForm<Inputs>({
mode: "onBlur", mode: "onBlur",
defaultValues: { defaultValues: {
@@ -163,8 +161,8 @@ export function SetRegisterPasswordForm({
onClick={handleSubmit(submitRegister)} onClick={handleSubmit(submitRegister)}
data-testid="submit-button" data-testid="submit-button"
> >
{loading && <Spinner className="h-5 w-5 mr-2" />} {loading && <Spinner className="h-5 w-5 mr-2" />}{" "}
{t("password.submit")} <Translated i18nKey="password.submit" namespace="register" />
</Button> </Button>
</div> </div>
</form> </form>

View File

@@ -3,7 +3,6 @@
import { getNextUrl } from "@/lib/client"; import { getNextUrl } from "@/lib/client";
import { verifyTOTP } from "@/lib/server/verify"; import { verifyTOTP } from "@/lib/server/verify";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { useTranslations } from "next-intl";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { QRCodeSVG } from "qrcode.react"; import { QRCodeSVG } from "qrcode.react";
@@ -14,6 +13,7 @@ import { Button, ButtonVariants } from "./button";
import { CopyToClipboard } from "./copy-to-clipboard"; import { CopyToClipboard } from "./copy-to-clipboard";
import { TextInput } from "./input"; import { TextInput } from "./input";
import { Spinner } from "./spinner"; import { Spinner } from "./spinner";
import { Translated } from "./translated";
type Inputs = { type Inputs = {
code: string; code: string;
@@ -39,8 +39,6 @@ export function TotpRegister({
checkAfter, checkAfter,
loginSettings, loginSettings,
}: Props) { }: Props) {
const t = useTranslations("otp");
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const router = useRouter(); const router = useRouter();
@@ -148,7 +146,7 @@ export function TotpRegister({
data-testid="submit-button" data-testid="submit-button"
> >
{loading && <Spinner className="h-5 w-5 mr-2" />} {loading && <Spinner className="h-5 w-5 mr-2" />}
{t("set.submit")} <Translated i18nKey="set.submit" namespace="otp" />
</Button> </Button>
</div> </div>
</form> </form>

View File

@@ -0,0 +1,23 @@
import { useTranslations } from "next-intl";
export function Translated({
i18nKey,
children,
namespace,
data,
...props
}: {
i18nKey: string;
children?: React.ReactNode;
namespace?: string;
data?: any;
} & React.HTMLAttributes<HTMLSpanElement>) {
const t = useTranslations(namespace);
const helperKey = `${namespace ? `${namespace}.` : ""}${i18nKey}`;
return (
<span data-i18n-key={helperKey} {...props}>
{t(i18nKey, data)}
</span>
);
}

View File

@@ -2,7 +2,6 @@
import { sendLoginname } from "@/lib/server/loginname"; import { sendLoginname } from "@/lib/server/loginname";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { ReactNode, useEffect, useState } from "react"; import { ReactNode, useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@@ -11,6 +10,7 @@ import { BackButton } from "./back-button";
import { Button, ButtonVariants } from "./button"; import { Button, ButtonVariants } from "./button";
import { TextInput } from "./input"; import { TextInput } from "./input";
import { Spinner } from "./spinner"; import { Spinner } from "./spinner";
import { Translated } from "./translated";
type Inputs = { type Inputs = {
loginName: string; loginName: string;
@@ -37,7 +37,6 @@ export function UsernameForm({
allowRegister, allowRegister,
children, children,
}: Props) { }: Props) {
const t = useTranslations("loginname");
const { register, handleSubmit, formState } = useForm<Inputs>({ const { register, handleSubmit, formState } = useForm<Inputs>({
mode: "onBlur", mode: "onBlur",
defaultValues: { defaultValues: {
@@ -127,7 +126,7 @@ export function UsernameForm({
disabled={loading} disabled={loading}
data-testid="register-button" data-testid="register-button"
> >
{t("register")} <Translated i18nKey="register" namespace="loginname" />
</button> </button>
)} )}
</div> </div>
@@ -152,7 +151,7 @@ export function UsernameForm({
onClick={handleSubmit((e) => submitLoginName(e, organization))} onClick={handleSubmit((e) => submitLoginName(e, organization))}
> >
{loading && <Spinner className="h-5 w-5 mr-2" />} {loading && <Spinner className="h-5 w-5 mr-2" />}
continue <Translated i18nKey="submit" namespace="loginname" />
</Button> </Button>
</div> </div>
</form> </form>

View File

@@ -2,7 +2,6 @@
import { Alert, AlertType } from "@/components/alert"; import { Alert, AlertType } from "@/components/alert";
import { resendVerification, sendVerification } from "@/lib/server/verify"; import { resendVerification, sendVerification } from "@/lib/server/verify";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@@ -10,6 +9,7 @@ import { BackButton } from "./back-button";
import { Button, ButtonVariants } from "./button"; import { Button, ButtonVariants } from "./button";
import { TextInput } from "./input"; import { TextInput } from "./input";
import { Spinner } from "./spinner"; import { Spinner } from "./spinner";
import { Translated } from "./translated";
type Inputs = { type Inputs = {
code: string; code: string;
@@ -32,8 +32,6 @@ export function VerifyForm({
code, code,
isInvite, isInvite,
}: Props) { }: Props) {
const t = useTranslations("verify");
const router = useRouter(); const router = useRouter();
const { register, handleSubmit, formState } = useForm<Inputs>({ const { register, handleSubmit, formState } = useForm<Inputs>({
@@ -117,7 +115,7 @@ export function VerifyForm({
<Alert type={AlertType.INFO}> <Alert type={AlertType.INFO}>
<div className="flex flex-row"> <div className="flex flex-row">
<span className="flex-1 mr-auto text-left"> <span className="flex-1 mr-auto text-left">
{t("verify.noCodeReceived")} <Translated i18nKey="verify.noCodeReceived" namespace="verify" />
</span> </span>
<button <button
aria-label="Resend Code" aria-label="Resend Code"
@@ -129,7 +127,7 @@ export function VerifyForm({
}} }}
data-testid="resend-button" data-testid="resend-button"
> >
{t("verify.resendCode")} <Translated i18nKey="verify.resendCode" namespace="verify" />
</button> </button>
</div> </div>
</Alert> </Alert>
@@ -161,7 +159,7 @@ export function VerifyForm({
data-testid="submit-button" data-testid="submit-button"
> >
{loading && <Spinner className="h-5 w-5 mr-2" />} {loading && <Spinner className="h-5 w-5 mr-2" />}
{t("verify.submit")} <Translated i18nKey="verify.submit" namespace="verify" />
</Button> </Button>
</div> </div>
</form> </form>

View File

@@ -1,4 +1,7 @@
import { LANGS, LANGUAGE_COOKIE_NAME, LANGUAGE_HEADER_NAME } from "@/lib/i18n"; import { LANGS, LANGUAGE_COOKIE_NAME, LANGUAGE_HEADER_NAME } from "@/lib/i18n";
import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { getHostedLoginTranslation } from "@/lib/zitadel";
import { JsonObject } from "@zitadel/client";
import deepmerge from "deepmerge"; import deepmerge from "deepmerge";
import { getRequestConfig } from "next-intl/server"; import { getRequestConfig } from "next-intl/server";
import { cookies, headers } from "next/headers"; import { cookies, headers } from "next/headers";
@@ -9,6 +12,26 @@ export default getRequestConfig(async () => {
let locale: string = fallback; let locale: string = fallback;
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const i18nOrganization = _headers.get("x-zitadel-i18n-organization") || ""; // You may need to set this header in middleware
console.log("i18nOrganization:", i18nOrganization);
let translations: JsonObject | {} = {};
try {
const i18nJSON = await getHostedLoginTranslation({
serviceUrl,
locale,
organization: i18nOrganization,
});
if (i18nJSON) {
translations = i18nJSON;
}
} catch (error) {
console.warn("Error fetching custom translations:", error);
}
const languageHeader = await (await headers()).get(LANGUAGE_HEADER_NAME); const languageHeader = await (await headers()).get(LANGUAGE_HEADER_NAME);
if (languageHeader) { if (languageHeader) {
const headerLocale = languageHeader.split(",")[0].split("-")[0]; // Extract the language code const headerLocale = languageHeader.split(",")[0].split("-")[0]; // Extract the language code
@@ -24,12 +47,13 @@ export default getRequestConfig(async () => {
} }
} }
const userMessages = (await import(`../../locales/${locale}.json`)).default; const customMessages = translations;
const localeMessages = (await import(`../../locales/${locale}.json`)).default;
const fallbackMessages = (await import(`../../locales/${fallback}.json`)) const fallbackMessages = (await import(`../../locales/${fallback}.json`))
.default; .default;
return { return {
locale, locale,
messages: deepmerge(fallbackMessages, userMessages), messages: deepmerge.all([fallbackMessages, localeMessages, customMessages]),
}; };
}); });

View File

@@ -59,6 +59,42 @@ async function cacheWrapper<T>(callback: Promise<T>) {
return callback; return callback;
} }
export async function getHostedLoginTranslation({
serviceUrl,
organization,
locale,
}: {
serviceUrl: string;
organization?: string;
locale?: string;
}) {
const settingsService: Client<typeof SettingsService> =
await createServiceForHost(SettingsService, serviceUrl);
const callback = settingsService
.getHostedLoginTranslation(
{
level: organization
? {
case: "organizationId",
value: organization,
}
: {
case: "instance",
value: true,
},
locale: locale,
},
{},
)
.then((resp) => {
console.log(resp);
return resp.translations ? resp.translations : undefined;
});
return useCache ? cacheWrapper(callback) : callback;
}
export async function getBrandingSettings({ export async function getBrandingSettings({
serviceUrl, serviceUrl,
organization, organization,

View File

@@ -10,21 +10,51 @@ export const config = {
"/oidc/:path*", "/oidc/:path*",
"/idps/callback/:path*", "/idps/callback/:path*",
"/saml/:path*", "/saml/:path*",
"/:path*",
], ],
}; };
export async function middleware(request: NextRequest) { export async function middleware(request: NextRequest) {
// Add the original URL as a header to all requests
const requestHeaders = new Headers(request.headers);
// Extract "organization" search param from the URL and set it as a header if available
const organization = request.nextUrl.searchParams.get("organization");
if (organization) {
requestHeaders.set("x-zitadel-i18n-organization", organization);
}
// Only run the rest of the logic for the original matcher paths
const matchedPaths = [
"/.well-known/",
"/oauth/",
"/oidc/",
"/idps/callback/",
"/saml/",
];
const isMatched = matchedPaths.some((prefix) =>
request.nextUrl.pathname.startsWith(prefix),
);
if (!isMatched) {
// For all other routes, just add the header and continue
return NextResponse.next({
request: { headers: requestHeaders },
});
}
// escape proxy if the environment is setup for multitenancy // escape proxy if the environment is setup for multitenancy
if (!process.env.ZITADEL_API_URL || !process.env.ZITADEL_SERVICE_USER_TOKEN) { if (!process.env.ZITADEL_API_URL || !process.env.ZITADEL_SERVICE_USER_TOKEN) {
return NextResponse.next(); return NextResponse.next({
request: { headers: requestHeaders },
});
} }
const _headers = await headers(); const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers); const { serviceUrl } = getServiceUrlFromHeaders(_headers);
// Call the /security route handler // Call the /security route handler
// TODO check this on cloud run deployment
const securityResponse = await fetch(`${request.nextUrl.origin}/security`); const securityResponse = await fetch(`${request.nextUrl.origin}/security`);
if (!securityResponse.ok) { if (!securityResponse.ok) {
@@ -32,7 +62,9 @@ export async function middleware(request: NextRequest) {
"Failed to fetch security settings:", "Failed to fetch security settings:",
securityResponse.statusText, securityResponse.statusText,
); );
return NextResponse.next(); // Fallback if the request fails return NextResponse.next({
request: { headers: requestHeaders },
});
} }
const { settings: securitySettings } = await securityResponse.json(); const { settings: securitySettings } = await securityResponse.json();
@@ -41,13 +73,8 @@ export async function middleware(request: NextRequest) {
.replace("https://", "") .replace("https://", "")
.replace("http://", ""); .replace("http://", "");
const requestHeaders = new Headers(request.headers); // Add additional headers as before
// 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-public-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", instanceHost); requestHeaders.set("x-zitadel-instance-host", instanceHost);
const responseHeaders = new Headers(); const responseHeaders = new Headers();
@@ -55,7 +82,6 @@ export async function middleware(request: NextRequest) {
responseHeaders.set("Access-Control-Allow-Headers", "*"); responseHeaders.set("Access-Control-Allow-Headers", "*");
if (securitySettings?.embeddedIframe?.enabled) { if (securitySettings?.embeddedIframe?.enabled) {
securitySettings.embeddedIframe.allowedOrigins;
responseHeaders.set( responseHeaders.set(
"Content-Security-Policy", "Content-Security-Policy",
`${DEFAULT_CSP} frame-ancestors ${securitySettings.embeddedIframe.allowedOrigins.join(" ")};`, `${DEFAULT_CSP} frame-ancestors ${securitySettings.embeddedIframe.allowedOrigins.join(" ")};`,

View File

@@ -28,7 +28,7 @@ target "typescript-proto-client-out" {
] ]
} }
# proto-files is only used to build login-core-mock against which the integration tests run. # proto-files is only used to build core-mock against which the integration tests run.
# To build the proto-client, we use buf to generate and download the client code directly. # To build the proto-client, we use buf to generate and download the client code directly.
# It is not login-prefixed, so it is easily extendable. # It is not login-prefixed, so it is easily extendable.
# To extend this bake-file.hcl, set the context of all login-prefixed targets to a different directory. # To extend this bake-file.hcl, set the context of all login-prefixed targets to a different directory.
@@ -75,10 +75,11 @@ target "login-client" {
} }
variable "LOGIN_CORE_MOCK_TAG" { variable "LOGIN_CORE_MOCK_TAG" {
default = "login-core-mock:local" default = "core-mock:local"
} }
target "login-core-mock" { # the core-mock context must not be overwritten, so we don't prefix it with login-.
target "core-mock" {
context = "${LOGIN_DIR}apps/login-test-integration/core-mock" context = "${LOGIN_DIR}apps/login-test-integration/core-mock"
contexts = { contexts = {
protos = "target:proto-files" protos = "target:proto-files"

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