From a3e1d6a3ffe2f2c356cdfcfaee6cdda3e9ec026a Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 22 Jul 2025 11:18:15 +0200 Subject: [PATCH] fix(login): default lifetime, show expiration on accounts page (#10297) This PR fixes an issue where the password lifetime was not applied correctly in certain scenarios. It also improves the sessions page by providing more information about expiration and verification timestamps and a mobile layout for clearing sessions. Screenshot 2025-07-22 at 08 56 14 Closes https://github.com/zitadel/typescript/issues/481 --- login/apps/login/locales/de.json | 4 +- login/apps/login/locales/en.json | 4 +- login/apps/login/locales/es.json | 6 +- login/apps/login/locales/it.json | 6 +- login/apps/login/locales/pl.json | 4 +- login/apps/login/locales/ru.json | 4 +- login/apps/login/locales/zh.json | 6 +- login/apps/login/package.json | 1 + .../login/src/app/(login)/accounts/page.tsx | 6 +- login/apps/login/src/app/(login)/layout.tsx | 55 ++--- .../login/src/app/(login)/logout/page.tsx | 6 +- .../src/app/(login)/otp/[method]/page.tsx | 8 +- .../src/app/(login)/otp/[method]/set/page.tsx | 3 +- .../login/src/components/session-item.tsx | 182 +++++++++------- login/apps/login/src/lib/cookies.ts | 2 +- login/apps/login/src/lib/server/cookie.ts | 66 ++++-- login/apps/login/src/lib/server/otp.ts | 83 -------- login/apps/login/src/lib/server/passkeys.ts | 18 +- login/apps/login/src/lib/server/password.ts | 34 ++- login/apps/login/src/lib/server/session.ts | 16 +- login/apps/login/src/lib/zitadel.ts | 6 +- login/apps/login/turbo.json | 3 + pnpm-lock.yaml | 201 ++++++++++++++++++ 23 files changed, 467 insertions(+), 257 deletions(-) delete mode 100644 login/apps/login/src/lib/server/otp.ts diff --git a/login/apps/login/locales/de.json b/login/apps/login/locales/de.json index 75897a628e..7d3173bb94 100644 --- a/login/apps/login/locales/de.json +++ b/login/apps/login/locales/de.json @@ -6,7 +6,9 @@ "title": "Konten", "description": "Wählen Sie das Konto aus, das Sie verwenden möchten.", "addAnother": "Ein weiteres Konto hinzufügen", - "noResults": "Keine Konten gefunden" + "noResults": "Keine Konten gefunden", + "verified": "verifiziert", + "expired": "abgelaufen" }, "logout": { "title": "Logout", diff --git a/login/apps/login/locales/en.json b/login/apps/login/locales/en.json index 9f95403063..eeecdd023f 100644 --- a/login/apps/login/locales/en.json +++ b/login/apps/login/locales/en.json @@ -6,7 +6,9 @@ "title": "Accounts", "description": "Select the account you want to use.", "addAnother": "Add another account", - "noResults": "No accounts found" + "noResults": "No accounts found", + "verified": "verified", + "expired": "expired" }, "logout": { "title": "Logout", diff --git a/login/apps/login/locales/es.json b/login/apps/login/locales/es.json index fe88bb94c6..558dbd03b5 100644 --- a/login/apps/login/locales/es.json +++ b/login/apps/login/locales/es.json @@ -4,9 +4,11 @@ }, "accounts": { "title": "Cuentas", - "description": "Selecciona la cuenta que deseas usar.", + "description": "Seleccione la cuenta que desea utilizar.", "addAnother": "Agregar otra cuenta", - "noResults": "No se encontraron cuentas" + "noResults": "No se encontraron cuentas", + "verified": "verificado", + "expired": "expirado" }, "logout": { "title": "Cerrar sesión", diff --git a/login/apps/login/locales/it.json b/login/apps/login/locales/it.json index 1229a1a4c0..cc5a0b8c4f 100644 --- a/login/apps/login/locales/it.json +++ b/login/apps/login/locales/it.json @@ -4,9 +4,11 @@ }, "accounts": { "title": "Account", - "description": "Seleziona l'account che desideri utilizzare.", + "description": "Seleziona l'account che vuoi utilizzare.", "addAnother": "Aggiungi un altro account", - "noResults": "Nessun account trovato" + "noResults": "Nessun account trovato", + "verified": "verificato", + "expired": "scaduto" }, "logout": { "title": "Esci", diff --git a/login/apps/login/locales/pl.json b/login/apps/login/locales/pl.json index 9fea6a19fa..cd418d5c8c 100644 --- a/login/apps/login/locales/pl.json +++ b/login/apps/login/locales/pl.json @@ -6,7 +6,9 @@ "title": "Konta", "description": "Wybierz konto, którego chcesz użyć.", "addAnother": "Dodaj kolejne konto", - "noResults": "Nie znaleziono kont" + "noResults": "Nie znaleziono kont", + "verified": "zweryfikowany", + "expired": "wygasł" }, "logout": { "title": "Wyloguj się", diff --git a/login/apps/login/locales/ru.json b/login/apps/login/locales/ru.json index e745f1ae59..e5f735db94 100644 --- a/login/apps/login/locales/ru.json +++ b/login/apps/login/locales/ru.json @@ -6,7 +6,9 @@ "title": "Аккаунты", "description": "Выберите аккаунт, который хотите использовать.", "addAnother": "Добавить другой аккаунт", - "noResults": "Аккаунты не найдены" + "noResults": "Аккаунты не найдены", + "verified": "проверенный", + "expired": "истёк" }, "logout": { "title": "Выход", diff --git a/login/apps/login/locales/zh.json b/login/apps/login/locales/zh.json index 5a9cb3a4eb..73af30ab97 100644 --- a/login/apps/login/locales/zh.json +++ b/login/apps/login/locales/zh.json @@ -4,9 +4,11 @@ }, "accounts": { "title": "账户", - "description": "选择您想使用的账户。", + "description": "选择您要使用的账户。", "addAnother": "添加另一个账户", - "noResults": "未找到账户" + "noResults": "未找到账户", + "verified": "已验证", + "expired": "已过期" }, "logout": { "title": "注销", diff --git a/login/apps/login/package.json b/login/apps/login/package.json index 655aeb8f28..e45dbae7f9 100644 --- a/login/apps/login/package.json +++ b/login/apps/login/package.json @@ -25,6 +25,7 @@ "dependencies": { "@headlessui/react": "^2.1.9", "@heroicons/react": "2.1.3", + "@radix-ui/react-tooltip": "^1.2.7", "@tailwindcss/forms": "0.5.7", "@vercel/analytics": "^1.2.2", "@zitadel/client": "latest", diff --git a/login/apps/login/src/app/(login)/accounts/page.tsx b/login/apps/login/src/app/(login)/accounts/page.tsx index cebcd014bb..e4e6b387dc 100644 --- a/login/apps/login/src/app/(login)/accounts/page.tsx +++ b/login/apps/login/src/app/(login)/accounts/page.tsx @@ -15,12 +15,12 @@ import { headers } from "next/headers"; import Link from "next/link"; async function loadSessions({ serviceUrl }: { serviceUrl: string }) { - const ids: (string | undefined)[] = await getAllSessionCookieIds(); + const cookieIds = await getAllSessionCookieIds(); - if (ids && ids.length) { + if (cookieIds && cookieIds.length) { const response = await listSessions({ serviceUrl, - ids: ids.filter((id) => !!id) as string[], + ids: cookieIds.filter((id) => !!id) as string[], }); return response?.sessions ?? []; } else { diff --git a/login/apps/login/src/app/(login)/layout.tsx b/login/apps/login/src/app/(login)/layout.tsx index 0a75a920f1..dbce9804c9 100644 --- a/login/apps/login/src/app/(login)/layout.tsx +++ b/login/apps/login/src/app/(login)/layout.tsx @@ -5,6 +5,7 @@ import { LanguageSwitcher } from "@/components/language-switcher"; import { Skeleton } from "@/components/skeleton"; import { Theme } from "@/components/theme"; import { ThemeProvider } from "@/components/theme-provider"; +import * as Tooltip from "@radix-ui/react-tooltip"; import { Analytics } from "@vercel/analytics/react"; import { Lato } from "next/font/google"; import { ReactNode, Suspense } from "react"; @@ -24,36 +25,38 @@ export default async function RootLayout({ - -
- -
-
-
- + + +
+ +
+
+
+ +
-
- } - > - -
-
- {children} -
- - + } + > + +
+
+ {children} +
+ + +
-
- - + + + diff --git a/login/apps/login/src/app/(login)/logout/page.tsx b/login/apps/login/src/app/(login)/logout/page.tsx index 853461d7d6..76e4dc3800 100644 --- a/login/apps/login/src/app/(login)/logout/page.tsx +++ b/login/apps/login/src/app/(login)/logout/page.tsx @@ -12,12 +12,12 @@ import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; import { headers } from "next/headers"; async function loadSessions({ serviceUrl }: { serviceUrl: string }) { - const ids: (string | undefined)[] = await getAllSessionCookieIds(); + const cookieIds = await getAllSessionCookieIds(); - if (ids && ids.length) { + if (cookieIds && cookieIds.length) { const response = await listSessions({ serviceUrl, - ids: ids.filter((id) => !!id) as string[], + ids: cookieIds.filter((id) => !!id) as string[], }); return response?.sessions ?? []; } else { diff --git a/login/apps/login/src/app/(login)/otp/[method]/page.tsx b/login/apps/login/src/app/(login)/otp/[method]/page.tsx index af4159d62e..94164c757f 100644 --- a/login/apps/login/src/app/(login)/otp/[method]/page.tsx +++ b/login/apps/login/src/app/(login)/otp/[method]/page.tsx @@ -41,17 +41,13 @@ export default async function Page(props: { const { method } = params; const session = sessionId - ? await loadSessionById(serviceUrl, sessionId, organization) + ? await loadSessionById(sessionId, organization) : await loadMostRecentSession({ serviceUrl, sessionParams: { loginName, organization }, }); - async function loadSessionById( - host: string, - sessionId: string, - organization?: string, - ) { + async function loadSessionById(sessionId: string, organization?: string) { const recent = await getSessionCookieById({ sessionId, organization }); return getSession({ serviceUrl, diff --git a/login/apps/login/src/app/(login)/otp/[method]/set/page.tsx b/login/apps/login/src/app/(login)/otp/[method]/set/page.tsx index f74093ce8e..b43bb973f5 100644 --- a/login/apps/login/src/app/(login)/otp/[method]/set/page.tsx +++ b/login/apps/login/src/app/(login)/otp/[method]/set/page.tsx @@ -66,7 +66,6 @@ export default async function Page(props: { error = err; }); } else if (method === "sms") { - // does not work await addOTPSMS({ serviceUrl, userId: session.factors.user.id, @@ -74,7 +73,6 @@ export default async function Page(props: { error = new Error("Could not add OTP via SMS"); }); } else if (method === "email") { - // works await addOTPEmail({ serviceUrl, userId: session.factors.user.id, @@ -106,6 +104,7 @@ export default async function Page(props: { paramsToContinue.append("requestId", requestId); } urlToContinue = `/otp/${method}?` + paramsToContinue; + // immediately check the OTP on the next page if sms or email was set up if (["email", "sms"].includes(method)) { return redirect(urlToContinue); diff --git a/login/apps/login/src/components/session-item.tsx b/login/apps/login/src/components/session-item.tsx index f189d05828..7faeade9b0 100644 --- a/login/apps/login/src/components/session-item.tsx +++ b/login/apps/login/src/components/session-item.tsx @@ -3,6 +3,7 @@ import { sendLoginname } from "@/lib/server/loginname"; import { clearSession, continueWithSession } from "@/lib/server/session"; import { XCircleIcon } from "@heroicons/react/24/outline"; +import * as Tooltip from "@radix-ui/react-tooltip"; import { Timestamp, timestampDate } from "@zitadel/client"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import moment from "moment"; @@ -10,6 +11,7 @@ import { useLocale } from "next-intl"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { Avatar } from "./avatar"; +import { Translated } from "./translated"; export function isSessionValid(session: Partial): { valid: boolean; @@ -66,91 +68,107 @@ export function SessionItem({ const router = useRouter(); return ( - + className="group flex flex-row items-center rounded-md border border-divider-light bg-background-light-400 px-4 py-2 transition-all hover:shadow-lg dark:bg-background-dark-400 dark:hover:bg-white/10" + > +
+ +
+ +
+ {session.factors?.user?.displayName} + + {session.factors?.user?.loginName} + + {valid ? ( + + {" "} + {verifiedAt && moment(timestampDate(verifiedAt)).fromNow()} + + ) : ( + verifiedAt && ( + + {" "} + {session.expirationDate && + moment(timestampDate(session.expirationDate)).fromNow()} + + ) + )} +
+ + +
+ {valid ? ( +
+ ) : ( +
+ )} + + { + event.preventDefault(); + event.stopPropagation(); + clearSessionId(session.id).then(() => { + reload(); + }); + }} + /> +
+ + + {valid && session.expirationDate && ( + + + Expires {moment(timestampDate(session.expirationDate)).fromNow()} + + + + )} + ); } diff --git a/login/apps/login/src/lib/cookies.ts b/login/apps/login/src/lib/cookies.ts index 45463d7a4d..71008de24e 100644 --- a/login/apps/login/src/lib/cookies.ts +++ b/login/apps/login/src/lib/cookies.ts @@ -242,7 +242,7 @@ export async function getSessionCookieByLoginName({ */ export async function getAllSessionCookieIds( cleanup: boolean = false, -): Promise { +): Promise { const cookiesList = await cookies(); const stringifiedCookie = cookiesList.get("sessions"); diff --git a/login/apps/login/src/lib/server/cookie.ts b/login/apps/login/src/lib/server/cookie.ts index 841fc06b3a..7f87f49731 100644 --- a/login/apps/login/src/lib/server/cookie.ts +++ b/login/apps/login/src/lib/server/cookie.ts @@ -55,10 +55,21 @@ export async function createSessionAndUpdateCookie(command: { const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); + let sessionLifetime = command.lifetime; + + if (!sessionLifetime) { + console.warn("No session lifetime provided, using default of 24 hours."); + + sessionLifetime = { + seconds: BigInt(24 * 60 * 60), // 24 hours + nanos: 0, + } as Duration; // for usecases where the lifetime is not specified (user discovery) + } + const createdSession = await createSessionFromChecks({ serviceUrl, checks: command.checks, - lifetime: command.lifetime, + lifetime: sessionLifetime, }); if (createdSession) { @@ -126,11 +137,24 @@ export async function createSessionForIdpAndUpdateCookie({ const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); + let sessionLifetime = lifetime; + + if (!sessionLifetime) { + console.warn( + "No IDP session lifetime provided, using default of 24 hours.", + ); + + sessionLifetime = { + seconds: BigInt(24 * 60 * 60), // 24 hours + nanos: 0, + } as Duration; + } + const createdSession = await createSessionForUserIdAndIdpIntent({ serviceUrl, userId, idpIntent, - lifetime, + lifetime: sessionLifetime, }).catch((error: ErrorDetail | CredentialsCheckError) => { console.error("Could not set session", error); if ("failedAttempts" in error && error.failedAttempts) { @@ -190,41 +214,41 @@ export type SessionWithChallenges = Session & { challenges: Challenges | undefined; }; -export async function setSessionAndUpdateCookie( - recentCookie: CustomCookieData, - checks?: Checks, - challenges?: RequestChallenges, - requestId?: string, - lifetime?: Duration, -) { +export async function setSessionAndUpdateCookie(command: { + recentCookie: CustomCookieData; + checks?: Checks; + challenges?: RequestChallenges; + requestId?: string; + lifetime: Duration; +}) { const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); return setSession({ serviceUrl, - sessionId: recentCookie.id, - sessionToken: recentCookie.token, - challenges, - checks, - lifetime, + sessionId: command.recentCookie.id, + sessionToken: command.recentCookie.token, + challenges: command.challenges, + checks: command.checks, + lifetime: command.lifetime, }) .then((updatedSession) => { if (updatedSession) { const sessionCookie: CustomCookieData = { - id: recentCookie.id, + id: command.recentCookie.id, token: updatedSession.sessionToken, - creationTs: recentCookie.creationTs, - expirationTs: recentCookie.expirationTs, + creationTs: command.recentCookie.creationTs, + expirationTs: command.recentCookie.expirationTs, // just overwrite the changeDate with the new one changeTs: updatedSession.details?.changeDate ? `${timestampMs(updatedSession.details.changeDate)}` : "", - loginName: recentCookie.loginName, - organization: recentCookie.organization, + loginName: command.recentCookie.loginName, + organization: command.recentCookie.organization, }; - if (requestId) { - sessionCookie.requestId = requestId; + if (command.requestId) { + sessionCookie.requestId = command.requestId; } return getSession({ diff --git a/login/apps/login/src/lib/server/otp.ts b/login/apps/login/src/lib/server/otp.ts deleted file mode 100644 index f3d4a1536a..0000000000 --- a/login/apps/login/src/lib/server/otp.ts +++ /dev/null @@ -1,83 +0,0 @@ -"use server"; - -import { setSessionAndUpdateCookie } from "@/lib/server/cookie"; -import { create } from "@zitadel/client"; -import { - CheckOTPSchema, - ChecksSchema, - CheckTOTPSchema, -} from "@zitadel/proto/zitadel/session/v2/session_service_pb"; -import { headers } from "next/headers"; -import { - getMostRecentSessionCookie, - getSessionCookieById, - getSessionCookieByLoginName, -} from "../cookies"; -import { getServiceUrlFromHeaders } from "../service-url"; -import { getLoginSettings } from "../zitadel"; - -export type SetOTPCommand = { - loginName?: string; - sessionId?: string; - organization?: string; - requestId?: string; - code: string; - method: string; -}; - -export async function setOTP(command: SetOTPCommand) { - const _headers = await headers(); - const { serviceUrl } = getServiceUrlFromHeaders(_headers); - - const recentSession = command.sessionId - ? await getSessionCookieById({ sessionId: command.sessionId }).catch( - (error) => { - return Promise.reject(error); - }, - ) - : command.loginName - ? await getSessionCookieByLoginName({ - loginName: command.loginName, - organization: command.organization, - }).catch((error) => { - return Promise.reject(error); - }) - : await getMostRecentSessionCookie().catch((error) => { - return Promise.reject(error); - }); - - const checks = create(ChecksSchema, {}); - - if (command.method === "time-based") { - checks.totp = create(CheckTOTPSchema, { - code: command.code, - }); - } else if (command.method === "sms") { - checks.otpSms = create(CheckOTPSchema, { - code: command.code, - }); - } else if (command.method === "email") { - checks.otpEmail = create(CheckOTPSchema, { - code: command.code, - }); - } - - const loginSettings = await getLoginSettings({ - serviceUrl, - organization: command.organization, - }); - - return setSessionAndUpdateCookie( - recentSession, - checks, - undefined, - command.requestId, - loginSettings?.secondFactorCheckLifetime, - ).then((session) => { - return { - sessionId: session.id, - factors: session.factors, - challenges: session.challenges, - }; - }); -} diff --git a/login/apps/login/src/lib/server/passkeys.ts b/login/apps/login/src/lib/server/passkeys.ts index 60aa2c92b4..ca603471ab 100644 --- a/login/apps/login/src/lib/server/passkeys.ts +++ b/login/apps/login/src/lib/server/passkeys.ts @@ -211,19 +211,27 @@ export async function sendPasskey(command: SendPasskeyCommand) { organization, }); - const lifetime = checks?.webAuthN + let lifetime = checks?.webAuthN ? loginSettings?.multiFactorCheckLifetime // TODO different lifetime for webauthn u2f/passkey : checks?.otpEmail || checks?.otpSms ? loginSettings?.secondFactorCheckLifetime : undefined; - const session = await setSessionAndUpdateCookie( - recentSession, + if (!lifetime) { + console.warn("No passkey lifetime provided, defaulting to 24 hours"); + + lifetime = { + seconds: BigInt(60 * 60 * 24), // default to 24 hours + nanos: 0, + } as Duration; + } + + const session = await setSessionAndUpdateCookie({ + recentCookie: recentSession, checks, - undefined, requestId, lifetime, - ); + }); if (!session || !session?.factors?.user?.id) { return { error: "Could not update session" }; diff --git a/login/apps/login/src/lib/server/password.ts b/login/apps/login/src/lib/server/password.ts index 5c6fb03aa5..013255d06f 100644 --- a/login/apps/login/src/lib/server/password.ts +++ b/login/apps/login/src/lib/server/password.ts @@ -16,7 +16,7 @@ import { setPassword, setUserPassword, } from "@/lib/zitadel"; -import { ConnectError, create } from "@zitadel/client"; +import { ConnectError, create, Duration } from "@zitadel/client"; import { createUserServiceClient } from "@zitadel/client/v2"; import { Checks, @@ -152,14 +152,32 @@ export async function sendPassword(command: UpdateSessionCommand) { // this is a fake error message to hide that the user does not even exist return { error: "Could not verify password" }; } else { + loginSettings = await getLoginSettings({ + serviceUrl, + organization: sessionCookie.organization, + }); + + if (!loginSettings) { + return { error: "Could not load login settings" }; + } + + let lifetime = loginSettings.passwordCheckLifetime; + + if (!lifetime) { + console.warn("No password lifetime provided, defaulting to 24 hours"); + lifetime = { + seconds: BigInt(60 * 60 * 24), // default to 24 hours + nanos: 0, + } as Duration; + } + try { - session = await setSessionAndUpdateCookie( - sessionCookie, - command.checks, - undefined, - command.requestId, - loginSettings?.passwordCheckLifetime, - ); + session = await setSessionAndUpdateCookie({ + recentCookie: sessionCookie, + checks: command.checks, + requestId: command.requestId, + lifetime, + }); } catch (error: any) { if ("failedAttempts" in error && error.failedAttempts) { const lockoutSettings = await getLockoutSettings({ diff --git a/login/apps/login/src/lib/server/session.ts b/login/apps/login/src/lib/server/session.ts index d04d1d25b4..957c89ad81 100644 --- a/login/apps/login/src/lib/server/session.ts +++ b/login/apps/login/src/lib/server/session.ts @@ -154,19 +154,27 @@ export async function updateSession(options: UpdateSessionCommand) { organization, }); - const lifetime = checks?.webAuthN + let lifetime = checks?.webAuthN ? loginSettings?.multiFactorCheckLifetime // TODO different lifetime for webauthn u2f/passkey : checks?.otpEmail || checks?.otpSms ? loginSettings?.secondFactorCheckLifetime : undefined; - const session = await setSessionAndUpdateCookie( - recentSession, + if (!lifetime) { + console.warn("No lifetime provided for session, defaulting to 24 hours"); + lifetime = { + seconds: BigInt(60 * 60 * 24), // default to 24 hours + nanos: 0, + } as Duration; + } + + const session = await setSessionAndUpdateCookie({ + recentCookie: recentSession, checks, challenges, requestId, lifetime, - ); + }); if (!session) { return { error: "Could not update session" }; diff --git a/login/apps/login/src/lib/zitadel.ts b/login/apps/login/src/lib/zitadel.ts index bad773092c..5e4583f8af 100644 --- a/login/apps/login/src/lib/zitadel.ts +++ b/login/apps/login/src/lib/zitadel.ts @@ -298,7 +298,7 @@ export async function createSessionFromChecks({ }: { serviceUrl: string; checks: Checks; - lifetime?: Duration; + lifetime: Duration; }) { const sessionService: Client = await createServiceForHost(SessionService, serviceUrl); @@ -320,7 +320,7 @@ export async function createSessionForUserIdAndIdpIntent({ idpIntentId?: string | undefined; idpIntentToken?: string | undefined; }; - lifetime?: Duration; + lifetime: Duration; }) { const sessionService: Client = await createServiceForHost(SessionService, serviceUrl); @@ -355,7 +355,7 @@ export async function setSession({ sessionToken: string; challenges: RequestChallenges | undefined; checks?: Checks; - lifetime?: Duration; + lifetime: Duration; }) { const sessionService: Client = await createServiceForHost(SessionService, serviceUrl); diff --git a/login/apps/login/turbo.json b/login/apps/login/turbo.json index f05cd41d01..db0e93d5ed 100644 --- a/login/apps/login/turbo.json +++ b/login/apps/login/turbo.json @@ -8,6 +8,9 @@ "build:login:standalone": { "outputs": ["dist/**", ".next/**", "!.next/cache/**"] }, + "dev": { + "dependsOn": ["@zitadel/client#build", "@zitadel/proto#generate"] + }, "test": { "dependsOn": ["@zitadel/client#build"] }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a8a22a41ce..901b332f2d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -421,6 +421,9 @@ importers: '@heroicons/react': specifier: 2.1.3 version: 2.1.3(react@19.1.0) + '@radix-ui/react-tooltip': + specifier: ^1.2.7 + version: 1.2.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tailwindcss/forms': specifier: 0.5.7 version: 0.5.7(tailwindcss@3.4.14) @@ -4259,6 +4262,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-tooltip@1.2.7': + resolution: {integrity: sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-use-callback-ref@1.1.0': resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} peerDependencies: @@ -4398,6 +4414,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/rect@1.1.0': resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} @@ -19496,6 +19525,15 @@ snapshots: '@types/react': 19.1.2 '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-avatar@1.1.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-context': 1.1.1(@types/react@19.1.2)(react@18.3.1) @@ -19548,6 +19586,12 @@ snapshots: optionalDependencies: '@types/react': 19.1.2 + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.2)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + '@radix-ui/react-context@1.1.1(@types/react@19.1.2)(react@18.3.1)': dependencies: react: 18.3.1 @@ -19560,6 +19604,12 @@ snapshots: optionalDependencies: '@types/react': 19.1.2 + '@radix-ui/react-context@1.1.2(@types/react@19.1.2)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + '@radix-ui/react-direction@1.1.0(@types/react@19.1.2)(react@18.3.1)': dependencies: react: 18.3.1 @@ -19585,6 +19635,19 @@ snapshots: '@types/react': 19.1.2 '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-dismissable-layer@1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.1 @@ -19676,6 +19739,13 @@ snapshots: optionalDependencies: '@types/react': 19.1.2 + '@radix-ui/react-id@1.1.1(@types/react@19.1.2)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + '@radix-ui/react-popover@1.1.6(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.1 @@ -19753,6 +19823,24 @@ snapshots: '@types/react': 19.1.2 '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-popper@1.2.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@floating-ui/react-dom': 2.1.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/rect': 1.1.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-portal@1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -19783,6 +19871,16 @@ snapshots: '@types/react': 19.1.2 '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-presence@1.1.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.2)(react@18.3.1) @@ -19803,6 +19901,16 @@ snapshots: '@types/react': 19.1.2 '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-presence@1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-primitive@2.0.1(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-slot': 1.1.1(@types/react@19.1.2)(react@18.3.1) @@ -19830,6 +19938,15 @@ snapshots: '@types/react': 19.1.2 '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-roving-focus@1.1.10(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 @@ -19885,6 +20002,13 @@ snapshots: optionalDependencies: '@types/react': 19.1.2 + '@radix-ui/react-slot@1.2.3(@types/react@19.1.2)(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + '@radix-ui/react-tabs@1.1.12(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 @@ -19921,6 +20045,26 @@ snapshots: '@types/react': 19.1.2 '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-tooltip@1.2.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@19.1.2)(react@18.3.1)': dependencies: react: 18.3.1 @@ -19933,6 +20077,12 @@ snapshots: optionalDependencies: '@types/react': 19.1.2 + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.2)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + '@radix-ui/react-use-controllable-state@1.1.0(@types/react@19.1.2)(react@18.3.1)': dependencies: '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.1.2)(react@18.3.1) @@ -19948,6 +20098,14 @@ snapshots: optionalDependencies: '@types/react': 19.1.2 + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.1.2)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.1.2)(react@18.3.1)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@18.3.1) @@ -19955,6 +20113,13 @@ snapshots: optionalDependencies: '@types/react': 19.1.2 + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.1.2)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@19.1.2)(react@18.3.1)': dependencies: '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.1.2)(react@18.3.1) @@ -19969,6 +20134,13 @@ snapshots: optionalDependencies: '@types/react': 19.1.2 + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.1.2)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + '@radix-ui/react-use-layout-effect@1.1.0(@types/react@19.1.2)(react@18.3.1)': dependencies: react: 18.3.1 @@ -19981,6 +20153,12 @@ snapshots: optionalDependencies: '@types/react': 19.1.2 + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.2)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + '@radix-ui/react-use-previous@1.1.0(@types/react@19.1.2)(react@18.3.1)': dependencies: react: 18.3.1 @@ -20001,6 +20179,13 @@ snapshots: optionalDependencies: '@types/react': 19.1.2 + '@radix-ui/react-use-rect@1.1.1(@types/react@19.1.2)(react@19.1.0)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + '@radix-ui/react-use-size@1.1.0(@types/react@19.1.2)(react@18.3.1)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.1.2)(react@18.3.1) @@ -20015,6 +20200,13 @@ snapshots: optionalDependencies: '@types/react': 19.1.2 + '@radix-ui/react-use-size@1.1.1(@types/react@19.1.2)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + '@radix-ui/react-visually-hidden@1.1.1(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -20024,6 +20216,15 @@ snapshots: '@types/react': 19.1.2 '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/rect@1.1.0': {} '@radix-ui/rect@1.1.1': {}