mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-15 10:25:11 +00:00
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:
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user