fix(login): refactor SAML Post Flow to bypass cookie limits, fix SAML SP post (#11170)

Closes #10816
Closes #10759

# Which Problems Are Solved

This PR refactors the SAML Post binding flow to address issues with
payloads exceeding browser cookie size limits (typically ~4KB).
Previously, the SAML form data was serialized into a cookie, and the
user was redirected to an intermediate /saml-post page to render the
form. This approach failed for large SAML assertions.

# How the Problems Are Solved

The new implementation bypasses cookies entirely for the payload
transfer. Server actions now return the form data directly to the
client, which immediately renders and submits the form.

- Direct Data Transfer: Updated `startIdentityProviderFlow` to return an
object { url, fields } instead of a redirect URL when the next step is
formData.
- Server Actions: Updated redirectToIdp, sendLoginname, and
completeAuthFlow to propagate this samlData object to the client.
- Client-Side Auto-Submission: Added a new AutoSubmitForm component that
renders a hidden form with the provided fields and automatically submits
it to the IdP on mount.

---------

Co-authored-by: conblem <[email protected]>
This commit is contained in:
Max Peintner
2026-02-09 13:46:30 +01:00
committed by GitHub
co-authored by conblem
parent 6f4f54708e
commit 9492c12c09
36 changed files with 891 additions and 1454 deletions
@@ -0,0 +1,29 @@
"use client";
import { useEffect, useRef } from "react";
type Props = {
url: string;
fields: Record<string, string>;
};
export function AutoSubmitForm({ url, fields }: Props) {
const formRef = useRef<HTMLFormElement>(null);
useEffect(() => {
if (formRef.current) {
formRef.current.submit();
}
}, []);
return (
<form ref={formRef} action={url} method="post">
{Object.entries(fields).map(([key, value]) => (
<input key={key} type="hidden" name={key} value={value} />
))}
<noscript>
<button type="submit">Continue</button>
</noscript>
</form>
);
}
@@ -16,6 +16,8 @@ import { TextInput } from "./input";
import { PasswordComplexity } from "./password-complexity";
import { Spinner } from "./spinner";
import { Translated } from "./translated";
import { handleServerActionResponse } from "@/lib/client";
import { AutoSubmitForm } from "./auto-submit-form";
type Inputs =
| {
@@ -47,6 +49,7 @@ export function ChangePasswordForm({ passwordComplexitySettings, sessionId, logi
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const [samlData, setSamlData] = useState<{ url: string; fields: Record<string, string> } | null>(null);
async function submitChange(values: Inputs) {
setLoading(true);
@@ -90,14 +93,7 @@ export function ChangePasswordForm({ passwordComplexitySettings, sessionId, logi
setLoading(false);
});
if (passwordResponse && "error" in passwordResponse && passwordResponse.error) {
setError(passwordResponse.error);
return;
}
if (passwordResponse && "redirect" in passwordResponse && passwordResponse.redirect) {
return router.push(passwordResponse.redirect);
}
handleServerActionResponse(passwordResponse as any, router, setSamlData, setError);
return;
}
@@ -122,7 +118,9 @@ export function ChangePasswordForm({ passwordComplexitySettings, sessionId, logi
hasMinLength;
return (
<form className="w-full">
<>
{samlData && <AutoSubmitForm url={samlData.url} fields={samlData.fields} />}
<form className="w-full">
<div className="mb-4 grid grid-cols-1 gap-4 pt-4">
<div className="">
<TextInput
@@ -174,6 +172,7 @@ export function ChangePasswordForm({ passwordComplexitySettings, sessionId, logi
{loading && <Spinner className="mr-2 h-5 w-5" />} <Translated i18nKey="change.submit" namespace="password" />
</Button>
</div>
</form>
</form>
</>
);
}
@@ -7,6 +7,8 @@ import { useRouter } from "next/navigation";
import { EMAIL, SMS, TOTP, U2F } from "./auth-methods";
import { Translated } from "./translated";
import { useState } from "react";
import { handleServerActionResponse } from "@/lib/client";
import { AutoSubmitForm } from "./auto-submit-form";
import { Alert } from "./alert";
type Props = {
@@ -40,6 +42,7 @@ export function ChooseSecondFactorToSetup({
const params = new URLSearchParams({});
const [error, setError] = useState<string>("");
const [samlData, setSamlData] = useState<{ url: string; fields: Record<string, string> } | null>(null);
if (loginName) {
params.append("loginName", loginName);
@@ -59,6 +62,7 @@ export function ChooseSecondFactorToSetup({
return (
<>
{samlData && <AutoSubmitForm url={samlData.url} fields={samlData.fields} />}
<div className="grid w-full grid-cols-1 gap-5 pt-4">
{loginSettings.secondFactors.map((factor) => {
switch (factor) {
@@ -89,15 +93,7 @@ export function ChooseSecondFactorToSetup({
requestId,
});
if (skipResponse && "error" in skipResponse && skipResponse.error) {
setError(skipResponse.error);
return;
}
// For regular flows (non-OIDC/SAML), return URL for client-side navigation
if ("redirect" in skipResponse && skipResponse.redirect) {
router.push(skipResponse.redirect);
}
handleServerActionResponse(skipResponse, router, setSamlData, setError);
}}
type="button"
data-testid="reset-button"
@@ -1,6 +1,7 @@
"use client";
import { processIDPCallback } from "@/lib/server/idp-intent";
import { AutoSubmitForm } from "./auto-submit-form";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";
@@ -37,6 +38,7 @@ export function IdpProcessHandler({
const t = useTranslations("idp");
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [samlData, setSamlData] = useState<{ url: string; fields: Record<string, string> } | null>(null);
const executedRef = useRef(false);
const router = useRouter();
@@ -74,6 +76,13 @@ export function IdpProcessHandler({
return;
}
if (result.samlData) {
console.log("[IDP Process Handler] Received samlData, rendering AutoSubmitForm");
setSamlData(result.samlData);
setLoading(false);
return;
}
setError(t("processing.noRedirect"));
setLoading(false);
})
@@ -86,6 +95,7 @@ export function IdpProcessHandler({
return (
<div className="flex min-h-screen items-center justify-center">
{samlData && <AutoSubmitForm url={samlData.url} fields={samlData.fields} />}
{loading && (
<div className="flex flex-col items-center space-y-4">
<Spinner className="h-8 w-8" />
+98 -105
View File
@@ -1,13 +1,13 @@
"use client";
import { completeFlowOrGetUrl } from "@/lib/client";
import { handleServerActionResponse, completeFlowOrGetUrl } from "@/lib/client";
import { updateOrCreateSession } from "@/lib/server/session";
import { create } from "@zitadel/client";
import { RequestChallengesSchema } from "@zitadel/proto/zitadel/session/v2/challenge_pb";
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useForm } from "react-hook-form";
import { Alert, AlertType } from "./alert";
import { BackButton } from "./back-button";
@@ -16,6 +16,7 @@ import { Button, ButtonVariants } from "./button";
import { TextInput } from "./input";
import { Spinner } from "./spinner";
import { Translated } from "./translated";
import { AutoSubmitForm } from "./auto-submit-form";
// either loginName or sessionId must be provided
type Props = {
@@ -38,6 +39,7 @@ export function LoginOTP({ host, loginName, sessionId, requestId, organization,
const [error, setError] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const [samlData, setSamlData] = useState<{ url: string; fields: Record<string, string> } | null>(null);
const router = useRouter();
@@ -50,22 +52,10 @@ export function LoginOTP({ host, loginName, sessionId, requestId, organization,
},
});
useEffect(() => {
if (!initialized.current && ["email", "sms"].includes(method) && !code) {
initialized.current = true;
setLoading(true);
updateSessionForOTPChallenge()
.catch((error) => {
setError(error);
return;
})
.finally(() => {
setLoading(false);
});
}
}, []);
async function updateSessionForOTPChallenge() {
const updateSessionForOTPChallenge = useCallback(async (): Promise<{
error?: string;
[key: string]: any;
}> => {
let challenges;
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
@@ -93,29 +83,41 @@ export function LoginOTP({ host, loginName, sessionId, requestId, organization,
});
}
setLoading(true);
const response = await updateOrCreateSession({
loginName,
sessionId,
organization,
challenges,
requestId,
})
.catch(() => {
setError("Could not request OTP challenge");
return;
})
.finally(() => {
setLoading(false);
let response;
try {
response = await updateOrCreateSession({
loginName,
sessionId,
organization,
challenges,
requestId,
});
} catch {
return { error: "Could not request OTP challenge" };
}
if (response && "error" in response && response.error) {
setError(response.error);
return;
return { error: response.error };
}
return response;
}
}, [method, host, requestId, loginName, sessionId, organization]);
useEffect(() => {
if (!initialized.current && ["email", "sms"].includes(method) && !code) {
initialized.current = true;
setLoading(true);
updateSessionForOTPChallenge()
.then((response) => {
if (response?.error) {
setError(response.error);
}
})
.finally(() => {
setLoading(false);
});
}
}, [updateSessionForOTPChallenge, method, code]);
async function submitCode(values: Inputs, organization?: string) {
setLoading(true);
@@ -174,7 +176,7 @@ export function LoginOTP({ host, loginName, sessionId, requestId, organization,
return response;
}
function setCodeAndContinue(values: Inputs, organization?: string) {
function setCodeAndContinue(values: Inputs) {
return submitCode(values, organization).then(async (response) => {
if (response && "sessionId" in response) {
setLoading(true);
@@ -198,14 +200,7 @@ export function LoginOTP({ host, loginName, sessionId, requestId, organization,
);
setLoading(false);
if ("error" in callbackResponse) {
setError(callbackResponse.error);
return;
}
if ("redirect" in callbackResponse) {
return router.push(callbackResponse.redirect);
}
handleServerActionResponse(callbackResponse, router, setSamlData, setError);
} else {
setLoading(false);
}
@@ -214,68 +209,66 @@ export function LoginOTP({ host, loginName, sessionId, requestId, organization,
}
return (
<form className="w-full">
{["email", "sms"].includes(method) && (
<Alert type={AlertType.INFO}>
<div className="flex flex-row">
<span className="mr-auto flex-1 text-left">
<Translated i18nKey="verify.noCodeReceived" namespace="otp" />
</span>
<button
aria-label="Resend OTP Code"
disabled={loading}
type="button"
className="ml-4 cursor-pointer text-primary-light-500 hover:text-primary-light-400 disabled:cursor-default disabled:text-gray-400 dark:text-primary-dark-500 hover:dark:text-primary-dark-400 dark:disabled:text-gray-700"
onClick={() => {
setLoading(true);
updateSessionForOTPChallenge()
.catch((error) => {
setError(error);
return;
})
.finally(() => {
setLoading(false);
});
}}
data-testid="resend-button"
>
<Translated i18nKey="verify.resendCode" namespace="otp" />
</button>
</div>
</Alert>
)}
<div className="mt-4">
<TextInput
type="text"
{...register("code", { required: t("verify.required.code") })}
label={t("verify.labels.code")}
autoComplete="one-time-code"
data-testid="code-text-input"
/>
</div>
{error && (
<div className="py-4" data-testid="error">
<Alert>{error}</Alert>
<>
{samlData && <AutoSubmitForm url={samlData.url} fields={samlData.fields} />}
<form className="w-full">
{["email", "sms"].includes(method) && (
<Alert type={AlertType.INFO}>
<div className="flex flex-row">
<span className="mr-auto flex-1 text-left">
<Translated i18nKey="verify.noCodeReceived" namespace="otp" />
</span>
<button
aria-label="Resend OTP Code"
disabled={loading}
type="button"
className="ml-4 cursor-pointer text-primary-light-500 hover:text-primary-light-400 disabled:cursor-default disabled:text-gray-400 dark:text-primary-dark-500 hover:dark:text-primary-dark-400 dark:disabled:text-gray-700"
onClick={async () => {
setLoading(true);
const response = await updateSessionForOTPChallenge();
if (response?.error) {
setError(response.error);
}
setLoading(false);
}}
data-testid="resend-button"
>
<Translated i18nKey="verify.resendCode" namespace="otp" />
</button>
</div>
</Alert>
)}
<div className="mt-4">
<TextInput
type="text"
{...register("code", { required: t("verify.required.code") })}
label={t("verify.labels.code")}
autoComplete="one-time-code"
data-testid="code-text-input"
/>
</div>
)}
<div className="mt-8 flex w-full flex-row items-center">
<BackButton data-testid="back-button" />
<span className="flex-grow"></span>
<Button
type="submit"
className="self-end"
variant={ButtonVariants.Primary}
disabled={loading || !formState.isValid}
onClick={handleSubmit((e) => {
setCodeAndContinue(e, organization);
})}
data-testid="submit-button"
>
{loading && <Spinner className="mr-2 h-5 w-5" />} <Translated i18nKey="verify.submit" namespace="otp" />
</Button>
</div>
</form>
{error && (
<div className="py-4" data-testid="error">
<Alert>{error}</Alert>
</div>
)}
<div className="mt-8 flex w-full flex-row items-center">
<BackButton data-testid="back-button" />
<span className="flex-grow"></span>
<Button
type="submit"
className="self-end"
variant={ButtonVariants.Primary}
disabled={loading || !formState.isValid}
onClick={handleSubmit(setCodeAndContinue)}
data-testid="submit-button"
>
{loading && <Spinner className="mr-2 h-5 w-5" />} <Translated i18nKey="verify.submit" namespace="otp" />
</Button>
</div>
</form>
</>
);
}
+25 -28
View File
@@ -9,11 +9,13 @@ import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { useTranslations } from "next-intl";
import { handleServerActionResponse } from "@/lib/client";
import { Alert } from "./alert";
import { BackButton } from "./back-button";
import { Button, ButtonVariants } from "./button";
import { Spinner } from "./spinner";
import { Translated } from "./translated";
import { AutoSubmitForm } from "./auto-submit-form";
// either loginName or sessionId must be provided
type Props = {
@@ -28,6 +30,7 @@ type Props = {
export function LoginPasskey({ loginName, sessionId, requestId, altPassword, organization, login = true }: Props) {
const [error, setError] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const [samlData, setSamlData] = useState<{ url: string; fields: Record<string, string> } | null>(null);
const t = useTranslations("passkey");
const router = useRouter();
@@ -106,37 +109,30 @@ export function LoginPasskey({ loginName, sessionId, requestId, altPassword, org
async function submitLogin(data: JsonObject) {
setLoading(true);
const response = await sendPasskey({
loginName,
sessionId,
organization,
checks: {
webAuthN: { credentialAssertionData: data },
} as Checks,
requestId,
})
.catch(() => {
setError(t("verify.errors.couldNotVerifyPasskey"));
return;
})
.finally(() => {
setLoading(false);
try {
const response = await sendPasskey({
loginName,
sessionId,
organization,
checks: {
webAuthN: { credentialAssertionData: data },
} as Checks,
requestId,
});
if (response && "error" in response && response.error) {
setError(response.error);
return;
}
const handled = handleServerActionResponse(response, router, setSamlData, setError);
if (response && "redirect" in response && response.redirect) {
return router.push(response.redirect);
}
// If we got here, something went wrong - no redirect or error was returned
if (!response) {
setError(t("verify.errors.noResponseReceived"));
} else {
setError(t("verify.errors.noRedirectProvided"));
if (!handled) {
if (!response) {
setError(t("verify.errors.noResponseReceived"));
} else {
setError(t("verify.errors.noRedirectProvided"));
}
}
} catch {
setError(t("verify.errors.couldNotVerifyPasskey"));
} finally {
setLoading(false);
}
}
@@ -191,6 +187,7 @@ export function LoginPasskey({ loginName, sessionId, requestId, altPassword, org
return (
<div className="w-full">
{samlData && <AutoSubmitForm url={samlData.url} fields={samlData.fields} />}
{error && (
<div className="py-4">
<Alert>{error}</Alert>
+67 -69
View File
@@ -14,6 +14,8 @@ import { Button, ButtonVariants } from "./button";
import { TextInput } from "./input";
import { Spinner } from "./spinner";
import { Translated } from "./translated";
import { handleServerActionResponse } from "@/lib/client";
import { AutoSubmitForm } from "./auto-submit-form";
type Inputs = {
password: string;
@@ -36,6 +38,7 @@ export function PasswordForm({ loginSettings, loginName, organization, defaultOr
const [info, setInfo] = useState<string>("");
const [error, setError] = useState<string>("");
const [samlData, setSamlData] = useState<{ url: string; fields: Record<string, string> } | null>(null);
const [loading, setLoading] = useState<boolean>(false);
@@ -45,30 +48,22 @@ export function PasswordForm({ loginSettings, loginName, organization, defaultOr
setError("");
setLoading(true);
const response = await sendPassword({
loginName,
organization,
defaultOrganization,
checks: create(ChecksSchema, {
password: { password: values.password },
}),
requestId,
})
.catch(() => {
setError(t("verify.errors.couldNotVerifyPassword"));
return;
})
.finally(() => {
setLoading(false);
try {
const response = await sendPassword({
loginName,
organization,
defaultOrganization,
checks: create(ChecksSchema, {
password: { password: values.password },
}),
requestId,
});
if (response && "error" in response && response.error) {
setError(response.error);
return;
}
if (response && "redirect" in response && response.redirect) {
return router.push(response.redirect);
handleServerActionResponse(response, router, setSamlData, setError);
} catch {
setError(t("verify.errors.couldNotVerifyPassword"));
} finally {
setLoading(false);
}
}
@@ -114,56 +109,59 @@ export function PasswordForm({ loginSettings, loginName, organization, defaultOr
}
return (
<form className="w-full">
<div className={`${error && "transform-gpu animate-shake"}`}>
<TextInput
type="password"
autoComplete="password"
{...register("password", { required: t("verify.required.password") })}
label={t("verify.labels.password")}
data-testid="password-text-input"
/>
{!loginSettings?.hidePasswordReset && (
<button
className="text-sm transition-all hover:text-primary-light-500 dark:hover:text-primary-dark-500"
onClick={() => resetPasswordAndContinue()}
type="button"
disabled={loading}
data-testid="reset-button"
>
<Translated i18nKey="verify.resetPassword" namespace="password" />
</button>
<>
{samlData && <AutoSubmitForm url={samlData.url} fields={samlData.fields} />}
<form className="w-full">
<div className={`${error && "transform-gpu animate-shake"}`}>
<TextInput
type="password"
autoComplete="password"
{...register("password", { required: t("verify.required.password") })}
label={t("verify.labels.password")}
data-testid="password-text-input"
/>
{!loginSettings?.hidePasswordReset && (
<button
className="text-sm transition-all hover:text-primary-light-500 dark:hover:text-primary-dark-500"
onClick={() => resetPasswordAndContinue()}
type="button"
disabled={loading}
data-testid="reset-button"
>
<Translated i18nKey="verify.resetPassword" namespace="password" />
</button>
)}
{loginName && <input type="hidden" name="loginName" autoComplete="username" value={loginName} />}
</div>
{info && (
<div className="py-4">
<Alert type={AlertType.INFO}>{info}</Alert>
</div>
)}
{loginName && <input type="hidden" name="loginName" autoComplete="username" value={loginName} />}
</div>
{error && (
<div className="py-4" data-testid="error">
<Alert>{error}</Alert>
</div>
)}
{info && (
<div className="py-4">
<Alert type={AlertType.INFO}>{info}</Alert>
<div className="mt-8 flex w-full flex-row items-center">
<BackButton data-testid="back-button" />
<span className="flex-grow"></span>
<Button
type="submit"
className="self-end"
variant={ButtonVariants.Primary}
disabled={loading || !formState.isValid}
onClick={handleSubmit(submitPassword)}
data-testid="submit-button"
>
{loading && <Spinner className="mr-2 h-5 w-5" />} <Translated i18nKey="verify.submit" namespace="password" />
</Button>
</div>
)}
{error && (
<div className="py-4" data-testid="error">
<Alert>{error}</Alert>
</div>
)}
<div className="mt-8 flex w-full flex-row items-center">
<BackButton data-testid="back-button" />
<span className="flex-grow"></span>
<Button
type="submit"
className="self-end"
variant={ButtonVariants.Primary}
disabled={loading || !formState.isValid}
onClick={handleSubmit(submitPassword)}
data-testid="submit-button"
>
{loading && <Spinner className="mr-2 h-5 w-5" />} <Translated i18nKey="verify.submit" namespace="password" />
</Button>
</div>
</form>
</form>
</>
);
}
@@ -1,6 +1,7 @@
"use client";
import { registerUserAndLinkToIDP } from "@/lib/server/register";
import { handleServerActionResponse } from "@/lib/client";
import { useState } from "react";
import { useTranslations } from "next-intl";
import { FieldValues, useForm } from "react-hook-form";
@@ -11,12 +12,14 @@ import { Button, ButtonVariants } from "./button";
import { TextInput } from "./input";
import { Spinner } from "./spinner";
import { Translated } from "./translated";
import { AutoSubmitForm } from "./auto-submit-form";
type Inputs =
| {
firstname: string;
lastname: string;
email: string;
username?: string;
}
| FieldValues;
@@ -34,7 +37,7 @@ type Props = {
};
idpUserId: string;
idpId: string;
idpUserName: string;
idpUserName?: string;
};
export function RegisterFormIDPIncomplete({
@@ -60,96 +63,107 @@ export function RegisterFormIDPIncomplete({
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const [samlData, setSamlData] = useState<{ url: string; fields: Record<string, string> } | null>(null);
async function submitAndRegister(values: Inputs) {
setLoading(true);
const response = await registerUserAndLinkToIDP({
idpId: idpId,
idpUserName: idpUserName,
idpUserId: idpUserId,
email: values.email,
firstName: values.firstname,
lastName: values.lastname,
organization: organization,
requestId: requestId,
idpIntent: idpIntent,
})
.catch(() => {
setError("Could not register user");
return;
})
.finally(() => {
setLoading(false);
try {
const response = await registerUserAndLinkToIDP({
idpId: idpId,
idpUserName: idpUserName ? idpUserName : values.username,
idpUserId: idpUserId,
email: values.email,
firstName: values.firstname,
lastName: values.lastname,
organization: organization,
requestId: requestId,
idpIntent: idpIntent,
});
if (response && "error" in response && response.error) {
setError(response.error);
return;
}
if (response && "redirect" in response && response.redirect) {
return router.push(response.redirect);
handleServerActionResponse(response, router, setSamlData, setError);
} catch {
setError("Could not register user");
} finally {
setLoading(false);
}
}
const { errors } = formState;
return (
<form className="w-full">
<div className="mb-4 grid grid-cols-2 gap-4">
<div className="">
<TextInput
type="firstname"
autoComplete="firstname"
required
{...register("firstname", { required: t("required.firstname") })}
label={t("labels.firstname")}
error={errors.firstname?.message as string}
data-testid="firstname-text-input"
/>
<>
{samlData && <AutoSubmitForm url={samlData.url} fields={samlData.fields} />}
<form className="w-full">
<div className="mb-4 grid grid-cols-1 gap-4">
{!idpUserName && (
<div className="">
<TextInput
type="text"
autoComplete="username"
required
{...register("username", { required: "Username is required" })}
label="Username"
error={errors.username?.message as string}
data-testid="username-text-input"
/>
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div className="">
<TextInput
type="firstname"
autoComplete="firstname"
required
{...register("firstname", { required: t("required.firstname") })}
label={t("labels.firstname")}
error={errors.firstname?.message as string}
data-testid="firstname-text-input"
/>
</div>
<div className="">
<TextInput
type="lastname"
autoComplete="lastname"
required
{...register("lastname", { required: t("required.lastname") })}
label={t("labels.lastname")}
error={errors.lastname?.message as string}
data-testid="lastname-text-input"
/>
</div>
</div>
<div className="">
<TextInput
type="email"
autoComplete="email"
required
{...register("email", { required: t("required.email") })}
label={t("labels.email")}
error={errors.email?.message as string}
data-testid="email-text-input"
/>
</div>
</div>
<div className="">
<TextInput
type="lastname"
autoComplete="lastname"
required
{...register("lastname", { required: t("required.lastname") })}
label={t("labels.lastname")}
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: t("required.email") })}
label={t("labels.email")}
error={errors.email?.message as string}
data-testid="email-text-input"
/>
</div>
</div>
{error && (
<div className="py-4">
<Alert>{error}</Alert>
</div>
)}
{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}
onClick={handleSubmit(submitAndRegister)}
data-testid="submit-button"
>
{loading && <Spinner className="mr-2 h-5 w-5" />} <Translated i18nKey="submit" namespace="register" />
</Button>
</div>
</form>
<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}
onClick={handleSubmit(submitAndRegister)}
data-testid="submit-button"
>
{loading && <Spinner className="mr-2 h-5 w-5" />} <Translated i18nKey="submit" namespace="register" />
</Button>
</div>
</form>
</>
);
}
+101 -103
View File
@@ -1,6 +1,7 @@
"use client";
import { registerUser } from "@/lib/server/register";
import { handleServerActionResponse } from "@/lib/client";
import { LegalAndSupportSettings } from "@zitadel/proto/zitadel/settings/v2/legal_settings_pb";
import { LoginSettings, PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { useRouter } from "next/navigation";
@@ -15,6 +16,7 @@ import { TextInput } from "./input";
import { PrivacyPolicyCheckboxes } from "./privacy-policy-checkboxes";
import { Spinner } from "./spinner";
import { Translated } from "./translated";
import { AutoSubmitForm } from "./auto-submit-form";
type Inputs =
| {
@@ -59,36 +61,29 @@ export function RegisterForm({
const [loading, setLoading] = useState<boolean>(false);
const [selected, setSelected] = useState<AuthenticationMethod>(methods[0]);
const [error, setError] = useState<string>("");
const [samlData, setSamlData] = useState<{ url: string; fields: Record<string, string> } | null>(null);
const router = useRouter();
async function submitAndRegister(values: Inputs) {
setLoading(true);
const response = await registerUser({
email: values.email,
firstName: values.firstname,
lastName: values.lastname,
organization: organization,
requestId: requestId,
})
.catch(() => {
setError(t("errors.couldNotRegisterUser"));
return;
})
.finally(() => {
setLoading(false);
try {
const response = await registerUser({
email: values.email,
firstName: values.firstname,
lastName: values.lastname,
organization: organization,
requestId: requestId,
});
if (response && "error" in response && response.error) {
setError(response.error);
return;
}
handleServerActionResponse(response, router, setSamlData, setError);
if (response && "redirect" in response && response.redirect) {
return router.push(response.redirect);
return response;
} catch {
setError(t("errors.couldNotRegisterUser"));
} finally {
setLoading(false);
}
return response;
}
async function submitAndContinue(value: Inputs, withPassword: boolean = false) {
@@ -119,93 +114,96 @@ export function RegisterForm({
const canSubmit = formState.isValid && (!isLegalAcceptanceRequired || tosAndPolicyAccepted);
return (
<form className="w-full">
<div className="mb-4 grid grid-cols-2 gap-4">
<div className="">
<TextInput
type="firstname"
autoComplete="firstname"
required
{...register("firstname", { required: t("required.firstname") })}
label={t("labels.firstname")}
error={errors.firstname?.message as string}
data-testid="firstname-text-input"
/>
</div>
<div className="">
<TextInput
type="lastname"
autoComplete="lastname"
required
{...register("lastname", { required: t("required.lastname") })}
label={t("labels.lastname")}
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: t("required.email") })}
label={t("labels.email")}
error={errors.email?.message as string}
data-testid="email-text-input"
/>
</div>
</div>
{(legal?.tosLink || legal?.privacyPolicyLink) && (
<PrivacyPolicyCheckboxes legal={legal} onChange={setTosAndPolicyAccepted} />
)}
{/* show chooser if both methods are allowed */}
{loginSettings && loginSettings.allowLocalAuthentication && loginSettings.passkeysType == PasskeysType.ALLOWED && (
<>
<p className="ztdl-p mb-6 mt-4 block text-left">
<Translated i18nKey="selectMethod" namespace="register" />
</p>
<div className="pb-4">
<AuthenticationMethodRadio selected={selected} selectionChanged={setSelected} />
<>
{samlData && <AutoSubmitForm url={samlData.url} fields={samlData.fields} />}
<form className="w-full">
<div className="mb-4 grid grid-cols-2 gap-4">
<div className="">
<TextInput
type="firstname"
autoComplete="firstname"
required
{...register("firstname", { required: t("required.firstname") })}
label={t("labels.firstname")}
error={errors.firstname?.message as string}
data-testid="firstname-text-input"
/>
</div>
</>
)}
{!loginSettings?.allowLocalAuthentication &&
loginSettings?.passkeysType !== PasskeysType.ALLOWED &&
(!loginSettings?.allowExternalIdp || !idpCount) && (
<div className="">
<TextInput
type="lastname"
autoComplete="lastname"
required
{...register("lastname", { required: t("required.lastname") })}
label={t("labels.lastname")}
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: t("required.email") })}
label={t("labels.email")}
error={errors.email?.message as string}
data-testid="email-text-input"
/>
</div>
</div>
{(legal?.tosLink || legal?.privacyPolicyLink) && (
<PrivacyPolicyCheckboxes legal={legal} onChange={setTosAndPolicyAccepted} />
)}
{/* show chooser if both methods are allowed */}
{loginSettings && loginSettings.allowLocalAuthentication && loginSettings.passkeysType == PasskeysType.ALLOWED && (
<>
<p className="ztdl-p mb-6 mt-4 block text-left">
<Translated i18nKey="selectMethod" namespace="register" />
</p>
<div className="pb-4">
<AuthenticationMethodRadio selected={selected} selectionChanged={setSelected} />
</div>
</>
)}
{!loginSettings?.allowLocalAuthentication &&
loginSettings?.passkeysType !== PasskeysType.ALLOWED &&
(!loginSettings?.allowExternalIdp || !idpCount) && (
<div className="py-4">
<Alert type={AlertType.INFO}>
<Translated i18nKey="noMethodAvailableWarning" namespace="register" />
</Alert>
</div>
)}
{error && (
<div className="py-4">
<Alert type={AlertType.INFO}>
<Translated i18nKey="noMethodAvailableWarning" namespace="register" />
</Alert>
<Alert>{error}</Alert>
</div>
)}
{error && (
<div className="py-4">
<Alert>{error}</Alert>
<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 || !canSubmit}
onClick={handleSubmit((values) => {
const usePasswordToContinue: boolean =
loginSettings?.allowLocalAuthentication && loginSettings?.passkeysType == PasskeysType.ALLOWED
? !(selected === methods[0]) // choose selection if both available
: !!loginSettings?.allowLocalAuthentication; // if password is chosen
// set password as default if only password is allowed
return submitAndContinue(values, usePasswordToContinue);
})}
data-testid="submit-button"
>
{loading && <Spinner className="mr-2 h-5 w-5" />}
<Translated i18nKey="submit" namespace="register" />
</Button>
</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 || !canSubmit}
onClick={handleSubmit((values) => {
const usePasswordToContinue: boolean =
loginSettings?.allowLocalAuthentication && loginSettings?.passkeysType == PasskeysType.ALLOWED
? !(selected === methods[0]) // choose selection if both available
: !!loginSettings?.allowLocalAuthentication; // if password is chosen
// set password as default if only password is allowed
return submitAndContinue(values, usePasswordToContinue);
})}
data-testid="submit-button"
>
{loading && <Spinner className="mr-2 h-5 w-5" />}
<Translated i18nKey="submit" namespace="register" />
</Button>
</div>
</form>
</form>
</>
);
}
+30 -39
View File
@@ -1,7 +1,7 @@
"use client";
import { coerceToArrayBuffer, coerceToBase64Url } from "@/helpers/base64";
import { completeFlowOrGetUrl } from "@/lib/client";
import { completeFlowOrGetUrl, handleServerActionResponse } from "@/lib/client";
import { addU2F, verifyU2F } from "@/lib/server/u2f";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { RegisterU2FResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
@@ -12,6 +12,7 @@ import { BackButton } from "./back-button";
import { Button, ButtonVariants } from "./button";
import { Spinner } from "./spinner";
import { Translated } from "./translated";
import { AutoSubmitForm } from "./auto-submit-form";
type Props = {
loginName?: string;
@@ -24,6 +25,7 @@ type Props = {
export function RegisterU2f({ loginName, sessionId, organization, requestId, checkAfter, loginSettings }: Props) {
const [error, setError] = useState<string>("");
const [samlData, setSamlData] = useState<{ url: string; fields: Record<string, string> } | null>(null);
const [loading, setLoading] = useState<boolean>(false);
@@ -155,14 +157,7 @@ export function RegisterU2f({ loginName, sessionId, organization, requestId, che
loginSettings?.defaultRedirectUri,
);
if ("error" in callbackResponse) {
setError(callbackResponse.error);
return;
}
if ("redirect" in callbackResponse) {
return router.push(callbackResponse.redirect);
}
handleServerActionResponse(callbackResponse, router, setSamlData, setError);
} else if (loginName) {
const callbackResponse = await completeFlowOrGetUrl(
{
@@ -172,42 +167,38 @@ export function RegisterU2f({ loginName, sessionId, organization, requestId, che
loginSettings?.defaultRedirectUri,
);
if ("error" in callbackResponse) {
setError(callbackResponse.error);
return;
}
if ("redirect" in callbackResponse) {
return router.push(callbackResponse.redirect);
}
handleServerActionResponse(callbackResponse, router, setSamlData, setError);
}
}
}
}
return (
<form className="w-full">
{error && (
<div className="py-4">
<Alert>{error}</Alert>
<>
{samlData && <AutoSubmitForm url={samlData.url} fields={samlData.fields} />}
<form className="w-full">
{error && (
<div className="py-4">
<Alert>{error}</Alert>
</div>
)}
<div className="mt-8 flex w-full flex-row items-center">
<BackButton data-testid="back-button" />
<span className="flex-grow"></span>
<Button
type="submit"
className="self-end"
variant={ButtonVariants.Primary}
disabled={loading}
onClick={submitRegisterAndContinue}
data-testid="submit-button"
>
{loading && <Spinner className="mr-2 h-5 w-5" />} <Translated i18nKey="set.submit" namespace="u2f" />
</Button>
</div>
)}
<div className="mt-8 flex w-full flex-row items-center">
<BackButton data-testid="back-button" />
<span className="flex-grow"></span>
<Button
type="submit"
className="self-end"
variant={ButtonVariants.Primary}
disabled={loading}
onClick={submitRegisterAndContinue}
data-testid="submit-button"
>
{loading && <Spinner className="mr-2 h-5 w-5" />} <Translated i18nKey="set.submit" namespace="u2f" />
</Button>
</div>
</form>
</form>
</>
);
}
+15 -26
View File
@@ -12,6 +12,8 @@ import { useRouter } from "next/navigation";
import React, { useState } from "react";
import { Avatar } from "./avatar";
import { Translated } from "./translated";
import { handleServerActionResponse } from "@/lib/client";
import { AutoSubmitForm } from "./auto-submit-form";
export function isSessionValid(session: Partial<Session>): {
valid: boolean;
@@ -52,6 +54,7 @@ export function SessionItem({ session, reload, requestId }: { session: Session;
}
const { valid, verifiedAt } = isSessionValid(session);
const [samlData, setSamlData] = useState<{ url: string; fields: Record<string, string> } | null>(null);
const [_error, setError] = useState<string | null>(null);
@@ -59,6 +62,7 @@ export function SessionItem({ session, reload, requestId }: { session: Session;
return (
<Tooltip.Root delayDuration={300}>
{samlData && <AutoSubmitForm url={samlData.url} fields={samlData.fields} />}
<Tooltip.Trigger asChild>
<button
onClick={async () => {
@@ -70,36 +74,21 @@ export function SessionItem({ session, reload, requestId }: { session: Session;
const callbackResponse = await continueWithSession(sessionPayload);
if (callbackResponse && "error" in callbackResponse) {
setError(callbackResponse.error);
return;
}
if (callbackResponse && "redirect" in callbackResponse) {
return router.push(callbackResponse.redirect);
}
handleServerActionResponse(callbackResponse, router, setSamlData, (e) => setError(e));
} else if (session.factors?.user) {
setLoading(true);
const res = await sendLoginname({
loginName: session.factors?.user?.loginName,
organization: session.factors.user.organizationId,
requestId: requestId,
})
.catch(() => {
setError("An internal error occurred");
return;
})
.finally(() => {
setLoading(false);
try {
const res = await sendLoginname({
loginName: session.factors?.user?.loginName,
organization: session.factors.user.organizationId,
requestId: requestId,
});
if (res && "redirect" in res && res.redirect) {
return router.push(res.redirect);
}
if (res && "error" in res && res.error) {
setError(res.error);
return;
handleServerActionResponse(res, router, setSamlData, (e) => setError(e));
} catch {
setError("An internal error occurred");
} finally {
setLoading(false);
}
}
}}
@@ -16,6 +16,8 @@ import { TextInput } from "./input";
import { PasswordComplexity } from "./password-complexity";
import { Spinner } from "./spinner";
import { Translated } from "./translated";
import { handleServerActionResponse } from "@/lib/client";
import { AutoSubmitForm } from "./auto-submit-form";
type Inputs =
| {
@@ -57,6 +59,7 @@ export function SetPasswordForm({
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const [samlData, setSamlData] = useState<{ url: string; fields: Record<string, string> } | null>(null);
const router = useRouter();
@@ -144,14 +147,7 @@ export function SetPasswordForm({
setLoading(false);
});
if (passwordResponse && "error" in passwordResponse && passwordResponse.error) {
setError(passwordResponse.error);
return;
}
if (passwordResponse && "redirect" in passwordResponse && passwordResponse.redirect) {
return router.push(passwordResponse.redirect);
}
handleServerActionResponse(passwordResponse as any, router, setSamlData, setError);
return;
}
@@ -176,7 +172,9 @@ export function SetPasswordForm({
hasMinLength;
return (
<form className="w-full">
<>
{samlData && <AutoSubmitForm url={samlData.url} fields={samlData.fields} />}
<form className="w-full">
<div className="mb-4 grid grid-cols-1 gap-4 pt-4">
{codeRequired && (
<Alert type={AlertType.INFO}>
@@ -264,6 +262,7 @@ export function SetPasswordForm({
{loading && <Spinner className="mr-2 h-5 w-5" />} <Translated i18nKey="set.submit" namespace="password" />
</Button>
</div>
</form>
</form>
</>
);
}
@@ -2,6 +2,7 @@
import { lowerCaseValidator, numberValidator, symbolValidator, upperCaseValidator } from "@/helpers/validators";
import { registerUser } from "@/lib/server/register";
import { handleServerActionResponse } from "@/lib/client";
import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
import { useRouter } from "next/navigation";
import { useState } from "react";
@@ -14,6 +15,7 @@ import { TextInput } from "./input";
import { PasswordComplexity } from "./password-complexity";
import { Spinner } from "./spinner";
import { Translated } from "./translated";
import { AutoSubmitForm } from "./auto-submit-form";
type Inputs =
| {
@@ -52,34 +54,27 @@ export function SetRegisterPasswordForm({
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const [samlData, setSamlData] = useState<{ url: string; fields: Record<string, string> } | null>(null);
const router = useRouter();
async function submitRegister(values: Inputs) {
setLoading(true);
const response = await registerUser({
email: email,
firstName: firstname,
lastName: lastname,
organization: organization,
requestId: requestId,
password: values.password,
})
.catch(() => {
setError(t("errors.couldNotRegisterUser"));
return;
})
.finally(() => {
setLoading(false);
try {
const response = await registerUser({
email: email,
firstName: firstname,
lastName: lastname,
organization: organization,
requestId: requestId,
password: values.password,
});
if (response && "error" in response && response.error) {
setError(response.error);
return;
}
if (response && "redirect" in response && response.redirect) {
return router.push(response.redirect);
handleServerActionResponse(response, router, setSamlData, setError);
} catch {
setError(t("errors.couldNotRegisterUser"));
} finally {
setLoading(false);
}
}
@@ -103,58 +98,61 @@ export function SetRegisterPasswordForm({
hasMinLength;
return (
<form className="w-full">
<div className="mb-4 grid grid-cols-1 gap-4 pt-4">
<div className="">
<TextInput
type="password"
autoComplete="new-password"
required
{...register("password", {
required: t("password.required.password"),
})}
label={t("password.labels.password")}
error={errors.password?.message as string}
data-testid="password-text-input"
/>
<>
{samlData && <AutoSubmitForm url={samlData.url} fields={samlData.fields} />}
<form className="w-full">
<div className="mb-4 grid grid-cols-1 gap-4 pt-4">
<div className="">
<TextInput
type="password"
autoComplete="new-password"
required
{...register("password", {
required: t("password.required.password"),
})}
label={t("password.labels.password")}
error={errors.password?.message as string}
data-testid="password-text-input"
/>
</div>
<div className="">
<TextInput
type="password"
required
autoComplete="new-password"
{...register("confirmPassword", {
required: t("password.required.confirmPassword"),
})}
label={t("password.labels.confirmPassword")}
error={errors.confirmPassword?.message as string}
data-testid="password-confirm-text-input"
/>
</div>
</div>
<div className="">
<TextInput
type="password"
required
autoComplete="new-password"
{...register("confirmPassword", {
required: t("password.required.confirmPassword"),
})}
label={t("password.labels.confirmPassword")}
error={errors.confirmPassword?.message as string}
data-testid="password-confirm-text-input"
{passwordComplexitySettings && (
<PasswordComplexity
passwordComplexitySettings={passwordComplexitySettings}
password={watchPassword}
equals={!!watchPassword && watchPassword === watchConfirmPassword}
/>
)}
{error && <Alert>{error}</Alert>}
<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 || !policyIsValid || !formState.isValid || watchPassword !== watchConfirmPassword}
onClick={handleSubmit(submitRegister)}
data-testid="submit-button"
>
{loading && <Spinner className="mr-2 h-5 w-5" />} <Translated i18nKey="password.submit" namespace="register" />
</Button>
</div>
</div>
{passwordComplexitySettings && (
<PasswordComplexity
passwordComplexitySettings={passwordComplexitySettings}
password={watchPassword}
equals={!!watchPassword && watchPassword === watchConfirmPassword}
/>
)}
{error && <Alert>{error}</Alert>}
<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 || !policyIsValid || !formState.isValid || watchPassword !== watchConfirmPassword}
onClick={handleSubmit(submitRegister)}
data-testid="submit-button"
>
{loading && <Spinner className="mr-2 h-5 w-5" />} <Translated i18nKey="password.submit" namespace="register" />
</Button>
</div>
</form>
</form>
</>
);
}
@@ -13,6 +13,7 @@ import { SignInWithGithub } from "./idps/sign-in-with-github";
import { SignInWithGitlab } from "./idps/sign-in-with-gitlab";
import { SignInWithGoogle } from "./idps/sign-in-with-google";
import { Translated } from "./translated";
import { AutoSubmitForm } from "./auto-submit-form";
export interface SignInWithIDPProps {
children?: ReactNode;
@@ -68,6 +69,7 @@ export function SignInWithIdp({
return (
<div className="flex w-full flex-col space-y-2 text-sm">
{state?.samlData && <AutoSubmitForm url={state.samlData.url} fields={state.samlData.fields} />}
{showLabel && (
<p className="ztdl-p text-center">
<Translated i18nKey="orSignInWith" namespace="idp" />
+6 -17
View File
@@ -1,6 +1,6 @@
"use client";
import { completeFlowOrGetUrl } from "@/lib/client";
import { completeFlowOrGetUrl, handleServerActionResponse } from "@/lib/client";
import { verifyTOTP } from "@/lib/server/verify";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import Link from "next/link";
@@ -15,6 +15,7 @@ import { CopyToClipboard } from "./copy-to-clipboard";
import { TextInput } from "./input";
import { Spinner } from "./spinner";
import { Translated } from "./translated";
import { AutoSubmitForm } from "./auto-submit-form";
type Inputs = {
code: string;
@@ -33,6 +34,7 @@ type Props = {
export function TotpRegister({ uri, loginName, sessionId, requestId, organization, checkAfter, loginSettings }: Props) {
const [error, setError] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const [samlData, setSamlData] = useState<{ url: string; fields: Record<string, string> } | null>(null);
const router = useRouter();
const { register, handleSubmit, formState } = useForm<Inputs>({
@@ -74,14 +76,7 @@ export function TotpRegister({ uri, loginName, sessionId, requestId, organizatio
loginSettings?.defaultRedirectUri,
);
if ("error" in callbackResponse) {
setError(callbackResponse.error);
return;
}
if ("redirect" in callbackResponse) {
return router.push(callbackResponse.redirect);
}
handleServerActionResponse(callbackResponse, router, setSamlData, setError);
} else if (loginName) {
const callbackResponse = await completeFlowOrGetUrl(
{
@@ -91,14 +86,7 @@ export function TotpRegister({ uri, loginName, sessionId, requestId, organizatio
loginSettings?.defaultRedirectUri,
);
if ("error" in callbackResponse) {
setError(callbackResponse.error);
return;
}
if ("redirect" in callbackResponse) {
return router.push(callbackResponse.redirect);
}
handleServerActionResponse(callbackResponse, router, setSamlData, setError);
}
}
})
@@ -113,6 +101,7 @@ export function TotpRegister({ uri, loginName, sessionId, requestId, organizatio
return (
<div className="flex flex-col items-center">
{samlData && <AutoSubmitForm url={samlData.url} fields={samlData.fields} />}
{uri && (
<>
<QRCodeSVG className="my-4 h-40 w-40 rounded-md bg-white p-2" value={uri} />
+72 -75
View File
@@ -1,6 +1,7 @@
"use client";
import { sendLoginname } from "@/lib/server/loginname";
import { handleServerActionResponse } from "@/lib/client";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
@@ -12,6 +13,7 @@ import { TextInput } from "./input";
import { Spinner } from "./spinner";
import { Translated } from "./translated";
import { useTranslations } from "next-intl";
import { AutoSubmitForm } from "./auto-submit-form";
type Inputs = {
loginName: string;
@@ -51,36 +53,28 @@ export function UsernameForm({
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const [samlData, setSamlData] = useState<{ url: string; fields: Record<string, string> } | null>(null);
async function submitLoginName(values: Inputs, organization?: string) {
setLoading(true);
const res = await sendLoginname({
loginName: values.loginName,
organization,
defaultOrganization,
requestId,
suffix,
ignoreUnknownUsernames: loginSettings?.ignoreUnknownUsernames,
})
.catch(() => {
setError(t("errors.internalError"));
return;
})
.finally(() => {
setLoading(false);
try {
const res = await sendLoginname({
loginName: values.loginName,
organization,
defaultOrganization,
requestId,
suffix,
ignoreUnknownUsernames: loginSettings?.ignoreUnknownUsernames,
});
if (res && "redirect" in res && res.redirect) {
return router.push(res.redirect);
handleServerActionResponse(res, router, setSamlData, setError);
return res;
} catch {
setError(t("errors.internalError"));
} finally {
setLoading(false);
}
if (res && "error" in res && res.error) {
setError(res.error);
return;
}
return res;
}
useEffect(() => {
@@ -100,59 +94,62 @@ export function UsernameForm({
}
return (
<form className="w-full">
<div className="">
<TextInput
type="text"
autoComplete="username"
{...register("loginName", { required: t("required.loginName") })}
label={inputLabel}
data-testid="username-text-input"
suffix={suffix}
/>
{allowRegister && (
<button
className="text-sm transition-all hover:text-primary-light-500 dark:hover:text-primary-dark-500"
onClick={() => {
const registerParams = new URLSearchParams();
if (organization) {
registerParams.append("organization", organization);
}
if (requestId) {
registerParams.append("requestId", requestId);
}
<>
{samlData && <AutoSubmitForm url={samlData.url} fields={samlData.fields} />}
<form className="w-full">
<div className="">
<TextInput
type="text"
autoComplete="username"
{...register("loginName", { required: t("required.loginName") })}
label={inputLabel}
data-testid="username-text-input"
suffix={suffix}
/>
{allowRegister && (
<button
className="text-sm transition-all hover:text-primary-light-500 dark:hover:text-primary-dark-500"
onClick={() => {
const registerParams = new URLSearchParams();
if (organization) {
registerParams.append("organization", organization);
}
if (requestId) {
registerParams.append("requestId", requestId);
}
router.push("/register?" + registerParams);
}}
type="button"
disabled={loading}
data-testid="register-button"
>
<Translated i18nKey="register" namespace="loginname" />
</button>
)}
</div>
{error && (
<div className="py-4" data-testid="error">
<Alert>{error}</Alert>
router.push("/register?" + registerParams);
}}
type="button"
disabled={loading}
data-testid="register-button"
>
<Translated i18nKey="register" namespace="loginname" />
</button>
)}
</div>
)}
<div className="mt-4 flex w-full flex-row items-center">
<BackButton data-testid="back-button" />
<span className="flex-grow"></span>
<Button
data-testid="submit-button"
type="submit"
className="self-end"
variant={ButtonVariants.Primary}
disabled={loading || !formState.isValid}
onClick={handleSubmit((e) => submitLoginName(e, organization))}
>
{loading && <Spinner className="mr-2 h-5 w-5" />}
<Translated i18nKey="submit" namespace="loginname" />
</Button>
</div>
</form>
{error && (
<div className="py-4" data-testid="error">
<Alert>{error}</Alert>
</div>
)}
<div className="mt-4 flex w-full flex-row items-center">
<BackButton data-testid="back-button" />
<span className="flex-grow"></span>
<Button
data-testid="submit-button"
type="submit"
className="self-end"
variant={ButtonVariants.Primary}
disabled={loading || !formState.isValid}
onClick={handleSubmit((e) => submitLoginName(e, organization))}
>
{loading && <Spinner className="mr-2 h-5 w-5" />}
<Translated i18nKey="submit" namespace="loginname" />
</Button>
</div>
</form>
</>
);
}
+17 -21
View File
@@ -2,6 +2,7 @@
import { Alert, AlertType } from "@/components/alert";
import { resendVerification, sendVerification } from "@/lib/server/verify";
import { handleServerActionResponse } from "@/lib/client";
import { UNKNOWN_USER_ID } from "@/lib/constants";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useRef, useState } from "react";
@@ -12,6 +13,7 @@ import { Button, ButtonVariants } from "./button";
import { TextInput } from "./input";
import { Spinner } from "./spinner";
import { Translated } from "./translated";
import { AutoSubmitForm } from "./auto-submit-form";
type Inputs = {
code: string;
@@ -39,6 +41,7 @@ export function VerifyForm({ userId, loginName, organization, requestId, code, i
const t = useTranslations("verify");
const [error, setError] = useState<string>("");
const [samlData, setSamlData] = useState<{ url: string; fields: Record<string, string> } | null>(null);
const [loading, setLoading] = useState<boolean>(false);
@@ -80,29 +83,21 @@ export function VerifyForm({ userId, loginName, organization, requestId, code, i
setError("");
setLoading(true);
const response = await sendVerification({
code: value.code,
userId,
isInvite: isInvite,
loginName: loginName,
organization: organization,
requestId: requestId,
})
.catch(() => {
setError(t("errors.couldNotVerifyUser"));
return;
})
.finally(() => {
setLoading(false);
try {
const response = await sendVerification({
code: value.code,
userId,
isInvite: isInvite,
loginName: loginName,
organization: organization,
requestId: requestId,
});
if (response && "error" in response && response?.error) {
setError(response.error);
return;
}
if (response && "redirect" in response && response?.redirect) {
return router.push(response?.redirect);
handleServerActionResponse(response, router, setSamlData, setError);
} catch {
setError(t("errors.couldNotVerifyUser"));
} finally {
setLoading(false);
}
},
[isInvite, userId],
@@ -117,6 +112,7 @@ export function VerifyForm({ userId, loginName, organization, requestId, code, i
return (
<>
{samlData && <AutoSubmitForm url={samlData.url} fields={samlData.fields} />}
<form className="w-full">
<Alert type={AlertType.INFO}>
<div className="flex flex-row">