Files
zitadel/apps/login/src/app/(login)/verify/page.tsx
T
df75be96ff Merge commit from fork
* fix: Improve host header handling and deployment mode detection

* ZITADEL_CLOUD env

* refactor service config

* service config

* cleanup

* cleanup

* util for original and public host

* instanceHost, publicHost

* cleanup constructUrl

* cleanup middleware

* add x-zitadel-forward-host as fallback for current cloud setup

* fix build

* fix: lint

* fix: update test mocks

* fix: provide basepath to unit tests

* host handling

* unit tests

* imports

* fix: getPublicHost instead of getInstanceHost

* unit tests

* update nextjs, implement jwt verify for logout_token

* pass logout information in jwt to login v2

* handle hosts in jwks url

* point idp callbacks to the api and not the login UI

* logout_hint out of logout token, typecheck

* rm unused imports

* fix oidc integration test

---------

Co-authored-by: Livio Spring <[email protected]>
2025-12-08 10:01:49 +01:00

178 lines
5.2 KiB
TypeScript

import { Alert, AlertType } from "@/components/alert";
import { DynamicTheme } from "@/components/dynamic-theme";
import { Translated } from "@/components/translated";
import { UserAvatar } from "@/components/user-avatar";
import { VerifyForm } from "@/components/verify-form";
import { getPublicHostWithProtocol } from "@/lib/server/host";
import { sendEmailCode, sendInviteEmailCode } from "@/lib/server/verify";
import { getServiceConfig } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings, getUserByID } from "@/lib/zitadel";
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { headers } from "next/headers";
export async function generateMetadata(): Promise<Metadata> {
const t = await getTranslations("verify");
return { title: t("verify.title") };
}
export default async function Page(props: { searchParams: Promise<any> }) {
const searchParams = await props.searchParams;
const { userId, loginName, code, organization, requestId, invite, send } = searchParams;
const _headers = await headers();
const { serviceConfig } = getServiceConfig(_headers);
const branding = await getBrandingSettings({ serviceConfig, organization });
let sessionFactors;
let user: User | undefined;
let human: HumanUser | undefined;
let id: string | undefined;
let error: string | undefined;
const doSend = send === "true";
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
async function sendEmail(userId: string) {
const hostWithProtocol = await getPublicHostWithProtocol(_headers);
if (invite === "true") {
await sendInviteEmailCode({
userId,
urlTemplate:
`${hostWithProtocol}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` +
(requestId ? `&requestId=${requestId}` : ""),
}).catch((apiError) => {
console.error("Could not send invitation email", apiError);
error = "inviteSendFailed";
});
} else {
await sendEmailCode({
userId,
urlTemplate:
`${hostWithProtocol}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
(requestId ? `&requestId=${requestId}` : ""),
}).catch((apiError) => {
console.error("Could not send verification email", apiError);
error = "emailSendFailed";
});
}
}
if ("loginName" in searchParams) {
sessionFactors = await loadMostRecentSession({
serviceConfig,
sessionParams: {
loginName,
organization,
},
});
if (doSend && sessionFactors?.factors?.user?.id) {
await sendEmail(sessionFactors.factors.user.id);
}
} else if ("userId" in searchParams && userId) {
if (doSend) {
await sendEmail(userId);
}
const userResponse = await getUserByID({ serviceConfig, userId });
if (userResponse) {
user = userResponse.user;
if (user?.type.case === "human") {
human = user.type.value as HumanUser;
}
}
}
id = userId ?? sessionFactors?.factors?.user?.id;
if (!id) {
throw Error("Failed to get user id");
}
const params = new URLSearchParams({
userId: userId,
initial: "true", // defines that a code is not required and is therefore not shown in the UI
});
if (loginName) {
params.set("loginName", loginName);
}
if (organization) {
params.set("organization", organization);
}
if (requestId) {
params.set("requestId", requestId);
}
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col space-y-4">
<h1>
<Translated i18nKey="verify.title" namespace="verify" />
</h1>
<p className="ztdl-p">
<Translated i18nKey="verify.description" namespace="verify" />
</p>
{sessionFactors ? (
<UserAvatar
loginName={loginName ?? sessionFactors.factors?.user?.loginName}
displayName={sessionFactors.factors?.user?.displayName}
showDropdown
searchParams={searchParams}
></UserAvatar>
) : (
user && (
<UserAvatar loginName={user.preferredLoginName} displayName={human?.profile?.displayName} showDropdown={false} />
)
)}
</div>
<div className="w-full">
{error && (
<div className="py-4">
<Alert>
<Translated i18nKey={`errors.${error}`} namespace="verify" />
</Alert>
</div>
)}
{!id && (
<div className="py-4">
<Alert>
<Translated i18nKey="unknownContext" namespace="error" />
</Alert>
</div>
)}
{id && send && (
<div className="w-full py-4">
<Alert type={AlertType.INFO}>
<Translated i18nKey="verify.codeSent" namespace="verify" />
</Alert>
</div>
)}
<VerifyForm
loginName={loginName}
organization={organization}
userId={id}
code={code}
isInvite={invite === "true"}
requestId={requestId}
/>
</div>
</DynamicTheme>
);
}