mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 23:17:33 +00:00
Merge branch 'main' into saml-post
This commit is contained in:
@@ -72,6 +72,10 @@
|
|||||||
"linkingError": {
|
"linkingError": {
|
||||||
"title": "Konto-Verknüpfung fehlgeschlagen",
|
"title": "Konto-Verknüpfung fehlgeschlagen",
|
||||||
"description": "Beim Verknüpfen Ihres Kontos ist ein Fehler aufgetreten."
|
"description": "Beim Verknüpfen Ihres Kontos ist ein Fehler aufgetreten."
|
||||||
|
},
|
||||||
|
"completeRegister": {
|
||||||
|
"title": "Registrierung abschließen",
|
||||||
|
"description": "Bitte vervollständige die Registrierung, um dein Konto zu erstellen."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mfa": {
|
"mfa": {
|
||||||
@@ -149,11 +153,13 @@
|
|||||||
},
|
},
|
||||||
"title": "Registrieren",
|
"title": "Registrieren",
|
||||||
"description": "Erstellen Sie Ihr ZITADEL-Konto.",
|
"description": "Erstellen Sie Ihr ZITADEL-Konto.",
|
||||||
|
"noMethodAvailableWarning": "Keine Authentifizierungsmethode verfügbar. Bitte wenden Sie sich an den Administrator.",
|
||||||
"selectMethod": "Wählen Sie die Methode, mit der Sie sich authentifizieren möchten",
|
"selectMethod": "Wählen Sie die Methode, mit der Sie sich authentifizieren möchten",
|
||||||
"agreeTo": "Um sich zu registrieren, müssen Sie den Nutzungsbedingungen zustimmen",
|
"agreeTo": "Um sich zu registrieren, müssen Sie den Nutzungsbedingungen zustimmen",
|
||||||
"termsOfService": "Nutzungsbedingungen",
|
"termsOfService": "Nutzungsbedingungen",
|
||||||
"privacyPolicy": "Datenschutzrichtlinie",
|
"privacyPolicy": "Datenschutzrichtlinie",
|
||||||
"submit": "Weiter",
|
"submit": "Weiter",
|
||||||
|
"orUseIDP": "oder verwenden Sie einen Identitätsanbieter",
|
||||||
"password": {
|
"password": {
|
||||||
"title": "Passwort festlegen",
|
"title": "Passwort festlegen",
|
||||||
"description": "Legen Sie das Passwort für Ihr Konto fest",
|
"description": "Legen Sie das Passwort für Ihr Konto fest",
|
||||||
|
@@ -72,6 +72,10 @@
|
|||||||
"linkingError": {
|
"linkingError": {
|
||||||
"title": "Account linking failed",
|
"title": "Account linking failed",
|
||||||
"description": "An error occurred while trying to link your account."
|
"description": "An error occurred while trying to link your account."
|
||||||
|
},
|
||||||
|
"completeRegister": {
|
||||||
|
"title": "Complete your data",
|
||||||
|
"description": "You need to complete your registration by providing your email address and name."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mfa": {
|
"mfa": {
|
||||||
@@ -149,11 +153,13 @@
|
|||||||
},
|
},
|
||||||
"title": "Register",
|
"title": "Register",
|
||||||
"description": "Create your ZITADEL account.",
|
"description": "Create your ZITADEL account.",
|
||||||
|
"noMethodAvailableWarning": "No authentication method available. Please contact your administrator.",
|
||||||
"selectMethod": "Select the method you would like to authenticate",
|
"selectMethod": "Select the method you would like to authenticate",
|
||||||
"agreeTo": "To register you must agree to the terms and conditions",
|
"agreeTo": "To register you must agree to the terms and conditions",
|
||||||
"termsOfService": "Terms of Service",
|
"termsOfService": "Terms of Service",
|
||||||
"privacyPolicy": "Privacy Policy",
|
"privacyPolicy": "Privacy Policy",
|
||||||
"submit": "Continue",
|
"submit": "Continue",
|
||||||
|
"orUseIDP": "or use an Identity Provider",
|
||||||
"password": {
|
"password": {
|
||||||
"title": "Set Password",
|
"title": "Set Password",
|
||||||
"description": "Set the password for your account",
|
"description": "Set the password for your account",
|
||||||
|
@@ -72,6 +72,10 @@
|
|||||||
"linkingError": {
|
"linkingError": {
|
||||||
"title": "Error al vincular la cuenta",
|
"title": "Error al vincular la cuenta",
|
||||||
"description": "Ocurrió un error al intentar vincular tu cuenta."
|
"description": "Ocurrió un error al intentar vincular tu cuenta."
|
||||||
|
},
|
||||||
|
"completeRegister": {
|
||||||
|
"title": "Completar registro",
|
||||||
|
"description": "Para completar el registro, debes establecer una contraseña."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mfa": {
|
"mfa": {
|
||||||
@@ -149,11 +153,13 @@
|
|||||||
},
|
},
|
||||||
"title": "Registrarse",
|
"title": "Registrarse",
|
||||||
"description": "Crea tu cuenta ZITADEL.",
|
"description": "Crea tu cuenta ZITADEL.",
|
||||||
|
"noMethodAvailableWarning": "No hay métodos de autenticación disponibles. Por favor, contacta a tu administrador.",
|
||||||
"selectMethod": "Selecciona el método con el que deseas autenticarte",
|
"selectMethod": "Selecciona el método con el que deseas autenticarte",
|
||||||
"agreeTo": "Para registrarte debes aceptar los términos y condiciones",
|
"agreeTo": "Para registrarte debes aceptar los términos y condiciones",
|
||||||
"termsOfService": "Términos de Servicio",
|
"termsOfService": "Términos de Servicio",
|
||||||
"privacyPolicy": "Política de Privacidad",
|
"privacyPolicy": "Política de Privacidad",
|
||||||
"submit": "Continuar",
|
"submit": "Continuar",
|
||||||
|
"orUseIDP": "o usa un Proveedor de Identidad",
|
||||||
"password": {
|
"password": {
|
||||||
"title": "Establecer Contraseña",
|
"title": "Establecer Contraseña",
|
||||||
"description": "Establece la contraseña para tu cuenta",
|
"description": "Establece la contraseña para tu cuenta",
|
||||||
|
@@ -72,6 +72,10 @@
|
|||||||
"linkingError": {
|
"linkingError": {
|
||||||
"title": "Collegamento account fallito",
|
"title": "Collegamento account fallito",
|
||||||
"description": "Si è verificato un errore durante il tentativo di collegare il tuo account."
|
"description": "Si è verificato un errore durante il tentativo di collegare il tuo account."
|
||||||
|
},
|
||||||
|
"completeRegister": {
|
||||||
|
"title": "Completa la registrazione",
|
||||||
|
"description": "Completa la registrazione del tuo account."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mfa": {
|
"mfa": {
|
||||||
@@ -149,11 +153,13 @@
|
|||||||
},
|
},
|
||||||
"title": "Registrati",
|
"title": "Registrati",
|
||||||
"description": "Crea il tuo account ZITADEL.",
|
"description": "Crea il tuo account ZITADEL.",
|
||||||
|
"noMethodAvailableWarning": "Nessun metodo di autenticazione disponibile. Contatta l'amministratore di sistema per assistenza.",
|
||||||
"selectMethod": "Seleziona il metodo con cui desideri autenticarti",
|
"selectMethod": "Seleziona il metodo con cui desideri autenticarti",
|
||||||
"agreeTo": "Per registrarti devi accettare i termini e le condizioni",
|
"agreeTo": "Per registrarti devi accettare i termini e le condizioni",
|
||||||
"termsOfService": "Termini di Servizio",
|
"termsOfService": "Termini di Servizio",
|
||||||
"privacyPolicy": "Informativa sulla Privacy",
|
"privacyPolicy": "Informativa sulla Privacy",
|
||||||
"submit": "Continua",
|
"submit": "Continua",
|
||||||
|
"orUseIDP": "o usa un Identity Provider",
|
||||||
"password": {
|
"password": {
|
||||||
"title": "Imposta Password",
|
"title": "Imposta Password",
|
||||||
"description": "Imposta la password per il tuo account",
|
"description": "Imposta la password per il tuo account",
|
||||||
|
@@ -72,6 +72,10 @@
|
|||||||
"linkingError": {
|
"linkingError": {
|
||||||
"title": "Powiązanie konta nie powiodło się",
|
"title": "Powiązanie konta nie powiodło się",
|
||||||
"description": "Wystąpił błąd podczas próby powiązania konta."
|
"description": "Wystąpił błąd podczas próby powiązania konta."
|
||||||
|
},
|
||||||
|
"completeRegister": {
|
||||||
|
"title": "Ukończ rejestrację",
|
||||||
|
"description": "Ukończ rejestrację swojego konta."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mfa": {
|
"mfa": {
|
||||||
@@ -149,11 +153,13 @@
|
|||||||
},
|
},
|
||||||
"title": "Rejestracja",
|
"title": "Rejestracja",
|
||||||
"description": "Utwórz konto ZITADEL.",
|
"description": "Utwórz konto ZITADEL.",
|
||||||
|
"noMethodAvailableWarning": "Brak dostępnych metod uwierzytelniania. Skontaktuj się z administratorem.",
|
||||||
"selectMethod": "Wybierz metodę uwierzytelniania, której chcesz użyć",
|
"selectMethod": "Wybierz metodę uwierzytelniania, której chcesz użyć",
|
||||||
"agreeTo": "Aby się zarejestrować, musisz zaakceptować warunki korzystania",
|
"agreeTo": "Aby się zarejestrować, musisz zaakceptować warunki korzystania",
|
||||||
"termsOfService": "Regulamin",
|
"termsOfService": "Regulamin",
|
||||||
"privacyPolicy": "Polityka prywatności",
|
"privacyPolicy": "Polityka prywatności",
|
||||||
"submit": "Kontynuuj",
|
"submit": "Kontynuuj",
|
||||||
|
"orUseIDP": "lub użyj dostawcy tożsamości",
|
||||||
"password": {
|
"password": {
|
||||||
"title": "Ustaw hasło",
|
"title": "Ustaw hasło",
|
||||||
"description": "Ustaw hasło dla swojego konta",
|
"description": "Ustaw hasło dla swojego konta",
|
||||||
|
@@ -72,6 +72,10 @@
|
|||||||
"linkingError": {
|
"linkingError": {
|
||||||
"title": "Ошибка привязки аккаунта",
|
"title": "Ошибка привязки аккаунта",
|
||||||
"description": "Произошла ошибка при попытке привязать аккаунт."
|
"description": "Произошла ошибка при попытке привязать аккаунт."
|
||||||
|
},
|
||||||
|
"completeRegister": {
|
||||||
|
"title": "Завершите регистрацию",
|
||||||
|
"description": "Завершите регистрацию вашего аккаунта."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mfa": {
|
"mfa": {
|
||||||
@@ -149,11 +153,13 @@
|
|||||||
},
|
},
|
||||||
"title": "Регистрация",
|
"title": "Регистрация",
|
||||||
"description": "Создайте свой аккаунт ZITADEL.",
|
"description": "Создайте свой аккаунт ZITADEL.",
|
||||||
|
"noMethodAvailableWarning": "Нет доступных методов аутентификации. Обратитесь к администратору.",
|
||||||
"selectMethod": "Выберите метод аутентификации",
|
"selectMethod": "Выберите метод аутентификации",
|
||||||
"agreeTo": "Для регистрации необходимо принять условия:",
|
"agreeTo": "Для регистрации необходимо принять условия:",
|
||||||
"termsOfService": "Условия использования",
|
"termsOfService": "Условия использования",
|
||||||
"privacyPolicy": "Политика конфиденциальности",
|
"privacyPolicy": "Политика конфиденциальности",
|
||||||
"submit": "Продолжить",
|
"submit": "Продолжить",
|
||||||
|
"orUseIDP": "или используйте Identity Provider",
|
||||||
"password": {
|
"password": {
|
||||||
"title": "Установить пароль",
|
"title": "Установить пароль",
|
||||||
"description": "Установите пароль для вашего аккаунта",
|
"description": "Установите пароль для вашего аккаунта",
|
||||||
|
@@ -72,6 +72,10 @@
|
|||||||
"linkingError": {
|
"linkingError": {
|
||||||
"title": "账户链接失败",
|
"title": "账户链接失败",
|
||||||
"description": "链接账户时发生错误。"
|
"description": "链接账户时发生错误。"
|
||||||
|
},
|
||||||
|
"completeRegister": {
|
||||||
|
"title": "完成注册",
|
||||||
|
"description": "完成您的账户注册。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mfa": {
|
"mfa": {
|
||||||
@@ -149,11 +153,13 @@
|
|||||||
},
|
},
|
||||||
"title": "注册",
|
"title": "注册",
|
||||||
"description": "创建您的 ZITADEL 账户。",
|
"description": "创建您的 ZITADEL 账户。",
|
||||||
|
"noMethodAvailableWarning": "没有可用的认证方法。请联系您的系统管理员。",
|
||||||
"selectMethod": "选择您想使用的认证方法",
|
"selectMethod": "选择您想使用的认证方法",
|
||||||
"agreeTo": "注册即表示您同意条款和条件",
|
"agreeTo": "注册即表示您同意条款和条件",
|
||||||
"termsOfService": "服务条款",
|
"termsOfService": "服务条款",
|
||||||
"privacyPolicy": "隐私政策",
|
"privacyPolicy": "隐私政策",
|
||||||
"submit": "继续",
|
"submit": "继续",
|
||||||
|
"orUseIDP": "或使用身份提供者",
|
||||||
"password": {
|
"password": {
|
||||||
"title": "设置密码",
|
"title": "设置密码",
|
||||||
"description": "为您的账户设置密码",
|
"description": "为您的账户设置密码",
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
import { IdpSignin } from "@/components/idp-signin";
|
import { IdpSignin } from "@/components/idp-signin";
|
||||||
|
import { completeIDP } from "@/components/idps/pages/complete-idp";
|
||||||
import { linkingFailed } from "@/components/idps/pages/linking-failed";
|
import { linkingFailed } from "@/components/idps/pages/linking-failed";
|
||||||
import { linkingSuccess } from "@/components/idps/pages/linking-success";
|
import { linkingSuccess } from "@/components/idps/pages/linking-success";
|
||||||
import { loginFailed } from "@/components/idps/pages/login-failed";
|
import { loginFailed } from "@/components/idps/pages/login-failed";
|
||||||
@@ -9,24 +10,63 @@ import {
|
|||||||
addHuman,
|
addHuman,
|
||||||
addIDPLink,
|
addIDPLink,
|
||||||
getBrandingSettings,
|
getBrandingSettings,
|
||||||
|
getDefaultOrg,
|
||||||
getIDPByID,
|
getIDPByID,
|
||||||
getLoginSettings,
|
getLoginSettings,
|
||||||
getOrgsByDomain,
|
getOrgsByDomain,
|
||||||
listUsers,
|
listUsers,
|
||||||
retrieveIDPIntent,
|
retrieveIDPIntent,
|
||||||
|
updateHuman,
|
||||||
} from "@/lib/zitadel";
|
} from "@/lib/zitadel";
|
||||||
import { ConnectError, create } from "@zitadel/client";
|
import { ConnectError, create } from "@zitadel/client";
|
||||||
import { AutoLinkingOption } from "@zitadel/proto/zitadel/idp/v2/idp_pb";
|
import { AutoLinkingOption } from "@zitadel/proto/zitadel/idp/v2/idp_pb";
|
||||||
import { OrganizationSchema } from "@zitadel/proto/zitadel/object/v2/object_pb";
|
import { OrganizationSchema } from "@zitadel/proto/zitadel/object/v2/object_pb";
|
||||||
|
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
|
||||||
import {
|
import {
|
||||||
AddHumanUserRequest,
|
AddHumanUserRequest,
|
||||||
AddHumanUserRequestSchema,
|
AddHumanUserRequestSchema,
|
||||||
|
UpdateHumanUserRequestSchema,
|
||||||
} from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
} 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";
|
||||||
|
|
||||||
const ORG_SUFFIX_REGEX = /(?<=@)(.+)/;
|
const ORG_SUFFIX_REGEX = /(?<=@)(.+)/;
|
||||||
|
|
||||||
|
async function resolveOrganizationForUser({
|
||||||
|
organization,
|
||||||
|
addHumanUser,
|
||||||
|
serviceUrl,
|
||||||
|
}: {
|
||||||
|
organization?: string;
|
||||||
|
addHumanUser?: { username?: string };
|
||||||
|
serviceUrl: string;
|
||||||
|
}): Promise<string | undefined> {
|
||||||
|
if (organization) return organization;
|
||||||
|
|
||||||
|
if (addHumanUser?.username && ORG_SUFFIX_REGEX.test(addHumanUser.username)) {
|
||||||
|
const matched = ORG_SUFFIX_REGEX.exec(addHumanUser.username);
|
||||||
|
const suffix = matched?.[1] ?? "";
|
||||||
|
|
||||||
|
const orgs = await getOrgsByDomain({
|
||||||
|
serviceUrl,
|
||||||
|
domain: suffix,
|
||||||
|
});
|
||||||
|
const orgToCheckForDiscovery =
|
||||||
|
orgs.result && orgs.result.length === 1 ? orgs.result[0].id : undefined;
|
||||||
|
|
||||||
|
if (orgToCheckForDiscovery) {
|
||||||
|
const orgLoginSettings = await getLoginSettings({
|
||||||
|
serviceUrl,
|
||||||
|
organization: orgToCheckForDiscovery,
|
||||||
|
});
|
||||||
|
if (orgLoginSettings?.allowDomainDiscovery) {
|
||||||
|
return orgToCheckForDiscovery;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
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>>;
|
||||||
params: Promise<{ provider: string }>;
|
params: Promise<{ provider: string }>;
|
||||||
@@ -35,17 +75,26 @@ export default async function Page(props: {
|
|||||||
const searchParams = await props.searchParams;
|
const searchParams = await props.searchParams;
|
||||||
const locale = getLocale();
|
const locale = getLocale();
|
||||||
const t = await getTranslations({ locale, namespace: "idp" });
|
const t = await getTranslations({ locale, namespace: "idp" });
|
||||||
const { id, token, requestId, organization, link } = searchParams;
|
let { id, token, requestId, organization, link } = searchParams;
|
||||||
const { provider } = params;
|
const { provider } = params;
|
||||||
|
|
||||||
const _headers = await headers();
|
const _headers = await headers();
|
||||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||||
|
|
||||||
const branding = await getBrandingSettings({
|
let branding = await getBrandingSettings({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
organization,
|
organization,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!organization) {
|
||||||
|
const org: Organization | null = await getDefaultOrg({
|
||||||
|
serviceUrl,
|
||||||
|
});
|
||||||
|
if (org) {
|
||||||
|
organization = org.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!provider || !id || !token) {
|
if (!provider || !id || !token) {
|
||||||
return loginFailed(branding, "IDP context missing");
|
return loginFailed(branding, "IDP context missing");
|
||||||
}
|
}
|
||||||
@@ -59,18 +108,6 @@ export default async function Page(props: {
|
|||||||
const { idpInformation, userId } = intent;
|
const { idpInformation, userId } = intent;
|
||||||
let { addHumanUser } = intent;
|
let { addHumanUser } = intent;
|
||||||
|
|
||||||
// sign in user. If user should be linked continue
|
|
||||||
if (userId && !link) {
|
|
||||||
// TODO: update user if idp.options.isAutoUpdate is true
|
|
||||||
|
|
||||||
return loginSuccess(
|
|
||||||
userId,
|
|
||||||
{ idpIntentId: id, idpIntentToken: token },
|
|
||||||
requestId,
|
|
||||||
branding,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!idpInformation) {
|
if (!idpInformation) {
|
||||||
return loginFailed(branding, "IDP information missing");
|
return loginFailed(branding, "IDP information missing");
|
||||||
}
|
}
|
||||||
@@ -79,12 +116,41 @@ export default async function Page(props: {
|
|||||||
serviceUrl,
|
serviceUrl,
|
||||||
id: idpInformation.idpId,
|
id: idpInformation.idpId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const options = idp?.config?.options;
|
const options = idp?.config?.options;
|
||||||
|
|
||||||
if (!idp) {
|
if (!idp) {
|
||||||
throw new Error("IDP not found");
|
throw new Error("IDP not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sign in user. If user should be linked continue
|
||||||
|
if (userId && !link) {
|
||||||
|
// if auto update is enabled, we will update the user with the new information
|
||||||
|
if (options?.isAutoUpdate && addHumanUser) {
|
||||||
|
try {
|
||||||
|
await updateHuman({
|
||||||
|
serviceUrl,
|
||||||
|
request: create(UpdateHumanUserRequestSchema, {
|
||||||
|
userId: userId,
|
||||||
|
profile: addHumanUser.profile,
|
||||||
|
email: addHumanUser.email,
|
||||||
|
phone: addHumanUser.phone,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
// Log the error and continue with the login process
|
||||||
|
console.warn("An error occurred while updating the user:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return loginSuccess(
|
||||||
|
userId,
|
||||||
|
{ idpIntentId: id, idpIntentToken: token },
|
||||||
|
requestId,
|
||||||
|
branding,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (link) {
|
if (link) {
|
||||||
if (!options?.isLinkingAllowed) {
|
if (!options?.isLinkingAllowed) {
|
||||||
// linking was probably disallowed since the invitation was created
|
// linking was probably disallowed since the invitation was created
|
||||||
@@ -176,88 +242,95 @@ export default async function Page(props: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.isAutoCreation) {
|
let newUser;
|
||||||
let orgToRegisterOn: string | undefined = organization;
|
// automatic creation of a user is allowed and data is complete
|
||||||
let newUser;
|
if (options?.isAutoCreation && addHumanUser) {
|
||||||
|
const orgToRegisterOn = await resolveOrganizationForUser({
|
||||||
|
organization,
|
||||||
|
addHumanUser,
|
||||||
|
serviceUrl,
|
||||||
|
});
|
||||||
|
|
||||||
if (
|
let addHumanUserWithOrganization: AddHumanUserRequest;
|
||||||
!orgToRegisterOn &&
|
if (orgToRegisterOn) {
|
||||||
addHumanUser?.username && // username or email?
|
const organizationSchema = create(OrganizationSchema, {
|
||||||
ORG_SUFFIX_REGEX.test(addHumanUser.username)
|
org: { case: "orgId", value: orgToRegisterOn },
|
||||||
) {
|
|
||||||
const matched = ORG_SUFFIX_REGEX.exec(addHumanUser.username);
|
|
||||||
const suffix = matched?.[1] ?? "";
|
|
||||||
|
|
||||||
// this just returns orgs where the suffix is set as primary domain
|
|
||||||
const orgs = await getOrgsByDomain({
|
|
||||||
serviceUrl,
|
|
||||||
domain: suffix,
|
|
||||||
});
|
});
|
||||||
const orgToCheckForDiscovery =
|
|
||||||
orgs.result && orgs.result.length === 1 ? orgs.result[0].id : undefined;
|
|
||||||
|
|
||||||
const orgLoginSettings = await getLoginSettings({
|
addHumanUserWithOrganization = create(AddHumanUserRequestSchema, {
|
||||||
serviceUrl,
|
...addHumanUser,
|
||||||
organization: orgToCheckForDiscovery,
|
organization: organizationSchema,
|
||||||
});
|
});
|
||||||
if (orgLoginSettings?.allowDomainDiscovery) {
|
} else {
|
||||||
orgToRegisterOn = orgToCheckForDiscovery;
|
addHumanUserWithOrganization = create(
|
||||||
}
|
AddHumanUserRequestSchema,
|
||||||
}
|
addHumanUser,
|
||||||
|
|
||||||
if (addHumanUser) {
|
|
||||||
let addHumanUserWithOrganization: AddHumanUserRequest;
|
|
||||||
if (orgToRegisterOn) {
|
|
||||||
const organizationSchema = create(OrganizationSchema, {
|
|
||||||
org: { case: "orgId", value: orgToRegisterOn },
|
|
||||||
});
|
|
||||||
|
|
||||||
addHumanUserWithOrganization = create(AddHumanUserRequestSchema, {
|
|
||||||
...addHumanUser,
|
|
||||||
organization: organizationSchema,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
addHumanUserWithOrganization = create(
|
|
||||||
AddHumanUserRequestSchema,
|
|
||||||
addHumanUser,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
newUser = await addHuman({
|
|
||||||
serviceUrl,
|
|
||||||
request: addHumanUserWithOrganization,
|
|
||||||
});
|
|
||||||
} catch (error: unknown) {
|
|
||||||
console.error(
|
|
||||||
"An error occurred while creating the user:",
|
|
||||||
error,
|
|
||||||
addHumanUser,
|
|
||||||
);
|
|
||||||
return loginFailed(
|
|
||||||
branding,
|
|
||||||
(error as ConnectError).message
|
|
||||||
? (error as ConnectError).message
|
|
||||||
: "Could not create user",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newUser) {
|
|
||||||
return (
|
|
||||||
<DynamicTheme branding={branding}>
|
|
||||||
<div className="flex flex-col items-center space-y-4">
|
|
||||||
<h1>{t("registerSuccess.title")}</h1>
|
|
||||||
<p className="ztdl-p">{t("registerSuccess.description")}</p>
|
|
||||||
<IdpSignin
|
|
||||||
userId={newUser.userId}
|
|
||||||
idpIntent={{ idpIntentId: id, idpIntentToken: token }}
|
|
||||||
requestId={requestId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</DynamicTheme>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
newUser = await addHuman({
|
||||||
|
serviceUrl,
|
||||||
|
request: addHumanUserWithOrganization,
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error(
|
||||||
|
"An error occurred while creating the user:",
|
||||||
|
error,
|
||||||
|
addHumanUser,
|
||||||
|
);
|
||||||
|
return loginFailed(
|
||||||
|
branding,
|
||||||
|
(error as ConnectError).message
|
||||||
|
? (error as ConnectError).message
|
||||||
|
: "Could not create user",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (options?.isCreationAllowed) {
|
||||||
|
// if no user was found, we will create a new user manually / redirect to the registration page
|
||||||
|
const orgToRegisterOn = await resolveOrganizationForUser({
|
||||||
|
organization,
|
||||||
|
addHumanUser,
|
||||||
|
serviceUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (orgToRegisterOn) {
|
||||||
|
branding = await getBrandingSettings({
|
||||||
|
serviceUrl,
|
||||||
|
organization: orgToRegisterOn,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!orgToRegisterOn) {
|
||||||
|
return loginFailed(branding, "No organization found for registration");
|
||||||
|
}
|
||||||
|
|
||||||
|
return completeIDP({
|
||||||
|
branding,
|
||||||
|
idpIntent: { idpIntentId: id, idpIntentToken: token },
|
||||||
|
addHumanUser,
|
||||||
|
organization: orgToRegisterOn,
|
||||||
|
requestId,
|
||||||
|
idpUserId: idpInformation?.userId,
|
||||||
|
idpId: idpInformation?.idpId,
|
||||||
|
idpUserName: idpInformation?.userName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newUser) {
|
||||||
|
return (
|
||||||
|
<DynamicTheme branding={branding}>
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<h1>{t("registerSuccess.title")}</h1>
|
||||||
|
<p className="ztdl-p">{t("registerSuccess.description")}</p>
|
||||||
|
<IdpSignin
|
||||||
|
userId={newUser.userId}
|
||||||
|
idpIntent={{ idpIntentId: id, idpIntentToken: token }}
|
||||||
|
requestId={requestId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DynamicTheme>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// return login failed if no linking or creation is allowed and no user was found
|
// return login failed if no linking or creation is allowed and no user was found
|
||||||
|
@@ -75,7 +75,7 @@ export default async function Page(props: {
|
|||||||
submit={submit}
|
submit={submit}
|
||||||
allowRegister={!!loginSettings?.allowRegister}
|
allowRegister={!!loginSettings?.allowRegister}
|
||||||
>
|
>
|
||||||
{identityProviders && (
|
{identityProviders && loginSettings?.allowExternalIdp && (
|
||||||
<SignInWithIdp
|
<SignInWithIdp
|
||||||
identityProviders={identityProviders}
|
identityProviders={identityProviders}
|
||||||
requestId={requestId}
|
requestId={requestId}
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
|
import { Alert } from "@/components/alert";
|
||||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
import { RegisterForm } from "@/components/register-form";
|
import { RegisterForm } from "@/components/register-form";
|
||||||
|
import { SignInWithIdp } from "@/components/sign-in-with-idp";
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
import {
|
import {
|
||||||
|
getActiveIdentityProviders,
|
||||||
getBrandingSettings,
|
getBrandingSettings,
|
||||||
getDefaultOrg,
|
getDefaultOrg,
|
||||||
getLegalAndSupportSettings,
|
getLegalAndSupportSettings,
|
||||||
@@ -9,6 +12,7 @@ import {
|
|||||||
getPasswordComplexitySettings,
|
getPasswordComplexitySettings,
|
||||||
} from "@/lib/zitadel";
|
} from "@/lib/zitadel";
|
||||||
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
|
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 { getLocale, getTranslations } from "next-intl/server";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
|
|
||||||
@@ -18,6 +22,7 @@ export default async function Page(props: {
|
|||||||
const searchParams = await props.searchParams;
|
const searchParams = await props.searchParams;
|
||||||
const locale = getLocale();
|
const locale = getLocale();
|
||||||
const t = await getTranslations({ locale, namespace: "register" });
|
const t = await getTranslations({ locale, namespace: "register" });
|
||||||
|
const tError = await getTranslations({ locale, namespace: "error" });
|
||||||
|
|
||||||
let { firstname, lastname, email, organization, requestId } = searchParams;
|
let { firstname, lastname, email, organization, requestId } = searchParams;
|
||||||
|
|
||||||
@@ -52,6 +57,15 @@ export default async function Page(props: {
|
|||||||
organization,
|
organization,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const identityProviders = await getActiveIdentityProviders({
|
||||||
|
serviceUrl,
|
||||||
|
orgId: organization,
|
||||||
|
}).then((resp) => {
|
||||||
|
return resp.identityProviders.filter((idp) => {
|
||||||
|
return idp.options?.isAutoCreation || idp.options?.isCreationAllowed; // check if IDP allows to create account automatically or manual creation is allowed
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
if (!loginSettings?.allowRegister) {
|
if (!loginSettings?.allowRegister) {
|
||||||
return (
|
return (
|
||||||
<DynamicTheme branding={branding}>
|
<DynamicTheme branding={branding}>
|
||||||
@@ -69,16 +83,39 @@ export default async function Page(props: {
|
|||||||
<h1>{t("title")}</h1>
|
<h1>{t("title")}</h1>
|
||||||
<p className="ztdl-p">{t("description")}</p>
|
<p className="ztdl-p">{t("description")}</p>
|
||||||
|
|
||||||
{legal && passwordComplexitySettings && (
|
{!organization && <Alert>{tError("unknownContext")}</Alert>}
|
||||||
<RegisterForm
|
|
||||||
legal={legal}
|
{legal &&
|
||||||
organization={organization}
|
passwordComplexitySettings &&
|
||||||
firstname={firstname}
|
organization &&
|
||||||
lastname={lastname}
|
(loginSettings.allowUsernamePassword ||
|
||||||
email={email}
|
loginSettings.passkeysType == PasskeysType.ALLOWED) && (
|
||||||
requestId={requestId}
|
<RegisterForm
|
||||||
loginSettings={loginSettings}
|
idpCount={
|
||||||
></RegisterForm>
|
!loginSettings?.allowExternalIdp ? 0 : identityProviders.length
|
||||||
|
}
|
||||||
|
legal={legal}
|
||||||
|
organization={organization}
|
||||||
|
firstname={firstname}
|
||||||
|
lastname={lastname}
|
||||||
|
email={email}
|
||||||
|
requestId={requestId}
|
||||||
|
loginSettings={loginSettings}
|
||||||
|
></RegisterForm>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loginSettings?.allowExternalIdp && !!identityProviders.length && (
|
||||||
|
<>
|
||||||
|
<div className="py-3 flex flex-col items-center">
|
||||||
|
<p className="ztdl-p text-center">{t("orUseIDP")}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SignInWithIdp
|
||||||
|
identityProviders={identityProviders}
|
||||||
|
requestId={requestId}
|
||||||
|
organization={organization}
|
||||||
|
></SignInWithIdp>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DynamicTheme>
|
</DynamicTheme>
|
||||||
|
@@ -33,7 +33,7 @@ export default async function Page(props: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const missingData = !firstname || !lastname || !email;
|
const missingData = !firstname || !lastname || !email || !organization;
|
||||||
|
|
||||||
const legal = await getLegalAndSupportSettings({
|
const legal = await getLegalAndSupportSettings({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
@@ -73,7 +73,7 @@ export default async function Page(props: {
|
|||||||
email={email}
|
email={email}
|
||||||
firstname={firstname}
|
firstname={firstname}
|
||||||
lastname={lastname}
|
lastname={lastname}
|
||||||
organization={organization}
|
organization={organization as string} // organization is guaranteed to be a string here otherwise we would have returned earlier
|
||||||
requestId={requestId}
|
requestId={requestId}
|
||||||
></SetRegisterPasswordForm>
|
></SetRegisterPasswordForm>
|
||||||
)}
|
)}
|
||||||
|
54
apps/login/src/components/idps/pages/complete-idp.tsx
Normal file
54
apps/login/src/components/idps/pages/complete-idp.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { RegisterFormIDPIncomplete } from "@/components/register-form-idp-incomplete";
|
||||||
|
import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb";
|
||||||
|
import { AddHumanUserRequest } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||||
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
|
import { DynamicTheme } from "../../dynamic-theme";
|
||||||
|
|
||||||
|
export async function completeIDP({
|
||||||
|
idpUserId,
|
||||||
|
idpId,
|
||||||
|
idpUserName,
|
||||||
|
addHumanUser,
|
||||||
|
requestId,
|
||||||
|
organization,
|
||||||
|
branding,
|
||||||
|
idpIntent,
|
||||||
|
}: {
|
||||||
|
idpUserId: string;
|
||||||
|
idpId: string;
|
||||||
|
idpUserName: string;
|
||||||
|
addHumanUser?: AddHumanUserRequest;
|
||||||
|
requestId?: string;
|
||||||
|
organization: string;
|
||||||
|
branding?: BrandingSettings;
|
||||||
|
idpIntent: {
|
||||||
|
idpIntentId: string;
|
||||||
|
idpIntentToken: string;
|
||||||
|
};
|
||||||
|
}) {
|
||||||
|
const locale = getLocale();
|
||||||
|
const t = await getTranslations({ locale, namespace: "idp" });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DynamicTheme branding={branding}>
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<h1>{t("completeRegister.title")}</h1>
|
||||||
|
<p className="ztdl-p">{t("completeRegister.description")}</p>
|
||||||
|
|
||||||
|
<RegisterFormIDPIncomplete
|
||||||
|
idpUserId={idpUserId}
|
||||||
|
idpId={idpId}
|
||||||
|
idpUserName={idpUserName}
|
||||||
|
defaultValues={{
|
||||||
|
email: addHumanUser?.email?.email || "",
|
||||||
|
firstname: addHumanUser?.profile?.givenName || "",
|
||||||
|
lastname: addHumanUser?.profile?.familyName || "",
|
||||||
|
}}
|
||||||
|
requestId={requestId}
|
||||||
|
organization={organization}
|
||||||
|
idpIntent={idpIntent}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DynamicTheme>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { inviteUser } from "@/lib/server/invite";
|
import { registerUserAndLinkToIDP } from "@/lib/server/register";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -20,26 +20,39 @@ type Inputs =
|
|||||||
| FieldValues;
|
| FieldValues;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
firstname?: string;
|
organization: string;
|
||||||
lastname?: string;
|
requestId?: string;
|
||||||
email?: string;
|
idpIntent: {
|
||||||
organization?: string;
|
idpIntentId: string;
|
||||||
|
idpIntentToken: string;
|
||||||
|
};
|
||||||
|
defaultValues?: {
|
||||||
|
firstname?: string;
|
||||||
|
lastname?: string;
|
||||||
|
email?: string;
|
||||||
|
};
|
||||||
|
idpUserId: string;
|
||||||
|
idpId: string;
|
||||||
|
idpUserName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function InviteForm({
|
export function RegisterFormIDPIncomplete({
|
||||||
email,
|
|
||||||
firstname,
|
|
||||||
lastname,
|
|
||||||
organization,
|
organization,
|
||||||
|
requestId,
|
||||||
|
idpIntent,
|
||||||
|
defaultValues,
|
||||||
|
idpUserId,
|
||||||
|
idpId,
|
||||||
|
idpUserName,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const t = useTranslations("register");
|
const t = useTranslations("register");
|
||||||
|
|
||||||
const { register, handleSubmit, formState } = useForm<Inputs>({
|
const { register, handleSubmit, formState } = useForm<Inputs>({
|
||||||
mode: "onBlur",
|
mode: "onBlur",
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: email ?? "",
|
email: defaultValues?.email ?? "",
|
||||||
firstName: firstname ?? "",
|
firstname: defaultValues?.firstname ?? "",
|
||||||
lastname: lastname ?? "",
|
lastname: defaultValues?.lastname ?? "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,39 +61,37 @@ export function InviteForm({
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
async function submitAndContinue(values: Inputs) {
|
async function submitAndRegister(values: Inputs) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await inviteUser({
|
const response = await registerUserAndLinkToIDP({
|
||||||
|
idpId: idpId,
|
||||||
|
idpUserName: idpUserName,
|
||||||
|
idpUserId: idpUserId,
|
||||||
email: values.email,
|
email: values.email,
|
||||||
firstName: values.firstname,
|
firstName: values.firstname,
|
||||||
lastName: values.lastname,
|
lastName: values.lastname,
|
||||||
organization: organization,
|
organization: organization,
|
||||||
|
requestId: requestId,
|
||||||
|
idpIntent: idpIntent,
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setError("Could not create invitation Code");
|
setError("Could not register user");
|
||||||
return;
|
return;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response && typeof response === "object" && "error" in response) {
|
if (response && "error" in response && response.error) {
|
||||||
setError(response.error);
|
setError(response.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response) {
|
if (response && "redirect" in response && response.redirect) {
|
||||||
setError("Could not create invitation Code");
|
return router.push(response.redirect);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams({});
|
return response;
|
||||||
|
|
||||||
if (response) {
|
|
||||||
params.append("userId", response);
|
|
||||||
}
|
|
||||||
|
|
||||||
return router.push(`/invite/success?` + params);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { errors } = formState;
|
const { errors } = formState;
|
||||||
@@ -88,16 +99,6 @@ export function InviteForm({
|
|||||||
return (
|
return (
|
||||||
<form className="w-full">
|
<form className="w-full">
|
||||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||||
<div className="col-span-2">
|
|
||||||
<TextInput
|
|
||||||
type="email"
|
|
||||||
autoComplete="email"
|
|
||||||
required
|
|
||||||
{...register("email", { required: "This field is required" })}
|
|
||||||
label="E-mail"
|
|
||||||
error={errors.email?.message as string}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="">
|
<div className="">
|
||||||
<TextInput
|
<TextInput
|
||||||
type="firstname"
|
type="firstname"
|
||||||
@@ -106,6 +107,7 @@ export function InviteForm({
|
|||||||
{...register("firstname", { required: "This field is required" })}
|
{...register("firstname", { required: "This field is required" })}
|
||||||
label="First name"
|
label="First name"
|
||||||
error={errors.firstname?.message as string}
|
error={errors.firstname?.message as string}
|
||||||
|
data-testid="firstname-text-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="">
|
<div className="">
|
||||||
@@ -116,6 +118,18 @@ export function InviteForm({
|
|||||||
{...register("lastname", { required: "This field is required" })}
|
{...register("lastname", { required: "This field is required" })}
|
||||||
label="Last name"
|
label="Last name"
|
||||||
error={errors.lastname?.message as string}
|
error={errors.lastname?.message as string}
|
||||||
|
data-testid="lastname-text-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<TextInput
|
||||||
|
type="email"
|
||||||
|
autoComplete="email"
|
||||||
|
required
|
||||||
|
{...register("email", { required: "This field is required" })}
|
||||||
|
label="E-mail"
|
||||||
|
error={errors.email?.message as string}
|
||||||
|
data-testid="email-text-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -127,12 +141,13 @@ export function InviteForm({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-8 flex w-full flex-row items-center justify-between">
|
<div className="mt-8 flex w-full flex-row items-center justify-between">
|
||||||
<BackButton />
|
<BackButton data-testid="back-button" />
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant={ButtonVariants.Primary}
|
variant={ButtonVariants.Primary}
|
||||||
disabled={loading || !formState.isValid}
|
disabled={loading || !formState.isValid}
|
||||||
onClick={handleSubmit(submitAndContinue)}
|
onClick={handleSubmit(submitAndRegister)}
|
||||||
|
data-testid="submit-button"
|
||||||
>
|
>
|
||||||
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
||||||
{t("submit")}
|
{t("submit")}
|
@@ -10,7 +10,7 @@ import { useTranslations } from "next-intl";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { FieldValues, useForm } from "react-hook-form";
|
import { FieldValues, useForm } from "react-hook-form";
|
||||||
import { Alert } from "./alert";
|
import { Alert, AlertType } from "./alert";
|
||||||
import {
|
import {
|
||||||
AuthenticationMethod,
|
AuthenticationMethod,
|
||||||
AuthenticationMethodRadio,
|
AuthenticationMethodRadio,
|
||||||
@@ -35,9 +35,10 @@ type Props = {
|
|||||||
firstname?: string;
|
firstname?: string;
|
||||||
lastname?: string;
|
lastname?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
organization?: string;
|
organization: string;
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
loginSettings?: LoginSettings;
|
loginSettings?: LoginSettings;
|
||||||
|
idpCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function RegisterForm({
|
export function RegisterForm({
|
||||||
@@ -48,6 +49,7 @@ export function RegisterForm({
|
|||||||
organization,
|
organization,
|
||||||
requestId,
|
requestId,
|
||||||
loginSettings,
|
loginSettings,
|
||||||
|
idpCount = 0,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const t = useTranslations("register");
|
const t = useTranslations("register");
|
||||||
|
|
||||||
@@ -165,16 +167,29 @@ export function RegisterForm({
|
|||||||
onChange={setTosAndPolicyAccepted}
|
onChange={setTosAndPolicyAccepted}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<p className="mt-4 ztdl-p mb-6 block text-left">{t("selectMethod")}</p>
|
|
||||||
{/* show chooser if both methods are allowed */}
|
{/* show chooser if both methods are allowed */}
|
||||||
{loginSettings &&
|
{loginSettings &&
|
||||||
loginSettings.allowUsernamePassword &&
|
loginSettings.allowUsernamePassword &&
|
||||||
loginSettings.passkeysType == PasskeysType.ALLOWED && (
|
loginSettings.passkeysType == PasskeysType.ALLOWED && (
|
||||||
<div className="pb-4">
|
<>
|
||||||
<AuthenticationMethodRadio
|
<p className="mt-4 ztdl-p mb-6 block text-left">
|
||||||
selected={selected}
|
{t("selectMethod")}
|
||||||
selectionChanged={setSelected}
|
</p>
|
||||||
/>
|
|
||||||
|
<div className="pb-4">
|
||||||
|
<AuthenticationMethodRadio
|
||||||
|
selected={selected}
|
||||||
|
selectionChanged={setSelected}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loginSettings?.allowUsernamePassword &&
|
||||||
|
loginSettings?.passkeysType != PasskeysType.ALLOWED &&
|
||||||
|
(!loginSettings?.allowExternalIdp || !idpCount) && (
|
||||||
|
<div className="py-4">
|
||||||
|
<Alert type={AlertType.INFO}>{t("noMethodAvailableWarning")}</Alert>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -183,6 +198,7 @@ export function RegisterForm({
|
|||||||
<Alert>{error}</Alert>
|
<Alert>{error}</Alert>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-8 flex w-full flex-row items-center justify-between">
|
<div className="mt-8 flex w-full flex-row items-center justify-between">
|
||||||
<BackButton data-testid="back-button" />
|
<BackButton data-testid="back-button" />
|
||||||
<Button
|
<Button
|
||||||
|
@@ -31,7 +31,7 @@ type Props = {
|
|||||||
email: string;
|
email: string;
|
||||||
firstname: string;
|
firstname: string;
|
||||||
lastname: string;
|
lastname: string;
|
||||||
organization?: string;
|
organization: string;
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -109,15 +109,20 @@ export async function createSessionAndUpdateCookie(command: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createSessionForIdpAndUpdateCookie(
|
export async function createSessionForIdpAndUpdateCookie({
|
||||||
userId: string,
|
userId,
|
||||||
|
idpIntent,
|
||||||
|
requestId,
|
||||||
|
lifetime,
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
idpIntent: {
|
idpIntent: {
|
||||||
idpIntentId?: string | undefined;
|
idpIntentId?: string | undefined;
|
||||||
idpIntentToken?: string | undefined;
|
idpIntentToken?: string | undefined;
|
||||||
},
|
};
|
||||||
requestId: string | undefined,
|
requestId: string | undefined;
|
||||||
lifetime?: Duration,
|
lifetime?: Duration;
|
||||||
): Promise<Session> {
|
}): Promise<Session> {
|
||||||
const _headers = await headers();
|
const _headers = await headers();
|
||||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||||
|
|
||||||
|
@@ -122,12 +122,12 @@ export async function createNewSessionFromIdpIntent(
|
|||||||
organization: userResponse.user.details?.resourceOwner,
|
organization: userResponse.user.details?.resourceOwner,
|
||||||
});
|
});
|
||||||
|
|
||||||
const session = await createSessionForIdpAndUpdateCookie(
|
const session = await createSessionForIdpAndUpdateCookie({
|
||||||
command.userId,
|
userId: command.userId,
|
||||||
command.idpIntent,
|
idpIntent: command.idpIntent,
|
||||||
command.requestId,
|
requestId: command.requestId,
|
||||||
loginSettings?.externalLoginCheckLifetime,
|
lifetime: loginSettings?.externalLoginCheckLifetime,
|
||||||
);
|
});
|
||||||
|
|
||||||
if (!session || !session.factors?.user) {
|
if (!session || !session.factors?.user) {
|
||||||
return { error: "Could not create session" };
|
return { error: "Could not create session" };
|
||||||
|
@@ -1,58 +0,0 @@
|
|||||||
"use server";
|
|
||||||
|
|
||||||
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-url";
|
|
||||||
|
|
||||||
type InviteUserCommand = {
|
|
||||||
email: string;
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
password?: string;
|
|
||||||
organization?: string;
|
|
||||||
requestId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RegisterUserResponse = {
|
|
||||||
userId: string;
|
|
||||||
sessionId: string;
|
|
||||||
factors: Factors | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function inviteUser(command: InviteUserCommand) {
|
|
||||||
const _headers = await headers();
|
|
||||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
|
||||||
const host = _headers.get("host");
|
|
||||||
|
|
||||||
if (!host) {
|
|
||||||
return { error: "Could not get domain" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const human = await addHumanUser({
|
|
||||||
serviceUrl,
|
|
||||||
email: command.email,
|
|
||||||
firstName: command.firstName,
|
|
||||||
lastName: command.lastName,
|
|
||||||
password: command.password ? command.password : undefined,
|
|
||||||
organization: command.organization,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!human) {
|
|
||||||
return { error: "Could not create user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
|
|
||||||
|
|
||||||
const codeResponse = await createInviteCode({
|
|
||||||
serviceUrl,
|
|
||||||
urlTemplate: `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true`,
|
|
||||||
userId: human.userId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!codeResponse || !human) {
|
|
||||||
return { error: "Could not create invite code" };
|
|
||||||
}
|
|
||||||
|
|
||||||
return human.userId;
|
|
||||||
}
|
|
@@ -1,7 +1,15 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { createSessionAndUpdateCookie } from "@/lib/server/cookie";
|
import {
|
||||||
import { addHumanUser, getLoginSettings, getUserByID } from "@/lib/zitadel";
|
createSessionAndUpdateCookie,
|
||||||
|
createSessionForIdpAndUpdateCookie,
|
||||||
|
} from "@/lib/server/cookie";
|
||||||
|
import {
|
||||||
|
addHumanUser,
|
||||||
|
addIDPLink,
|
||||||
|
getLoginSettings,
|
||||||
|
getUserByID,
|
||||||
|
} from "@/lib/zitadel";
|
||||||
import { create } from "@zitadel/client";
|
import { create } from "@zitadel/client";
|
||||||
import { Factors } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
import { Factors } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||||
import {
|
import {
|
||||||
@@ -18,7 +26,7 @@ type RegisterUserCommand = {
|
|||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
organization?: string;
|
organization: string;
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -133,3 +141,93 @@ export async function registerUser(command: RegisterUserCommand) {
|
|||||||
return { redirect: url };
|
return { redirect: url };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RegisterUserAndLinkToIDPommand = {
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
organization: string;
|
||||||
|
requestId?: string;
|
||||||
|
idpIntent: {
|
||||||
|
idpIntentId: string;
|
||||||
|
idpIntentToken: string;
|
||||||
|
};
|
||||||
|
idpUserId: string;
|
||||||
|
idpId: string;
|
||||||
|
idpUserName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type registerUserAndLinkToIDPResponse = {
|
||||||
|
userId: string;
|
||||||
|
sessionId: string;
|
||||||
|
factors: Factors | undefined;
|
||||||
|
};
|
||||||
|
export async function registerUserAndLinkToIDP(
|
||||||
|
command: RegisterUserAndLinkToIDPommand,
|
||||||
|
) {
|
||||||
|
const _headers = await headers();
|
||||||
|
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||||
|
const host = _headers.get("host");
|
||||||
|
|
||||||
|
if (!host || typeof host !== "string") {
|
||||||
|
throw new Error("No host found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const addResponse = await addHumanUser({
|
||||||
|
serviceUrl,
|
||||||
|
email: command.email,
|
||||||
|
firstName: command.firstName,
|
||||||
|
lastName: command.lastName,
|
||||||
|
organization: command.organization,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!addResponse) {
|
||||||
|
return { error: "Could not create user" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginSettings = await getLoginSettings({
|
||||||
|
serviceUrl,
|
||||||
|
organization: command.organization,
|
||||||
|
});
|
||||||
|
|
||||||
|
const idpLink = await addIDPLink({
|
||||||
|
serviceUrl,
|
||||||
|
idp: {
|
||||||
|
id: command.idpId,
|
||||||
|
userId: command.idpUserId,
|
||||||
|
userName: command.idpUserName,
|
||||||
|
},
|
||||||
|
userId: addResponse.userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!idpLink) {
|
||||||
|
return { error: "Could not link IDP to user" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await createSessionForIdpAndUpdateCookie({
|
||||||
|
requestId: command.requestId,
|
||||||
|
userId: addResponse.userId, // the user we just created
|
||||||
|
idpIntent: command.idpIntent,
|
||||||
|
lifetime: loginSettings?.externalLoginCheckLifetime,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session || !session.factors?.user) {
|
||||||
|
return { error: "Could not create session" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = await getNextUrl(
|
||||||
|
command.requestId && session.id
|
||||||
|
? {
|
||||||
|
sessionId: session.id,
|
||||||
|
requestId: command.requestId,
|
||||||
|
organization: session.factors.user.organizationId,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
loginName: session.factors.user.loginName,
|
||||||
|
organization: session.factors.user.organizationId,
|
||||||
|
},
|
||||||
|
loginSettings?.defaultRedirectUri,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { redirect: url };
|
||||||
|
}
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
import { Client, create, Duration } from "@zitadel/client";
|
import { Client, create, Duration } from "@zitadel/client";
|
||||||
import { makeReqCtx } from "@zitadel/client/v2";
|
import { makeReqCtx } from "@zitadel/client/v2";
|
||||||
import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_service_pb";
|
import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_service_pb";
|
||||||
import { TextQueryMethod } from "@zitadel/proto/zitadel/object/v2/object_pb";
|
import {
|
||||||
|
OrganizationSchema,
|
||||||
|
TextQueryMethod,
|
||||||
|
} from "@zitadel/proto/zitadel/object/v2/object_pb";
|
||||||
import {
|
import {
|
||||||
CreateCallbackRequest,
|
CreateCallbackRequest,
|
||||||
OIDCService,
|
OIDCService,
|
||||||
@@ -32,11 +35,13 @@ import {
|
|||||||
import { SendInviteCodeSchema } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
import { SendInviteCodeSchema } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||||
import {
|
import {
|
||||||
AddHumanUserRequest,
|
AddHumanUserRequest,
|
||||||
|
AddHumanUserRequestSchema,
|
||||||
ResendEmailCodeRequest,
|
ResendEmailCodeRequest,
|
||||||
ResendEmailCodeRequestSchema,
|
ResendEmailCodeRequestSchema,
|
||||||
SendEmailCodeRequestSchema,
|
SendEmailCodeRequestSchema,
|
||||||
SetPasswordRequest,
|
SetPasswordRequest,
|
||||||
SetPasswordRequestSchema,
|
SetPasswordRequestSchema,
|
||||||
|
UpdateHumanUserRequest,
|
||||||
UserService,
|
UserService,
|
||||||
VerifyPasskeyRegistrationRequest,
|
VerifyPasskeyRegistrationRequest,
|
||||||
VerifyU2FRegistrationRequest,
|
VerifyU2FRegistrationRequest,
|
||||||
@@ -387,8 +392,8 @@ export type AddHumanUserData = {
|
|||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
email: string;
|
email: string;
|
||||||
password: string | undefined;
|
password?: string;
|
||||||
organization: string | undefined;
|
organization: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function addHumanUser({
|
export async function addHumanUser({
|
||||||
@@ -404,23 +409,36 @@ export async function addHumanUser({
|
|||||||
serviceUrl,
|
serviceUrl,
|
||||||
);
|
);
|
||||||
|
|
||||||
return userService.addHumanUser({
|
let addHumanUserRequest: AddHumanUserRequest = create(
|
||||||
email: {
|
AddHumanUserRequestSchema,
|
||||||
email,
|
{
|
||||||
verification: {
|
email: {
|
||||||
case: "isVerified",
|
email,
|
||||||
value: false,
|
verification: {
|
||||||
|
case: "isVerified",
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
username: email,
|
||||||
|
profile: { givenName: firstName, familyName: lastName },
|
||||||
|
passwordType: password
|
||||||
|
? { case: "password", value: { password } }
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
username: email,
|
);
|
||||||
profile: { givenName: firstName, familyName: lastName },
|
|
||||||
organization: organization
|
if (organization) {
|
||||||
? { org: { case: "orgId", value: organization } }
|
const organizationSchema = create(OrganizationSchema, {
|
||||||
: undefined,
|
org: { case: "orgId", value: organization },
|
||||||
passwordType: password
|
});
|
||||||
? { case: "password", value: { password } }
|
|
||||||
: undefined,
|
addHumanUserRequest = {
|
||||||
});
|
...addHumanUserRequest,
|
||||||
|
organization: organizationSchema,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return userService.addHumanUser(addHumanUserRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addHuman({
|
export async function addHuman({
|
||||||
@@ -438,6 +456,21 @@ export async function addHuman({
|
|||||||
return userService.addHumanUser(request);
|
return userService.addHumanUser(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateHuman({
|
||||||
|
serviceUrl,
|
||||||
|
request,
|
||||||
|
}: {
|
||||||
|
serviceUrl: string;
|
||||||
|
request: UpdateHumanUserRequest;
|
||||||
|
}) {
|
||||||
|
const userService: Client<typeof UserService> = await createServiceForHost(
|
||||||
|
UserService,
|
||||||
|
serviceUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
return userService.updateHumanUser(request);
|
||||||
|
}
|
||||||
|
|
||||||
export async function verifyTOTPRegistration({
|
export async function verifyTOTPRegistration({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
code,
|
code,
|
||||||
|
Reference in New Issue
Block a user