Merge branch 'main' into ldap

This commit is contained in:
Max Peintner
2025-06-24 10:20:50 +02:00
87 changed files with 1702 additions and 783 deletions

View File

@@ -65,7 +65,7 @@ You can already use the current state, and extend it with your needs.
- [x] Apple - [x] Apple
- [x] Generic OIDC - [x] Generic OIDC
- [x] Generic OAuth - [x] Generic OAuth
- [ ] Generic JWT - [x] Generic JWT
- [ ] LDAP - [ ] LDAP
- [ ] SAML SP - [ ] SAML SP
- Multifactor Registration an Login - Multifactor Registration an Login
@@ -73,7 +73,7 @@ You can already use the current state, and extend it with your needs.
- [x] TOTP - [x] TOTP
- [x] OTP: Email Code - [x] OTP: Email Code
- [x] OTP: SMS Code - [x] OTP: SMS Code
- [ ] Password Change/Reset - [x] Password Change/Reset
- [x] Domain Discovery - [x] Domain Discovery
- [x] Branding - [x] Branding
- OIDC Standard - OIDC Standard

View File

@@ -8,10 +8,22 @@
"addAnother": "Ein weiteres Konto hinzufügen", "addAnother": "Ein weiteres Konto hinzufügen",
"noResults": "Keine Konten gefunden" "noResults": "Keine Konten gefunden"
}, },
"logout": {
"title": "Logout",
"description": "Wählen Sie den Account aus, das Sie entfernen möchten",
"noResults": "Keine Konten gefunden",
"clear": "Session beenden",
"verifiedAt": "Zuletzt aktiv: {time}",
"success": {
"title": "Logout erfolgreich",
"description": "Sie haben sich erfolgreich abgemeldet."
}
},
"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": {
@@ -62,6 +74,10 @@
"linkingError": { "linkingError": {
"title": "Konto-Verknüpfung fehlgeschlagen", "title": "Konto-Verknüpfung fehlgeschlagen",
"description": "Beim Verknüpfen Ihres Kontos ist ein Fehler aufgetreten." "description": "Beim Verknüpfen Ihres Kontos ist ein Fehler aufgetreten."
},
"completeRegister": {
"title": "Registrierung abschließen",
"description": "Bitte vervollständige die Registrierung, um dein Konto zu erstellen."
} }
}, },
"mfa": { "mfa": {
@@ -139,11 +155,13 @@
}, },
"title": "Registrieren", "title": "Registrieren",
"description": "Erstellen Sie Ihr ZITADEL-Konto.", "description": "Erstellen Sie Ihr ZITADEL-Konto.",
"noMethodAvailableWarning": "Keine Authentifizierungsmethode verfügbar. Bitte wenden Sie sich an den Administrator.",
"selectMethod": "Wählen Sie die Methode, mit der Sie sich authentifizieren möchten", "selectMethod": "Wählen Sie die Methode, mit der Sie sich authentifizieren möchten",
"agreeTo": "Um sich zu registrieren, müssen Sie den Nutzungsbedingungen zustimmen", "agreeTo": "Um sich zu registrieren, müssen Sie den Nutzungsbedingungen zustimmen",
"termsOfService": "Nutzungsbedingungen", "termsOfService": "Nutzungsbedingungen",
"privacyPolicy": "Datenschutzrichtlinie", "privacyPolicy": "Datenschutzrichtlinie",
"submit": "Weiter", "submit": "Weiter",
"orUseIDP": "oder verwenden Sie einen Identitätsanbieter",
"password": { "password": {
"title": "Passwort festlegen", "title": "Passwort festlegen",
"description": "Legen Sie das Passwort für Ihr Konto fest", "description": "Legen Sie das Passwort für Ihr Konto fest",

View File

@@ -8,10 +8,22 @@
"addAnother": "Add another account", "addAnother": "Add another account",
"noResults": "No accounts found" "noResults": "No accounts found"
}, },
"logout": {
"title": "Logout",
"description": "Click an account to end the session",
"noResults": "No accounts found",
"clear": "End Session",
"verifiedAt": "Last active: {time}",
"success": {
"title": "Logout successful",
"description": "You have successfully logged out."
}
},
"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": {
@@ -62,6 +74,10 @@
"linkingError": { "linkingError": {
"title": "Account linking failed", "title": "Account linking failed",
"description": "An error occurred while trying to link your account." "description": "An error occurred while trying to link your account."
},
"completeRegister": {
"title": "Complete your data",
"description": "You need to complete your registration by providing your email address and name."
} }
}, },
"ldap": { "ldap": {
@@ -146,11 +162,13 @@
}, },
"title": "Register", "title": "Register",
"description": "Create your ZITADEL account.", "description": "Create your ZITADEL account.",
"noMethodAvailableWarning": "No authentication method available. Please contact your administrator.",
"selectMethod": "Select the method you would like to authenticate", "selectMethod": "Select the method you would like to authenticate",
"agreeTo": "To register you must agree to the terms and conditions", "agreeTo": "To register you must agree to the terms and conditions",
"termsOfService": "Terms of Service", "termsOfService": "Terms of Service",
"privacyPolicy": "Privacy Policy", "privacyPolicy": "Privacy Policy",
"submit": "Continue", "submit": "Continue",
"orUseIDP": "or use an Identity Provider",
"password": { "password": {
"title": "Set Password", "title": "Set Password",
"description": "Set the password for your account", "description": "Set the password for your account",

View File

@@ -8,10 +8,22 @@
"addAnother": "Agregar otra cuenta", "addAnother": "Agregar otra cuenta",
"noResults": "No se encontraron cuentas" "noResults": "No se encontraron cuentas"
}, },
"logout": {
"title": "Cerrar sesión",
"description": "Selecciona la cuenta que deseas eliminar",
"noResults": "No se encontraron cuentas",
"clear": "Eliminar sesión",
"verifiedAt": "Última actividad: {time}",
"success": {
"title": "Cierre de sesión exitoso",
"description": "Has cerrado sesión correctamente."
}
},
"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": {
@@ -62,6 +74,10 @@
"linkingError": { "linkingError": {
"title": "Error al vincular la cuenta", "title": "Error al vincular la cuenta",
"description": "Ocurrió un error al intentar vincular tu cuenta." "description": "Ocurrió un error al intentar vincular tu cuenta."
},
"completeRegister": {
"title": "Completar registro",
"description": "Para completar el registro, debes establecer una contraseña."
} }
}, },
"mfa": { "mfa": {
@@ -139,11 +155,13 @@
}, },
"title": "Registrarse", "title": "Registrarse",
"description": "Crea tu cuenta ZITADEL.", "description": "Crea tu cuenta ZITADEL.",
"noMethodAvailableWarning": "No hay métodos de autenticación disponibles. Por favor, contacta a tu administrador.",
"selectMethod": "Selecciona el método con el que deseas autenticarte", "selectMethod": "Selecciona el método con el que deseas autenticarte",
"agreeTo": "Para registrarte debes aceptar los términos y condiciones", "agreeTo": "Para registrarte debes aceptar los términos y condiciones",
"termsOfService": "Términos de Servicio", "termsOfService": "Términos de Servicio",
"privacyPolicy": "Política de Privacidad", "privacyPolicy": "Política de Privacidad",
"submit": "Continuar", "submit": "Continuar",
"orUseIDP": "o usa un Proveedor de Identidad",
"password": { "password": {
"title": "Establecer Contraseña", "title": "Establecer Contraseña",
"description": "Establece la contraseña para tu cuenta", "description": "Establece la contraseña para tu cuenta",

View File

@@ -8,10 +8,22 @@
"addAnother": "Aggiungi un altro account", "addAnother": "Aggiungi un altro account",
"noResults": "Nessun account trovato" "noResults": "Nessun account trovato"
}, },
"logout": {
"title": "Esci",
"description": "Seleziona l'account che desideri uscire",
"noResults": "Nessun account trovato",
"clear": "Elimina sessione",
"verifiedAt": "Ultima attività: {time}",
"success": {
"title": "Uscita riuscita",
"description": "Hai effettuato l'uscita con successo."
}
},
"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": {
@@ -62,6 +74,10 @@
"linkingError": { "linkingError": {
"title": "Collegamento account fallito", "title": "Collegamento account fallito",
"description": "Si è verificato un errore durante il tentativo di collegare il tuo account." "description": "Si è verificato un errore durante il tentativo di collegare il tuo account."
},
"completeRegister": {
"title": "Completa la registrazione",
"description": "Completa la registrazione del tuo account."
} }
}, },
"mfa": { "mfa": {
@@ -139,11 +155,13 @@
}, },
"title": "Registrati", "title": "Registrati",
"description": "Crea il tuo account ZITADEL.", "description": "Crea il tuo account ZITADEL.",
"noMethodAvailableWarning": "Nessun metodo di autenticazione disponibile. Contatta l'amministratore di sistema per assistenza.",
"selectMethod": "Seleziona il metodo con cui desideri autenticarti", "selectMethod": "Seleziona il metodo con cui desideri autenticarti",
"agreeTo": "Per registrarti devi accettare i termini e le condizioni", "agreeTo": "Per registrarti devi accettare i termini e le condizioni",
"termsOfService": "Termini di Servizio", "termsOfService": "Termini di Servizio",
"privacyPolicy": "Informativa sulla Privacy", "privacyPolicy": "Informativa sulla Privacy",
"submit": "Continua", "submit": "Continua",
"orUseIDP": "o usa un Identity Provider",
"password": { "password": {
"title": "Imposta Password", "title": "Imposta Password",
"description": "Imposta la password per il tuo account", "description": "Imposta la password per il tuo account",

View File

@@ -8,10 +8,22 @@
"addAnother": "Dodaj kolejne konto", "addAnother": "Dodaj kolejne konto",
"noResults": "Nie znaleziono kont" "noResults": "Nie znaleziono kont"
}, },
"logout": {
"title": "Wyloguj się",
"description": "Wybierz konto, które chcesz usunąć",
"noResults": "Nie znaleziono kont",
"clear": "Usuń sesję",
"verifiedAt": "Ostatnia aktywność: {time}",
"success": {
"title": "Wylogowanie udane",
"description": "Pomyślnie się wylogowałeś."
}
},
"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": {
@@ -62,6 +74,10 @@
"linkingError": { "linkingError": {
"title": "Powiązanie konta nie powiodło się", "title": "Powiązanie konta nie powiodło się",
"description": "Wystąpił błąd podczas próby powiązania konta." "description": "Wystąpił błąd podczas próby powiązania konta."
},
"completeRegister": {
"title": "Ukończ rejestrację",
"description": "Ukończ rejestrację swojego konta."
} }
}, },
"mfa": { "mfa": {
@@ -139,11 +155,13 @@
}, },
"title": "Rejestracja", "title": "Rejestracja",
"description": "Utwórz konto ZITADEL.", "description": "Utwórz konto ZITADEL.",
"noMethodAvailableWarning": "Brak dostępnych metod uwierzytelniania. Skontaktuj się z administratorem.",
"selectMethod": "Wybierz metodę uwierzytelniania, której chcesz użyć", "selectMethod": "Wybierz metodę uwierzytelniania, której chcesz użyć",
"agreeTo": "Aby się zarejestrować, musisz zaakceptować warunki korzystania", "agreeTo": "Aby się zarejestrować, musisz zaakceptować warunki korzystania",
"termsOfService": "Regulamin", "termsOfService": "Regulamin",
"privacyPolicy": "Polityka prywatności", "privacyPolicy": "Polityka prywatności",
"submit": "Kontynuuj", "submit": "Kontynuuj",
"orUseIDP": "lub użyj dostawcy tożsamości",
"password": { "password": {
"title": "Ustaw hasło", "title": "Ustaw hasło",
"description": "Ustaw hasło dla swojego konta", "description": "Ustaw hasło dla swojego konta",

View File

@@ -8,10 +8,22 @@
"addAnother": "Добавить другой аккаунт", "addAnother": "Добавить другой аккаунт",
"noResults": "Аккаунты не найдены" "noResults": "Аккаунты не найдены"
}, },
"logout": {
"title": "Выход",
"description": "Выберите аккаунт, который хотите удалить",
"noResults": "Аккаунты не найдены",
"clear": "Удалить сессию",
"verifiedAt": "Последняя активность: {time}",
"success": {
"title": "Выход выполнен успешно",
"description": "Вы успешно вышли из системы."
}
},
"loginname": { "loginname": {
"title": "С возвращением!", "title": "С возвращением!",
"description": "Введите свои данные для входа.", "description": "Введите свои данные для входа.",
"register": "Зарегистрировать нового пользователя" "register": "Зарегистрировать нового пользователя",
"submit": "Продолжить"
}, },
"password": { "password": {
"verify": { "verify": {
@@ -62,6 +74,10 @@
"linkingError": { "linkingError": {
"title": "Ошибка привязки аккаунта", "title": "Ошибка привязки аккаунта",
"description": "Произошла ошибка при попытке привязать аккаунт." "description": "Произошла ошибка при попытке привязать аккаунт."
},
"completeRegister": {
"title": "Завершите регистрацию",
"description": "Завершите регистрацию вашего аккаунта."
} }
}, },
"mfa": { "mfa": {
@@ -139,11 +155,13 @@
}, },
"title": "Регистрация", "title": "Регистрация",
"description": "Создайте свой аккаунт ZITADEL.", "description": "Создайте свой аккаунт ZITADEL.",
"noMethodAvailableWarning": "Нет доступных методов аутентификации. Обратитесь к администратору.",
"selectMethod": "Выберите метод аутентификации", "selectMethod": "Выберите метод аутентификации",
"agreeTo": "Для регистрации необходимо принять условия:", "agreeTo": "Для регистрации необходимо принять условия:",
"termsOfService": "Условия использования", "termsOfService": "Условия использования",
"privacyPolicy": "Политика конфиденциальности", "privacyPolicy": "Политика конфиденциальности",
"submit": "Продолжить", "submit": "Продолжить",
"orUseIDP": "или используйте Identity Provider",
"password": { "password": {
"title": "Установить пароль", "title": "Установить пароль",
"description": "Установите пароль для вашего аккаунта", "description": "Установите пароль для вашего аккаунта",

View File

@@ -8,10 +8,22 @@
"addAnother": "添加另一个账户", "addAnother": "添加另一个账户",
"noResults": "未找到账户" "noResults": "未找到账户"
}, },
"logout": {
"title": "注销",
"description": "选择您想注销的账户",
"noResults": "未找到账户",
"clear": "注销会话",
"verifiedAt": "最后活动时间:{time}",
"success": {
"title": "注销成功",
"description": "您已成功注销。"
}
},
"loginname": { "loginname": {
"title": "欢迎回来!", "title": "欢迎回来!",
"description": "请输入您的登录信息。", "description": "请输入您的登录信息。",
"register": "注册新用户" "register": "注册新用户",
"submit": "继续"
}, },
"password": { "password": {
"verify": { "verify": {
@@ -62,6 +74,10 @@
"linkingError": { "linkingError": {
"title": "账户链接失败", "title": "账户链接失败",
"description": "链接账户时发生错误。" "description": "链接账户时发生错误。"
},
"completeRegister": {
"title": "完成注册",
"description": "完成您的账户注册。"
} }
}, },
"mfa": { "mfa": {
@@ -139,11 +155,13 @@
}, },
"title": "注册", "title": "注册",
"description": "创建您的 ZITADEL 账户。", "description": "创建您的 ZITADEL 账户。",
"noMethodAvailableWarning": "没有可用的认证方法。请联系您的系统管理员。",
"selectMethod": "选择您想使用的认证方法", "selectMethod": "选择您想使用的认证方法",
"agreeTo": "注册即表示您同意条款和条件", "agreeTo": "注册即表示您同意条款和条件",
"termsOfService": "服务条款", "termsOfService": "服务条款",
"privacyPolicy": "隐私政策", "privacyPolicy": "隐私政策",
"submit": "继续", "submit": "继续",
"orUseIDP": "或使用身份提供者",
"password": { "password": {
"title": "设置密码", "title": "设置密码",
"description": "为您的账户设置密码", "description": "为您的账户设置密码",

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,7 +3,7 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "next dev",
"test": "concurrently --timings --kill-others-on-fail 'npm:test:unit' 'npm:test:integration'", "test": "concurrently --timings --kill-others-on-fail 'npm:test:unit' 'npm:test:integration'",
"test:watch": "concurrently --kill-others 'npm:test:unit:watch' 'npm:test:integration:watch'", "test:watch": "concurrently --kill-others 'npm:test:unit:watch' 'npm:test:integration:watch'",
"test:unit": "vitest", "test:unit": "vitest",
@@ -45,10 +45,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",
@@ -56,7 +55,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}
@@ -186,11 +193,12 @@ export default async function Page(props: {
{loginSettings?.allowExternalIdp && !!identityProviders.length && ( {loginSettings?.allowExternalIdp && !!identityProviders.length && (
<> <>
{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
identityProviders={identityProviders} identityProviders={identityProviders}
requestId={requestId} requestId={requestId}

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

@@ -1,51 +1,98 @@
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { IdpSignin } from "@/components/idp-signin"; import { IdpSignin } from "@/components/idp-signin";
import { completeIDP } from "@/components/idps/pages/complete-idp";
import { linkingFailed } from "@/components/idps/pages/linking-failed"; 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,
addIDPLink, addIDPLink,
getBrandingSettings, getBrandingSettings,
getDefaultOrg,
getIDPByID, getIDPByID,
getLoginSettings, getLoginSettings,
getOrgsByDomain, getOrgsByDomain,
listUsers, listUsers,
retrieveIDPIntent, retrieveIDPIntent,
updateHuman,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { ConnectError, create } from "@zitadel/client"; import { ConnectError, create } from "@zitadel/client";
import { AutoLinkingOption } from "@zitadel/proto/zitadel/idp/v2/idp_pb"; import { AutoLinkingOption } from "@zitadel/proto/zitadel/idp/v2/idp_pb";
import { OrganizationSchema } from "@zitadel/proto/zitadel/object/v2/object_pb"; import { OrganizationSchema } from "@zitadel/proto/zitadel/object/v2/object_pb";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { import {
AddHumanUserRequest, AddHumanUserRequest,
AddHumanUserRequestSchema, AddHumanUserRequestSchema,
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 = /(?<=@)(.+)/;
async function resolveOrganizationForUser({
organization,
addHumanUser,
serviceUrl,
}: {
organization?: string;
addHumanUser?: { username?: string };
serviceUrl: string;
}): Promise<string | undefined> {
if (organization) return organization;
if (addHumanUser?.username && ORG_SUFFIX_REGEX.test(addHumanUser.username)) {
const matched = ORG_SUFFIX_REGEX.exec(addHumanUser.username);
const suffix = matched?.[1] ?? "";
const orgs = await getOrgsByDomain({
serviceUrl,
domain: suffix,
});
const orgToCheckForDiscovery =
orgs.result && orgs.result.length === 1 ? orgs.result[0].id : undefined;
if (orgToCheckForDiscovery) {
const orgLoginSettings = await getLoginSettings({
serviceUrl,
organization: orgToCheckForDiscovery,
});
if (orgLoginSettings?.allowDomainDiscovery) {
return orgToCheckForDiscovery;
}
}
}
return undefined;
}
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>>;
params: Promise<{ provider: string }>; params: Promise<{ provider: string }>;
}) { }) {
const params = await props.params; const params = await props.params;
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale(); let { id, token, requestId, organization, link } = searchParams;
const t = await getTranslations({ locale, namespace: "idp" });
const { id, token, requestId, organization, link } = searchParams;
const { provider } = params; const { provider } = params;
const _headers = await headers(); const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers); const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const branding = await getBrandingSettings({ let branding = await getBrandingSettings({
serviceUrl, serviceUrl,
organization, organization,
}); });
if (!organization) {
const org: Organization | null = await getDefaultOrg({
serviceUrl,
});
if (org) {
organization = org.id;
}
}
if (!provider || !id || !token) { if (!provider || !id || !token) {
return loginFailed(branding, "IDP context missing"); return loginFailed(branding, "IDP context missing");
} }
@@ -59,18 +106,6 @@ export default async function Page(props: {
const { idpInformation, userId } = intent; const { idpInformation, userId } = intent;
let { addHumanUser } = intent; let { addHumanUser } = intent;
// sign in user. If user should be linked continue
if (userId && !link) {
// TODO: update user if idp.options.isAutoUpdate is true
return loginSuccess(
userId,
{ idpIntentId: id, idpIntentToken: token },
requestId,
branding,
);
}
if (!idpInformation) { if (!idpInformation) {
return loginFailed(branding, "IDP information missing"); return loginFailed(branding, "IDP information missing");
} }
@@ -79,12 +114,41 @@ export default async function Page(props: {
serviceUrl, serviceUrl,
id: idpInformation.idpId, id: idpInformation.idpId,
}); });
const options = idp?.config?.options; const options = idp?.config?.options;
if (!idp) { if (!idp) {
throw new Error("IDP not found"); throw new Error("IDP not found");
} }
// sign in user. If user should be linked continue
if (userId && !link) {
// if auto update is enabled, we will update the user with the new information
if (options?.isAutoUpdate && addHumanUser) {
try {
await updateHuman({
serviceUrl,
request: create(UpdateHumanUserRequestSchema, {
userId: userId,
profile: addHumanUser.profile,
email: addHumanUser.email,
phone: addHumanUser.phone,
}),
});
} catch (error: unknown) {
// Log the error and continue with the login process
console.warn("An error occurred while updating the user:", error);
}
}
return loginSuccess(
userId,
{ idpIntentId: id, idpIntentToken: token },
requestId,
branding,
);
}
if (link) { if (link) {
if (!options?.isLinkingAllowed) { if (!options?.isLinkingAllowed) {
// linking was probably disallowed since the invitation was created // linking was probably disallowed since the invitation was created
@@ -120,7 +184,7 @@ export default async function Page(props: {
} }
// search for potential user via username, then link // search for potential user via username, then link
if (options?.isLinkingAllowed) { if (options?.autoLinking) {
let foundUser; let foundUser;
const email = addHumanUser?.email?.email; const email = addHumanUser?.email?.email;
@@ -176,36 +240,15 @@ export default async function Page(props: {
} }
} }
if (options?.isCreationAllowed && options.isAutoCreation) {
let orgToRegisterOn: string | undefined = organization;
let newUser; let newUser;
// automatic creation of a user is allowed and data is complete
if ( if (options?.isAutoCreation && addHumanUser) {
!orgToRegisterOn && const orgToRegisterOn = await resolveOrganizationForUser({
addHumanUser?.username && // username or email? organization,
ORG_SUFFIX_REGEX.test(addHumanUser.username) addHumanUser,
) {
const matched = ORG_SUFFIX_REGEX.exec(addHumanUser.username);
const suffix = matched?.[1] ?? "";
// this just returns orgs where the suffix is set as primary domain
const orgs = await getOrgsByDomain({
serviceUrl, serviceUrl,
domain: suffix,
}); });
const orgToCheckForDiscovery =
orgs.result && orgs.result.length === 1 ? orgs.result[0].id : undefined;
const orgLoginSettings = await getLoginSettings({
serviceUrl,
organization: orgToCheckForDiscovery,
});
if (orgLoginSettings?.allowDomainDiscovery) {
orgToRegisterOn = orgToCheckForDiscovery;
}
}
if (addHumanUser) {
let addHumanUserWithOrganization: AddHumanUserRequest; let addHumanUserWithOrganization: AddHumanUserRequest;
if (orgToRegisterOn) { if (orgToRegisterOn) {
const organizationSchema = create(OrganizationSchema, { const organizationSchema = create(OrganizationSchema, {
@@ -241,14 +284,47 @@ export default async function Page(props: {
: "Could not create user", : "Could not create user",
); );
} }
} else if (options?.isCreationAllowed) {
// if no user was found, we will create a new user manually / redirect to the registration page
const orgToRegisterOn = await resolveOrganizationForUser({
organization,
addHumanUser,
serviceUrl,
});
if (orgToRegisterOn) {
branding = await getBrandingSettings({
serviceUrl,
organization: orgToRegisterOn,
});
}
if (!orgToRegisterOn) {
return loginFailed(branding, "No organization found for registration");
}
return completeIDP({
branding,
idpIntent: { idpIntentId: id, idpIntentToken: token },
addHumanUser,
organization: orgToRegisterOn,
requestId,
idpUserId: idpInformation?.userId,
idpId: idpInformation?.idpId,
idpUserName: idpInformation?.userName,
});
} }
if (newUser) { if (newUser) {
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 }}
@@ -258,7 +334,6 @@ export default async function Page(props: {
</DynamicTheme> </DynamicTheme>
); );
} }
}
// return login failed if no linking or creation is allowed and no user was found // return login failed if no linking or creation is allowed and no user was found
return loginFailed(branding, "No user found"); return loginFailed(branding, "No user found");

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}
@@ -76,7 +78,7 @@ export default async function Page(props: {
allowRegister={!!loginSettings?.allowRegister} allowRegister={!!loginSettings?.allowRegister}
></UsernameForm> ></UsernameForm>
{identityProviders && ( {identityProviders && loginSettings?.allowExternalIdp && (
<div className="w-full pt-6 pb-4"> <div className="w-full pt-6 pb-4">
<SignInWithIdp <SignInWithIdp
identityProviders={identityProviders} identityProviders={identityProviders}

View File

@@ -0,0 +1,86 @@
import { DynamicTheme } from "@/components/dynamic-theme";
import { SessionsClearList } from "@/components/sessions-clear-list";
import { Translated } from "@/components/translated";
import { getAllSessionCookieIds } from "@/lib/cookies";
import { getServiceUrlFromHeaders } from "@/lib/service-url";
import {
getBrandingSettings,
getDefaultOrg,
listSessions,
} from "@/lib/zitadel";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { headers } from "next/headers";
async function loadSessions({ serviceUrl }: { serviceUrl: string }) {
const ids: (string | undefined)[] = await getAllSessionCookieIds();
if (ids && ids.length) {
const response = await listSessions({
serviceUrl,
ids: ids.filter((id) => !!id) as string[],
});
return response?.sessions ?? [];
} else {
console.info("No session cookie found.");
return [];
}
}
export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) {
const searchParams = await props.searchParams;
const organization = searchParams?.organization;
const postLogoutRedirectUri = searchParams?.post_logout_redirect_uri;
const logoutHint = searchParams?.logout_hint;
const UILocales = searchParams?.ui_locales; // TODO implement with new translation service
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
let defaultOrganization;
if (!organization) {
const org: Organization | null = await getDefaultOrg({
serviceUrl,
});
if (org) {
defaultOrganization = org.id;
}
}
let sessions = await loadSessions({ serviceUrl });
const branding = await getBrandingSettings({
serviceUrl,
organization: organization ?? defaultOrganization,
});
const params = new URLSearchParams();
if (organization) {
params.append("organization", organization);
}
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>
<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">
<SessionsClearList
sessions={sessions}
logoutHint={logoutHint}
postLogoutRedirectUri={postLogoutRedirectUri}
organization={organization ?? defaultOrganization}
/>
</div>
</div>
</DynamicTheme>
);
}

View File

@@ -0,0 +1,43 @@
import { DynamicTheme } from "@/components/dynamic-theme";
import { Translated } from "@/components/translated";
import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { getBrandingSettings, getDefaultOrg } from "@/lib/zitadel";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { headers } from "next/headers";
export default async function Page(props: { searchParams: Promise<any> }) {
const searchParams = await props.searchParams;
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { login_hint, organization } = searchParams;
let defaultOrganization;
if (!organization) {
const org: Organization | null = await getDefaultOrg({
serviceUrl,
});
if (org) {
defaultOrganization = org.id;
}
}
const branding = await getBrandingSettings({
serviceUrl,
organization,
});
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>
<Translated i18nKey="success.title" namespace="logout" />
</h1>
<p className="ztdl-p mb-6 block">
<Translated i18nKey="success.description" namespace="logout" />
</p>
</div>
</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";
@@ -10,18 +11,13 @@ 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(); let { loginName, organization, requestId, alt } = searchParams;
const t = await getTranslations({ locale, namespace: "password" });
const tError = await getTranslations({ locale, namespace: "error" });
let { loginName, organization, requestId } = searchParams;
const _headers = await headers(); const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers); const { serviceUrl } = getServiceUrlFromHeaders(_headers);
@@ -65,15 +61,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

@@ -1,7 +1,11 @@
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 { Translated } from "@/components/translated";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { import {
getActiveIdentityProviders,
getBrandingSettings, getBrandingSettings,
getDefaultOrg, getDefaultOrg,
getLegalAndSupportSettings, getLegalAndSupportSettings,
@@ -9,7 +13,8 @@ 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 { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { getLocale } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
export default async function Page(props: { export default async function Page(props: {
@@ -17,7 +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" });
let { firstname, lastname, email, organization, requestId } = searchParams; let { firstname, lastname, email, organization, requestId } = searchParams;
@@ -52,12 +56,25 @@ export default async function Page(props: {
organization, organization,
}); });
const identityProviders = await getActiveIdentityProviders({
serviceUrl,
orgId: organization,
}).then((resp) => {
return resp.identityProviders.filter((idp) => {
return idp.options?.isAutoCreation || idp.options?.isCreationAllowed; // check if IDP allows to create account automatically or manual creation is allowed
});
});
if (!loginSettings?.allowRegister) { if (!loginSettings?.allowRegister) {
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>
); );
@@ -66,11 +83,28 @@ 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>
{legal && passwordComplexitySettings && ( {!organization && (
<Alert>
<Translated i18nKey="unknownContext" namespace="error" />
</Alert>
)}
{legal &&
passwordComplexitySettings &&
organization &&
(loginSettings.allowUsernamePassword ||
loginSettings.passkeysType == PasskeysType.ALLOWED) && (
<RegisterForm <RegisterForm
idpCount={
!loginSettings?.allowExternalIdp ? 0 : identityProviders.length
}
legal={legal} legal={legal}
organization={organization} organization={organization}
firstname={firstname} firstname={firstname}
@@ -80,6 +114,22 @@ export default async function Page(props: {
loginSettings={loginSettings} loginSettings={loginSettings}
></RegisterForm> ></RegisterForm>
)} )}
{loginSettings?.allowExternalIdp && !!identityProviders.length && (
<>
<div className="py-3 flex flex-col items-center">
<p className="ztdl-p text-center">
<Translated i18nKey="orUseIDP" namespace="register" />
</p>
</div>
<SignInWithIdp
identityProviders={identityProviders}
requestId={requestId}
organization={organization}
></SignInWithIdp>
</>
)}
</div> </div>
</DynamicTheme> </DynamicTheme>
); );

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;
@@ -33,7 +31,7 @@ export default async function Page(props: {
} }
} }
const missingData = !firstname || !lastname || !email; const missingData = !firstname || !lastname || !email || !organization;
const legal = await getLegalAndSupportSettings({ const legal = await getLegalAndSupportSettings({
serviceUrl, serviceUrl,
@@ -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
@@ -73,7 +79,7 @@ export default async function Page(props: {
email={email} email={email}
firstname={firstname} firstname={firstname}
lastname={lastname} lastname={lastname}
organization={organization} organization={organization as string} // organization is guaranteed to be a string here otherwise we would have returned earlier
requestId={requestId} requestId={requestId}
></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,39 +1,18 @@
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 { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { import {
getBrandingSettings, getBrandingSettings,
getLoginSettings, getLoginSettings,
getSession,
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";
async function loadSessionById(
serviceUrl: string,
sessionId: string,
organization?: string,
) {
const recent = await getSessionCookieById({ sessionId, organization });
return getSession({
serviceUrl,
sessionId: recent.id,
sessionToken: recent.token,
}).then((response) => {
if (response?.session) {
return response.session;
}
});
}
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);
@@ -84,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,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

@@ -0,0 +1,55 @@
import { RegisterFormIDPIncomplete } from "@/components/register-form-idp-incomplete";
import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb";
import { AddHumanUserRequest } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { DynamicTheme } from "../../dynamic-theme";
import { Translated } from "../../translated";
export async function completeIDP({
idpUserId,
idpId,
idpUserName,
addHumanUser,
requestId,
organization,
branding,
idpIntent,
}: {
idpUserId: string;
idpId: string;
idpUserName: string;
addHumanUser?: AddHumanUserRequest;
requestId?: string;
organization: string;
branding?: BrandingSettings;
idpIntent: {
idpIntentId: string;
idpIntentToken: string;
};
}) {
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>
<Translated i18nKey="completeRegister.title" namespace="idp" />
</h1>
<p className="ztdl-p">
<Translated i18nKey="completeRegister.description" namespace="idp" />
</p>
<RegisterFormIDPIncomplete
idpUserId={idpUserId}
idpId={idpId}
idpUserName={idpUserName}
defaultValues={{
email: addHumanUser?.email?.email || "",
firstname: addHumanUser?.profile?.givenName || "",
lastname: addHumanUser?.profile?.familyName || "",
}}
requestId={requestId}
organization={organization}
idpIntent={idpIntent}
/>
</div>
</DynamicTheme>
);
}

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

@@ -43,7 +43,7 @@ export function LanguageSwitcher() {
> >
{selected.name} {selected.name}
<ChevronDownIcon <ChevronDownIcon
className="group pointer-events-none absolute top-2.5 right-2.5 size-4 fill-white/60" className="group pointer-events-none absolute top-2.5 right-2.5 size-4"
aria-hidden="true" aria-hidden="true"
/> />
</ListboxButton> </ListboxButton>
@@ -61,7 +61,7 @@ export function LanguageSwitcher() {
value={lang} value={lang}
className="group flex cursor-default items-center gap-2 rounded-lg py-1.5 px-3 select-none data-[focus]:bg-black/10 dark:data-[focus]:bg-white/10" className="group flex cursor-default items-center gap-2 rounded-lg py-1.5 px-3 select-none data-[focus]:bg-black/10 dark:data-[focus]:bg-white/10"
> >
<CheckIcon className="invisible size-4 fill-white group-data-[selected]:visible" /> <CheckIcon className="invisible size-4 group-data-[selected]:visible" />
<div className="text-sm/6 text-black dark:text-white"> <div className="text-sm/6 text-black dark:text-white">
{lang.name} {lang.name}
</div> </div>

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;
@@ -31,8 +31,6 @@ export function PasswordForm({
organization, organization,
requestId, requestId,
}: Props) { }: Props) {
const t = useTranslations("password");
const { register, handleSubmit, formState } = useForm<Inputs>({ const { register, handleSubmit, formState } = useForm<Inputs>({
mode: "onBlur", mode: "onBlur",
}); });
@@ -132,7 +130,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>
)} )}
@@ -169,8 +167,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 { inviteUser } from "@/lib/server/invite"; 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 =
| { | {
@@ -20,26 +20,37 @@ type Inputs =
| FieldValues; | FieldValues;
type Props = { type Props = {
organization: string;
requestId?: string;
idpIntent: {
idpIntentId: string;
idpIntentToken: string;
};
defaultValues?: {
firstname?: string; firstname?: string;
lastname?: string; lastname?: string;
email?: string; email?: string;
organization?: string; };
idpUserId: string;
idpId: string;
idpUserName: string;
}; };
export function InviteForm({ export function RegisterFormIDPIncomplete({
email,
firstname,
lastname,
organization, organization,
requestId,
idpIntent,
defaultValues,
idpUserId,
idpId,
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: {
email: email ?? "", email: defaultValues?.email ?? "",
firstName: firstname ?? "", firstname: defaultValues?.firstname ?? "",
lastname: lastname ?? "", lastname: defaultValues?.lastname ?? "",
}, },
}); });
@@ -48,39 +59,37 @@ export function InviteForm({
const router = useRouter(); const router = useRouter();
async function submitAndContinue(values: Inputs) { async function submitAndRegister(values: Inputs) {
setLoading(true); setLoading(true);
const response = await inviteUser({ const response = await registerUserAndLinkToIDP({
idpId: idpId,
idpUserName: idpUserName,
idpUserId: idpUserId,
email: values.email, email: values.email,
firstName: values.firstname, firstName: values.firstname,
lastName: values.lastname, lastName: values.lastname,
organization: organization, organization: organization,
requestId: requestId,
idpIntent: idpIntent,
}) })
.catch(() => { .catch(() => {
setError("Could not create invitation Code"); setError("Could not register user");
return; return;
}) })
.finally(() => { .finally(() => {
setLoading(false); setLoading(false);
}); });
if (response && typeof response === "object" && "error" in response) { if (response && "error" in response && response.error) {
setError(response.error); setError(response.error);
return; return;
} }
if (!response) { if (response && "redirect" in response && response.redirect) {
setError("Could not create invitation Code"); return router.push(response.redirect);
return;
} }
const params = new URLSearchParams({}); return response;
if (response) {
params.append("userId", response);
}
return router.push(`/invite/success?` + params);
} }
const { errors } = formState; const { errors } = formState;
@@ -88,16 +97,6 @@ export function InviteForm({
return ( return (
<form className="w-full"> <form className="w-full">
<div className="grid grid-cols-2 gap-4 mb-4"> <div className="grid grid-cols-2 gap-4 mb-4">
<div className="col-span-2">
<TextInput
type="email"
autoComplete="email"
required
{...register("email", { required: "This field is required" })}
label="E-mail"
error={errors.email?.message as string}
/>
</div>
<div className=""> <div className="">
<TextInput <TextInput
type="firstname" type="firstname"
@@ -106,6 +105,7 @@ export function InviteForm({
{...register("firstname", { required: "This field is required" })} {...register("firstname", { required: "This field is required" })}
label="First name" label="First name"
error={errors.firstname?.message as string} error={errors.firstname?.message as string}
data-testid="firstname-text-input"
/> />
</div> </div>
<div className=""> <div className="">
@@ -116,6 +116,18 @@ export function InviteForm({
{...register("lastname", { required: "This field is required" })} {...register("lastname", { required: "This field is required" })}
label="Last name" label="Last name"
error={errors.lastname?.message as string} error={errors.lastname?.message as string}
data-testid="lastname-text-input"
/>
</div>
<div className="col-span-2">
<TextInput
type="email"
autoComplete="email"
required
{...register("email", { required: "This field is required" })}
label="E-mail"
error={errors.email?.message as string}
data-testid="email-text-input"
/> />
</div> </div>
</div> </div>
@@ -127,15 +139,16 @@ export function InviteForm({
)} )}
<div className="mt-8 flex w-full flex-row items-center justify-between"> <div className="mt-8 flex w-full flex-row items-center justify-between">
<BackButton /> <BackButton data-testid="back-button" />
<Button <Button
type="submit" type="submit"
variant={ButtonVariants.Primary} variant={ButtonVariants.Primary}
disabled={loading || !formState.isValid} disabled={loading || !formState.isValid}
onClick={handleSubmit(submitAndContinue)} onClick={handleSubmit(submitAndRegister)}
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,11 +6,10 @@ 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";
import { Alert } from "./alert"; import { Alert, AlertType } from "./alert";
import { import {
AuthenticationMethod, AuthenticationMethod,
AuthenticationMethodRadio, AuthenticationMethodRadio,
@@ -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 =
| { | {
@@ -35,9 +35,10 @@ type Props = {
firstname?: string; firstname?: string;
lastname?: string; lastname?: string;
email?: string; email?: string;
organization?: string; organization: string;
requestId?: string; requestId?: string;
loginSettings?: LoginSettings; loginSettings?: LoginSettings;
idpCount: number;
}; };
export function RegisterForm({ export function RegisterForm({
@@ -48,9 +49,8 @@ export function RegisterForm({
organization, organization,
requestId, requestId,
loginSettings, loginSettings,
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: {
@@ -165,17 +165,34 @@ export function RegisterForm({
onChange={setTosAndPolicyAccepted} onChange={setTosAndPolicyAccepted}
/> />
)} )}
<p className="mt-4 ztdl-p mb-6 block text-left">{t("selectMethod")}</p>
{/* show chooser if both methods are allowed */} {/* show chooser if both methods are allowed */}
{loginSettings && {loginSettings &&
loginSettings.allowUsernamePassword && loginSettings.allowUsernamePassword &&
loginSettings.passkeysType == PasskeysType.ALLOWED && ( loginSettings.passkeysType == PasskeysType.ALLOWED && (
<>
<p className="mt-4 ztdl-p mb-6 block text-left">
<Translated i18nKey="selectMethod" namespace="register" />
</p>
<div className="pb-4"> <div className="pb-4">
<AuthenticationMethodRadio <AuthenticationMethodRadio
selected={selected} selected={selected}
selectionChanged={setSelected} selectionChanged={setSelected}
/> />
</div> </div>
</>
)}
{!loginSettings?.allowUsernamePassword &&
loginSettings?.passkeysType !== PasskeysType.ALLOWED &&
(!loginSettings?.allowExternalIdp || !idpCount) && (
<div className="py-4">
<Alert type={AlertType.INFO}>
<Translated
i18nKey="noMethodAvailableWarning"
namespace="register"
/>
</Alert>
</div>
)} )}
{error && ( {error && (
@@ -183,6 +200,7 @@ export function RegisterForm({
<Alert>{error}</Alert> <Alert>{error}</Alert>
</div> </div>
)} )}
<div className="mt-8 flex w-full flex-row items-center justify-between"> <div className="mt-8 flex w-full flex-row items-center justify-between">
<BackButton data-testid="back-button" /> <BackButton data-testid="back-button" />
<Button <Button
@@ -201,7 +219,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

@@ -0,0 +1,105 @@
"use client";
import { clearSession } from "@/lib/server/session";
import { timestampDate } from "@zitadel/client";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import moment from "moment";
import { useLocale } from "next-intl";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { Avatar } from "./avatar";
import { isSessionValid } from "./session-item";
import { Translated } from "./translated";
export function SessionClearItem({
session,
reload,
}: {
session: Session;
reload: () => void;
}) {
const currentLocale = useLocale();
moment.locale(currentLocale === "zh" ? "zh-cn" : currentLocale);
const [loading, setLoading] = useState<boolean>(false);
async function clearSessionId(id: string) {
setLoading(true);
const response = await clearSession({
sessionId: id,
})
.catch((error) => {
setError(error.message);
return;
})
.finally(() => {
setLoading(false);
});
return response;
}
const { valid, verifiedAt } = isSessionValid(session);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
return (
<button
onClick={async () => {
clearSessionId(session.id).then(() => {
reload();
});
}}
className="group flex flex-row items-center bg-background-light-400 dark:bg-background-dark-400 border border-divider-light hover:shadow-lg dark:hover:bg-white/10 py-2 px-4 rounded-md transition-all"
>
<div className="pr-4">
<Avatar
size="small"
loginName={session.factors?.user?.loginName as string}
name={session.factors?.user?.displayName ?? ""}
/>
</div>
<div className="flex flex-col items-start overflow-hidden">
<span className="">{session.factors?.user?.displayName}</span>
<span className="text-xs opacity-80 text-ellipsis">
{session.factors?.user?.loginName}
</span>
{valid ? (
<span className="text-xs opacity-80 text-ellipsis">
{verifiedAt && (
<Translated
i18nKey="verfiedAt"
namespace="logout"
data={{ time: moment(timestampDate(verifiedAt)).fromNow() }}
/>
)}
</span>
) : (
verifiedAt && (
<span className="text-xs opacity-80 text-ellipsis">
expired{" "}
{session.expirationDate &&
moment(timestampDate(session.expirationDate)).fromNow()}
</span>
)
)}
</div>
<span className="flex-grow"></span>
<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">
<Translated i18nKey="clear" namespace="logout" />
</div>
{valid ? (
<div className="absolute h-2 w-2 bg-green-500 rounded-full mx-2 transform right-0 transition-all"></div>
) : (
<div className="absolute h-2 w-2 bg-red-500 rounded-full mx-2 transform right-0 transition-all"></div>
)}
</div>
</button>
);
}

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { sendLoginname } from "@/lib/server/loginname"; import { sendLoginname } from "@/lib/server/loginname";
import { cleanupSession, continueWithSession } from "@/lib/server/session"; import { clearSession, continueWithSession } from "@/lib/server/session";
import { XCircleIcon } from "@heroicons/react/24/outline"; import { XCircleIcon } from "@heroicons/react/24/outline";
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";
@@ -43,9 +43,9 @@ export function SessionItem({
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
async function clearSession(id: string) { async function clearSessionId(id: string) {
setLoading(true); setLoading(true);
const response = await cleanupSession({ const response = await clearSession({
sessionId: id, sessionId: id,
}) })
.catch((error) => { .catch((error) => {
@@ -145,7 +145,7 @@ export function SessionItem({
onClick={(event) => { onClick={(event) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
clearSession(session.id).then(() => { clearSessionId(session.id).then(() => {
reload(); reload();
}); });
}} }}

View File

@@ -0,0 +1,109 @@
"use client";
import { clearSession } from "@/lib/server/session";
import { timestampDate } from "@zitadel/client";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { redirect, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { Alert, AlertType } from "./alert";
import { SessionClearItem } from "./session-clear-item";
import { Translated } from "./translated";
type Props = {
sessions: Session[];
postLogoutRedirectUri?: string;
logoutHint?: string;
organization?: string;
};
export function SessionsClearList({
sessions,
logoutHint,
postLogoutRedirectUri,
organization,
}: Props) {
const [list, setList] = useState<Session[]>(sessions);
const router = useRouter();
async function clearHintedSession() {
console.log("Clearing session for login hint:", logoutHint);
// If a login hint is provided, we logout that specific session
const sessionIdToBeCleared = sessions.find((session) => {
return session.factors?.user?.loginName === logoutHint;
})?.id;
if (sessionIdToBeCleared) {
const clearSessionResponse = await clearSession({
sessionId: sessionIdToBeCleared,
}).catch((error) => {
console.error("Error clearing session:", error);
return;
});
if (!clearSessionResponse) {
console.error("Failed to clear session for login hint:", logoutHint);
}
if (postLogoutRedirectUri) {
return redirect(postLogoutRedirectUri);
}
const params = new URLSearchParams();
if (organization) {
params.set("organization", organization);
}
return router.push("/logout/success?" + params);
} else {
console.warn(`No session found for login hint: ${logoutHint}`);
}
}
useEffect(() => {
if (logoutHint) {
clearHintedSession();
}
}, []);
return sessions ? (
<div className="flex flex-col space-y-2">
{list
.filter((session) => session?.factors?.user?.loginName)
// sort by change date descending
.sort((a, b) => {
const dateA = a.changeDate
? timestampDate(a.changeDate).getTime()
: 0;
const dateB = b.changeDate
? timestampDate(b.changeDate).getTime()
: 0;
return dateB - dateA;
})
// TODO: add sorting to move invalid sessions to the bottom
.map((session, index) => {
return (
<SessionClearItem
session={session}
reload={() => {
setList(list.filter((s) => s.id !== session.id));
if (postLogoutRedirectUri) {
router.push(postLogoutRedirectUri);
}
}}
key={"session-" + index}
/>
);
})}
{list.length === 0 && (
<Alert type={AlertType.INFO}>
<Translated i18nKey="noResults" namespace="logout" />
</Alert>
)}
</div>
) : (
<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 =
| { | {
@@ -31,7 +31,7 @@ type Props = {
email: string; email: string;
firstname: string; firstname: string;
lastname: string; lastname: string;
organization?: string; organization: string;
requestId?: string; requestId?: string;
}; };
@@ -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

@@ -56,6 +56,7 @@ export function SignInWithIdp({
[IdentityProviderType.GITLAB_SELF_HOSTED]: SignInWithGitlab, [IdentityProviderType.GITLAB_SELF_HOSTED]: SignInWithGitlab,
[IdentityProviderType.SAML]: SignInWithGeneric, [IdentityProviderType.SAML]: SignInWithGeneric,
[IdentityProviderType.LDAP]: SignInWithGeneric, [IdentityProviderType.LDAP]: SignInWithGeneric,
[IdentityProviderType.JWT]: SignInWithGeneric,
}; };
const Component = components[type]; const Component = components[type];

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>
@@ -149,7 +148,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

@@ -141,7 +141,7 @@ export async function removeSessionFromCookie<T>({
session: SessionCookie<T>; session: SessionCookie<T>;
cleanup?: boolean; cleanup?: boolean;
sameSite?: boolean | "lax" | "strict" | "none" | undefined; sameSite?: boolean | "lax" | "strict" | "none" | undefined;
}): Promise<any> { }) {
const cookiesList = await cookies(); const cookiesList = await cookies();
const stringifiedCookie = cookiesList.get("sessions"); const stringifiedCookie = cookiesList.get("sessions");

View File

@@ -26,6 +26,8 @@ export function idpTypeToSlug(idpType: IdentityProviderType) {
return "oidc"; return "oidc";
case IdentityProviderType.LDAP: case IdentityProviderType.LDAP:
return "ldap"; return "ldap";
case IdentityProviderType.JWT:
return "jwt";
default: default:
throw new Error("Unknown identity provider type"); throw new Error("Unknown identity provider type");
} }
@@ -66,6 +68,9 @@ export function idpTypeToIdentityProviderType(
case IDPType.IDP_TYPE_OIDC: case IDPType.IDP_TYPE_OIDC:
return IdentityProviderType.OIDC; return IdentityProviderType.OIDC;
case IDPType.IDP_TYPE_JWT:
return IdentityProviderType.JWT;
default: default:
throw new Error("Unknown identity provider type"); throw new Error("Unknown identity provider type");
} }

View File

@@ -109,15 +109,20 @@ export async function createSessionAndUpdateCookie(command: {
} }
} }
export async function createSessionForIdpAndUpdateCookie( export async function createSessionForIdpAndUpdateCookie({
userId: string, userId,
idpIntent,
requestId,
lifetime,
}: {
userId: string;
idpIntent: { idpIntent: {
idpIntentId?: string | undefined; idpIntentId?: string | undefined;
idpIntentToken?: string | undefined; idpIntentToken?: string | undefined;
}, };
requestId: string | undefined, requestId: string | undefined;
lifetime?: Duration, lifetime?: Duration;
): Promise<Session> { }): Promise<Session> {
const _headers = await headers(); const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers); const { serviceUrl } = getServiceUrlFromHeaders(_headers);

View File

@@ -137,12 +137,12 @@ export async function createNewSessionFromIdpIntent(
organization: userResponse.user.details?.resourceOwner, organization: userResponse.user.details?.resourceOwner,
}); });
const session = await createSessionForIdpAndUpdateCookie( const session = await createSessionForIdpAndUpdateCookie({
command.userId, userId: command.userId,
command.idpIntent, idpIntent: command.idpIntent,
command.requestId, requestId: command.requestId,
loginSettings?.externalLoginCheckLifetime, lifetime: loginSettings?.externalLoginCheckLifetime,
); });
if (!session || !session.factors?.user) { if (!session || !session.factors?.user) {
return { error: "Could not create session" }; return { error: "Could not create session" };

View File

@@ -1,58 +0,0 @@
"use server";
import { addHumanUser, createInviteCode } from "@/lib/zitadel";
import { Factors } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { headers } from "next/headers";
import { getServiceUrlFromHeaders } from "../service-url";
type InviteUserCommand = {
email: string;
firstName: string;
lastName: string;
password?: string;
organization?: string;
requestId?: string;
};
export type RegisterUserResponse = {
userId: string;
sessionId: string;
factors: Factors | undefined;
};
export async function inviteUser(command: InviteUserCommand) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const host = _headers.get("host");
if (!host) {
return { error: "Could not get domain" };
}
const human = await addHumanUser({
serviceUrl,
email: command.email,
firstName: command.firstName,
lastName: command.lastName,
password: command.password ? command.password : undefined,
organization: command.organization,
});
if (!human) {
return { error: "Could not create user" };
}
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
const codeResponse = await createInviteCode({
serviceUrl,
urlTemplate: `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true`,
userId: human.userId,
});
if (!codeResponse || !human) {
return { error: "Could not create invite code" };
}
return human.userId;
}

View File

@@ -1,7 +1,15 @@
"use server"; "use server";
import { createSessionAndUpdateCookie } from "@/lib/server/cookie"; import {
import { addHumanUser, getLoginSettings, getUserByID } from "@/lib/zitadel"; createSessionAndUpdateCookie,
createSessionForIdpAndUpdateCookie,
} from "@/lib/server/cookie";
import {
addHumanUser,
addIDPLink,
getLoginSettings,
getUserByID,
} from "@/lib/zitadel";
import { create } from "@zitadel/client"; import { create } from "@zitadel/client";
import { Factors } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { Factors } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { import {
@@ -18,7 +26,7 @@ type RegisterUserCommand = {
firstName: string; firstName: string;
lastName: string; lastName: string;
password?: string; password?: string;
organization?: string; organization: string;
requestId?: string; requestId?: string;
}; };
@@ -133,3 +141,93 @@ export async function registerUser(command: RegisterUserCommand) {
return { redirect: url }; return { redirect: url };
} }
} }
type RegisterUserAndLinkToIDPommand = {
email: string;
firstName: string;
lastName: string;
organization: string;
requestId?: string;
idpIntent: {
idpIntentId: string;
idpIntentToken: string;
};
idpUserId: string;
idpId: string;
idpUserName: string;
};
export type registerUserAndLinkToIDPResponse = {
userId: string;
sessionId: string;
factors: Factors | undefined;
};
export async function registerUserAndLinkToIDP(
command: RegisterUserAndLinkToIDPommand,
) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const host = _headers.get("host");
if (!host || typeof host !== "string") {
throw new Error("No host found");
}
const addResponse = await addHumanUser({
serviceUrl,
email: command.email,
firstName: command.firstName,
lastName: command.lastName,
organization: command.organization,
});
if (!addResponse) {
return { error: "Could not create user" };
}
const loginSettings = await getLoginSettings({
serviceUrl,
organization: command.organization,
});
const idpLink = await addIDPLink({
serviceUrl,
idp: {
id: command.idpId,
userId: command.idpUserId,
userName: command.idpUserName,
},
userId: addResponse.userId,
});
if (!idpLink) {
return { error: "Could not link IDP to user" };
}
const session = await createSessionForIdpAndUpdateCookie({
requestId: command.requestId,
userId: addResponse.userId, // the user we just created
idpIntent: command.idpIntent,
lifetime: loginSettings?.externalLoginCheckLifetime,
});
if (!session || !session.factors?.user) {
return { error: "Could not create session" };
}
const url = await getNextUrl(
command.requestId && session.id
? {
sessionId: session.id,
requestId: command.requestId,
organization: session.factors.user.organizationId,
}
: {
loginName: session.factors.user.loginName,
organization: session.factors.user.organizationId,
},
loginSettings?.defaultRedirectUri,
);
return { redirect: url };
}

View File

@@ -202,30 +202,6 @@ export async function clearSession(options: ClearSessionOptions) {
const { sessionId } = options; const { sessionId } = options;
const session = await getSessionCookieById({ sessionId });
const deletedSession = await deleteSession({
serviceUrl,
sessionId: session.id,
sessionToken: session.token,
});
const securitySettings = await getSecuritySettings({ serviceUrl });
const sameSite = securitySettings?.embeddedIframe?.enabled ? "none" : true;
if (deletedSession) {
return removeSessionFromCookie({ session, sameSite });
}
}
type CleanupSessionCommand = {
sessionId: string;
};
export async function cleanupSession({ sessionId }: CleanupSessionCommand) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const sessionCookie = await getSessionCookieById({ sessionId }); const sessionCookie = await getSessionCookieById({ sessionId });
const deleteResponse = await deleteSession({ const deleteResponse = await deleteSession({

View File

@@ -1,7 +1,10 @@
import { Client, create, Duration } from "@zitadel/client"; import { Client, create, Duration } from "@zitadel/client";
import { makeReqCtx } from "@zitadel/client/v2"; import { makeReqCtx } from "@zitadel/client/v2";
import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_service_pb"; import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_service_pb";
import { TextQueryMethod } from "@zitadel/proto/zitadel/object/v2/object_pb"; import {
OrganizationSchema,
TextQueryMethod,
} from "@zitadel/proto/zitadel/object/v2/object_pb";
import { import {
CreateCallbackRequest, CreateCallbackRequest,
OIDCService, OIDCService,
@@ -32,11 +35,13 @@ import {
import { SendInviteCodeSchema } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { SendInviteCodeSchema } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { import {
AddHumanUserRequest, AddHumanUserRequest,
AddHumanUserRequestSchema,
ResendEmailCodeRequest, ResendEmailCodeRequest,
ResendEmailCodeRequestSchema, ResendEmailCodeRequestSchema,
SendEmailCodeRequestSchema, SendEmailCodeRequestSchema,
SetPasswordRequest, SetPasswordRequest,
SetPasswordRequestSchema, SetPasswordRequestSchema,
UpdateHumanUserRequest,
UserService, UserService,
VerifyPasskeyRegistrationRequest, VerifyPasskeyRegistrationRequest,
VerifyU2FRegistrationRequest, VerifyU2FRegistrationRequest,
@@ -54,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,
@@ -387,8 +428,8 @@ export type AddHumanUserData = {
firstName: string; firstName: string;
lastName: string; lastName: string;
email: string; email: string;
password: string | undefined; password?: string;
organization: string | undefined; organization: string;
}; };
export async function addHumanUser({ export async function addHumanUser({
@@ -404,7 +445,9 @@ export async function addHumanUser({
serviceUrl, serviceUrl,
); );
return userService.addHumanUser({ let addHumanUserRequest: AddHumanUserRequest = create(
AddHumanUserRequestSchema,
{
email: { email: {
email, email,
verification: { verification: {
@@ -414,13 +457,24 @@ export async function addHumanUser({
}, },
username: email, username: email,
profile: { givenName: firstName, familyName: lastName }, profile: { givenName: firstName, familyName: lastName },
organization: organization
? { org: { case: "orgId", value: organization } }
: undefined,
passwordType: password passwordType: password
? { case: "password", value: { password } } ? { case: "password", value: { password } }
: undefined, : undefined,
},
);
if (organization) {
const organizationSchema = create(OrganizationSchema, {
org: { case: "orgId", value: organization },
}); });
addHumanUserRequest = {
...addHumanUserRequest,
organization: organizationSchema,
};
}
return userService.addHumanUser(addHumanUserRequest);
} }
export async function addHuman({ export async function addHuman({
@@ -438,6 +492,21 @@ export async function addHuman({
return userService.addHumanUser(request); return userService.addHumanUser(request);
} }
export async function updateHuman({
serviceUrl,
request,
}: {
serviceUrl: string;
request: UpdateHumanUserRequest;
}) {
const userService: Client<typeof UserService> = await createServiceForHost(
UserService,
serviceUrl,
);
return userService.updateHumanUser(request);
}
export async function verifyTOTPRegistration({ export async function verifyTOTPRegistration({
serviceUrl, serviceUrl,
code, code,

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(" ")};`,

209
pnpm-lock.yaml generated
View File

@@ -85,7 +85,7 @@ importers:
version: 0.5.7(tailwindcss@3.4.14) version: 0.5.7(tailwindcss@3.4.14)
'@vercel/analytics': '@vercel/analytics':
specifier: ^1.2.2 specifier: ^1.2.2
version: 1.3.1(next@15.4.0-canary.3(@babel/core@7.26.10)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.87.0))(react@19.1.0) version: 1.3.1(next@15.4.0-canary.86(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.87.0))(react@19.1.0)
'@zitadel/client': '@zitadel/client':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/zitadel-client version: link:../../packages/zitadel-client
@@ -101,9 +101,6 @@ importers:
deepmerge: deepmerge:
specifier: ^4.3.1 specifier: ^4.3.1
version: 4.3.1 version: 4.3.1
jose:
specifier: ^5.3.0
version: 5.8.0
lucide-react: lucide-react:
specifier: 0.469.0 specifier: 0.469.0
version: 0.469.0(react@19.1.0) version: 0.469.0(react@19.1.0)
@@ -111,14 +108,14 @@ importers:
specifier: ^2.29.4 specifier: ^2.29.4
version: 2.30.1 version: 2.30.1
next: next:
specifier: 15.4.0-canary.3 specifier: 15.4.0-canary.86
version: 15.4.0-canary.3(@babel/core@7.26.10)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.87.0) version: 15.4.0-canary.86(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.87.0)
next-intl: next-intl:
specifier: ^3.25.1 specifier: ^3.25.1
version: 3.25.1(next@15.4.0-canary.3(@babel/core@7.26.10)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.87.0))(react@19.1.0) version: 3.26.5(next@15.4.0-canary.86(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.87.0))(react@19.1.0)
next-themes: next-themes:
specifier: ^0.2.1 specifier: ^0.2.1
version: 0.2.1(next@15.4.0-canary.3(@babel/core@7.26.10)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.87.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) version: 0.2.1(next@15.4.0-canary.86(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.87.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
nice-grpc: nice-grpc:
specifier: 2.0.1 specifier: 2.0.1
version: 2.0.1 version: 2.0.1
@@ -134,9 +131,6 @@ importers:
react-hook-form: react-hook-form:
specifier: 7.39.5 specifier: 7.39.5
version: 7.39.5(react@19.1.0) version: 7.39.5(react@19.1.0)
swr:
specifier: ^2.2.0
version: 2.2.5(react@19.1.0)
tinycolor2: tinycolor2:
specifier: 1.4.2 specifier: 1.4.2
version: 1.4.2 version: 1.4.2
@@ -807,9 +801,6 @@ packages:
'@formatjs/icu-skeleton-parser@1.8.8': '@formatjs/icu-skeleton-parser@1.8.8':
resolution: {integrity: sha512-vHwK3piXwamFcx5YQdCdJxUQ1WdTl6ANclt5xba5zLGDv5Bsur7qz8AD7BevaKxITwpgDeU0u8My3AIibW9ywA==} resolution: {integrity: sha512-vHwK3piXwamFcx5YQdCdJxUQ1WdTl6ANclt5xba5zLGDv5Bsur7qz8AD7BevaKxITwpgDeU0u8My3AIibW9ywA==}
'@formatjs/intl-localematcher@0.5.4':
resolution: {integrity: sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==}
'@formatjs/intl-localematcher@0.5.8': '@formatjs/intl-localematcher@0.5.8':
resolution: {integrity: sha512-I+WDNWWJFZie+jkfkiK5Mp4hEDyRSEvmyfYadflOno/mmKJKcB17fEpEH0oJu/OWhhCJ8kJBDz2YMd/6cDl7Mg==} resolution: {integrity: sha512-I+WDNWWJFZie+jkfkiK5Mp4hEDyRSEvmyfYadflOno/mmKJKcB17fEpEH0oJu/OWhhCJ8kJBDz2YMd/6cDl7Mg==}
@@ -998,56 +989,56 @@ packages:
resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==}
hasBin: true hasBin: true
'@next/env@15.4.0-canary.3': '@next/env@15.4.0-canary.86':
resolution: {integrity: sha512-lu4pB2e3Z/d+B0rxEm9YuQMb57Hd96iJUBZgVlcRNemlIryr0GByu17kvN6nBk3JjbWL8h+MW90stpGzGdhbqg==} resolution: {integrity: sha512-WPrEvwqHnjeLx05ncJvqizbBJJFlQGRbxzOnL/pZWKzo19auM9x5Se87P27+E/D/d6jJS801l+thF85lfobAZQ==}
'@next/eslint-plugin-next@14.2.18': '@next/eslint-plugin-next@14.2.18':
resolution: {integrity: sha512-KyYTbZ3GQwWOjX3Vi1YcQbekyGP0gdammb7pbmmi25HBUCINzDReyrzCMOJIeZisK1Q3U6DT5Rlc4nm2/pQeXA==} resolution: {integrity: sha512-KyYTbZ3GQwWOjX3Vi1YcQbekyGP0gdammb7pbmmi25HBUCINzDReyrzCMOJIeZisK1Q3U6DT5Rlc4nm2/pQeXA==}
'@next/swc-darwin-arm64@15.4.0-canary.3': '@next/swc-darwin-arm64@15.4.0-canary.86':
resolution: {integrity: sha512-w9u8IpwLb/JS7HzHLt24smP4FxIYMgciOtYNUCognO1xh1XZfqqjDIrRAXDuuYDPKrc1i2EvI24R5eDTz7EYMQ==} resolution: {integrity: sha512-1ofBmzjPkmoMdM+dXvybZ/Roq8HRo0sFzcwXk7/FJNOufuwyK+QKdSpLE7pHlPR7ZREqfEMj61ONO+gAK+zOJw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@next/swc-darwin-x64@15.4.0-canary.3': '@next/swc-darwin-x64@15.4.0-canary.86':
resolution: {integrity: sha512-5pL1hBRw8h1XeArzWYjCDERtRFIfrMAz1Nq9m1np8FrTuHclE7xitKKfOJqqmBbO9dWtnZIfA8lZl9bdlNEUZg==} resolution: {integrity: sha512-WCKSrllvwzYi4TgrSdgxKSOF2nhieeaWWOeGucn0OXy50uOAamr0HwP5OaIBCx3oRar4w66gvs4IrdTdMedeJA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@next/swc-linux-arm64-gnu@15.4.0-canary.3': '@next/swc-linux-arm64-gnu@15.4.0-canary.86':
resolution: {integrity: sha512-vx6cU4jKoecF2QZw3CQqJrzb+D0WhNzHHoWUN8O+YKPnX0oG4wEtAQWSWisxKjNrU1U4TiraOql0nOQBUOKwaQ==} resolution: {integrity: sha512-8qn7DJVNFjhEIDo2ts0YCsO7g+vJjPWh8Ur8lBK3XspeX0BPsF4s+YmgidrpzRXeIfoo2uYLkkXcy/57CVDblw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@next/swc-linux-arm64-musl@15.4.0-canary.3': '@next/swc-linux-arm64-musl@15.4.0-canary.86':
resolution: {integrity: sha512-7ig1sQHRRgTrj4QHt5l8OT1z2SJnEAHbnEY9SDP2HilwQIfgOAOxveFDBR+f/8AMdAKhCTSeMyrZsivpC0xTUA==} resolution: {integrity: sha512-8MTn6N4Ja25neMLu2Bra1lqW9AWPqsYg0BVs5M/cxL0QkcN3mak/8LLX1vbzz7GigMGSA+NLwg+ol8lglfgIGA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@next/swc-linux-x64-gnu@15.4.0-canary.3': '@next/swc-linux-x64-gnu@15.4.0-canary.86':
resolution: {integrity: sha512-fML6pzNX9i3DlrCOdE6A1TbVL0aIQkIDDCjrbn/f37hOn88god1OrVd/d4J4w1YqLKQWpmJPnUn6Bkn8qXqbRw==} resolution: {integrity: sha512-hIhzDwWDQHnH0M0Pzaqs1c5fa4+LHiLLEBuPJQvhBxQfH+Eh86DWiWHDCaoNiURvdRPg6uCuF2MjwptrMplEkg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@next/swc-linux-x64-musl@15.4.0-canary.3': '@next/swc-linux-x64-musl@15.4.0-canary.86':
resolution: {integrity: sha512-87/JPkbr3fgvASdWW2qBVuaXwcjSxgy+CTllj2DgYB7e7BEzT7QJEdj0HJZljBjVbN5oT1FOKwhaVRgRWuwYLQ==} resolution: {integrity: sha512-FG6SBuSeRWYMNu6tsfaZ4iDzv3BLxlpRncO2xvKKQPeUdDSQ0cehuHYnx8fRte8IOAJ3rlbRd6NXvrDarqu92Q==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@next/swc-win32-arm64-msvc@15.4.0-canary.3': '@next/swc-win32-arm64-msvc@15.4.0-canary.86':
resolution: {integrity: sha512-cTZh72h3ZX8z0lhdVs5m38uyy83mW5r0jz6hKagysPT06uTdOAypK6CRqG5CJSN7RM0n7CkfcO6ExjDqhkDhRA==} resolution: {integrity: sha512-3HvZo4VuyINrNYplRhvC8ILdKwi/vFDHOcTN/I4ru039TFpu2eO6VtXsLBdOdJjGslSSSBYkX+6yRrghihAZDA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@next/swc-win32-x64-msvc@15.4.0-canary.3': '@next/swc-win32-x64-msvc@15.4.0-canary.86':
resolution: {integrity: sha512-8oZKOKRGad4EVZ94L5Sz2EP59khHIeKGKg+/z8r5mCbBtupLPTXmWjrXoi1R55hHRXJjbW2D5NwcPfJn/ltZ3Q==} resolution: {integrity: sha512-UO9JzGGj7GhtSJFdI0Bl0dkIIBfgbhXLsgNVmq9Z/CsUsQB6J9RS/BMhsxfVwhO+RETk13nFpNutMAhAwcuD8w==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
@@ -1824,10 +1815,6 @@ packages:
peerDependencies: peerDependencies:
esbuild: '>=0.18' esbuild: '>=0.18'
busboy@1.6.0:
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
engines: {node: '>=10.16.0'}
cac@6.7.14: cac@6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -1856,9 +1843,6 @@ packages:
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
caniuse-lite@1.0.30001680:
resolution: {integrity: sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==}
caniuse-lite@1.0.30001715: caniuse-lite@1.0.30001715:
resolution: {integrity: sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==} resolution: {integrity: sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==}
@@ -2159,10 +2143,6 @@ packages:
engines: {node: '>=0.10'} engines: {node: '>=0.10'}
hasBin: true hasBin: true
detect-libc@2.0.3:
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
engines: {node: '>=8'}
detect-libc@2.0.4: detect-libc@2.0.4:
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -3389,11 +3369,11 @@ packages:
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
next-intl@3.25.1: next-intl@3.26.5:
resolution: {integrity: sha512-Z2dJWn5f/b1sb8EmuJcuDhbQTIp4RG1KBFAILgRt/y27W0ifU7Ll/os3liphUY4InyRH89uShTAk7ItAlpr0uA==} resolution: {integrity: sha512-EQlCIfY0jOhRldiFxwSXG+ImwkQtDEfQeSOEQp6ieAGSLWGlgjdb/Ck/O7wMfC430ZHGeUKVKax8KGusTPKCgg==}
peerDependencies: peerDependencies:
next: ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 next: ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
next-themes@0.2.1: next-themes@0.2.1:
resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==} resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==}
@@ -3402,13 +3382,13 @@ packages:
react: '*' react: '*'
react-dom: '*' react-dom: '*'
next@15.4.0-canary.3: next@15.4.0-canary.86:
resolution: {integrity: sha512-OkwxAFNQeuE0vNL7tTwU+jm3nf3x3D5DHSmjRlFktsedGtxZiILZTq6UNExNaFBjttR+2Y6oGqRsFWXC4ob1Wg==} resolution: {integrity: sha512-lGeO0sOvPZ7oFIklqRA863YzRL1bW+kT/OqU3N6RBquHldiucZwnZKQceZdn6WcHEFmWIHzZV+SMG1JEK7hZLg==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
'@opentelemetry/api': ^1.1.0 '@opentelemetry/api': ^1.1.0
'@playwright/test': ^1.41.2 '@playwright/test': ^1.51.1
babel-plugin-react-compiler: '*' babel-plugin-react-compiler: '*'
react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
@@ -4170,10 +4150,6 @@ packages:
stream-combiner@0.0.4: stream-combiner@0.0.4:
resolution: {integrity: sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==} resolution: {integrity: sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==}
streamsearch@1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'}
string-argv@0.3.2: string-argv@0.3.2:
resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
engines: {node: '>=0.6.19'} engines: {node: '>=0.6.19'}
@@ -4276,11 +4252,6 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
swr@2.2.5:
resolution: {integrity: sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==}
peerDependencies:
react: ^16.11.0 || ^17.0.0 || ^18.0.0
symbol-tree@3.2.4: symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
@@ -4560,15 +4531,10 @@ packages:
uri-js@4.4.1: uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
use-intl@3.25.1: use-intl@3.26.5:
resolution: {integrity: sha512-Xeyl0+BjlBf6fJr2h5W/CESZ2IQAH7jzXYK4c/ao+qR26jNPW3FXBLjg7eLRxdeI6QaLcYGLtH3WYhC9I0+6Yg==} resolution: {integrity: sha512-OdsJnC/znPvHCHLQH/duvQNXnP1w0hPfS+tkSi3mAbfjYBGh4JnyfdwkQBfIVf7t8gs9eSX/CntxUMvtKdG2MQ==}
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
use-sync-external-store@1.2.2:
resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
util-deprecate@1.0.2: util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@@ -5350,10 +5316,6 @@ snapshots:
'@formatjs/ecma402-abstract': 2.2.4 '@formatjs/ecma402-abstract': 2.2.4
tslib: 2.8.1 tslib: 2.8.1
'@formatjs/intl-localematcher@0.5.4':
dependencies:
tslib: 2.8.1
'@formatjs/intl-localematcher@0.5.8': '@formatjs/intl-localematcher@0.5.8':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
@@ -5538,34 +5500,34 @@ snapshots:
- encoding - encoding
- supports-color - supports-color
'@next/env@15.4.0-canary.3': {} '@next/env@15.4.0-canary.86': {}
'@next/eslint-plugin-next@14.2.18': '@next/eslint-plugin-next@14.2.18':
dependencies: dependencies:
glob: 10.3.10 glob: 10.3.10
'@next/swc-darwin-arm64@15.4.0-canary.3': '@next/swc-darwin-arm64@15.4.0-canary.86':
optional: true optional: true
'@next/swc-darwin-x64@15.4.0-canary.3': '@next/swc-darwin-x64@15.4.0-canary.86':
optional: true optional: true
'@next/swc-linux-arm64-gnu@15.4.0-canary.3': '@next/swc-linux-arm64-gnu@15.4.0-canary.86':
optional: true optional: true
'@next/swc-linux-arm64-musl@15.4.0-canary.3': '@next/swc-linux-arm64-musl@15.4.0-canary.86':
optional: true optional: true
'@next/swc-linux-x64-gnu@15.4.0-canary.3': '@next/swc-linux-x64-gnu@15.4.0-canary.86':
optional: true optional: true
'@next/swc-linux-x64-musl@15.4.0-canary.3': '@next/swc-linux-x64-musl@15.4.0-canary.86':
optional: true optional: true
'@next/swc-win32-arm64-msvc@15.4.0-canary.3': '@next/swc-win32-arm64-msvc@15.4.0-canary.86':
optional: true optional: true
'@next/swc-win32-x64-msvc@15.4.0-canary.3': '@next/swc-win32-x64-msvc@15.4.0-canary.86':
optional: true optional: true
'@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1':
@@ -6027,11 +5989,11 @@ snapshots:
'@ungap/structured-clone@1.2.0': {} '@ungap/structured-clone@1.2.0': {}
'@vercel/analytics@1.3.1(next@15.4.0-canary.3(@babel/core@7.26.10)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.87.0))(react@19.1.0)': '@vercel/analytics@1.3.1(next@15.4.0-canary.86(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.87.0))(react@19.1.0)':
dependencies: dependencies:
server-only: 0.0.1 server-only: 0.0.1
optionalDependencies: optionalDependencies:
next: 15.4.0-canary.3(@babel/core@7.26.10)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.87.0) next: 15.4.0-canary.86(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.87.0)
react: 19.1.0 react: 19.1.0
'@vercel/git-hooks@1.0.0': {} '@vercel/git-hooks@1.0.0': {}
@@ -6099,7 +6061,7 @@ snapshots:
agent-base@6.0.2: agent-base@6.0.2:
dependencies: dependencies:
debug: 4.4.0(supports-color@5.5.0) debug: 4.4.0
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -6338,10 +6300,6 @@ snapshots:
esbuild: 0.25.2 esbuild: 0.25.2
load-tsconfig: 0.2.5 load-tsconfig: 0.2.5
busboy@1.6.0:
dependencies:
streamsearch: 1.1.0
cac@6.7.14: {} cac@6.7.14: {}
cachedir@2.4.0: {} cachedir@2.4.0: {}
@@ -6368,8 +6326,6 @@ snapshots:
camelcase-css@2.0.1: {} camelcase-css@2.0.1: {}
caniuse-lite@1.0.30001680: {}
caniuse-lite@1.0.30001715: {} caniuse-lite@1.0.30001715: {}
case-anything@2.1.13: {} case-anything@2.1.13: {}
@@ -6636,6 +6592,10 @@ snapshots:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
debug@4.4.0:
dependencies:
ms: 2.1.3
debug@4.4.0(supports-color@5.5.0): debug@4.4.0(supports-color@5.5.0):
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
@@ -6713,9 +6673,6 @@ snapshots:
detect-libc@1.0.3: {} detect-libc@1.0.3: {}
detect-libc@2.0.3:
optional: true
detect-libc@2.0.4: {} detect-libc@2.0.4: {}
didyoumean@1.2.2: {} didyoumean@1.2.2: {}
@@ -7321,7 +7278,7 @@ snapshots:
follow-redirects@1.15.9(debug@4.4.0): follow-redirects@1.15.9(debug@4.4.0):
optionalDependencies: optionalDependencies:
debug: 4.4.0(supports-color@5.5.0) debug: 4.4.0
for-each@0.3.3: for-each@0.3.3:
dependencies: dependencies:
@@ -7581,7 +7538,7 @@ snapshots:
http-proxy-agent@7.0.2: http-proxy-agent@7.0.2:
dependencies: dependencies:
agent-base: 7.1.3 agent-base: 7.1.3
debug: 4.4.0(supports-color@5.5.0) debug: 4.4.0
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -7594,14 +7551,14 @@ snapshots:
https-proxy-agent@5.0.1: https-proxy-agent@5.0.1:
dependencies: dependencies:
agent-base: 6.0.2 agent-base: 6.0.2
debug: 4.4.0(supports-color@5.5.0) debug: 4.4.0
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
https-proxy-agent@7.0.6: https-proxy-agent@7.0.6:
dependencies: dependencies:
agent-base: 7.1.3 agent-base: 7.1.3
debug: 4.4.0(supports-color@5.5.0) debug: 4.4.0
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -7950,7 +7907,7 @@ snapshots:
dependencies: dependencies:
chalk: 5.4.1 chalk: 5.4.1
commander: 13.1.0 commander: 13.1.0
debug: 4.4.0(supports-color@5.5.0) debug: 4.4.0
execa: 8.0.1 execa: 8.0.1
lilconfig: 3.1.3 lilconfig: 3.1.3
listr2: 8.3.2 listr2: 8.3.2
@@ -8134,40 +8091,38 @@ snapshots:
negotiator@1.0.0: {} negotiator@1.0.0: {}
next-intl@3.25.1(next@15.4.0-canary.3(@babel/core@7.26.10)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.87.0))(react@19.1.0): next-intl@3.26.5(next@15.4.0-canary.86(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.87.0))(react@19.1.0):
dependencies: dependencies:
'@formatjs/intl-localematcher': 0.5.4 '@formatjs/intl-localematcher': 0.5.8
negotiator: 1.0.0 negotiator: 1.0.0
next: 15.4.0-canary.3(@babel/core@7.26.10)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.87.0) next: 15.4.0-canary.86(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.87.0)
react: 19.1.0 react: 19.1.0
use-intl: 3.25.1(react@19.1.0) use-intl: 3.26.5(react@19.1.0)
next-themes@0.2.1(next@15.4.0-canary.3(@babel/core@7.26.10)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.87.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0): next-themes@0.2.1(next@15.4.0-canary.86(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.87.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies: dependencies:
next: 15.4.0-canary.3(@babel/core@7.26.10)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.87.0) next: 15.4.0-canary.86(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.87.0)
react: 19.1.0 react: 19.1.0
react-dom: 19.1.0(react@19.1.0) react-dom: 19.1.0(react@19.1.0)
next@15.4.0-canary.3(@babel/core@7.26.10)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.87.0): next@15.4.0-canary.86(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.87.0):
dependencies: dependencies:
'@next/env': 15.4.0-canary.3 '@next/env': 15.4.0-canary.86
'@swc/counter': 0.1.3
'@swc/helpers': 0.5.15 '@swc/helpers': 0.5.15
busboy: 1.6.0 caniuse-lite: 1.0.30001715
caniuse-lite: 1.0.30001680
postcss: 8.4.31 postcss: 8.4.31
react: 19.1.0 react: 19.1.0
react-dom: 19.1.0(react@19.1.0) react-dom: 19.1.0(react@19.1.0)
styled-jsx: 5.1.6(@babel/core@7.26.10)(react@19.1.0) styled-jsx: 5.1.6(react@19.1.0)
optionalDependencies: optionalDependencies:
'@next/swc-darwin-arm64': 15.4.0-canary.3 '@next/swc-darwin-arm64': 15.4.0-canary.86
'@next/swc-darwin-x64': 15.4.0-canary.3 '@next/swc-darwin-x64': 15.4.0-canary.86
'@next/swc-linux-arm64-gnu': 15.4.0-canary.3 '@next/swc-linux-arm64-gnu': 15.4.0-canary.86
'@next/swc-linux-arm64-musl': 15.4.0-canary.3 '@next/swc-linux-arm64-musl': 15.4.0-canary.86
'@next/swc-linux-x64-gnu': 15.4.0-canary.3 '@next/swc-linux-x64-gnu': 15.4.0-canary.86
'@next/swc-linux-x64-musl': 15.4.0-canary.3 '@next/swc-linux-x64-musl': 15.4.0-canary.86
'@next/swc-win32-arm64-msvc': 15.4.0-canary.3 '@next/swc-win32-arm64-msvc': 15.4.0-canary.86
'@next/swc-win32-x64-msvc': 15.4.0-canary.3 '@next/swc-win32-x64-msvc': 15.4.0-canary.86
'@playwright/test': 1.52.0 '@playwright/test': 1.52.0
sass: 1.87.0 sass: 1.87.0
sharp: 0.34.1 sharp: 0.34.1
@@ -8737,7 +8692,7 @@ snapshots:
sharp@0.34.1: sharp@0.34.1:
dependencies: dependencies:
color: 4.2.3 color: 4.2.3
detect-libc: 2.0.3 detect-libc: 2.0.4
semver: 7.7.1 semver: 7.7.1
optionalDependencies: optionalDependencies:
'@img/sharp-darwin-arm64': 0.34.1 '@img/sharp-darwin-arm64': 0.34.1
@@ -8882,7 +8837,7 @@ snapshots:
arg: 5.0.2 arg: 5.0.2
bluebird: 3.7.2 bluebird: 3.7.2
check-more-types: 2.24.0 check-more-types: 2.24.0
debug: 4.4.0(supports-color@5.5.0) debug: 4.4.0
execa: 5.1.1 execa: 5.1.1
lazy-ass: 1.6.0 lazy-ass: 1.6.0
ps-tree: 1.2.0 ps-tree: 1.2.0
@@ -8900,8 +8855,6 @@ snapshots:
dependencies: dependencies:
duplexer: 0.1.2 duplexer: 0.1.2
streamsearch@1.1.0: {}
string-argv@0.3.2: {} string-argv@0.3.2: {}
string-width@4.2.3: string-width@4.2.3:
@@ -8990,12 +8943,10 @@ snapshots:
strip-json-comments@3.1.1: {} strip-json-comments@3.1.1: {}
styled-jsx@5.1.6(@babel/core@7.26.10)(react@19.1.0): styled-jsx@5.1.6(react@19.1.0):
dependencies: dependencies:
client-only: 0.0.1 client-only: 0.0.1
react: 19.1.0 react: 19.1.0
optionalDependencies:
'@babel/core': 7.26.10
sucrase@3.35.0: sucrase@3.35.0:
dependencies: dependencies:
@@ -9021,12 +8972,6 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {} supports-preserve-symlinks-flag@1.0.0: {}
swr@2.2.5(react@19.1.0):
dependencies:
client-only: 0.0.1
react: 19.1.0
use-sync-external-store: 1.2.2(react@19.1.0)
symbol-tree@3.2.4: {} symbol-tree@3.2.4: {}
tabbable@6.2.0: {} tabbable@6.2.0: {}
@@ -9309,16 +9254,12 @@ snapshots:
dependencies: dependencies:
punycode: 2.3.1 punycode: 2.3.1
use-intl@3.25.1(react@19.1.0): use-intl@3.26.5(react@19.1.0):
dependencies: dependencies:
'@formatjs/fast-memoize': 2.2.3 '@formatjs/fast-memoize': 2.2.3
intl-messageformat: 10.7.7 intl-messageformat: 10.7.7
react: 19.1.0 react: 19.1.0
use-sync-external-store@1.2.2(react@19.1.0):
dependencies:
react: 19.1.0
util-deprecate@1.0.2: {} util-deprecate@1.0.2: {}
uuid@11.1.0: {} uuid@11.1.0: {}