mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 15:27:33 +00:00
Merge pull request #467 from zitadel/invite-expired-resend
fix: invite flow when expired
This commit is contained in:
@@ -7,6 +7,7 @@ import { UserAvatar } from "@/components/user-avatar";
|
|||||||
import { getSessionCookieById } from "@/lib/cookies";
|
import { getSessionCookieById } from "@/lib/cookies";
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
import { loadMostRecentSession } from "@/lib/session";
|
import { loadMostRecentSession } from "@/lib/session";
|
||||||
|
import { checkUserVerification } from "@/lib/verify-helper";
|
||||||
import {
|
import {
|
||||||
getActiveIdentityProviders,
|
getActiveIdentityProviders,
|
||||||
getBrandingSettings,
|
getBrandingSettings,
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||||
import { getLocale, getTranslations } from "next-intl/server";
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
export default async function Page(props: {
|
export default async function Page(props: {
|
||||||
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
||||||
@@ -92,20 +94,49 @@ export default async function Page(props: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sessionWithData) {
|
if (
|
||||||
|
!sessionWithData ||
|
||||||
|
!sessionWithData.factors ||
|
||||||
|
!sessionWithData.factors.user
|
||||||
|
) {
|
||||||
return <Alert>{tError("unknownContext")}</Alert>;
|
return <Alert>{tError("unknownContext")}</Alert>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const branding = await getBrandingSettings({
|
const branding = await getBrandingSettings({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
organization: sessionWithData.factors?.user?.organizationId,
|
organization: sessionWithData.factors.user?.organizationId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const loginSettings = await getLoginSettings({
|
const loginSettings = await getLoginSettings({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
organization: sessionWithData.factors?.user?.organizationId,
|
organization: sessionWithData.factors.user?.organizationId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// check if user was verified recently
|
||||||
|
const isUserVerified = await checkUserVerification(
|
||||||
|
sessionWithData.factors.user?.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isUserVerified) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
loginName: sessionWithData.factors.user.loginName as string,
|
||||||
|
send: "true", // set this to true to request a new code immediately
|
||||||
|
});
|
||||||
|
|
||||||
|
if (requestId) {
|
||||||
|
params.append("requestId", requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organization || sessionWithData.factors.user.organizationId) {
|
||||||
|
params.append(
|
||||||
|
"organization",
|
||||||
|
organization ?? (sessionWithData.factors.user.organizationId as string),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
redirect(`/verify?` + params);
|
||||||
|
}
|
||||||
|
|
||||||
const identityProviders = await getActiveIdentityProviders({
|
const identityProviders = await getActiveIdentityProviders({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
orgId: sessionWithData.factors?.user?.organizationId,
|
orgId: sessionWithData.factors?.user?.organizationId,
|
||||||
|
@@ -2,18 +2,11 @@ import { Alert, AlertType } from "@/components/alert";
|
|||||||
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 { sendEmailCode } from "@/lib/server/verify";
|
import { sendEmailCode } from "@/lib/server/verify";
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
import { loadMostRecentSession } from "@/lib/session";
|
import { loadMostRecentSession } from "@/lib/session";
|
||||||
import { checkUserVerification } from "@/lib/verify-helper";
|
import { getBrandingSettings, getUserByID } from "@/lib/zitadel";
|
||||||
import {
|
|
||||||
getBrandingSettings,
|
|
||||||
getUserByID,
|
|
||||||
listAuthenticationMethodTypes,
|
|
||||||
} from "@/lib/zitadel";
|
|
||||||
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 { getLocale, getTranslations } from "next-intl/server";
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
|
|
||||||
@@ -101,19 +94,6 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
|||||||
throw Error("Failed to get user id");
|
throw Error("Failed to get user id");
|
||||||
}
|
}
|
||||||
|
|
||||||
let authMethods: AuthenticationMethodType[] | null = null;
|
|
||||||
if (human?.email?.isVerified) {
|
|
||||||
const authMethodsResponse = await listAuthenticationMethodTypes({
|
|
||||||
serviceUrl,
|
|
||||||
userId,
|
|
||||||
});
|
|
||||||
if (authMethodsResponse.authMethodTypes) {
|
|
||||||
authMethods = authMethodsResponse.authMethodTypes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasValidUserVerificationCheck = await checkUserVerification(id);
|
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
userId: userId,
|
userId: userId,
|
||||||
initial: "true", // defines that a code is not required and is therefore not shown in the UI
|
initial: "true", // defines that a code is not required and is therefore not shown in the UI
|
||||||
@@ -171,27 +151,15 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* show a button to setup auth method for the user otherwise show the UI for reverifying */}
|
{/* always show the code form / TODO improve UI for email links which were already used (currently we get an error code 3 due being reused) */}
|
||||||
{human?.email?.isVerified && hasValidUserVerificationCheck ? (
|
<VerifyForm
|
||||||
// show page for already verified users
|
loginName={loginName}
|
||||||
<VerifyRedirectButton
|
organization={organization}
|
||||||
userId={id}
|
userId={id}
|
||||||
loginName={loginName}
|
code={code}
|
||||||
organization={organization}
|
isInvite={invite === "true"}
|
||||||
requestId={requestId}
|
requestId={requestId}
|
||||||
authMethods={authMethods}
|
/>
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
// check if auth methods are set
|
|
||||||
<VerifyForm
|
|
||||||
loginName={loginName}
|
|
||||||
organization={organization}
|
|
||||||
userId={id}
|
|
||||||
code={code}
|
|
||||||
isInvite={invite === "true"}
|
|
||||||
requestId={requestId}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</DynamicTheme>
|
</DynamicTheme>
|
||||||
);
|
);
|
||||||
|
@@ -6,6 +6,7 @@ import {
|
|||||||
} from "@/lib/server/verify";
|
} from "@/lib/server/verify";
|
||||||
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_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 { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Alert, AlertType } from "./alert";
|
import { Alert, AlertType } from "./alert";
|
||||||
import { BackButton } from "./back-button";
|
import { BackButton } from "./back-button";
|
||||||
@@ -29,6 +30,7 @@ export function VerifyRedirectButton({
|
|||||||
const [error, setError] = useState<string>("");
|
const [error, setError] = useState<string>("");
|
||||||
|
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
async function submitAndContinue(): Promise<boolean | void> {
|
async function submitAndContinue(): Promise<boolean | void> {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -50,7 +52,7 @@ export function VerifyRedirectButton({
|
|||||||
} as SendVerificationRedirectWithoutCheckCommand;
|
} as SendVerificationRedirectWithoutCheckCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendVerificationRedirectWithoutCheck(command)
|
const response = await sendVerificationRedirectWithoutCheck(command)
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setError("Could not verify");
|
setError("Could not verify");
|
||||||
return;
|
return;
|
||||||
@@ -58,6 +60,16 @@ export function VerifyRedirectButton({
|
|||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (response && "error" in response && response.error) {
|
||||||
|
setError(response.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response && "redirect" in response && response.redirect) {
|
||||||
|
router.push(response.redirect);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createInviteCode,
|
|
||||||
getLoginSettings,
|
getLoginSettings,
|
||||||
getSession,
|
getSession,
|
||||||
getUserByID,
|
getUserByID,
|
||||||
listAuthenticationMethodTypes,
|
listAuthenticationMethodTypes,
|
||||||
|
resendInviteCode,
|
||||||
verifyEmail,
|
verifyEmail,
|
||||||
verifyInviteCode,
|
verifyInviteCode,
|
||||||
verifyTOTPRegistration,
|
verifyTOTPRegistration,
|
||||||
@@ -71,14 +71,16 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
|
|||||||
serviceUrl,
|
serviceUrl,
|
||||||
userId: command.userId,
|
userId: command.userId,
|
||||||
verificationCode: command.code,
|
verificationCode: command.code,
|
||||||
}).catch(() => {
|
}).catch((error) => {
|
||||||
|
console.warn(error);
|
||||||
return { error: "Could not verify invite" };
|
return { error: "Could not verify invite" };
|
||||||
})
|
})
|
||||||
: await verifyEmail({
|
: await verifyEmail({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
userId: command.userId,
|
userId: command.userId,
|
||||||
verificationCode: command.code,
|
verificationCode: command.code,
|
||||||
}).catch(() => {
|
}).catch((error) => {
|
||||||
|
console.warn(error);
|
||||||
return { error: "Could not verify email" };
|
return { error: "Could not verify email" };
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -273,15 +275,11 @@ export async function resendVerification(command: resendVerifyEmailCommand) {
|
|||||||
|
|
||||||
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
|
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
|
||||||
|
|
||||||
// create a new invite whenever the resend is called
|
|
||||||
return command.isInvite
|
return command.isInvite
|
||||||
? createInviteCode({
|
? resendInviteCode({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
userId: command.userId,
|
userId: command.userId,
|
||||||
urlTemplate:
|
})
|
||||||
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` +
|
|
||||||
(command.requestId ? `&requestId=${command.requestId}` : ""),
|
|
||||||
}) //resendInviteCode({ serviceUrl, userId: command.userId })
|
|
||||||
: sendEmailCode({
|
: sendEmailCode({
|
||||||
userId: command.userId,
|
userId: command.userId,
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
|
Reference in New Issue
Block a user