diff --git a/apps/login/.env.integration b/apps/login/.env.integration index 90adb84eee..705d7d0733 100644 --- a/apps/login/.env.integration +++ b/apps/login/.env.integration @@ -1,3 +1,6 @@ ZITADEL_API_URL=http://localhost:22222 +ZITADEL_SERVICE_USER_ID="yolo" +ZITADEL_SERVICE_USER_TOKEN="yolo" EMAIL_VERIFICATION=true -DEBUG=true \ No newline at end of file +DEBUG=true +NEXT_PUBLIC_BASE_PATH="" \ No newline at end of file diff --git a/apps/login/next-env-vars.d.ts b/apps/login/next-env-vars.d.ts new file mode 100644 index 0000000000..35fcb10919 --- /dev/null +++ b/apps/login/next-env-vars.d.ts @@ -0,0 +1,31 @@ +declare namespace NodeJS { + interface ProcessEnv { + // Allow any environment variable that matches the pattern + [key: `${string}_AUDIENCE`]: string; // The system api url + [key: `${string}_SYSTEM_USER_ID`]: string; // The service user id + [key: `${string}_SYSTEM_USER_PRIVATE_KEY`]: string; // The service user private key + + AUDIENCE: string; // The fallback system api url + SYSTEM_USER_ID: string; // The fallback service user id + SYSTEM_USER_PRIVATE_KEY: string; // The fallback service user private key + + /** + * Self hosting: The instance url + */ + ZITADEL_API_URL: string; + + /** + * Self hosting: The service user id + */ + ZITADEL_SERVICE_USER_ID: string; + /** + * Self hosting: The service user token + */ + ZITADEL_SERVICE_USER_TOKEN: string; + + /** + * Optional: wheter a user must have verified email + */ + EMAIL_VERIFICATION: string; + } +} diff --git a/apps/login/next.config.mjs b/apps/login/next.config.mjs index a6e1d1a6d8..32209b11e7 100755 --- a/apps/login/next.config.mjs +++ b/apps/login/next.config.mjs @@ -35,6 +35,7 @@ const secureHeaders = [ ]; const nextConfig = { + basePath: process.env.NEXT_PUBLIC_BASE_PATH, reactStrictMode: true, // Recommended for the `pages` directory, default in `app`. experimental: { dynamicIO: true, diff --git a/apps/login/package.json b/apps/login/package.json index 6617a416b0..6b71bca77d 100644 --- a/apps/login/package.json +++ b/apps/login/package.json @@ -44,14 +44,15 @@ "clsx": "1.2.1", "copy-to-clipboard": "^3.3.3", "deepmerge": "^4.3.1", + "jose": "^5.3.0", "moment": "^2.29.4", - "next": "15.0.4-canary.23", + "next": "15.2.0-canary.33", "next-intl": "^3.25.1", "next-themes": "^0.2.1", "nice-grpc": "2.0.1", "qrcode.react": "^3.1.0", - "react": "19.0.0-rc-66855b96-20241106", - "react-dom": "19.0.0-rc-66855b96-20241106", + "react": "19.0.0", + "react-dom": "19.0.0", "react-hook-form": "7.39.5", "swr": "^2.2.0", "tinycolor2": "1.4.2" @@ -62,19 +63,20 @@ "@testing-library/react": "^16.0.1", "@types/ms": "0.7.34", "@types/node": "22.9.0", - "@types/react": "npm:types-react@19.0.0-rc.1", - "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1", + "@types/react": "19.0.2", + "@types/react-dom": "19.0.2", "@types/tinycolor2": "1.4.3", "@types/uuid": "^10.0.0", "@vercel/git-hooks": "1.0.0", + "@zitadel/eslint-config": "workspace:*", "@zitadel/prettier-config": "workspace:*", + "@zitadel/tailwind-config": "workspace:*", "@zitadel/tsconfig": "workspace:*", "autoprefixer": "10.4.20", "concurrently": "^9.1.0", "cypress": "^13.15.2", "del-cli": "6.0.0", "env-cmd": "^10.0.0", - "@zitadel/eslint-config": "workspace:*", "grpc-tools": "1.12.4", "jsdom": "^25.0.1", "lint-staged": "15.2.10", @@ -86,7 +88,6 @@ "start-server-and-test": "^2.0.8", "tailwindcss": "3.4.14", "ts-proto": "^2.2.7", - "typescript": "^5.6.3", - "@zitadel/tailwind-config": "workspace:*" + "typescript": "^5.6.3" } } diff --git a/apps/login/readme.md b/apps/login/readme.md index fd6ba6f48c..4df81f9f9d 100644 --- a/apps/login/readme.md +++ b/apps/login/readme.md @@ -389,10 +389,6 @@ In future, self service options to jump to are shown below, like: ## Currently NOT Supported -Timebased features like the multifactor init prompt or password expiry, are not supported due to a current limitation in the API. Lockout settings which keeps track of the password retries, will also be implemented in a later stage. - -- Lockout Settings -- Password Expiry Settings - Login Settings: multifactor init prompt - forceMFA on login settings is not checked for IDPs diff --git a/apps/login/src/app/(login)/accounts/page.tsx b/apps/login/src/app/(login)/accounts/page.tsx index 49fbad6202..bc63d990c9 100644 --- a/apps/login/src/app/(login)/accounts/page.tsx +++ b/apps/login/src/app/(login)/accounts/page.tsx @@ -1,6 +1,7 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { SessionsList } from "@/components/sessions-list"; import { getAllSessionCookieIds } from "@/lib/cookies"; +import { getServiceUrlFromHeaders } from "@/lib/service"; import { getBrandingSettings, getDefaultOrg, @@ -9,15 +10,24 @@ import { 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 { headers } from "next/headers"; import Link from "next/link"; -async function loadSessions() { - const ids = await getAllSessionCookieIds(); +async function loadSessions({ + serviceUrl, + serviceRegion, +}: { + serviceUrl: string; + serviceRegion: string; +}) { + const ids: (string | undefined)[] = await getAllSessionCookieIds(); if (ids && ids.length) { - const response = await listSessions( - ids.filter((id: string | undefined) => !!id), - ); + const response = await listSessions({ + serviceUrl, + serviceRegion, + ids: ids.filter((id) => !!id) as string[], + }); return response?.sessions ?? []; } else { console.info("No session cookie found."); @@ -35,19 +45,27 @@ export default async function Page(props: { const authRequestId = searchParams?.authRequestId; const organization = searchParams?.organization; + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + let defaultOrganization; if (!organization) { - const org: Organization | null = await getDefaultOrg(); + const org: Organization | null = await getDefaultOrg({ + serviceUrl, + serviceRegion, + }); if (org) { defaultOrganization = org.id; } } - let sessions = await loadSessions(); + let sessions = await loadSessions({ serviceUrl, serviceRegion }); - const branding = await getBrandingSettings( - organization ?? defaultOrganization, - ); + const branding = await getBrandingSettings({ + serviceUrl, + serviceRegion, + organization: organization ?? defaultOrganization, + }); const params = new URLSearchParams(); diff --git a/apps/login/src/app/(login)/authenticator/set/page.tsx b/apps/login/src/app/(login)/authenticator/set/page.tsx index 7203484df0..634b116815 100644 --- a/apps/login/src/app/(login)/authenticator/set/page.tsx +++ b/apps/login/src/app/(login)/authenticator/set/page.tsx @@ -5,6 +5,7 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { SignInWithIdp } from "@/components/sign-in-with-idp"; import { UserAvatar } from "@/components/user-avatar"; import { getSessionCookieById } from "@/lib/cookies"; +import { getServiceUrlFromHeaders } from "@/lib/service"; import { loadMostRecentSession } from "@/lib/session"; import { getActiveIdentityProviders, @@ -16,6 +17,7 @@ import { } from "@/lib/zitadel"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { getLocale, getTranslations } from "next-intl/server"; +import { headers } from "next/headers"; export default async function Page(props: { searchParams: Promise>; @@ -27,19 +29,30 @@ export default async function Page(props: { const { loginName, authRequestId, organization, sessionId } = searchParams; - const sessionWithData = sessionId - ? await loadSessionById(sessionId, organization) - : await loadSessionByLoginname(loginName, organization); + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); - async function getAuthMethodsAndUser(session?: Session) { + const sessionWithData = sessionId + ? await loadSessionById(serviceUrl, sessionId, organization) + : await loadSessionByLoginname(serviceUrl, loginName, organization); + + async function getAuthMethodsAndUser( + serviceUrl: string, + serviceRegion: string, + session?: Session, + ) { const userId = session?.factors?.user?.id; if (!userId) { throw Error("Could not get user id from session"); } - return listAuthenticationMethodTypes(userId).then((methods) => { - return getUserByID(userId).then((user) => { + return listAuthenticationMethodTypes({ + serviceUrl, + serviceRegion, + userId, + }).then((methods) => { + return getUserByID({ serviceUrl, serviceRegion, userId }).then((user) => { const humanUser = user.user?.type.case === "human" ? user.user?.type.value : undefined; @@ -55,24 +68,39 @@ export default async function Page(props: { } async function loadSessionByLoginname( + host: string, loginName?: string, organization?: string, ) { return loadMostRecentSession({ - loginName, - organization, + serviceUrl, + serviceRegion, + sessionParams: { + loginName, + organization, + }, }).then((session) => { - return getAuthMethodsAndUser(session); + return getAuthMethodsAndUser(serviceUrl, serviceRegion, session); }); } - async function loadSessionById(sessionId: string, organization?: string) { + async function loadSessionById( + host: string, + sessionId: string, + organization?: string, + ) { const recent = await getSessionCookieById({ sessionId, organization }); return getSession({ + serviceUrl, + serviceRegion, sessionId: recent.id, sessionToken: recent.token, }).then((sessionResponse) => { - return getAuthMethodsAndUser(sessionResponse.session); + return getAuthMethodsAndUser( + serviceUrl, + serviceRegion, + sessionResponse.session, + ); }); } @@ -80,18 +108,24 @@ export default async function Page(props: { return {tError("unknownContext")}; } - const branding = await getBrandingSettings( - sessionWithData.factors?.user?.organizationId, - ); + const branding = await getBrandingSettings({ + serviceUrl, + serviceRegion, + organization: sessionWithData.factors?.user?.organizationId, + }); - const loginSettings = await getLoginSettings( - sessionWithData.factors?.user?.organizationId, - ); + const loginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization: sessionWithData.factors?.user?.organizationId, + }); - const identityProviders = await getActiveIdentityProviders( - sessionWithData.factors?.user?.organizationId, - true, - ).then((resp) => { + const identityProviders = await getActiveIdentityProviders({ + serviceUrl, + serviceRegion, + orgId: sessionWithData.factors?.user?.organizationId, + linking_allowed: true, + }).then((resp) => { return resp.identityProviders; }); 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 a3cc0ee883..c1e4de1b26 100644 --- a/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx @@ -1,7 +1,9 @@ import { DynamicTheme } from "@/components/dynamic-theme"; +import { getServiceUrlFromHeaders } from "@/lib/service"; import { getBrandingSettings } from "@/lib/zitadel"; import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { getLocale, getTranslations } from "next-intl/server"; +import { headers } from "next/headers"; // This configuration shows the given name in the respective IDP button as fallback const PROVIDER_NAME_MAPPING: { @@ -22,7 +24,14 @@ export default async function Page(props: { const { organization } = searchParams; - const branding = await getBrandingSettings(organization); + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + + const branding = await getBrandingSettings({ + serviceUrl, + serviceRegion, + organization, + }); return ( 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 0de29fd1ba..425e9f0caf 100644 --- a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx @@ -5,6 +5,7 @@ import { linkingSuccess } from "@/components/idps/pages/linking-success"; import { loginFailed } from "@/components/idps/pages/login-failed"; import { loginSuccess } from "@/components/idps/pages/login-success"; import { idpTypeToIdentityProviderType, PROVIDER_MAPPING } from "@/lib/idp"; +import { getServiceUrlFromHeaders } from "@/lib/service"; import { addHuman, addIDPLink, @@ -23,6 +24,7 @@ import { AddHumanUserRequestSchema, } 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 = /(?<=@)(.+)/; @@ -37,13 +39,25 @@ export default async function Page(props: { const { id, token, authRequestId, organization, link } = searchParams; const { provider } = params; - const branding = await getBrandingSettings(organization); + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + + const branding = await getBrandingSettings({ + serviceUrl, + serviceRegion, + organization, + }); if (!provider || !id || !token) { return loginFailed(branding, "IDP context missing"); } - const intent = await retrieveIDPIntent(id, token); + const intent = await retrieveIDPIntent({ + serviceUrl, + serviceRegion, + id, + token, + }); const { idpInformation, userId } = intent; @@ -63,7 +77,11 @@ export default async function Page(props: { return loginFailed(branding, "IDP information missing"); } - const idp = await getIDPByID(idpInformation.idpId); + const idp = await getIDPByID({ + serviceUrl, + serviceRegion, + id: idpInformation.idpId, + }); const options = idp?.config?.options; if (!idp) { @@ -80,14 +98,16 @@ export default async function Page(props: { let idpLink; try { - idpLink = await addIDPLink( - { + idpLink = await addIDPLink({ + serviceUrl, + serviceRegion, + idp: { id: idpInformation.idpId, userId: idpInformation.userId, userName: idpInformation.userName, }, userId, - ); + }); } catch (error) { console.error(error); return linkingFailed(branding); @@ -111,19 +131,23 @@ export default async function Page(props: { const email = PROVIDER_MAPPING[providerType](idpInformation).email?.email; if (options.autoLinking === AutoLinkingOption.EMAIL && email) { - foundUser = await listUsers({ email }).then((response) => { - return response.result ? response.result[0] : null; - }); + foundUser = await listUsers({ serviceUrl, serviceRegion, email }).then( + (response) => { + return response.result ? response.result[0] : null; + }, + ); } else if (options.autoLinking === AutoLinkingOption.USERNAME) { foundUser = await listUsers( options.autoLinking === AutoLinkingOption.USERNAME - ? { userName: idpInformation.userName } - : { email }, + ? { serviceUrl, serviceRegion, userName: idpInformation.userName } + : { serviceUrl, serviceRegion, email }, ).then((response) => { return response.result ? response.result[0] : null; }); } else { foundUser = await listUsers({ + serviceUrl, + serviceRegion, userName: idpInformation.userName, email, }).then((response) => { @@ -134,14 +158,16 @@ export default async function Page(props: { if (foundUser) { let idpLink; try { - idpLink = await addIDPLink( - { + idpLink = await addIDPLink({ + serviceUrl, + serviceRegion, + idp: { id: idpInformation.idpId, userId: idpInformation.userId, userName: idpInformation.userName, }, - foundUser.userId, - ); + userId: foundUser.userId, + }); } catch (error) { console.error(error); return linkingFailed(branding); @@ -175,11 +201,19 @@ export default async function Page(props: { const suffix = matched?.[1] ?? ""; // this just returns orgs where the suffix is set as primary domain - const orgs = await getOrgsByDomain(suffix); + const orgs = await getOrgsByDomain({ + serviceUrl, + serviceRegion, + domain: suffix, + }); const orgToCheckForDiscovery = orgs.result && orgs.result.length === 1 ? orgs.result[0].id : undefined; - const orgLoginSettings = await getLoginSettings(orgToCheckForDiscovery); + const orgLoginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization: orgToCheckForDiscovery, + }); if (orgLoginSettings?.allowDomainDiscovery) { orgToRegisterOn = orgToCheckForDiscovery; } @@ -196,7 +230,11 @@ export default async function Page(props: { }); } - const newUser = await addHuman(userData); + const newUser = await addHuman({ + serviceUrl, + serviceRegion, + request: userData, + }); if (newUser) { return ( diff --git a/apps/login/src/app/(login)/idp/page.tsx b/apps/login/src/app/(login)/idp/page.tsx index 30d1c9fab5..80829557ec 100644 --- a/apps/login/src/app/(login)/idp/page.tsx +++ b/apps/login/src/app/(login)/idp/page.tsx @@ -1,16 +1,9 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { SignInWithIdp } from "@/components/sign-in-with-idp"; -import { getBrandingSettings, settingsService } from "@/lib/zitadel"; -import { makeReqCtx } from "@zitadel/client/v2"; +import { getServiceUrlFromHeaders } from "@/lib/service"; +import { getActiveIdentityProviders, getBrandingSettings } from "@/lib/zitadel"; import { getLocale, getTranslations } from "next-intl/server"; - -function getIdentityProviders(orgId?: string) { - return settingsService - .getActiveIdentityProviders({ ctx: makeReqCtx(orgId) }, {}) - .then((resp) => { - return resp.identityProviders; - }); -} +import { headers } from "next/headers"; export default async function Page(props: { searchParams: Promise>; @@ -22,9 +15,22 @@ export default async function Page(props: { const authRequestId = searchParams?.authRequestId; const organization = searchParams?.organization; - const identityProviders = await getIdentityProviders(organization); + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); - const branding = await getBrandingSettings(organization); + const identityProviders = await getActiveIdentityProviders({ + serviceUrl, + serviceRegion, + orgId: organization, + }).then((resp) => { + return resp.identityProviders; + }); + + const branding = await getBrandingSettings({ + serviceUrl, + serviceRegion, + organization, + }); return ( diff --git a/apps/login/src/app/(login)/invite/page.tsx b/apps/login/src/app/(login)/invite/page.tsx index 18c60eb993..f9c8405855 100644 --- a/apps/login/src/app/(login)/invite/page.tsx +++ b/apps/login/src/app/(login)/invite/page.tsx @@ -1,6 +1,7 @@ import { Alert, AlertType } from "@/components/alert"; import { DynamicTheme } from "@/components/dynamic-theme"; import { InviteForm } from "@/components/invite-form"; +import { getServiceUrlFromHeaders } from "@/lib/service"; import { getBrandingSettings, getDefaultOrg, @@ -8,6 +9,7 @@ import { getPasswordComplexitySettings, } from "@/lib/zitadel"; import { getLocale, getTranslations } from "next-intl/server"; +import { headers } from "next/headers"; export default async function Page(props: { searchParams: Promise>; @@ -18,8 +20,11 @@ export default async function Page(props: { let { firstname, lastname, email, organization } = searchParams; + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + if (!organization) { - const org = await getDefaultOrg(); + const org = await getDefaultOrg({ serviceUrl, serviceRegion }); if (!org) { throw new Error("No default organization found"); } @@ -27,12 +32,23 @@ export default async function Page(props: { organization = org.id; } - const loginSettings = await getLoginSettings(organization); + const loginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization, + }); - const passwordComplexitySettings = - await getPasswordComplexitySettings(organization); + const passwordComplexitySettings = await getPasswordComplexitySettings({ + serviceUrl, + serviceRegion, + organization, + }); - const branding = await getBrandingSettings(organization); + const branding = await getBrandingSettings({ + serviceUrl, + serviceRegion, + organization, + }); return ( diff --git a/apps/login/src/app/(login)/invite/success/page.tsx b/apps/login/src/app/(login)/invite/success/page.tsx index 96c4984159..155849ebb9 100644 --- a/apps/login/src/app/(login)/invite/success/page.tsx +++ b/apps/login/src/app/(login)/invite/success/page.tsx @@ -2,9 +2,11 @@ import { Alert, AlertType } from "@/components/alert"; import { Button, ButtonVariants } from "@/components/button"; import { DynamicTheme } from "@/components/dynamic-theme"; import { UserAvatar } from "@/components/user-avatar"; +import { getServiceUrlFromHeaders } from "@/lib/service"; import { getBrandingSettings, getDefaultOrg, getUserByID } from "@/lib/zitadel"; import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { getLocale, getTranslations } from "next-intl/server"; +import { headers } from "next/headers"; import Link from "next/link"; export default async function Page(props: { @@ -16,8 +18,11 @@ export default async function Page(props: { let { userId, organization } = searchParams; + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + if (!organization) { - const org = await getDefaultOrg(); + const org = await getDefaultOrg({ serviceUrl, serviceRegion }); if (!org) { throw new Error("No default organization found"); } @@ -25,12 +30,20 @@ export default async function Page(props: { organization = org.id; } - const branding = await getBrandingSettings(organization); + const branding = await getBrandingSettings({ + serviceUrl, + serviceRegion, + organization, + }); let user: User | undefined; let human: HumanUser | undefined; if (userId) { - const userResponse = await getUserByID(userId); + const userResponse = await getUserByID({ + serviceUrl, + serviceRegion, + userId, + }); if (userResponse) { user = userResponse.user; if (user?.type.case === "human") { diff --git a/apps/login/src/app/(login)/loginname/page.tsx b/apps/login/src/app/(login)/loginname/page.tsx index 5400f64b9d..7f8cb92812 100644 --- a/apps/login/src/app/(login)/loginname/page.tsx +++ b/apps/login/src/app/(login)/loginname/page.tsx @@ -1,6 +1,7 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { SignInWithIdp } from "@/components/sign-in-with-idp"; import { UsernameForm } from "@/components/username-form"; +import { getServiceUrlFromHeaders } from "@/lib/service"; import { getActiveIdentityProviders, getBrandingSettings, @@ -9,6 +10,7 @@ import { } 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>; @@ -23,29 +25,45 @@ export default async function Page(props: { const suffix = searchParams?.suffix; const submit: boolean = searchParams?.submit === "true"; + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + let defaultOrganization; if (!organization) { - const org: Organization | null = await getDefaultOrg(); + const org: Organization | null = await getDefaultOrg({ + serviceUrl, + serviceRegion, + }); if (org) { defaultOrganization = org.id; } } - const loginSettings = await getLoginSettings( - organization ?? defaultOrganization, - ); + const loginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization: organization ?? defaultOrganization, + }); - const contextLoginSettings = await getLoginSettings(organization); + const contextLoginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization, + }); - const identityProviders = await getActiveIdentityProviders( - organization ?? defaultOrganization, - ).then((resp) => { + const identityProviders = await getActiveIdentityProviders({ + serviceUrl, + serviceRegion, + orgId: organization ?? defaultOrganization, + }).then((resp) => { return resp.identityProviders; }); - const branding = await getBrandingSettings( - organization ?? defaultOrganization, - ); + const branding = await getBrandingSettings({ + serviceUrl, + serviceRegion, + organization: organization ?? defaultOrganization, + }); return ( diff --git a/apps/login/src/app/(login)/mfa/page.tsx b/apps/login/src/app/(login)/mfa/page.tsx index 071806db04..53fc650788 100644 --- a/apps/login/src/app/(login)/mfa/page.tsx +++ b/apps/login/src/app/(login)/mfa/page.tsx @@ -4,6 +4,7 @@ import { ChooseSecondFactor } from "@/components/choose-second-factor"; import { DynamicTheme } from "@/components/dynamic-theme"; import { UserAvatar } from "@/components/user-avatar"; import { getSessionCookieById } from "@/lib/cookies"; +import { getServiceUrlFromHeaders } from "@/lib/service"; import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings, @@ -11,6 +12,7 @@ import { listAuthenticationMethodTypes, } from "@/lib/zitadel"; import { getLocale, getTranslations } from "next-intl/server"; +import { headers } from "next/headers"; export default async function Page(props: { searchParams: Promise>; @@ -22,41 +24,59 @@ export default async function Page(props: { const { loginName, authRequestId, organization, sessionId } = searchParams; + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + const sessionFactors = sessionId - ? await loadSessionById(sessionId, organization) - : await loadSessionByLoginname(loginName, organization); + ? await loadSessionById(serviceUrl, sessionId, organization) + : await loadSessionByLoginname(serviceUrl, loginName, organization); async function loadSessionByLoginname( + serviceUrl: string, loginName?: string, organization?: string, ) { return loadMostRecentSession({ - loginName, - organization, + serviceUrl, + serviceRegion, + sessionParams: { + loginName, + organization, + }, }).then((session) => { if (session && session.factors?.user?.id) { - return listAuthenticationMethodTypes(session.factors.user.id).then( - (methods) => { - return { - factors: session?.factors, - authMethods: methods.authMethodTypes ?? [], - }; - }, - ); + return listAuthenticationMethodTypes({ + serviceUrl, + serviceRegion, + userId: session.factors.user.id, + }).then((methods) => { + return { + factors: session?.factors, + authMethods: methods.authMethodTypes ?? [], + }; + }); } }); } - async function loadSessionById(sessionId: string, organization?: string) { + async function loadSessionById( + host: string, + sessionId: string, + organization?: string, + ) { const recent = await getSessionCookieById({ sessionId, organization }); return getSession({ + serviceUrl, + serviceRegion, sessionId: recent.id, sessionToken: recent.token, }).then((response) => { if (response?.session && response.session.factors?.user?.id) { - return listAuthenticationMethodTypes( - response.session.factors.user.id, - ).then((methods) => { + return listAuthenticationMethodTypes({ + serviceUrl, + serviceRegion, + userId: response.session.factors.user.id, + }).then((methods) => { return { factors: response.session?.factors, authMethods: methods.authMethodTypes ?? [], @@ -66,7 +86,11 @@ export default async function Page(props: { }); } - const branding = await getBrandingSettings(organization); + const branding = await getBrandingSettings({ + serviceUrl, + serviceRegion, + organization, + }); return ( diff --git a/apps/login/src/app/(login)/mfa/set/page.tsx b/apps/login/src/app/(login)/mfa/set/page.tsx index dce54618dc..64e9cd7605 100644 --- a/apps/login/src/app/(login)/mfa/set/page.tsx +++ b/apps/login/src/app/(login)/mfa/set/page.tsx @@ -4,6 +4,7 @@ import { ChooseSecondFactorToSetup } from "@/components/choose-second-factor-to- import { DynamicTheme } from "@/components/dynamic-theme"; import { UserAvatar } from "@/components/user-avatar"; import { getSessionCookieById } from "@/lib/cookies"; +import { getServiceUrlFromHeaders } from "@/lib/service"; import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings, @@ -15,6 +16,7 @@ import { 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): { valid: boolean; @@ -49,19 +51,26 @@ export default async function Page(props: { sessionId, } = searchParams; - const sessionWithData = sessionId - ? await loadSessionById(sessionId, organization) - : await loadSessionByLoginname(loginName, organization); + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); - async function getAuthMethodsAndUser(session?: Session) { + const sessionWithData = sessionId + ? await loadSessionById(serviceUrl, sessionId, organization) + : await loadSessionByLoginname(serviceUrl, loginName, organization); + + async function getAuthMethodsAndUser(host: string, session?: Session) { const userId = session?.factors?.user?.id; if (!userId) { throw Error("Could not get user id from session"); } - return listAuthenticationMethodTypes(userId).then((methods) => { - return getUserByID(userId).then((user) => { + return listAuthenticationMethodTypes({ + serviceUrl, + serviceRegion, + userId, + }).then((methods) => { + return getUserByID({ serviceUrl, serviceRegion, userId }).then((user) => { const humanUser = user.user?.type.case === "human" ? user.user?.type.value : undefined; @@ -77,31 +86,48 @@ export default async function Page(props: { } async function loadSessionByLoginname( + host: string, loginName?: string, organization?: string, ) { return loadMostRecentSession({ - loginName, - organization, + serviceUrl, + serviceRegion, + sessionParams: { + loginName, + organization, + }, }).then((session) => { - return getAuthMethodsAndUser(session); + return getAuthMethodsAndUser(serviceUrl, session); }); } - async function loadSessionById(sessionId: string, organization?: string) { + async function loadSessionById( + host: string, + sessionId: string, + organization?: string, + ) { const recent = await getSessionCookieById({ sessionId, organization }); return getSession({ + serviceUrl, + serviceRegion, sessionId: recent.id, sessionToken: recent.token, }).then((sessionResponse) => { - return getAuthMethodsAndUser(sessionResponse.session); + return getAuthMethodsAndUser(serviceUrl, sessionResponse.session); }); } - const branding = await getBrandingSettings(organization); - const loginSettings = await getLoginSettings( - sessionWithData.factors?.user?.organizationId, - ); + const branding = await getBrandingSettings({ + serviceUrl, + serviceRegion, + organization, + }); + const loginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization: sessionWithData.factors?.user?.organizationId, + }); const { valid } = isSessionValid(sessionWithData); diff --git a/apps/login/src/app/(login)/otp/[method]/page.tsx b/apps/login/src/app/(login)/otp/[method]/page.tsx index 1755561238..3f3072dd07 100644 --- a/apps/login/src/app/(login)/otp/[method]/page.tsx +++ b/apps/login/src/app/(login)/otp/[method]/page.tsx @@ -3,6 +3,7 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { LoginOTP } from "@/components/login-otp"; import { UserAvatar } from "@/components/user-avatar"; import { getSessionCookieById } from "@/lib/cookies"; +import { getServiceUrlFromHeaders } from "@/lib/service"; import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings, @@ -22,6 +23,14 @@ export default async function Page(props: { const t = await getTranslations({ locale, namespace: "otp" }); const tError = await getTranslations({ locale, namespace: "error" }); + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + + if (!host || typeof host !== "string") { + throw new Error("No host found"); + } + const { loginName, // send from password page userId, // send from email link @@ -35,12 +44,22 @@ export default async function Page(props: { const { method } = params; const session = sessionId - ? await loadSessionById(sessionId, organization) - : await loadMostRecentSession({ loginName, organization }); + ? await loadSessionById(serviceUrl, sessionId, organization) + : await loadMostRecentSession({ + serviceUrl, + serviceRegion, + sessionParams: { loginName, organization }, + }); - async function loadSessionById(sessionId: string, organization?: string) { + async function loadSessionById( + host: string, + sessionId: string, + organization?: string, + ) { const recent = await getSessionCookieById({ sessionId, organization }); return getSession({ + serviceUrl, + serviceRegion, sessionId: recent.id, sessionToken: recent.token, }).then((response) => { @@ -51,15 +70,17 @@ export default async function Page(props: { } // email links do not come with organization, thus we need to use the session's organization - const branding = await getBrandingSettings( - organization ?? session?.factors?.user?.organizationId, - ); + const branding = await getBrandingSettings({ + serviceUrl, + serviceRegion, + organization: organization ?? session?.factors?.user?.organizationId, + }); - const loginSettings = await getLoginSettings( - organization ?? session?.factors?.user?.organizationId, - ); - - const host = (await headers()).get("host"); + const loginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization: organization ?? session?.factors?.user?.organizationId, + }); return ( 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 e64b4debe0..3de97510fd 100644 --- a/apps/login/src/app/(login)/otp/[method]/set/page.tsx +++ b/apps/login/src/app/(login)/otp/[method]/set/page.tsx @@ -4,6 +4,7 @@ import { Button, ButtonVariants } from "@/components/button"; import { DynamicTheme } from "@/components/dynamic-theme"; import { TotpRegister } from "@/components/totp-register"; import { UserAvatar } from "@/components/user-avatar"; +import { getServiceUrlFromHeaders } from "@/lib/service"; import { loadMostRecentSession } from "@/lib/session"; import { addOTPEmail, @@ -14,6 +15,7 @@ import { } 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"; @@ -31,18 +33,37 @@ export default async function Page(props: { searchParams; const { method } = params; - const branding = await getBrandingSettings(organization); - const loginSettings = await getLoginSettings(organization); + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + + const branding = await getBrandingSettings({ + serviceUrl, + serviceRegion, + organization, + }); + const loginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization, + }); const session = await loadMostRecentSession({ - loginName, - organization, + serviceUrl, + serviceRegion, + sessionParams: { + loginName, + organization, + }, }); let totpResponse: RegisterTOTPResponse | undefined, error: Error | undefined; if (session && session.factors?.user?.id) { if (method === "time-based") { - await registerTOTP(session.factors.user.id) + await registerTOTP({ + serviceUrl, + serviceRegion, + userId: session.factors.user.id, + }) .then((resp) => { if (resp) { totpResponse = resp; @@ -53,12 +74,20 @@ export default async function Page(props: { }); } else if (method === "sms") { // does not work - await addOTPSMS(session.factors.user.id).catch((error) => { + await addOTPSMS({ + serviceUrl, + serviceRegion, + userId: session.factors.user.id, + }).catch((error) => { error = new Error("Could not add OTP via SMS"); }); } else if (method === "email") { // works - await addOTPEmail(session.factors.user.id).catch((error) => { + await addOTPEmail({ + serviceUrl, + serviceRegion, + userId: session.factors.user.id, + }).catch((error) => { error = new Error("Could not add OTP via Email"); }); } else { diff --git a/apps/login/src/app/(login)/passkey/page.tsx b/apps/login/src/app/(login)/passkey/page.tsx index 0804f3ce2e..312eacc571 100644 --- a/apps/login/src/app/(login)/passkey/page.tsx +++ b/apps/login/src/app/(login)/passkey/page.tsx @@ -3,13 +3,11 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { LoginPasskey } from "@/components/login-passkey"; import { UserAvatar } from "@/components/user-avatar"; import { getSessionCookieById } from "@/lib/cookies"; +import { getServiceUrlFromHeaders } from "@/lib/service"; import { loadMostRecentSession } from "@/lib/session"; -import { - getBrandingSettings, - getLoginSettings, - getSession, -} from "@/lib/zitadel"; +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>; @@ -22,13 +20,26 @@ export default async function Page(props: { const { loginName, altPassword, authRequestId, organization, sessionId } = searchParams; - const sessionFactors = sessionId - ? await loadSessionById(sessionId, organization) - : await loadMostRecentSession({ loginName, organization }); + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); - async function loadSessionById(sessionId: string, organization?: string) { + const sessionFactors = sessionId + ? await loadSessionById(serviceUrl, sessionId, organization) + : await loadMostRecentSession({ + serviceUrl, + serviceRegion, + sessionParams: { loginName, organization }, + }); + + async function loadSessionById( + serviceUrl: string, + sessionId: string, + organization?: string, + ) { const recent = await getSessionCookieById({ sessionId, organization }); return getSession({ + serviceUrl, + serviceRegion, sessionId: recent.id, sessionToken: recent.token, }).then((response) => { @@ -38,9 +49,11 @@ export default async function Page(props: { }); } - const branding = await getBrandingSettings(organization); - - const loginSettings = await getLoginSettings(organization); + const branding = await getBrandingSettings({ + serviceUrl, + serviceRegion, + organization, + }); return ( @@ -66,7 +79,6 @@ export default async function Page(props: { authRequestId={authRequestId} altPassword={altPassword === "true"} organization={organization} - loginSettings={loginSettings} /> )} diff --git a/apps/login/src/app/(login)/passkey/set/page.tsx b/apps/login/src/app/(login)/passkey/set/page.tsx index 26a2de2428..e2f34ae830 100644 --- a/apps/login/src/app/(login)/passkey/set/page.tsx +++ b/apps/login/src/app/(login)/passkey/set/page.tsx @@ -2,9 +2,11 @@ import { Alert, AlertType } from "@/components/alert"; import { DynamicTheme } from "@/components/dynamic-theme"; import { RegisterPasskey } from "@/components/register-passkey"; import { UserAvatar } from "@/components/user-avatar"; +import { getServiceUrlFromHeaders } from "@/lib/service"; import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings } from "@/lib/zitadel"; import { getLocale, getTranslations } from "next-intl/server"; +import { headers } from "next/headers"; export default async function Page(props: { searchParams: Promise>; @@ -17,12 +19,23 @@ export default async function Page(props: { const { loginName, prompt, organization, authRequestId, userId } = searchParams; + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + const session = await loadMostRecentSession({ - loginName, - organization, + serviceUrl, + serviceRegion, + sessionParams: { + loginName, + organization, + }, }); - const branding = await getBrandingSettings(organization); + const branding = await getBrandingSettings({ + serviceUrl, + serviceRegion, + organization, + }); return ( diff --git a/apps/login/src/app/(login)/password/change/page.tsx b/apps/login/src/app/(login)/password/change/page.tsx index bbcaea4950..28f77a4b6d 100644 --- a/apps/login/src/app/(login)/password/change/page.tsx +++ b/apps/login/src/app/(login)/password/change/page.tsx @@ -2,6 +2,7 @@ import { Alert } from "@/components/alert"; import { ChangePasswordForm } from "@/components/change-password-form"; import { DynamicTheme } from "@/components/dynamic-theme"; import { UserAvatar } from "@/components/user-avatar"; +import { getServiceUrlFromHeaders } from "@/lib/service"; import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings, @@ -9,10 +10,14 @@ import { getPasswordComplexitySettings, } from "@/lib/zitadel"; import { getLocale, getTranslations } from "next-intl/server"; +import { headers } from "next/headers"; export default async function Page(props: { searchParams: Promise>; }) { + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + const searchParams = await props.searchParams; const locale = getLocale(); const t = await getTranslations({ locale, namespace: "password" }); @@ -22,19 +27,31 @@ export default async function Page(props: { // also allow no session to be found (ignoreUnkownUsername) const sessionFactors = await loadMostRecentSession({ - loginName, + serviceUrl, + serviceRegion, + sessionParams: { + loginName, + organization, + }, + }); + + const branding = await getBrandingSettings({ + serviceUrl, + serviceRegion, organization, }); - const branding = await getBrandingSettings(organization); + const passwordComplexity = await getPasswordComplexitySettings({ + serviceUrl, + serviceRegion, + organization: sessionFactors?.factors?.user?.organizationId, + }); - const passwordComplexity = await getPasswordComplexitySettings( - sessionFactors?.factors?.user?.organizationId, - ); - - const loginSettings = await getLoginSettings( - sessionFactors?.factors?.user?.organizationId, - ); + const loginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization: sessionFactors?.factors?.user?.organizationId, + }); return ( diff --git a/apps/login/src/app/(login)/password/page.tsx b/apps/login/src/app/(login)/password/page.tsx index a2c10c3238..b9b1756813 100644 --- a/apps/login/src/app/(login)/password/page.tsx +++ b/apps/login/src/app/(login)/password/page.tsx @@ -2,6 +2,7 @@ import { Alert } from "@/components/alert"; import { DynamicTheme } from "@/components/dynamic-theme"; import { PasswordForm } from "@/components/password-form"; import { UserAvatar } from "@/components/user-avatar"; +import { getServiceUrlFromHeaders } from "@/lib/service"; import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings, @@ -11,6 +12,7 @@ import { import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { getLocale, getTranslations } from "next-intl/server"; +import { headers } from "next/headers"; export default async function Page(props: { searchParams: Promise>; @@ -22,9 +24,15 @@ export default async function Page(props: { let { loginName, organization, authRequestId, alt } = searchParams; + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + let defaultOrganization; if (!organization) { - const org: Organization | null = await getDefaultOrg(); + const org: Organization | null = await getDefaultOrg({ + serviceUrl, + serviceRegion, + }); if (org) { defaultOrganization = org.id; @@ -35,20 +43,28 @@ export default async function Page(props: { let sessionFactors; try { sessionFactors = await loadMostRecentSession({ - loginName, - organization, + serviceUrl, + serviceRegion, + sessionParams: { + loginName, + organization, + }, }); } catch (error) { // ignore error to continue to show the password form console.warn(error); } - const branding = await getBrandingSettings( - organization ?? defaultOrganization, - ); - const loginSettings = await getLoginSettings( - organization ?? defaultOrganization, - ); + const branding = await getBrandingSettings({ + serviceUrl, + serviceRegion, + organization: organization ?? defaultOrganization, + }); + const loginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization: organization ?? defaultOrganization, + }); return ( diff --git a/apps/login/src/app/(login)/password/set/page.tsx b/apps/login/src/app/(login)/password/set/page.tsx index d60ac7023c..a6537b3513 100644 --- a/apps/login/src/app/(login)/password/set/page.tsx +++ b/apps/login/src/app/(login)/password/set/page.tsx @@ -2,6 +2,7 @@ import { Alert, AlertType } from "@/components/alert"; import { DynamicTheme } from "@/components/dynamic-theme"; import { SetPasswordForm } from "@/components/set-password-form"; import { UserAvatar } from "@/components/user-avatar"; +import { getServiceUrlFromHeaders } from "@/lib/service"; import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings, @@ -12,6 +13,7 @@ import { 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 { headers } from "next/headers"; export default async function Page(props: { searchParams: Promise>; @@ -24,27 +26,48 @@ export default async function Page(props: { const { userId, loginName, organization, authRequestId, code, initial } = searchParams; + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + // also allow no session to be found (ignoreUnkownUsername) let session: Session | undefined; if (loginName) { session = await loadMostRecentSession({ - loginName, - organization, + serviceUrl, + serviceRegion, + sessionParams: { + loginName, + organization, + }, }); } - const branding = await getBrandingSettings(organization); + const branding = await getBrandingSettings({ + serviceUrl, + serviceRegion, + organization, + }); - const passwordComplexity = await getPasswordComplexitySettings( - session?.factors?.user?.organizationId, - ); + const passwordComplexity = await getPasswordComplexitySettings({ + serviceUrl, + serviceRegion, + organization: session?.factors?.user?.organizationId, + }); - const loginSettings = await getLoginSettings(organization); + const loginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization, + }); let user: User | undefined; let displayName: string | undefined; if (userId) { - const userResponse = await getUserByID(userId); + const userResponse = await getUserByID({ + serviceUrl, + serviceRegion, + userId, + }); user = userResponse.user; if (user?.type.case === "human") { diff --git a/apps/login/src/app/(login)/register/page.tsx b/apps/login/src/app/(login)/register/page.tsx index 2cd1c35346..9fe2f3393c 100644 --- a/apps/login/src/app/(login)/register/page.tsx +++ b/apps/login/src/app/(login)/register/page.tsx @@ -1,5 +1,6 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { RegisterForm } from "@/components/register-form"; +import { getServiceUrlFromHeaders } from "@/lib/service"; import { getBrandingSettings, getDefaultOrg, @@ -9,6 +10,7 @@ import { } 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>; @@ -20,20 +22,41 @@ export default async function Page(props: { let { firstname, lastname, email, organization, authRequestId } = searchParams; + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + if (!organization) { - const org: Organization | null = await getDefaultOrg(); + const org: Organization | null = await getDefaultOrg({ + serviceUrl, + serviceRegion, + }); if (org) { organization = org.id; } } - const legal = await getLegalAndSupportSettings(organization); - const passwordComplexitySettings = - await getPasswordComplexitySettings(organization); + const legal = await getLegalAndSupportSettings({ + serviceUrl, + serviceRegion, + organization, + }); + const passwordComplexitySettings = await getPasswordComplexitySettings({ + serviceUrl, + serviceRegion, + organization, + }); - const branding = await getBrandingSettings(organization); + const branding = await getBrandingSettings({ + serviceUrl, + serviceRegion, + organization, + }); - const loginSettings = await getLoginSettings(organization); + const loginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization, + }); if (!loginSettings?.allowRegister) { return ( diff --git a/apps/login/src/app/(login)/register/password/page.tsx b/apps/login/src/app/(login)/register/password/page.tsx index 48e454c312..aeda4d56f6 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 { getServiceUrlFromHeaders } from "@/lib/service"; import { getBrandingSettings, getDefaultOrg, @@ -9,6 +10,7 @@ import { } 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>; @@ -20,8 +22,14 @@ export default async function Page(props: { let { firstname, lastname, email, organization, authRequestId } = searchParams; + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + if (!organization) { - const org: Organization | null = await getDefaultOrg(); + const org: Organization | null = await getDefaultOrg({ + serviceUrl, + serviceRegion, + }); if (org) { organization = org.id; } @@ -29,13 +37,28 @@ export default async function Page(props: { const missingData = !firstname || !lastname || !email; - const legal = await getLegalAndSupportSettings(organization); - const passwordComplexitySettings = - await getPasswordComplexitySettings(organization); + const legal = await getLegalAndSupportSettings({ + serviceUrl, + serviceRegion, + organization, + }); + const passwordComplexitySettings = await getPasswordComplexitySettings({ + serviceUrl, + serviceRegion, + organization, + }); - const branding = await getBrandingSettings(organization); + const branding = await getBrandingSettings({ + serviceUrl, + serviceRegion, + organization, + }); - const loginSettings = await getLoginSettings(organization); + const loginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization, + }); return missingData ? ( diff --git a/apps/login/src/app/(login)/signedin/page.tsx b/apps/login/src/app/(login)/signedin/page.tsx index 4fc9ac8546..f689713479 100644 --- a/apps/login/src/app/(login)/signedin/page.tsx +++ b/apps/login/src/app/(login)/signedin/page.tsx @@ -3,6 +3,7 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { SelfServiceMenu } from "@/components/self-service-menu"; import { UserAvatar } from "@/components/user-avatar"; import { getMostRecentCookieWithLoginname } from "@/lib/cookies"; +import { getServiceUrlFromHeaders } from "@/lib/service"; import { createCallback, getBrandingSettings, @@ -15,15 +16,23 @@ import { SessionSchema, } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; import { getLocale, getTranslations } from "next-intl/server"; +import { headers } from "next/headers"; import Link from "next/link"; import { redirect } from "next/navigation"; -async function loadSession(loginName: string, authRequestId?: string) { +async function loadSession( + serviceUrl: string, + serviceRegion: string, + loginName: string, + authRequestId?: string, +) { const recent = await getMostRecentCookieWithLoginname({ loginName }); if (authRequestId) { - return createCallback( - create(CreateCallbackRequestSchema, { + return createCallback({ + serviceUrl, + serviceRegion, + req: create(CreateCallbackRequestSchema, { authRequestId, callbackKind: { case: "session", @@ -33,17 +42,20 @@ async function loadSession(loginName: string, authRequestId?: string) { }), }, }), - ).then(({ callbackUrl }) => { + }).then(({ callbackUrl }) => { return redirect(callbackUrl); }); } - return getSession({ sessionId: recent.id, sessionToken: recent.token }).then( - (response) => { - if (response?.session) { - return response.session; - } - }, - ); + return getSession({ + serviceUrl, + serviceRegion, + sessionId: recent.id, + sessionToken: recent.token, + }).then((response) => { + if (response?.session) { + return response.session; + } + }); } export default async function Page(props: { searchParams: Promise }) { @@ -51,14 +63,30 @@ export default async function Page(props: { searchParams: Promise }) { const locale = getLocale(); const t = await getTranslations({ locale, namespace: "signedin" }); - const { loginName, authRequestId, organization } = searchParams; - const sessionFactors = await loadSession(loginName, authRequestId); + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); - const branding = await getBrandingSettings(organization); + const { loginName, authRequestId, organization } = searchParams; + const sessionFactors = await loadSession( + serviceUrl, + serviceRegion, + loginName, + authRequestId, + ); + + const branding = await getBrandingSettings({ + serviceUrl, + serviceRegion, + organization, + }); let loginSettings; if (!authRequestId) { - loginSettings = await getLoginSettings(organization); + loginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization, + }); } return ( diff --git a/apps/login/src/app/(login)/u2f/page.tsx b/apps/login/src/app/(login)/u2f/page.tsx index e4dd2bd8d2..e0a21103a8 100644 --- a/apps/login/src/app/(login)/u2f/page.tsx +++ b/apps/login/src/app/(login)/u2f/page.tsx @@ -3,9 +3,11 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { LoginPasskey } from "@/components/login-passkey"; import { UserAvatar } from "@/components/user-avatar"; import { getSessionCookieById } from "@/lib/cookies"; +import { getServiceUrlFromHeaders } from "@/lib/service"; 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>; @@ -17,15 +19,37 @@ export default async function Page(props: { const { loginName, authRequestId, sessionId, organization } = searchParams; - const branding = await getBrandingSettings(organization); + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + + if (!host || typeof host !== "string") { + throw new Error("No host found"); + } + + const branding = await getBrandingSettings({ + serviceUrl, + serviceRegion, + organization, + }); const sessionFactors = sessionId - ? await loadSessionById(sessionId, organization) - : await loadMostRecentSession({ loginName, organization }); + ? await loadSessionById(serviceUrl, sessionId, organization) + : await loadMostRecentSession({ + serviceUrl, + serviceRegion, + sessionParams: { loginName, organization }, + }); - async function loadSessionById(sessionId: string, organization?: string) { + async function loadSessionById( + host: string, + sessionId: string, + organization?: string, + ) { const recent = await getSessionCookieById({ sessionId, organization }); return getSession({ + serviceUrl, + serviceRegion, sessionId: recent.id, sessionToken: recent.token, }).then((response) => { diff --git a/apps/login/src/app/(login)/u2f/set/page.tsx b/apps/login/src/app/(login)/u2f/set/page.tsx index f9f3daeaba..850f2652d0 100644 --- a/apps/login/src/app/(login)/u2f/set/page.tsx +++ b/apps/login/src/app/(login)/u2f/set/page.tsx @@ -2,9 +2,11 @@ import { Alert } from "@/components/alert"; import { DynamicTheme } from "@/components/dynamic-theme"; import { RegisterU2f } from "@/components/register-u2f"; import { UserAvatar } from "@/components/user-avatar"; +import { getServiceUrlFromHeaders } from "@/lib/service"; import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings } from "@/lib/zitadel"; import { getLocale, getTranslations } from "next-intl/server"; +import { headers } from "next/headers"; export default async function Page(props: { searchParams: Promise>; @@ -16,12 +18,23 @@ export default async function Page(props: { const { loginName, organization, authRequestId, checkAfter } = searchParams; + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + const sessionFactors = await loadMostRecentSession({ - loginName, - organization, + serviceUrl, + serviceRegion, + sessionParams: { + loginName, + organization, + }, }); - const branding = await getBrandingSettings(organization); + const branding = await getBrandingSettings({ + serviceUrl, + serviceRegion, + organization, + }); return ( diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index 128623963b..628d07f36f 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -4,6 +4,7 @@ import { UserAvatar } from "@/components/user-avatar"; import { VerifyForm } from "@/components/verify-form"; import { VerifyRedirectButton } from "@/components/verify-redirect-button"; import { sendEmailCode } from "@/lib/server/verify"; +import { getServiceUrlFromHeaders } from "@/lib/service"; import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings, @@ -13,6 +14,7 @@ import { 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: { searchParams: Promise }) { const searchParams = await props.searchParams; @@ -23,7 +25,19 @@ export default async function Page(props: { searchParams: Promise }) { const { userId, loginName, code, organization, authRequestId, invite } = searchParams; - const branding = await getBrandingSettings(organization); + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + + if (!host || typeof host !== "string") { + throw new Error("No host found"); + } + + const branding = await getBrandingSettings({ + serviceUrl, + serviceRegion, + organization, + }); let sessionFactors; let user: User | undefined; @@ -34,14 +48,22 @@ export default async function Page(props: { searchParams: Promise }) { if ("loginName" in searchParams) { sessionFactors = await loadMostRecentSession({ - loginName, - organization, + serviceUrl, + serviceRegion, + sessionParams: { + loginName, + organization, + }, }); if (doSend && sessionFactors?.factors?.user?.id) { await sendEmailCode({ + serviceUrl, + serviceRegion, userId: sessionFactors?.factors?.user?.id, - authRequestId, + urlTemplate: + `${host.includes("localhost") ? "http://" : "https://"}${host}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` + + (authRequestId ? `&authRequestId=${authRequestId}` : ""), }).catch((error) => { console.error("Could not resend verification email", error); throw Error("Failed to send verification email"); @@ -50,15 +72,23 @@ export default async function Page(props: { searchParams: Promise }) { } else if ("userId" in searchParams && userId) { if (doSend) { await sendEmailCode({ + serviceUrl, + serviceRegion, userId, - authRequestId, + urlTemplate: + `${host.includes("localhost") ? "http://" : "https://"}${host}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` + + (authRequestId ? `&authRequestId=${authRequestId}` : ""), }).catch((error) => { console.error("Could not resend verification email", error); throw Error("Failed to send verification email"); }); } - const userResponse = await getUserByID(userId); + const userResponse = await getUserByID({ + serviceUrl, + serviceRegion, + userId, + }); if (userResponse) { user = userResponse.user; if (user?.type.case === "human") { diff --git a/apps/login/src/app/login/route.ts b/apps/login/src/app/login/route.ts index f5741bb39c..2cea4b3b0b 100644 --- a/apps/login/src/app/login/route.ts +++ b/apps/login/src/app/login/route.ts @@ -1,6 +1,7 @@ import { getAllSessions } from "@/lib/cookies"; import { idpTypeToSlug } from "@/lib/idp"; import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname"; +import { getServiceUrlFromHeaders } from "@/lib/service"; import { createCallback, getActiveIdentityProviders, @@ -22,16 +23,27 @@ import { } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { headers } from "next/headers"; import { NextRequest, NextResponse } from "next/server"; export const dynamic = "force-dynamic"; export const revalidate = false; export const fetchCache = "default-no-store"; -async function loadSessions(ids: string[]): Promise { - const response = await listSessions( - ids.filter((id: string | undefined) => !!id), - ); +async function loadSessions({ + serviceUrl, + serviceRegion, + ids, +}: { + serviceUrl: string; + serviceRegion: string; + ids: string[]; +}): Promise { + const response = await listSessions({ + serviceUrl, + serviceRegion, + ids: ids.filter((id: string | undefined) => !!id), + }); return response?.sessions ?? []; } @@ -44,7 +56,11 @@ const IDP_SCOPE_REGEX = /urn:zitadel:iam:org:idp:id:(.+)/; * mfa is required, session is not valid anymore (e.g. session expired, user logged out, etc.) * to check for mfa for automatically selected session -> const response = await listAuthenticationMethodTypes(userId); **/ -async function isSessionValid(session: Session): Promise { +async function isSessionValid( + serviceUrl: string, + serviceRegion: string, + session: Session, +): Promise { // session can't be checked without user if (!session.factors?.user) { console.warn("Session has no user"); @@ -53,9 +69,11 @@ async function isSessionValid(session: Session): Promise { let mfaValid = true; - const authMethodTypes = await listAuthenticationMethodTypes( - session.factors.user.id, - ); + const authMethodTypes = await listAuthenticationMethodTypes({ + serviceUrl, + serviceRegion, + userId: session.factors.user.id, + }); const authMethods = authMethodTypes.authMethodTypes; if (authMethods && authMethods.includes(AuthenticationMethodType.TOTP)) { @@ -101,9 +119,11 @@ async function isSessionValid(session: Session): Promise { } } else { // only check settings if no auth methods are available, as this would require a setup - const loginSettings = await getLoginSettings( - session.factors?.user?.organizationId, - ); + const loginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization: session.factors?.user?.organizationId, + }); if (loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly) { const otpEmail = session.factors.otpEmail?.verifiedAt; const otpSms = session.factors.otpSms?.verifiedAt; @@ -144,6 +164,8 @@ async function isSessionValid(session: Session): Promise { } async function findValidSession( + serviceUrl: string, + serviceRegion: string, sessions: Session[], authRequest: AuthRequest, ): Promise { @@ -170,7 +192,7 @@ async function findValidSession( // return the first valid session according to settings for (const session of sessionsWithHint) { - if (await isSessionValid(session)) { + if (await isSessionValid(serviceUrl, serviceRegion, session)) { return session; } } @@ -183,6 +205,9 @@ export async function GET(request: NextRequest) { const authRequestId = searchParams.get("authRequest"); const sessionId = searchParams.get("sessionId"); + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + // TODO: find a better way to handle _rsc (react server components) requests and block them to avoid conflicts when creating oidc callback const _rsc = searchParams.get("_rsc"); if (_rsc) { @@ -193,7 +218,7 @@ export async function GET(request: NextRequest) { const ids = sessionCookies.map((s) => s.id); let sessions: Session[] = []; if (ids && ids.length) { - sessions = await loadSessions(ids); + sessions = await loadSessions({ serviceUrl, serviceRegion, ids }); } if (authRequestId && sessionId) { @@ -206,7 +231,11 @@ export async function GET(request: NextRequest) { if (selectedSession && selectedSession.id) { console.log(`Found session ${selectedSession.id}`); - const isValid = await isSessionValid(selectedSession); + const isValid = await isSessionValid( + serviceUrl, + serviceRegion, + selectedSession, + ); console.log("Session is valid:", isValid); @@ -239,15 +268,17 @@ export async function GET(request: NextRequest) { // works not with _rsc request try { - const { callbackUrl } = await createCallback( - create(CreateCallbackRequestSchema, { + const { callbackUrl } = await createCallback({ + serviceUrl, + serviceRegion, + req: create(CreateCallbackRequestSchema, { authRequestId, callbackKind: { case: "session", value: create(SessionSchema, session), }, }), - ); + }); if (callbackUrl) { return NextResponse.redirect(callbackUrl); } else { @@ -265,9 +296,11 @@ export async function GET(request: NextRequest) { "code" in error && error?.code === 9 ) { - const loginSettings = await getLoginSettings( - selectedSession.factors?.user?.organizationId, - ); + const loginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization: selectedSession.factors?.user?.organizationId, + }); if (loginSettings?.defaultRedirectUri) { return NextResponse.redirect(loginSettings.defaultRedirectUri); @@ -297,7 +330,11 @@ export async function GET(request: NextRequest) { } if (authRequestId) { - const { authRequest } = await getAuthRequest({ authRequestId }); + const { authRequest } = await getAuthRequest({ + serviceUrl, + serviceRegion, + authRequestId, + }); let organization = ""; let suffix = ""; @@ -324,7 +361,11 @@ export async function GET(request: NextRequest) { const matched = ORG_DOMAIN_SCOPE_REGEX.exec(orgDomainScope); const orgDomain = matched?.[1] ?? ""; if (orgDomain) { - const orgs = await getOrgsByDomain(orgDomain); + const orgs = await getOrgsByDomain({ + serviceUrl, + serviceRegion, + domain: orgDomain, + }); if (orgs.result && orgs.result.length === 1) { organization = orgs.result[0].id ?? ""; suffix = orgDomain; @@ -337,9 +378,11 @@ export async function GET(request: NextRequest) { const matched = IDP_SCOPE_REGEX.exec(idpScope); idpId = matched?.[1] ?? ""; - const identityProviders = await getActiveIdentityProviders( - organization ? organization : undefined, - ).then((resp) => { + const identityProviders = await getActiveIdentityProviders({ + serviceUrl, + serviceRegion, + orgId: organization ? organization : undefined, + }).then((resp) => { return resp.identityProviders; }); @@ -362,6 +405,8 @@ export async function GET(request: NextRequest) { } return startIdentityProviderFlow({ + serviceUrl, + serviceRegion, idpId, urls: { successUrl: @@ -460,7 +505,12 @@ export async function GET(request: NextRequest) { * This means that the user should not be prompted to enter their password again. * Instead, the server attempts to silently authenticate the user using an existing session or other authentication mechanisms that do not require user interaction **/ - const selectedSession = await findValidSession(sessions, authRequest); + const selectedSession = await findValidSession( + serviceUrl, + serviceRegion, + sessions, + authRequest, + ); if (!selectedSession || !selectedSession.id) { return NextResponse.json( @@ -485,19 +535,26 @@ export async function GET(request: NextRequest) { sessionToken: cookie.token, }; - const { callbackUrl } = await createCallback( - create(CreateCallbackRequestSchema, { + const { callbackUrl } = await createCallback({ + serviceUrl, + serviceRegion, + req: create(CreateCallbackRequestSchema, { authRequestId, callbackKind: { case: "session", value: create(SessionSchema, session), }, }), - ); + }); return NextResponse.redirect(callbackUrl); } else { // check for loginHint, userId hint and valid sessions - let selectedSession = await findValidSession(sessions, authRequest); + let selectedSession = await findValidSession( + serviceUrl, + serviceRegion, + sessions, + authRequest, + ); if (!selectedSession || !selectedSession.id) { return gotoAccounts(); @@ -517,15 +574,17 @@ export async function GET(request: NextRequest) { }; try { - const { callbackUrl } = await createCallback( - create(CreateCallbackRequestSchema, { + const { callbackUrl } = await createCallback({ + serviceUrl, + serviceRegion, + req: create(CreateCallbackRequestSchema, { authRequestId, callbackKind: { case: "session", value: create(SessionSchema, session), }, }), - ); + }); if (callbackUrl) { return NextResponse.redirect(callbackUrl); } else { diff --git a/apps/login/src/components/login-otp.tsx b/apps/login/src/components/login-otp.tsx index 21d895e370..c5be74d252 100644 --- a/apps/login/src/components/login-otp.tsx +++ b/apps/login/src/components/login-otp.tsx @@ -18,6 +18,7 @@ import { Spinner } from "./spinner"; // either loginName or sessionId must be provided type Props = { + host: string | null; loginName?: string; sessionId?: string; authRequestId?: string; @@ -25,7 +26,6 @@ type Props = { method: string; code?: string; loginSettings?: LoginSettings; - host: string | null; }; type Inputs = { @@ -33,6 +33,7 @@ type Inputs = { }; export function LoginOTP({ + host, loginName, sessionId, authRequestId, @@ -40,7 +41,6 @@ export function LoginOTP({ method, code, loginSettings, - host, }: Props) { const t = useTranslations("otp"); diff --git a/apps/login/src/components/login-passkey.tsx b/apps/login/src/components/login-passkey.tsx index 5e05cdb6a8..a5beae7396 100644 --- a/apps/login/src/components/login-passkey.tsx +++ b/apps/login/src/components/login-passkey.tsx @@ -9,7 +9,6 @@ import { UserVerificationRequirement, } from "@zitadel/proto/zitadel/session/v2/challenge_pb"; import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; -import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; import { useEffect, useRef, useState } from "react"; @@ -26,7 +25,6 @@ type Props = { altPassword: boolean; login?: boolean; organization?: string; - loginSettings?: LoginSettings; }; export function LoginPasskey({ @@ -36,7 +34,6 @@ export function LoginPasskey({ altPassword, organization, login = true, - loginSettings, }: Props) { const t = useTranslations("passkey"); @@ -47,7 +44,6 @@ export function LoginPasskey({ const initialized = useRef(false); - // TODO: move this to server side useEffect(() => { if (!initialized.current) { initialized.current = true; diff --git a/apps/login/src/components/totp-register.tsx b/apps/login/src/components/totp-register.tsx index e3b5985aea..40aa94a165 100644 --- a/apps/login/src/components/totp-register.tsx +++ b/apps/login/src/components/totp-register.tsx @@ -1,6 +1,7 @@ "use client"; + import { getNextUrl } from "@/lib/client"; -import { verifyTOTP } from "@/lib/server-actions"; +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"; diff --git a/apps/login/src/lib/api.ts b/apps/login/src/lib/api.ts new file mode 100644 index 0000000000..15e85fd2b6 --- /dev/null +++ b/apps/login/src/lib/api.ts @@ -0,0 +1,40 @@ +import { newSystemToken } from "@zitadel/client/node"; + +export async function systemAPIToken({ + serviceRegion, +}: { + serviceRegion: string; +}) { + const prefix = serviceRegion.toUpperCase(); + const token = { + audience: process.env[prefix + "_AUDIENCE"], + userID: process.env[prefix + "_SYSTEM_USER_ID"], + token: Buffer.from( + process.env[prefix.toUpperCase() + "_SYSTEM_USER_PRIVATE_KEY"] as string, + "base64", + ).toString("utf-8"), + }; + + if (!token.audience || !token.userID || !token.token) { + const fallbackToken = { + audience: process.env.AUDIENCE, + userID: process.env.SYSTEM_USER_ID, + token: Buffer.from( + process.env.SYSTEM_USER_PRIVATE_KEY, + "base64", + ).toString("utf-8"), + }; + + return newSystemToken({ + audience: fallbackToken.audience, + subject: fallbackToken.userID, + key: fallbackToken.token, + }); + } + + return newSystemToken({ + audience: token.audience, + subject: token.userID, + key: token.token, + }); +} diff --git a/apps/login/src/lib/self.ts b/apps/login/src/lib/self.ts index 0328adfaff..178583fb40 100644 --- a/apps/login/src/lib/self.ts +++ b/apps/login/src/lib/self.ts @@ -2,16 +2,20 @@ import { createServerTransport } from "@zitadel/client/node"; import { createUserServiceClient } from "@zitadel/client/v2"; +import { headers } from "next/headers"; import { getSessionCookieById } from "./cookies"; +import { getServiceUrlFromHeaders } from "./service"; import { getSession } from "./zitadel"; -const transport = (token: string) => - createServerTransport(token, { - baseUrl: process.env.ZITADEL_API_URL!, +const transport = async (serviceUrl: string, token: string) => { + return createServerTransport(token, { + baseUrl: serviceUrl, }); +}; -const myUserService = (sessionToken: string) => { - return createUserServiceClient(transport(sessionToken)); +const myUserService = async (serviceUrl: string, sessionToken: string) => { + const transportPromise = await transport(serviceUrl, sessionToken); + return createUserServiceClient(transportPromise); }; export async function setMyPassword({ @@ -21,9 +25,14 @@ export async function setMyPassword({ sessionId: string; password: string; }) { + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + const sessionCookie = await getSessionCookieById({ sessionId }); const { session } = await getSession({ + serviceUrl, + serviceRegion, sessionId: sessionCookie.id, sessionToken: sessionCookie.token, }); @@ -32,7 +41,7 @@ export async function setMyPassword({ return { error: "Could not load session" }; } - const service = await myUserService(`${sessionCookie.token}`); + const service = await myUserService(serviceUrl, `${sessionCookie.token}`); if (!session?.factors?.user?.id) { return { error: "No user id found in session" }; diff --git a/apps/login/src/lib/server-actions.ts b/apps/login/src/lib/server-actions.ts deleted file mode 100644 index ce0726075f..0000000000 --- a/apps/login/src/lib/server-actions.ts +++ /dev/null @@ -1,21 +0,0 @@ -"use server"; - -import { loadMostRecentSession } from "./session"; -import { verifyTOTPRegistration } from "./zitadel"; - -export async function verifyTOTP( - code: string, - loginName?: string, - organization?: string, -) { - return loadMostRecentSession({ - loginName, - organization, - }).then((session) => { - if (session?.factors?.user?.id) { - return verifyTOTPRegistration(code, session.factors.user.id); - } else { - throw Error("No user id found in session."); - } - }); -} diff --git a/apps/login/src/lib/server/cookie.ts b/apps/login/src/lib/server/cookie.ts index 91447174f6..03a421674d 100644 --- a/apps/login/src/lib/server/cookie.ts +++ b/apps/login/src/lib/server/cookie.ts @@ -7,13 +7,20 @@ import { getSession, setSession, } from "@/lib/zitadel"; -import { Duration, timestampMs } from "@zitadel/client"; +import { ConnectError, Duration, timestampMs } from "@zitadel/client"; +import { + CredentialsCheckError, + CredentialsCheckErrorSchema, + ErrorDetail, +} from "@zitadel/proto/zitadel/message_pb"; import { Challenges, RequestChallenges, } from "@zitadel/proto/zitadel/session/v2/challenge_pb"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { headers } from "next/headers"; +import { getServiceUrlFromHeaders } from "../service"; type CustomCookieData = { id: string; @@ -26,16 +33,40 @@ type CustomCookieData = { authRequestId?: string; // if its linked to an OIDC flow }; +const passwordAttemptsHandler = (error: ConnectError) => { + const details = error.findDetails(CredentialsCheckErrorSchema); + + if (details[0] && "failedAttempts" in details[0]) { + const failedAttempts = details[0].failedAttempts; + throw { + error: `Failed to authenticate: You had ${failedAttempts} password attempts.`, + failedAttempts: failedAttempts, + }; + } + throw error; +}; + export async function createSessionAndUpdateCookie( checks: Checks, challenges: RequestChallenges | undefined, authRequestId: string | undefined, lifetime?: Duration, ): Promise { - const createdSession = await createSessionFromChecks(checks, challenges); + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + + const createdSession = await createSessionFromChecks({ + serviceUrl, + serviceRegion, + checks, + challenges, + lifetime, + }); if (createdSession) { return getSession({ + serviceUrl, + serviceRegion, sessionId: createdSession.sessionId, sessionToken: createdSession.sessionToken, }).then((response) => { @@ -85,17 +116,33 @@ export async function createSessionForIdpAndUpdateCookie( authRequestId: string | undefined, lifetime?: Duration, ): Promise { - const createdSession = await createSessionForUserIdAndIdpIntent( + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + + const createdSession = await createSessionForUserIdAndIdpIntent({ + serviceUrl, + serviceRegion, userId, idpIntent, lifetime, - ); + }).catch((error: ErrorDetail | CredentialsCheckError) => { + console.error("Could not set session", error); + if ("failedAttempts" in error && error.failedAttempts) { + throw { + error: `Failed to authenticate: You had ${error.failedAttempts} password attempts.`, + failedAttempts: error.failedAttempts, + }; + } + throw error; + }); if (!createdSession) { throw "Could not create session"; } const { session } = await getSession({ + serviceUrl, + serviceRegion, sessionId: createdSession.sessionId, sessionToken: createdSession.sessionToken, }); @@ -142,63 +189,72 @@ export async function setSessionAndUpdateCookie( authRequestId?: string, lifetime?: Duration, ) { - return setSession( - recentCookie.id, - recentCookie.token, + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + + return setSession({ + serviceUrl, + serviceRegion, + sessionId: recentCookie.id, + sessionToken: recentCookie.token, challenges, checks, lifetime, - ).then((updatedSession) => { - if (updatedSession) { - const sessionCookie: CustomCookieData = { - id: recentCookie.id, - token: updatedSession.sessionToken, - creationTs: recentCookie.creationTs, - expirationTs: recentCookie.expirationTs, - // just overwrite the changeDate with the new one - changeTs: updatedSession.details?.changeDate - ? `${timestampMs(updatedSession.details.changeDate)}` - : "", - loginName: recentCookie.loginName, - organization: recentCookie.organization, - }; + }) + .then((updatedSession) => { + if (updatedSession) { + const sessionCookie: CustomCookieData = { + id: recentCookie.id, + token: updatedSession.sessionToken, + creationTs: recentCookie.creationTs, + expirationTs: recentCookie.expirationTs, + // just overwrite the changeDate with the new one + changeTs: updatedSession.details?.changeDate + ? `${timestampMs(updatedSession.details.changeDate)}` + : "", + loginName: recentCookie.loginName, + organization: recentCookie.organization, + }; - if (authRequestId) { - sessionCookie.authRequestId = authRequestId; - } - - return getSession({ - sessionId: sessionCookie.id, - sessionToken: sessionCookie.token, - }).then((response) => { - if (response?.session && response.session.factors?.user?.loginName) { - const { session } = response; - const newCookie: CustomCookieData = { - id: sessionCookie.id, - token: updatedSession.sessionToken, - creationTs: sessionCookie.creationTs, - expirationTs: sessionCookie.expirationTs, - // just overwrite the changeDate with the new one - changeTs: updatedSession.details?.changeDate - ? `${timestampMs(updatedSession.details.changeDate)}` - : "", - loginName: session.factors?.user?.loginName ?? "", - organization: session.factors?.user?.organizationId ?? "", - }; - - if (sessionCookie.authRequestId) { - newCookie.authRequestId = sessionCookie.authRequestId; - } - - return updateSessionCookie(sessionCookie.id, newCookie).then(() => { - return { challenges: updatedSession.challenges, ...session }; - }); - } else { - throw "could not get session or session does not have loginName"; + if (authRequestId) { + sessionCookie.authRequestId = authRequestId; } - }); - } else { - throw "Session not be set"; - } - }); + + return getSession({ + serviceUrl, + serviceRegion, + sessionId: sessionCookie.id, + sessionToken: sessionCookie.token, + }).then((response) => { + if (response?.session && response.session.factors?.user?.loginName) { + const { session } = response; + const newCookie: CustomCookieData = { + id: sessionCookie.id, + token: updatedSession.sessionToken, + creationTs: sessionCookie.creationTs, + expirationTs: sessionCookie.expirationTs, + // just overwrite the changeDate with the new one + changeTs: updatedSession.details?.changeDate + ? `${timestampMs(updatedSession.details.changeDate)}` + : "", + loginName: session.factors?.user?.loginName ?? "", + organization: session.factors?.user?.organizationId ?? "", + }; + + if (sessionCookie.authRequestId) { + newCookie.authRequestId = sessionCookie.authRequestId; + } + + return updateSessionCookie(sessionCookie.id, newCookie).then(() => { + return { challenges: updatedSession.challenges, ...session }; + }); + } else { + throw "could not get session or session does not have loginName"; + } + }); + } else { + throw "Session not be set"; + } + }) + .catch(passwordAttemptsHandler); } diff --git a/apps/login/src/lib/server/idp.ts b/apps/login/src/lib/server/idp.ts index b48f796160..c12f518fd3 100644 --- a/apps/login/src/lib/server/idp.ts +++ b/apps/login/src/lib/server/idp.ts @@ -7,6 +7,7 @@ import { } from "@/lib/zitadel"; import { headers } from "next/headers"; import { getNextUrl } from "../client"; +import { getServiceUrlFromHeaders } from "../service"; import { checkEmailVerification } from "../verify-helper"; import { createSessionForIdpAndUpdateCookie } from "./cookie"; @@ -17,13 +18,17 @@ export type StartIDPFlowCommand = { }; export async function startIDPFlow(command: StartIDPFlowCommand) { - const host = (await headers()).get("host"); + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); if (!host) { return { error: "Could not get host" }; } return startIdentityProviderFlow({ + serviceUrl, + serviceRegion, idpId: command.idpId, urls: { successUrl: `${host.includes("localhost") ? "http://" : "https://"}${host}${command.successUrl}`, @@ -55,19 +60,33 @@ type CreateNewSessionCommand = { export async function createNewSessionFromIdpIntent( command: CreateNewSessionCommand, ) { + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + + if (!host) { + return { error: "Could not get domain" }; + } + if (!command.userId || !command.idpIntent) { throw new Error("No userId or loginName provided"); } - const userResponse = await getUserByID(command.userId); + const userResponse = await getUserByID({ + serviceUrl, + serviceRegion, + userId: command.userId, + }); if (!userResponse || !userResponse.user) { return { error: "User not found in the system" }; } - const loginSettings = await getLoginSettings( - userResponse.user.details?.resourceOwner, - ); + const loginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization: userResponse.user.details?.resourceOwner, + }); const session = await createSessionForIdpAndUpdateCookie( command.userId, diff --git a/apps/login/src/lib/server/invite.ts b/apps/login/src/lib/server/invite.ts index 3c68587898..864c91540e 100644 --- a/apps/login/src/lib/server/invite.ts +++ b/apps/login/src/lib/server/invite.ts @@ -3,6 +3,7 @@ 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"; type InviteUserCommand = { email: string; @@ -20,9 +21,17 @@ export type RegisterUserResponse = { }; export async function inviteUser(command: InviteUserCommand) { - const host = (await headers()).get("host"); + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + + if (!host) { + return { error: "Could not get domain" }; + } const human = await addHumanUser({ + serviceUrl, + serviceRegion, email: command.email, firstName: command.firstName, lastName: command.lastName, @@ -34,7 +43,12 @@ export async function inviteUser(command: InviteUserCommand) { return { error: "Could not create user" }; } - const codeResponse = await createInviteCode(human.userId, host); + const codeResponse = await createInviteCode({ + serviceUrl, + serviceRegion, + urlTemplate: `${host.includes("localhost") ? "http://" : "https://"}${host}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true`, + userId: human.userId, + }); if (!codeResponse || !human) { return { error: "Could not create invite code" }; diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index 65acb80bf0..18070ab76c 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -8,6 +8,7 @@ import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp"; import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; +import { getServiceUrlFromHeaders } from "../service"; import { checkInvite } from "../verify-helper"; import { getActiveIdentityProviders, @@ -32,13 +33,27 @@ export type SendLoginnameCommand = { const ORG_SUFFIX_REGEX = /(?<=@)(.+)/; export async function sendLoginname(command: SendLoginnameCommand) { - const loginSettingsByContext = await getLoginSettings(command.organization); + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + + if (!host) { + throw new Error("Could not get domain"); + } + + const loginSettingsByContext = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization: command.organization, + }); if (!loginSettingsByContext) { return { error: "Could not get login settings" }; } let searchUsersRequest: SearchUsersCommand = { + serviceUrl, + serviceRegion, searchValue: command.loginName, organizationId: command.organization, loginSettings: loginSettingsByContext, @@ -58,14 +73,18 @@ export async function sendLoginname(command: SendLoginnameCommand) { const { result: potentialUsers } = searchResult; const redirectUserToSingleIDPIfAvailable = async () => { - const identityProviders = await getActiveIdentityProviders( - command.organization, - ).then((resp) => { + const identityProviders = await getActiveIdentityProviders({ + serviceUrl, + serviceRegion, + orgId: command.organization, + }).then((resp) => { return resp.identityProviders; }); if (identityProviders.length === 1) { - const host = (await headers()).get("host"); + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); if (!host) { return { error: "Could not get host" }; @@ -86,6 +105,8 @@ export async function sendLoginname(command: SendLoginnameCommand) { } const resp = await startIdentityProviderFlow({ + serviceUrl, + serviceRegion, idpId: identityProviders[0].id, urls: { successUrl: @@ -104,12 +125,18 @@ export async function sendLoginname(command: SendLoginnameCommand) { }; const redirectUserToIDP = async (userId: string) => { - const identityProviders = await listIDPLinks(userId).then((resp) => { + const identityProviders = await listIDPLinks({ + serviceUrl, + serviceRegion, + userId, + }).then((resp) => { return resp.result; }); if (identityProviders.length === 1) { - const host = (await headers()).get("host"); + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); if (!host) { return { error: "Could not get host" }; @@ -117,7 +144,11 @@ export async function sendLoginname(command: SendLoginnameCommand) { const identityProviderId = identityProviders[0].idpId; - const idp = await getIDPByID(identityProviderId); + const idp = await getIDPByID({ + serviceUrl, + serviceRegion, + id: identityProviderId, + }); const idpType = idp?.type; @@ -139,6 +170,8 @@ export async function sendLoginname(command: SendLoginnameCommand) { } const resp = await startIdentityProviderFlow({ + serviceUrl, + serviceRegion, idpId: idp.id, urls: { successUrl: @@ -162,9 +195,11 @@ export async function sendLoginname(command: SendLoginnameCommand) { const user = potentialUsers[0]; const userId = potentialUsers[0].userId; - const userLoginSettings = await getLoginSettings( - user.details?.resourceOwner, - ); + const userLoginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization: user.details?.resourceOwner, + }); // compare with the concatenated suffix when set const concatLoginname = command.suffix @@ -219,9 +254,11 @@ export async function sendLoginname(command: SendLoginnameCommand) { return { error: "Initial User not supported" }; } - const methods = await listAuthenticationMethodTypes( - session.factors?.user?.id, - ); + const methods = await listAuthenticationMethodTypes({ + serviceUrl, + serviceRegion, + userId: session.factors?.user?.id, + }); // this can be expected to be an invite as users created in console have a password set. if (!methods.authMethodTypes || !methods.authMethodTypes.length) { @@ -376,11 +413,19 @@ export async function sendLoginname(command: SendLoginnameCommand) { const suffix = matched?.[1] ?? ""; // this just returns orgs where the suffix is set as primary domain - const orgs = await getOrgsByDomain(suffix); + const orgs = await getOrgsByDomain({ + serviceUrl, + serviceRegion, + domain: suffix, + }); const orgToCheckForDiscovery = orgs.result && orgs.result.length === 1 ? orgs.result[0].id : undefined; - const orgLoginSettings = await getLoginSettings(orgToCheckForDiscovery); + const orgLoginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization: orgToCheckForDiscovery, + }); if (orgLoginSettings?.allowDomainDiscovery) { orgToRegisterOn = orgToCheckForDiscovery; } diff --git a/apps/login/src/lib/server/otp.ts b/apps/login/src/lib/server/otp.ts index b91d8eac7c..6d56d0c538 100644 --- a/apps/login/src/lib/server/otp.ts +++ b/apps/login/src/lib/server/otp.ts @@ -7,11 +7,13 @@ import { 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"; import { getLoginSettings } from "../zitadel"; export type SetOTPCommand = { @@ -24,6 +26,9 @@ export type SetOTPCommand = { }; export async function setOTP(command: SetOTPCommand) { + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + const recentSession = command.sessionId ? await getSessionCookieById({ sessionId: command.sessionId }).catch( (error) => { @@ -57,7 +62,11 @@ export async function setOTP(command: SetOTPCommand) { }); } - const loginSettings = await getLoginSettings(command.organization); + const loginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization: command.organization, + }); return setSessionAndUpdateCookie( recentSession, diff --git a/apps/login/src/lib/server/passkeys.ts b/apps/login/src/lib/server/passkeys.ts index c21076265c..819f319bd4 100644 --- a/apps/login/src/lib/server/passkeys.ts +++ b/apps/login/src/lib/server/passkeys.ts @@ -22,6 +22,7 @@ import { getSessionCookieById, getSessionCookieByLoginName, } from "../cookies"; +import { getServiceUrlFromHeaders } from "../service"; import { checkEmailVerification } from "../verify-helper"; import { setSessionAndUpdateCookie } from "./cookie"; @@ -41,18 +42,22 @@ export async function registerPasskeyLink( ): Promise { const { sessionId } = command; - const sessionCookie = await getSessionCookieById({ sessionId }); - const session = await getSession({ - sessionId: sessionCookie.id, - sessionToken: sessionCookie.token, - }); - - const host = (await headers()).get("host"); + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); if (!host) { throw new Error("Could not get domain"); } + const sessionCookie = await getSessionCookieById({ sessionId }); + const session = await getSession({ + serviceUrl, + serviceRegion, + sessionId: sessionCookie.id, + sessionToken: sessionCookie.token, + }); + const [hostname, port] = host.split(":"); if (!hostname) { @@ -67,19 +72,29 @@ export async function registerPasskeyLink( // TODO: add org context // use session token to add the passkey - const registerLink = await createPasskeyRegistrationLink( + const registerLink = await createPasskeyRegistrationLink({ + serviceUrl, + serviceRegion, userId, - // sessionCookie.token, - ); + }); if (!registerLink.code) { throw new Error("Missing code in response"); } - return registerPasskey(userId, registerLink.code, hostname); + return registerPasskey({ + serviceUrl, + serviceRegion, + userId, + code: registerLink.code, + domain: hostname, + }); } export async function verifyPasskeyRegistration(command: VerifyPasskeyCommand) { + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + // if no name is provided, try to generate one from the user agent let passkeyName = command.passkeyName; if (!!!passkeyName) { @@ -96,6 +111,8 @@ export async function verifyPasskeyRegistration(command: VerifyPasskeyCommand) { sessionId: command.sessionId, }); const session = await getSession({ + serviceUrl, + serviceRegion, sessionId: sessionCookie.id, sessionToken: sessionCookie.token, }); @@ -105,14 +122,16 @@ export async function verifyPasskeyRegistration(command: VerifyPasskeyCommand) { throw new Error("Could not get session"); } - return zitadelVerifyPasskeyRegistration( - create(VerifyPasskeyRegistrationRequestSchema, { + return zitadelVerifyPasskeyRegistration({ + serviceUrl, + serviceRegion, + request: create(VerifyPasskeyRegistrationRequestSchema, { passkeyId: command.passkeyId, publicKeyCredential: command.publicKeyCredential, passkeyName, userId, }), - ); + }); } type SendPasskeyCommand = { @@ -138,13 +157,14 @@ export async function sendPasskey(command: SendPasskeyCommand) { }; } - const host = (await headers()).get("host"); + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); - if (!host) { - return { error: "Could not get host" }; - } - - const loginSettings = await getLoginSettings(organization); + const loginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization, + }); const lifetime = checks?.webAuthN ? loginSettings?.multiFactorCheckLifetime // TODO different lifetime for webauthn u2f/passkey @@ -164,7 +184,11 @@ export async function sendPasskey(command: SendPasskeyCommand) { return { error: "Could not update session" }; } - const userResponse = await getUserByID(session?.factors?.user?.id); + const userResponse = await getUserByID({ + serviceUrl, + serviceRegion, + userId: session?.factors?.user?.id, + }); if (!userResponse.user) { return { error: "User not found in the system" }; diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index 3b7a24a718..9a464e22d8 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -5,7 +5,9 @@ import { setSessionAndUpdateCookie, } from "@/lib/server/cookie"; import { + getLockoutSettings, getLoginSettings, + getPasswordExpirySettings, getSession, getUserByID, listAuthenticationMethodTypes, @@ -30,6 +32,7 @@ import { import { headers } from "next/headers"; import { getNextUrl } from "../client"; import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies"; +import { getServiceUrlFromHeaders } from "../service"; import { checkEmailVerification, checkMFAFactors, @@ -43,9 +46,17 @@ type ResetPasswordCommand = { }; export async function resetPassword(command: ResetPasswordCommand) { - const host = (await headers()).get("host"); + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + + if (!host || typeof host !== "string") { + throw new Error("No host found"); + } const users = await listUsers({ + serviceUrl, + serviceRegion, loginName: command.loginName, organizationId: command.organization, }); @@ -59,7 +70,14 @@ export async function resetPassword(command: ResetPasswordCommand) { } const userId = users.result[0].userId; - return passwordReset(userId, host, command.authRequestId); + return passwordReset({ + serviceUrl, + serviceRegion, + userId, + urlTemplate: + `${host.includes("localhost") ? "http://" : "https://"}${host}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + + (command.authRequestId ? `&authRequestId=${command.authRequestId}` : ""), + }); } export type UpdateSessionCommand = { @@ -70,6 +88,9 @@ export type UpdateSessionCommand = { }; export async function sendPassword(command: UpdateSessionCommand) { + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + let sessionCookie = await getSessionCookieByLoginName({ loginName: command.loginName, organization: command.organization, @@ -83,6 +104,8 @@ export async function sendPassword(command: UpdateSessionCommand) { if (!sessionCookie) { const users = await listUsers({ + serviceUrl, + serviceRegion, loginName: command.loginName, organizationId: command.organization, }); @@ -95,32 +118,80 @@ export async function sendPassword(command: UpdateSessionCommand) { password: { password: command.checks.password?.password }, }); - loginSettings = await getLoginSettings(command.organization); + loginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization: command.organization, + }); - session = await createSessionAndUpdateCookie( - checks, - undefined, - command.authRequestId, - loginSettings?.passwordCheckLifetime, - ); + try { + session = await createSessionAndUpdateCookie( + checks, + undefined, + command.authRequestId, + loginSettings?.passwordCheckLifetime, + ); + } catch (error: any) { + if ("failedAttempts" in error && error.failedAttempts) { + const lockoutSettings = await getLockoutSettings({ + serviceUrl, + serviceRegion, + orgId: command.organization, + }); + + return { + error: + `Failed to authenticate. You had ${error.failedAttempts} of ${lockoutSettings?.maxPasswordAttempts} password attempts.` + + (lockoutSettings?.maxPasswordAttempts && + error.failedAttempts >= lockoutSettings?.maxPasswordAttempts + ? "Contact your administrator to unlock your account" + : ""), + }; + } + return { error: "Could not create session for user" }; + } } // this is a fake error message to hide that the user does not even exist return { error: "Could not verify password" }; } else { - session = await setSessionAndUpdateCookie( - sessionCookie, - command.checks, - undefined, - command.authRequestId, - loginSettings?.passwordCheckLifetime, - ); + try { + session = await setSessionAndUpdateCookie( + sessionCookie, + command.checks, + undefined, + command.authRequestId, + loginSettings?.passwordCheckLifetime, + ); + } catch (error: any) { + if ("failedAttempts" in error && error.failedAttempts) { + const lockoutSettings = await getLockoutSettings({ + serviceUrl, + serviceRegion, + orgId: command.organization, + }); + + return { + error: + `Failed to authenticate. You had ${error.failedAttempts} of ${lockoutSettings?.maxPasswordAttempts} password attempts.` + + (lockoutSettings?.maxPasswordAttempts && + error.failedAttempts >= lockoutSettings?.maxPasswordAttempts + ? " Contact your administrator to unlock your account" + : ""), + }; + } + throw error; + } if (!session?.factors?.user?.id) { return { error: "Could not create session for user" }; } - const userResponse = await getUserByID(session?.factors?.user?.id); + const userResponse = await getUserByID({ + serviceUrl, + serviceRegion, + userId: session?.factors?.user?.id, + }); if (!userResponse.user) { return { error: "User not found in the system" }; @@ -130,9 +201,12 @@ export async function sendPassword(command: UpdateSessionCommand) { } if (!loginSettings) { - loginSettings = await getLoginSettings( - command.organization ?? session.factors?.user?.organizationId, - ); + loginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization: + command.organization ?? session.factors?.user?.organizationId, + }); } if (!session?.factors?.user?.id || !sessionCookie) { @@ -141,8 +215,15 @@ export async function sendPassword(command: UpdateSessionCommand) { const humanUser = user.type.case === "human" ? user.type.value : undefined; + const expirySettings = await getPasswordExpirySettings({ + serviceUrl, + serviceRegion, + orgId: command.organization ?? session.factors?.user?.organizationId, + }); + // check if the user has to change password first const passwordChangedCheck = checkPasswordChangeRequired( + expirySettings, session, humanUser, command.organization, @@ -173,9 +254,11 @@ export async function sendPassword(command: UpdateSessionCommand) { // if password, check if user has MFA methods let authMethods; if (command.checks && command.checks.password && session.factors?.user?.id) { - const response = await listAuthenticationMethodTypes( - session.factors.user.id, - ); + const response = await listAuthenticationMethodTypes({ + serviceUrl, + serviceRegion, + userId: session.factors.user.id, + }); if (response.authMethodTypes && response.authMethodTypes.length) { authMethods = response.authMethodTypes; } @@ -227,15 +310,29 @@ export async function changePassword(command: { userId: string; password: string; }) { + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + // check for init state - const { user } = await getUserByID(command.userId); + const { user } = await getUserByID({ + serviceUrl, + serviceRegion, + userId: command.userId, + }); if (!user || user.userId !== command.userId) { return { error: "Could not send Password Reset Link" }; } const userId = user.userId; - return setUserPassword(userId, command.password, user, command.code); + return setUserPassword({ + serviceUrl, + serviceRegion, + userId, + password: command.password, + user, + code: command.code, + }); } type CheckSessionAndSetPasswordCommand = { @@ -247,9 +344,14 @@ export async function checkSessionAndSetPassword({ sessionId, password, }: CheckSessionAndSetPasswordCommand) { + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + const sessionCookie = await getSessionCookieById({ sessionId }); const { session } = await getSession({ + serviceUrl, + serviceRegion, sessionId: sessionCookie.id, sessionToken: sessionCookie.token, }); @@ -266,9 +368,11 @@ export async function checkSessionAndSetPassword({ }); // check if the user has no password set in order to set a password - const authmethods = await listAuthenticationMethodTypes( - session.factors.user.id, - ); + const authmethods = await listAuthenticationMethodTypes({ + serviceUrl, + serviceRegion, + userId: session.factors.user.id, + }); if (!authmethods) { return { error: "Could not load auth methods" }; @@ -285,9 +389,11 @@ export async function checkSessionAndSetPassword({ (method) => !authmethods.authMethodTypes.includes(method), ); - const loginSettings = await getLoginSettings( - session.factors.user.organizationId, - ); + const loginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization: session.factors.user.organizationId, + }); const forceMfa = !!( loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly @@ -295,24 +401,32 @@ export async function checkSessionAndSetPassword({ // if the user has no MFA but MFA is enforced, we can set a password otherwise we use the token of the user if (forceMfa && hasNoMFAMethods) { - return setPassword(payload).catch((error) => { - // throw error if failed precondition (ex. User is not yet initialized) - if (error.code === 9 && error.message) { - return { error: "Failed precondition" }; - } else { - throw error; - } - }); + return setPassword({ serviceUrl, serviceRegion, payload }).catch( + (error) => { + // throw error if failed precondition (ex. User is not yet initialized) + if (error.code === 9 && error.message) { + return { error: "Failed precondition" }; + } else { + throw error; + } + }, + ); } else { - const myUserService = (sessionToken: string) => { - return createUserServiceClient( - createServerTransport(sessionToken, { - baseUrl: process.env.ZITADEL_API_URL!, - }), - ); + const transport = async (serviceUrl: string, token: string) => { + return createServerTransport(token, { + baseUrl: serviceUrl, + }); }; - const selfService = await myUserService(`${sessionCookie.token}`); + const myUserService = async (serviceUrl: string, sessionToken: string) => { + const transportPromise = await transport(serviceUrl, sessionToken); + return createUserServiceClient(transportPromise); + }; + + const selfService = await myUserService( + serviceUrl, + `${sessionCookie.token}`, + ); return selfService .setPassword( diff --git a/apps/login/src/lib/server/register.ts b/apps/login/src/lib/server/register.ts index 284689523a..2a23af3073 100644 --- a/apps/login/src/lib/server/register.ts +++ b/apps/login/src/lib/server/register.ts @@ -8,7 +8,9 @@ import { ChecksJson, ChecksSchema, } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { headers } from "next/headers"; import { getNextUrl } from "../client"; +import { getServiceUrlFromHeaders } from "../service"; import { checkEmailVerification } from "../verify-helper"; type RegisterUserCommand = { @@ -26,7 +28,17 @@ export type RegisterUserResponse = { factors: Factors | undefined; }; export async function registerUser(command: RegisterUserCommand) { + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + + if (!host || typeof host !== "string") { + throw new Error("No host found"); + } + const addResponse = await addHumanUser({ + serviceUrl, + serviceRegion, email: command.email, firstName: command.firstName, lastName: command.lastName, @@ -38,7 +50,11 @@ export async function registerUser(command: RegisterUserCommand) { return { error: "Could not create user" }; } - const loginSettings = await getLoginSettings(command.organization); + const loginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization: command.organization, + }); let checkPayload: any = { user: { search: { case: "userId", value: addResponse.userId } }, @@ -76,7 +92,11 @@ export async function registerUser(command: RegisterUserCommand) { return { redirect: "/passkey/set?" + params }; } else { - const userResponse = await getUserByID(session?.factors?.user?.id); + const userResponse = await getUserByID({ + serviceUrl, + serviceRegion, + userId: session?.factors?.user?.id, + }); if (!userResponse.user) { return { error: "User not found in the system" }; diff --git a/apps/login/src/lib/server/session.ts b/apps/login/src/lib/server/session.ts index 70bc18f6d5..7f71ec8f14 100644 --- a/apps/login/src/lib/server/session.ts +++ b/apps/login/src/lib/server/session.ts @@ -18,14 +18,20 @@ import { getSessionCookieByLoginName, removeSessionFromCookie, } from "../cookies"; +import { getServiceUrlFromHeaders } from "../service"; export async function continueWithSession({ authRequestId, ...session }: Session & { authRequestId?: string }) { - const loginSettings = await getLoginSettings( - session.factors?.user?.organizationId, - ); + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + + const loginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization: session.factors?.user?.organizationId, + }); const url = authRequestId && session.id && session.factors?.user @@ -82,7 +88,9 @@ export async function updateSession(options: UpdateSessionCommand) { }; } - const host = (await headers()).get("host"); + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); if (!host) { return { error: "Could not get host" }; @@ -99,7 +107,11 @@ export async function updateSession(options: UpdateSessionCommand) { challenges.webAuthN.domain = hostname; } - const loginSettings = await getLoginSettings(organization); + const loginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization, + }); const lifetime = checks?.webAuthN ? loginSettings?.multiFactorCheckLifetime // TODO different lifetime for webauthn u2f/passkey @@ -122,9 +134,11 @@ export async function updateSession(options: UpdateSessionCommand) { // if password, check if user has MFA methods let authMethods; if (checks && checks.password && session.factors?.user?.id) { - const response = await listAuthenticationMethodTypes( - session.factors.user.id, - ); + const response = await listAuthenticationMethodTypes({ + serviceUrl, + serviceRegion, + userId: session.factors.user.id, + }); if (response.authMethodTypes && response.authMethodTypes.length) { authMethods = response.authMethodTypes; } @@ -143,11 +157,19 @@ type ClearSessionOptions = { }; export async function clearSession(options: ClearSessionOptions) { + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + const { sessionId } = options; const session = await getSessionCookieById({ sessionId }); - const deletedSession = await deleteSession(session.id, session.token); + const deletedSession = await deleteSession({ + serviceUrl, + serviceRegion, + sessionId: session.id, + sessionToken: session.token, + }); if (deletedSession) { return removeSessionFromCookie(session); @@ -159,12 +181,17 @@ type CleanupSessionCommand = { }; export async function cleanupSession({ sessionId }: CleanupSessionCommand) { + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + const sessionCookie = await getSessionCookieById({ sessionId }); - const deleteResponse = await deleteSession( - sessionCookie.id, - sessionCookie.token, - ); + const deleteResponse = await deleteSession({ + serviceUrl, + serviceRegion, + sessionId: sessionCookie.id, + sessionToken: sessionCookie.token, + }); if (!deleteResponse) { throw new Error("Could not delete session"); diff --git a/apps/login/src/lib/server/u2f.ts b/apps/login/src/lib/server/u2f.ts index 5cbd80611b..60f1c12b66 100644 --- a/apps/login/src/lib/server/u2f.ts +++ b/apps/login/src/lib/server/u2f.ts @@ -6,6 +6,7 @@ import { VerifyU2FRegistrationRequestSchema } from "@zitadel/proto/zitadel/user/ import { headers } from "next/headers"; import { userAgent } from "next/server"; import { getSessionCookieById } from "../cookies"; +import { getServiceUrlFromHeaders } from "../service"; type RegisterU2FCommand = { sessionId: string; @@ -19,6 +20,14 @@ type VerifyU2FCommand = { }; export async function addU2F(command: RegisterU2FCommand) { + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + + if (!host || typeof host !== "string") { + throw new Error("No host found"); + } + const sessionCookie = await getSessionCookieById({ sessionId: command.sessionId, }); @@ -28,16 +37,12 @@ export async function addU2F(command: RegisterU2FCommand) { } const session = await getSession({ + serviceUrl, + serviceRegion, sessionId: sessionCookie.id, sessionToken: sessionCookie.token, }); - const host = (await headers()).get("host"); - - if (!host) { - return { error: "Could not get domain" }; - } - const [hostname, port] = host.split(":"); if (!hostname) { @@ -50,10 +55,18 @@ export async function addU2F(command: RegisterU2FCommand) { return { error: "Could not get session" }; } - return registerU2F(userId, hostname); + return registerU2F({ serviceUrl, serviceRegion, userId, domain: hostname }); } export async function verifyU2F(command: VerifyU2FCommand) { + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + + if (!host || typeof host !== "string") { + throw new Error("No host found"); + } + let passkeyName = command.passkeyName; if (!!!passkeyName) { const headersList = await headers(); @@ -69,6 +82,8 @@ export async function verifyU2F(command: VerifyU2FCommand) { }); const session = await getSession({ + serviceUrl, + serviceRegion, sessionId: sessionCookie.id, sessionToken: sessionCookie.token, }); @@ -79,12 +94,12 @@ export async function verifyU2F(command: VerifyU2FCommand) { return { error: "Could not get session" }; } - const req = create(VerifyU2FRegistrationRequestSchema, { + const request = create(VerifyU2FRegistrationRequestSchema, { u2fId: command.u2fId, publicKeyCredential: command.publicKeyCredential, tokenName: passkeyName, userId, }); - return verifyU2FRegistration(req); + return verifyU2FRegistration({ serviceUrl, serviceRegion, request }); } diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts index 2c0c78272a..2ab94a9252 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -9,6 +9,7 @@ import { resendInviteCode, verifyEmail, verifyInviteCode, + verifyTOTPRegistration, sendEmailCode as zitadelSendEmailCode, } from "@/lib/zitadel"; import { create } from "@zitadel/client"; @@ -18,9 +19,40 @@ import { User } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { headers } from "next/headers"; import { getNextUrl } from "../client"; import { getSessionCookieByLoginName } from "../cookies"; +import { getServiceUrlFromHeaders } from "../service"; +import { loadMostRecentSession } from "../session"; import { checkMFAFactors } from "../verify-helper"; import { createSessionAndUpdateCookie } from "./cookie"; +export async function verifyTOTP( + code: string, + loginName?: string, + organization?: string, +) { + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + + return loadMostRecentSession({ + serviceUrl, + serviceRegion, + sessionParams: { + loginName, + organization, + }, + }).then((session) => { + if (session?.factors?.user?.id) { + return verifyTOTPRegistration({ + serviceUrl, + serviceRegion, + code, + userId: session.factors.user.id, + }); + } else { + throw Error("No user id found in session."); + } + }); +} + type VerifyUserByEmailCommand = { userId: string; loginName?: string; // to determine already existing session @@ -31,11 +63,24 @@ type VerifyUserByEmailCommand = { }; export async function sendVerification(command: VerifyUserByEmailCommand) { + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + const verifyResponse = command.isInvite - ? await verifyInviteCode(command.userId, command.code).catch(() => { + ? await verifyInviteCode({ + serviceUrl, + serviceRegion, + userId: command.userId, + verificationCode: command.code, + }).catch(() => { return { error: "Could not verify invite" }; }) - : await verifyEmail(command.userId, command.code).catch(() => { + : await verifyEmail({ + serviceUrl, + serviceRegion, + userId: command.userId, + verificationCode: command.code, + }).catch(() => { return { error: "Could not verify email" }; }); @@ -63,6 +108,8 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { } session = await getSession({ + serviceUrl, + serviceRegion, sessionId: sessionCookie.id, sessionToken: sessionCookie.token, }).then((response) => { @@ -75,7 +122,11 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { return { error: "Could not create session for user" }; } - const userResponse = await getUserByID(session?.factors?.user?.id); + const userResponse = await getUserByID({ + serviceUrl, + serviceRegion, + userId: session?.factors?.user?.id, + }); if (!userResponse?.user) { return { error: "Could not load user" }; @@ -83,7 +134,11 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { user = userResponse.user; } else { - const userResponse = await getUserByID(command.userId); + const userResponse = await getUserByID({ + serviceUrl, + serviceRegion, + userId: command.userId, + }); if (!userResponse || !userResponse.user) { return { error: "Could not load user" }; @@ -119,9 +174,17 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { return { error: "Could not load user" }; } - const loginSettings = await getLoginSettings(user.details?.resourceOwner); + const loginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization: user.details?.resourceOwner, + }); - const authMethodResponse = await listAuthenticationMethodTypes(user.userId); + const authMethodResponse = await listAuthenticationMethodTypes({ + serviceUrl, + serviceRegion, + userId: user.userId, + }); if (!authMethodResponse || !authMethodResponse.authMethodTypes) { return { error: "Could not load possible authenticators" }; @@ -189,21 +252,42 @@ type resendVerifyEmailCommand = { }; export async function resendVerification(command: resendVerifyEmailCommand) { - const host = (await headers()).get("host"); + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + + if (!host) { + return { error: "No host found" }; + } return command.isInvite - ? resendInviteCode(command.userId) - : resendEmailCode(command.userId, host, command.authRequestId); + ? resendInviteCode({ serviceUrl, serviceRegion, userId: command.userId }) + : resendEmailCode({ + userId: command.userId, + serviceUrl, + serviceRegion, + urlTemplate: + `${host.includes("localhost") ? "http://" : "https://"}${host}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + + (command.authRequestId + ? `&authRequestId=${command.authRequestId}` + : ""), + }); } type sendEmailCommand = { + serviceUrl: string; + serviceRegion: string; userId: string; - authRequestId?: string; + urlTemplate: string; }; export async function sendEmailCode(command: sendEmailCommand) { - const host = (await headers()).get("host"); - return zitadelSendEmailCode(command.userId, host, command.authRequestId); + return zitadelSendEmailCode({ + serviceUrl: command.serviceUrl, + serviceRegion: command.serviceRegion, + userId: command.userId, + urlTemplate: command.urlTemplate, + }); } export type SendVerificationRedirectWithoutCheckCommand = { @@ -217,6 +301,9 @@ export type SendVerificationRedirectWithoutCheckCommand = { export async function sendVerificationRedirectWithoutCheck( command: SendVerificationRedirectWithoutCheckCommand, ) { + const _headers = await headers(); + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + if (!("loginName" in command || "userId" in command)) { return { error: "No userId, nor loginname provided" }; } @@ -237,6 +324,8 @@ export async function sendVerificationRedirectWithoutCheck( } session = await getSession({ + serviceUrl, + serviceRegion, sessionId: sessionCookie.id, sessionToken: sessionCookie.token, }).then((response) => { @@ -249,7 +338,11 @@ export async function sendVerificationRedirectWithoutCheck( return { error: "Could not create session for user" }; } - const userResponse = await getUserByID(session?.factors?.user?.id); + const userResponse = await getUserByID({ + serviceUrl, + serviceRegion, + userId: session?.factors?.user?.id, + }); if (!userResponse?.user) { return { error: "Could not load user" }; @@ -257,7 +350,11 @@ export async function sendVerificationRedirectWithoutCheck( user = userResponse.user; } else if ("userId" in command) { - const userResponse = await getUserByID(command.userId); + const userResponse = await getUserByID({ + serviceUrl, + serviceRegion, + userId: command.userId, + }); if (!userResponse?.user) { return { error: "Could not load user" }; @@ -293,7 +390,11 @@ export async function sendVerificationRedirectWithoutCheck( return { error: "Could not load user" }; } - const authMethodResponse = await listAuthenticationMethodTypes(user.userId); + const authMethodResponse = await listAuthenticationMethodTypes({ + serviceUrl, + serviceRegion, + userId: user.userId, + }); if (!authMethodResponse || !authMethodResponse.authMethodTypes) { return { error: "Could not load possible authenticators" }; @@ -315,7 +416,11 @@ export async function sendVerificationRedirectWithoutCheck( return { redirect: `/authenticator/set?${params}` }; } - const loginSettings = await getLoginSettings(user.details?.resourceOwner); + const loginSettings = await getLoginSettings({ + serviceUrl, + serviceRegion, + organization: user.details?.resourceOwner, + }); // redirect to mfa factor if user has one, or redirect to set one up const mfaFactorCheck = checkMFAFactors( diff --git a/apps/login/src/lib/service.ts b/apps/login/src/lib/service.ts new file mode 100644 index 0000000000..e543f35467 --- /dev/null +++ b/apps/login/src/lib/service.ts @@ -0,0 +1,81 @@ +import { createClientFor } from "@zitadel/client"; +import { createServerTransport } from "@zitadel/client/node"; +import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_service_pb"; +import { OIDCService } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; +import { OrganizationService } from "@zitadel/proto/zitadel/org/v2/org_service_pb"; +import { SessionService } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb"; +import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers"; +import { systemAPIToken } from "./api"; + +type ServiceClass = + | typeof IdentityProviderService + | typeof UserService + | typeof OrganizationService + | typeof SessionService + | typeof OIDCService + | typeof SettingsService; + +export async function createServiceForHost( + service: T, + serviceUrl: string, + serviceRegion: string, +) { + let token; + + // if we are running in a multitenancy context, use the system user token + if ( + process.env[serviceRegion + "_AUDIENCE"] && + process.env[serviceRegion + "_SYSTEM_USER_ID"] && + process.env[serviceRegion + "_SYSTEM_USER_PRIVATE_KEY"] + ) { + token = await systemAPIToken({ serviceRegion }); + } else if (process.env.ZITADEL_SERVICE_USER_TOKEN) { + token = process.env.ZITADEL_SERVICE_USER_TOKEN; + } + + if (!serviceUrl) { + throw new Error("No instance url found"); + } + + if (!token) { + throw new Error("No token found"); + } + + const transport = createServerTransport(token, { + baseUrl: serviceUrl, + }); + + return createClientFor(service)(transport); +} + +export function getServiceUrlFromHeaders(headers: ReadonlyHeaders): { + serviceUrl: string; + serviceRegion: string; +} { + let instanceUrl: string = process.env.ZITADEL_API_URL; + + const forwardedHost = headers.get("x-zitadel-forward-host"); + // use the forwarded host if available (multitenant), otherwise fall back to the host of the deployment itself + if (forwardedHost) { + instanceUrl = forwardedHost; + instanceUrl = instanceUrl.startsWith("https://") + ? instanceUrl + : `https://${instanceUrl}`; + } else { + const host = headers.get("host"); + + if (host) { + const [hostname, port] = host.split(":"); + if (hostname !== "localhost") { + instanceUrl = host; + } + } + } + + return { + serviceUrl: instanceUrl, + serviceRegion: headers.get("x-zitadel-region") || "", + }; +} diff --git a/apps/login/src/lib/session.ts b/apps/login/src/lib/session.ts index 29ceb3764b..58fe1d20df 100644 --- a/apps/login/src/lib/session.ts +++ b/apps/login/src/lib/session.ts @@ -1,17 +1,31 @@ import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { GetSessionResponse } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { getMostRecentCookieWithLoginname } from "./cookies"; -import { sessionService } from "./zitadel"; +import { getSession } from "./zitadel"; -export async function loadMostRecentSession(sessionParams: { - loginName?: string; - organization?: string; -}): Promise { +type LoadMostRecentSessionParams = { + serviceUrl: string; + serviceRegion: string; + sessionParams: { + loginName?: string; + organization?: string; + }; +}; + +export async function loadMostRecentSession({ + serviceUrl, + serviceRegion, + sessionParams, +}: LoadMostRecentSessionParams): Promise { const recent = await getMostRecentCookieWithLoginname({ loginName: sessionParams.loginName, organization: sessionParams.organization, }); - return sessionService - .getSession({ sessionId: recent.id, sessionToken: recent.token }, {}) - .then((resp: GetSessionResponse) => resp.session); + + return getSession({ + serviceUrl, + serviceRegion, + sessionId: recent.id, + sessionToken: recent.token, + }).then((resp: GetSessionResponse) => resp.session); } diff --git a/apps/login/src/lib/verify-helper.ts b/apps/login/src/lib/verify-helper.ts index b37287a959..053d1cc71f 100644 --- a/apps/login/src/lib/verify-helper.ts +++ b/apps/login/src/lib/verify-helper.ts @@ -1,15 +1,29 @@ +import { timestampDate } from "@zitadel/client"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { PasswordExpirySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb"; import { HumanUser } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import moment from "moment"; export function checkPasswordChangeRequired( + expirySettings: PasswordExpirySettings | undefined, session: Session, humanUser: HumanUser | undefined, organization?: string, authRequestId?: string, ) { - if (humanUser?.passwordChangeRequired) { + let isOutdated = false; + if (expirySettings?.maxAgeDays && humanUser?.passwordChanged) { + const maxAgeDays = Number(expirySettings.maxAgeDays); // Convert bigint to number + const passwordChangedDate = moment( + timestampDate(humanUser.passwordChanged), + ); + const outdatedPassword = passwordChangedDate.add(maxAgeDays, "days"); + isOutdated = moment().isAfter(outdatedPassword); + } + + if (humanUser?.passwordChangeRequired || isOutdated) { const params = new URLSearchParams({ loginName: session.factors?.user?.loginName as string, }); diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index 835b16a46b..535f4fd4cb 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -1,32 +1,20 @@ -import { createServerTransport } from "@zitadel/client/node"; -import { - createIdpServiceClient, - createOIDCServiceClient, - createOrganizationServiceClient, - createSessionServiceClient, - createSettingsServiceClient, - createUserServiceClient, - makeReqCtx, -} from "@zitadel/client/v2"; -import { RequestChallenges } from "@zitadel/proto/zitadel/session/v2/challenge_pb"; -import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; -import { - AddHumanUserRequest, - ResendEmailCodeRequest, - ResendEmailCodeRequestSchema, - RetrieveIdentityProviderIntentRequest, - SendEmailCodeRequestSchema, - SetPasswordRequest, - SetPasswordRequestSchema, - VerifyPasskeyRegistrationRequest, - VerifyU2FRegistrationRequest, -} from "@zitadel/proto/zitadel/user/v2/user_service_pb"; - -import { create, Duration } from "@zitadel/client"; +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 { CreateCallbackRequest } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; +import { + CreateCallbackRequest, + OIDCService, +} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; +import { OrganizationService } from "@zitadel/proto/zitadel/org/v2/org_service_pb"; +import { RequestChallenges } from "@zitadel/proto/zitadel/session/v2/challenge_pb"; +import { + Checks, + SessionService, +} from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb"; import { SendEmailVerificationCodeSchema } from "@zitadel/proto/zitadel/user/v2/email_pb"; import type { RedirectURLsJson } from "@zitadel/proto/zitadel/user/v2/idp_pb"; import { @@ -42,19 +30,19 @@ import { User, UserState, } from "@zitadel/proto/zitadel/user/v2/user_pb"; +import { + AddHumanUserRequest, + ResendEmailCodeRequest, + ResendEmailCodeRequestSchema, + SendEmailCodeRequestSchema, + SetPasswordRequest, + SetPasswordRequestSchema, + UserService, + VerifyPasskeyRegistrationRequest, + VerifyU2FRegistrationRequest, +} from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { unstable_cacheLife as cacheLife } from "next/cache"; - -const transport = createServerTransport( - process.env.ZITADEL_SERVICE_USER_TOKEN!, - { baseUrl: process.env.ZITADEL_API_URL! }, -); - -export const sessionService = createSessionServiceClient(transport); -export const userService = createUserServiceClient(transport); -export const oidcService = createOIDCServiceClient(transport); -export const idpService = createIdpServiceClient(transport); -export const orgService = createOrganizationServiceClient(transport); -export const settingsService = createSettingsServiceClient(transport); +import { createServiceForHost } from "./service"; const useCache = process.env.DEBUG !== "true"; @@ -65,7 +53,18 @@ async function cacheWrapper(callback: Promise) { return callback; } -export async function getBrandingSettings(organization?: string) { +export async function getBrandingSettings({ + serviceUrl, + serviceRegion, + organization, +}: { + serviceUrl: string; + serviceRegion: string; + organization?: string; +}) { + const settingsService: Client = + await createServiceForHost(SettingsService, serviceUrl, serviceRegion); + const callback = settingsService .getBrandingSettings({ ctx: makeReqCtx(organization) }, {}) .then((resp) => (resp.settings ? resp.settings : undefined)); @@ -73,41 +72,145 @@ export async function getBrandingSettings(organization?: string) { return useCache ? cacheWrapper(callback) : callback; } -export async function getLoginSettings(orgId?: string) { +export async function getLoginSettings({ + serviceUrl, + serviceRegion, + organization, +}: { + serviceUrl: string; + serviceRegion: string; + organization?: string; +}) { + const settingsService: Client = + await createServiceForHost(SettingsService, serviceUrl, serviceRegion); + const callback = settingsService - .getLoginSettings({ ctx: makeReqCtx(orgId) }, {}) + .getLoginSettings({ ctx: makeReqCtx(organization) }, {}) .then((resp) => (resp.settings ? resp.settings : undefined)); return useCache ? cacheWrapper(callback) : callback; } -export async function listIDPLinks(userId: string) { - return userService.listIDPLinks( - { - userId, - }, - {}, - ); +export async function getLockoutSettings({ + serviceUrl, + serviceRegion, + orgId, +}: { + serviceUrl: string; + serviceRegion: string; + orgId?: string; +}) { + const settingsService: Client = + await createServiceForHost(SettingsService, serviceUrl, serviceRegion); + + const callback = settingsService + .getLockoutSettings({ ctx: makeReqCtx(orgId) }, {}) + .then((resp) => (resp.settings ? resp.settings : undefined)); + + return useCache ? cacheWrapper(callback) : callback; } -export async function addOTPEmail(userId: string) { - return userService.addOTPEmail( - { - userId, - }, - {}, - ); +export async function getPasswordExpirySettings({ + serviceUrl, + serviceRegion, + orgId, +}: { + serviceUrl: string; + serviceRegion: string; + orgId?: string; +}) { + const settingsService: Client = + await createServiceForHost(SettingsService, serviceUrl, serviceRegion); + + const callback = settingsService + .getPasswordExpirySettings({ ctx: makeReqCtx(orgId) }, {}) + .then((resp) => (resp.settings ? resp.settings : undefined)); + + return useCache ? cacheWrapper(callback) : callback; } -export async function addOTPSMS(userId: string) { +export async function listIDPLinks({ + serviceUrl, + serviceRegion, + userId, +}: { + serviceUrl: string; + serviceRegion: string; + userId: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + serviceRegion, + ); + + return userService.listIDPLinks({ userId }, {}); +} + +export async function addOTPEmail({ + serviceUrl, + serviceRegion, + userId, +}: { + serviceUrl: string; + serviceRegion: string; + userId: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + serviceRegion, + ); + + return userService.addOTPEmail({ userId }, {}); +} + +export async function addOTPSMS({ + serviceUrl, + serviceRegion, + userId, +}: { + serviceUrl: string; + serviceRegion: string; + userId: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + serviceRegion, + ); + return userService.addOTPSMS({ userId }, {}); } -export async function registerTOTP(userId: string) { +export async function registerTOTP({ + serviceUrl, + serviceRegion, + userId, +}: { + serviceUrl: string; + serviceRegion: string; + userId: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + serviceRegion, + ); + return userService.registerTOTP({ userId }, {}); } -export async function getGeneralSettings() { +export async function getGeneralSettings({ + serviceUrl, + serviceRegion, +}: { + serviceUrl: string; + serviceRegion: string; +}) { + const settingsService: Client = + await createServiceForHost(SettingsService, serviceUrl, serviceRegion); + const callback = settingsService .getGeneralSettings({}, {}) .then((resp) => resp.supportedLanguages); @@ -115,7 +218,18 @@ export async function getGeneralSettings() { return useCache ? cacheWrapper(callback) : callback; } -export async function getLegalAndSupportSettings(organization?: string) { +export async function getLegalAndSupportSettings({ + serviceUrl, + serviceRegion, + organization, +}: { + serviceUrl: string; + serviceRegion: string; + organization?: string; +}) { + const settingsService: Client = + await createServiceForHost(SettingsService, serviceUrl, serviceRegion); + const callback = settingsService .getLegalAndSupportSettings({ ctx: makeReqCtx(organization) }, {}) .then((resp) => (resp.settings ? resp.settings : undefined)); @@ -123,7 +237,18 @@ export async function getLegalAndSupportSettings(organization?: string) { return useCache ? cacheWrapper(callback) : callback; } -export async function getPasswordComplexitySettings(organization?: string) { +export async function getPasswordComplexitySettings({ + serviceUrl, + serviceRegion, + organization, +}: { + serviceUrl: string; + serviceRegion: string; + organization?: string; +}) { + const settingsService: Client = + await createServiceForHost(SettingsService, serviceUrl, serviceRegion); + const callback = settingsService .getPasswordComplexitySettings({ ctx: makeReqCtx(organization) }) .then((resp) => (resp.settings ? resp.settings : undefined)); @@ -131,29 +256,44 @@ export async function getPasswordComplexitySettings(organization?: string) { return useCache ? cacheWrapper(callback) : callback; } -export async function createSessionFromChecks( - checks: Checks, - challenges: RequestChallenges | undefined, - lifetime?: Duration, -) { - return sessionService.createSession( - { - checks: checks, - challenges, - lifetime, - }, - {}, - ); +export async function createSessionFromChecks({ + serviceUrl, + serviceRegion, + checks, + challenges, + lifetime, +}: { + serviceUrl: string; + serviceRegion: string; + checks: Checks; + challenges: RequestChallenges | undefined; + lifetime?: Duration; +}) { + const sessionService: Client = + await createServiceForHost(SessionService, serviceUrl, serviceRegion); + + return sessionService.createSession({ checks, challenges, lifetime }, {}); } -export async function createSessionForUserIdAndIdpIntent( - userId: string, +export async function createSessionForUserIdAndIdpIntent({ + serviceUrl, + serviceRegion, + userId, + idpIntent, + lifetime, +}: { + serviceUrl: string; + serviceRegion: string; + userId: string; idpIntent: { idpIntentId?: string | undefined; idpIntentToken?: string | undefined; - }, - lifetime?: Duration, -) { + }; + lifetime?: Duration; +}) { + const sessionService: Client = + await createServiceForHost(SessionService, serviceUrl, serviceRegion); + return sessionService.createSession({ checks: { user: { @@ -168,13 +308,26 @@ export async function createSessionForUserIdAndIdpIntent( }); } -export async function setSession( - sessionId: string, - sessionToken: string, - challenges: RequestChallenges | undefined, - checks?: Checks, - lifetime?: Duration, -) { +export async function setSession({ + serviceUrl, + serviceRegion, + sessionId, + sessionToken, + challenges, + checks, + lifetime, +}: { + serviceUrl: string; + serviceRegion: string; + sessionId: string; + sessionToken: string; + challenges: RequestChallenges | undefined; + checks?: Checks; + lifetime?: Duration; +}) { + const sessionService: Client = + await createServiceForHost(SessionService, serviceUrl, serviceRegion); + return sessionService.setSession( { sessionId, @@ -189,27 +342,60 @@ export async function setSession( } export async function getSession({ + serviceUrl, + serviceRegion, sessionId, sessionToken, }: { + serviceUrl: string; + serviceRegion: string; sessionId: string; sessionToken: string; }) { + const sessionService: Client = + await createServiceForHost(SessionService, serviceUrl, serviceRegion); + return sessionService.getSession({ sessionId, sessionToken }, {}); } -export async function deleteSession(sessionId: string, sessionToken: string) { +export async function deleteSession({ + serviceUrl, + serviceRegion, + sessionId, + sessionToken, +}: { + serviceUrl: string; + serviceRegion: string; + sessionId: string; + sessionToken: string; +}) { + const sessionService: Client = + await createServiceForHost(SessionService, serviceUrl, serviceRegion); + return sessionService.deleteSession({ sessionId, sessionToken }, {}); } -export async function listSessions(ids: string[]) { +type ListSessionsCommand = { + serviceUrl: string; + serviceRegion: string; + ids: string[]; +}; + +export async function listSessions({ + serviceUrl, + serviceRegion, + ids, +}: ListSessionsCommand) { + const sessionService: Client = + await createServiceForHost(SessionService, serviceUrl, serviceRegion); + return sessionService.listSessions( { queries: [ { query: { case: "idsQuery", - value: { ids: ids }, + value: { ids }, }, }, ], @@ -219,6 +405,8 @@ export async function listSessions(ids: string[]) { } export type AddHumanUserData = { + serviceUrl: string; + serviceRegion: string; firstName: string; lastName: string; email: string; @@ -227,12 +415,20 @@ export type AddHumanUserData = { }; export async function addHumanUser({ + serviceUrl, + serviceRegion, email, firstName, lastName, password, organization, }: AddHumanUserData) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + serviceRegion, + ); + return userService.addHumanUser({ email: { email, @@ -247,71 +443,162 @@ export async function addHumanUser({ ? { org: { case: "orgId", value: organization } } : undefined, passwordType: password - ? { case: "password", value: { password: password } } + ? { case: "password", value: { password } } : undefined, }); } -export async function addHuman(request: AddHumanUserRequest) { +export async function addHuman({ + serviceUrl, + serviceRegion, + request, +}: { + serviceUrl: string; + serviceRegion: string; + request: AddHumanUserRequest; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + serviceRegion, + ); + return userService.addHumanUser(request); } -export async function verifyTOTPRegistration(code: string, userId: string) { +export async function verifyTOTPRegistration({ + serviceUrl, + serviceRegion, + code, + userId, +}: { + serviceUrl: string; + serviceRegion: string; + code: string; + userId: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + serviceRegion, + ); + return userService.verifyTOTPRegistration({ code, userId }, {}); } -export async function getUserByID(userId: string) { +export async function getUserByID({ + serviceUrl, + serviceRegion, + userId, +}: { + serviceUrl: string; + serviceRegion: string; + userId: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + serviceRegion, + ); + return userService.getUserByID({ userId }, {}); } -export async function verifyInviteCode( - userId: string, - verificationCode: string, -) { +export async function verifyInviteCode({ + serviceUrl, + serviceRegion, + userId, + verificationCode, +}: { + serviceUrl: string; + serviceRegion: string; + userId: string; + verificationCode: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + serviceRegion, + ); + return userService.verifyInviteCode({ userId, verificationCode }, {}); } -export async function resendInviteCode(userId: string) { +export async function resendInviteCode({ + serviceUrl, + serviceRegion, + userId, +}: { + serviceUrl: string; + serviceRegion: string; + userId: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + serviceRegion, + ); + return userService.resendInviteCode({ userId }, {}); } -export async function sendEmailCode( - userId: string, - host: string | null, - authRequestId?: string, -) { - let medium = create(SendEmailCodeRequestSchema, { - userId, +export async function sendEmailCode({ + serviceUrl, + serviceRegion, + userId, + urlTemplate, +}: { + serviceUrl: string; + serviceRegion: string; + userId: string; + urlTemplate: string; +}) { + let medium = create(SendEmailCodeRequestSchema, { userId }); + + medium = create(SendEmailCodeRequestSchema, { + ...medium, + verification: { + case: "sendCode", + value: create(SendEmailVerificationCodeSchema, { + urlTemplate, + }), + }, }); - if (host) { - medium = create(SendEmailCodeRequestSchema, { - ...medium, - verification: { - case: "sendCode", - value: create(SendEmailVerificationCodeSchema, { - urlTemplate: - `${host.includes("localhost") ? "http://" : "https://"}${host}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` + - (authRequestId ? `&authRequestId=${authRequestId}` : ""), - }), - }, - }); - } + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + serviceRegion, + ); return userService.sendEmailCode(medium, {}); } -export async function createInviteCode(userId: string, host: string | null) { +export async function createInviteCode({ + serviceUrl, + serviceRegion, + urlTemplate, + userId, +}: { + serviceUrl: string; + serviceRegion: string; + urlTemplate: string; + userId: string; +}) { let medium = create(SendInviteCodeSchema, { applicationName: "Typescript Login", }); - if (host) { - medium = { - ...medium, - urlTemplate: `${host.includes("localhost") ? "http://" : "https://"}${host}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true`, - }; - } + medium = { + ...medium, + urlTemplate, + }; + + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + serviceRegion, + ); return userService.createInviteCode( { @@ -326,6 +613,8 @@ export async function createInviteCode(userId: string, host: string | null) { } export type ListUsersCommand = { + serviceUrl: string; + serviceRegion: string; loginName?: string; userName?: string; email?: string; @@ -334,6 +623,8 @@ export type ListUsersCommand = { }; export async function listUsers({ + serviceUrl, + serviceRegion, loginName, userName, phone, @@ -349,7 +640,7 @@ export async function listUsers({ query: { case: "loginNameQuery", value: { - loginName: loginName, + loginName, method: TextQueryMethod.EQUALS, }, }, @@ -363,7 +654,7 @@ export async function listUsers({ query: { case: "userNameQuery", value: { - userName: userName, + userName, method: TextQueryMethod.EQUALS, }, }, @@ -422,10 +713,18 @@ export async function listUsers({ ); } - return userService.listUsers({ queries: queries }); + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + serviceRegion, + ); + + return userService.listUsers({ queries }); } export type SearchUsersCommand = { + serviceUrl: string; + serviceRegion: string; searchValue: string; loginSettings: LoginSettings; organizationId?: string; @@ -469,8 +768,9 @@ const EmailQuery = (searchValue: string) => * this is a dedicated search function to search for users from the loginname page * it searches users based on the loginName or userName and org suffix combination, and falls back to email and phone if no users are found * */ - export async function searchUsers({ + serviceUrl, + serviceRegion, searchValue, loginSettings, organizationId, @@ -501,7 +801,13 @@ export async function searchUsers({ ); } - const loginNameResult = await userService.listUsers({ queries: queries }); + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + serviceRegion, + ); + + const loginNameResult = await userService.listUsers({ queries }); if (!loginNameResult || !loginNameResult.details) { return { error: "An error occurred." }; @@ -583,7 +889,16 @@ export async function searchUsers({ return { error: "User not found in the system" }; } -export async function getDefaultOrg(): Promise { +export async function getDefaultOrg({ + serviceUrl, + serviceRegion, +}: { + serviceUrl: string; + serviceRegion: string; +}): Promise { + const orgService: Client = + await createServiceForHost(OrganizationService, serviceUrl, serviceRegion); + return orgService .listOrganizations( { @@ -601,7 +916,18 @@ export async function getDefaultOrg(): Promise { .then((resp) => (resp?.result && resp.result[0] ? resp.result[0] : null)); } -export async function getOrgsByDomain(domain: string) { +export async function getOrgsByDomain({ + serviceUrl, + serviceRegion, + domain, +}: { + serviceUrl: string; + serviceRegion: string; + domain: string; +}) { + const orgService: Client = + await createServiceForHost(OrganizationService, serviceUrl, serviceRegion); + return orgService.listOrganizations( { queries: [ @@ -618,12 +944,22 @@ export async function getOrgsByDomain(domain: string) { } export async function startIdentityProviderFlow({ + serviceUrl, + serviceRegion, idpId, urls, }: { + serviceUrl: string; + serviceRegion: string; idpId: string; urls: RedirectURLsJson; }) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + serviceRegion, + ); + return userService.startIdentityProviderIntent({ idpId, content: { @@ -634,9 +970,22 @@ export async function startIdentityProviderFlow({ } export async function retrieveIdentityProviderInformation({ + serviceUrl, + serviceRegion, idpIntentId, idpIntentToken, -}: RetrieveIdentityProviderIntentRequest) { +}: { + serviceUrl: string; + serviceRegion: string; + idpIntentId: string; + idpIntentToken: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + serviceRegion, + ); + return userService.retrieveIdentityProviderIntent({ idpIntentId, idpIntentToken, @@ -644,20 +993,60 @@ export async function retrieveIdentityProviderInformation({ } export async function getAuthRequest({ + serviceUrl, + serviceRegion, authRequestId, }: { + serviceUrl: string; + serviceRegion: string; authRequestId: string; }) { + const oidcService = await createServiceForHost( + OIDCService, + serviceUrl, + serviceRegion, + ); + return oidcService.getAuthRequest({ authRequestId, }); } -export async function createCallback(req: CreateCallbackRequest) { +export async function createCallback({ + serviceUrl, + serviceRegion, + req, +}: { + serviceUrl: string; + serviceRegion: string; + req: CreateCallbackRequest; +}) { + const oidcService = await createServiceForHost( + OIDCService, + serviceUrl, + serviceRegion, + ); + return oidcService.createCallback(req); } -export async function verifyEmail(userId: string, verificationCode: string) { +export async function verifyEmail({ + serviceUrl, + serviceRegion, + userId, + verificationCode, +}: { + serviceUrl: string; + serviceRegion: string; + userId: string; + verificationCode: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + serviceRegion, + ); + return userService.verifyEmail( { userId, @@ -667,47 +1056,95 @@ export async function verifyEmail(userId: string, verificationCode: string) { ); } -export async function resendEmailCode( - userId: string, - host: string | null, - authRequestId?: string, -) { +export async function resendEmailCode({ + serviceUrl, + serviceRegion, + userId, + urlTemplate, +}: { + serviceUrl: string; + serviceRegion: string; + userId: string; + urlTemplate: string; +}) { let request: ResendEmailCodeRequest = create(ResendEmailCodeRequestSchema, { userId, }); - if (host) { - const medium = create(SendEmailVerificationCodeSchema, { - urlTemplate: - `${host.includes("localhost") ? "http://" : "https://"}${host}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + - (authRequestId ? `&authRequestId=${authRequestId}` : ""), - }); + const medium = create(SendEmailVerificationCodeSchema, { + urlTemplate, + }); - request = { ...request, verification: { case: "sendCode", value: medium } }; - } + request = { ...request, verification: { case: "sendCode", value: medium } }; + + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + serviceRegion, + ); return userService.resendEmailCode(request, {}); } -export function retrieveIDPIntent(id: string, token: string) { +export async function retrieveIDPIntent({ + serviceUrl, + serviceRegion, + id, + token, +}: { + serviceUrl: string; + serviceRegion: string; + id: string; + token: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + serviceRegion, + ); + return userService.retrieveIdentityProviderIntent( { idpIntentId: id, idpIntentToken: token }, {}, ); } -export function getIDPByID(id: string) { +export async function getIDPByID({ + serviceUrl, + serviceRegion, + id, +}: { + serviceUrl: string; + serviceRegion: string; + id: string; +}) { + const idpService: Client = + await createServiceForHost( + IdentityProviderService, + serviceUrl, + serviceRegion, + ); + return idpService.getIDPByID({ id }, {}).then((resp) => resp.idp); } -export function addIDPLink( - idp: { - id: string; - userId: string; - userName: string; - }, - userId: string, -) { +export async function addIDPLink({ + serviceUrl, + serviceRegion, + idp, + userId, +}: { + serviceUrl: string; + serviceRegion: string; + idp: { id: string; userId: string; userName: string }; + userId: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + serviceRegion, + ); + return userService.addIDPLink( { idpLink: { @@ -721,28 +1158,31 @@ export function addIDPLink( ); } -/** - * - * @param userId the id of the user where the email should be set - * @returns the newly set email - */ -export async function passwordReset( - userId: string, - host: string | null, - authRequestId?: string, -) { +export async function passwordReset({ + serviceUrl, + serviceRegion, + userId, + urlTemplate, +}: { + serviceUrl: string; + serviceRegion: string; + userId: string; + urlTemplate?: string; +}) { let medium = create(SendPasswordResetLinkSchema, { notificationType: NotificationType.Email, }); - if (host) { - medium = { - ...medium, - urlTemplate: - `${host.includes("localhost") ? "http://" : "https://"}${host}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + - (authRequestId ? `&authRequestId=${authRequestId}` : ""), - }; - } + medium = { + ...medium, + urlTemplate, + }; + + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + serviceRegion, + ); return userService.passwordReset( { @@ -756,19 +1196,21 @@ export async function passwordReset( ); } -/** - * - * @param userId userId of the user to set the password for - * @param password the new password - * @param code optional if the password should be set with a code (reset), no code for initial setup of password - * @returns - */ -export async function setUserPassword( - userId: string, - password: string, - user: User, - code?: string, -) { +export async function setUserPassword({ + serviceUrl, + serviceRegion, + userId, + password, + user, + code, +}: { + serviceUrl: string; + serviceRegion: string; + userId: string; + password: string; + user: User; + code?: string; +}) { let payload = create(SetPasswordRequestSchema, { userId, newPassword: { @@ -778,7 +1220,11 @@ export async function setUserPassword( // check if the user has no password set in order to set a password if (!code) { - const authmethods = await listAuthenticationMethodTypes(userId); + const authmethods = await listAuthenticationMethodTypes({ + serviceUrl, + serviceRegion, + userId, + }); // if the user has no authmethods set, we can set a password otherwise we need a code if ( @@ -799,6 +1245,12 @@ export async function setUserPassword( }; } + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + serviceRegion, + ); + return userService.setPassword(payload, {}).catch((error) => { // throw error if failed precondition (ex. User is not yet initialized) if (error.code === 9 && error.message) { @@ -809,27 +1261,45 @@ export async function setUserPassword( }); } -export async function setPassword(payload: SetPasswordRequest) { +export async function setPassword({ + serviceUrl, + serviceRegion, + payload, +}: { + serviceUrl: string; + serviceRegion: string; + payload: SetPasswordRequest; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + serviceRegion, + ); + return userService.setPassword(payload, {}); } /** * - * @param server + * @param host * @param userId the id of the user where the email should be set * @returns the newly set email */ +export async function createPasskeyRegistrationLink({ + serviceUrl, + serviceRegion, + userId, +}: { + serviceUrl: string; + serviceRegion: string; + userId: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + serviceRegion, + ); -// TODO check for token requirements! -export async function createPasskeyRegistrationLink( - userId: string, - // token: string, -) { - // const transport = createServerTransport(token, { - // baseUrl: process.env.ZITADEL_API_URL!, - // }); - - // const service = createUserServiceClient(transport); return userService.createPasskeyRegistrationLink({ userId, medium: { @@ -841,12 +1311,28 @@ export async function createPasskeyRegistrationLink( /** * + * @param host * @param userId the id of the user where the email should be set * @param domain the domain on which the factor is registered * @returns the newly set email */ +export async function registerU2F({ + serviceUrl, + serviceRegion, + userId, + domain, +}: { + serviceUrl: string; + serviceRegion: string; + userId: string; + domain: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + serviceRegion, + ); -export async function registerU2F(userId: string, domain: string) { return userService.registerU2F({ userId, domain, @@ -855,48 +1341,107 @@ export async function registerU2F(userId: string, domain: string) { /** * - * @param userId the id of the user where the email should be set - * @param domain the domain on which the factor is registered - * @returns the newly set email + * @param host + * @param request the request object for verifying U2F registration + * @returns the result of the verification */ -export async function verifyU2FRegistration( - request: VerifyU2FRegistrationRequest, -) { +export async function verifyU2FRegistration({ + serviceUrl, + serviceRegion, + request, +}: { + serviceUrl: string; + serviceRegion: string; + request: VerifyU2FRegistrationRequest; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + serviceRegion, + ); + return userService.verifyU2FRegistration(request, {}); } -export async function getActiveIdentityProviders( - orgId?: string, - linking_allowed?: boolean, -) { +/** + * + * @param host + * @param orgId the organization ID + * @param linking_allowed whether linking is allowed + * @returns the active identity providers + */ +export async function getActiveIdentityProviders({ + serviceUrl, + serviceRegion, + orgId, + linking_allowed, +}: { + serviceUrl: string; + serviceRegion: string; + orgId?: string; + linking_allowed?: boolean; +}) { const props: any = { ctx: makeReqCtx(orgId) }; if (linking_allowed) { props.linkingAllowed = linking_allowed; } + const settingsService: Client = + await createServiceForHost(SettingsService, serviceUrl, serviceRegion); + return settingsService.getActiveIdentityProviders(props, {}); } /** * - * @param userId the id of the user where the email should be set - * @returns the newly set email + * @param host + * @param request the request object for verifying passkey registration + * @returns the result of the verification */ -export async function verifyPasskeyRegistration( - request: VerifyPasskeyRegistrationRequest, -) { +export async function verifyPasskeyRegistration({ + serviceUrl, + serviceRegion, + request, +}: { + serviceUrl: string; + serviceRegion: string; + request: VerifyPasskeyRegistrationRequest; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + serviceRegion, + ); + return userService.verifyPasskeyRegistration(request, {}); } /** * + * @param host * @param userId the id of the user where the email should be set + * @param code the code for registering the passkey + * @param domain the domain on which the factor is registered * @returns the newly set email */ -export async function registerPasskey( - userId: string, - code: { id: string; code: string }, - domain: string, -) { +export async function registerPasskey({ + serviceUrl, + serviceRegion, + userId, + code, + domain, +}: { + serviceUrl: string; + serviceRegion: string; + userId: string; + code: { id: string; code: string }; + domain: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + serviceRegion, + ); + return userService.registerPasskey({ userId, code, @@ -906,10 +1451,25 @@ export async function registerPasskey( /** * + * @param host * @param userId the id of the user where the email should be set - * @returns the newly set email + * @returns the list of authentication method types */ -export async function listAuthenticationMethodTypes(userId: string) { +export async function listAuthenticationMethodTypes({ + serviceUrl, + serviceRegion, + userId, +}: { + serviceUrl: string; + serviceRegion: string; + userId: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + serviceRegion, + ); + return userService.listAuthenticationMethodTypes({ userId, }); diff --git a/apps/login/src/middleware.ts b/apps/login/src/middleware.ts index 93cb65581c..8ffe8e5830 100644 --- a/apps/login/src/middleware.ts +++ b/apps/login/src/middleware.ts @@ -1,4 +1,6 @@ +import { headers } from "next/headers"; import { NextRequest, NextResponse } from "next/server"; +import { getServiceUrlFromHeaders } from "./lib/service"; export const config = { matcher: [ @@ -9,28 +11,40 @@ export const config = { ], }; -const INSTANCE = process.env.ZITADEL_API_URL; -const SERVICE_USER_ID = process.env.ZITADEL_SERVICE_USER_ID as string; +export async function middleware(request: NextRequest) { + // escape proxy if the environment is setup for multitenancy + if ( + !process.env.ZITADEL_API_URL || + !process.env.ZITADEL_SERVICE_USER_ID || + !process.env.ZITADEL_SERVICE_USER_TOKEN + ) { + return NextResponse.next(); + } + + const _headers = await headers(); + + const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers); + + const instanceHost = `${serviceUrl}`.replace("https://", ""); -export function middleware(request: NextRequest) { const requestHeaders = new Headers(request.headers); - requestHeaders.set("x-zitadel-login-client", SERVICE_USER_ID); + requestHeaders.set( + "x-zitadel-login-client", + process.env.ZITADEL_SERVICE_USER_ID, + ); // this is a workaround for the next.js server not forwarding the host header // requestHeaders.set("x-zitadel-forwarded", `host="${request.nextUrl.host}"`); requestHeaders.set("x-zitadel-public-host", `${request.nextUrl.host}`); // this is a workaround for the next.js server not forwarding the host header - requestHeaders.set( - "x-zitadel-instance-host", - `${INSTANCE}`.replace(/^https?:\/\//, ""), - ); + requestHeaders.set("x-zitadel-instance-host", instanceHost); const responseHeaders = new Headers(); responseHeaders.set("Access-Control-Allow-Origin", "*"); responseHeaders.set("Access-Control-Allow-Headers", "*"); - request.nextUrl.href = `${INSTANCE}${request.nextUrl.pathname}${request.nextUrl.search}`; + request.nextUrl.href = `${serviceUrl}${request.nextUrl.pathname}${request.nextUrl.search}`; return NextResponse.rewrite(request.nextUrl, { request: { headers: requestHeaders, diff --git a/apps/login/tsconfig.json b/apps/login/tsconfig.json index a1efe752c5..c855c43225 100755 --- a/apps/login/tsconfig.json +++ b/apps/login/tsconfig.json @@ -2,6 +2,7 @@ "extends": "@zitadel/tsconfig/nextjs.json", "compilerOptions": { "jsx": "preserve", + "target": "es2022", "baseUrl": ".", "paths": { "@/*": ["./src/*"] diff --git a/package.json b/package.json index 82d893f238..560bf82332 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,7 @@ }, "pnpm": { "overrides": { - "@typescript-eslint/parser": "^7.9.0", - "@types/react": "npm:types-react@19.0.0-rc.1", - "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1" + "@typescript-eslint/parser": "^7.9.0" } }, "devDependencies": { diff --git a/packages/zitadel-client/CHANGELOG.md b/packages/zitadel-client/CHANGELOG.md index 8e4b17643a..0144cbc0dd 100644 --- a/packages/zitadel-client/CHANGELOG.md +++ b/packages/zitadel-client/CHANGELOG.md @@ -1,5 +1,11 @@ # @zitadel/client +## 1.0.4 + +### Patch Changes + +- 28dc956: dynamic properties for system token utility + ## 1.0.3 ### Patch Changes diff --git a/packages/zitadel-client/package.json b/packages/zitadel-client/package.json index e6b5323f2f..f3e1cbbce9 100644 --- a/packages/zitadel-client/package.json +++ b/packages/zitadel-client/package.json @@ -1,6 +1,6 @@ { "name": "@zitadel/client", - "version": "1.0.3", + "version": "1.0.4", "license": "MIT", "publishConfig": { "access": "public" diff --git a/packages/zitadel-client/src/index.ts b/packages/zitadel-client/src/index.ts index 64c3af5050..66bb55c561 100644 --- a/packages/zitadel-client/src/index.ts +++ b/packages/zitadel-client/src/index.ts @@ -1,8 +1,10 @@ -export { toDate } from "./helpers"; +export { createClientFor, toDate } from "./helpers"; export { NewAuthorizationBearerInterceptor } from "./interceptors"; // TODO: Move this to `./protobuf.ts` and export it from there export { create, fromJson, toJson } from "@bufbuild/protobuf"; export type { JsonObject } from "@bufbuild/protobuf"; +export type { GenService } from "@bufbuild/protobuf/codegenv1"; export { TimestampSchema, timestampDate, timestampFromDate, timestampFromMs, timestampMs } from "@bufbuild/protobuf/wkt"; export type { Duration, Timestamp } from "@bufbuild/protobuf/wkt"; +export type { Client, Code, ConnectError } from "@connectrpc/connect"; diff --git a/packages/zitadel-client/src/node.ts b/packages/zitadel-client/src/node.ts index 8f70a4edf1..db7838ebc3 100644 --- a/packages/zitadel-client/src/node.ts +++ b/packages/zitadel-client/src/node.ts @@ -27,13 +27,23 @@ export function createClientTransport(token: string, opts: GrpcTransportOptions) }); } -export async function newSystemToken() { +export async function newSystemToken({ + audience, + subject, + key, + expirationTime, +}: { + audience: string; + subject: string; + key: string; + expirationTime?: number | string | Date; +}) { return await new SignJWT({}) .setProtectedHeader({ alg: "RS256" }) .setIssuedAt() - .setExpirationTime("1h") - .setIssuer(process.env.ZITADEL_SYSTEM_API_USERID ?? "") - .setSubject(process.env.ZITADEL_SYSTEM_API_USERID ?? "") - .setAudience(process.env.ZITADEL_ISSUER ?? "") - .sign(await importPKCS8(process.env.ZITADEL_SYSTEM_API_KEY ?? "", "RS256")); + .setExpirationTime(expirationTime ?? "1h") + .setIssuer(subject) + .setSubject(subject) + .setAudience(audience) + .sign(await importPKCS8(key, "RS256")); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 61d01b79c5..007c6d6003 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,8 +6,6 @@ settings: overrides: '@typescript-eslint/parser': ^7.9.0 - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 importers: @@ -78,16 +76,16 @@ importers: dependencies: '@headlessui/react': specifier: ^2.1.9 - version: 2.1.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + version: 2.1.9(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@heroicons/react': specifier: 2.1.3 - version: 2.1.3(react@19.0.0-rc-66855b96-20241106) + version: 2.1.3(react@19.0.0) '@tailwindcss/forms': specifier: 0.5.7 version: 0.5.7(tailwindcss@3.4.14) '@vercel/analytics': specifier: ^1.2.2 - version: 1.3.1(next@15.0.4-canary.23(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(sass@1.80.7))(react@19.0.0-rc-66855b96-20241106) + version: 1.3.1(next@15.2.0-canary.33(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7))(react@19.0.0) '@zitadel/client': specifier: workspace:* version: link:../../packages/zitadel-client @@ -103,36 +101,39 @@ importers: deepmerge: specifier: ^4.3.1 version: 4.3.1 + jose: + specifier: ^5.3.0 + version: 5.8.0 moment: specifier: ^2.29.4 version: 2.30.1 next: - specifier: 15.0.4-canary.23 - version: 15.0.4-canary.23(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(sass@1.80.7) + specifier: 15.2.0-canary.33 + version: 15.2.0-canary.33(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7) next-intl: specifier: ^3.25.1 - version: 3.25.1(next@15.0.4-canary.23(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(sass@1.80.7))(react@19.0.0-rc-66855b96-20241106) + version: 3.25.1(next@15.2.0-canary.33(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7))(react@19.0.0) next-themes: specifier: ^0.2.1 - version: 0.2.1(next@15.0.4-canary.23(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(sass@1.80.7))(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + version: 0.2.1(next@15.2.0-canary.33(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) nice-grpc: specifier: 2.0.1 version: 2.0.1 qrcode.react: specifier: ^3.1.0 - version: 3.1.0(react@19.0.0-rc-66855b96-20241106) + version: 3.1.0(react@19.0.0) react: - specifier: 19.0.0-rc-66855b96-20241106 - version: 19.0.0-rc-66855b96-20241106 + specifier: 19.0.0 + version: 19.0.0 react-dom: - specifier: 19.0.0-rc-66855b96-20241106 - version: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + specifier: 19.0.0 + version: 19.0.0(react@19.0.0) react-hook-form: specifier: 7.39.5 - version: 7.39.5(react@19.0.0-rc-66855b96-20241106) + version: 7.39.5(react@19.0.0) swr: specifier: ^2.2.0 - version: 2.2.5(react@19.0.0-rc-66855b96-20241106) + version: 2.2.5(react@19.0.0) tinycolor2: specifier: 1.4.2 version: 1.4.2 @@ -145,7 +146,7 @@ importers: version: 6.6.3 '@testing-library/react': specifier: ^16.0.1 - version: 16.0.1(@testing-library/dom@10.4.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) + version: 16.0.1(@testing-library/dom@10.4.0)(@types/react-dom@19.0.2(@types/react@19.0.2))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@types/ms': specifier: 0.7.34 version: 0.7.34 @@ -153,11 +154,11 @@ importers: specifier: 22.9.0 version: 22.9.0 '@types/react': - specifier: npm:types-react@19.0.0-rc.1 - version: types-react@19.0.0-rc.1 + specifier: 19.0.2 + version: 19.0.2 '@types/react-dom': - specifier: npm:types-react-dom@19.0.0-rc.1 - version: types-react-dom@19.0.0-rc.1 + specifier: 19.0.2 + version: 19.0.2(@types/react@19.0.2) '@types/tinycolor2': specifier: 1.4.3 version: 1.4.3 @@ -1132,56 +1133,56 @@ packages: resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} hasBin: true - '@next/env@15.0.4-canary.23': - resolution: {integrity: sha512-NfBMRPa10yaEzQ693kGEsgHL58Y27jSbGCDbyXy14dx3z6UeQZQfEVRAwJ4iG1V6gND9+CzzugtiXvJZfSlC9A==} + '@next/env@15.2.0-canary.33': + resolution: {integrity: sha512-y3EPM+JYKU8t2K+i6bc0QrotEZVGpqu9eVjprj4cfS8QZyZcL54s+W9aGB0TBuGavU9tQdZ50W186+toeMV+hw==} '@next/eslint-plugin-next@14.2.18': resolution: {integrity: sha512-KyYTbZ3GQwWOjX3Vi1YcQbekyGP0gdammb7pbmmi25HBUCINzDReyrzCMOJIeZisK1Q3U6DT5Rlc4nm2/pQeXA==} - '@next/swc-darwin-arm64@15.0.4-canary.23': - resolution: {integrity: sha512-sX3MaDUiFiMT14KSx5mJz6B+IH9k7+buNniNrDP7iz4YG28jssm9e8uHbiWXsbn9jnkQUJu8PHoUOLhgjZgtsQ==} + '@next/swc-darwin-arm64@15.2.0-canary.33': + resolution: {integrity: sha512-+fCdK2KmR6lWoCTk1fSd5pvbiLZHfZF+D/Xdz3xrXw+pbnBtXWLKQrPT0bCtDseMxD31qcOywq5mAApvI3EGpA==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.0.4-canary.23': - resolution: {integrity: sha512-KJRSDVvEPuvjRKe9IY3YMAv9KMOmB/U5+7g0c3OTT/50x1KL0XOlgnc+Af2GdZKIrkKiAdTFG54AHaSD584yHg==} + '@next/swc-darwin-x64@15.2.0-canary.33': + resolution: {integrity: sha512-GrrU+tSmeBRow+7bnn7i5M96g3tc28hPH5t5Y65qUXGmmrZwGZN1e1d+8QbXPdAGkvjEPcOkUNQuQVpp1qpYPA==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.0.4-canary.23': - resolution: {integrity: sha512-0EqeqGdlG0MPDYGE/cPtTvBLtBiWDAd7fSRgRhIga6CkuaRVFKuTeRrsjTa0v+51C2OawjQp2N3ww1zBLuBhcg==} + '@next/swc-linux-arm64-gnu@15.2.0-canary.33': + resolution: {integrity: sha512-8RnGxnUpASHoUf6aHUifmZom5b4Ow5nTdCib/CNYXZ6VLuL5ocvmr+DXs/SKzi9h8OHR7JkLwKXHCcF8WyscSg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.0.4-canary.23': - resolution: {integrity: sha512-O06Gw8HU0z9f1b4TiGb0u1o87hgLa0yEW1odyLPE1d3+JKwhkh4L1Ug9uLpeqEUnxCoIrwVomEUyQBPGNQtq0Q==} + '@next/swc-linux-arm64-musl@15.2.0-canary.33': + resolution: {integrity: sha512-COyE0LzMuLBZSR+Z/TOGilyJPdwSU588Vt0+o8GoECkoDEnjyuO2s2nHa2kDAcEfUEPkhlo0tErU3mF+8AVOTQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.0.4-canary.23': - resolution: {integrity: sha512-BvERc3hri6eyUHnasZgwcRCdR8WpfCdKKe/M12Q+ZAkTeJeVkLXNakznaZbBWdlCc77F/NeHz/OWoQWUTpKm3g==} + '@next/swc-linux-x64-gnu@15.2.0-canary.33': + resolution: {integrity: sha512-3Y9lqJs+ftU9jgbLdCtvAvF8MNJsJYGMH7icb8QMs1+yOyHHbmwkZoElKdjwfUWzQ2sX28ywp73GWq4HbrsoUg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.0.4-canary.23': - resolution: {integrity: sha512-FF5LNTdra/tHxdHjRR3lb+UxFgRVT+v3EMruueQg6BpOqpciodyCkkYQFrx2DitpADojQ6bBBFBDs6KIb8jB5w==} + '@next/swc-linux-x64-musl@15.2.0-canary.33': + resolution: {integrity: sha512-FS9iA+RkZlhdWGQEKtsplVBXIYZJUn5nsRB+1UY46b3uaL6dDypu13ODaSwYuAwXGgkrZBVF9AFO3y4biBnPlA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.0.4-canary.23': - resolution: {integrity: sha512-XnHD7fqQYZR1XCCuAf8+yAdkMpzAFz2pWmny2K6g5C7BalrwNuxWLsM5LycW1PTMzSqkzLJeXCG6AZu099u7/w==} + '@next/swc-win32-arm64-msvc@15.2.0-canary.33': + resolution: {integrity: sha512-Ji9CtBbUx06qvvN/rPohJN2FEFGsUv26F50f2nMRYRwrq3POXDjloGOiRocrjU0ty/cUzCz71qTUfKdmv/ajmg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.0.4-canary.23': - resolution: {integrity: sha512-HGoW8LjYxbUhkND+vJ/21dWQ7sdv4SIUQDv2r/FpcdHFMzb5M/jgQVqcMFkqg2ibH65ZAcVBM0ICcUnTLlX7PQ==} + '@next/swc-win32-x64-msvc@15.2.0-canary.33': + resolution: {integrity: sha512-hjdbGnkwIZ8zN2vlS6lNsEJO37HRtcEGimzfkruBMsi/DwJBqkJvZbNC/XCJy3HFcU58igncqV52p1IPjmAJAw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1478,6 +1479,9 @@ packages: '@swc/helpers@0.5.13': resolution: {integrity: sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==} + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@swc/helpers@0.5.5': resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} @@ -1513,8 +1517,8 @@ packages: engines: {node: '>=18'} peerDependencies: '@testing-library/dom': ^10.0.0 - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': ^18.0.0 + '@types/react-dom': ^18.0.0 react: ^18.0.0 react-dom: ^18.0.0 peerDependenciesMeta: @@ -1553,11 +1557,13 @@ packages: '@types/node@22.9.0': resolution: {integrity: sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==} - '@types/prop-types@15.7.12': - resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} + '@types/react-dom@19.0.2': + resolution: {integrity: sha512-c1s+7TKFaDRRxr1TxccIX2u7sfCnc3RxkVyBIUA2lCpyqCF+QoAwQ/CBg7bsMdVwP120HEH143VQezKtef5nCg==} + peerDependencies: + '@types/react': ^19.0.0 - '@types/react@18.3.12': - resolution: {integrity: sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==} + '@types/react@19.0.2': + resolution: {integrity: sha512-USU8ZI/xyKJwFTpjSVIrSeHBVAGagkHQKPNbxeWwql/vDmnTIBgx+TJnhFnj1NXgz8XfprU0egV2dROLGpsBEg==} '@types/sinonjs__fake-timers@8.1.1': resolution: {integrity: sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==} @@ -3465,16 +3471,16 @@ packages: react: '*' react-dom: '*' - next@15.0.4-canary.23: - resolution: {integrity: sha512-xCjjBx4csWdG4MP9tKV/C25OIDbN0o+zovMC5zd4yvE4lrd43Y5tt+w171IGUueb6VbPLTSlDaXvqOtrxKJXzQ==} + next@15.2.0-canary.33: + resolution: {integrity: sha512-WF8QLeYkakuYwksdWY/F+Bi8tNJfIbiSYk9hCmldn9sNp1lU3lqI1hrW1ynbcMSaXC+qQEr7yol2OdvVZ4nZYQ==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 '@playwright/test': ^1.41.2 babel-plugin-react-compiler: '*' - react: ^18.2.0 || 19.0.0-rc-380f5d67-20241113 - react-dom: ^18.2.0 || 19.0.0-rc-380f5d67-20241113 + 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 sass: ^1.3.0 peerDependenciesMeta: '@opentelemetry/api': @@ -3947,10 +3953,10 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - react-dom@19.0.0-rc-66855b96-20241106: - resolution: {integrity: sha512-D25vdaytZ1wFIRiwNU98NPQ/upS2P8Co4/oNoa02PzHbh8deWdepjm5qwZM/46OdSiGv4WSWwxP55RO9obqJEQ==} + react-dom@19.0.0: + resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==} peerDependencies: - react: 19.0.0-rc-66855b96-20241106 + react: ^19.0.0 react-hook-form@7.39.5: resolution: {integrity: sha512-OE0HKyz5IPc6svN2wd+e+evidZrw4O4WZWAWYzQVZuHi+hYnHFSLnxOq0ddjbdmaLIsLHut/ab7j72y2QT3+KA==} @@ -3968,8 +3974,8 @@ packages: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} - react@19.0.0-rc-66855b96-20241106: - resolution: {integrity: sha512-klH7xkT71SxRCx4hb1hly5FJB21Hz0ACyxbXYAECEqssUjtJeFUAaI2U1DgJAzkGEnvEm3DkxuBchMC/9K4ipg==} + react@19.0.0: + resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} engines: {node: '>=0.10.0'} read-cache@1.0.0: @@ -4089,8 +4095,8 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} - scheduler@0.25.0-rc-66855b96-20241106: - resolution: {integrity: sha512-HQXp/Mnp/MMRSXMQF7urNFla+gmtXW/Gr1KliuR0iboTit4KvZRY8KYaq5ccCTAOJiUqQh2rE2F3wgUekmgdlA==} + scheduler@0.25.0: + resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} @@ -4574,12 +4580,6 @@ packages: resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} engines: {node: '>= 0.4'} - types-react-dom@19.0.0-rc.1: - resolution: {integrity: sha512-VSLZJl8VXCD0fAWp7DUTFUDCcZ8DVXOQmjhJMD03odgeFmu14ZQJHCXeETm3BEAhJqfgJaFkLnGkQv88sRx0fQ==} - - types-react@19.0.0-rc.1: - resolution: {integrity: sha512-RshndUfqTW6K3STLPis8BtAYCGOkMbtvYsi90gmVNDZBXUyUc5juf2PE9LfS/JmOlUIRO8cWTS/1MTnmhjDqyQ==} - typescript@5.6.3: resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} engines: {node: '>=14.17'} @@ -5253,7 +5253,7 @@ snapshots: '@emnapi/runtime@1.3.1': dependencies: - tslib: 2.7.0 + tslib: 2.8.1 optional: true '@esbuild/aix-ppc64@0.21.5': @@ -5438,18 +5438,18 @@ snapshots: '@floating-ui/core': 1.6.8 '@floating-ui/utils': 0.2.8 - '@floating-ui/react-dom@2.1.2(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + '@floating-ui/react-dom@2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@floating-ui/dom': 1.6.11 - react: 19.0.0-rc-66855b96-20241106 - react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) - '@floating-ui/react@0.26.24(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + '@floating-ui/react@0.26.24(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@floating-ui/react-dom': 2.1.2(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@floating-ui/react-dom': 2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@floating-ui/utils': 0.2.8 - react: 19.0.0-rc-66855b96-20241106 - react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) tabbable: 6.2.0 '@floating-ui/utils@0.2.8': {} @@ -5501,18 +5501,18 @@ snapshots: dependencies: '@hapi/hoek': 9.3.0 - '@headlessui/react@2.1.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + '@headlessui/react@2.1.9(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@floating-ui/react': 0.26.24(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) - '@react-aria/focus': 3.18.3(react@19.0.0-rc-66855b96-20241106) - '@react-aria/interactions': 3.22.3(react@19.0.0-rc-66855b96-20241106) - '@tanstack/react-virtual': 3.10.6(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) - react: 19.0.0-rc-66855b96-20241106 - react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + '@floating-ui/react': 0.26.24(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@react-aria/focus': 3.18.3(react@19.0.0) + '@react-aria/interactions': 3.22.3(react@19.0.0) + '@tanstack/react-virtual': 3.10.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) - '@heroicons/react@2.1.3(react@19.0.0-rc-66855b96-20241106)': + '@heroicons/react@2.1.3(react@19.0.0)': dependencies: - react: 19.0.0-rc-66855b96-20241106 + react: 19.0.0 '@humanwhocodes/config-array@0.13.0': dependencies: @@ -5660,34 +5660,34 @@ snapshots: - encoding - supports-color - '@next/env@15.0.4-canary.23': {} + '@next/env@15.2.0-canary.33': {} '@next/eslint-plugin-next@14.2.18': dependencies: glob: 10.3.10 - '@next/swc-darwin-arm64@15.0.4-canary.23': + '@next/swc-darwin-arm64@15.2.0-canary.33': optional: true - '@next/swc-darwin-x64@15.0.4-canary.23': + '@next/swc-darwin-x64@15.2.0-canary.33': optional: true - '@next/swc-linux-arm64-gnu@15.0.4-canary.23': + '@next/swc-linux-arm64-gnu@15.2.0-canary.33': optional: true - '@next/swc-linux-arm64-musl@15.0.4-canary.23': + '@next/swc-linux-arm64-musl@15.2.0-canary.33': optional: true - '@next/swc-linux-x64-gnu@15.0.4-canary.23': + '@next/swc-linux-x64-gnu@15.2.0-canary.33': optional: true - '@next/swc-linux-x64-musl@15.0.4-canary.23': + '@next/swc-linux-x64-musl@15.2.0-canary.33': optional: true - '@next/swc-win32-arm64-msvc@15.0.4-canary.23': + '@next/swc-win32-arm64-msvc@15.2.0-canary.33': optional: true - '@next/swc-win32-x64-msvc@15.0.4-canary.23': + '@next/swc-win32-x64-msvc@15.2.0-canary.33': optional: true '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': @@ -5810,45 +5810,45 @@ snapshots: '@protobufjs/utf8@1.1.0': {} - '@react-aria/focus@3.18.3(react@19.0.0-rc-66855b96-20241106)': + '@react-aria/focus@3.18.3(react@19.0.0)': dependencies: - '@react-aria/interactions': 3.22.3(react@19.0.0-rc-66855b96-20241106) - '@react-aria/utils': 3.25.3(react@19.0.0-rc-66855b96-20241106) - '@react-types/shared': 3.25.0(react@19.0.0-rc-66855b96-20241106) + '@react-aria/interactions': 3.22.3(react@19.0.0) + '@react-aria/utils': 3.25.3(react@19.0.0) + '@react-types/shared': 3.25.0(react@19.0.0) '@swc/helpers': 0.5.5 clsx: 2.1.1 - react: 19.0.0-rc-66855b96-20241106 + react: 19.0.0 - '@react-aria/interactions@3.22.3(react@19.0.0-rc-66855b96-20241106)': + '@react-aria/interactions@3.22.3(react@19.0.0)': dependencies: - '@react-aria/ssr': 3.9.6(react@19.0.0-rc-66855b96-20241106) - '@react-aria/utils': 3.25.3(react@19.0.0-rc-66855b96-20241106) - '@react-types/shared': 3.25.0(react@19.0.0-rc-66855b96-20241106) + '@react-aria/ssr': 3.9.6(react@19.0.0) + '@react-aria/utils': 3.25.3(react@19.0.0) + '@react-types/shared': 3.25.0(react@19.0.0) '@swc/helpers': 0.5.5 - react: 19.0.0-rc-66855b96-20241106 + react: 19.0.0 - '@react-aria/ssr@3.9.6(react@19.0.0-rc-66855b96-20241106)': + '@react-aria/ssr@3.9.6(react@19.0.0)': dependencies: '@swc/helpers': 0.5.5 - react: 19.0.0-rc-66855b96-20241106 + react: 19.0.0 - '@react-aria/utils@3.25.3(react@19.0.0-rc-66855b96-20241106)': + '@react-aria/utils@3.25.3(react@19.0.0)': dependencies: - '@react-aria/ssr': 3.9.6(react@19.0.0-rc-66855b96-20241106) - '@react-stately/utils': 3.10.4(react@19.0.0-rc-66855b96-20241106) - '@react-types/shared': 3.25.0(react@19.0.0-rc-66855b96-20241106) + '@react-aria/ssr': 3.9.6(react@19.0.0) + '@react-stately/utils': 3.10.4(react@19.0.0) + '@react-types/shared': 3.25.0(react@19.0.0) '@swc/helpers': 0.5.5 clsx: 2.1.1 - react: 19.0.0-rc-66855b96-20241106 + react: 19.0.0 - '@react-stately/utils@3.10.4(react@19.0.0-rc-66855b96-20241106)': + '@react-stately/utils@3.10.4(react@19.0.0)': dependencies: '@swc/helpers': 0.5.13 - react: 19.0.0-rc-66855b96-20241106 + react: 19.0.0 - '@react-types/shared@3.25.0(react@19.0.0-rc-66855b96-20241106)': + '@react-types/shared@3.25.0(react@19.0.0)': dependencies: - react: 19.0.0-rc-66855b96-20241106 + react: 19.0.0 '@rollup/rollup-android-arm-eabi@4.25.0': optional: true @@ -5922,6 +5922,10 @@ snapshots: dependencies: tslib: 2.7.0 + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + '@swc/helpers@0.5.5': dependencies: '@swc/counter': 0.1.3 @@ -5937,11 +5941,11 @@ snapshots: mini-svg-data-uri: 1.4.4 tailwindcss: 3.4.14 - '@tanstack/react-virtual@3.10.6(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + '@tanstack/react-virtual@3.10.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@tanstack/virtual-core': 3.10.6 - react: 19.0.0-rc-66855b96-20241106 - react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) '@tanstack/virtual-core@3.10.6': {} @@ -5966,15 +5970,15 @@ snapshots: lodash: 4.17.21 redent: 3.0.0 - '@testing-library/react@16.0.1(@testing-library/dom@10.4.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)': + '@testing-library/react@16.0.1(@testing-library/dom@10.4.0)(@types/react-dom@19.0.2(@types/react@19.0.2))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@babel/runtime': 7.25.6 '@testing-library/dom': 10.4.0 - react: 19.0.0-rc-66855b96-20241106 - react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) optionalDependencies: - '@types/react': types-react@19.0.0-rc.1 - '@types/react-dom': types-react-dom@19.0.0-rc.1 + '@types/react': 19.0.2 + '@types/react-dom': 19.0.2(@types/react@19.0.2) '@types/aria-query@5.0.4': {} @@ -6011,11 +6015,12 @@ snapshots: dependencies: undici-types: 6.19.8 - '@types/prop-types@15.7.12': {} - - '@types/react@18.3.12': + '@types/react-dom@19.0.2(@types/react@19.0.2)': + dependencies: + '@types/react': 19.0.2 + + '@types/react@19.0.2': dependencies: - '@types/prop-types': 15.7.12 csstype: 3.1.3 '@types/sinonjs__fake-timers@8.1.1': {} @@ -6142,12 +6147,12 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@vercel/analytics@1.3.1(next@15.0.4-canary.23(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(sass@1.80.7))(react@19.0.0-rc-66855b96-20241106)': + '@vercel/analytics@1.3.1(next@15.2.0-canary.33(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7))(react@19.0.0)': dependencies: server-only: 0.0.1 optionalDependencies: - next: 15.0.4-canary.23(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(sass@1.80.7) - react: 19.0.0-rc-66855b96-20241106 + next: 15.2.0-canary.33(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7) + react: 19.0.0 '@vercel/git-hooks@1.0.0': {} @@ -8212,40 +8217,40 @@ snapshots: negotiator@1.0.0: {} - next-intl@3.25.1(next@15.0.4-canary.23(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(sass@1.80.7))(react@19.0.0-rc-66855b96-20241106): + next-intl@3.25.1(next@15.2.0-canary.33(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7))(react@19.0.0): dependencies: '@formatjs/intl-localematcher': 0.5.4 negotiator: 1.0.0 - next: 15.0.4-canary.23(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(sass@1.80.7) - react: 19.0.0-rc-66855b96-20241106 - use-intl: 3.25.1(react@19.0.0-rc-66855b96-20241106) + next: 15.2.0-canary.33(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7) + react: 19.0.0 + use-intl: 3.25.1(react@19.0.0) - next-themes@0.2.1(next@15.0.4-canary.23(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(sass@1.80.7))(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106): + next-themes@0.2.1(next@15.2.0-canary.33(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7))(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: - next: 15.0.4-canary.23(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(sass@1.80.7) - react: 19.0.0-rc-66855b96-20241106 - react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + next: 15.2.0-canary.33(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) - next@15.0.4-canary.23(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(sass@1.80.7): + next@15.2.0-canary.33(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7): dependencies: - '@next/env': 15.0.4-canary.23 + '@next/env': 15.2.0-canary.33 '@swc/counter': 0.1.3 - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.15 busboy: 1.6.0 caniuse-lite: 1.0.30001680 postcss: 8.4.31 - react: 19.0.0-rc-66855b96-20241106 - react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) - styled-jsx: 5.1.6(@babel/core@7.26.0)(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + styled-jsx: 5.1.6(@babel/core@7.26.0)(react@19.0.0) optionalDependencies: - '@next/swc-darwin-arm64': 15.0.4-canary.23 - '@next/swc-darwin-x64': 15.0.4-canary.23 - '@next/swc-linux-arm64-gnu': 15.0.4-canary.23 - '@next/swc-linux-arm64-musl': 15.0.4-canary.23 - '@next/swc-linux-x64-gnu': 15.0.4-canary.23 - '@next/swc-linux-x64-musl': 15.0.4-canary.23 - '@next/swc-win32-arm64-msvc': 15.0.4-canary.23 - '@next/swc-win32-x64-msvc': 15.0.4-canary.23 + '@next/swc-darwin-arm64': 15.2.0-canary.33 + '@next/swc-darwin-x64': 15.2.0-canary.33 + '@next/swc-linux-arm64-gnu': 15.2.0-canary.33 + '@next/swc-linux-arm64-musl': 15.2.0-canary.33 + '@next/swc-linux-x64-gnu': 15.2.0-canary.33 + '@next/swc-linux-x64-musl': 15.2.0-canary.33 + '@next/swc-win32-arm64-msvc': 15.2.0-canary.33 + '@next/swc-win32-x64-msvc': 15.2.0-canary.33 '@playwright/test': 1.48.2 sass: 1.80.7 sharp: 0.33.5 @@ -8603,9 +8608,9 @@ snapshots: punycode@2.3.1: {} - qrcode.react@3.1.0(react@19.0.0-rc-66855b96-20241106): + qrcode.react@3.1.0(react@19.0.0): dependencies: - react: 19.0.0-rc-66855b96-20241106 + react: 19.0.0 qs@6.13.0: dependencies: @@ -8613,14 +8618,14 @@ snapshots: queue-microtask@1.2.3: {} - react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106): + react-dom@19.0.0(react@19.0.0): dependencies: - react: 19.0.0-rc-66855b96-20241106 - scheduler: 0.25.0-rc-66855b96-20241106 + react: 19.0.0 + scheduler: 0.25.0 - react-hook-form@7.39.5(react@19.0.0-rc-66855b96-20241106): + react-hook-form@7.39.5(react@19.0.0): dependencies: - react: 19.0.0-rc-66855b96-20241106 + react: 19.0.0 react-is@16.13.1: {} @@ -8628,7 +8633,7 @@ snapshots: react-refresh@0.14.2: {} - react@19.0.0-rc-66855b96-20241106: {} + react@19.0.0: {} read-cache@1.0.0: dependencies: @@ -8782,7 +8787,7 @@ snapshots: dependencies: xmlchars: 2.2.0 - scheduler@0.25.0-rc-66855b96-20241106: {} + scheduler@0.25.0: {} semver@6.3.1: {} @@ -9041,10 +9046,10 @@ snapshots: strip-json-comments@3.1.1: {} - styled-jsx@5.1.6(@babel/core@7.26.0)(react@19.0.0-rc-66855b96-20241106): + styled-jsx@5.1.6(@babel/core@7.26.0)(react@19.0.0): dependencies: client-only: 0.0.1 - react: 19.0.0-rc-66855b96-20241106 + react: 19.0.0 optionalDependencies: '@babel/core': 7.26.0 @@ -9072,11 +9077,11 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - swr@2.2.5(react@19.0.0-rc-66855b96-20241106): + swr@2.2.5(react@19.0.0): dependencies: client-only: 0.0.1 - react: 19.0.0-rc-66855b96-20241106 - use-sync-external-store: 1.2.2(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0 + use-sync-external-store: 1.2.2(react@19.0.0) symbol-tree@3.2.4: {} @@ -9329,14 +9334,6 @@ snapshots: is-typed-array: 1.1.13 possible-typed-array-names: 1.0.0 - types-react-dom@19.0.0-rc.1: - dependencies: - '@types/react': 18.3.12 - - types-react@19.0.0-rc.1: - dependencies: - csstype: 3.1.3 - typescript@5.6.3: {} unbox-primitive@1.0.2: @@ -9374,15 +9371,15 @@ snapshots: dependencies: punycode: 2.3.1 - use-intl@3.25.1(react@19.0.0-rc-66855b96-20241106): + use-intl@3.25.1(react@19.0.0): dependencies: '@formatjs/fast-memoize': 2.2.3 intl-messageformat: 10.7.7 - react: 19.0.0-rc-66855b96-20241106 + react: 19.0.0 - use-sync-external-store@1.2.2(react@19.0.0-rc-66855b96-20241106): + use-sync-external-store@1.2.2(react@19.0.0): dependencies: - react: 19.0.0-rc-66855b96-20241106 + react: 19.0.0 util-deprecate@1.0.2: {} diff --git a/turbo.json b/turbo.json index 2817c8c157..bf02da7ea0 100644 --- a/turbo.json +++ b/turbo.json @@ -4,16 +4,21 @@ "globalDependencies": ["**/.env.*local"], "globalEnv": [ "DEBUG", + "VERCEL_URL", + "EMAIL_VERIFICATION", + "EU1_AUDIENCE", + "EU1_SYSTEM_USER_ID", + "EU1_SYSTEM_USER_PRIVATE_KEY", + "US1_AUDIENCE", + "US1_SYSTEM_USER_ID", + "US1_SYSTEM_USER_PRIVATE_KEY", + "AUDIENCE", + "SYSTEM_USER_ID", + "SYSTEM_USER_PRIVATE_KEY", "ZITADEL_API_URL", "ZITADEL_SERVICE_USER_ID", "ZITADEL_SERVICE_USER_TOKEN", - "ZITADEL_SYSTEM_API_URL", - "ZITADEL_SYSTEM_API_USERID", - "ZITADEL_SYSTEM_API_KEY", - "ZITADEL_ISSUER", - "ZITADEL_ADMIN_TOKEN", - "EMAIL_VERIFICATION", - "VERCEL_URL" + "NEXT_PUBLIC_BASE_PATH" ], "tasks": { "generate": {