always create session from /verify page, cleanup idp session, theme wrapper

This commit is contained in:
Max Peintner
2024-10-25 15:46:00 +02:00
parent 27d4f9b640
commit 3a99d7fe93
7 changed files with 158 additions and 85 deletions

View File

@@ -1,9 +1,8 @@
import { Alert, AlertType } from "@/components/alert"; import { Alert } from "@/components/alert";
import { BackButton } from "@/components/back-button";
import { Button, ButtonVariants } from "@/components/button";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { VerifyForm } from "@/components/verify-form"; import { VerifyForm } from "@/components/verify-form";
import { VerifyRedirectButton } from "@/components/verify-redirect-button";
import { import {
getBrandingSettings, getBrandingSettings,
getUserByID, getUserByID,
@@ -12,7 +11,6 @@ import {
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { getLocale, getTranslations } from "next-intl/server"; import { getLocale, getTranslations } from "next-intl/server";
import Link from "next/link";
export default async function Page({ searchParams }: { searchParams: any }) { export default async function Page({ searchParams }: { searchParams: any }) {
const locale = getLocale(); const locale = getLocale();
@@ -85,26 +83,13 @@ export default async function Page({ searchParams }: { searchParams: any }) {
showDropdown={false} showDropdown={false}
/> />
)} )}
{human?.email?.isVerified ? (
<>
<Alert type={AlertType.INFO}>{t("success")}</Alert>
<div className="mt-8 flex w-full flex-row items-center"> {human?.email?.isVerified ? (
<BackButton /> <VerifyRedirectButton
<span className="flex-grow"></span> userId={userId}
{authMethods?.length === 0 && ( authRequestId={authRequestId}
<Link href={`/authenticator/set?+${params}`}> authMethods={authMethods}
<Button />
type="submit"
className="self-end"
variant={ButtonVariants.Primary}
>
{t("setupAuthenticator")}
</Button>
</Link>
)}
</div>
</>
) : ( ) : (
// check if auth methods are set // check if auth methods are set
<VerifyForm <VerifyForm

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { createNewSession } from "@/lib/server/session"; import { createNewSessionForIdp } from "@/lib/server/session";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Alert } from "./alert"; import { Alert } from "./alert";
@@ -27,7 +27,7 @@ export function IdpSignin({
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
createNewSession({ createNewSessionForIdp({
userId, userId,
idpIntent: { idpIntent: {
idpIntentId, idpIntentId,

View File

@@ -12,9 +12,7 @@ type Props = {
export const ThemeWrapper = ({ children, branding }: Props) => { export const ThemeWrapper = ({ children, branding }: Props) => {
useEffect(() => { useEffect(() => {
setTheme(document, branding); setTheme(document, branding);
}, []); }, [branding]);
const defaultClasses = ""; return <div>{children}</div>;
return <div className={defaultClasses}>{children}</div>;
}; };

View File

@@ -4,7 +4,7 @@ import { Alert } from "@/components/alert";
import { resendVerification, sendVerification } from "@/lib/server/email"; import { resendVerification, sendVerification } from "@/lib/server/email";
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 { useCallback, useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { Button, ButtonVariants } from "./button"; import { Button, ButtonVariants } from "./button";
import { TextInput } from "./input"; import { TextInput } from "./input";
@@ -37,12 +37,6 @@ export function VerifyForm({ userId, code, isInvite, params }: Props) {
const router = useRouter(); const router = useRouter();
useEffect(() => {
if (code) {
submitCodeAndContinue({ code });
}
}, []);
async function resendCode() { async function resendCode() {
setError(""); setError("");
setLoading(true); setLoading(true);
@@ -60,29 +54,32 @@ export function VerifyForm({ userId, code, isInvite, params }: Props) {
return response; return response;
} }
async function submitCodeAndContinue(value: Inputs): Promise<boolean | void> { const fcn = useCallback(
setLoading(true); async function submitCodeAndContinue(
value: Inputs,
): Promise<boolean | void> {
setLoading(true);
await sendVerification({
code: value.code,
userId,
isInvite: isInvite,
}).catch((error) => {
setError("Could not verify user");
setLoading(false);
return;
});
const verifyResponse = await sendVerification({
code: value.code,
userId,
isInvite: isInvite,
}).catch(() => {
setError("Could not verify user");
setLoading(false); setLoading(false);
return; },
}); [isInvite, userId],
);
setLoading(false); useEffect(() => {
if (code) {
if (!verifyResponse) { fcn({ code });
setError("Could not verify user");
return;
} else {
setError("");
return router.push("/authenticator/set?" + params);
} }
} }, [code, fcn]);
return ( return (
<> <>
@@ -116,7 +113,7 @@ export function VerifyForm({ userId, code, isInvite, params }: Props) {
className="self-end" className="self-end"
variant={ButtonVariants.Primary} variant={ButtonVariants.Primary}
disabled={loading || !formState.isValid} disabled={loading || !formState.isValid}
onClick={handleSubmit(submitCodeAndContinue)} onClick={handleSubmit(fcn)}
> >
{loading && <Spinner className="h-5 w-5 mr-2" />} {loading && <Spinner className="h-5 w-5 mr-2" />}
{t("verify.submit")} {t("verify.submit")}

View File

@@ -0,0 +1,68 @@
"use client";
import { sendVerificationRedirectWithoutCheck } from "@/lib/server/email";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { Alert, AlertType } from "./alert";
import { BackButton } from "./back-button";
import { Button, ButtonVariants } from "./button";
import { Spinner } from "./spinner";
export function VerifyRedirectButton({
userId,
authRequestId,
authMethods,
}: {
userId: string;
authRequestId: string;
authMethods: AuthenticationMethodType[] | null;
}) {
const t = useTranslations("verify");
const [error, setError] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
async function submitAndContinue(): Promise<boolean | void> {
setLoading(true);
await sendVerificationRedirectWithoutCheck({
userId,
authRequestId,
}).catch((error) => {
setError("Could not verify user");
setLoading(false);
return;
});
setLoading(false);
}
return (
<>
<Alert type={AlertType.INFO}>{t("success")}</Alert>
{error && (
<div className="py-4">
<Alert>{error}</Alert>
</div>
)}
<div className="mt-8 flex w-full flex-row items-center">
<BackButton />
<span className="flex-grow"></span>
{authMethods?.length === 0 && (
<Button
onClick={() => submitAndContinue()}
type="submit"
className="self-end"
variant={ButtonVariants.Primary}
>
{loading && <Spinner className="h-5 w-5 mr-2" />}
{t("setupAuthenticator")}
</Button>
)}
</div>
</>
);
}

View File

@@ -76,12 +76,6 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
} }
return redirect("/authenticator/set?" + params); return redirect("/authenticator/set?" + params);
} }
// return {
// authMethodTypes: authMethodResponse.authMethodTypes,
// sessionId: session.id,
// factors: session.factors,
// };
} }
type resendVerifyEmailCommand = { type resendVerifyEmailCommand = {
@@ -94,3 +88,52 @@ export async function resendVerification(command: resendVerifyEmailCommand) {
? resendEmailCode(command.userId) ? resendEmailCode(command.userId)
: resendInviteCode(command.userId); : resendInviteCode(command.userId);
} }
export async function sendVerificationRedirectWithoutCheck(command: {
userId: string;
authRequestId?: string;
}) {
const userResponse = await getUserByID(command.userId);
if (!userResponse || !userResponse.user) {
return { error: "Could not load user" };
}
const checks = create(ChecksSchema, {
user: {
search: {
case: "loginName",
value: userResponse.user.preferredLoginName,
},
},
});
const session = await createSessionAndUpdateCookie(
checks,
undefined,
command.authRequestId,
);
const authMethodResponse = await listAuthenticationMethodTypes(
command.userId,
);
if (!authMethodResponse || !authMethodResponse.authMethodTypes) {
return { error: "Could not load possible authenticators" };
}
// if no authmethods are found on the user, redirect to set one up
if (
authMethodResponse &&
authMethodResponse.authMethodTypes &&
authMethodResponse.authMethodTypes.length == 0
) {
const params = new URLSearchParams({
sessionId: session.id,
});
if (session.factors?.user?.loginName) {
params.set("loginName", session.factors?.user?.loginName);
}
return redirect("/authenticator/set?" + params);
}
}

View File

@@ -1,17 +1,12 @@
"use server"; "use server";
import { import {
createSessionAndUpdateCookie,
createSessionForIdpAndUpdateCookie, createSessionForIdpAndUpdateCookie,
setSessionAndUpdateCookie, setSessionAndUpdateCookie,
} from "@/lib/server/cookie"; } from "@/lib/server/cookie";
import { deleteSession, listAuthenticationMethodTypes } from "@/lib/zitadel"; import { deleteSession, listAuthenticationMethodTypes } from "@/lib/zitadel";
import { create } from "@zitadel/client";
import { RequestChallenges } from "@zitadel/proto/zitadel/session/v2/challenge_pb"; import { RequestChallenges } from "@zitadel/proto/zitadel/session/v2/challenge_pb";
import { import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
Checks,
ChecksSchema,
} from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { import {
getMostRecentSessionCookie, getMostRecentSessionCookie,
@@ -31,26 +26,13 @@ type CreateNewSessionCommand = {
authRequestId?: string; authRequestId?: string;
}; };
export async function createNewSession(options: CreateNewSessionCommand) { export async function createNewSessionForIdp(options: CreateNewSessionCommand) {
const { userId, idpIntent, loginName, password, authRequestId } = options; const { userId, idpIntent, authRequestId } = options;
if (userId && idpIntent) { if (!userId || !idpIntent) {
return createSessionForIdpAndUpdateCookie(userId, idpIntent, authRequestId);
} else if (loginName) {
const checks = create(
ChecksSchema,
password
? {
user: { search: { case: "loginName", value: loginName } },
password: { password },
}
: { user: { search: { case: "loginName", value: loginName } } },
);
return createSessionAndUpdateCookie(checks, undefined, authRequestId);
} else {
throw new Error("No userId or loginName provided"); throw new Error("No userId or loginName provided");
} }
return createSessionForIdpAndUpdateCookie(userId, idpIntent, authRequestId);
} }
export type UpdateSessionCommand = { export type UpdateSessionCommand = {