mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 08:37:32 +00:00
user missing data form for IDPs
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||
import { IdpSignin } from "@/components/idp-signin";
|
||||
import { completeIDP } from "@/components/idps/pages/complete-idp";
|
||||
import { linkingFailed } from "@/components/idps/pages/linking-failed";
|
||||
import { linkingSuccess } from "@/components/idps/pages/linking-success";
|
||||
import { loginFailed } from "@/components/idps/pages/login-failed";
|
||||
@@ -24,7 +25,6 @@ import {
|
||||
} from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||
import { getLocale, getTranslations } from "next-intl/server";
|
||||
import { headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
const ORG_SUFFIX_REGEX = /(?<=@)(.+)/;
|
||||
|
||||
@@ -177,9 +177,10 @@ export default async function Page(props: {
|
||||
}
|
||||
}
|
||||
|
||||
if (options?.isAutoCreation) {
|
||||
let newUser;
|
||||
// automatic creation of a user is allowed and data is complete
|
||||
if (options?.isAutoCreation && addHumanUser) {
|
||||
let orgToRegisterOn: string | undefined = organization;
|
||||
let newUser;
|
||||
|
||||
if (
|
||||
!orgToRegisterOn &&
|
||||
@@ -206,70 +207,68 @@ export default async function Page(props: {
|
||||
}
|
||||
}
|
||||
|
||||
// if addHumanUser is provided in the intent, expect that it can be created otherwise show an error
|
||||
if (addHumanUser) {
|
||||
let addHumanUserWithOrganization: AddHumanUserRequest;
|
||||
if (orgToRegisterOn) {
|
||||
const organizationSchema = create(OrganizationSchema, {
|
||||
org: { case: "orgId", value: orgToRegisterOn },
|
||||
});
|
||||
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",
|
||||
);
|
||||
}
|
||||
addHumanUserWithOrganization = create(AddHumanUserRequestSchema, {
|
||||
...addHumanUser,
|
||||
organization: organizationSchema,
|
||||
});
|
||||
} else {
|
||||
// if no user was found, we will create a new user manually / redirect to the registration page
|
||||
if (options.isCreationAllowed) {
|
||||
const registerParams = new URLSearchParams({
|
||||
idpIntentId: id,
|
||||
idpIntentToken: token,
|
||||
organization: organization ?? "",
|
||||
});
|
||||
return redirect(`/register?${registerParams})}`);
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
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 no user was found, we will create a new user manually / redirect to the registration page
|
||||
if (options?.isCreationAllowed) {
|
||||
return completeIDP({
|
||||
branding,
|
||||
idpInformation,
|
||||
organization,
|
||||
requestId,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
|
38
apps/login/src/components/idps/pages/complete-idp.tsx
Normal file
38
apps/login/src/components/idps/pages/complete-idp.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { RegisterFormIDPIncomplete } from "@/components/register-form-idp-incomplete";
|
||||
import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb";
|
||||
import { IDPInformation } from "@zitadel/proto/zitadel/user/v2/idp_pb";
|
||||
import { getLocale, getTranslations } from "next-intl/server";
|
||||
import { DynamicTheme } from "../../dynamic-theme";
|
||||
|
||||
export async function completeIDP({
|
||||
userId,
|
||||
idpInformation,
|
||||
requestId,
|
||||
organization,
|
||||
branding,
|
||||
}: {
|
||||
userId: string;
|
||||
idpInformation: IDPInformation;
|
||||
requestId?: string;
|
||||
organization?: string;
|
||||
branding?: BrandingSettings;
|
||||
}) {
|
||||
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("loginSuccess.title")}</h1>
|
||||
<p className="ztdl-p">{t("loginSuccess.description")}</p>
|
||||
|
||||
<RegisterFormIDPIncomplete
|
||||
userId={userId}
|
||||
idpInformation={idpInformation}
|
||||
requestId={requestId}
|
||||
organization={organization}
|
||||
/>
|
||||
</div>
|
||||
</DynamicTheme>
|
||||
);
|
||||
}
|
143
apps/login/src/components/register-form-idp-incomplete.tsx
Normal file
143
apps/login/src/components/register-form-idp-incomplete.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
"use client";
|
||||
|
||||
import { IDPInformation } from "@zitadel/proto/zitadel/user/v2/idp_pb";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { FieldValues, useForm } from "react-hook-form";
|
||||
import { Alert } from "./alert";
|
||||
import { AuthenticationMethod, methods } from "./authentication-method-radio";
|
||||
import { BackButton } from "./back-button";
|
||||
import { Button, ButtonVariants } from "./button";
|
||||
import { TextInput } from "./input";
|
||||
import { Spinner } from "./spinner";
|
||||
|
||||
type Inputs =
|
||||
| {
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
email: string;
|
||||
}
|
||||
| FieldValues;
|
||||
|
||||
type Props = {
|
||||
organization?: string;
|
||||
requestId?: string;
|
||||
idpInformation?: IDPInformation;
|
||||
};
|
||||
|
||||
export function RegisterFormIDPIncomplete({
|
||||
organization,
|
||||
requestId,
|
||||
idpInformation,
|
||||
}: Props) {
|
||||
const t = useTranslations("register");
|
||||
|
||||
const { register, handleSubmit, formState } = useForm<Inputs>({
|
||||
mode: "onBlur",
|
||||
defaultValues: {
|
||||
email: idpInformation?.rawInformation?.email ?? "",
|
||||
firstName: idpInformation?.rawInformation?.firstname ?? "",
|
||||
lastname: idpInformation?.rawInformation?.lastname ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [selected, setSelected] = useState<AuthenticationMethod>(methods[0]);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
async function submitAndRegister(values: Inputs) {
|
||||
setLoading(true);
|
||||
const response = await registerUserAndLinkToIDP({
|
||||
email: values.email,
|
||||
firstName: values.firstname,
|
||||
lastName: values.lastname,
|
||||
organization: organization,
|
||||
requestId: requestId,
|
||||
})
|
||||
.catch(() => {
|
||||
setError("Could not register user");
|
||||
return;
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
if (response && "error" in response && response.error) {
|
||||
setError(response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response && "redirect" in response && response.redirect) {
|
||||
return router.push(response.redirect);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
const { errors } = formState;
|
||||
|
||||
return (
|
||||
<form className="w-full">
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div className="">
|
||||
<TextInput
|
||||
type="firstname"
|
||||
autoComplete="firstname"
|
||||
required
|
||||
{...register("firstname", { required: "This field is required" })}
|
||||
label="First name"
|
||||
error={errors.firstname?.message as string}
|
||||
data-testid="firstname-text-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="">
|
||||
<TextInput
|
||||
type="lastname"
|
||||
autoComplete="lastname"
|
||||
required
|
||||
{...register("lastname", { required: "This field is required" })}
|
||||
label="Last name"
|
||||
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>
|
||||
|
||||
<p className="mt-4 ztdl-p mb-6 block text-left">{t("completeData")}</p>
|
||||
|
||||
{error && (
|
||||
<div className="py-4">
|
||||
<Alert>{error}</Alert>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-8 flex w-full flex-row items-center justify-between">
|
||||
<BackButton data-testid="back-button" />
|
||||
<Button
|
||||
type="submit"
|
||||
variant={ButtonVariants.Primary}
|
||||
disabled={loading || !formState.isValid || !tosAndPolicyAccepted}
|
||||
onClick={handleSubmit(submitAndRegister)}
|
||||
data-testid="submit-button"
|
||||
>
|
||||
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
||||
{t("submit")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
@@ -109,15 +109,20 @@ export async function createSessionAndUpdateCookie(command: {
|
||||
}
|
||||
}
|
||||
|
||||
export async function createSessionForIdpAndUpdateCookie(
|
||||
userId: string,
|
||||
export async function createSessionForIdpAndUpdateCookie({
|
||||
userId,
|
||||
idpIntent,
|
||||
requestId,
|
||||
lifetime,
|
||||
}: {
|
||||
userId: string;
|
||||
idpIntent: {
|
||||
idpIntentId?: string | undefined;
|
||||
idpIntentToken?: string | undefined;
|
||||
},
|
||||
requestId: string | undefined,
|
||||
lifetime?: Duration,
|
||||
): Promise<Session> {
|
||||
};
|
||||
requestId: string | undefined;
|
||||
lifetime?: Duration;
|
||||
}): Promise<Session> {
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
|
||||
|
@@ -122,12 +122,12 @@ export async function createNewSessionFromIdpIntent(
|
||||
organization: userResponse.user.details?.resourceOwner,
|
||||
});
|
||||
|
||||
const session = await createSessionForIdpAndUpdateCookie(
|
||||
command.userId,
|
||||
command.idpIntent,
|
||||
command.requestId,
|
||||
loginSettings?.externalLoginCheckLifetime,
|
||||
);
|
||||
const session = await createSessionForIdpAndUpdateCookie({
|
||||
userId: command.userId,
|
||||
idpIntent: command.idpIntent,
|
||||
requestId: command.requestId,
|
||||
lifetime: loginSettings?.externalLoginCheckLifetime,
|
||||
});
|
||||
|
||||
if (!session || !session.factors?.user) {
|
||||
return { error: "Could not create session" };
|
||||
|
@@ -1,6 +1,9 @@
|
||||
"use server";
|
||||
|
||||
import { createSessionAndUpdateCookie } from "@/lib/server/cookie";
|
||||
import {
|
||||
createSessionAndUpdateCookie,
|
||||
createSessionForIdpAndUpdateCookie,
|
||||
} from "@/lib/server/cookie";
|
||||
import { addHumanUser, getLoginSettings, getUserByID } from "@/lib/zitadel";
|
||||
import { create } from "@zitadel/client";
|
||||
import { Factors } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||
@@ -133,3 +136,79 @@ export async function registerUser(command: RegisterUserCommand) {
|
||||
return { redirect: url };
|
||||
}
|
||||
}
|
||||
|
||||
type RegisterUserAndLinkToIDPommand = {
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
organization?: string;
|
||||
requestId?: string;
|
||||
idpIntent: {
|
||||
idpIntentId: string;
|
||||
idpIntentToken: string;
|
||||
};
|
||||
userId: 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,
|
||||
});
|
||||
|
||||
// TODO: addIDPLink to addResponse
|
||||
|
||||
const session = await createSessionForIdpAndUpdateCookie({
|
||||
requestId: command.requestId,
|
||||
userId: command.userId,
|
||||
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 };
|
||||
}
|
||||
|
@@ -387,7 +387,7 @@ export type AddHumanUserData = {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
password: string | undefined;
|
||||
password?: string;
|
||||
organization: string | undefined;
|
||||
};
|
||||
|
||||
|
Reference in New Issue
Block a user