diff --git a/apps/login/.env.integration b/apps/login/.env.integration index 03bff984d9..90adb84eee 100644 --- a/apps/login/.env.integration +++ b/apps/login/.env.integration @@ -1,3 +1,3 @@ ZITADEL_API_URL=http://localhost:22222 -CACHE_REVALIDATION_INTERVAL_IN_SECONDS=3600 +EMAIL_VERIFICATION=true DEBUG=true \ No newline at end of file diff --git a/apps/login/src/app/(login)/accounts/page.tsx b/apps/login/src/app/(login)/accounts/page.tsx index 9d2e6a0b9f..49fbad6202 100644 --- a/apps/login/src/app/(login)/accounts/page.tsx +++ b/apps/login/src/app/(login)/accounts/page.tsx @@ -49,6 +49,16 @@ export default async function Page(props: { organization ?? defaultOrganization, ); + const params = new URLSearchParams(); + + if (authRequestId) { + params.append("authRequestId", authRequestId); + } + + if (organization) { + params.append("organization", organization); + } + return (
@@ -57,16 +67,7 @@ export default async function Page(props: {
- +
diff --git a/apps/login/src/app/error.tsx b/apps/login/src/app/(login)/error.tsx similarity index 100% rename from apps/login/src/app/error.tsx rename to apps/login/src/app/(login)/error.tsx diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index fc11f45921..62f2284485 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -3,6 +3,8 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { UserAvatar } from "@/components/user-avatar"; import { VerifyForm } from "@/components/verify-form"; import { VerifyRedirectButton } from "@/components/verify-redirect-button"; +import { resendVerification } from "@/lib/server/verify"; +import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings, getUserByID, @@ -18,14 +20,49 @@ export default async function Page(props: { searchParams: Promise }) { const t = await getTranslations({ locale, namespace: "verify" }); const tError = await getTranslations({ locale, namespace: "error" }); - const { userId, loginName, code, organization, authRequestId, invite } = - searchParams; + const { + userId, + loginName, + code, + organization, + authRequestId, + invite, + skipsend, + } = searchParams; const branding = await getBrandingSettings(organization); + let sessionFactors; let user: User | undefined; let human: HumanUser | undefined; - if (userId) { + let id: string | undefined; + + if ("loginName" in searchParams) { + sessionFactors = await loadMostRecentSession({ + loginName, + organization, + }); + + if (!skipsend && sessionFactors?.factors?.user?.id) { + await resendVerification({ + userId: sessionFactors?.factors?.user?.id, + isInvite: invite === "true", + }).catch((error) => { + console.error("Could not resend verification email", error); + throw Error("Could not request email"); + }); + } + } else if ("userId" in searchParams && userId) { + if (!skipsend) { + await resendVerification({ + userId, + isInvite: invite === "true", + }).catch((error) => { + console.error("Could not resend verification email", error); + throw Error("Could not request email"); + }); + } + const userResponse = await getUserByID(userId); if (userResponse) { user = userResponse.user; @@ -35,6 +72,8 @@ export default async function Page(props: { searchParams: Promise }) { } } + id = userId ?? sessionFactors?.factors?.user?.id; + let authMethods: AuthenticationMethodType[] | null = null; if (human?.email?.isVerified) { const authMethodsResponse = await listAuthenticationMethodTypes(userId); @@ -66,7 +105,7 @@ export default async function Page(props: { searchParams: Promise }) {

{t("verify.title")}

{t("verify.description")}

- {!userId && ( + {!id && ( <>

{t("verify.title")}

{t("verify.description")}

@@ -77,29 +116,44 @@ export default async function Page(props: { searchParams: Promise }) { )} - {user && ( + {sessionFactors ? ( + loginName={loginName ?? sessionFactors.factors?.user?.loginName} + displayName={sessionFactors.factors?.user?.displayName} + showDropdown + searchParams={searchParams} + > + ) : ( + user && ( + + ) )} - {human?.email?.isVerified ? ( - - ) : ( - // check if auth methods are set - - )} + {id && + (human?.email?.isVerified ? ( + // show page for already verified users + + ) : ( + // check if auth methods are set + + ))}
); diff --git a/apps/login/src/components/idp-signin.tsx b/apps/login/src/components/idp-signin.tsx index 543cd64b2c..c2f3fe40b3 100644 --- a/apps/login/src/components/idp-signin.tsx +++ b/apps/login/src/components/idp-signin.tsx @@ -1,6 +1,6 @@ "use client"; -import { createNewSessionForIdp } from "@/lib/server/session"; +import { createNewSessionFromIdpIntent } from "@/lib/server/idp"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { Alert } from "./alert"; @@ -27,7 +27,7 @@ export function IdpSignin({ const router = useRouter(); useEffect(() => { - createNewSessionForIdp({ + createNewSessionFromIdpIntent({ userId, idpIntent: { idpIntentId, diff --git a/apps/login/src/components/login-passkey.tsx b/apps/login/src/components/login-passkey.tsx index 2f1cd53363..5e05cdb6a8 100644 --- a/apps/login/src/components/login-passkey.tsx +++ b/apps/login/src/components/login-passkey.tsx @@ -1,9 +1,9 @@ "use client"; import { coerceToArrayBuffer, coerceToBase64Url } from "@/helpers/base64"; -import { getNextUrl } from "@/lib/client"; +import { sendPasskey } from "@/lib/server/passkeys"; import { updateSession } from "@/lib/server/session"; -import { create } from "@zitadel/client"; +import { create, JsonObject } from "@zitadel/client"; import { RequestChallengesSchema, UserVerificationRequirement, @@ -118,9 +118,9 @@ export function LoginPasskey({ return session; } - async function submitLogin(data: any) { + async function submitLogin(data: JsonObject) { setLoading(true); - const response = await updateSession({ + const response = await sendPasskey({ loginName, sessionId, organization, @@ -142,7 +142,9 @@ export function LoginPasskey({ return; } - return response; + if (response && "redirect" in response && response.redirect) { + return router.push(response.redirect); + } } async function submitLoginAndContinue( @@ -192,31 +194,7 @@ export function LoginPasskey({ }, }; - return submitLogin(data).then(async (resp) => { - const url = - authRequestId && resp?.sessionId - ? await getNextUrl( - { - sessionId: resp.sessionId, - authRequestId: authRequestId, - organization: organization, - }, - loginSettings?.defaultRedirectUri, - ) - : resp?.factors?.user?.loginName - ? await getNextUrl( - { - loginName: resp.factors.user.loginName, - organization: organization, - }, - loginSettings?.defaultRedirectUri, - ) - : null; - - if (url) { - router.push(url); - } - }); + return submitLogin(data); }) .finally(() => { setLoading(false); diff --git a/apps/login/src/components/register-passkey.tsx b/apps/login/src/components/register-passkey.tsx index 9a71830261..e737168678 100644 --- a/apps/login/src/components/register-passkey.tsx +++ b/apps/login/src/components/register-passkey.tsx @@ -1,7 +1,10 @@ "use client"; import { coerceToArrayBuffer, coerceToBase64Url } from "@/helpers/base64"; -import { registerPasskeyLink, verifyPasskey } from "@/lib/server/passkeys"; +import { + registerPasskeyLink, + verifyPasskeyRegistration, +} from "@/lib/server/passkeys"; import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; import { useState } from "react"; @@ -45,7 +48,7 @@ export function RegisterPasskey({ sessionId: string, ) { setLoading(true); - const response = await verifyPasskey({ + const response = await verifyPasskeyRegistration({ passkeyId, passkeyName, publicKeyCredential, diff --git a/apps/login/src/components/verify-form.tsx b/apps/login/src/components/verify-form.tsx index 003b261b02..6b6189297e 100644 --- a/apps/login/src/components/verify-form.tsx +++ b/apps/login/src/components/verify-form.tsx @@ -1,7 +1,7 @@ "use client"; import { Alert, AlertType } from "@/components/alert"; -import { resendVerification, sendVerification } from "@/lib/server/email"; +import { resendVerification, sendVerification } from "@/lib/server/verify"; import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useState } from "react"; @@ -17,12 +17,21 @@ type Inputs = { type Props = { userId: string; + loginName?: string; + organization?: string; code?: string; isInvite: boolean; - params: URLSearchParams; + authRequestId?: string; }; -export function VerifyForm({ userId, code, isInvite, params }: Props) { +export function VerifyForm({ + userId, + loginName, + organization, + authRequestId, + code, + isInvite, +}: Props) { const t = useTranslations("verify"); const router = useRouter(); @@ -67,6 +76,9 @@ export function VerifyForm({ userId, code, isInvite, params }: Props) { code: value.code, userId, isInvite: isInvite, + loginName: loginName, + organization: organization, + authRequestId: authRequestId, }) .catch(() => { setError("Could not verify user"); @@ -76,12 +88,12 @@ export function VerifyForm({ userId, code, isInvite, params }: Props) { setLoading(false); }); - if (response?.error) { + if (response && "error" in response && response?.error) { setError(response.error); return; } - if (response?.redirect) { + if (response && "redirect" in response && response?.redirect) { return router.push(response?.redirect); } }, diff --git a/apps/login/src/components/verify-redirect-button.tsx b/apps/login/src/components/verify-redirect-button.tsx index 4fe313cd1d..09a00efe37 100644 --- a/apps/login/src/components/verify-redirect-button.tsx +++ b/apps/login/src/components/verify-redirect-button.tsx @@ -1,6 +1,9 @@ "use client"; -import { sendVerificationRedirectWithoutCheck } from "@/lib/server/email"; +import { + sendVerificationRedirectWithoutCheck, + SendVerificationRedirectWithoutCheckCommand, +} from "@/lib/server/verify"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { useTranslations } from "next-intl"; import { useState } from "react"; @@ -11,12 +14,16 @@ import { Spinner } from "./spinner"; export function VerifyRedirectButton({ userId, + loginName, authRequestId, authMethods, + organization, }: { - userId: string; + userId?: string; + loginName?: string; authRequestId: string; authMethods: AuthenticationMethodType[] | null; + organization?: string; }) { const t = useTranslations("verify"); const [error, setError] = useState(""); @@ -26,10 +33,24 @@ export function VerifyRedirectButton({ async function submitAndContinue(): Promise { setLoading(true); - await sendVerificationRedirectWithoutCheck({ - userId, + let command = { + organization, authRequestId, - }) + } as SendVerificationRedirectWithoutCheckCommand; + + if (userId) { + command = { + ...command, + userId, + } as SendVerificationRedirectWithoutCheckCommand; + } else if (loginName) { + command = { + ...command, + loginName, + } as SendVerificationRedirectWithoutCheckCommand; + } + + await sendVerificationRedirectWithoutCheck(command) .catch((error) => { setError("Could not verify user"); return; diff --git a/apps/login/src/lib/server/email.ts b/apps/login/src/lib/server/email.ts deleted file mode 100644 index 51eb1a019b..0000000000 --- a/apps/login/src/lib/server/email.ts +++ /dev/null @@ -1,138 +0,0 @@ -"use server"; - -import { - getUserByID, - listAuthenticationMethodTypes, - resendEmailCode, - resendInviteCode, - verifyEmail, - verifyInviteCode, -} from "@/lib/zitadel"; -import { create } from "@zitadel/client"; -import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; -import { createSessionAndUpdateCookie } from "./cookie"; - -type VerifyUserByEmailCommand = { - userId: string; - code: string; - isInvite: boolean; - authRequestId?: string; -}; - -export async function sendVerification(command: VerifyUserByEmailCommand) { - const verifyResponse = command.isInvite - ? await verifyInviteCode(command.userId, command.code).catch(() => { - return { error: "Could not verify invite" }; - }) - : await verifyEmail(command.userId, command.code).catch(() => { - return { error: "Could not verify email" }; - }); - - if (!verifyResponse) { - return { error: "Could not verify user" }; - } - - 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}` }; - } -} - -type resendVerifyEmailCommand = { - userId: string; - isInvite: boolean; -}; - -export async function resendVerification(command: resendVerifyEmailCommand) { - return command.isInvite - ? resendInviteCode(command.userId) - : resendEmailCode(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}` }; - } -} diff --git a/apps/login/src/lib/server/idp.ts b/apps/login/src/lib/server/idp.ts index ebb755987e..fb9cf66a4f 100644 --- a/apps/login/src/lib/server/idp.ts +++ b/apps/login/src/lib/server/idp.ts @@ -1,7 +1,14 @@ "use server"; -import { startIdentityProviderFlow } from "@/lib/zitadel"; +import { + getLoginSettings, + getUserByID, + startIdentityProviderFlow, +} from "@/lib/zitadel"; import { headers } from "next/headers"; +import { getNextUrl } from "../client"; +import { checkEmailVerification } from "../verify-helper"; +import { createSessionForIdpAndUpdateCookie } from "./cookie"; export type StartIDPFlowCommand = { idpId: string; @@ -32,3 +39,85 @@ export async function startIDPFlow(command: StartIDPFlowCommand) { } }); } + +type CreateNewSessionCommand = { + userId: string; + idpIntent: { + idpIntentId: string; + idpIntentToken: string; + }; + loginName?: string; + password?: string; + organization?: string; + authRequestId?: string; +}; + +export async function createNewSessionFromIdpIntent( + command: CreateNewSessionCommand, +) { + if (!command.userId || !command.idpIntent) { + throw new Error("No userId or loginName provided"); + } + + const userResponse = await getUserByID(command.userId); + + if (!userResponse || !userResponse.user) { + return { error: "Could not find user" }; + } + + const loginSettings = await getLoginSettings( + userResponse.user.details?.resourceOwner, + ); + + const session = await createSessionForIdpAndUpdateCookie( + command.userId, + command.idpIntent, + command.authRequestId, + loginSettings?.externalLoginCheckLifetime, + ); + + if (!session || !session.factors?.user) { + return { error: "Could not create session" }; + } + + const humanUser = + userResponse.user.type.case === "human" + ? userResponse.user.type.value + : undefined; + + // check to see if user was verified + const emailVerificationCheck = checkEmailVerification( + session, + humanUser, + command.organization, + command.authRequestId, + ); + + if (emailVerificationCheck?.redirect) { + return emailVerificationCheck; + } + + // TODO: check if user has MFA methods + // const mfaFactorCheck = checkMFAFactors(session, loginSettings, authMethods, organization, authRequestId); + // if (mfaFactorCheck?.redirect) { + // return mfaFactorCheck; + // } + + const url = await getNextUrl( + command.authRequestId && session.id + ? { + sessionId: session.id, + authRequestId: command.authRequestId, + organization: session.factors.user.organizationId, + } + : { + loginName: session.factors.user.loginName, + organization: session.factors.user.organizationId, + }, + loginSettings?.defaultRedirectUri, + ); + + if (url) { + return { redirect: url }; + } +} diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index 295f9b455f..54a989f986 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -8,6 +8,7 @@ import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp"; import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; +import { checkInvite } from "../verify-helper"; import { getActiveIdentityProviders, getIDPByID, @@ -171,29 +172,21 @@ export async function sendLoginname(command: SendLoginnameCommand) { ); if (!methods.authMethodTypes || !methods.authMethodTypes.length) { - if ( - potentialUsers[0].type.case === "human" && - potentialUsers[0].type.value.email && - !potentialUsers[0].type.value.email.isVerified - ) { - const paramsVerify = new URLSearchParams({ - loginName: session.factors?.user?.loginName, - userId: session.factors?.user?.id, // verify needs user id - invite: "true", // TODO: check - set this to true as we dont expect old email verification method here - }); + const humanUser = + potentialUsers[0].type.case === "human" + ? potentialUsers[0].type.value + : undefined; - if (command.organization || session.factors?.user?.organizationId) { - paramsVerify.append( - "organization", - command.organization ?? session.factors?.user?.organizationId, - ); - } + // redirect to /verify invite if no auth method is set and email is not verified + const inviteCheck = checkInvite( + session, + humanUser, + session.factors.user.organizationId, + command.authRequestId, + ); - if (command.authRequestId) { - paramsVerify.append("authRequestId", command.authRequestId); - } - - return { redirect: "/verify?" + paramsVerify }; + if (inviteCheck?.redirect) { + return inviteCheck; } const paramsAuthenticatorSetup = new URLSearchParams({ @@ -350,8 +343,9 @@ export async function sendLoginname(command: SendLoginnameCommand) { if (command.authRequestId) { params.set("authRequestId", command.authRequestId); } + if (command.loginName) { - params.set("loginName", command.loginName); + params.set("email", command.loginName); } return { redirect: "/register?" + params }; diff --git a/apps/login/src/lib/server/passkeys.ts b/apps/login/src/lib/server/passkeys.ts index 181962cae1..ca27d310f3 100644 --- a/apps/login/src/lib/server/passkeys.ts +++ b/apps/login/src/lib/server/passkeys.ts @@ -2,18 +2,28 @@ import { createPasskeyRegistrationLink, + getLoginSettings, getSession, + getUserByID, registerPasskey, - verifyPasskeyRegistration, + verifyPasskeyRegistration as zitadelVerifyPasskeyRegistration, } from "@/lib/zitadel"; -import { create } from "@zitadel/client"; +import { create, Duration } from "@zitadel/client"; +import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { RegisterPasskeyResponse, VerifyPasskeyRegistrationRequestSchema, } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { headers } from "next/headers"; import { userAgent } from "next/server"; -import { getSessionCookieById } from "../cookies"; +import { getNextUrl } from "../client"; +import { + getMostRecentSessionCookie, + getSessionCookieById, + getSessionCookieByLoginName, +} from "../cookies"; +import { checkEmailVerification } from "../verify-helper"; +import { setSessionAndUpdateCookie } from "./cookie"; type VerifyPasskeyCommand = { passkeyId: string; @@ -69,7 +79,7 @@ export async function registerPasskeyLink( return registerPasskey(userId, registerLink.code, hostname); } -export async function verifyPasskey(command: VerifyPasskeyCommand) { +export async function verifyPasskeyRegistration(command: VerifyPasskeyCommand) { // if no name is provided, try to generate one from the user agent let passkeyName = command.passkeyName; if (!!!passkeyName) { @@ -95,7 +105,7 @@ export async function verifyPasskey(command: VerifyPasskeyCommand) { throw new Error("Could not get session"); } - return verifyPasskeyRegistration( + return zitadelVerifyPasskeyRegistration( create(VerifyPasskeyRegistrationRequestSchema, { passkeyId: command.passkeyId, publicKeyCredential: command.publicKeyCredential, @@ -104,3 +114,97 @@ export async function verifyPasskey(command: VerifyPasskeyCommand) { }), ); } + +type SendPasskeyCommand = { + loginName?: string; + sessionId?: string; + organization?: string; + checks?: Checks; + authRequestId?: string; + lifetime?: Duration; +}; + +export async function sendPasskey(command: SendPasskeyCommand) { + let { loginName, sessionId, organization, checks, authRequestId } = command; + const recentSession = sessionId + ? await getSessionCookieById({ sessionId }) + : loginName + ? await getSessionCookieByLoginName({ loginName, organization }) + : await getMostRecentSessionCookie(); + + if (!recentSession) { + return { + error: "Could not find session", + }; + } + + const host = (await headers()).get("host"); + + if (!host) { + return { error: "Could not get host" }; + } + + const loginSettings = await getLoginSettings(organization); + + const lifetime = checks?.webAuthN + ? loginSettings?.multiFactorCheckLifetime // TODO different lifetime for webauthn u2f/passkey + : checks?.otpEmail || checks?.otpSms + ? loginSettings?.secondFactorCheckLifetime + : undefined; + + const session = await setSessionAndUpdateCookie( + recentSession, + checks, + undefined, + authRequestId, + lifetime, + ); + + if (!session || !session?.factors?.user?.id) { + return { error: "Could not update session" }; + } + + const userResponse = await getUserByID(session?.factors?.user?.id); + + if (!userResponse.user) { + return { error: "Could not find user" }; + } + + const humanUser = + userResponse.user.type.case === "human" + ? userResponse.user.type.value + : undefined; + + const emailVerificationCheck = checkEmailVerification( + session, + humanUser, + organization, + authRequestId, + ); + + if (emailVerificationCheck?.redirect) { + return emailVerificationCheck; + } + + const url = + authRequestId && session.id + ? await getNextUrl( + { + sessionId: session.id, + authRequestId: authRequestId, + organization: organization, + }, + loginSettings?.defaultRedirectUri, + ) + : session?.factors?.user?.loginName + ? await getNextUrl( + { + loginName: session.factors.user.loginName, + organization: organization, + }, + loginSettings?.defaultRedirectUri, + ) + : null; + + return { redirect: url }; +} diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index 47bf260eaa..3a6805e59e 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -30,6 +30,11 @@ import { import { headers } from "next/headers"; import { getNextUrl } from "../client"; import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies"; +import { + checkEmailVerification, + checkMFAFactors, + checkPasswordChangeRequired, +} from "../verify-helper"; type ResetPasswordCommand = { loginName: string; @@ -134,6 +139,37 @@ export async function sendPassword(command: UpdateSessionCommand) { return { error: "Could not create session for user" }; } + const humanUser = user.type.case === "human" ? user.type.value : undefined; + + // check if the user has to change password first + const passwordChangedCheck = checkPasswordChangeRequired( + session, + humanUser, + command.organization, + command.authRequestId, + ); + + if (passwordChangedCheck?.redirect) { + return passwordChangedCheck; + } + + // throw error if user is in initial state here and do not continue + if (user.state === UserState.INITIAL) { + return { error: "Initial User not supported" }; + } + + // check to see if user was verified + const emailVerificationCheck = checkEmailVerification( + session, + humanUser, + command.organization, + command.authRequestId, + ); + + if (emailVerificationCheck?.redirect) { + return emailVerificationCheck; + } + // if password, check if user has MFA methods let authMethods; if (command.checks && command.checks.password && session.factors?.user?.id) { @@ -145,131 +181,23 @@ export async function sendPassword(command: UpdateSessionCommand) { } } - if (!authMethods || !session.factors?.user?.loginName) { + if (!authMethods) { return { error: "Could not verify password!" }; } - const humanUser = user.type.case === "human" ? user.type.value : undefined; - - // check if the user has to change password first - if (humanUser?.passwordChangeRequired) { - const params = new URLSearchParams({ - loginName: session.factors?.user?.loginName, - }); - - if (command.organization || session.factors?.user?.organizationId) { - params.append("organization", session.factors?.user?.organizationId); - } - - if (command.authRequestId) { - params.append("authRequestId", command.authRequestId); - } - - return { redirect: "/password/change?" + params }; - } - - const availableMultiFactors = authMethods?.filter( - (m: AuthenticationMethodType) => - m !== AuthenticationMethodType.PASSWORD && - m !== AuthenticationMethodType.PASSKEY, + const mfaFactorCheck = checkMFAFactors( + session, + loginSettings, + authMethods, + command.organization, + command.authRequestId, ); - if (availableMultiFactors?.length == 1) { - const params = new URLSearchParams({ - loginName: session.factors?.user.loginName, - }); - - if (command.authRequestId) { - params.append("authRequestId", command.authRequestId); - } - - if (command.organization || session.factors?.user?.organizationId) { - params.append( - "organization", - command.organization ?? session.factors?.user?.organizationId, - ); - } - - const factor = availableMultiFactors[0]; - // if passwordless is other method, but user selected password as alternative, perform a login - if (factor === AuthenticationMethodType.TOTP) { - return { redirect: `/otp/time-based?` + params }; - } else if (factor === AuthenticationMethodType.OTP_SMS) { - return { redirect: `/otp/sms?` + params }; - } else if (factor === AuthenticationMethodType.OTP_EMAIL) { - return { redirect: `/otp/email?` + params }; - } else if (factor === AuthenticationMethodType.U2F) { - return { redirect: `/u2f?` + params }; - } - } else if (availableMultiFactors?.length >= 1) { - const params = new URLSearchParams({ - loginName: session.factors.user.loginName, - }); - - if (command.authRequestId) { - params.append("authRequestId", command.authRequestId); - } - - if (command.organization || session.factors?.user?.organizationId) { - params.append( - "organization", - command.organization ?? session.factors?.user?.organizationId, - ); - } - - return { redirect: `/mfa?` + params }; + if (mfaFactorCheck?.redirect) { + return mfaFactorCheck; } - // TODO: check if handling of userstate INITIAL is needed - else if (user.state === UserState.INITIAL) { - return { error: "Initial User not supported" }; - } else if ( - (loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly) && - !availableMultiFactors.length - ) { - const params = new URLSearchParams({ - loginName: session.factors.user.loginName, - force: "true", // this defines if the mfa is forced in the settings - checkAfter: "true", // this defines if the check is directly made after the setup - }); - if (command.authRequestId) { - params.append("authRequestId", command.authRequestId); - } - - if (command.organization || session.factors?.user?.organizationId) { - params.append( - "organization", - command.organization ?? session.factors?.user?.organizationId, - ); - } - - // TODO: provide a way to setup passkeys on mfa page? - return { redirect: `/mfa/set?` + params }; - } - // TODO: implement passkey setup - - // else if ( - // submitted.factors && - // !submitted.factors.webAuthN && // if session was not verified with a passkey - // promptPasswordless && // if explicitly prompted due policy - // !isAlternative // escaped if password was used as an alternative method - // ) { - // const params = new URLSearchParams({ - // loginName: submitted.factors.user.loginName, - // prompt: "true", - // }); - - // if (authRequestId) { - // params.append("authRequestId", authRequestId); - // } - - // if (organization) { - // params.append("organization", organization); - // } - - // return router.push(`/passkey/set?` + params); - // } - else if (command.authRequestId && session.id) { + if (command.authRequestId && session.id) { const nextUrl = await getNextUrl( { sessionId: session.id, diff --git a/apps/login/src/lib/server/register.ts b/apps/login/src/lib/server/register.ts index 01ddb0d8c8..a73867deb0 100644 --- a/apps/login/src/lib/server/register.ts +++ b/apps/login/src/lib/server/register.ts @@ -1,7 +1,7 @@ "use server"; import { createSessionAndUpdateCookie } from "@/lib/server/cookie"; -import { addHumanUser, getLoginSettings } from "@/lib/zitadel"; +import { addHumanUser, getLoginSettings, getUserByID } from "@/lib/zitadel"; import { create } from "@zitadel/client"; import { Factors } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { @@ -9,6 +9,7 @@ import { ChecksSchema, } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { getNextUrl } from "../client"; +import { checkEmailVerification } from "../verify-helper"; type RegisterUserCommand = { email: string; @@ -25,7 +26,7 @@ export type RegisterUserResponse = { factors: Factors | undefined; }; export async function registerUser(command: RegisterUserCommand) { - const human = await addHumanUser({ + const addResponse = await addHumanUser({ email: command.email, firstName: command.firstName, lastName: command.lastName, @@ -33,14 +34,14 @@ export async function registerUser(command: RegisterUserCommand) { organization: command.organization, }); - if (!human) { + if (!addResponse) { return { error: "Could not create user" }; } const loginSettings = await getLoginSettings(command.organization); let checkPayload: any = { - user: { search: { case: "userId", value: human.userId } }, + user: { search: { case: "userId", value: addResponse.userId } }, }; if (command.password) { @@ -75,6 +76,29 @@ export async function registerUser(command: RegisterUserCommand) { return { redirect: "/passkey/set?" + params }; } else { + const userResponse = await getUserByID(session?.factors?.user?.id); + + if (!userResponse.user) { + return { error: "Could not find user" }; + } + + const humanUser = + userResponse.user.type.case === "human" + ? userResponse.user.type.value + : undefined; + + const emailVerificationCheck = checkEmailVerification( + session, + humanUser, + session.factors.user.organizationId, + command.authRequestId, + //true, // skip send as a mail was send during registration + ); + + if (emailVerificationCheck?.redirect) { + return emailVerificationCheck; + } + const url = await getNextUrl( command.authRequestId && session.id ? { diff --git a/apps/login/src/lib/server/session.ts b/apps/login/src/lib/server/session.ts index 69aac95a10..70bc18f6d5 100644 --- a/apps/login/src/lib/server/session.ts +++ b/apps/login/src/lib/server/session.ts @@ -1,13 +1,9 @@ "use server"; -import { - createSessionForIdpAndUpdateCookie, - setSessionAndUpdateCookie, -} from "@/lib/server/cookie"; +import { setSessionAndUpdateCookie } from "@/lib/server/cookie"; import { deleteSession, getLoginSettings, - getUserByID, listAuthenticationMethodTypes, } from "@/lib/zitadel"; import { Duration } from "@zitadel/client"; @@ -23,62 +19,6 @@ import { removeSessionFromCookie, } from "../cookies"; -type CreateNewSessionCommand = { - userId: string; - idpIntent: { - idpIntentId: string; - idpIntentToken: string; - }; - loginName?: string; - password?: string; - authRequestId?: string; -}; - -export async function createNewSessionForIdp(options: CreateNewSessionCommand) { - const { userId, idpIntent, authRequestId } = options; - - if (!userId || !idpIntent) { - throw new Error("No userId or loginName provided"); - } - - const user = await getUserByID(userId); - - if (!user) { - return { error: "Could not find user" }; - } - - const loginSettings = await getLoginSettings(user.details?.resourceOwner); - - const session = await createSessionForIdpAndUpdateCookie( - userId, - idpIntent, - authRequestId, - loginSettings?.externalLoginCheckLifetime, - ); - - if (!session || !session.factors?.user) { - return { error: "Could not create session" }; - } - - const url = await getNextUrl( - authRequestId && session.id - ? { - sessionId: session.id, - authRequestId: authRequestId, - organization: session.factors.user.organizationId, - } - : { - loginName: session.factors.user.loginName, - organization: session.factors.user.organizationId, - }, - loginSettings?.defaultRedirectUri, - ); - - if (url) { - return { redirect: url }; - } -} - export async function continueWithSession({ authRequestId, ...session diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts new file mode 100644 index 0000000000..61c4bbb806 --- /dev/null +++ b/apps/login/src/lib/server/verify.ts @@ -0,0 +1,342 @@ +"use server"; + +import { + getLoginSettings, + getSession, + getUserByID, + listAuthenticationMethodTypes, + resendEmailCode, + resendInviteCode, + verifyEmail, + verifyInviteCode, +} from "@/lib/zitadel"; +import { create } from "@zitadel/client"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { User } from "@zitadel/proto/zitadel/user/v2/user_pb"; +import { headers } from "next/headers"; +import { getNextUrl } from "../client"; +import { getSessionCookieByLoginName } from "../cookies"; +import { checkMFAFactors } from "../verify-helper"; +import { createSessionAndUpdateCookie } from "./cookie"; + +type VerifyUserByEmailCommand = { + userId: string; + loginName?: string; // to determine already existing session + organization?: string; + code: string; + isInvite: boolean; + authRequestId?: string; +}; + +export async function sendVerification(command: VerifyUserByEmailCommand) { + const verifyResponse = command.isInvite + ? await verifyInviteCode(command.userId, command.code).catch(() => { + return { error: "Could not verify invite" }; + }) + : await verifyEmail(command.userId, command.code).catch(() => { + return { error: "Could not verify email" }; + }); + + if (!verifyResponse) { + return { error: "Could not verify user" }; + } + + let session: Session | undefined; + let user: User | undefined; + + if ("loginName" in command) { + const sessionCookie = await getSessionCookieByLoginName({ + loginName: command.loginName, + organization: command.organization, + }).catch((error) => { + console.warn("Ignored error:", error); + }); + + if (!sessionCookie) { + return { error: "Could not load session cookie" }; + } + + session = await getSession({ + sessionId: sessionCookie.id, + sessionToken: sessionCookie.token, + }).then((response) => { + if (response?.session) { + return response.session; + } + }); + + if (!session?.factors?.user?.id) { + return { error: "Could not create session for user" }; + } + + const userResponse = await getUserByID(session?.factors?.user?.id); + + if (!userResponse?.user) { + return { error: "Could not load user" }; + } + + user = userResponse.user; + } else { + const userResponse = await getUserByID(command.userId); + + if (!userResponse || !userResponse.user) { + return { error: "Could not load user" }; + } + + user = userResponse.user; + + const checks = create(ChecksSchema, { + user: { + search: { + case: "loginName", + value: userResponse.user.preferredLoginName, + }, + }, + }); + + session = await createSessionAndUpdateCookie( + checks, + undefined, + command.authRequestId, + ); + } + + if (!session?.factors?.user?.id) { + return { error: "Could not create session for user" }; + } + + if (!session?.factors?.user?.id) { + return { error: "Could not create session for user" }; + } + + if (!user) { + return { error: "Could not load user" }; + } + + const loginSettings = await getLoginSettings(user.details?.resourceOwner); + + const authMethodResponse = await listAuthenticationMethodTypes(user.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}` }; + } + + // redirect to mfa factor if user has one, or redirect to set one up + const mfaFactorCheck = checkMFAFactors( + session, + loginSettings, + authMethodResponse.authMethodTypes, + command.organization, + command.authRequestId, + ); + + if (mfaFactorCheck?.redirect) { + return mfaFactorCheck; + } + + // login user if no additional steps are required + if (command.authRequestId && session.id) { + const nextUrl = await getNextUrl( + { + sessionId: session.id, + authRequestId: command.authRequestId, + organization: + command.organization ?? session.factors?.user?.organizationId, + }, + loginSettings?.defaultRedirectUri, + ); + + return { redirect: nextUrl }; + } + + const url = await getNextUrl( + { + loginName: session.factors.user.loginName, + organization: session.factors?.user?.organizationId, + }, + loginSettings?.defaultRedirectUri, + ); + + return { redirect: url }; +} + +type resendVerifyEmailCommand = { + userId: string; + isInvite: boolean; + authRequestId?: string; +}; + +export async function resendVerification(command: resendVerifyEmailCommand) { + const host = (await headers()).get("host"); + + return command.isInvite + ? resendInviteCode(command.userId) + : resendEmailCode(command.userId, host, command.authRequestId); +} + +export type SendVerificationRedirectWithoutCheckCommand = { + organization?: string; + authRequestId?: string; +} & ( + | { userId: string; loginName?: never } + | { userId?: never; loginName: string } +); + +export async function sendVerificationRedirectWithoutCheck( + command: SendVerificationRedirectWithoutCheckCommand, +) { + if (!("loginName" in command || "userId" in command)) { + return { error: "No userId, nor loginname provided" }; + } + + let session: Session | undefined; + let user: User | undefined; + + if ("loginName" in command) { + const sessionCookie = await getSessionCookieByLoginName({ + loginName: command.loginName, + organization: command.organization, + }).catch((error) => { + console.warn("Ignored error:", error); + }); + + if (!sessionCookie) { + return { error: "Could not load session cookie" }; + } + + session = await getSession({ + sessionId: sessionCookie.id, + sessionToken: sessionCookie.token, + }).then((response) => { + if (response?.session) { + return response.session; + } + }); + + if (!session?.factors?.user?.id) { + return { error: "Could not create session for user" }; + } + + const userResponse = await getUserByID(session?.factors?.user?.id); + + if (!userResponse?.user) { + return { error: "Could not load user" }; + } + + user = userResponse.user; + } else if ("userId" in command) { + const userResponse = await getUserByID(command.userId); + + if (!userResponse?.user) { + return { error: "Could not load user" }; + } + + user = userResponse.user; + + const checks = create(ChecksSchema, { + user: { + search: { + case: "loginName", + value: userResponse.user.preferredLoginName, + }, + }, + }); + + session = await createSessionAndUpdateCookie( + checks, + undefined, + command.authRequestId, + ); + } + + if (!session?.factors?.user?.id) { + return { error: "Could not create session for user" }; + } + + if (!session?.factors?.user?.id) { + return { error: "Could not create session for user" }; + } + + if (!user) { + return { error: "Could not load user" }; + } + + const authMethodResponse = await listAuthenticationMethodTypes(user.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}` }; + } + + const loginSettings = await getLoginSettings(user.details?.resourceOwner); + + // redirect to mfa factor if user has one, or redirect to set one up + const mfaFactorCheck = checkMFAFactors( + session, + loginSettings, + authMethodResponse.authMethodTypes, + command.organization, + command.authRequestId, + ); + + if (mfaFactorCheck?.redirect) { + return mfaFactorCheck; + } + + // login user if no additional steps are required + if (command.authRequestId && session.id) { + const nextUrl = await getNextUrl( + { + sessionId: session.id, + authRequestId: command.authRequestId, + organization: + command.organization ?? session.factors?.user?.organizationId, + }, + loginSettings?.defaultRedirectUri, + ); + + return { redirect: nextUrl }; + } + + const url = await getNextUrl( + { + loginName: session.factors.user.loginName, + organization: session.factors?.user?.organizationId, + }, + loginSettings?.defaultRedirectUri, + ); + + return { redirect: url }; +} diff --git a/apps/login/src/lib/verify-helper.ts b/apps/login/src/lib/verify-helper.ts new file mode 100644 index 0000000000..85b5ca68e4 --- /dev/null +++ b/apps/login/src/lib/verify-helper.ts @@ -0,0 +1,199 @@ +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { HumanUser } from "@zitadel/proto/zitadel/user/v2/user_pb"; +import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; + +export function checkPasswordChangeRequired( + session: Session, + humanUser: HumanUser | undefined, + organization?: string, + authRequestId?: string, +) { + if (humanUser?.passwordChangeRequired) { + const params = new URLSearchParams({ + loginName: session.factors?.user?.loginName as string, + }); + + if (organization || session.factors?.user?.organizationId) { + params.append( + "organization", + session.factors?.user?.organizationId as string, + ); + } + + if (authRequestId) { + params.append("authRequestId", authRequestId); + } + + return { redirect: "/password/change?" + params }; + } +} + +export function checkInvite( + session: Session, + humanUser?: HumanUser, + organization?: string, + authRequestId?: string, +) { + if (humanUser?.email && humanUser.email.isVerified) { + const paramsVerify = new URLSearchParams({ + loginName: session.factors?.user?.loginName as string, + userId: session.factors?.user?.id as string, // verify needs user id + invite: "true", // TODO: check - set this to true as we dont expect old email verification method here + }); + + if (organization || session.factors?.user?.organizationId) { + paramsVerify.append( + "organization", + organization ?? (session.factors?.user?.organizationId as string), + ); + } + + if (authRequestId) { + paramsVerify.append("authRequestId", authRequestId); + } + + return { redirect: "/verify?" + paramsVerify }; + } +} + +export function checkEmailVerification( + session: Session, + humanUser?: HumanUser, + organization?: string, + authRequestId?: string, + skipSend?: boolean, +) { + if ( + !humanUser?.email?.isVerified && + process.env.EMAIL_VERIFICATION === "true" + ) { + const params = new URLSearchParams({ + loginName: session.factors?.user?.loginName as string, + }); + + if (authRequestId) { + params.append("authRequestId", authRequestId); + } + + if (organization || session.factors?.user?.organizationId) { + params.append( + "organization", + organization ?? (session.factors?.user?.organizationId as string), + ); + } + + if (skipSend) { + params.append("skipsend", "true"); + } + + return { redirect: `/verify?` + params }; + } +} + +export function checkMFAFactors( + session: Session, + loginSettings: LoginSettings | undefined, + authMethods: AuthenticationMethodType[], + organization?: string, + authRequestId?: string, +) { + const availableMultiFactors = authMethods?.filter( + (m: AuthenticationMethodType) => + m !== AuthenticationMethodType.PASSWORD && + m !== AuthenticationMethodType.PASSKEY, + ); + + if (availableMultiFactors?.length == 1) { + const params = new URLSearchParams({ + loginName: session.factors?.user?.loginName as string, + }); + + if (authRequestId) { + params.append("authRequestId", authRequestId); + } + + if (organization || session.factors?.user?.organizationId) { + params.append( + "organization", + organization ?? (session.factors?.user?.organizationId as string), + ); + } + + const factor = availableMultiFactors[0]; + // if passwordless is other method, but user selected password as alternative, perform a login + if (factor === AuthenticationMethodType.TOTP) { + return { redirect: `/otp/time-based?` + params }; + } else if (factor === AuthenticationMethodType.OTP_SMS) { + return { redirect: `/otp/sms?` + params }; + } else if (factor === AuthenticationMethodType.OTP_EMAIL) { + return { redirect: `/otp/email?` + params }; + } else if (factor === AuthenticationMethodType.U2F) { + return { redirect: `/u2f?` + params }; + } + } else if (availableMultiFactors?.length >= 1) { + const params = new URLSearchParams({ + loginName: session.factors?.user?.loginName as string, + }); + + if (authRequestId) { + params.append("authRequestId", authRequestId); + } + + if (organization || session.factors?.user?.organizationId) { + params.append( + "organization", + organization ?? (session.factors?.user?.organizationId as string), + ); + } + + return { redirect: `/mfa?` + params }; + } else if ( + (loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly) && + !availableMultiFactors.length + ) { + const params = new URLSearchParams({ + loginName: session.factors?.user?.loginName as string, + force: "true", // this defines if the mfa is forced in the settings + checkAfter: "true", // this defines if the check is directly made after the setup + }); + + if (authRequestId) { + params.append("authRequestId", authRequestId); + } + + if (organization || session.factors?.user?.organizationId) { + params.append( + "organization", + organization ?? (session.factors?.user?.organizationId as string), + ); + } + + // TODO: provide a way to setup passkeys on mfa page? + return { redirect: `/mfa/set?` + params }; + } + + // TODO: implement passkey setup + + // else if ( + // submitted.factors && + // !submitted.factors.webAuthN && // if session was not verified with a passkey + // promptPasswordless && // if explicitly prompted due policy + // !isAlternative // escaped if password was used as an alternative method + // ) { + // const params = new URLSearchParams({ + // loginName: submitted.factors.user.loginName, + // prompt: "true", + // }); + + // if (authRequestId) { + // params.append("authRequestId", authRequestId); + // } + + // if (organization) { + // params.append("organization", organization); + // } + + // return router.push(`/passkey/set?` + params); + // } +} diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index aaf0a7c05b..8cf756807b 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -12,6 +12,8 @@ import { RequestChallenges } from "@zitadel/proto/zitadel/session/v2/challenge_p import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { AddHumanUserRequest, + ResendEmailCodeRequest, + ResendEmailCodeRequestSchema, RetrieveIdentityProviderIntentRequest, SetPasswordRequest, SetPasswordRequestSchema, @@ -23,6 +25,7 @@ import { create, Duration } from "@zitadel/client"; import { TextQueryMethod } from "@zitadel/proto/zitadel/object/v2/object_pb"; import { CreateCallbackRequest } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; +import { SendEmailVerificationCodeSchema } from "@zitadel/proto/zitadel/user/v2/email_pb"; import type { RedirectURLsJson } from "@zitadel/proto/zitadel/user/v2/idp_pb"; import { NotificationType, @@ -448,13 +451,28 @@ export async function verifyEmail(userId: string, verificationCode: string) { ); } -export async function resendEmailCode(userId: string) { - return userService.resendEmailCode( - { - userId, - }, - {}, - ); +export async function resendEmailCode( + userId: string, + host: string | null, + authRequestId?: string, +) { + let request: ResendEmailCodeRequest = create(ResendEmailCodeRequestSchema, { + userId, + }); + + if (host) { + const medium = create(SendEmailVerificationCodeSchema, { + urlTemplate: + `${host.includes("localhost") ? "http://" : "https://"}${host}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + + (authRequestId ? `&authRequestId=${authRequestId}` : ""), + }); + + request = { ...request, verification: { case: "sendCode", value: medium } }; + } + + console.log(request); + + return userService.resendEmailCode(request, {}); } export function retrieveIDPIntent(id: string, token: string) { diff --git a/packages/zitadel-client/src/index.ts b/packages/zitadel-client/src/index.ts index 7cf14163bf..64c3af5050 100644 --- a/packages/zitadel-client/src/index.ts +++ b/packages/zitadel-client/src/index.ts @@ -3,5 +3,6 @@ export { NewAuthorizationBearerInterceptor } from "./interceptors"; // TODO: Move this to `./protobuf.ts` and export it from there export { create, fromJson, toJson } from "@bufbuild/protobuf"; +export type { JsonObject } from "@bufbuild/protobuf"; export { TimestampSchema, timestampDate, timestampFromDate, timestampFromMs, timestampMs } from "@bufbuild/protobuf/wkt"; export type { Duration, Timestamp } from "@bufbuild/protobuf/wkt"; diff --git a/packages/zitadel-tsconfig/package.json b/packages/zitadel-tsconfig/package.json index 238d7fbd70..b20542f468 100644 --- a/packages/zitadel-tsconfig/package.json +++ b/packages/zitadel-tsconfig/package.json @@ -2,7 +2,6 @@ "name": "@zitadel/tsconfig", "version": "0.0.0", "private": true, - "type": "module", "license": "MIT", "publishConfig": { "access": "public" diff --git a/turbo.json b/turbo.json index a98ff8726d..2817c8c157 100644 --- a/turbo.json +++ b/turbo.json @@ -12,7 +12,7 @@ "ZITADEL_SYSTEM_API_KEY", "ZITADEL_ISSUER", "ZITADEL_ADMIN_TOKEN", - "CACHE_REVALIDATION_INTERVAL_IN_SECONDS", + "EMAIL_VERIFICATION", "VERCEL_URL" ], "tasks": {