mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 11: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 { 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";
|
||||||
@@ -24,7 +25,6 @@ import {
|
|||||||
} 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";
|
||||||
import { redirect } from "next/navigation";
|
|
||||||
|
|
||||||
const ORG_SUFFIX_REGEX = /(?<=@)(.+)/;
|
const ORG_SUFFIX_REGEX = /(?<=@)(.+)/;
|
||||||
|
|
||||||
@@ -177,9 +177,10 @@ export default async function Page(props: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.isAutoCreation) {
|
|
||||||
let orgToRegisterOn: string | undefined = organization;
|
|
||||||
let newUser;
|
let newUser;
|
||||||
|
// automatic creation of a user is allowed and data is complete
|
||||||
|
if (options?.isAutoCreation && addHumanUser) {
|
||||||
|
let orgToRegisterOn: string | undefined = organization;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!orgToRegisterOn &&
|
!orgToRegisterOn &&
|
||||||
@@ -206,8 +207,6 @@ 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;
|
let addHumanUserWithOrganization: AddHumanUserRequest;
|
||||||
if (orgToRegisterOn) {
|
if (orgToRegisterOn) {
|
||||||
const organizationSchema = create(OrganizationSchema, {
|
const organizationSchema = create(OrganizationSchema, {
|
||||||
@@ -243,16 +242,17 @@ export default async function Page(props: {
|
|||||||
: "Could not create user",
|
: "Could not create user",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} 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 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) {
|
if (newUser) {
|
||||||
@@ -270,7 +270,6 @@ export default async function Page(props: {
|
|||||||
</DynamicTheme>
|
</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
|
||||||
return loginFailed(branding, "No user found");
|
return loginFailed(branding, "No user 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(
|
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,6 +1,9 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { createSessionAndUpdateCookie } from "@/lib/server/cookie";
|
import {
|
||||||
|
createSessionAndUpdateCookie,
|
||||||
|
createSessionForIdpAndUpdateCookie,
|
||||||
|
} from "@/lib/server/cookie";
|
||||||
import { addHumanUser, getLoginSettings, getUserByID } from "@/lib/zitadel";
|
import { addHumanUser, 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";
|
||||||
@@ -133,3 +136,79 @@ 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;
|
||||||
|
};
|
||||||
|
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;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
email: string;
|
email: string;
|
||||||
password: string | undefined;
|
password?: string;
|
||||||
organization: string | undefined;
|
organization: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user