self service change password

This commit is contained in:
peintnermax
2024-10-16 18:11:54 +02:00
parent 77c00fb666
commit bc3e09ed0c
9 changed files with 178 additions and 103 deletions

View File

@@ -26,6 +26,11 @@
"codeSent": "Ein Code wurde an Ihre E-Mail-Adresse gesendet.",
"resend": "Erneut senden",
"submit": "Weiter"
},
"change": {
"title": "Passwort ändern",
"description": "Legen Sie das Passwort für Ihr Konto fest",
"submit": "Weiter"
}
},
"idp": {

View File

@@ -26,6 +26,11 @@
"codeSent": "A code has been sent to your email address.",
"resend": "Resend code",
"submit": "Continue"
},
"change": {
"title": "Change Password",
"description": "Set the password for your account",
"submit": "Continue"
}
},
"idp": {

View File

@@ -26,6 +26,11 @@
"codeSent": "Se ha enviado un código a su correo electrónico.",
"resend": "Reenviar código",
"submit": "Continuar"
},
"change": {
"title": "Cambiar Contraseña",
"description": "Establece la contraseña para tu cuenta",
"submit": "Continuar"
}
},
"idp": {

View File

@@ -26,6 +26,11 @@
"codeSent": "Un codice è stato inviato al tuo indirizzo email.",
"resend": "Invia di nuovo",
"submit": "Continua"
},
"change": {
"title": "Cambia Password",
"description": "Imposta la password per il tuo account",
"submit": "Continua"
}
},
"idp": {

View File

@@ -1,78 +0,0 @@
import { Alert } from "@/components/alert";
import { ChangePasswordForm } from "@/components/change-password-form";
import { DynamicTheme } from "@/components/dynamic-theme";
import { UserAvatar } from "@/components/user-avatar";
import { getSessionCookieById } from "@/lib/cookies";
import {
getBrandingSettings,
getPasswordComplexitySettings,
getSession,
} from "@/lib/zitadel";
export default async function Page({
searchParams,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
}) {
const { sessionId } = searchParams;
if (!sessionId) {
return (
<div>
<h1>Session ID not found</h1>
</div>
);
}
const sessionCookie = await getSessionCookieById({
sessionId,
});
const { session } = await getSession({
sessionId: sessionCookie.id,
sessionToken: sessionCookie.token,
});
const passwordComplexitySettings = await getPasswordComplexitySettings(
session?.factors?.user?.organizationId,
);
const branding = await getBrandingSettings(
session?.factors?.user?.organizationId,
);
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>Set Password</h1>
<p className="ztdl-p">Set the password for your account</p>
{!session && (
<div className="py-4">
<Alert>
Could not get the context of the user. Make sure to enter the
username first or provide a loginName as searchParam.
</Alert>
</div>
)}
{session && (
<UserAvatar
loginName={session.factors?.user?.loginName}
displayName={session.factors?.user?.displayName}
showDropdown
searchParams={searchParams}
></UserAvatar>
)}
{passwordComplexitySettings && session?.factors?.user?.id && (
<ChangePasswordForm
passwordComplexitySettings={passwordComplexitySettings}
userId={session.factors.user.id}
sessionId={sessionId}
></ChangePasswordForm>
)}
</div>
</DynamicTheme>
);
}

View File

@@ -0,0 +1,80 @@
import { Alert } from "@/components/alert";
import { ChangePasswordForm } from "@/components/change-password-form";
import { DynamicTheme } from "@/components/dynamic-theme";
import { UserAvatar } from "@/components/user-avatar";
import { loadMostRecentSession } from "@/lib/session";
import {
getBrandingSettings,
getLoginSettings,
getPasswordComplexitySettings,
} from "@/lib/zitadel";
import { getLocale, getTranslations } from "next-intl/server";
export default async function Page({
searchParams,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
}) {
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "password" });
const { loginName, organization, authRequestId, code } = searchParams;
// also allow no session to be found (ignoreUnkownUsername)
const sessionFactors = await loadMostRecentSession({
loginName,
organization,
});
const branding = await getBrandingSettings(organization);
const passwordComplexity = await getPasswordComplexitySettings(
sessionFactors?.factors?.user?.organizationId,
);
const loginSettings = await getLoginSettings(organization);
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>
{sessionFactors?.factors?.user?.displayName ?? t("change.title")}
</h1>
<p className="ztdl-p mb-6 block">{t("change.description")}</p>
{/* show error only if usernames should be shown to be unknown */}
{(!sessionFactors || !loginName) &&
!loginSettings?.ignoreUnknownUsernames && (
<div className="py-4">
<Alert>{t("error:unknownContext")}</Alert>
</div>
)}
{sessionFactors && (
<UserAvatar
loginName={loginName ?? sessionFactors.factors?.user?.loginName}
displayName={sessionFactors.factors?.user?.displayName}
showDropdown
searchParams={searchParams}
></UserAvatar>
)}
{passwordComplexity &&
loginName &&
sessionFactors?.factors?.user?.id ? (
<ChangePasswordForm
sessionId={sessionFactors.id}
loginName={loginName}
authRequestId={authRequestId}
organization={organization}
passwordComplexitySettings={passwordComplexity}
/>
) : (
<div className="py-4">
<Alert>{t("error:failedLoading")}</Alert>
</div>
)}
</div>
</DynamicTheme>
);
}

View File

@@ -6,8 +6,9 @@ import {
symbolValidator,
upperCaseValidator,
} from "@/helpers/validators";
import { setPassword } from "@/lib/self";
import { setMyPassword } from "@/lib/self";
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";
@@ -27,15 +28,21 @@ type Inputs =
type Props = {
passwordComplexitySettings: PasswordComplexitySettings;
userId: string;
sessionId: string;
loginName: string;
authRequestId?: string;
organization?: string;
};
export function ChangePasswordForm({
passwordComplexitySettings,
userId,
sessionId,
loginName,
authRequestId,
organization,
}: Props) {
const t = useTranslations("password");
const { register, handleSubmit, watch, formState } = useForm<Inputs>({
mode: "onBlur",
defaultValues: {
@@ -51,9 +58,8 @@ export function ChangePasswordForm({
async function submitChange(values: Inputs) {
setLoading(true);
const response = await setPassword({
const response = await setMyPassword({
sessionId: sessionId,
userId: userId,
password: values.password,
}).catch(() => {
setError("Could not change password");
@@ -61,12 +67,36 @@ export function ChangePasswordForm({
setLoading(false);
if (response && "error" in response) {
setError(response.error);
return;
}
if (!response) {
setError("Could not change password");
return;
}
return response;
const params = new URLSearchParams({});
if (loginName) {
params.append("loginName", loginName);
}
if (organization) {
params.append("organization", organization);
}
if (authRequestId && sessionId) {
if (authRequestId) {
params.append("authRequest", authRequestId);
}
return router.push(`/login?` + params);
} else {
if (authRequestId) {
params.append("authRequestId", authRequestId);
}
return router.push(`/signedin?` + params);
}
}
const { errors } = formState;
@@ -99,9 +129,9 @@ export function ChangePasswordForm({
autoComplete="new-password"
required
{...register("password", {
required: "You have to provide a password!",
required: "You have to provide a new password!",
})}
label="Password"
label="New Password"
error={errors.password?.message as string}
/>
</div>
@@ -143,7 +173,7 @@ export function ChangePasswordForm({
onClick={handleSubmit(submitChange)}
>
{loading && <Spinner className="h-5 w-5 mr-2" />}
continue
{t("change.submit")}
</Button>
</div>
</form>

View File

@@ -61,15 +61,17 @@ export function PasswordForm({
}),
authRequestId,
}).catch(() => {
setLoading(false);
setError("Could not verify password");
return;
});
setLoading(false);
if (response && "error" in response && response.error) {
setError(response.error);
}
setLoading(false);
return response;
}

View File

@@ -6,6 +6,7 @@ import {
} from "@zitadel/client/v2";
import { createServerTransport } from "@zitadel/node";
import { getSessionCookieById } from "./cookies";
import { getSession } from "./zitadel";
const transport = (token: string) =>
createServerTransport(token, {
@@ -19,26 +20,46 @@ const sessionService = (sessionId: string) => {
});
};
const userService = (sessionId: string) => {
return getSessionCookieById({ sessionId }).then((session) => {
return createUserServiceClient(transport(session.token));
});
const myUserService = (sessionToken: string) => {
return createUserServiceClient(transport(sessionToken));
};
export async function setPassword({
export async function setMyPassword({
sessionId,
userId,
password,
}: {
sessionId: string;
userId: string;
password: string;
}) {
return (await userService(sessionId)).setPassword(
{
userId,
newPassword: { password, changeRequired: false },
},
{},
);
const sessionCookie = await getSessionCookieById({ sessionId });
const { session } = await getSession({
sessionId: sessionCookie.id,
sessionToken: sessionCookie.token,
});
if (!session) {
return { error: "Could not load session" };
}
const service = await myUserService(sessionCookie.token);
if (!session?.factors?.user?.id) {
return { error: "No user id found in session" };
}
return service
.setPassword(
{
userId: session.factors.user.id,
newPassword: { password, changeRequired: false },
},
{},
)
.catch((error) => {
if (error.code === 7) {
return { error: "Session is not valid." };
}
throw error;
});
}