diff --git a/README.md b/README.md index 67aef1f959..5007ddf2ab 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ You can already use the current state, and extend it with your needs. - [x] Apple - [x] Generic OIDC - [x] Generic OAuth - - [ ] Generic JWT + - [x] Generic JWT - [ ] LDAP - [ ] SAML SP - Multifactor Registration an Login @@ -73,7 +73,7 @@ You can already use the current state, and extend it with your needs. - [x] TOTP - [x] OTP: Email Code - [x] OTP: SMS Code -- [ ] Password Change/Reset +- [x] Password Change/Reset - [x] Domain Discovery - [x] Branding - OIDC Standard diff --git a/apps/login/locales/de.json b/apps/login/locales/de.json index 7606ef1a0a..ecb4740682 100644 --- a/apps/login/locales/de.json +++ b/apps/login/locales/de.json @@ -8,10 +8,22 @@ "addAnother": "Ein weiteres Konto hinzufügen", "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": { "title": "Willkommen zurück!", "description": "Geben Sie Ihre Anmeldedaten ein.", - "register": "Neuen Benutzer registrieren" + "register": "Neuen Benutzer registrieren", + "submit": "Weiter" }, "password": { "verify": { @@ -62,6 +74,10 @@ "linkingError": { "title": "Konto-Verknüpfung fehlgeschlagen", "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": { @@ -139,11 +155,13 @@ }, "title": "Registrieren", "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", "agreeTo": "Um sich zu registrieren, müssen Sie den Nutzungsbedingungen zustimmen", "termsOfService": "Nutzungsbedingungen", "privacyPolicy": "Datenschutzrichtlinie", "submit": "Weiter", + "orUseIDP": "oder verwenden Sie einen Identitätsanbieter", "password": { "title": "Passwort festlegen", "description": "Legen Sie das Passwort für Ihr Konto fest", diff --git a/apps/login/locales/en.json b/apps/login/locales/en.json index 81c93bb549..9f95403063 100644 --- a/apps/login/locales/en.json +++ b/apps/login/locales/en.json @@ -8,10 +8,22 @@ "addAnother": "Add another account", "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": { "title": "Welcome back!", "description": "Enter your login data.", - "register": "Register new user" + "register": "Register new user", + "submit": "Continue" }, "password": { "verify": { @@ -62,6 +74,10 @@ "linkingError": { "title": "Account linking failed", "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": { @@ -146,11 +162,13 @@ }, "title": "Register", "description": "Create your ZITADEL account.", + "noMethodAvailableWarning": "No authentication method available. Please contact your administrator.", "selectMethod": "Select the method you would like to authenticate", "agreeTo": "To register you must agree to the terms and conditions", "termsOfService": "Terms of Service", "privacyPolicy": "Privacy Policy", "submit": "Continue", + "orUseIDP": "or use an Identity Provider", "password": { "title": "Set Password", "description": "Set the password for your account", diff --git a/apps/login/locales/es.json b/apps/login/locales/es.json index f5a0b05801..40e3392e4a 100644 --- a/apps/login/locales/es.json +++ b/apps/login/locales/es.json @@ -8,10 +8,22 @@ "addAnother": "Agregar otra cuenta", "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": { "title": "¡Bienvenido de nuevo!", "description": "Introduce tus datos de acceso.", - "register": "Registrar nuevo usuario" + "register": "Registrar nuevo usuario", + "submit": "Continuar" }, "password": { "verify": { @@ -62,6 +74,10 @@ "linkingError": { "title": "Error al vincular la 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": { @@ -139,11 +155,13 @@ }, "title": "Registrarse", "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", "agreeTo": "Para registrarte debes aceptar los términos y condiciones", "termsOfService": "Términos de Servicio", "privacyPolicy": "Política de Privacidad", "submit": "Continuar", + "orUseIDP": "o usa un Proveedor de Identidad", "password": { "title": "Establecer Contraseña", "description": "Establece la contraseña para tu cuenta", diff --git a/apps/login/locales/it.json b/apps/login/locales/it.json index b12d0e978d..6f562f0974 100644 --- a/apps/login/locales/it.json +++ b/apps/login/locales/it.json @@ -8,10 +8,22 @@ "addAnother": "Aggiungi un altro account", "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": { "title": "Bentornato!", "description": "Inserisci i tuoi dati di accesso.", - "register": "Registrati come nuovo utente" + "register": "Registrati come nuovo utente", + "submit": "Continua" }, "password": { "verify": { @@ -62,6 +74,10 @@ "linkingError": { "title": "Collegamento account fallito", "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": { @@ -139,11 +155,13 @@ }, "title": "Registrati", "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", "agreeTo": "Per registrarti devi accettare i termini e le condizioni", "termsOfService": "Termini di Servizio", "privacyPolicy": "Informativa sulla Privacy", "submit": "Continua", + "orUseIDP": "o usa un Identity Provider", "password": { "title": "Imposta Password", "description": "Imposta la password per il tuo account", diff --git a/apps/login/locales/pl.json b/apps/login/locales/pl.json index 6ce67bada9..58c77b1d09 100644 --- a/apps/login/locales/pl.json +++ b/apps/login/locales/pl.json @@ -8,10 +8,22 @@ "addAnother": "Dodaj kolejne konto", "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": { "title": "Witamy ponownie!", "description": "Wprowadź dane logowania.", - "register": "Zarejestruj nowego użytkownika" + "register": "Zarejestruj nowego użytkownika", + "submit": "Kontynuuj" }, "password": { "verify": { @@ -62,6 +74,10 @@ "linkingError": { "title": "Powiązanie konta nie powiodło się", "description": "Wystąpił błąd podczas próby powiązania konta." + }, + "completeRegister": { + "title": "Ukończ rejestrację", + "description": "Ukończ rejestrację swojego konta." } }, "mfa": { @@ -139,11 +155,13 @@ }, "title": "Rejestracja", "description": "Utwórz konto ZITADEL.", + "noMethodAvailableWarning": "Brak dostępnych metod uwierzytelniania. Skontaktuj się z administratorem.", "selectMethod": "Wybierz metodę uwierzytelniania, której chcesz użyć", "agreeTo": "Aby się zarejestrować, musisz zaakceptować warunki korzystania", "termsOfService": "Regulamin", "privacyPolicy": "Polityka prywatności", "submit": "Kontynuuj", + "orUseIDP": "lub użyj dostawcy tożsamości", "password": { "title": "Ustaw hasło", "description": "Ustaw hasło dla swojego konta", diff --git a/apps/login/locales/ru.json b/apps/login/locales/ru.json index 77d0314199..b73f9b99c6 100644 --- a/apps/login/locales/ru.json +++ b/apps/login/locales/ru.json @@ -8,10 +8,22 @@ "addAnother": "Добавить другой аккаунт", "noResults": "Аккаунты не найдены" }, + "logout": { + "title": "Выход", + "description": "Выберите аккаунт, который хотите удалить", + "noResults": "Аккаунты не найдены", + "clear": "Удалить сессию", + "verifiedAt": "Последняя активность: {time}", + "success": { + "title": "Выход выполнен успешно", + "description": "Вы успешно вышли из системы." + } + }, "loginname": { "title": "С возвращением!", "description": "Введите свои данные для входа.", - "register": "Зарегистрировать нового пользователя" + "register": "Зарегистрировать нового пользователя", + "submit": "Продолжить" }, "password": { "verify": { @@ -62,6 +74,10 @@ "linkingError": { "title": "Ошибка привязки аккаунта", "description": "Произошла ошибка при попытке привязать аккаунт." + }, + "completeRegister": { + "title": "Завершите регистрацию", + "description": "Завершите регистрацию вашего аккаунта." } }, "mfa": { @@ -139,11 +155,13 @@ }, "title": "Регистрация", "description": "Создайте свой аккаунт ZITADEL.", + "noMethodAvailableWarning": "Нет доступных методов аутентификации. Обратитесь к администратору.", "selectMethod": "Выберите метод аутентификации", "agreeTo": "Для регистрации необходимо принять условия:", "termsOfService": "Условия использования", "privacyPolicy": "Политика конфиденциальности", "submit": "Продолжить", + "orUseIDP": "или используйте Identity Provider", "password": { "title": "Установить пароль", "description": "Установите пароль для вашего аккаунта", diff --git a/apps/login/locales/zh.json b/apps/login/locales/zh.json index e03a9fd62d..1023660882 100644 --- a/apps/login/locales/zh.json +++ b/apps/login/locales/zh.json @@ -8,10 +8,22 @@ "addAnother": "添加另一个账户", "noResults": "未找到账户" }, + "logout": { + "title": "注销", + "description": "选择您想注销的账户", + "noResults": "未找到账户", + "clear": "注销会话", + "verifiedAt": "最后活动时间:{time}", + "success": { + "title": "注销成功", + "description": "您已成功注销。" + } + }, "loginname": { "title": "欢迎回来!", "description": "请输入您的登录信息。", - "register": "注册新用户" + "register": "注册新用户", + "submit": "继续" }, "password": { "verify": { @@ -62,6 +74,10 @@ "linkingError": { "title": "账户链接失败", "description": "链接账户时发生错误。" + }, + "completeRegister": { + "title": "完成注册", + "description": "完成您的账户注册。" } }, "mfa": { @@ -139,11 +155,13 @@ }, "title": "注册", "description": "创建您的 ZITADEL 账户。", + "noMethodAvailableWarning": "没有可用的认证方法。请联系您的系统管理员。", "selectMethod": "选择您想使用的认证方法", "agreeTo": "注册即表示您同意条款和条件", "termsOfService": "服务条款", "privacyPolicy": "隐私政策", "submit": "继续", + "orUseIDP": "或使用身份提供者", "password": { "title": "设置密码", "description": "为您的账户设置密码", diff --git a/apps/login/next.config.mjs b/apps/login/next.config.mjs index edf5e54595..dea34d603b 100755 --- a/apps/login/next.config.mjs +++ b/apps/login/next.config.mjs @@ -26,8 +26,6 @@ const secureHeaders = [ key: "X-XSS-Protection", 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", value: `${DEFAULT_CSP} frame-ancestors 'none'`, diff --git a/apps/login/package.json b/apps/login/package.json index b8afa2007f..bc52864e4d 100644 --- a/apps/login/package.json +++ b/apps/login/package.json @@ -3,7 +3,7 @@ "private": true, "type": "module", "scripts": { - "dev": "next dev --turbopack", + "dev": "next dev", "test": "concurrently --timings --kill-others-on-fail 'npm:test:unit' 'npm:test:integration'", "test:watch": "concurrently --kill-others 'npm:test:unit:watch' 'npm:test:integration:watch'", "test:unit": "vitest", @@ -45,10 +45,9 @@ "clsx": "1.2.1", "copy-to-clipboard": "^3.3.3", "deepmerge": "^4.3.1", - "jose": "^5.3.0", "lucide-react": "0.469.0", "moment": "^2.29.4", - "next": "15.4.0-canary.3", + "next": "15.4.0-canary.86", "next-intl": "^3.25.1", "next-themes": "^0.2.1", "nice-grpc": "2.0.1", @@ -56,7 +55,6 @@ "react": "19.1.0", "react-dom": "19.1.0", "react-hook-form": "7.39.5", - "swr": "^2.2.0", "tinycolor2": "1.4.2", "uuid": "^11.1.0" }, diff --git a/apps/login/src/app/(login)/accounts/page.tsx b/apps/login/src/app/(login)/accounts/page.tsx index 32c88d17bf..a1e99401e2 100644 --- a/apps/login/src/app/(login)/accounts/page.tsx +++ b/apps/login/src/app/(login)/accounts/page.tsx @@ -1,5 +1,6 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { SessionsList } from "@/components/sessions-list"; +import { Translated } from "@/components/translated"; import { getAllSessionCookieIds } from "@/lib/cookies"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { @@ -9,7 +10,7 @@ import { } from "@/lib/zitadel"; import { UserPlusIcon } from "@heroicons/react/24/outline"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; -import { getLocale, getTranslations } from "next-intl/server"; +import { getLocale } from "next-intl/server"; import { headers } from "next/headers"; import Link from "next/link"; @@ -33,7 +34,6 @@ export default async function Page(props: { }) { const searchParams = await props.searchParams; const locale = getLocale(); - const t = await getTranslations({ locale, namespace: "accounts" }); const requestId = searchParams?.requestId; const organization = searchParams?.organization; @@ -71,8 +71,12 @@ export default async function Page(props: { return (
-

{t("title")}

-

{t("description")}

+

+ +

+

+ +

@@ -81,7 +85,9 @@ export default async function Page(props: {
- {t("addAnother")} + + +
diff --git a/apps/login/src/app/(login)/authenticator/set/page.tsx b/apps/login/src/app/(login)/authenticator/set/page.tsx index 82b494f200..a339426c89 100644 --- a/apps/login/src/app/(login)/authenticator/set/page.tsx +++ b/apps/login/src/app/(login)/authenticator/set/page.tsx @@ -3,6 +3,7 @@ import { BackButton } from "@/components/back-button"; import { ChooseAuthenticatorToSetup } from "@/components/choose-authenticator-to-setup"; import { DynamicTheme } from "@/components/dynamic-theme"; import { SignInWithIdp } from "@/components/sign-in-with-idp"; +import { Translated } from "@/components/translated"; import { UserAvatar } from "@/components/user-avatar"; import { getSessionCookieById } from "@/lib/cookies"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; @@ -17,7 +18,7 @@ import { listAuthenticationMethodTypes, } from "@/lib/zitadel"; 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 { redirect } from "next/navigation"; @@ -26,8 +27,6 @@ export default async function Page(props: { }) { const searchParams = await props.searchParams; const locale = getLocale(); - const t = await getTranslations({ locale, namespace: "authenticator" }); - const tError = await getTranslations({ locale, namespace: "error" }); const { loginName, requestId, organization, sessionId } = searchParams; @@ -99,7 +98,11 @@ export default async function Page(props: { !sessionWithData.factors || !sessionWithData.factors.user ) { - return {tError("unknownContext")}; + return ( + + + + ); } const branding = await getBrandingSettings({ @@ -165,9 +168,13 @@ export default async function Page(props: { return (
-

{t("title")}

+

+ +

-

{t("description")}

+

+ +

- {identityProviders.length && ( -
-

{t("linkWithIDP")}

-
- )} +
+

+ +

+
+ >; }) { const searchParams = await props.searchParams; - const locale = getLocale(); - const t = await getTranslations({ locale }); const userCode = searchParams?.user_code; const requestId = searchParams?.requestId; const organization = searchParams?.organization; if (!userCode || !requestId) { - return
{t("error.noUserCode")}
; + return ( +
+ +
+ ); } const _headers = await headers(); @@ -34,7 +36,11 @@ export default async function Page(props: { }); if (!deviceAuthorizationRequest) { - return
{t("error.noDeviceRequest")}
; + return ( +
+ +
+ ); } let defaultOrganization; @@ -66,15 +72,19 @@ export default async function Page(props: {

- {t("device.request.title", { - appName: deviceAuthorizationRequest?.appName, - })} +

- {t("device.request.description", { - appName: deviceAuthorizationRequest?.appName, - })} +

>; }) { const searchParams = await props.searchParams; - const locale = getLocale(); - const t = await getTranslations({ locale, namespace: "device" }); const userCode = searchParams?.user_code; const organization = searchParams?.organization; @@ -37,8 +35,12 @@ export default async function Page(props: { return (
-

{t("usercode.title")}

-

{t("usercode.description")}

+

+ +

+

+ +

diff --git a/apps/login/src/app/(login)/error.tsx b/apps/login/src/app/(login)/error.tsx index bee6516a59..d14150a4b4 100644 --- a/apps/login/src/app/(login)/error.tsx +++ b/apps/login/src/app/(login)/error.tsx @@ -2,7 +2,7 @@ import { Boundary } from "@/components/boundary"; import { Button } from "@/components/button"; -import { useTranslations } from "next-intl"; +import { Translated } from "@/components/translated"; import { useEffect } from "react"; export default function Error({ error, reset }: any) { @@ -10,8 +10,6 @@ export default function Error({ error, reset }: any) { console.log("logging error:", error); }, [error]); - const t = useTranslations("error"); - return (
@@ -19,7 +17,9 @@ export default function Error({ error, reset }: any) { Error: {error?.message}
- +
diff --git a/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx b/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx index de6ad858d9..f2b7a19b91 100644 --- a/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx @@ -1,6 +1,7 @@ import { Alert, AlertType } from "@/components/alert"; import { ChooseAuthenticatorToLogin } from "@/components/choose-authenticator-to-login"; import { DynamicTheme } from "@/components/dynamic-theme"; +import { Translated } from "@/components/translated"; import { UserAvatar } from "@/components/user-avatar"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { @@ -11,7 +12,6 @@ import { } from "@/lib/zitadel"; import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; -import { getLocale, getTranslations } from "next-intl/server"; import { headers } from "next/headers"; export default async function Page(props: { @@ -19,8 +19,6 @@ export default async function Page(props: { params: Promise<{ provider: string }>; }) { const searchParams = await props.searchParams; - const locale = getLocale(); - const t = await getTranslations({ locale, namespace: "idp" }); const { organization, userId } = searchParams; @@ -77,8 +75,12 @@ export default async function Page(props: { return (
-

{t("loginError.title")}

- {t("loginError.description")} +

+ +

+ + + {userId && authMethods.length && ( <> diff --git a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx index 2c9724f13a..ae9feff6b7 100644 --- a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx @@ -1,51 +1,98 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { IdpSignin } from "@/components/idp-signin"; +import { completeIDP } from "@/components/idps/pages/complete-idp"; import { linkingFailed } from "@/components/idps/pages/linking-failed"; import { linkingSuccess } from "@/components/idps/pages/linking-success"; import { loginFailed } from "@/components/idps/pages/login-failed"; import { loginSuccess } from "@/components/idps/pages/login-success"; +import { Translated } from "@/components/translated"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { addHuman, addIDPLink, getBrandingSettings, + getDefaultOrg, getIDPByID, getLoginSettings, getOrgsByDomain, listUsers, retrieveIDPIntent, + updateHuman, } from "@/lib/zitadel"; import { ConnectError, create } from "@zitadel/client"; import { AutoLinkingOption } from "@zitadel/proto/zitadel/idp/v2/idp_pb"; import { OrganizationSchema } from "@zitadel/proto/zitadel/object/v2/object_pb"; +import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; import { AddHumanUserRequest, AddHumanUserRequestSchema, + UpdateHumanUserRequestSchema, } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; -import { getLocale, getTranslations } from "next-intl/server"; import { headers } from "next/headers"; const ORG_SUFFIX_REGEX = /(?<=@)(.+)/; +async function resolveOrganizationForUser({ + organization, + addHumanUser, + serviceUrl, +}: { + organization?: string; + addHumanUser?: { username?: string }; + serviceUrl: string; +}): Promise { + 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: { searchParams: Promise>; params: Promise<{ provider: string }>; }) { const params = await props.params; const searchParams = await props.searchParams; - const locale = getLocale(); - const t = await getTranslations({ locale, namespace: "idp" }); - const { id, token, requestId, organization, link } = searchParams; + let { id, token, requestId, organization, link } = searchParams; const { provider } = params; const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); - const branding = await getBrandingSettings({ + let branding = await getBrandingSettings({ serviceUrl, organization, }); + if (!organization) { + const org: Organization | null = await getDefaultOrg({ + serviceUrl, + }); + if (org) { + organization = org.id; + } + } + if (!provider || !id || !token) { return loginFailed(branding, "IDP context missing"); } @@ -59,18 +106,6 @@ export default async function Page(props: { const { idpInformation, userId } = 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) { return loginFailed(branding, "IDP information missing"); } @@ -79,12 +114,41 @@ export default async function Page(props: { serviceUrl, id: idpInformation.idpId, }); + const options = idp?.config?.options; if (!idp) { 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 (!options?.isLinkingAllowed) { // 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 - if (options?.isLinkingAllowed) { + if (options?.autoLinking) { let foundUser; const email = addHumanUser?.email?.email; @@ -176,88 +240,99 @@ 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 (options?.isAutoCreation && addHumanUser) { + const orgToRegisterOn = await resolveOrganizationForUser({ + organization, + addHumanUser, + serviceUrl, + }); - if ( - !orgToRegisterOn && - addHumanUser?.username && // username or email? - ORG_SUFFIX_REGEX.test(addHumanUser.username) - ) { - 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, - domain: suffix, + let addHumanUserWithOrganization: AddHumanUserRequest; + if (orgToRegisterOn) { + const organizationSchema = create(OrganizationSchema, { + org: { case: "orgId", value: orgToRegisterOn }, }); - const orgToCheckForDiscovery = - orgs.result && orgs.result.length === 1 ? orgs.result[0].id : undefined; - const orgLoginSettings = await getLoginSettings({ - serviceUrl, - organization: orgToCheckForDiscovery, + addHumanUserWithOrganization = create(AddHumanUserRequestSchema, { + ...addHumanUser, + organization: organizationSchema, }); - if (orgLoginSettings?.allowDomainDiscovery) { - orgToRegisterOn = orgToCheckForDiscovery; - } - } - - if (addHumanUser) { - let addHumanUserWithOrganization: AddHumanUserRequest; - if (orgToRegisterOn) { - const organizationSchema = create(OrganizationSchema, { - org: { case: "orgId", value: orgToRegisterOn }, - }); - - addHumanUserWithOrganization = create(AddHumanUserRequestSchema, { - ...addHumanUser, - organization: organizationSchema, - }); - } else { - addHumanUserWithOrganization = create( - AddHumanUserRequestSchema, - addHumanUser, - ); - } - - try { - newUser = await addHuman({ - serviceUrl, - request: addHumanUserWithOrganization, - }); - } catch (error: unknown) { - console.error( - "An error occurred while creating the user:", - error, - addHumanUser, - ); - return loginFailed( - branding, - (error as ConnectError).message - ? (error as ConnectError).message - : "Could not create user", - ); - } - } - - if (newUser) { - return ( - -
-

{t("registerSuccess.title")}

-

{t("registerSuccess.description")}

- -
-
+ } else { + addHumanUserWithOrganization = create( + AddHumanUserRequestSchema, + addHumanUser, ); } + + try { + newUser = await addHuman({ + serviceUrl, + request: addHumanUserWithOrganization, + }); + } catch (error: unknown) { + console.error( + "An error occurred while creating the user:", + error, + addHumanUser, + ); + return loginFailed( + branding, + (error as ConnectError).message + ? (error as ConnectError).message + : "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) { + return ( + +
+

+ +

+

+ +

+ +
+
+ ); } // return login failed if no linking or creation is allowed and no user was found diff --git a/apps/login/src/app/(login)/idp/page.tsx b/apps/login/src/app/(login)/idp/page.tsx index db492cb79c..ab16e897e5 100644 --- a/apps/login/src/app/(login)/idp/page.tsx +++ b/apps/login/src/app/(login)/idp/page.tsx @@ -1,16 +1,14 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { SignInWithIdp } from "@/components/sign-in-with-idp"; +import { Translated } from "@/components/translated"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getActiveIdentityProviders, getBrandingSettings } from "@/lib/zitadel"; -import { getLocale, getTranslations } from "next-intl/server"; import { headers } from "next/headers"; export default async function Page(props: { searchParams: Promise>; }) { const searchParams = await props.searchParams; - const locale = getLocale(); - const t = await getTranslations({ locale, namespace: "idp" }); const requestId = searchParams?.requestId; const organization = searchParams?.organization; @@ -33,8 +31,12 @@ export default async function Page(props: { return (
-

{t("title")}

-

{t("description")}

+

+ +

+

+ +

{identityProviders && ( >; }) { const searchParams = await props.searchParams; - const locale = getLocale(); - const t = await getTranslations({ locale, namespace: "loginname" }); const loginName = searchParams?.loginName; const requestId = searchParams?.requestId; @@ -63,8 +61,12 @@ export default async function Page(props: { return (
-

{t("title")}

-

{t("description")}

+

+ +

+

+ +

- {identityProviders && ( + {identityProviders && loginSettings?.allowExternalIdp && (
!!id) as string[], + }); + return response?.sessions ?? []; + } else { + console.info("No session cookie found."); + return []; + } +} + +export default async function Page(props: { + searchParams: Promise>; +}) { + 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 ( + +
+

+ +

+

+ +

+ +
+ +
+
+
+ ); +} diff --git a/apps/login/src/app/(login)/logout/success/page.tsx b/apps/login/src/app/(login)/logout/success/page.tsx new file mode 100644 index 0000000000..e7ec459f03 --- /dev/null +++ b/apps/login/src/app/(login)/logout/success/page.tsx @@ -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 }) { + 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 ( + +
+

+ +

+

+ +

+
+
+ ); +} diff --git a/apps/login/src/app/(login)/mfa/page.tsx b/apps/login/src/app/(login)/mfa/page.tsx index c65d6d3058..5543cdf66f 100644 --- a/apps/login/src/app/(login)/mfa/page.tsx +++ b/apps/login/src/app/(login)/mfa/page.tsx @@ -2,6 +2,7 @@ import { Alert } from "@/components/alert"; import { BackButton } from "@/components/back-button"; import { ChooseSecondFactor } from "@/components/choose-second-factor"; import { DynamicTheme } from "@/components/dynamic-theme"; +import { Translated } from "@/components/translated"; import { UserAvatar } from "@/components/user-avatar"; import { getSessionCookieById } from "@/lib/cookies"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; @@ -11,16 +12,12 @@ import { getSession, listAuthenticationMethodTypes, } from "@/lib/zitadel"; -import { getLocale, getTranslations } from "next-intl/server"; import { headers } from "next/headers"; export default async function Page(props: { searchParams: Promise>; }) { 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; @@ -90,9 +87,13 @@ export default async function Page(props: { return (
-

{t("verify.title")}

+

+ +

-

{t("verify.description")}

+

+ +

{sessionFactors && ( )} - {!(loginName || sessionId) && {tError("unknownContext")}} + {!(loginName || sessionId) && ( + + + + )} {sessionFactors ? ( ) : ( - {t("verify.noResults")} + + + )}
diff --git a/apps/login/src/app/(login)/mfa/set/page.tsx b/apps/login/src/app/(login)/mfa/set/page.tsx index c7f2fa6599..ebfa358d6d 100644 --- a/apps/login/src/app/(login)/mfa/set/page.tsx +++ b/apps/login/src/app/(login)/mfa/set/page.tsx @@ -2,6 +2,7 @@ import { Alert } from "@/components/alert"; import { BackButton } from "@/components/back-button"; import { ChooseSecondFactorToSetup } from "@/components/choose-second-factor-to-setup"; import { DynamicTheme } from "@/components/dynamic-theme"; +import { Translated } from "@/components/translated"; import { UserAvatar } from "@/components/user-avatar"; import { getSessionCookieById } from "@/lib/cookies"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; @@ -15,7 +16,6 @@ import { } from "@/lib/zitadel"; import { Timestamp, timestampDate } from "@zitadel/client"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; -import { getLocale, getTranslations } from "next-intl/server"; import { headers } from "next/headers"; function isSessionValid(session: Partial): { @@ -38,9 +38,6 @@ export default async function Page(props: { searchParams: Promise>; }) { 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 } = searchParams; @@ -119,9 +116,13 @@ export default async function Page(props: { return (
-

{t("set.title")}

+

+ +

-

{t("set.description")}

+

+ +

{sessionWithData && ( )} - {!(loginName || sessionId) && {tError("unknownContext")}} + {!(loginName || sessionId) && ( + + + + )} - {!valid && {tError("sessionExpired")}} + {!valid && ( + + + + )} {isSessionValid(sessionWithData).valid && loginSettings && diff --git a/apps/login/src/app/(login)/otp/[method]/page.tsx b/apps/login/src/app/(login)/otp/[method]/page.tsx index ee58420c42..2d9daac64f 100644 --- a/apps/login/src/app/(login)/otp/[method]/page.tsx +++ b/apps/login/src/app/(login)/otp/[method]/page.tsx @@ -1,6 +1,7 @@ import { Alert } from "@/components/alert"; import { DynamicTheme } from "@/components/dynamic-theme"; import { LoginOTP } from "@/components/login-otp"; +import { Translated } from "@/components/translated"; import { UserAvatar } from "@/components/user-avatar"; import { getSessionCookieById } from "@/lib/cookies"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; @@ -10,7 +11,7 @@ import { getLoginSettings, getSession, } from "@/lib/zitadel"; -import { getLocale, getTranslations } from "next-intl/server"; +import { getLocale } from "next-intl/server"; import { headers } from "next/headers"; export default async function Page(props: { @@ -20,8 +21,6 @@ export default async function Page(props: { const params = await props.params; const searchParams = await props.searchParams; const locale = getLocale(); - const t = await getTranslations({ locale, namespace: "otp" }); - const tError = await getTranslations({ locale, namespace: "error" }); const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); @@ -81,20 +80,30 @@ export default async function Page(props: { return (
-

{t("verify.title")}

+

+ +

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

{t("verify.totpDescription")}

+

+ +

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

{t("verify.smsDescription")}

+

+ +

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

{t("verify.emailDescription")}

+

+ +

)} {!session && (
- {tError("unknownContext")} + + +
)} diff --git a/apps/login/src/app/(login)/otp/[method]/set/page.tsx b/apps/login/src/app/(login)/otp/[method]/set/page.tsx index d3fc4c89f7..f74093ce8e 100644 --- a/apps/login/src/app/(login)/otp/[method]/set/page.tsx +++ b/apps/login/src/app/(login)/otp/[method]/set/page.tsx @@ -3,6 +3,7 @@ import { BackButton } from "@/components/back-button"; import { Button, ButtonVariants } from "@/components/button"; import { DynamicTheme } from "@/components/dynamic-theme"; import { TotpRegister } from "@/components/totp-register"; +import { Translated } from "@/components/translated"; import { UserAvatar } from "@/components/user-avatar"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; @@ -14,7 +15,6 @@ import { registerTOTP, } from "@/lib/zitadel"; import { RegisterTOTPResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; -import { getLocale, getTranslations } from "next-intl/server"; import { headers } from "next/headers"; import Link from "next/link"; import { redirect } from "next/navigation"; @@ -25,9 +25,6 @@ export default async function Page(props: { }) { const params = await props.params; const searchParams = await props.searchParams; - const locale = getLocale(); - const t = await getTranslations({ locale, namespace: "otp" }); - const tError = await getTranslations({ locale, namespace: "error" }); const { loginName, organization, sessionId, requestId, checkAfter } = searchParams; @@ -128,10 +125,14 @@ export default async function Page(props: { return (
-

{t("set.title")}

+

+ +

{!session && (
- {tError("unknownContext")} + + +
)} @@ -152,7 +153,12 @@ export default async function Page(props: { {totpResponse && "uri" in totpResponse && "secret" in totpResponse ? ( <> -

{t("set.totpRegisterDescription")}

+

+ +

- {t("set.submit")} +
diff --git a/apps/login/src/app/(login)/passkey/page.tsx b/apps/login/src/app/(login)/passkey/page.tsx index e24585e7e0..bef71986f3 100644 --- a/apps/login/src/app/(login)/passkey/page.tsx +++ b/apps/login/src/app/(login)/passkey/page.tsx @@ -1,21 +1,18 @@ import { Alert } from "@/components/alert"; import { DynamicTheme } from "@/components/dynamic-theme"; import { LoginPasskey } from "@/components/login-passkey"; +import { Translated } from "@/components/translated"; import { UserAvatar } from "@/components/user-avatar"; import { getSessionCookieById } from "@/lib/cookies"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings, getSession } from "@/lib/zitadel"; -import { getLocale, getTranslations } from "next-intl/server"; import { headers } from "next/headers"; export default async function Page(props: { searchParams: Promise>; }) { const searchParams = await props.searchParams; - const locale = getLocale(); - const t = await getTranslations({ locale, namespace: "passkey" }); - const tError = await getTranslations({ locale, namespace: "error" }); const { loginName, altPassword, requestId, organization, sessionId } = searchParams; @@ -55,7 +52,9 @@ export default async function Page(props: { return (
-

{t("verify.title")}

+

+ +

{sessionFactors && ( )} -

{t("verify.description")}

+

+ +

- {!(loginName || sessionId) && {tError("unknownContext")}} + {!(loginName || sessionId) && ( + + + + )} {(loginName || sessionId) && ( >; }) { 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; @@ -37,7 +34,9 @@ export default async function Page(props: { return (
-

{t("set.title")}

+

+ +

{session && ( )} -

{t("set.description")}

+

+ +

- {t("set.info.description")} + - {t("set.info.link")} + {!session && (
- {tError("unknownContext")} + + +
)} diff --git a/apps/login/src/app/(login)/password/change/page.tsx b/apps/login/src/app/(login)/password/change/page.tsx index 05f8cd6a10..78ba88d282 100644 --- a/apps/login/src/app/(login)/password/change/page.tsx +++ b/apps/login/src/app/(login)/password/change/page.tsx @@ -1,6 +1,7 @@ import { Alert } from "@/components/alert"; import { ChangePasswordForm } from "@/components/change-password-form"; import { DynamicTheme } from "@/components/dynamic-theme"; +import { Translated } from "@/components/translated"; import { UserAvatar } from "@/components/user-avatar"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; @@ -9,7 +10,6 @@ import { getLoginSettings, getPasswordComplexitySettings, } from "@/lib/zitadel"; -import { getLocale, getTranslations } from "next-intl/server"; import { headers } from "next/headers"; export default async function Page(props: { @@ -19,9 +19,6 @@ export default async function Page(props: { const { serviceUrl } = getServiceUrlFromHeaders(_headers); 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; @@ -53,15 +50,21 @@ export default async function Page(props: {

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

-

{t("change.description")}

+

+ +

{/* show error only if usernames should be shown to be unknown */} {(!sessionFactors || !loginName) && !loginSettings?.ignoreUnknownUsernames && (
- {tError("unknownContext")} + + +
)} @@ -86,7 +89,9 @@ export default async function Page(props: { /> ) : (
- {tError("failedLoading")} + + +
)}
diff --git a/apps/login/src/app/(login)/password/page.tsx b/apps/login/src/app/(login)/password/page.tsx index 29a49c61f2..461c095157 100644 --- a/apps/login/src/app/(login)/password/page.tsx +++ b/apps/login/src/app/(login)/password/page.tsx @@ -1,6 +1,7 @@ import { Alert } from "@/components/alert"; import { DynamicTheme } from "@/components/dynamic-theme"; import { PasswordForm } from "@/components/password-form"; +import { Translated } from "@/components/translated"; import { UserAvatar } from "@/components/user-avatar"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; @@ -10,18 +11,13 @@ import { getLoginSettings, } from "@/lib/zitadel"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; -import { getLocale, getTranslations } from "next-intl/server"; import { headers } from "next/headers"; export default async function Page(props: { searchParams: Promise>; }) { const searchParams = await props.searchParams; - const locale = getLocale(); - const t = await getTranslations({ locale, namespace: "password" }); - const tError = await getTranslations({ locale, namespace: "error" }); - - let { loginName, organization, requestId } = searchParams; + let { loginName, organization, requestId, alt } = searchParams; const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); @@ -65,15 +61,21 @@ export default async function Page(props: {

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

-

{t("verify.description")}

+

+ +

{/* show error only if usernames should be shown to be unknown */} {(!sessionFactors || !loginName) && !loginSettings?.ignoreUnknownUsernames && (
- {tError("unknownContext")} + + +
)} diff --git a/apps/login/src/app/(login)/password/set/page.tsx b/apps/login/src/app/(login)/password/set/page.tsx index 26e065438c..b717fd5d96 100644 --- a/apps/login/src/app/(login)/password/set/page.tsx +++ b/apps/login/src/app/(login)/password/set/page.tsx @@ -1,6 +1,7 @@ import { Alert, AlertType } from "@/components/alert"; import { DynamicTheme } from "@/components/dynamic-theme"; import { SetPasswordForm } from "@/components/set-password-form"; +import { Translated } from "@/components/translated"; import { UserAvatar } from "@/components/user-avatar"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; @@ -12,7 +13,7 @@ import { } from "@/lib/zitadel"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb"; -import { getLocale, getTranslations } from "next-intl/server"; +import { getLocale } from "next-intl/server"; import { headers } from "next/headers"; export default async function Page(props: { @@ -20,8 +21,6 @@ export default async function Page(props: { }) { const searchParams = await props.searchParams; const locale = getLocale(); - const t = await getTranslations({ locale, namespace: "password" }); - const tError = await getTranslations({ locale, namespace: "error" }); const { userId, loginName, organization, requestId, code, initial } = searchParams; @@ -73,13 +72,21 @@ export default async function Page(props: { return (
-

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

-

{t("set.description")}

+

+ {session?.factors?.user?.displayName ?? ( + + )} +

+

+ +

{/* show error only if usernames should be shown to be unknown */} {loginName && !session && !loginSettings?.ignoreUnknownUsernames && (
- {tError("unknownContext")} + + +
)} @@ -99,7 +106,11 @@ export default async function Page(props: { > ) : null} - {!initial && {t("set.codeSent")}} + {!initial && ( + + + + )} {passwordComplexity && (loginName ?? user?.preferredLoginName) && @@ -115,7 +126,9 @@ export default async function Page(props: { /> ) : (
- {tError("failedLoading")} + + +
)}
diff --git a/apps/login/src/app/(login)/register/page.tsx b/apps/login/src/app/(login)/register/page.tsx index e50511edb1..aa83ad1ead 100644 --- a/apps/login/src/app/(login)/register/page.tsx +++ b/apps/login/src/app/(login)/register/page.tsx @@ -1,7 +1,11 @@ +import { Alert } from "@/components/alert"; import { DynamicTheme } from "@/components/dynamic-theme"; 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 { + getActiveIdentityProviders, getBrandingSettings, getDefaultOrg, getLegalAndSupportSettings, @@ -9,7 +13,8 @@ import { getPasswordComplexitySettings, } from "@/lib/zitadel"; 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"; export default async function Page(props: { @@ -17,7 +22,6 @@ export default async function Page(props: { }) { const searchParams = await props.searchParams; const locale = getLocale(); - const t = await getTranslations({ locale, namespace: "register" }); let { firstname, lastname, email, organization, requestId } = searchParams; @@ -52,12 +56,25 @@ export default async function Page(props: { 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) { return (
-

{t("disabled.title")}

-

{t("disabled.description")}

+

+ +

+

+ +

); @@ -66,19 +83,52 @@ export default async function Page(props: { return (
-

{t("title")}

-

{t("description")}

+

+ +

+

+ +

- {legal && passwordComplexitySettings && ( - + {!organization && ( + + + + )} + + {legal && + passwordComplexitySettings && + organization && + (loginSettings.allowUsernamePassword || + loginSettings.passkeysType == PasskeysType.ALLOWED) && ( + + )} + + {loginSettings?.allowExternalIdp && !!identityProviders.length && ( + <> +
+

+ +

+
+ + + )}
diff --git a/apps/login/src/app/(login)/register/password/page.tsx b/apps/login/src/app/(login)/register/password/page.tsx index ee6fa03e59..e9689f0f5e 100644 --- a/apps/login/src/app/(login)/register/password/page.tsx +++ b/apps/login/src/app/(login)/register/password/page.tsx @@ -1,5 +1,6 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { SetRegisterPasswordForm } from "@/components/set-register-password-form"; +import { Translated } from "@/components/translated"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getBrandingSettings, @@ -9,15 +10,12 @@ import { getPasswordComplexitySettings, } from "@/lib/zitadel"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; -import { getLocale, getTranslations } from "next-intl/server"; import { headers } from "next/headers"; export default async function Page(props: { searchParams: Promise>; }) { const searchParams = await props.searchParams; - const locale = getLocale(); - const t = await getTranslations({ locale, namespace: "register" }); let { firstname, lastname, email, organization, 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({ serviceUrl, @@ -57,15 +55,23 @@ export default async function Page(props: { return missingData ? (
-

{t("missingdata.title")}

-

{t("missingdata.description")}

+

+ +

+

+ +

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

{t("password.title")}

-

{t("description")}

+

+ +

+

+ +

{legal && passwordComplexitySettings && ( )} @@ -82,8 +88,12 @@ export default async function Page(props: { ) : (
-

{t("disabled.title")}

-

{t("disabled.description")}

+

+ +

+

+ +

); diff --git a/apps/login/src/app/(login)/signedin/page.tsx b/apps/login/src/app/(login)/signedin/page.tsx index dd19096244..5b2ed5fbf4 100644 --- a/apps/login/src/app/(login)/signedin/page.tsx +++ b/apps/login/src/app/(login)/signedin/page.tsx @@ -1,6 +1,7 @@ import { Alert, AlertType } from "@/components/alert"; import { Button, ButtonVariants } from "@/components/button"; import { DynamicTheme } from "@/components/dynamic-theme"; +import { Translated } from "@/components/translated"; import { UserAvatar } from "@/components/user-avatar"; import { getMostRecentCookieWithLoginname, @@ -14,7 +15,6 @@ import { getLoginSettings, getSession, } from "@/lib/zitadel"; -import { getLocale, getTranslations } from "next-intl/server"; import { headers } from "next/headers"; import Link from "next/link"; @@ -37,8 +37,6 @@ async function loadSessionById( export default async function Page(props: { searchParams: Promise }) { const searchParams = await props.searchParams; - const locale = getLocale(); - const t = await getTranslations({ locale, namespace: "signedin" }); const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); @@ -66,8 +64,12 @@ export default async function Page(props: { searchParams: Promise }) { return (
-

{t("error.title")}

-

{t("error.description")}

+

+ +

+

+ +

{err.message}
@@ -94,9 +96,15 @@ export default async function Page(props: { searchParams: Promise }) {

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

-

{t("description")}

+

+ +

}) { className="self-end" variant={ButtonVariants.Primary} > - {t("continue")} +
diff --git a/apps/login/src/app/(login)/u2f/page.tsx b/apps/login/src/app/(login)/u2f/page.tsx index b16dc88f4b..7fba7be1be 100644 --- a/apps/login/src/app/(login)/u2f/page.tsx +++ b/apps/login/src/app/(login)/u2f/page.tsx @@ -1,12 +1,13 @@ import { Alert } from "@/components/alert"; import { DynamicTheme } from "@/components/dynamic-theme"; import { LoginPasskey } from "@/components/login-passkey"; +import { Translated } from "@/components/translated"; import { UserAvatar } from "@/components/user-avatar"; import { getSessionCookieById } from "@/lib/cookies"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings, getSession } from "@/lib/zitadel"; -import { getLocale, getTranslations } from "next-intl/server"; +import { getLocale } from "next-intl/server"; import { headers } from "next/headers"; export default async function Page(props: { @@ -14,8 +15,6 @@ export default async function Page(props: { }) { const searchParams = await props.searchParams; const locale = getLocale(); - const t = await getTranslations({ locale, namespace: "u2f" }); - const tError = await getTranslations({ locale, namespace: "error" }); const { loginName, requestId, sessionId, organization } = searchParams; @@ -59,7 +58,9 @@ export default async function Page(props: { return (
-

{t("verify.title")}

+

+ +

{sessionFactors && ( )} -

{t("verify.description")}

+

+ +

- {!(loginName || sessionId) && {tError("unknownContext")}} + {!(loginName || sessionId) && ( + + + + )} {(loginName || sessionId) && (
-

{t("set.title")}

+

+ +

{sessionFactors && ( )} -

{t("set.description")}

+

+ {" "} + +

{!sessionFactors && (
- {tError("unknownContext")} + + +
)} diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index 7634ff063a..a61d4e608c 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -1,5 +1,6 @@ import { Alert, AlertType } from "@/components/alert"; import { DynamicTheme } from "@/components/dynamic-theme"; +import { Translated } from "@/components/translated"; import { UserAvatar } from "@/components/user-avatar"; import { VerifyForm } from "@/components/verify-form"; import { sendEmailCode, sendInviteEmailCode } from "@/lib/server/verify"; @@ -7,14 +8,12 @@ import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings, getUserByID } from "@/lib/zitadel"; 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"; export default async function Page(props: { searchParams: Promise }) { const searchParams = await props.searchParams; const locale = getLocale(); - const t = await getTranslations({ locale, namespace: "verify" }); - const tError = await getTranslations({ locale, namespace: "error" }); const { userId, loginName, code, organization, requestId, invite, send } = searchParams; @@ -121,23 +120,26 @@ export default async function Page(props: { searchParams: Promise }) { return (
-

{t("verify.title")}

-

{t("verify.description")}

+

+ +

+

+ +

{!id && ( - <> -

{t("verify.title")}

-

{t("verify.description")}

- -
- {tError("unknownContext")} -
- +
+ + + +
)} {id && send && (
- {t("verify.codeSent")} + + +
)} diff --git a/apps/login/src/app/(login)/verify/success/page.tsx b/apps/login/src/app/(login)/verify/success/page.tsx index 678687a7f6..a0df0327c4 100644 --- a/apps/login/src/app/(login)/verify/success/page.tsx +++ b/apps/login/src/app/(login)/verify/success/page.tsx @@ -1,39 +1,18 @@ import { DynamicTheme } from "@/components/dynamic-theme"; +import { Translated } from "@/components/translated"; import { UserAvatar } from "@/components/user-avatar"; -import { getSessionCookieById } from "@/lib/cookies"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings, getLoginSettings, - getSession, getUserByID, } from "@/lib/zitadel"; import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb"; -import { getLocale, getTranslations } from "next-intl/server"; 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 }) { const searchParams = await props.searchParams; - const locale = getLocale(); - const t = await getTranslations({ locale, namespace: "verify" }); const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); @@ -84,8 +63,12 @@ export default async function Page(props: { searchParams: Promise }) { return (
-

{t("successTitle")}

-

{t("successDescription")}

+

+ +

+

+ +

{sessionFactors ? ( void; }) { - const t = useTranslations("error"); - return ( // global-error must include html and body tags @@ -25,7 +23,9 @@ export default function GlobalError({ Error: {error?.message}
- +
diff --git a/apps/login/src/components/authentication-method-radio.tsx b/apps/login/src/components/authentication-method-radio.tsx index 1b2af2d167..c3b273ab46 100644 --- a/apps/login/src/components/authentication-method-radio.tsx +++ b/apps/login/src/components/authentication-method-radio.tsx @@ -1,7 +1,7 @@ "use client"; import { RadioGroup } from "@headlessui/react"; -import { useTranslations } from "next-intl"; +import { Translated } from "./translated"; export enum AuthenticationMethod { Passkey = "passkey", @@ -20,8 +20,6 @@ export function AuthenticationMethodRadio({ selected: any; selectionChanged: (value: any) => void; }) { - const t = useTranslations("register"); - return (
@@ -80,7 +78,18 @@ export function AuthenticationMethodRadio({ as="p" className={`font-medium ${checked ? "" : ""}`} > - {t(`methods.${method}`)} + {method === AuthenticationMethod.Passkey && ( + + )} + {method === AuthenticationMethod.Password && ( + + )}
diff --git a/apps/login/src/components/back-button.tsx b/apps/login/src/components/back-button.tsx index fe348af9c4..31d4a880ad 100644 --- a/apps/login/src/components/back-button.tsx +++ b/apps/login/src/components/back-button.tsx @@ -1,11 +1,10 @@ "use client"; -import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; import { Button, ButtonVariants } from "./button"; +import { Translated } from "./translated"; export function BackButton() { - const t = useTranslations("common"); const router = useRouter(); return ( ); } diff --git a/apps/login/src/components/change-password-form.tsx b/apps/login/src/components/change-password-form.tsx index 54aab7b3ca..00513d8dda 100644 --- a/apps/login/src/components/change-password-form.tsx +++ b/apps/login/src/components/change-password-form.tsx @@ -13,7 +13,6 @@ import { import { create } from "@zitadel/client"; import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb"; -import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { FieldValues, useForm } from "react-hook-form"; @@ -23,6 +22,7 @@ import { Button, ButtonVariants } from "./button"; import { TextInput } from "./input"; import { PasswordComplexity } from "./password-complexity"; import { Spinner } from "./spinner"; +import { Translated } from "./translated"; type Inputs = | { @@ -46,7 +46,6 @@ export function ChangePasswordForm({ requestId, organization, }: Props) { - const t = useTranslations("password"); const router = useRouter(); const { register, handleSubmit, watch, formState } = useForm({ @@ -203,8 +202,8 @@ export function ChangePasswordForm({ onClick={handleSubmit(submitChange)} data-testid="submit-button" > - {loading && } - {t("change.submit")} + {loading && }{" "} +
diff --git a/apps/login/src/components/choose-authenticator-to-login.tsx b/apps/login/src/components/choose-authenticator-to-login.tsx index f7e3cdf8f8..0f5dd79134 100644 --- a/apps/login/src/components/choose-authenticator-to-login.tsx +++ b/apps/login/src/components/choose-authenticator-to-login.tsx @@ -3,8 +3,8 @@ import { PasskeysType, } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; -import { useTranslations } from "next-intl"; import { PASSKEYS, PASSWORD } from "./auth-methods"; +import { Translated } from "./translated"; type Props = { authMethods: AuthenticationMethodType[]; @@ -17,13 +17,13 @@ export function ChooseAuthenticatorToLogin({ params, loginSettings, }: Props) { - const t = useTranslations("idp"); - return ( <> {authMethods.includes(AuthenticationMethodType.PASSWORD) && loginSettings?.allowUsernamePassword && ( -
Choose an alternative method to login
+
+ +
)}
{authMethods.includes(AuthenticationMethodType.PASSWORD) && diff --git a/apps/login/src/components/choose-authenticator-to-setup.tsx b/apps/login/src/components/choose-authenticator-to-setup.tsx index 9075e3286e..4aa4de720a 100644 --- a/apps/login/src/components/choose-authenticator-to-setup.tsx +++ b/apps/login/src/components/choose-authenticator-to-setup.tsx @@ -3,9 +3,9 @@ import { PasskeysType, } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; -import { useTranslations } from "next-intl"; import { Alert, AlertType } from "./alert"; import { PASSKEYS, PASSWORD } from "./auth-methods"; +import { Translated } from "./translated"; type Props = { authMethods: AuthenticationMethodType[]; @@ -18,16 +18,23 @@ export function ChooseAuthenticatorToSetup({ params, loginSettings, }: Props) { - const t = useTranslations("authenticator"); - if (authMethods.length !== 0) { - return {t("allSetup")}; + return ( + + + + ); } else { return ( <> {loginSettings.passkeysType == PasskeysType.NOT_ALLOWED && !loginSettings.allowUsernamePassword && ( - {t("noMethodsAvailable")} + + + )}
diff --git a/apps/login/src/components/choose-second-factor-to-setup.tsx b/apps/login/src/components/choose-second-factor-to-setup.tsx index e56379e147..edd0ae2b61 100644 --- a/apps/login/src/components/choose-second-factor-to-setup.tsx +++ b/apps/login/src/components/choose-second-factor-to-setup.tsx @@ -6,9 +6,9 @@ import { SecondFactorType, } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; -import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; import { EMAIL, SMS, TOTP, U2F } from "./auth-methods"; +import { Translated } from "./translated"; type Props = { userId: string; @@ -37,7 +37,6 @@ export function ChooseSecondFactorToSetup({ emailVerified, force, }: Props) { - const t = useTranslations("mfa"); const router = useRouter(); const params = new URLSearchParams({}); @@ -112,7 +111,7 @@ export function ChooseSecondFactorToSetup({ type="button" data-testid="reset-button" > - {t("set.skip")} + )} diff --git a/apps/login/src/components/consent.tsx b/apps/login/src/components/consent.tsx index d3d30b3113..e60ed2901b 100644 --- a/apps/login/src/components/consent.tsx +++ b/apps/login/src/components/consent.tsx @@ -8,6 +8,7 @@ import { useState } from "react"; import { Alert } from "./alert"; import { Button, ButtonVariants } from "./button"; import { Spinner } from "./spinner"; +import { Translated } from "./translated"; export function ConsentScreen({ scope, @@ -50,7 +51,7 @@ export function ConsentScreen({
    {scopes?.length === 0 && ( - {t("device.scope.openid")} + )} {scopes?.map((s) => { @@ -73,7 +74,11 @@ export function ConsentScreen({

- {t("device.request.disclaimer", { appName: appName })} +

{error && ( @@ -91,7 +96,7 @@ export function ConsentScreen({ data-testid="deny-button" > {loading && } - {t("device.request.deny")} + @@ -102,7 +107,7 @@ export function ConsentScreen({ className="self-end" variant={ButtonVariants.Primary} > - {t("device.request.submit")} +
diff --git a/apps/login/src/components/device-code-form.tsx b/apps/login/src/components/device-code-form.tsx index e09adb1147..a1efc07207 100644 --- a/apps/login/src/components/device-code-form.tsx +++ b/apps/login/src/components/device-code-form.tsx @@ -2,7 +2,6 @@ import { Alert } from "@/components/alert"; import { getDeviceAuthorizationRequest } from "@/lib/server/oidc"; -import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { useForm } from "react-hook-form"; @@ -10,14 +9,13 @@ import { BackButton } from "./back-button"; import { Button, ButtonVariants } from "./button"; import { TextInput } from "./input"; import { Spinner } from "./spinner"; +import { Translated } from "./translated"; type Inputs = { userCode: string; }; export function DeviceCodeForm({ userCode }: { userCode?: string }) { - const t = useTranslations("verify"); - const router = useRouter(); const { register, handleSubmit, formState } = useForm({ @@ -87,8 +85,8 @@ export function DeviceCodeForm({ userCode }: { userCode?: string }) { onClick={handleSubmit(submitCodeAndContinue)} data-testid="submit-button" > - {loading && } - {t("verify.submit")} + {loading && }{" "} +
diff --git a/apps/login/src/components/idps/pages/complete-idp.tsx b/apps/login/src/components/idps/pages/complete-idp.tsx new file mode 100644 index 0000000000..2061a28e3e --- /dev/null +++ b/apps/login/src/components/idps/pages/complete-idp.tsx @@ -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 ( + +
+

+ +

+

+ +

+ + +
+
+ ); +} diff --git a/apps/login/src/components/idps/pages/linking-failed.tsx b/apps/login/src/components/idps/pages/linking-failed.tsx index f4016a3394..0c5a8264c4 100644 --- a/apps/login/src/components/idps/pages/linking-failed.tsx +++ b/apps/login/src/components/idps/pages/linking-failed.tsx @@ -1,20 +1,21 @@ import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb"; -import { getLocale, getTranslations } from "next-intl/server"; import { Alert, AlertType } from "../../alert"; import { DynamicTheme } from "../../dynamic-theme"; +import { Translated } from "../../translated"; export async function linkingFailed( branding?: BrandingSettings, error?: string, ) { - const locale = getLocale(); - const t = await getTranslations({ locale, namespace: "idp" }); - return (
-

{t("linkingError.title")}

-

{t("linkingError.description")}

+

+ +

+

+ +

{error && (
{{error}} diff --git a/apps/login/src/components/idps/pages/linking-success.tsx b/apps/login/src/components/idps/pages/linking-success.tsx index f4faa8e1bf..8d41cd8c32 100644 --- a/apps/login/src/components/idps/pages/linking-success.tsx +++ b/apps/login/src/components/idps/pages/linking-success.tsx @@ -1,7 +1,7 @@ import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb"; -import { getLocale, getTranslations } from "next-intl/server"; import { DynamicTheme } from "../../dynamic-theme"; import { IdpSignin } from "../../idp-signin"; +import { Translated } from "../../translated"; export async function linkingSuccess( userId: string, @@ -9,14 +9,15 @@ export async function linkingSuccess( requestId?: string, branding?: BrandingSettings, ) { - const locale = getLocale(); - const t = await getTranslations({ locale, namespace: "idp" }); - return (
-

{t("linkingSuccess.title")}

-

{t("linkingSuccess.description")}

+

+ +

+

+ +

-

{t("loginError.title")}

-

{t("loginError.description")}

+

+ +

+

+ +

{error && (
{{error}} diff --git a/apps/login/src/components/idps/pages/login-success.tsx b/apps/login/src/components/idps/pages/login-success.tsx index 6c884873f1..6beec160a9 100644 --- a/apps/login/src/components/idps/pages/login-success.tsx +++ b/apps/login/src/components/idps/pages/login-success.tsx @@ -1,7 +1,7 @@ import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb"; -import { getLocale, getTranslations } from "next-intl/server"; import { DynamicTheme } from "../../dynamic-theme"; import { IdpSignin } from "../../idp-signin"; +import { Translated } from "../../translated"; export async function loginSuccess( userId: string, @@ -9,14 +9,15 @@ export async function loginSuccess( requestId?: string, branding?: BrandingSettings, ) { - const locale = getLocale(); - const t = await getTranslations({ locale, namespace: "idp" }); - return (
-

{t("loginSuccess.title")}

-

{t("loginSuccess.description")}

+

+ +

+

+ +

(function SignInWithApple(props, ref) { const { children, name, ...restProps } = props; - const t = useTranslations("idp"); return ( @@ -24,7 +23,13 @@ export const SignInWithApple = forwardRef< {children ? ( children ) : ( - {name ? name : t("signInWithApple")} + + {name ? ( + name + ) : ( + + )} + )} ); diff --git a/apps/login/src/components/idps/sign-in-with-azure-ad.tsx b/apps/login/src/components/idps/sign-in-with-azure-ad.tsx index a3a4c82272..3cd33708b6 100644 --- a/apps/login/src/components/idps/sign-in-with-azure-ad.tsx +++ b/apps/login/src/components/idps/sign-in-with-azure-ad.tsx @@ -1,7 +1,7 @@ "use client"; -import { useTranslations } from "next-intl"; import { forwardRef } from "react"; +import { Translated } from "../translated"; import { BaseButton, SignInWithIdentityProviderProps } from "./base-button"; export const SignInWithAzureAd = forwardRef< @@ -9,7 +9,6 @@ export const SignInWithAzureAd = forwardRef< SignInWithIdentityProviderProps >(function SignInWithAzureAd(props, ref) { const { children, name, ...restProps } = props; - const t = useTranslations("idp"); return ( @@ -30,7 +29,13 @@ export const SignInWithAzureAd = forwardRef< {children ? ( children ) : ( - {name ? name : t("signInWithAzureAD")} + + {name ? ( + name + ) : ( + + )} + )} ); diff --git a/apps/login/src/components/idps/sign-in-with-github.tsx b/apps/login/src/components/idps/sign-in-with-github.tsx index 45108d17f7..8800e66c3d 100644 --- a/apps/login/src/components/idps/sign-in-with-github.tsx +++ b/apps/login/src/components/idps/sign-in-with-github.tsx @@ -1,7 +1,7 @@ "use client"; -import { useTranslations } from "next-intl"; import { forwardRef } from "react"; +import { Translated } from "../translated"; import { BaseButton, SignInWithIdentityProviderProps } from "./base-button"; function GitHubLogo() { @@ -42,7 +42,6 @@ export const SignInWithGithub = forwardRef< SignInWithIdentityProviderProps >(function SignInWithGithub(props, ref) { const { children, name, ...restProps } = props; - const t = useTranslations("idp"); return ( @@ -52,7 +51,13 @@ export const SignInWithGithub = forwardRef< {children ? ( children ) : ( - {name ? name : t("signInWithGithub")} + + {name ? ( + name + ) : ( + + )} + )} ); diff --git a/apps/login/src/components/idps/sign-in-with-gitlab.tsx b/apps/login/src/components/idps/sign-in-with-gitlab.tsx index 8a1ed7d349..00f3712a90 100644 --- a/apps/login/src/components/idps/sign-in-with-gitlab.tsx +++ b/apps/login/src/components/idps/sign-in-with-gitlab.tsx @@ -1,7 +1,7 @@ "use client"; -import { useTranslations } from "next-intl"; import { forwardRef } from "react"; +import { Translated } from "../translated"; import { BaseButton, SignInWithIdentityProviderProps } from "./base-button"; export const SignInWithGitlab = forwardRef< @@ -9,7 +9,6 @@ export const SignInWithGitlab = forwardRef< SignInWithIdentityProviderProps >(function SignInWithGitlab(props, ref) { const { children, name, ...restProps } = props; - const t = useTranslations("idp"); return ( @@ -41,7 +40,13 @@ export const SignInWithGitlab = forwardRef< {children ? ( children ) : ( - {name ? name : t("signInWithGitlab")} + + {name ? ( + name + ) : ( + + )} + )} ); diff --git a/apps/login/src/components/idps/sign-in-with-google.tsx b/apps/login/src/components/idps/sign-in-with-google.tsx index 6162ef4a96..4759ad69c9 100644 --- a/apps/login/src/components/idps/sign-in-with-google.tsx +++ b/apps/login/src/components/idps/sign-in-with-google.tsx @@ -1,7 +1,7 @@ "use client"; -import { useTranslations } from "next-intl"; import { forwardRef } from "react"; +import { Translated } from "../translated"; import { BaseButton, SignInWithIdentityProviderProps } from "./base-button"; export const SignInWithGoogle = forwardRef< @@ -9,7 +9,6 @@ export const SignInWithGoogle = forwardRef< SignInWithIdentityProviderProps >(function SignInWithGoogle(props, ref) { const { children, name, ...restProps } = props; - const t = useTranslations("idp"); return ( @@ -54,7 +53,13 @@ export const SignInWithGoogle = forwardRef< {children ? ( children ) : ( - {name ? name : t("signInWithGoogle")} + + {name ? ( + name + ) : ( + + )} + )} ); diff --git a/apps/login/src/components/language-switcher.tsx b/apps/login/src/components/language-switcher.tsx index 8d68248c4a..67b54e58e3 100644 --- a/apps/login/src/components/language-switcher.tsx +++ b/apps/login/src/components/language-switcher.tsx @@ -43,7 +43,7 @@ export function LanguageSwitcher() { > {selected.name}
diff --git a/apps/login/src/components/login-passkey.tsx b/apps/login/src/components/login-passkey.tsx index b364a9de45..5a3b0b6496 100644 --- a/apps/login/src/components/login-passkey.tsx +++ b/apps/login/src/components/login-passkey.tsx @@ -9,13 +9,13 @@ import { UserVerificationRequirement, } from "@zitadel/proto/zitadel/session/v2/challenge_pb"; import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; -import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; import { useEffect, useRef, useState } from "react"; import { Alert } from "./alert"; import { BackButton } from "./back-button"; import { Button, ButtonVariants } from "./button"; import { Spinner } from "./spinner"; +import { Translated } from "./translated"; // either loginName or sessionId must be provided type Props = { @@ -35,8 +35,6 @@ export function LoginPasskey({ organization, login = true, }: Props) { - const t = useTranslations("passkey"); - const [error, setError] = useState(""); const [loading, setLoading] = useState(false); @@ -234,7 +232,7 @@ export function LoginPasskey({ }} data-testid="password-button" > - {t("verify.usePassword")} + ) : ( @@ -273,8 +271,8 @@ export function LoginPasskey({ }} data-testid="submit-button" > - {loading && } - {t("verify.submit")} + {loading && }{" "} +
diff --git a/apps/login/src/components/password-form.tsx b/apps/login/src/components/password-form.tsx index a9b0a01316..3cd455c69c 100644 --- a/apps/login/src/components/password-form.tsx +++ b/apps/login/src/components/password-form.tsx @@ -4,7 +4,6 @@ import { resetPassword, sendPassword } from "@/lib/server/password"; import { create } from "@zitadel/client"; import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; -import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { useForm } from "react-hook-form"; @@ -13,6 +12,7 @@ import { BackButton } from "./back-button"; import { Button, ButtonVariants } from "./button"; import { TextInput } from "./input"; import { Spinner } from "./spinner"; +import { Translated } from "./translated"; type Inputs = { password: string; @@ -31,8 +31,6 @@ export function PasswordForm({ organization, requestId, }: Props) { - const t = useTranslations("password"); - const { register, handleSubmit, formState } = useForm({ mode: "onBlur", }); @@ -132,7 +130,7 @@ export function PasswordForm({ disabled={loading} data-testid="reset-button" > - {t("verify.resetPassword")} + )} @@ -169,8 +167,8 @@ export function PasswordForm({ onClick={handleSubmit(submitPassword)} data-testid="submit-button" > - {loading && } - {t("verify.submit")} + {loading && }{" "} +
diff --git a/apps/login/src/components/privacy-policy-checkboxes.tsx b/apps/login/src/components/privacy-policy-checkboxes.tsx index 5ac0340bcb..4ab0e33222 100644 --- a/apps/login/src/components/privacy-policy-checkboxes.tsx +++ b/apps/login/src/components/privacy-policy-checkboxes.tsx @@ -1,9 +1,9 @@ "use client"; import { LegalAndSupportSettings } from "@zitadel/proto/zitadel/settings/v2/legal_settings_pb"; -import { useTranslations } from "next-intl"; import Link from "next/link"; import { useState } from "react"; import { Checkbox } from "./checkbox"; +import { Translated } from "./translated"; type Props = { legal: LegalAndSupportSettings; @@ -16,7 +16,6 @@ type AcceptanceState = { }; export function PrivacyPolicyCheckboxes({ legal, onChange }: Props) { - const t = useTranslations("register"); const [acceptanceState, setAcceptanceState] = useState({ tosAccepted: false, privacyPolicyAccepted: false, @@ -25,7 +24,7 @@ export function PrivacyPolicyCheckboxes({ legal, onChange }: Props) { return ( <>

- {t("agreeTo")} + {legal?.helpLink && ( @@ -66,7 +65,7 @@ export function PrivacyPolicyCheckboxes({ legal, onChange }: Props) {

- {t("termsOfService")} +

@@ -95,7 +94,7 @@ export function PrivacyPolicyCheckboxes({ legal, onChange }: Props) { className="underline" target="_blank" > - {t("privacyPolicy")} +

diff --git a/apps/login/src/components/invite-form.tsx b/apps/login/src/components/register-form-idp-incomplete.tsx similarity index 62% rename from apps/login/src/components/invite-form.tsx rename to apps/login/src/components/register-form-idp-incomplete.tsx index 35c0bec028..b8a7765c9c 100644 --- a/apps/login/src/components/invite-form.tsx +++ b/apps/login/src/components/register-form-idp-incomplete.tsx @@ -1,7 +1,6 @@ "use client"; -import { inviteUser } from "@/lib/server/invite"; -import { useTranslations } from "next-intl"; +import { registerUserAndLinkToIDP } from "@/lib/server/register"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { FieldValues, useForm } from "react-hook-form"; @@ -10,6 +9,7 @@ import { BackButton } from "./back-button"; import { Button, ButtonVariants } from "./button"; import { TextInput } from "./input"; import { Spinner } from "./spinner"; +import { Translated } from "./translated"; type Inputs = | { @@ -20,26 +20,37 @@ type Inputs = | FieldValues; type Props = { - firstname?: string; - lastname?: string; - email?: string; - organization?: string; + organization: string; + requestId?: string; + idpIntent: { + idpIntentId: string; + idpIntentToken: string; + }; + defaultValues?: { + firstname?: string; + lastname?: string; + email?: string; + }; + idpUserId: string; + idpId: string; + idpUserName: string; }; -export function InviteForm({ - email, - firstname, - lastname, +export function RegisterFormIDPIncomplete({ organization, + requestId, + idpIntent, + defaultValues, + idpUserId, + idpId, + idpUserName, }: Props) { - const t = useTranslations("register"); - const { register, handleSubmit, formState } = useForm({ mode: "onBlur", defaultValues: { - email: email ?? "", - firstName: firstname ?? "", - lastname: lastname ?? "", + email: defaultValues?.email ?? "", + firstname: defaultValues?.firstname ?? "", + lastname: defaultValues?.lastname ?? "", }, }); @@ -48,39 +59,37 @@ export function InviteForm({ const router = useRouter(); - async function submitAndContinue(values: Inputs) { + async function submitAndRegister(values: Inputs) { setLoading(true); - const response = await inviteUser({ + const response = await registerUserAndLinkToIDP({ + idpId: idpId, + idpUserName: idpUserName, + idpUserId: idpUserId, email: values.email, firstName: values.firstname, lastName: values.lastname, organization: organization, + requestId: requestId, + idpIntent: idpIntent, }) .catch(() => { - setError("Could not create invitation Code"); + setError("Could not register user"); return; }) .finally(() => { setLoading(false); }); - if (response && typeof response === "object" && "error" in response) { + if (response && "error" in response && response.error) { setError(response.error); return; } - if (!response) { - setError("Could not create invitation Code"); - return; + if (response && "redirect" in response && response.redirect) { + return router.push(response.redirect); } - const params = new URLSearchParams({}); - - if (response) { - params.append("userId", response); - } - - return router.push(`/invite/success?` + params); + return response; } const { errors } = formState; @@ -88,16 +97,6 @@ export function InviteForm({ return (
-
- -
@@ -116,6 +116,18 @@ export function InviteForm({ {...register("lastname", { required: "This field is required" })} label="Last name" error={errors.lastname?.message as string} + data-testid="lastname-text-input" + /> +
+
+
@@ -127,15 +139,16 @@ export function InviteForm({ )}
- +
diff --git a/apps/login/src/components/register-form.tsx b/apps/login/src/components/register-form.tsx index 09e3f0b89b..6217bbcbb9 100644 --- a/apps/login/src/components/register-form.tsx +++ b/apps/login/src/components/register-form.tsx @@ -6,11 +6,10 @@ import { LoginSettings, PasskeysType, } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; -import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { FieldValues, useForm } from "react-hook-form"; -import { Alert } from "./alert"; +import { Alert, AlertType } from "./alert"; import { AuthenticationMethod, AuthenticationMethodRadio, @@ -21,6 +20,7 @@ import { Button, ButtonVariants } from "./button"; import { TextInput } from "./input"; import { PrivacyPolicyCheckboxes } from "./privacy-policy-checkboxes"; import { Spinner } from "./spinner"; +import { Translated } from "./translated"; type Inputs = | { @@ -35,9 +35,10 @@ type Props = { firstname?: string; lastname?: string; email?: string; - organization?: string; + organization: string; requestId?: string; loginSettings?: LoginSettings; + idpCount: number; }; export function RegisterForm({ @@ -48,9 +49,8 @@ export function RegisterForm({ organization, requestId, loginSettings, + idpCount = 0, }: Props) { - const t = useTranslations("register"); - const { register, handleSubmit, formState } = useForm({ mode: "onBlur", defaultValues: { @@ -165,16 +165,33 @@ export function RegisterForm({ onChange={setTosAndPolicyAccepted} /> )} -

{t("selectMethod")}

{/* show chooser if both methods are allowed */} {loginSettings && loginSettings.allowUsernamePassword && loginSettings.passkeysType == PasskeysType.ALLOWED && ( -
- + <> +

+ +

+ +
+ +
+ + )} + {!loginSettings?.allowUsernamePassword && + loginSettings?.passkeysType !== PasskeysType.ALLOWED && + (!loginSettings?.allowExternalIdp || !idpCount) && ( +
+ + +
)} @@ -183,6 +200,7 @@ export function RegisterForm({ {error}
)} +
diff --git a/apps/login/src/components/register-passkey.tsx b/apps/login/src/components/register-passkey.tsx index 8687312bbc..e21e1acdbb 100644 --- a/apps/login/src/components/register-passkey.tsx +++ b/apps/login/src/components/register-passkey.tsx @@ -5,7 +5,6 @@ import { registerPasskeyLink, verifyPasskeyRegistration, } from "@/lib/server/passkeys"; -import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { useForm } from "react-hook-form"; @@ -13,6 +12,7 @@ import { Alert } from "./alert"; import { BackButton } from "./back-button"; import { Button, ButtonVariants } from "./button"; import { Spinner } from "./spinner"; +import { Translated } from "./translated"; type Inputs = {}; @@ -29,8 +29,6 @@ export function RegisterPasskey({ organization, requestId, }: Props) { - const t = useTranslations("passkey"); - const { handleSubmit, formState } = useForm({ mode: "onBlur", }); @@ -198,7 +196,7 @@ export function RegisterPasskey({ continueAndLogin(); }} > - {t("set.skip")} + ) : ( @@ -213,8 +211,8 @@ export function RegisterPasskey({ onClick={handleSubmit(submitRegisterAndContinue)} data-testid="submit-button" > - {loading && } - {t("set.submit")} + {loading && }{" "} +
diff --git a/apps/login/src/components/register-u2f.tsx b/apps/login/src/components/register-u2f.tsx index 753eae017d..e72bf1fc69 100644 --- a/apps/login/src/components/register-u2f.tsx +++ b/apps/login/src/components/register-u2f.tsx @@ -5,13 +5,13 @@ import { getNextUrl } from "@/lib/client"; import { addU2F, verifyU2F } from "@/lib/server/u2f"; import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { RegisterU2FResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; -import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { Alert } from "./alert"; import { BackButton } from "./back-button"; import { Button, ButtonVariants } from "./button"; import { Spinner } from "./spinner"; +import { Translated } from "./translated"; type Props = { loginName?: string; @@ -30,8 +30,6 @@ export function RegisterU2f({ checkAfter, loginSettings, }: Props) { - const t = useTranslations("u2f"); - const [error, setError] = useState(""); const [loading, setLoading] = useState(false); @@ -218,8 +216,8 @@ export function RegisterU2f({ onClick={submitRegisterAndContinue} data-testid="submit-button" > - {loading && } - {t("set.submit")} + {loading && }{" "} +
diff --git a/apps/login/src/components/session-clear-item.tsx b/apps/login/src/components/session-clear-item.tsx new file mode 100644 index 0000000000..81930b11b3 --- /dev/null +++ b/apps/login/src/components/session-clear-item.tsx @@ -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(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(null); + + const router = useRouter(); + + return ( + + ); +} diff --git a/apps/login/src/components/session-item.tsx b/apps/login/src/components/session-item.tsx index c99f7fceca..94e7a19da5 100644 --- a/apps/login/src/components/session-item.tsx +++ b/apps/login/src/components/session-item.tsx @@ -1,7 +1,7 @@ "use client"; 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 { Timestamp, timestampDate } from "@zitadel/client"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; @@ -43,9 +43,9 @@ export function SessionItem({ const [loading, setLoading] = useState(false); - async function clearSession(id: string) { + async function clearSessionId(id: string) { setLoading(true); - const response = await cleanupSession({ + const response = await clearSession({ sessionId: id, }) .catch((error) => { @@ -145,7 +145,7 @@ export function SessionItem({ onClick={(event) => { event.preventDefault(); event.stopPropagation(); - clearSession(session.id).then(() => { + clearSessionId(session.id).then(() => { reload(); }); }} diff --git a/apps/login/src/components/sessions-clear-list.tsx b/apps/login/src/components/sessions-clear-list.tsx new file mode 100644 index 0000000000..5989948725 --- /dev/null +++ b/apps/login/src/components/sessions-clear-list.tsx @@ -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(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 ? ( +
+ {list + .filter((session) => session?.factors?.user?.loginName) + // sort by change date descending + .sort((a, b) => { + const dateA = a.changeDate + ? timestampDate(a.changeDate).getTime() + : 0; + const dateB = b.changeDate + ? timestampDate(b.changeDate).getTime() + : 0; + return dateB - dateA; + }) + // TODO: add sorting to move invalid sessions to the bottom + .map((session, index) => { + return ( + { + setList(list.filter((s) => s.id !== session.id)); + if (postLogoutRedirectUri) { + router.push(postLogoutRedirectUri); + } + }} + key={"session-" + index} + /> + ); + })} + {list.length === 0 && ( + + + + )} +
+ ) : ( + + + + ); +} diff --git a/apps/login/src/components/sessions-list.tsx b/apps/login/src/components/sessions-list.tsx index 50f621a62d..a3a1f8ed94 100644 --- a/apps/login/src/components/sessions-list.tsx +++ b/apps/login/src/components/sessions-list.tsx @@ -2,10 +2,10 @@ import { timestampDate } from "@zitadel/client"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; -import { useTranslations } from "next-intl"; import { useState } from "react"; import { Alert } from "./alert"; import { SessionItem } from "./session-item"; +import { Translated } from "./translated"; type Props = { sessions: Session[]; @@ -13,7 +13,6 @@ type Props = { }; export function SessionsList({ sessions, requestId }: Props) { - const t = useTranslations("accounts"); const [list, setList] = useState(sessions); return sessions ? (
@@ -44,6 +43,8 @@ export function SessionsList({ sessions, requestId }: Props) { })}
) : ( - {t("noResults")} + + + ); } diff --git a/apps/login/src/components/set-password-form.tsx b/apps/login/src/components/set-password-form.tsx index 08f5c7c4ef..2c3db8dbf2 100644 --- a/apps/login/src/components/set-password-form.tsx +++ b/apps/login/src/components/set-password-form.tsx @@ -14,7 +14,6 @@ import { import { create } from "@zitadel/client"; import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb"; -import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { FieldValues, useForm } from "react-hook-form"; @@ -24,6 +23,7 @@ import { Button, ButtonVariants } from "./button"; import { TextInput } from "./input"; import { PasswordComplexity } from "./password-complexity"; import { Spinner } from "./spinner"; +import { Translated } from "./translated"; type Inputs = | { @@ -52,8 +52,6 @@ export function SetPasswordForm({ code, codeRequired, }: Props) { - const t = useTranslations("password"); - const { register, handleSubmit, watch, formState } = useForm({ mode: "onBlur", defaultValues: { @@ -195,7 +193,7 @@ export function SetPasswordForm({
- {t("set.noCodeReceived")} +
@@ -279,8 +277,8 @@ export function SetPasswordForm({ onClick={handleSubmit(submitPassword)} data-testid="submit-button" > - {loading && } - {t("set.submit")} + {loading && }{" "} +
diff --git a/apps/login/src/components/set-register-password-form.tsx b/apps/login/src/components/set-register-password-form.tsx index 3f38a408d0..7660e60753 100644 --- a/apps/login/src/components/set-register-password-form.tsx +++ b/apps/login/src/components/set-register-password-form.tsx @@ -8,7 +8,6 @@ import { } from "@/helpers/validators"; import { registerUser } from "@/lib/server/register"; import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb"; -import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { FieldValues, useForm } from "react-hook-form"; @@ -18,6 +17,7 @@ import { Button, ButtonVariants } from "./button"; import { TextInput } from "./input"; import { PasswordComplexity } from "./password-complexity"; import { Spinner } from "./spinner"; +import { Translated } from "./translated"; type Inputs = | { @@ -31,7 +31,7 @@ type Props = { email: string; firstname: string; lastname: string; - organization?: string; + organization: string; requestId?: string; }; @@ -43,8 +43,6 @@ export function SetRegisterPasswordForm({ organization, requestId, }: Props) { - const t = useTranslations("register"); - const { register, handleSubmit, watch, formState } = useForm({ mode: "onBlur", defaultValues: { @@ -163,8 +161,8 @@ export function SetRegisterPasswordForm({ onClick={handleSubmit(submitRegister)} data-testid="submit-button" > - {loading && } - {t("password.submit")} + {loading && }{" "} +
diff --git a/apps/login/src/components/sign-in-with-idp.tsx b/apps/login/src/components/sign-in-with-idp.tsx index 7811c0bff3..13b179071c 100644 --- a/apps/login/src/components/sign-in-with-idp.tsx +++ b/apps/login/src/components/sign-in-with-idp.tsx @@ -56,6 +56,7 @@ export function SignInWithIdp({ [IdentityProviderType.GITLAB_SELF_HOSTED]: SignInWithGitlab, [IdentityProviderType.SAML]: SignInWithGeneric, [IdentityProviderType.LDAP]: SignInWithGeneric, + [IdentityProviderType.JWT]: SignInWithGeneric, }; const Component = components[type]; diff --git a/apps/login/src/components/totp-register.tsx b/apps/login/src/components/totp-register.tsx index b5c81d8645..ea40fffbf0 100644 --- a/apps/login/src/components/totp-register.tsx +++ b/apps/login/src/components/totp-register.tsx @@ -3,7 +3,6 @@ import { getNextUrl } from "@/lib/client"; import { verifyTOTP } from "@/lib/server/verify"; import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; -import { useTranslations } from "next-intl"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { QRCodeSVG } from "qrcode.react"; @@ -14,6 +13,7 @@ import { Button, ButtonVariants } from "./button"; import { CopyToClipboard } from "./copy-to-clipboard"; import { TextInput } from "./input"; import { Spinner } from "./spinner"; +import { Translated } from "./translated"; type Inputs = { code: string; @@ -39,8 +39,6 @@ export function TotpRegister({ checkAfter, loginSettings, }: Props) { - const t = useTranslations("otp"); - const [error, setError] = useState(""); const [loading, setLoading] = useState(false); const router = useRouter(); @@ -148,7 +146,7 @@ export function TotpRegister({ data-testid="submit-button" > {loading && } - {t("set.submit")} +
diff --git a/apps/login/src/components/translated.tsx b/apps/login/src/components/translated.tsx new file mode 100644 index 0000000000..807ea18e8f --- /dev/null +++ b/apps/login/src/components/translated.tsx @@ -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) { + const t = useTranslations(namespace); + const helperKey = `${namespace ? `${namespace}.` : ""}${i18nKey}`; + + return ( + + {t(i18nKey, data)} + + ); +} diff --git a/apps/login/src/components/username-form.tsx b/apps/login/src/components/username-form.tsx index a239da3528..1dffade4b5 100644 --- a/apps/login/src/components/username-form.tsx +++ b/apps/login/src/components/username-form.tsx @@ -2,7 +2,6 @@ import { sendLoginname } from "@/lib/server/loginname"; import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; -import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; import { ReactNode, useEffect, useState } from "react"; import { useForm } from "react-hook-form"; @@ -11,6 +10,7 @@ import { BackButton } from "./back-button"; import { Button, ButtonVariants } from "./button"; import { TextInput } from "./input"; import { Spinner } from "./spinner"; +import { Translated } from "./translated"; type Inputs = { loginName: string; @@ -37,7 +37,6 @@ export function UsernameForm({ allowRegister, children, }: Props) { - const t = useTranslations("loginname"); const { register, handleSubmit, formState } = useForm({ mode: "onBlur", defaultValues: { @@ -127,7 +126,7 @@ export function UsernameForm({ disabled={loading} data-testid="register-button" > - {t("register")} + )}
@@ -149,7 +148,7 @@ export function UsernameForm({ onClick={handleSubmit((e) => submitLoginName(e, organization))} > {loading && } - continue +
diff --git a/apps/login/src/components/verify-form.tsx b/apps/login/src/components/verify-form.tsx index 0933f598dd..dac4c91314 100644 --- a/apps/login/src/components/verify-form.tsx +++ b/apps/login/src/components/verify-form.tsx @@ -2,7 +2,6 @@ import { Alert, AlertType } from "@/components/alert"; import { resendVerification, sendVerification } from "@/lib/server/verify"; -import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useState } from "react"; import { useForm } from "react-hook-form"; @@ -10,6 +9,7 @@ import { BackButton } from "./back-button"; import { Button, ButtonVariants } from "./button"; import { TextInput } from "./input"; import { Spinner } from "./spinner"; +import { Translated } from "./translated"; type Inputs = { code: string; @@ -32,8 +32,6 @@ export function VerifyForm({ code, isInvite, }: Props) { - const t = useTranslations("verify"); - const router = useRouter(); const { register, handleSubmit, formState } = useForm({ @@ -117,7 +115,7 @@ export function VerifyForm({
- {t("verify.noCodeReceived")} +
@@ -161,7 +159,7 @@ export function VerifyForm({ data-testid="submit-button" > {loading && } - {t("verify.submit")} +
diff --git a/apps/login/src/i18n/request.ts b/apps/login/src/i18n/request.ts index 59c9da42cc..271d370f7c 100644 --- a/apps/login/src/i18n/request.ts +++ b/apps/login/src/i18n/request.ts @@ -1,4 +1,7 @@ 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 { getRequestConfig } from "next-intl/server"; import { cookies, headers } from "next/headers"; @@ -9,6 +12,26 @@ export default getRequestConfig(async () => { 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); if (languageHeader) { 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`)) .default; return { locale, - messages: deepmerge(fallbackMessages, userMessages), + messages: deepmerge.all([fallbackMessages, localeMessages, customMessages]), }; }); diff --git a/apps/login/src/lib/cookies.ts b/apps/login/src/lib/cookies.ts index 28393f1321..7de87a98e7 100644 --- a/apps/login/src/lib/cookies.ts +++ b/apps/login/src/lib/cookies.ts @@ -141,7 +141,7 @@ export async function removeSessionFromCookie({ session: SessionCookie; cleanup?: boolean; sameSite?: boolean | "lax" | "strict" | "none" | undefined; -}): Promise { +}) { const cookiesList = await cookies(); const stringifiedCookie = cookiesList.get("sessions"); diff --git a/apps/login/src/lib/idp.ts b/apps/login/src/lib/idp.ts index 66b3dfa594..d355f9ab56 100644 --- a/apps/login/src/lib/idp.ts +++ b/apps/login/src/lib/idp.ts @@ -26,6 +26,8 @@ export function idpTypeToSlug(idpType: IdentityProviderType) { return "oidc"; case IdentityProviderType.LDAP: return "ldap"; + case IdentityProviderType.JWT: + return "jwt"; default: throw new Error("Unknown identity provider type"); } @@ -66,6 +68,9 @@ export function idpTypeToIdentityProviderType( case IDPType.IDP_TYPE_OIDC: return IdentityProviderType.OIDC; + case IDPType.IDP_TYPE_JWT: + return IdentityProviderType.JWT; + default: throw new Error("Unknown identity provider type"); } diff --git a/apps/login/src/lib/server/cookie.ts b/apps/login/src/lib/server/cookie.ts index 88e0b48290..841fc06b3a 100644 --- a/apps/login/src/lib/server/cookie.ts +++ b/apps/login/src/lib/server/cookie.ts @@ -109,15 +109,20 @@ export async function createSessionAndUpdateCookie(command: { } } -export async function createSessionForIdpAndUpdateCookie( - userId: string, +export async function createSessionForIdpAndUpdateCookie({ + userId, + idpIntent, + requestId, + lifetime, +}: { + userId: string; idpIntent: { idpIntentId?: string | undefined; idpIntentToken?: string | undefined; - }, - requestId: string | undefined, - lifetime?: Duration, -): Promise { + }; + requestId: string | undefined; + lifetime?: Duration; +}): Promise { const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); diff --git a/apps/login/src/lib/server/idp.ts b/apps/login/src/lib/server/idp.ts index 367824331f..30ee0aa6fb 100644 --- a/apps/login/src/lib/server/idp.ts +++ b/apps/login/src/lib/server/idp.ts @@ -137,12 +137,12 @@ export async function createNewSessionFromIdpIntent( organization: userResponse.user.details?.resourceOwner, }); - const session = await createSessionForIdpAndUpdateCookie( - command.userId, - command.idpIntent, - command.requestId, - loginSettings?.externalLoginCheckLifetime, - ); + const session = await createSessionForIdpAndUpdateCookie({ + userId: command.userId, + idpIntent: command.idpIntent, + requestId: command.requestId, + lifetime: loginSettings?.externalLoginCheckLifetime, + }); if (!session || !session.factors?.user) { return { error: "Could not create session" }; diff --git a/apps/login/src/lib/server/invite.ts b/apps/login/src/lib/server/invite.ts deleted file mode 100644 index c0fc63fef5..0000000000 --- a/apps/login/src/lib/server/invite.ts +++ /dev/null @@ -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; -} diff --git a/apps/login/src/lib/server/register.ts b/apps/login/src/lib/server/register.ts index 25bea33527..f84b4c8d51 100644 --- a/apps/login/src/lib/server/register.ts +++ b/apps/login/src/lib/server/register.ts @@ -1,7 +1,15 @@ "use server"; -import { createSessionAndUpdateCookie } from "@/lib/server/cookie"; -import { addHumanUser, getLoginSettings, getUserByID } from "@/lib/zitadel"; +import { + createSessionAndUpdateCookie, + createSessionForIdpAndUpdateCookie, +} from "@/lib/server/cookie"; +import { + addHumanUser, + addIDPLink, + getLoginSettings, + getUserByID, +} from "@/lib/zitadel"; import { create } from "@zitadel/client"; import { Factors } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { @@ -18,7 +26,7 @@ type RegisterUserCommand = { firstName: string; lastName: string; password?: string; - organization?: string; + organization: string; requestId?: string; }; @@ -133,3 +141,93 @@ export async function registerUser(command: RegisterUserCommand) { 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 }; +} diff --git a/apps/login/src/lib/server/session.ts b/apps/login/src/lib/server/session.ts index 0535a27c35..2aceb3a1d0 100644 --- a/apps/login/src/lib/server/session.ts +++ b/apps/login/src/lib/server/session.ts @@ -202,30 +202,6 @@ export async function clearSession(options: ClearSessionOptions) { 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 deleteResponse = await deleteSession({ diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index 5cc4181e1f..c5f72266b9 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -1,7 +1,10 @@ import { Client, create, Duration } from "@zitadel/client"; import { makeReqCtx } from "@zitadel/client/v2"; 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 { CreateCallbackRequest, OIDCService, @@ -32,11 +35,13 @@ import { import { SendInviteCodeSchema } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { AddHumanUserRequest, + AddHumanUserRequestSchema, ResendEmailCodeRequest, ResendEmailCodeRequestSchema, SendEmailCodeRequestSchema, SetPasswordRequest, SetPasswordRequestSchema, + UpdateHumanUserRequest, UserService, VerifyPasskeyRegistrationRequest, VerifyU2FRegistrationRequest, @@ -54,6 +59,42 @@ async function cacheWrapper(callback: Promise) { return callback; } +export async function getHostedLoginTranslation({ + serviceUrl, + organization, + locale, +}: { + serviceUrl: string; + organization?: string; + locale?: string; +}) { + const settingsService: Client = + 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({ serviceUrl, organization, @@ -387,8 +428,8 @@ export type AddHumanUserData = { firstName: string; lastName: string; email: string; - password: string | undefined; - organization: string | undefined; + password?: string; + organization: string; }; export async function addHumanUser({ @@ -404,23 +445,36 @@ export async function addHumanUser({ serviceUrl, ); - return userService.addHumanUser({ - email: { - email, - verification: { - case: "isVerified", - value: false, + let addHumanUserRequest: AddHumanUserRequest = create( + AddHumanUserRequestSchema, + { + email: { + email, + verification: { + case: "isVerified", + value: false, + }, }, + username: email, + profile: { givenName: firstName, familyName: lastName }, + passwordType: password + ? { case: "password", value: { password } } + : undefined, }, - username: email, - profile: { givenName: firstName, familyName: lastName }, - organization: organization - ? { org: { case: "orgId", value: organization } } - : undefined, - passwordType: password - ? { case: "password", value: { password } } - : undefined, - }); + ); + + if (organization) { + const organizationSchema = create(OrganizationSchema, { + org: { case: "orgId", value: organization }, + }); + + addHumanUserRequest = { + ...addHumanUserRequest, + organization: organizationSchema, + }; + } + + return userService.addHumanUser(addHumanUserRequest); } export async function addHuman({ @@ -438,6 +492,21 @@ export async function addHuman({ return userService.addHumanUser(request); } +export async function updateHuman({ + serviceUrl, + request, +}: { + serviceUrl: string; + request: UpdateHumanUserRequest; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.updateHumanUser(request); +} + export async function verifyTOTPRegistration({ serviceUrl, code, diff --git a/apps/login/src/middleware.ts b/apps/login/src/middleware.ts index 4d66d0ab39..fa287e2bdd 100644 --- a/apps/login/src/middleware.ts +++ b/apps/login/src/middleware.ts @@ -10,21 +10,51 @@ export const config = { "/oidc/:path*", "/idps/callback/:path*", "/saml/:path*", + "/:path*", ], }; 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 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 { serviceUrl } = getServiceUrlFromHeaders(_headers); // Call the /security route handler - // TODO check this on cloud run deployment const securityResponse = await fetch(`${request.nextUrl.origin}/security`); if (!securityResponse.ok) { @@ -32,7 +62,9 @@ export async function middleware(request: NextRequest) { "Failed to fetch security settings:", securityResponse.statusText, ); - return NextResponse.next(); // Fallback if the request fails + return NextResponse.next({ + request: { headers: requestHeaders }, + }); } const { settings: securitySettings } = await securityResponse.json(); @@ -41,13 +73,8 @@ export async function middleware(request: NextRequest) { .replace("https://", "") .replace("http://", ""); - const requestHeaders = new Headers(request.headers); - - // this is a workaround for the next.js server not forwarding the host header - // requestHeaders.set("x-zitadel-forwarded", `host="${request.nextUrl.host}"`); + // Add additional headers as before 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); const responseHeaders = new Headers(); @@ -55,7 +82,6 @@ export async function middleware(request: NextRequest) { responseHeaders.set("Access-Control-Allow-Headers", "*"); if (securitySettings?.embeddedIframe?.enabled) { - securitySettings.embeddedIframe.allowedOrigins; responseHeaders.set( "Content-Security-Policy", `${DEFAULT_CSP} frame-ancestors ${securitySettings.embeddedIframe.allowedOrigins.join(" ")};`, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 46a448c2f1..fb7a635216 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -85,7 +85,7 @@ importers: version: 0.5.7(tailwindcss@3.4.14) '@vercel/analytics': 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': specifier: workspace:* version: link:../../packages/zitadel-client @@ -101,9 +101,6 @@ importers: deepmerge: specifier: ^4.3.1 version: 4.3.1 - jose: - specifier: ^5.3.0 - version: 5.8.0 lucide-react: specifier: 0.469.0 version: 0.469.0(react@19.1.0) @@ -111,14 +108,14 @@ importers: specifier: ^2.29.4 version: 2.30.1 next: - specifier: 15.4.0-canary.3 - 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) + specifier: 15.4.0-canary.86 + 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: 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: 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: specifier: 2.0.1 version: 2.0.1 @@ -134,9 +131,6 @@ importers: react-hook-form: specifier: 7.39.5 version: 7.39.5(react@19.1.0) - swr: - specifier: ^2.2.0 - version: 2.2.5(react@19.1.0) tinycolor2: specifier: 1.4.2 version: 1.4.2 @@ -807,9 +801,6 @@ packages: '@formatjs/icu-skeleton-parser@1.8.8': resolution: {integrity: sha512-vHwK3piXwamFcx5YQdCdJxUQ1WdTl6ANclt5xba5zLGDv5Bsur7qz8AD7BevaKxITwpgDeU0u8My3AIibW9ywA==} - '@formatjs/intl-localematcher@0.5.4': - resolution: {integrity: sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==} - '@formatjs/intl-localematcher@0.5.8': resolution: {integrity: sha512-I+WDNWWJFZie+jkfkiK5Mp4hEDyRSEvmyfYadflOno/mmKJKcB17fEpEH0oJu/OWhhCJ8kJBDz2YMd/6cDl7Mg==} @@ -998,56 +989,56 @@ packages: resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} hasBin: true - '@next/env@15.4.0-canary.3': - resolution: {integrity: sha512-lu4pB2e3Z/d+B0rxEm9YuQMb57Hd96iJUBZgVlcRNemlIryr0GByu17kvN6nBk3JjbWL8h+MW90stpGzGdhbqg==} + '@next/env@15.4.0-canary.86': + resolution: {integrity: sha512-WPrEvwqHnjeLx05ncJvqizbBJJFlQGRbxzOnL/pZWKzo19auM9x5Se87P27+E/D/d6jJS801l+thF85lfobAZQ==} '@next/eslint-plugin-next@14.2.18': resolution: {integrity: sha512-KyYTbZ3GQwWOjX3Vi1YcQbekyGP0gdammb7pbmmi25HBUCINzDReyrzCMOJIeZisK1Q3U6DT5Rlc4nm2/pQeXA==} - '@next/swc-darwin-arm64@15.4.0-canary.3': - resolution: {integrity: sha512-w9u8IpwLb/JS7HzHLt24smP4FxIYMgciOtYNUCognO1xh1XZfqqjDIrRAXDuuYDPKrc1i2EvI24R5eDTz7EYMQ==} + '@next/swc-darwin-arm64@15.4.0-canary.86': + resolution: {integrity: sha512-1ofBmzjPkmoMdM+dXvybZ/Roq8HRo0sFzcwXk7/FJNOufuwyK+QKdSpLE7pHlPR7ZREqfEMj61ONO+gAK+zOJw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.4.0-canary.3': - resolution: {integrity: sha512-5pL1hBRw8h1XeArzWYjCDERtRFIfrMAz1Nq9m1np8FrTuHclE7xitKKfOJqqmBbO9dWtnZIfA8lZl9bdlNEUZg==} + '@next/swc-darwin-x64@15.4.0-canary.86': + resolution: {integrity: sha512-WCKSrllvwzYi4TgrSdgxKSOF2nhieeaWWOeGucn0OXy50uOAamr0HwP5OaIBCx3oRar4w66gvs4IrdTdMedeJA==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.4.0-canary.3': - resolution: {integrity: sha512-vx6cU4jKoecF2QZw3CQqJrzb+D0WhNzHHoWUN8O+YKPnX0oG4wEtAQWSWisxKjNrU1U4TiraOql0nOQBUOKwaQ==} + '@next/swc-linux-arm64-gnu@15.4.0-canary.86': + resolution: {integrity: sha512-8qn7DJVNFjhEIDo2ts0YCsO7g+vJjPWh8Ur8lBK3XspeX0BPsF4s+YmgidrpzRXeIfoo2uYLkkXcy/57CVDblw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.4.0-canary.3': - resolution: {integrity: sha512-7ig1sQHRRgTrj4QHt5l8OT1z2SJnEAHbnEY9SDP2HilwQIfgOAOxveFDBR+f/8AMdAKhCTSeMyrZsivpC0xTUA==} + '@next/swc-linux-arm64-musl@15.4.0-canary.86': + resolution: {integrity: sha512-8MTn6N4Ja25neMLu2Bra1lqW9AWPqsYg0BVs5M/cxL0QkcN3mak/8LLX1vbzz7GigMGSA+NLwg+ol8lglfgIGA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.4.0-canary.3': - resolution: {integrity: sha512-fML6pzNX9i3DlrCOdE6A1TbVL0aIQkIDDCjrbn/f37hOn88god1OrVd/d4J4w1YqLKQWpmJPnUn6Bkn8qXqbRw==} + '@next/swc-linux-x64-gnu@15.4.0-canary.86': + resolution: {integrity: sha512-hIhzDwWDQHnH0M0Pzaqs1c5fa4+LHiLLEBuPJQvhBxQfH+Eh86DWiWHDCaoNiURvdRPg6uCuF2MjwptrMplEkg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.4.0-canary.3': - resolution: {integrity: sha512-87/JPkbr3fgvASdWW2qBVuaXwcjSxgy+CTllj2DgYB7e7BEzT7QJEdj0HJZljBjVbN5oT1FOKwhaVRgRWuwYLQ==} + '@next/swc-linux-x64-musl@15.4.0-canary.86': + resolution: {integrity: sha512-FG6SBuSeRWYMNu6tsfaZ4iDzv3BLxlpRncO2xvKKQPeUdDSQ0cehuHYnx8fRte8IOAJ3rlbRd6NXvrDarqu92Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.4.0-canary.3': - resolution: {integrity: sha512-cTZh72h3ZX8z0lhdVs5m38uyy83mW5r0jz6hKagysPT06uTdOAypK6CRqG5CJSN7RM0n7CkfcO6ExjDqhkDhRA==} + '@next/swc-win32-arm64-msvc@15.4.0-canary.86': + resolution: {integrity: sha512-3HvZo4VuyINrNYplRhvC8ILdKwi/vFDHOcTN/I4ru039TFpu2eO6VtXsLBdOdJjGslSSSBYkX+6yRrghihAZDA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.4.0-canary.3': - resolution: {integrity: sha512-8oZKOKRGad4EVZ94L5Sz2EP59khHIeKGKg+/z8r5mCbBtupLPTXmWjrXoi1R55hHRXJjbW2D5NwcPfJn/ltZ3Q==} + '@next/swc-win32-x64-msvc@15.4.0-canary.86': + resolution: {integrity: sha512-UO9JzGGj7GhtSJFdI0Bl0dkIIBfgbhXLsgNVmq9Z/CsUsQB6J9RS/BMhsxfVwhO+RETk13nFpNutMAhAwcuD8w==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1824,10 +1815,6 @@ packages: peerDependencies: esbuild: '>=0.18' - busboy@1.6.0: - resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} - engines: {node: '>=10.16.0'} - cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -1856,9 +1843,6 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - caniuse-lite@1.0.30001680: - resolution: {integrity: sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==} - caniuse-lite@1.0.30001715: resolution: {integrity: sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==} @@ -2159,10 +2143,6 @@ packages: engines: {node: '>=0.10'} hasBin: true - detect-libc@2.0.3: - resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} - engines: {node: '>=8'} - detect-libc@2.0.4: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} @@ -3389,11 +3369,11 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} - next-intl@3.25.1: - resolution: {integrity: sha512-Z2dJWn5f/b1sb8EmuJcuDhbQTIp4RG1KBFAILgRt/y27W0ifU7Ll/os3liphUY4InyRH89uShTAk7ItAlpr0uA==} + next-intl@3.26.5: + resolution: {integrity: sha512-EQlCIfY0jOhRldiFxwSXG+ImwkQtDEfQeSOEQp6ieAGSLWGlgjdb/Ck/O7wMfC430ZHGeUKVKax8KGusTPKCgg==} peerDependencies: next: ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 + 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: resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==} @@ -3402,13 +3382,13 @@ packages: react: '*' react-dom: '*' - next@15.4.0-canary.3: - resolution: {integrity: sha512-OkwxAFNQeuE0vNL7tTwU+jm3nf3x3D5DHSmjRlFktsedGtxZiILZTq6UNExNaFBjttR+2Y6oGqRsFWXC4ob1Wg==} + next@15.4.0-canary.86: + resolution: {integrity: sha512-lGeO0sOvPZ7oFIklqRA863YzRL1bW+kT/OqU3N6RBquHldiucZwnZKQceZdn6WcHEFmWIHzZV+SMG1JEK7hZLg==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 - '@playwright/test': ^1.41.2 + '@playwright/test': ^1.51.1 babel-plugin-react-compiler: '*' 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 @@ -4170,10 +4150,6 @@ packages: stream-combiner@0.0.4: 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: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -4276,11 +4252,6 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 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: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -4560,15 +4531,10 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - use-intl@3.25.1: - resolution: {integrity: sha512-Xeyl0+BjlBf6fJr2h5W/CESZ2IQAH7jzXYK4c/ao+qR26jNPW3FXBLjg7eLRxdeI6QaLcYGLtH3WYhC9I0+6Yg==} + use-intl@3.26.5: + resolution: {integrity: sha512-OdsJnC/znPvHCHLQH/duvQNXnP1w0hPfS+tkSi3mAbfjYBGh4JnyfdwkQBfIVf7t8gs9eSX/CntxUMvtKdG2MQ==} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 - - use-sync-external-store@1.2.2: - resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0 util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -5350,10 +5316,6 @@ snapshots: '@formatjs/ecma402-abstract': 2.2.4 tslib: 2.8.1 - '@formatjs/intl-localematcher@0.5.4': - dependencies: - tslib: 2.8.1 - '@formatjs/intl-localematcher@0.5.8': dependencies: tslib: 2.8.1 @@ -5538,34 +5500,34 @@ snapshots: - encoding - supports-color - '@next/env@15.4.0-canary.3': {} + '@next/env@15.4.0-canary.86': {} '@next/eslint-plugin-next@14.2.18': dependencies: glob: 10.3.10 - '@next/swc-darwin-arm64@15.4.0-canary.3': + '@next/swc-darwin-arm64@15.4.0-canary.86': optional: true - '@next/swc-darwin-x64@15.4.0-canary.3': + '@next/swc-darwin-x64@15.4.0-canary.86': optional: true - '@next/swc-linux-arm64-gnu@15.4.0-canary.3': + '@next/swc-linux-arm64-gnu@15.4.0-canary.86': optional: true - '@next/swc-linux-arm64-musl@15.4.0-canary.3': + '@next/swc-linux-arm64-musl@15.4.0-canary.86': optional: true - '@next/swc-linux-x64-gnu@15.4.0-canary.3': + '@next/swc-linux-x64-gnu@15.4.0-canary.86': optional: true - '@next/swc-linux-x64-musl@15.4.0-canary.3': + '@next/swc-linux-x64-musl@15.4.0-canary.86': optional: true - '@next/swc-win32-arm64-msvc@15.4.0-canary.3': + '@next/swc-win32-arm64-msvc@15.4.0-canary.86': optional: true - '@next/swc-win32-x64-msvc@15.4.0-canary.3': + '@next/swc-win32-x64-msvc@15.4.0-canary.86': optional: true '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': @@ -6027,11 +5989,11 @@ snapshots: '@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: server-only: 0.0.1 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 '@vercel/git-hooks@1.0.0': {} @@ -6099,7 +6061,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.0 transitivePeerDependencies: - supports-color @@ -6338,10 +6300,6 @@ snapshots: esbuild: 0.25.2 load-tsconfig: 0.2.5 - busboy@1.6.0: - dependencies: - streamsearch: 1.1.0 - cac@6.7.14: {} cachedir@2.4.0: {} @@ -6368,8 +6326,6 @@ snapshots: camelcase-css@2.0.1: {} - caniuse-lite@1.0.30001680: {} - caniuse-lite@1.0.30001715: {} case-anything@2.1.13: {} @@ -6636,6 +6592,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.0: + dependencies: + ms: 2.1.3 + debug@4.4.0(supports-color@5.5.0): dependencies: ms: 2.1.3 @@ -6713,9 +6673,6 @@ snapshots: detect-libc@1.0.3: {} - detect-libc@2.0.3: - optional: true - detect-libc@2.0.4: {} didyoumean@1.2.2: {} @@ -7321,7 +7278,7 @@ snapshots: follow-redirects@1.15.9(debug@4.4.0): optionalDependencies: - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.0 for-each@0.3.3: dependencies: @@ -7581,7 +7538,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.3 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.0 transitivePeerDependencies: - supports-color @@ -7594,14 +7551,14 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.0 transitivePeerDependencies: - supports-color https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.3 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.0 transitivePeerDependencies: - supports-color @@ -7950,7 +7907,7 @@ snapshots: dependencies: chalk: 5.4.1 commander: 13.1.0 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.0 execa: 8.0.1 lilconfig: 3.1.3 listr2: 8.3.2 @@ -8134,40 +8091,38 @@ snapshots: 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: - '@formatjs/intl-localematcher': 0.5.4 + '@formatjs/intl-localematcher': 0.5.8 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 - 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: - 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-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: - '@next/env': 15.4.0-canary.3 - '@swc/counter': 0.1.3 + '@next/env': 15.4.0-canary.86 '@swc/helpers': 0.5.15 - busboy: 1.6.0 - caniuse-lite: 1.0.30001680 + caniuse-lite: 1.0.30001715 postcss: 8.4.31 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: - '@next/swc-darwin-arm64': 15.4.0-canary.3 - '@next/swc-darwin-x64': 15.4.0-canary.3 - '@next/swc-linux-arm64-gnu': 15.4.0-canary.3 - '@next/swc-linux-arm64-musl': 15.4.0-canary.3 - '@next/swc-linux-x64-gnu': 15.4.0-canary.3 - '@next/swc-linux-x64-musl': 15.4.0-canary.3 - '@next/swc-win32-arm64-msvc': 15.4.0-canary.3 - '@next/swc-win32-x64-msvc': 15.4.0-canary.3 + '@next/swc-darwin-arm64': 15.4.0-canary.86 + '@next/swc-darwin-x64': 15.4.0-canary.86 + '@next/swc-linux-arm64-gnu': 15.4.0-canary.86 + '@next/swc-linux-arm64-musl': 15.4.0-canary.86 + '@next/swc-linux-x64-gnu': 15.4.0-canary.86 + '@next/swc-linux-x64-musl': 15.4.0-canary.86 + '@next/swc-win32-arm64-msvc': 15.4.0-canary.86 + '@next/swc-win32-x64-msvc': 15.4.0-canary.86 '@playwright/test': 1.52.0 sass: 1.87.0 sharp: 0.34.1 @@ -8737,7 +8692,7 @@ snapshots: sharp@0.34.1: dependencies: color: 4.2.3 - detect-libc: 2.0.3 + detect-libc: 2.0.4 semver: 7.7.1 optionalDependencies: '@img/sharp-darwin-arm64': 0.34.1 @@ -8882,7 +8837,7 @@ snapshots: arg: 5.0.2 bluebird: 3.7.2 check-more-types: 2.24.0 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.0 execa: 5.1.1 lazy-ass: 1.6.0 ps-tree: 1.2.0 @@ -8900,8 +8855,6 @@ snapshots: dependencies: duplexer: 0.1.2 - streamsearch@1.1.0: {} - string-argv@0.3.2: {} string-width@4.2.3: @@ -8990,12 +8943,10 @@ snapshots: 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: client-only: 0.0.1 react: 19.1.0 - optionalDependencies: - '@babel/core': 7.26.10 sucrase@3.35.0: dependencies: @@ -9021,12 +8972,6 @@ snapshots: 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: {} tabbable@6.2.0: {} @@ -9309,16 +9254,12 @@ snapshots: dependencies: punycode: 2.3.1 - use-intl@3.25.1(react@19.1.0): + use-intl@3.26.5(react@19.1.0): dependencies: '@formatjs/fast-memoize': 2.2.3 intl-messageformat: 10.7.7 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: {} uuid@11.1.0: {}