Merge branch 'main' into dependabot/github_actions/actions/setup-node-4

This commit is contained in:
Max Peintner
2025-02-03 09:44:16 +01:00
committed by GitHub
59 changed files with 2654 additions and 914 deletions

View File

@@ -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
DEBUG=true
NEXT_PUBLIC_BASE_PATH=""

31
apps/login/next-env-vars.d.ts vendored Normal file
View File

@@ -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;
}
}

View File

@@ -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,

View File

@@ -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"
}
}

View File

@@ -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

View File

@@ -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();

View File

@@ -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<Record<string | number | symbol, string | undefined>>;
@@ -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 <Alert>{tError("unknownContext")}</Alert>;
}
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;
});

View File

@@ -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 (
<DynamicTheme branding={branding}>

View File

@@ -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 (

View File

@@ -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<Record<string | number | symbol, string | undefined>>;
@@ -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 (
<DynamicTheme branding={branding}>

View File

@@ -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<Record<string | number | symbol, string | undefined>>;
@@ -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 (
<DynamicTheme branding={branding}>

View File

@@ -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") {

View File

@@ -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<Record<string | number | symbol, string | undefined>>;
@@ -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 (
<DynamicTheme branding={branding}>

View File

@@ -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<Record<string | number | symbol, string | undefined>>;
@@ -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 (
<DynamicTheme branding={branding}>

View File

@@ -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<Session>): {
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);

View File

@@ -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 (
<DynamicTheme branding={branding}>

View File

@@ -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 {

View File

@@ -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<Record<string | number | symbol, string | undefined>>;
@@ -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 (
<DynamicTheme branding={branding}>
@@ -66,7 +79,6 @@ export default async function Page(props: {
authRequestId={authRequestId}
altPassword={altPassword === "true"}
organization={organization}
loginSettings={loginSettings}
/>
)}
</div>

View File

@@ -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<Record<string | number | symbol, string | undefined>>;
@@ -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 (
<DynamicTheme branding={branding}>

View File

@@ -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<Record<string | number | symbol, string | undefined>>;
}) {
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 (
<DynamicTheme branding={branding}>

View File

@@ -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<Record<string | number | symbol, string | undefined>>;
@@ -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 (
<DynamicTheme branding={branding}>

View File

@@ -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<Record<string | number | symbol, string | undefined>>;
@@ -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") {

View File

@@ -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<Record<string | number | symbol, string | undefined>>;
@@ -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 (

View File

@@ -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<Record<string | number | symbol, string | undefined>>;
@@ -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 ? (
<DynamicTheme branding={branding}>

View File

@@ -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<any> }) {
@@ -51,14 +63,30 @@ export default async function Page(props: { searchParams: Promise<any> }) {
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 (

View File

@@ -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<Record<string | number | symbol, string | undefined>>;
@@ -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) => {

View File

@@ -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<Record<string | number | symbol, string | undefined>>;
@@ -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 (
<DynamicTheme branding={branding}>

View File

@@ -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<any> }) {
const searchParams = await props.searchParams;
@@ -23,7 +25,19 @@ export default async function Page(props: { searchParams: Promise<any> }) {
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<any> }) {
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<any> }) {
} 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") {

View File

@@ -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<Session[]> {
const response = await listSessions(
ids.filter((id: string | undefined) => !!id),
);
async function loadSessions({
serviceUrl,
serviceRegion,
ids,
}: {
serviceUrl: string;
serviceRegion: string;
ids: string[];
}): Promise<Session[]> {
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<boolean> {
async function isSessionValid(
serviceUrl: string,
serviceRegion: string,
session: Session,
): Promise<boolean> {
// 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<boolean> {
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<boolean> {
}
} 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<boolean> {
}
async function findValidSession(
serviceUrl: string,
serviceRegion: string,
sessions: Session[],
authRequest: AuthRequest,
): Promise<Session | undefined> {
@@ -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 {

View File

@@ -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");

View File

@@ -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;

View File

@@ -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";

40
apps/login/src/lib/api.ts Normal file
View File

@@ -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,
});
}

View File

@@ -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" };

View File

@@ -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.");
}
});
}

View File

@@ -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<Session> {
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<Session> {
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);
}

View File

@@ -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,

View File

@@ -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" };

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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<RegisterPasskeyResponse> {
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" };

View File

@@ -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(

View File

@@ -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" };

View File

@@ -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");

View File

@@ -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 });
}

View File

@@ -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(

View File

@@ -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<T extends ServiceClass>(
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<T>(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") || "",
};
}

View File

@@ -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<Session | undefined> {
type LoadMostRecentSessionParams = {
serviceUrl: string;
serviceRegion: string;
sessionParams: {
loginName?: string;
organization?: string;
};
};
export async function loadMostRecentSession({
serviceUrl,
serviceRegion,
sessionParams,
}: LoadMostRecentSessionParams): Promise<Session | undefined> {
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);
}

View File

@@ -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,
});

File diff suppressed because it is too large Load Diff

View File

@@ -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,

View File

@@ -2,6 +2,7 @@
"extends": "@zitadel/tsconfig/nextjs.json",
"compilerOptions": {
"jsx": "preserve",
"target": "es2022",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]

View File

@@ -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": {

View File

@@ -1,5 +1,11 @@
# @zitadel/client
## 1.0.4
### Patch Changes
- 28dc956: dynamic properties for system token utility
## 1.0.3
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@zitadel/client",
"version": "1.0.3",
"version": "1.0.4",
"license": "MIT",
"publishConfig": {
"access": "public"

View File

@@ -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";

View File

@@ -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"));
}

341
pnpm-lock.yaml generated
View File

@@ -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: {}

View File

@@ -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": {