mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-12 16:43:01 +00:00
auth methods on verify page
This commit is contained in:
@@ -15,9 +15,9 @@ export default async function Page({ searchParams }: { searchParams: any }) {
|
|||||||
loginName,
|
loginName,
|
||||||
sessionId,
|
sessionId,
|
||||||
code,
|
code,
|
||||||
submit,
|
|
||||||
organization,
|
organization,
|
||||||
authRequestId,
|
authRequestId,
|
||||||
|
invite,
|
||||||
} = searchParams;
|
} = searchParams;
|
||||||
|
|
||||||
const branding = await getBrandingSettings(organization);
|
const branding = await getBrandingSettings(organization);
|
||||||
@@ -41,11 +41,11 @@ export default async function Page({ searchParams }: { searchParams: any }) {
|
|||||||
userId={userId}
|
userId={userId}
|
||||||
loginName={loginName}
|
loginName={loginName}
|
||||||
code={code}
|
code={code}
|
||||||
submit={submit === "true"}
|
|
||||||
organization={organization}
|
organization={organization}
|
||||||
authRequestId={authRequestId}
|
authRequestId={authRequestId}
|
||||||
sessionId={sessionId}
|
sessionId={sessionId}
|
||||||
loginSettings={loginSettings}
|
loginSettings={loginSettings}
|
||||||
|
isInvite={invite === "true"}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full flex flex-row items-center justify-center border border-yellow-600/40 dark:border-yellow-500/20 bg-yellow-200/30 text-yellow-600 dark:bg-yellow-700/20 dark:text-yellow-200 rounded-md py-2 scroll-px-40">
|
<div className="w-full flex flex-row items-center justify-center border border-yellow-600/40 dark:border-yellow-500/20 bg-yellow-200/30 text-yellow-600 dark:bg-yellow-700/20 dark:text-yellow-200 rounded-md py-2 scroll-px-40">
|
||||||
|
|||||||
@@ -194,6 +194,74 @@ export const SMS = (alreadyAdded: boolean, link: string) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PASSKEYS = (alreadyAdded: boolean, link: string) => {
|
||||||
|
return (
|
||||||
|
<LinkWrapper alreadyAdded={alreadyAdded} link={link}>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"font-medium flex items-center",
|
||||||
|
alreadyAdded ? "" : "",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
className="w-8 h-8 mr-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M7.864 4.243A7.5 7.5 0 0119.5 10.5c0 2.92-.556 5.709-1.568 8.268M5.742 6.364A7.465 7.465 0 004.5 10.5a7.464 7.464 0 01-1.15 3.993m1.989 3.559A11.209 11.209 0 008.25 10.5a3.75 3.75 0 117.5 0c0 .527-.021 1.049-.064 1.565M12 10.5a14.94 14.94 0 01-3.6 9.75m6.633-4.596a18.666 18.666 0 01-2.485 5.33"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>Passkeys</span>
|
||||||
|
</div>
|
||||||
|
{alreadyAdded && (
|
||||||
|
<>
|
||||||
|
<Setup />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</LinkWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PASSWORD = (alreadyAdded: boolean, link: string) => {
|
||||||
|
return (
|
||||||
|
<LinkWrapper alreadyAdded={alreadyAdded} link={link}>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"font-medium flex items-center",
|
||||||
|
alreadyAdded ? "" : "",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
className="w-8 h-8 mr-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M7.864 4.243A7.5 7.5 0 0119.5 10.5c0 2.92-.556 5.709-1.568 8.268M5.742 6.364A7.465 7.465 0 004.5 10.5a7.464 7.464 0 01-1.15 3.993m1.989 3.559A11.209 11.209 0 008.25 10.5a3.75 3.75 0 117.5 0c0 .527-.021 1.049-.064 1.565M12 10.5a14.94 14.94 0 01-3.6 9.75m6.633-4.596a18.666 18.666 0 01-2.485 5.33"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>Password</span>
|
||||||
|
</div>
|
||||||
|
{alreadyAdded && (
|
||||||
|
<>
|
||||||
|
<Setup />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</LinkWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
function Setup() {
|
function Setup() {
|
||||||
return (
|
return (
|
||||||
<div className="transform absolute right-2 top-0">
|
<div className="transform absolute right-2 top-0">
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Alert } from "@/components/alert";
|
import { Alert } from "@/components/alert";
|
||||||
import { resendVerifyEmail, verifyUserByEmail } from "@/lib/server/email";
|
import { resendVerifyEmail, verifyUser } from "@/lib/server/email";
|
||||||
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
||||||
|
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
import { PASSKEYS, PASSWORD } from "./auth-methods";
|
||||||
import { Button, ButtonVariants } from "./button";
|
import { Button, ButtonVariants } from "./button";
|
||||||
import { TextInput } from "./input";
|
import { TextInput } from "./input";
|
||||||
import { Spinner } from "./spinner";
|
import { Spinner } from "./spinner";
|
||||||
@@ -19,22 +21,22 @@ type Props = {
|
|||||||
userId: string;
|
userId: string;
|
||||||
loginName: string;
|
loginName: string;
|
||||||
code: string;
|
code: string;
|
||||||
submit: boolean;
|
|
||||||
organization?: string;
|
organization?: string;
|
||||||
authRequestId?: string;
|
authRequestId?: string;
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
loginSettings?: LoginSettings;
|
loginSettings?: LoginSettings;
|
||||||
|
isInvite: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function VerifyEmailForm({
|
export function VerifyEmailForm({
|
||||||
userId,
|
userId,
|
||||||
loginName,
|
loginName,
|
||||||
code,
|
code,
|
||||||
submit,
|
|
||||||
organization,
|
organization,
|
||||||
authRequestId,
|
authRequestId,
|
||||||
sessionId,
|
sessionId,
|
||||||
loginSettings,
|
loginSettings,
|
||||||
|
isInvite,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const t = useTranslations("verify");
|
const t = useTranslations("verify");
|
||||||
|
|
||||||
@@ -45,8 +47,12 @@ export function VerifyEmailForm({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [authMethods, setAuthMethods] = useState<
|
||||||
|
AuthenticationMethodType[] | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (submit && code && userId) {
|
if (code && userId) {
|
||||||
// When we navigate to this page, we always want to be redirected if submit is true and the parameters are valid.
|
// When we navigate to this page, we always want to be redirected if submit is true and the parameters are valid.
|
||||||
// For programmatic verification, the /verifyemail API should be used.
|
// For programmatic verification, the /verifyemail API should be used.
|
||||||
submitCodeAndContinue({ code });
|
submitCodeAndContinue({ code });
|
||||||
@@ -59,6 +65,21 @@ export function VerifyEmailForm({
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const params = new URLSearchParams({});
|
||||||
|
|
||||||
|
if (loginName) {
|
||||||
|
params.append("loginName", loginName);
|
||||||
|
}
|
||||||
|
if (sessionId) {
|
||||||
|
params.append("sessionId", sessionId);
|
||||||
|
}
|
||||||
|
if (authRequestId) {
|
||||||
|
params.append("authRequestId", authRequestId);
|
||||||
|
}
|
||||||
|
if (organization) {
|
||||||
|
params.append("organization", organization);
|
||||||
|
}
|
||||||
|
|
||||||
async function resendCode() {
|
async function resendCode() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await resendVerifyEmail({
|
const response = await resendVerifyEmail({
|
||||||
@@ -73,9 +94,10 @@ export function VerifyEmailForm({
|
|||||||
|
|
||||||
async function submitCodeAndContinue(value: Inputs): Promise<boolean | void> {
|
async function submitCodeAndContinue(value: Inputs): Promise<boolean | void> {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const verifyResponse = await verifyUserByEmail({
|
const verifyResponse = await verifyUser({
|
||||||
code: value.code,
|
code: value.code,
|
||||||
userId,
|
userId,
|
||||||
|
isInvite: isInvite,
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
setError("Could not verify email");
|
setError("Could not verify email");
|
||||||
});
|
});
|
||||||
@@ -87,6 +109,10 @@ export function VerifyEmailForm({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (verifyResponse.authMethodTypes) {
|
||||||
|
setAuthMethods(verifyResponse.authMethodTypes);
|
||||||
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams({});
|
const params = new URLSearchParams({});
|
||||||
|
|
||||||
if (organization) {
|
if (organization) {
|
||||||
@@ -102,7 +128,7 @@ export function VerifyEmailForm({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return !authMethods ? (
|
||||||
<form className="w-full">
|
<form className="w-full">
|
||||||
<div className="">
|
<div className="">
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -141,5 +167,12 @@ export function VerifyEmailForm({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 gap-5 w-full pt-4">
|
||||||
|
{!authMethods.includes(AuthenticationMethodType.PASSWORD) &&
|
||||||
|
PASSWORD(false, "/password/set?" + params)}
|
||||||
|
{!authMethods.includes(AuthenticationMethodType.PASSKEY) &&
|
||||||
|
PASSKEYS(false, "/passkeys/set?" + params)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,36 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { resendEmailCode, verifyEmail } from "@/lib/zitadel";
|
import {
|
||||||
|
listAuthenticationMethodTypes,
|
||||||
|
resendEmailCode,
|
||||||
|
verifyEmail,
|
||||||
|
verifyInviteCode,
|
||||||
|
} from "@/lib/zitadel";
|
||||||
|
|
||||||
type VerifyUserByEmailCommand = {
|
type VerifyUserByEmailCommand = {
|
||||||
userId: string;
|
userId: string;
|
||||||
code: string;
|
code: string;
|
||||||
|
isInvite: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function verifyUserByEmail(command: VerifyUserByEmailCommand) {
|
export async function verifyUser(command: VerifyUserByEmailCommand) {
|
||||||
return verifyEmail(command.userId, command.code);
|
const verifyResponse = command.isInvite
|
||||||
|
? await verifyInviteCode(command.userId, command.code)
|
||||||
|
: await verifyEmail(command.userId, command.code);
|
||||||
|
|
||||||
|
if (!verifyResponse) {
|
||||||
|
return { error: "Could not verify user email" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const authMethodResponse = await listAuthenticationMethodTypes(
|
||||||
|
command.userId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!authMethodResponse || !authMethodResponse.authMethodTypes) {
|
||||||
|
return { error: "Could not load possible authenticators" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { authMethodTypes: authMethodResponse.authMethodTypes };
|
||||||
}
|
}
|
||||||
|
|
||||||
type resendVerifyEmailCommand = {
|
type resendVerifyEmailCommand = {
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ export async function createInviteCode(userId: string, host: string | null) {
|
|||||||
if (host) {
|
if (host) {
|
||||||
medium = {
|
medium = {
|
||||||
...medium,
|
...medium,
|
||||||
urlTemplate: `https://${host}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}`,
|
urlTemplate: `https://${host}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user