mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-13 10:57:32 +00:00
@@ -174,13 +174,15 @@
|
|||||||
},
|
},
|
||||||
"verify": {
|
"verify": {
|
||||||
"userIdMissing": "Keine Benutzer-ID angegeben!",
|
"userIdMissing": "Keine Benutzer-ID angegeben!",
|
||||||
"success": "Erfolgreich verifiziert",
|
"successTitle": "Benutzer verifiziert",
|
||||||
|
"successDescription": "Der Benutzer wurde erfolgreich verifiziert.",
|
||||||
"setupAuthenticator": "Authentifikator einrichten",
|
"setupAuthenticator": "Authentifikator einrichten",
|
||||||
"verify": {
|
"verify": {
|
||||||
"title": "Benutzer verifizieren",
|
"title": "Benutzer verifizieren",
|
||||||
"description": "Geben Sie den Code ein, der in der Bestätigungs-E-Mail angegeben ist.",
|
"description": "Geben Sie den Code ein, der in der Bestätigungs-E-Mail angegeben ist.",
|
||||||
"noCodeReceived": "Keinen Code erhalten?",
|
"noCodeReceived": "Keinen Code erhalten?",
|
||||||
"resendCode": "Code erneut senden",
|
"resendCode": "Code erneut senden",
|
||||||
|
"codeSent": "Ein Code wurde gerade an Ihre E-Mail-Adresse gesendet.",
|
||||||
"submit": "Weiter"
|
"submit": "Weiter"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -174,13 +174,15 @@
|
|||||||
},
|
},
|
||||||
"verify": {
|
"verify": {
|
||||||
"userIdMissing": "No userId provided!",
|
"userIdMissing": "No userId provided!",
|
||||||
"success": "The user has been verified successfully.",
|
"successTitle": "User verified",
|
||||||
|
"successDescription": "The user has been verified successfully.",
|
||||||
"setupAuthenticator": "Setup authenticator",
|
"setupAuthenticator": "Setup authenticator",
|
||||||
"verify": {
|
"verify": {
|
||||||
"title": "Verify user",
|
"title": "Verify user",
|
||||||
"description": "Enter the Code provided in the verification email.",
|
"description": "Enter the Code provided in the verification email.",
|
||||||
"noCodeReceived": "Didn't receive a code?",
|
"noCodeReceived": "Didn't receive a code?",
|
||||||
"resendCode": "Resend code",
|
"resendCode": "Resend code",
|
||||||
|
"codeSent": "A code has just been sent to your email address.",
|
||||||
"submit": "Continue"
|
"submit": "Continue"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -174,13 +174,15 @@
|
|||||||
},
|
},
|
||||||
"verify": {
|
"verify": {
|
||||||
"userIdMissing": "¡No se proporcionó userId!",
|
"userIdMissing": "¡No se proporcionó userId!",
|
||||||
"success": "¡Verificación exitosa!",
|
"successTitle": "Usuario verificado",
|
||||||
|
"successDescription": "El usuario ha sido verificado con éxito.",
|
||||||
"setupAuthenticator": "Configurar autenticador",
|
"setupAuthenticator": "Configurar autenticador",
|
||||||
"verify": {
|
"verify": {
|
||||||
"title": "Verificar usuario",
|
"title": "Verificar usuario",
|
||||||
"description": "Introduce el código proporcionado en el correo electrónico de verificación.",
|
"description": "Introduce el código proporcionado en el correo electrónico de verificación.",
|
||||||
"noCodeReceived": "¿No recibiste un código?",
|
"noCodeReceived": "¿No recibiste un código?",
|
||||||
"resendCode": "Reenviar código",
|
"resendCode": "Reenviar código",
|
||||||
|
"codeSent": "Se ha enviado un código a tu dirección de correo electrónico.",
|
||||||
"submit": "Continuar"
|
"submit": "Continuar"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -174,13 +174,15 @@
|
|||||||
},
|
},
|
||||||
"verify": {
|
"verify": {
|
||||||
"userIdMissing": "Nessun userId fornito!",
|
"userIdMissing": "Nessun userId fornito!",
|
||||||
"success": "Verifica effettuata con successo!",
|
"successTitle": "Utente verificato",
|
||||||
|
"successDescription": "L'utente è stato verificato con successo.",
|
||||||
"setupAuthenticator": "Configura autenticatore",
|
"setupAuthenticator": "Configura autenticatore",
|
||||||
"verify": {
|
"verify": {
|
||||||
"title": "Verifica utente",
|
"title": "Verifica utente",
|
||||||
"description": "Inserisci il codice fornito nell'email di verifica.",
|
"description": "Inserisci il codice fornito nell'email di verifica.",
|
||||||
"noCodeReceived": "Non hai ricevuto un codice?",
|
"noCodeReceived": "Non hai ricevuto un codice?",
|
||||||
"resendCode": "Invia di nuovo il codice",
|
"resendCode": "Invia di nuovo il codice",
|
||||||
|
"codeSent": "Un codice è stato appena inviato al tuo indirizzo email.",
|
||||||
"submit": "Continua"
|
"submit": "Continua"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -174,13 +174,15 @@
|
|||||||
},
|
},
|
||||||
"verify": {
|
"verify": {
|
||||||
"userIdMissing": "Nie podano identyfikatora użytkownika!",
|
"userIdMissing": "Nie podano identyfikatora użytkownika!",
|
||||||
"success": "Użytkownik został pomyślnie zweryfikowany.",
|
"successTitle": "Weryfikacja zakończona",
|
||||||
|
"successDescription": "Użytkownik został pomyślnie zweryfikowany.",
|
||||||
"setupAuthenticator": "Skonfiguruj uwierzytelnianie",
|
"setupAuthenticator": "Skonfiguruj uwierzytelnianie",
|
||||||
"verify": {
|
"verify": {
|
||||||
"title": "Zweryfikuj użytkownika",
|
"title": "Zweryfikuj użytkownika",
|
||||||
"description": "Wprowadź kod z wiadomości weryfikacyjnej.",
|
"description": "Wprowadź kod z wiadomości weryfikacyjnej.",
|
||||||
"noCodeReceived": "Nie otrzymałeś kodu?",
|
"noCodeReceived": "Nie otrzymałeś kodu?",
|
||||||
"resendCode": "Wyślij kod ponownie",
|
"resendCode": "Wyślij kod ponownie",
|
||||||
|
"codeSent": "Kod został właśnie wysłany na twój adres e-mail.",
|
||||||
"submit": "Kontynuuj"
|
"submit": "Kontynuuj"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -174,13 +174,15 @@
|
|||||||
},
|
},
|
||||||
"verify": {
|
"verify": {
|
||||||
"userIdMissing": "Не указан userId!",
|
"userIdMissing": "Не указан userId!",
|
||||||
"success": "Пользователь успешно подтверждён.",
|
"successTitle": "Пользователь подтверждён",
|
||||||
|
"successDescription": "Пользователь успешно подтверждён.",
|
||||||
"setupAuthenticator": "Настроить аутентификатор",
|
"setupAuthenticator": "Настроить аутентификатор",
|
||||||
"verify": {
|
"verify": {
|
||||||
"title": "Подтверждение пользователя",
|
"title": "Подтверждение пользователя",
|
||||||
"description": "Введите код из письма подтверждения.",
|
"description": "Введите код из письма подтверждения.",
|
||||||
"noCodeReceived": "Не получили код?",
|
"noCodeReceived": "Не получили код?",
|
||||||
"resendCode": "Отправить код повторно",
|
"resendCode": "Отправить код повторно",
|
||||||
|
"codeSent": "Код отправлен на ваш email.",
|
||||||
"submit": "Продолжить"
|
"submit": "Продолжить"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -174,13 +174,15 @@
|
|||||||
},
|
},
|
||||||
"verify": {
|
"verify": {
|
||||||
"userIdMissing": "未提供用户 ID!",
|
"userIdMissing": "未提供用户 ID!",
|
||||||
"success": "用户验证成功。",
|
"successTitle": "用户已验证",
|
||||||
|
"successDescription": "用户已成功验证。",
|
||||||
"setupAuthenticator": "设置认证器",
|
"setupAuthenticator": "设置认证器",
|
||||||
"verify": {
|
"verify": {
|
||||||
"title": "验证用户",
|
"title": "验证用户",
|
||||||
"description": "输入验证邮件中的验证码。",
|
"description": "输入验证邮件中的验证码。",
|
||||||
"noCodeReceived": "没有收到验证码?",
|
"noCodeReceived": "没有收到验证码?",
|
||||||
"resendCode": "重发验证码",
|
"resendCode": "重发验证码",
|
||||||
|
"codeSent": "刚刚发送了一封包含验证码的电子邮件。",
|
||||||
"submit": "继续"
|
"submit": "继续"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -7,6 +7,7 @@ import { UserAvatar } from "@/components/user-avatar";
|
|||||||
import { getSessionCookieById } from "@/lib/cookies";
|
import { getSessionCookieById } from "@/lib/cookies";
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
import { loadMostRecentSession } from "@/lib/session";
|
import { loadMostRecentSession } from "@/lib/session";
|
||||||
|
import { checkUserVerification } from "@/lib/verify-helper";
|
||||||
import {
|
import {
|
||||||
getActiveIdentityProviders,
|
getActiveIdentityProviders,
|
||||||
getBrandingSettings,
|
getBrandingSettings,
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||||
import { getLocale, getTranslations } from "next-intl/server";
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
export default async function Page(props: {
|
export default async function Page(props: {
|
||||||
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
||||||
@@ -92,20 +94,50 @@ export default async function Page(props: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sessionWithData) {
|
if (
|
||||||
|
!sessionWithData ||
|
||||||
|
!sessionWithData.factors ||
|
||||||
|
!sessionWithData.factors.user
|
||||||
|
) {
|
||||||
return <Alert>{tError("unknownContext")}</Alert>;
|
return <Alert>{tError("unknownContext")}</Alert>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const branding = await getBrandingSettings({
|
const branding = await getBrandingSettings({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
organization: sessionWithData.factors?.user?.organizationId,
|
organization: sessionWithData.factors.user?.organizationId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const loginSettings = await getLoginSettings({
|
const loginSettings = await getLoginSettings({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
organization: sessionWithData.factors?.user?.organizationId,
|
organization: sessionWithData.factors.user?.organizationId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// check if user was verified recently
|
||||||
|
const isUserVerified = await checkUserVerification(
|
||||||
|
sessionWithData.factors.user?.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isUserVerified) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
loginName: sessionWithData.factors.user.loginName as string,
|
||||||
|
invite: "true",
|
||||||
|
send: "true", // set this to true to request a new code immediately
|
||||||
|
});
|
||||||
|
|
||||||
|
if (requestId) {
|
||||||
|
params.append("requestId", requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organization || sessionWithData.factors.user.organizationId) {
|
||||||
|
params.append(
|
||||||
|
"organization",
|
||||||
|
organization ?? (sessionWithData.factors.user.organizationId as string),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
redirect(`/verify?` + params);
|
||||||
|
}
|
||||||
|
|
||||||
const identityProviders = await getActiveIdentityProviders({
|
const identityProviders = await getActiveIdentityProviders({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
orgId: sessionWithData.factors?.user?.organizationId,
|
orgId: sessionWithData.factors?.user?.organizationId,
|
||||||
@@ -152,13 +184,12 @@ export default async function Page(props: {
|
|||||||
></ChooseAuthenticatorToSetup>
|
></ChooseAuthenticatorToSetup>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{loginSettings?.allowExternalIdp && identityProviders && (
|
{loginSettings?.allowExternalIdp && !!identityProviders.length && (
|
||||||
<>
|
<>
|
||||||
{identityProviders.length && (
|
<div className="py-3 flex flex-col">
|
||||||
<div className="py-3 flex flex-col">
|
<p className="ztdl-p text-center">{t("linkWithIDP")}</p>
|
||||||
<p className="ztdl-p text-center">{t("linkWithIDP")}</p>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<SignInWithIdp
|
<SignInWithIdp
|
||||||
identityProviders={identityProviders}
|
identityProviders={identityProviders}
|
||||||
requestId={requestId}
|
requestId={requestId}
|
||||||
|
@@ -1,73 +0,0 @@
|
|||||||
import { Alert, AlertType } from "@/components/alert";
|
|
||||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
|
||||||
import { InviteForm } from "@/components/invite-form";
|
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
|
||||||
import {
|
|
||||||
getBrandingSettings,
|
|
||||||
getDefaultOrg,
|
|
||||||
getLoginSettings,
|
|
||||||
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 searchParams = await props.searchParams;
|
|
||||||
const locale = getLocale();
|
|
||||||
const t = await getTranslations({ locale, namespace: "invite" });
|
|
||||||
|
|
||||||
let { firstname, lastname, email, organization } = searchParams;
|
|
||||||
|
|
||||||
const _headers = await headers();
|
|
||||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
|
||||||
|
|
||||||
if (!organization) {
|
|
||||||
const org = await getDefaultOrg({ serviceUrl });
|
|
||||||
if (!org) {
|
|
||||||
throw new Error("No default organization found");
|
|
||||||
}
|
|
||||||
|
|
||||||
organization = org.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginSettings = await getLoginSettings({
|
|
||||||
serviceUrl,
|
|
||||||
organization,
|
|
||||||
});
|
|
||||||
|
|
||||||
const passwordComplexitySettings = await getPasswordComplexitySettings({
|
|
||||||
serviceUrl,
|
|
||||||
organization,
|
|
||||||
});
|
|
||||||
|
|
||||||
const branding = await getBrandingSettings({
|
|
||||||
serviceUrl,
|
|
||||||
organization,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DynamicTheme branding={branding}>
|
|
||||||
<div className="flex flex-col items-center space-y-4">
|
|
||||||
<h1>{t("title")}</h1>
|
|
||||||
<p className="ztdl-p">{t("description")}</p>
|
|
||||||
|
|
||||||
{!loginSettings?.allowRegister ? (
|
|
||||||
<Alert type={AlertType.ALERT}>{t("notAllowed")}</Alert>
|
|
||||||
) : (
|
|
||||||
<Alert type={AlertType.INFO}>{t("info")}</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{passwordComplexitySettings && loginSettings?.allowRegister && (
|
|
||||||
<InviteForm
|
|
||||||
organization={organization}
|
|
||||||
firstname={firstname}
|
|
||||||
lastname={lastname}
|
|
||||||
email={email}
|
|
||||||
></InviteForm>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</DynamicTheme>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -1,81 +0,0 @@
|
|||||||
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-url";
|
|
||||||
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: {
|
|
||||||
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
|
||||||
}) {
|
|
||||||
const searchParams = await props.searchParams;
|
|
||||||
const locale = getLocale();
|
|
||||||
const t = await getTranslations({ locale, namespace: "invite" });
|
|
||||||
|
|
||||||
let { userId, organization } = searchParams;
|
|
||||||
|
|
||||||
const _headers = await headers();
|
|
||||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
|
||||||
|
|
||||||
if (!organization) {
|
|
||||||
const org = await getDefaultOrg({ serviceUrl });
|
|
||||||
if (!org) {
|
|
||||||
throw new Error("No default organization found");
|
|
||||||
}
|
|
||||||
|
|
||||||
organization = org.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const branding = await getBrandingSettings({
|
|
||||||
serviceUrl,
|
|
||||||
organization,
|
|
||||||
});
|
|
||||||
|
|
||||||
let user: User | undefined;
|
|
||||||
let human: HumanUser | undefined;
|
|
||||||
if (userId) {
|
|
||||||
const userResponse = await getUserByID({
|
|
||||||
serviceUrl,
|
|
||||||
userId,
|
|
||||||
});
|
|
||||||
if (userResponse) {
|
|
||||||
user = userResponse.user;
|
|
||||||
if (user?.type.case === "human") {
|
|
||||||
human = user.type.value as HumanUser;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DynamicTheme branding={branding}>
|
|
||||||
<div className="flex flex-col items-center space-y-4">
|
|
||||||
<h1>{t("success.title")}</h1>
|
|
||||||
<p className="ztdl-p">{t("success.description")}</p>
|
|
||||||
{user && (
|
|
||||||
<UserAvatar
|
|
||||||
loginName={user.preferredLoginName}
|
|
||||||
displayName={human?.profile?.displayName}
|
|
||||||
showDropdown={false}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{human?.email?.isVerified ? (
|
|
||||||
<Alert type={AlertType.INFO}>{t("success.verified")}</Alert>
|
|
||||||
) : (
|
|
||||||
<Alert type={AlertType.INFO}>{t("success.notVerifiedYet")}</Alert>
|
|
||||||
)}
|
|
||||||
<div className="mt-8 flex w-full flex-row items-center justify-between">
|
|
||||||
<span></span>
|
|
||||||
<Link href="/invite">
|
|
||||||
<Button type="submit" variant={ButtonVariants.Primary}>
|
|
||||||
{t("success.submit")}
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DynamicTheme>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -1,18 +1,12 @@
|
|||||||
import { Alert } from "@/components/alert";
|
import { Alert, AlertType } from "@/components/alert";
|
||||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
import { UserAvatar } from "@/components/user-avatar";
|
import { UserAvatar } from "@/components/user-avatar";
|
||||||
import { VerifyForm } from "@/components/verify-form";
|
import { VerifyForm } from "@/components/verify-form";
|
||||||
import { VerifyRedirectButton } from "@/components/verify-redirect-button";
|
import { sendEmailCode, sendInviteEmailCode } from "@/lib/server/verify";
|
||||||
import { sendEmailCode } from "@/lib/server/verify";
|
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
import { loadMostRecentSession } from "@/lib/session";
|
import { loadMostRecentSession } from "@/lib/session";
|
||||||
import {
|
import { getBrandingSettings, getUserByID } from "@/lib/zitadel";
|
||||||
getBrandingSettings,
|
|
||||||
getUserByID,
|
|
||||||
listAuthenticationMethodTypes,
|
|
||||||
} from "@/lib/zitadel";
|
|
||||||
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
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 { getLocale, getTranslations } from "next-intl/server";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
|
|
||||||
@@ -22,16 +16,11 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
|||||||
const t = await getTranslations({ locale, namespace: "verify" });
|
const t = await getTranslations({ locale, namespace: "verify" });
|
||||||
const tError = await getTranslations({ locale, namespace: "error" });
|
const tError = await getTranslations({ locale, namespace: "error" });
|
||||||
|
|
||||||
const { userId, loginName, code, organization, requestId, invite } =
|
const { userId, loginName, code, organization, requestId, invite, send } =
|
||||||
searchParams;
|
searchParams;
|
||||||
|
|
||||||
const _headers = await headers();
|
const _headers = await headers();
|
||||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||||
const host = _headers.get("host");
|
|
||||||
|
|
||||||
if (!host || typeof host !== "string") {
|
|
||||||
throw new Error("No host found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const branding = await getBrandingSettings({
|
const branding = await getBrandingSettings({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
@@ -43,10 +32,40 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
|||||||
let human: HumanUser | undefined;
|
let human: HumanUser | undefined;
|
||||||
let id: string | undefined;
|
let id: string | undefined;
|
||||||
|
|
||||||
const doSend = invite !== "true";
|
const doSend = send === "true";
|
||||||
|
|
||||||
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
|
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
|
||||||
|
|
||||||
|
async function sendEmail(userId: string) {
|
||||||
|
const host = _headers.get("host");
|
||||||
|
|
||||||
|
if (!host || typeof host !== "string") {
|
||||||
|
throw new Error("No host found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invite === "true") {
|
||||||
|
await sendInviteEmailCode({
|
||||||
|
userId,
|
||||||
|
urlTemplate:
|
||||||
|
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` +
|
||||||
|
(requestId ? `&requestId=${requestId}` : ""),
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("Could not send invitation email", error);
|
||||||
|
throw Error("Failed to send invitation email");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await sendEmailCode({
|
||||||
|
userId,
|
||||||
|
urlTemplate:
|
||||||
|
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
|
||||||
|
(requestId ? `&requestId=${requestId}` : ""),
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("Could not send verification email", error);
|
||||||
|
throw Error("Failed to send verification email");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ("loginName" in searchParams) {
|
if ("loginName" in searchParams) {
|
||||||
sessionFactors = await loadMostRecentSession({
|
sessionFactors = await loadMostRecentSession({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
@@ -57,29 +76,11 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (doSend && sessionFactors?.factors?.user?.id) {
|
if (doSend && sessionFactors?.factors?.user?.id) {
|
||||||
await sendEmailCode({
|
await sendEmail(sessionFactors.factors.user.id);
|
||||||
serviceUrl,
|
|
||||||
userId: sessionFactors?.factors?.user?.id,
|
|
||||||
urlTemplate:
|
|
||||||
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` +
|
|
||||||
(requestId ? `&requestId=${requestId}` : ""),
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error("Could not resend verification email", error);
|
|
||||||
throw Error("Failed to send verification email");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else if ("userId" in searchParams && userId) {
|
} else if ("userId" in searchParams && userId) {
|
||||||
if (doSend) {
|
if (doSend) {
|
||||||
await sendEmailCode({
|
await sendEmail(userId);
|
||||||
serviceUrl,
|
|
||||||
userId,
|
|
||||||
urlTemplate:
|
|
||||||
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` +
|
|
||||||
(requestId ? `&requestId=${requestId}` : ""),
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error("Could not resend verification email", error);
|
|
||||||
throw Error("Failed to send verification email");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const userResponse = await getUserByID({
|
const userResponse = await getUserByID({
|
||||||
@@ -96,12 +97,8 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
|||||||
|
|
||||||
id = userId ?? sessionFactors?.factors?.user?.id;
|
id = userId ?? sessionFactors?.factors?.user?.id;
|
||||||
|
|
||||||
let authMethods: AuthenticationMethodType[] | null = null;
|
if (!id) {
|
||||||
if (human?.email?.isVerified) {
|
throw Error("Failed to get user id");
|
||||||
const authMethodsResponse = await listAuthenticationMethodTypes(userId);
|
|
||||||
if (authMethodsResponse.authMethodTypes) {
|
|
||||||
authMethods = authMethodsResponse.authMethodTypes;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
@@ -138,6 +135,12 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{id && send && (
|
||||||
|
<div className="py-4 w-full">
|
||||||
|
<Alert type={AlertType.INFO}>{t("verify.codeSent")}</Alert>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{sessionFactors ? (
|
{sessionFactors ? (
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
loginName={loginName ?? sessionFactors.factors?.user?.loginName}
|
loginName={loginName ?? sessionFactors.factors?.user?.loginName}
|
||||||
@@ -155,27 +158,14 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{id &&
|
<VerifyForm
|
||||||
(human?.email?.isVerified ? (
|
loginName={loginName}
|
||||||
// show page for already verified users
|
organization={organization}
|
||||||
<VerifyRedirectButton
|
userId={id}
|
||||||
userId={id}
|
code={code}
|
||||||
loginName={loginName}
|
isInvite={invite === "true"}
|
||||||
organization={organization}
|
requestId={requestId}
|
||||||
requestId={requestId}
|
/>
|
||||||
authMethods={authMethods}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
// check if auth methods are set
|
|
||||||
<VerifyForm
|
|
||||||
loginName={loginName}
|
|
||||||
organization={organization}
|
|
||||||
userId={id}
|
|
||||||
code={code}
|
|
||||||
isInvite={invite === "true"}
|
|
||||||
requestId={requestId}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</DynamicTheme>
|
</DynamicTheme>
|
||||||
);
|
);
|
||||||
|
109
apps/login/src/app/(login)/verify/success/page.tsx
Normal file
109
apps/login/src/app/(login)/verify/success/page.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
|
import { UserAvatar } from "@/components/user-avatar";
|
||||||
|
import { getSessionCookieById } from "@/lib/cookies";
|
||||||
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
|
import { loadMostRecentSession } from "@/lib/session";
|
||||||
|
import {
|
||||||
|
getBrandingSettings,
|
||||||
|
getLoginSettings,
|
||||||
|
getSession,
|
||||||
|
getUserByID,
|
||||||
|
} from "@/lib/zitadel";
|
||||||
|
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||||
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
|
import { headers } from "next/headers";
|
||||||
|
|
||||||
|
async function loadSessionById(
|
||||||
|
serviceUrl: string,
|
||||||
|
sessionId: string,
|
||||||
|
organization?: string,
|
||||||
|
) {
|
||||||
|
const recent = await getSessionCookieById({ sessionId, organization });
|
||||||
|
return getSession({
|
||||||
|
serviceUrl,
|
||||||
|
sessionId: recent.id,
|
||||||
|
sessionToken: recent.token,
|
||||||
|
}).then((response) => {
|
||||||
|
if (response?.session) {
|
||||||
|
return response.session;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Page(props: { searchParams: Promise<any> }) {
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
|
const locale = getLocale();
|
||||||
|
const t = await getTranslations({ locale, namespace: "verify" });
|
||||||
|
|
||||||
|
const _headers = await headers();
|
||||||
|
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||||
|
|
||||||
|
const { loginName, requestId, organization, userId } = searchParams;
|
||||||
|
|
||||||
|
const branding = await getBrandingSettings({
|
||||||
|
serviceUrl,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sessionFactors = await loadMostRecentSession({
|
||||||
|
serviceUrl,
|
||||||
|
sessionParams: { loginName, organization },
|
||||||
|
}).catch((error) => {
|
||||||
|
console.warn("Error loading session:", error);
|
||||||
|
});
|
||||||
|
|
||||||
|
let loginSettings;
|
||||||
|
if (!requestId) {
|
||||||
|
loginSettings = await getLoginSettings({
|
||||||
|
serviceUrl,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = userId ?? sessionFactors?.factors?.user?.id;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
throw Error("Failed to get user id");
|
||||||
|
}
|
||||||
|
|
||||||
|
const userResponse = await getUserByID({
|
||||||
|
serviceUrl,
|
||||||
|
userId: id,
|
||||||
|
});
|
||||||
|
|
||||||
|
let user: User | undefined;
|
||||||
|
let human: HumanUser | undefined;
|
||||||
|
|
||||||
|
if (userResponse) {
|
||||||
|
user = userResponse.user;
|
||||||
|
if (user?.type.case === "human") {
|
||||||
|
human = user.type.value as HumanUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DynamicTheme branding={branding}>
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<h1>{t("successTitle")}</h1>
|
||||||
|
<p className="ztdl-p mb-6 block">{t("successDescription")}</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>
|
||||||
|
</DynamicTheme>
|
||||||
|
);
|
||||||
|
}
|
@@ -83,6 +83,16 @@ export function RegisterPasskey({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("error" in resp && resp.error) {
|
||||||
|
setError(resp.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!("passkeyId" in resp)) {
|
||||||
|
setError("An error on registering passkey");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const passkeyId = resp.passkeyId;
|
const passkeyId = resp.passkeyId;
|
||||||
const options: CredentialCreationOptions =
|
const options: CredentialCreationOptions =
|
||||||
(resp.publicKeyCredentialCreationOptions as CredentialCreationOptions) ??
|
(resp.publicKeyCredentialCreationOptions as CredentialCreationOptions) ??
|
||||||
@@ -92,6 +102,7 @@ export function RegisterPasskey({
|
|||||||
setError("An error on registering passkey");
|
setError("An error on registering passkey");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
options.publicKey.challenge = coerceToArrayBuffer(
|
options.publicKey.challenge = coerceToArrayBuffer(
|
||||||
options.publicKey.challenge,
|
options.publicKey.challenge,
|
||||||
"challenge",
|
"challenge",
|
||||||
|
@@ -74,7 +74,7 @@ export function SignInWithIdp({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full space-y-2 text-sm">
|
<div className="flex flex-col w-full space-y-2 text-sm">
|
||||||
{identityProviders?.map(renderIDPButton)}
|
{!!identityProviders.length && identityProviders?.map(renderIDPButton)}
|
||||||
{state?.error && (
|
{state?.error && (
|
||||||
<div className="py-4">
|
<div className="py-4">
|
||||||
<Alert>{state?.error}</Alert>
|
<Alert>{state?.error}</Alert>
|
||||||
|
@@ -63,6 +63,11 @@ export function VerifyForm({
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (response && "error" in response && response?.error) {
|
||||||
|
setError(response.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,90 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import {
|
|
||||||
sendVerificationRedirectWithoutCheck,
|
|
||||||
SendVerificationRedirectWithoutCheckCommand,
|
|
||||||
} from "@/lib/server/verify";
|
|
||||||
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { Alert, AlertType } from "./alert";
|
|
||||||
import { BackButton } from "./back-button";
|
|
||||||
import { Button, ButtonVariants } from "./button";
|
|
||||||
import { Spinner } from "./spinner";
|
|
||||||
|
|
||||||
export function VerifyRedirectButton({
|
|
||||||
userId,
|
|
||||||
loginName,
|
|
||||||
requestId,
|
|
||||||
authMethods,
|
|
||||||
organization,
|
|
||||||
}: {
|
|
||||||
userId?: string;
|
|
||||||
loginName?: string;
|
|
||||||
requestId: string;
|
|
||||||
authMethods: AuthenticationMethodType[] | null;
|
|
||||||
organization?: string;
|
|
||||||
}) {
|
|
||||||
const t = useTranslations("verify");
|
|
||||||
const [error, setError] = useState<string>("");
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
|
||||||
|
|
||||||
async function submitAndContinue(): Promise<boolean | void> {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
let command = {
|
|
||||||
organization,
|
|
||||||
requestId,
|
|
||||||
} as SendVerificationRedirectWithoutCheckCommand;
|
|
||||||
|
|
||||||
if (userId) {
|
|
||||||
command = {
|
|
||||||
...command,
|
|
||||||
userId,
|
|
||||||
} as SendVerificationRedirectWithoutCheckCommand;
|
|
||||||
} else if (loginName) {
|
|
||||||
command = {
|
|
||||||
...command,
|
|
||||||
loginName,
|
|
||||||
} as SendVerificationRedirectWithoutCheckCommand;
|
|
||||||
}
|
|
||||||
|
|
||||||
await sendVerificationRedirectWithoutCheck(command)
|
|
||||||
.catch(() => {
|
|
||||||
setError("Could not verify");
|
|
||||||
return;
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Alert type={AlertType.INFO}>{t("success")}</Alert>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="py-4">
|
|
||||||
<Alert>{error}</Alert>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-8 flex w-full flex-row items-center">
|
|
||||||
<BackButton />
|
|
||||||
<span className="flex-grow"></span>
|
|
||||||
{authMethods?.length === 0 && (
|
|
||||||
<Button
|
|
||||||
onClick={() => submitAndContinue()}
|
|
||||||
type="submit"
|
|
||||||
className="self-end"
|
|
||||||
variant={ButtonVariants.Primary}
|
|
||||||
>
|
|
||||||
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
|
||||||
{t("setupAuthenticator")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -9,7 +9,6 @@ import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp";
|
|||||||
import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
||||||
import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||||
import { getServiceUrlFromHeaders } from "../service-url";
|
import { getServiceUrlFromHeaders } from "../service-url";
|
||||||
import { checkInvite } from "../verify-helper";
|
|
||||||
import {
|
import {
|
||||||
getActiveIdentityProviders,
|
getActiveIdentityProviders,
|
||||||
getIDPByID,
|
getIDPByID,
|
||||||
@@ -254,37 +253,27 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
|||||||
userId: session.factors?.user?.id,
|
userId: session.factors?.user?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
// this can be expected to be an invite as users created in console have a password set.
|
// always resend invite if user has no auth method set
|
||||||
if (!methods.authMethodTypes || !methods.authMethodTypes.length) {
|
if (!methods.authMethodTypes || !methods.authMethodTypes.length) {
|
||||||
// redirect to /verify invite if no auth method is set and email is not verified
|
const params = new URLSearchParams({
|
||||||
const inviteCheck = checkInvite(
|
loginName: session.factors?.user?.loginName as string,
|
||||||
session,
|
send: "true", // set this to true to request a new code immediately
|
||||||
humanUser,
|
invite: "true",
|
||||||
session.factors.user.organizationId,
|
|
||||||
command.requestId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (inviteCheck?.redirect) {
|
|
||||||
return inviteCheck;
|
|
||||||
}
|
|
||||||
|
|
||||||
const paramsAuthenticatorSetup = new URLSearchParams({
|
|
||||||
loginName: session.factors?.user?.loginName,
|
|
||||||
userId: session.factors?.user?.id, // verify needs user id
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (command.requestId) {
|
||||||
|
params.append("requestId", command.requestId);
|
||||||
|
}
|
||||||
|
|
||||||
if (command.organization || session.factors?.user?.organizationId) {
|
if (command.organization || session.factors?.user?.organizationId) {
|
||||||
paramsAuthenticatorSetup.append(
|
params.append(
|
||||||
"organization",
|
"organization",
|
||||||
command.organization ?? session.factors?.user?.organizationId,
|
command.organization ??
|
||||||
|
(session.factors?.user?.organizationId as string),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command.requestId) {
|
return { redirect: `/verify?` + params };
|
||||||
paramsAuthenticatorSetup.append("requestId", command.requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { redirect: "/authenticator/set?" + paramsAuthenticatorSetup };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (methods.authMethodTypes.length == 1) {
|
if (methods.authMethodTypes.length == 1) {
|
||||||
|
@@ -5,10 +5,12 @@ import {
|
|||||||
getLoginSettings,
|
getLoginSettings,
|
||||||
getSession,
|
getSession,
|
||||||
getUserByID,
|
getUserByID,
|
||||||
|
listAuthenticationMethodTypes,
|
||||||
registerPasskey,
|
registerPasskey,
|
||||||
verifyPasskeyRegistration as zitadelVerifyPasskeyRegistration,
|
verifyPasskeyRegistration as zitadelVerifyPasskeyRegistration,
|
||||||
} from "@/lib/zitadel";
|
} from "@/lib/zitadel";
|
||||||
import { create, Duration } from "@zitadel/client";
|
import { create, Duration, Timestamp, timestampDate } from "@zitadel/client";
|
||||||
|
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||||
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||||
import {
|
import {
|
||||||
RegisterPasskeyResponse,
|
RegisterPasskeyResponse,
|
||||||
@@ -23,7 +25,10 @@ import {
|
|||||||
getSessionCookieByLoginName,
|
getSessionCookieByLoginName,
|
||||||
} from "../cookies";
|
} from "../cookies";
|
||||||
import { getServiceUrlFromHeaders } from "../service-url";
|
import { getServiceUrlFromHeaders } from "../service-url";
|
||||||
import { checkEmailVerification } from "../verify-helper";
|
import {
|
||||||
|
checkEmailVerification,
|
||||||
|
checkUserVerification,
|
||||||
|
} from "../verify-helper";
|
||||||
import { setSessionAndUpdateCookie } from "./cookie";
|
import { setSessionAndUpdateCookie } from "./cookie";
|
||||||
|
|
||||||
type VerifyPasskeyCommand = {
|
type VerifyPasskeyCommand = {
|
||||||
@@ -37,9 +42,25 @@ type RegisterPasskeyCommand = {
|
|||||||
sessionId: string;
|
sessionId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function isSessionValid(session: Partial<Session>): {
|
||||||
|
valid: boolean;
|
||||||
|
verifiedAt?: Timestamp;
|
||||||
|
} {
|
||||||
|
const validPassword = session?.factors?.password?.verifiedAt;
|
||||||
|
const validPasskey = session?.factors?.webAuthN?.verifiedAt;
|
||||||
|
const stillValid = session.expirationDate
|
||||||
|
? timestampDate(session.expirationDate) > new Date()
|
||||||
|
: true;
|
||||||
|
|
||||||
|
const verifiedAt = validPassword || validPasskey;
|
||||||
|
const valid = !!((validPassword || validPasskey) && stillValid);
|
||||||
|
|
||||||
|
return { valid, verifiedAt };
|
||||||
|
}
|
||||||
|
|
||||||
export async function registerPasskeyLink(
|
export async function registerPasskeyLink(
|
||||||
command: RegisterPasskeyCommand,
|
command: RegisterPasskeyCommand,
|
||||||
): Promise<RegisterPasskeyResponse> {
|
): Promise<RegisterPasskeyResponse | { error: string }> {
|
||||||
const { sessionId } = command;
|
const { sessionId } = command;
|
||||||
|
|
||||||
const _headers = await headers();
|
const _headers = await headers();
|
||||||
@@ -57,6 +78,36 @@ export async function registerPasskeyLink(
|
|||||||
sessionToken: sessionCookie.token,
|
sessionToken: sessionCookie.token,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!session?.session?.factors?.user?.id) {
|
||||||
|
return { error: "Could not determine user from session" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionValid = isSessionValid(session.session);
|
||||||
|
|
||||||
|
if (!sessionValid) {
|
||||||
|
const authmethods = await listAuthenticationMethodTypes({
|
||||||
|
serviceUrl,
|
||||||
|
userId: session.session.factors.user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// if the user has no authmethods set, we need to check if the user was verified
|
||||||
|
if (authmethods.authMethodTypes.length !== 0) {
|
||||||
|
return {
|
||||||
|
error:
|
||||||
|
"You have to authenticate or have a valid User Verification Check",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if a verification was done earlier
|
||||||
|
const hasValidUserVerificationCheck = await checkUserVerification(
|
||||||
|
session.session.factors.user.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasValidUserVerificationCheck) {
|
||||||
|
return { error: "User Verification Check has to be done" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [hostname, port] = host.split(":");
|
const [hostname, port] = host.split(":");
|
||||||
|
|
||||||
if (!hostname) {
|
if (!hostname) {
|
||||||
|
@@ -37,6 +37,7 @@ import {
|
|||||||
checkEmailVerification,
|
checkEmailVerification,
|
||||||
checkMFAFactors,
|
checkMFAFactors,
|
||||||
checkPasswordChangeRequired,
|
checkPasswordChangeRequired,
|
||||||
|
checkUserVerification,
|
||||||
} from "../verify-helper";
|
} from "../verify-helper";
|
||||||
|
|
||||||
type ResetPasswordCommand = {
|
type ResetPasswordCommand = {
|
||||||
@@ -297,6 +298,7 @@ export async function sendPassword(command: UpdateSessionCommand) {
|
|||||||
return { redirect: url };
|
return { redirect: url };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this function lets users with code set a password or users with valid User Verification Check
|
||||||
export async function changePassword(command: {
|
export async function changePassword(command: {
|
||||||
code?: string;
|
code?: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -316,11 +318,39 @@ export async function changePassword(command: {
|
|||||||
}
|
}
|
||||||
const userId = user.userId;
|
const userId = user.userId;
|
||||||
|
|
||||||
|
if (user.state === UserState.INITIAL) {
|
||||||
|
return { error: "User Initial State is not supported" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the user has no password set in order to set a password
|
||||||
|
if (!command.code) {
|
||||||
|
const authmethods = await listAuthenticationMethodTypes({
|
||||||
|
serviceUrl,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// if the user has no authmethods set, we need to check if the user was verified
|
||||||
|
if (authmethods.authMethodTypes.length !== 0) {
|
||||||
|
return {
|
||||||
|
error:
|
||||||
|
"You have to provide a code or have a valid User Verification Check",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if a verification was done earlier
|
||||||
|
const hasValidUserVerificationCheck = await checkUserVerification(
|
||||||
|
user.userId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasValidUserVerificationCheck) {
|
||||||
|
return { error: "User Verification Check has to be done" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return setUserPassword({
|
return setUserPassword({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
userId,
|
userId,
|
||||||
password: command.password,
|
password: command.password,
|
||||||
user,
|
|
||||||
code: command.code,
|
code: command.code,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -1,24 +1,25 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
createInviteCode,
|
||||||
getLoginSettings,
|
getLoginSettings,
|
||||||
getSession,
|
getSession,
|
||||||
getUserByID,
|
getUserByID,
|
||||||
listAuthenticationMethodTypes,
|
listAuthenticationMethodTypes,
|
||||||
resendEmailCode,
|
|
||||||
resendInviteCode,
|
|
||||||
verifyEmail,
|
verifyEmail,
|
||||||
verifyInviteCode,
|
verifyInviteCode,
|
||||||
verifyTOTPRegistration,
|
verifyTOTPRegistration,
|
||||||
sendEmailCode as zitadelSendEmailCode,
|
sendEmailCode as zitadelSendEmailCode,
|
||||||
} from "@/lib/zitadel";
|
} from "@/lib/zitadel";
|
||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
import { create } from "@zitadel/client";
|
import { create } from "@zitadel/client";
|
||||||
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||||
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||||
import { User } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
import { cookies, headers } from "next/headers";
|
||||||
import { headers } from "next/headers";
|
|
||||||
import { getNextUrl } from "../client";
|
import { getNextUrl } from "../client";
|
||||||
import { getSessionCookieByLoginName } from "../cookies";
|
import { getSessionCookieByLoginName } from "../cookies";
|
||||||
|
import { getOrSetFingerprintId } from "../fingerprint";
|
||||||
import { getServiceUrlFromHeaders } from "../service-url";
|
import { getServiceUrlFromHeaders } from "../service-url";
|
||||||
import { loadMostRecentSession } from "../session";
|
import { loadMostRecentSession } from "../session";
|
||||||
import { checkMFAFactors } from "../verify-helper";
|
import { checkMFAFactors } from "../verify-helper";
|
||||||
@@ -69,14 +70,16 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
|
|||||||
serviceUrl,
|
serviceUrl,
|
||||||
userId: command.userId,
|
userId: command.userId,
|
||||||
verificationCode: command.code,
|
verificationCode: command.code,
|
||||||
}).catch(() => {
|
}).catch((error) => {
|
||||||
|
console.warn(error);
|
||||||
return { error: "Could not verify invite" };
|
return { error: "Could not verify invite" };
|
||||||
})
|
})
|
||||||
: await verifyEmail({
|
: await verifyEmail({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
userId: command.userId,
|
userId: command.userId,
|
||||||
verificationCode: command.code,
|
verificationCode: command.code,
|
||||||
}).catch(() => {
|
}).catch((error) => {
|
||||||
|
console.warn(error);
|
||||||
return { error: "Could not verify email" };
|
return { error: "Could not verify email" };
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -89,20 +92,26 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let session: Session | undefined;
|
let session: Session | undefined;
|
||||||
let user: User | undefined;
|
const userResponse = await getUserByID({
|
||||||
|
serviceUrl,
|
||||||
|
userId: command.userId,
|
||||||
|
});
|
||||||
|
|
||||||
if ("loginName" in command) {
|
if (!userResponse || !userResponse.user) {
|
||||||
const sessionCookie = await getSessionCookieByLoginName({
|
return { error: "Could not load user" };
|
||||||
loginName: command.loginName,
|
}
|
||||||
organization: command.organization,
|
|
||||||
}).catch((error) => {
|
|
||||||
console.warn("Ignored error:", error);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!sessionCookie) {
|
const user = userResponse.user;
|
||||||
return { error: "Could not load session cookie" };
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const sessionCookie = await getSessionCookieByLoginName({
|
||||||
|
loginName:
|
||||||
|
"loginName" in command ? command.loginName : user.preferredLoginName,
|
||||||
|
organization: command.organization,
|
||||||
|
}).catch((error) => {
|
||||||
|
console.warn("Ignored error:", error); // checked later
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sessionCookie) {
|
||||||
session = await getSession({
|
session = await getSession({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
sessionId: sessionCookie.id,
|
sessionId: sessionCookie.id,
|
||||||
@@ -112,65 +121,9 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
|
|||||||
return response.session;
|
return response.session;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!session?.factors?.user?.id) {
|
|
||||||
return { error: "Could not create session for user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const userResponse = await getUserByID({
|
|
||||||
serviceUrl,
|
|
||||||
userId: session?.factors?.user?.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userResponse?.user) {
|
|
||||||
return { error: "Could not load user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
user = userResponse.user;
|
|
||||||
} else {
|
|
||||||
const userResponse = await getUserByID({
|
|
||||||
serviceUrl,
|
|
||||||
userId: command.userId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userResponse || !userResponse.user) {
|
|
||||||
return { error: "Could not load user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
user = userResponse.user;
|
|
||||||
|
|
||||||
const checks = create(ChecksSchema, {
|
|
||||||
user: {
|
|
||||||
search: {
|
|
||||||
case: "loginName",
|
|
||||||
value: userResponse.user.preferredLoginName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
session = await createSessionAndUpdateCookie({
|
|
||||||
checks,
|
|
||||||
requestId: command.requestId,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!session?.factors?.user?.id) {
|
// load auth methods for user
|
||||||
return { error: "Could not create session for user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!session?.factors?.user?.id) {
|
|
||||||
return { error: "Could not create session for user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return { error: "Could not load user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginSettings = await getLoginSettings({
|
|
||||||
serviceUrl,
|
|
||||||
organization: user.details?.resourceOwner,
|
|
||||||
});
|
|
||||||
|
|
||||||
const authMethodResponse = await listAuthenticationMethodTypes({
|
const authMethodResponse = await listAuthenticationMethodTypes({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
userId: user.userId,
|
userId: user.userId,
|
||||||
@@ -186,6 +139,26 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
|
|||||||
authMethodResponse.authMethodTypes &&
|
authMethodResponse.authMethodTypes &&
|
||||||
authMethodResponse.authMethodTypes.length == 0
|
authMethodResponse.authMethodTypes.length == 0
|
||||||
) {
|
) {
|
||||||
|
if (!sessionCookie) {
|
||||||
|
const checks = create(ChecksSchema, {
|
||||||
|
user: {
|
||||||
|
search: {
|
||||||
|
case: "loginName",
|
||||||
|
value: userResponse.user.preferredLoginName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
session = await createSessionAndUpdateCookie({
|
||||||
|
checks,
|
||||||
|
requestId: command.requestId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return { error: "Could not create session" };
|
||||||
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
sessionId: session.id,
|
sessionId: session.id,
|
||||||
});
|
});
|
||||||
@@ -193,9 +166,62 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
|
|||||||
if (session.factors?.user?.loginName) {
|
if (session.factors?.user?.loginName) {
|
||||||
params.set("loginName", session.factors?.user?.loginName);
|
params.set("loginName", session.factors?.user?.loginName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set hash of userId and userAgentId to prevent attacks, checks are done for users with invalid sessions and invalid userAgentId
|
||||||
|
const cookiesList = await cookies();
|
||||||
|
const userAgentId = await getOrSetFingerprintId();
|
||||||
|
|
||||||
|
const verificationCheck = crypto
|
||||||
|
.createHash("sha256")
|
||||||
|
.update(`${user.userId}:${userAgentId}`)
|
||||||
|
.digest("hex");
|
||||||
|
|
||||||
|
await cookiesList.set({
|
||||||
|
name: "verificationCheck",
|
||||||
|
value: verificationCheck,
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
maxAge: 300, // 5 minutes
|
||||||
|
});
|
||||||
|
|
||||||
return { redirect: `/authenticator/set?${params}` };
|
return { redirect: `/authenticator/set?${params}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if no session found only show success page,
|
||||||
|
// if user is invited, recreate invite flow to not depend on session
|
||||||
|
if (!session?.factors?.user?.id) {
|
||||||
|
const verifySuccessParams = new URLSearchParams({});
|
||||||
|
|
||||||
|
if (command.userId) {
|
||||||
|
verifySuccessParams.set("userId", command.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
("loginName" in command && command.loginName) ||
|
||||||
|
user.preferredLoginName
|
||||||
|
) {
|
||||||
|
verifySuccessParams.set(
|
||||||
|
"loginName",
|
||||||
|
"loginName" in command && command.loginName
|
||||||
|
? command.loginName
|
||||||
|
: user.preferredLoginName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (command.requestId) {
|
||||||
|
verifySuccessParams.set("requestId", command.requestId);
|
||||||
|
}
|
||||||
|
if (command.organization) {
|
||||||
|
verifySuccessParams.set("organization", command.organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { redirect: `/verify/success?${verifySuccessParams}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginSettings = await getLoginSettings({
|
||||||
|
serviceUrl,
|
||||||
|
organization: user.details?.resourceOwner,
|
||||||
|
});
|
||||||
|
|
||||||
// redirect to mfa factor if user has one, or redirect to set one up
|
// redirect to mfa factor if user has one, or redirect to set one up
|
||||||
const mfaFactorCheck = await checkMFAFactors(
|
const mfaFactorCheck = await checkMFAFactors(
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
@@ -254,193 +280,50 @@ export async function resendVerification(command: resendVerifyEmailCommand) {
|
|||||||
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
|
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
|
||||||
|
|
||||||
return command.isInvite
|
return command.isInvite
|
||||||
? resendInviteCode({ serviceUrl, userId: command.userId })
|
? createInviteCode({
|
||||||
: resendEmailCode({
|
serviceUrl,
|
||||||
|
userId: command.userId,
|
||||||
|
urlTemplate:
|
||||||
|
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` +
|
||||||
|
(command.requestId ? `&requestId=${command.requestId}` : ""),
|
||||||
|
}).catch((error) => {
|
||||||
|
if (error.code === 9) {
|
||||||
|
return { error: "User is already verified!" };
|
||||||
|
}
|
||||||
|
return { error: "Could not resend invite" };
|
||||||
|
})
|
||||||
|
: zitadelSendEmailCode({
|
||||||
userId: command.userId,
|
userId: command.userId,
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
urlTemplate:
|
urlTemplate:
|
||||||
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
|
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
|
||||||
(command.requestId ? `&requestId=${command.requestId}` : ""),
|
(command.requestId ? `&requestId=${command.requestId}` : ""),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
type sendEmailCommand = {
|
type SendEmailCommand = {
|
||||||
serviceUrl: string;
|
|
||||||
|
|
||||||
userId: string;
|
userId: string;
|
||||||
urlTemplate: string;
|
urlTemplate: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function sendEmailCode(command: sendEmailCommand) {
|
export async function sendEmailCode(command: SendEmailCommand) {
|
||||||
|
const _headers = await headers();
|
||||||
|
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||||
|
|
||||||
return zitadelSendEmailCode({
|
return zitadelSendEmailCode({
|
||||||
serviceUrl: command.serviceUrl,
|
serviceUrl,
|
||||||
userId: command.userId,
|
userId: command.userId,
|
||||||
urlTemplate: command.urlTemplate,
|
urlTemplate: command.urlTemplate,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SendVerificationRedirectWithoutCheckCommand = {
|
export async function sendInviteEmailCode(command: SendEmailCommand) {
|
||||||
organization?: string;
|
|
||||||
requestId?: string;
|
|
||||||
} & (
|
|
||||||
| { userId: string; loginName?: never }
|
|
||||||
| { userId?: never; loginName: string }
|
|
||||||
);
|
|
||||||
|
|
||||||
export async function sendVerificationRedirectWithoutCheck(
|
|
||||||
command: SendVerificationRedirectWithoutCheckCommand,
|
|
||||||
) {
|
|
||||||
const _headers = await headers();
|
const _headers = await headers();
|
||||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||||
|
|
||||||
if (!("loginName" in command || "userId" in command)) {
|
return createInviteCode({
|
||||||
return { error: "No userId, nor loginname provided" };
|
|
||||||
}
|
|
||||||
|
|
||||||
let session: Session | undefined;
|
|
||||||
let user: User | undefined;
|
|
||||||
|
|
||||||
if ("loginName" in command) {
|
|
||||||
const sessionCookie = await getSessionCookieByLoginName({
|
|
||||||
loginName: command.loginName,
|
|
||||||
organization: command.organization,
|
|
||||||
}).catch((error) => {
|
|
||||||
console.warn("Ignored error:", error);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!sessionCookie) {
|
|
||||||
return { error: "Could not load session cookie" };
|
|
||||||
}
|
|
||||||
|
|
||||||
session = await getSession({
|
|
||||||
serviceUrl,
|
|
||||||
sessionId: sessionCookie.id,
|
|
||||||
sessionToken: sessionCookie.token,
|
|
||||||
}).then((response) => {
|
|
||||||
if (response?.session) {
|
|
||||||
return response.session;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!session?.factors?.user?.id) {
|
|
||||||
return { error: "Could not create session for user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const userResponse = await getUserByID({
|
|
||||||
serviceUrl,
|
|
||||||
userId: session?.factors?.user?.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userResponse?.user) {
|
|
||||||
return { error: "Could not load user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
user = userResponse.user;
|
|
||||||
} else if ("userId" in command) {
|
|
||||||
const userResponse = await getUserByID({
|
|
||||||
serviceUrl,
|
|
||||||
userId: command.userId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userResponse?.user) {
|
|
||||||
return { error: "Could not load user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
user = userResponse.user;
|
|
||||||
|
|
||||||
const checks = create(ChecksSchema, {
|
|
||||||
user: {
|
|
||||||
search: {
|
|
||||||
case: "loginName",
|
|
||||||
value: userResponse.user.preferredLoginName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
session = await createSessionAndUpdateCookie({
|
|
||||||
checks,
|
|
||||||
requestId: command.requestId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!session?.factors?.user?.id) {
|
|
||||||
return { error: "Could not create session for user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!session?.factors?.user?.id) {
|
|
||||||
return { error: "Could not create session for user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return { error: "Could not load user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const authMethodResponse = await listAuthenticationMethodTypes({
|
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
userId: user.userId,
|
userId: command.userId,
|
||||||
|
urlTemplate: command.urlTemplate,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!authMethodResponse || !authMethodResponse.authMethodTypes) {
|
|
||||||
return { error: "Could not load possible authenticators" };
|
|
||||||
}
|
|
||||||
|
|
||||||
// if no authmethods are found on the user, redirect to set one up
|
|
||||||
if (
|
|
||||||
authMethodResponse &&
|
|
||||||
authMethodResponse.authMethodTypes &&
|
|
||||||
authMethodResponse.authMethodTypes.length == 0
|
|
||||||
) {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
sessionId: session.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (session.factors?.user?.loginName) {
|
|
||||||
params.set("loginName", session.factors?.user?.loginName);
|
|
||||||
}
|
|
||||||
return { redirect: `/authenticator/set?${params}` };
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginSettings = await getLoginSettings({
|
|
||||||
serviceUrl,
|
|
||||||
organization: user.details?.resourceOwner,
|
|
||||||
});
|
|
||||||
|
|
||||||
// redirect to mfa factor if user has one, or redirect to set one up
|
|
||||||
const mfaFactorCheck = await checkMFAFactors(
|
|
||||||
serviceUrl,
|
|
||||||
session,
|
|
||||||
loginSettings,
|
|
||||||
authMethodResponse.authMethodTypes,
|
|
||||||
command.organization,
|
|
||||||
command.requestId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (mfaFactorCheck?.redirect) {
|
|
||||||
return mfaFactorCheck;
|
|
||||||
}
|
|
||||||
|
|
||||||
// login user if no additional steps are required
|
|
||||||
if (command.requestId && session.id) {
|
|
||||||
const nextUrl = await getNextUrl(
|
|
||||||
{
|
|
||||||
sessionId: session.id,
|
|
||||||
requestId: command.requestId,
|
|
||||||
organization:
|
|
||||||
command.organization ?? session.factors?.user?.organizationId,
|
|
||||||
},
|
|
||||||
loginSettings?.defaultRedirectUri,
|
|
||||||
);
|
|
||||||
|
|
||||||
return { redirect: nextUrl };
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = await getNextUrl(
|
|
||||||
{
|
|
||||||
loginName: session.factors.user.loginName,
|
|
||||||
organization: session.factors?.user?.organizationId,
|
|
||||||
},
|
|
||||||
loginSettings?.defaultRedirectUri,
|
|
||||||
);
|
|
||||||
|
|
||||||
return { redirect: url };
|
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,10 @@ import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings
|
|||||||
import { PasswordExpirySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
|
import { PasswordExpirySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
|
||||||
import { HumanUser } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
import { HumanUser } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||||
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||||
|
import crypto from "crypto";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
import { getFingerprintIdCookie } from "./fingerprint";
|
||||||
import { getUserByID } from "./zitadel";
|
import { getUserByID } from "./zitadel";
|
||||||
|
|
||||||
export function checkPasswordChangeRequired(
|
export function checkPasswordChangeRequired(
|
||||||
@@ -44,7 +47,7 @@ export function checkPasswordChangeRequired(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkInvite(
|
export function checkEmailVerified(
|
||||||
session: Session,
|
session: Session,
|
||||||
humanUser?: HumanUser,
|
humanUser?: HumanUser,
|
||||||
organization?: string,
|
organization?: string,
|
||||||
@@ -54,7 +57,7 @@ export function checkInvite(
|
|||||||
const paramsVerify = new URLSearchParams({
|
const paramsVerify = new URLSearchParams({
|
||||||
loginName: session.factors?.user?.loginName as string,
|
loginName: session.factors?.user?.loginName as string,
|
||||||
userId: session.factors?.user?.id as string, // verify needs user id
|
userId: session.factors?.user?.id as string, // verify needs user id
|
||||||
invite: "true", // TODO: check - set this to true as we dont expect old email verification method here
|
send: "true", // we request a new email code once the page is loaded
|
||||||
});
|
});
|
||||||
|
|
||||||
if (organization || session.factors?.user?.organizationId) {
|
if (organization || session.factors?.user?.organizationId) {
|
||||||
@@ -84,6 +87,7 @@ export function checkEmailVerification(
|
|||||||
) {
|
) {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
loginName: session.factors?.user?.loginName as string,
|
loginName: session.factors?.user?.loginName as string,
|
||||||
|
send: "true", // set this to true as we dont expect old email codes to be valid anymore
|
||||||
});
|
});
|
||||||
|
|
||||||
if (requestId) {
|
if (requestId) {
|
||||||
@@ -248,3 +252,38 @@ export async function checkMFAFactors(
|
|||||||
return { redirect: `/mfa/set?` + params };
|
return { redirect: `/mfa/set?` + params };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function checkUserVerification(userId: string): Promise<boolean> {
|
||||||
|
// check if a verification was done earlier
|
||||||
|
const cookiesList = await cookies();
|
||||||
|
|
||||||
|
// only read cookie to prevent issues on page.tsx
|
||||||
|
const fingerPrintCookie = await getFingerprintIdCookie();
|
||||||
|
|
||||||
|
if (!fingerPrintCookie || !fingerPrintCookie.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const verificationCheck = crypto
|
||||||
|
.createHash("sha256")
|
||||||
|
.update(`${userId}:${fingerPrintCookie.value}`)
|
||||||
|
.digest("hex");
|
||||||
|
|
||||||
|
const cookieValue = await cookiesList.get("verificationCheck")?.value;
|
||||||
|
|
||||||
|
if (!cookieValue) {
|
||||||
|
console.warn(
|
||||||
|
"User verification check cookie not found. User verification check failed.",
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cookieValue !== verificationCheck) {
|
||||||
|
console.warn(
|
||||||
|
`User verification check failed. Expected ${verificationCheck} but got ${cookieValue}`,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
@@ -29,11 +29,7 @@ import {
|
|||||||
SearchQuery,
|
SearchQuery,
|
||||||
SearchQuerySchema,
|
SearchQuerySchema,
|
||||||
} from "@zitadel/proto/zitadel/user/v2/query_pb";
|
} from "@zitadel/proto/zitadel/user/v2/query_pb";
|
||||||
import {
|
import { SendInviteCodeSchema } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||||
SendInviteCodeSchema,
|
|
||||||
User,
|
|
||||||
UserState,
|
|
||||||
} from "@zitadel/proto/zitadel/user/v2/user_pb";
|
|
||||||
import {
|
import {
|
||||||
AddHumanUserRequest,
|
AddHumanUserRequest,
|
||||||
ResendEmailCodeRequest,
|
ResendEmailCodeRequest,
|
||||||
@@ -506,21 +502,6 @@ export async function verifyInviteCode({
|
|||||||
return userService.verifyInviteCode({ userId, verificationCode }, {});
|
return userService.verifyInviteCode({ userId, verificationCode }, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resendInviteCode({
|
|
||||||
serviceUrl,
|
|
||||||
userId,
|
|
||||||
}: {
|
|
||||||
serviceUrl: string;
|
|
||||||
userId: string;
|
|
||||||
}) {
|
|
||||||
const userService: Client<typeof UserService> = await createServiceForHost(
|
|
||||||
UserService,
|
|
||||||
serviceUrl,
|
|
||||||
);
|
|
||||||
|
|
||||||
return userService.resendInviteCode({ userId }, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function sendEmailCode({
|
export async function sendEmailCode({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
userId,
|
userId,
|
||||||
@@ -1170,13 +1151,11 @@ export async function setUserPassword({
|
|||||||
serviceUrl,
|
serviceUrl,
|
||||||
userId,
|
userId,
|
||||||
password,
|
password,
|
||||||
user,
|
|
||||||
code,
|
code,
|
||||||
}: {
|
}: {
|
||||||
serviceUrl: string;
|
serviceUrl: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
password: string;
|
password: string;
|
||||||
user: User;
|
|
||||||
code?: string;
|
code?: string;
|
||||||
}) {
|
}) {
|
||||||
let payload = create(SetPasswordRequestSchema, {
|
let payload = create(SetPasswordRequestSchema, {
|
||||||
@@ -1186,22 +1165,6 @@ export async function setUserPassword({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// check if the user has no password set in order to set a password
|
|
||||||
if (!code) {
|
|
||||||
const authmethods = await listAuthenticationMethodTypes({
|
|
||||||
serviceUrl,
|
|
||||||
userId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// if the user has no authmethods set, we can set a password otherwise we need a code
|
|
||||||
if (
|
|
||||||
!(authmethods.authMethodTypes.length === 0) &&
|
|
||||||
user.state !== UserState.INITIAL
|
|
||||||
) {
|
|
||||||
return { error: "Provide a code to set a password" };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (code) {
|
if (code) {
|
||||||
payload = {
|
payload = {
|
||||||
...payload,
|
...payload,
|
||||||
|
Reference in New Issue
Block a user