auth methods on verify page

This commit is contained in:
peintnermax
2024-10-21 10:54:53 +02:00
parent 18a1262cb5
commit 98a5e042b2
5 changed files with 135 additions and 12 deletions

View File

@@ -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">

View File

@@ -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">

View File

@@ -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>
); );
} }

View File

@@ -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 = {

View File

@@ -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`,
}; };
} }