diff --git a/apps/login/src/app/(login)/mfa/page.tsx b/apps/login/src/app/(login)/mfa/page.tsx index 38b3e57cf9b..f61aa4d988f 100644 --- a/apps/login/src/app/(login)/mfa/page.tsx +++ b/apps/login/src/app/(login)/mfa/page.tsx @@ -19,6 +19,7 @@ export default async function Page({ }) { const locale = getLocale(); const t = await getTranslations({ locale, namespace: "mfa" }); + const tError = await getTranslations({ locale, namespace: "error" }); const { loginName, authRequestId, organization, sessionId } = searchParams; @@ -84,9 +85,7 @@ export default async function Page({ > )} - {!(loginName || sessionId) && ( - {t("error:unknownContext")} - )} + {!(loginName || sessionId) && {tError("unknownContext")}} {sessionFactors ? ( )} - {!(loginName || sessionId) && ( - {t("error:unknownContext")} - )} + {!(loginName || sessionId) && {tError("unknownContext")}} - {!valid && {t("error.sessionExpired")}} + {!valid && {tError("sessionExpired")}} {isSessionValid(sessionWithData).valid && loginSettings && diff --git a/apps/login/src/app/(login)/otp/[method]/page.tsx b/apps/login/src/app/(login)/otp/[method]/page.tsx index d1071dbcad5..7109d58f115 100644 --- a/apps/login/src/app/(login)/otp/[method]/page.tsx +++ b/apps/login/src/app/(login)/otp/[method]/page.tsx @@ -15,6 +15,7 @@ export default async function Page({ }) { const locale = getLocale(); const t = await getTranslations({ locale, namespace: "otp" }); + const tError = await getTranslations({ locale, namespace: "error" }); const { loginName, authRequestId, sessionId, organization, code, submit } = searchParams; @@ -44,7 +45,7 @@ export default async function Page({ {!session && (
- {t("error:unknownContext")} + {tError("unknownContext")}
)} diff --git a/apps/login/src/app/(login)/otp/[method]/set/page.tsx b/apps/login/src/app/(login)/otp/[method]/set/page.tsx index 7a9c28cbbde..b08a1facdee 100644 --- a/apps/login/src/app/(login)/otp/[method]/set/page.tsx +++ b/apps/login/src/app/(login)/otp/[method]/set/page.tsx @@ -25,6 +25,7 @@ export default async function Page({ }) { const locale = getLocale(); const t = await getTranslations({ locale, namespace: "otp" }); + const tError = await getTranslations({ locale, namespace: "error" }); const { loginName, organization, sessionId, authRequestId, checkAfter } = searchParams; @@ -105,7 +106,7 @@ export default async function Page({

{t("set.title")}

{!session && (
- {t("error:unknownContext")} + {tError("unknownContext")}
)} diff --git a/apps/login/src/app/(login)/passkey/page.tsx b/apps/login/src/app/(login)/passkey/page.tsx index 22a195673b7..fc0f701e805 100644 --- a/apps/login/src/app/(login)/passkey/page.tsx +++ b/apps/login/src/app/(login)/passkey/page.tsx @@ -14,6 +14,7 @@ export default async function Page({ }) { const locale = getLocale(); const t = await getTranslations({ locale, namespace: "passkey" }); + const tError = await getTranslations({ locale, namespace: "error" }); const { loginName, altPassword, authRequestId, organization, sessionId } = searchParams; @@ -51,9 +52,7 @@ export default async function Page({ )}

{t("verify.description")}

- {!(loginName || sessionId) && ( - {t("error:unknownContext")} - )} + {!(loginName || sessionId) && {tError("unknownContext")}} {(loginName || sessionId) && ( - {t("error:unknownContext")} + {tError("unknownContext")} )} diff --git a/apps/login/src/app/(login)/password/change/page.tsx b/apps/login/src/app/(login)/password/change/page.tsx index cf8a970cda4..006d2a15b5c 100644 --- a/apps/login/src/app/(login)/password/change/page.tsx +++ b/apps/login/src/app/(login)/password/change/page.tsx @@ -17,6 +17,7 @@ export default async function Page({ }) { const locale = getLocale(); const t = await getTranslations({ locale, namespace: "password" }); + const tError = await getTranslations({ locale, namespace: "error" }); const { loginName, organization, authRequestId, code } = searchParams; @@ -46,7 +47,7 @@ export default async function Page({ {(!sessionFactors || !loginName) && !loginSettings?.ignoreUnknownUsernames && (
- {t("error:unknownContext")} + {tError("unknownContext")}
)} @@ -71,7 +72,7 @@ export default async function Page({ /> ) : (
- {t("error:failedLoading")} + {tError("failedLoading")}
)} diff --git a/apps/login/src/app/(login)/password/page.tsx b/apps/login/src/app/(login)/password/page.tsx index 43cb969dc35..1f752850e63 100644 --- a/apps/login/src/app/(login)/password/page.tsx +++ b/apps/login/src/app/(login)/password/page.tsx @@ -14,6 +14,7 @@ export default async function Page({ }) { const locale = getLocale(); const t = await getTranslations({ locale, namespace: "password" }); + const tError = await getTranslations({ locale, namespace: "error" }); const { loginName, organization, authRequestId, alt } = searchParams; @@ -44,7 +45,7 @@ export default async function Page({ {(!sessionFactors || !loginName) && !loginSettings?.ignoreUnknownUsernames && (
- {t("error:unknownContext")} + {tError("unknownContext")}
)} diff --git a/apps/login/src/app/(login)/password/set/page.tsx b/apps/login/src/app/(login)/password/set/page.tsx index e99f79ef923..8e0a8a69932 100644 --- a/apps/login/src/app/(login)/password/set/page.tsx +++ b/apps/login/src/app/(login)/password/set/page.tsx @@ -7,7 +7,10 @@ import { getBrandingSettings, getLoginSettings, getPasswordComplexitySettings, + getUserByID, } from "@/lib/zitadel"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { getLocale, getTranslations } from "next-intl/server"; export default async function Page({ @@ -17,62 +20,83 @@ export default async function Page({ }) { const locale = getLocale(); const t = await getTranslations({ locale, namespace: "password" }); + const tError = await getTranslations({ locale, namespace: "error" }); - const { loginName, organization, authRequestId, code } = searchParams; + const { userId, loginName, organization, authRequestId, code } = searchParams; // also allow no session to be found (ignoreUnkownUsername) - const sessionFactors = await loadMostRecentSession({ - loginName, - organization, - }); + let session: Session | undefined; + if (loginName) { + session = await loadMostRecentSession({ + loginName, + organization, + }); + } const branding = await getBrandingSettings(organization); const passwordComplexity = await getPasswordComplexitySettings( - sessionFactors?.factors?.user?.organizationId, + session?.factors?.user?.organizationId, ); const loginSettings = await getLoginSettings(organization); + let user: User | undefined; + let displayName: string | undefined; + if (userId) { + const userResponse = await getUserByID(userId); + user = userResponse.user; + + if (user?.type.case === "human") { + displayName = (user.type.value as HumanUser).profile?.displayName; + } + } + return (
-

{sessionFactors?.factors?.user?.displayName ?? t("set.title")}

+

{session?.factors?.user?.displayName ?? t("set.title")}

{t("set.description")}

{/* show error only if usernames should be shown to be unknown */} - {(!sessionFactors || !loginName) && - !loginSettings?.ignoreUnknownUsernames && ( -
- {t("error:unknownContext")} -
- )} + {loginName && !session && !loginSettings?.ignoreUnknownUsernames && ( +
+ {tError("unknownContext")} +
+ )} - {sessionFactors && ( + {session ? ( - )} + ) : user ? ( + + ) : null} {t("set.codeSent")} {passwordComplexity && - loginName && - sessionFactors?.factors?.user?.id ? ( + (loginName ?? user?.preferredLoginName) && + (userId ?? session?.factors?.user?.id) ? ( ) : (
- {t("error:failedLoading")} + {tError("failedLoading")}
)}
diff --git a/apps/login/src/app/(login)/u2f/page.tsx b/apps/login/src/app/(login)/u2f/page.tsx index 60a661f60c1..36fef6d7869 100644 --- a/apps/login/src/app/(login)/u2f/page.tsx +++ b/apps/login/src/app/(login)/u2f/page.tsx @@ -14,6 +14,7 @@ export default async function Page({ }) { const locale = getLocale(); const t = await getTranslations({ locale, namespace: "u2f" }); + const tError = await getTranslations({ locale, namespace: "error" }); const { loginName, authRequestId, sessionId, organization } = searchParams; @@ -50,9 +51,7 @@ export default async function Page({ )}

{t("verify.description")}

- {!(loginName || sessionId) && ( - {t("error:unknownContext")} - )} + {!(loginName || sessionId) && {tError("unknownContext")}} {(loginName || sessionId) && ( - {t("error:unknownContext")} + {tError("unknownContext")} )} diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index 6ee00a30f84..07e00e7a080 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -8,6 +8,7 @@ import { getLocale, getTranslations } from "next-intl/server"; export default async function Page({ searchParams }: { searchParams: any }) { const locale = getLocale(); const t = await getTranslations({ locale, namespace: "verify" }); + const tError = await getTranslations({ locale, namespace: "error" }); const { userId, @@ -31,7 +32,7 @@ export default async function Page({ searchParams }: { searchParams: any }) { {!userId && (
- {t("error:unknownContext")} + {tError("unknownContext")}
)} diff --git a/apps/login/src/components/login-otp.tsx b/apps/login/src/components/login-otp.tsx index 8c370a4aef2..01cabedc2eb 100644 --- a/apps/login/src/components/login-otp.tsx +++ b/apps/login/src/components/login-otp.tsx @@ -139,9 +139,10 @@ export function LoginOTP({ organization, checks, authRequestId, - }).catch((error) => { - setError(error.message ?? "Could not verify OTP code"); + }).catch(() => { + setError("Could not verify OTP code"); setLoading(false); + return; }); setLoading(false); @@ -151,39 +152,41 @@ export function LoginOTP({ function setCodeAndContinue(values: Inputs, organization?: string) { return submitCode(values, organization).then((response) => { - if (authRequestId && response && response.sessionId) { - const params = new URLSearchParams({ - sessionId: response.sessionId, - authRequest: authRequestId, - }); + if (response) { + if (authRequestId && response && response.sessionId) { + const params = new URLSearchParams({ + sessionId: response.sessionId, + authRequest: authRequestId, + }); - if (organization) { - params.append("organization", organization); - } + if (organization) { + params.append("organization", organization); + } - if (authRequestId) { - params.append("authRequest", authRequestId); - } + if (authRequestId) { + params.append("authRequest", authRequestId); + } - if (sessionId) { - params.append("sessionId", sessionId); - } + if (sessionId) { + params.append("sessionId", sessionId); + } - return router.push(`/login?` + params); - } else { - const params = new URLSearchParams(); - if (response?.factors?.user?.loginName) { - params.append("loginName", response.factors.user.loginName); - } - if (authRequestId) { - params.append("authRequestId", authRequestId); - } + return router.push(`/login?` + params); + } else { + const params = new URLSearchParams(); + if (response?.factors?.user?.loginName) { + params.append("loginName", response.factors.user.loginName); + } + if (authRequestId) { + params.append("authRequestId", authRequestId); + } - if (organization) { - params.append("organization", organization); - } + if (organization) { + params.append("organization", organization); + } - return router.push(`/signedin?` + params); + return router.push(`/signedin?` + params); + } } }); } diff --git a/apps/login/src/components/password-form.tsx b/apps/login/src/components/password-form.tsx index bca61bcd954..98b0ec853ff 100644 --- a/apps/login/src/components/password-form.tsx +++ b/apps/login/src/components/password-form.tsx @@ -85,13 +85,13 @@ export function PasswordForm({ organization, }).catch(() => { setError("Could not reset password"); + return; }); setLoading(false); if (response && "error" in response) { setError(response.error); - return; } diff --git a/apps/login/src/components/set-password-form.tsx b/apps/login/src/components/set-password-form.tsx index ead010484d3..d32fabcd5a0 100644 --- a/apps/login/src/components/set-password-form.tsx +++ b/apps/login/src/components/set-password-form.tsx @@ -11,7 +11,6 @@ import { create } from "@zitadel/client"; import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb"; import { useTranslations } from "next-intl"; -import { useRouter } from "next/navigation"; import { useState } from "react"; import { FieldValues, useForm } from "react-hook-form"; import { Alert } from "./alert"; @@ -58,8 +57,6 @@ export function SetPasswordForm({ const [loading, setLoading] = useState(false); const [error, setError] = useState(""); - const router = useRouter(); - async function submitRegister(values: Inputs) { setLoading(true); const changeResponse = await changePassword({ diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index 3db467b0f30..c1467e6b39d 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -18,6 +18,7 @@ import { } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { User, UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { headers } from "next/headers"; import { redirect } from "next/navigation"; import { getSessionCookieByLoginName } from "../cookies"; @@ -27,6 +28,8 @@ type ResetPasswordCommand = { }; export async function resetPassword(command: ResetPasswordCommand) { + const host = headers().get("host"); + const users = await listUsers({ loginName: command.loginName, organizationId: command.organization, @@ -41,7 +44,7 @@ export async function resetPassword(command: ResetPasswordCommand) { } const userId = users.result[0].userId; - return passwordReset(userId); + return passwordReset(userId, host); } export type UpdateSessionCommand = { diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index 846552ddda2..e6ff00f4dc8 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -28,7 +28,10 @@ import { } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { PasswordComplexitySettingsSchema } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb"; import type { RedirectURLsJson } from "@zitadel/proto/zitadel/user/v2/idp_pb"; -import { NotificationType } from "@zitadel/proto/zitadel/user/v2/password_pb"; +import { + NotificationType, + SendPasswordResetLinkSchema, +} from "@zitadel/proto/zitadel/user/v2/password_pb"; import { SearchQuery, SearchQuerySchema, @@ -492,15 +495,24 @@ export function createUser( * @param userId the id of the user where the email should be set * @returns the newly set email */ -export async function passwordReset(userId: string) { +export async function passwordReset(userId: string, host: string | null) { + let medium = create(SendPasswordResetLinkSchema, { + notificationType: NotificationType.Email, + }); + + if (host) { + medium = { + ...medium, + urlTemplate: `https://${host}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}`, + }; + } + return userService.passwordReset( { userId, medium: { case: "sendLink", - value: { - notificationType: NotificationType.Email, - }, + value: medium, }, }, {},