tests, session using timestamp

This commit is contained in:
peintnermax
2024-10-23 14:28:33 +02:00
parent cde5f6cbd0
commit 52ce9219bb
21 changed files with 232 additions and 116 deletions

View File

@@ -1,10 +1,11 @@
import { stub } from "../support/mock";
describe("/verify", () => {
it("redirects after successful email verification", () => {
it("shows password and passkey method after successful invite verification", () => {
stub("zitadel.user.v2.UserService", "VerifyEmail");
cy.visit("/verify?userId=123&code=abc&submit=true");
cy.location("pathname", { timeout: 10_000 }).should("eq", "/loginname");
cy.visit("/verify?userId=123&code=abc&submit=true&invite=true");
cy.contains("Password");
cy.contains("Passkey");
});
it("shows an error if validation failed", () => {
stub("zitadel.user.v2.UserService", "VerifyEmail", {
@@ -14,6 +15,6 @@ describe("/verify", () => {
// TODO: Avoid uncaught exception in application
cy.once("uncaught:exception", () => false);
cy.visit("/verify?userId=123&code=abc&submit=true");
cy.contains("Could not verify email");
cy.contains("Could not verify user");
});
});

View File

@@ -139,6 +139,7 @@
"title": "Benutzer einladen",
"description": "Geben Sie die E-Mail-Adresse des Benutzers ein, den Sie einladen möchten.",
"info": "Der Benutzer erhält eine E-Mail mit einem Link, um sich zu registrieren.",
"notAllowed": "Sie haben keine Berechtigung, Benutzer einzuladen.",
"submit": "Einladen",
"success": {
"title": "Einladung erfolgreich",
@@ -153,11 +154,17 @@
"description": "Sie sind angemeldet."
},
"verify": {
"title": "Benutzer verifizieren",
"description": "Geben Sie den Code ein, der in der Bestätigungs-E-Mail angegeben ist.",
"userIdMissing": "Keine Benutzer-ID angegeben!",
"resendCode": "Code erneut senden",
"submit": "Weiter"
"verify": {
"title": "Benutzer verifizieren",
"description": "Geben Sie den Code ein, der in der Bestätigungs-E-Mail angegeben ist.",
"resendCode": "Code erneut senden",
"submit": "Weiter"
},
"setup": {
"title": "Authentifizierungsmethode auswählen",
"description": "Wählen Sie die Methode, mit der Sie sich authentifizieren möchten."
}
},
"error": {
"unknownContext": "Der Kontext des Benutzers konnte nicht ermittelt werden. Stellen Sie sicher, dass Sie zuerst den Benutzernamen eingeben oder einen loginName als Suchparameter angeben.",

View File

@@ -139,6 +139,7 @@
"title": "Invite User",
"description": "Provide the email address and the name of the user you want to invite.",
"info": "The user will receive an email with further instructions.",
"notAllowed": "Your settings do not allow you to invite users.",
"submit": "Continue",
"success": {
"title": "User invited",
@@ -153,11 +154,17 @@
"description": "You are signed in."
},
"verify": {
"title": "Verify user",
"description": "Enter the Code provided in the verification email.",
"userIdMissing": "No userId provided!",
"resendCode": "Resend code",
"submit": "Continue"
"verify": {
"title": "Verify user",
"description": "Enter the Code provided in the verification email.",
"resendCode": "Resend code",
"submit": "Continue"
},
"setup": {
"title": "Choose authentication method",
"description": "Select the method you would like to authenticate"
}
},
"error": {
"unknownContext": "Could not get the context of the user. Make sure to enter the username first or provide a loginName as searchParam.",

View File

@@ -139,6 +139,7 @@
"title": "Invitar usuario",
"description": "Introduce el correo electrónico del usuario que deseas invitar.",
"info": "El usuario recibirá un correo electrónico con un enlace para completar el registro.",
"notAllowed": "No tienes permiso para invitar usuarios.",
"submit": "Invitar usuario",
"success": {
"title": "¡Usuario invitado!",
@@ -153,11 +154,17 @@
"description": "Has iniciado sesión."
},
"verify": {
"title": "Verificar usuario",
"description": "Introduce el código proporcionado en el correo electrónico de verificación.",
"userIdMissing": "¡No se proporcionó userId!",
"resendCode": "Reenviar código",
"submit": "Continuar"
"verify": {
"title": "Verificar usuario",
"description": "Introduce el código proporcionado en el correo electrónico de verificación.",
"resendCode": "Reenviar código",
"submit": "Continuar"
},
"setup": {
"title": "Seleccionar método de autenticación",
"description": "Selecciona el método con el que deseas autenticarte"
}
},
"error": {
"unknownContext": "No se pudo obtener el contexto del usuario. Asegúrate de ingresar primero el nombre de usuario o proporcionar un loginName como parámetro de búsqueda.",

View File

@@ -139,6 +139,7 @@
"title": "Invita Utente",
"description": "Inserisci l'indirizzo email dell'utente che desideri invitare.",
"info": "L'utente riceverà un'email con ulteriori istruzioni.",
"notAllowed": "Non hai i permessi per invitare un utente.",
"submit": "Invita Utente",
"success": {
"title": "Invito inviato",
@@ -153,11 +154,17 @@
"description": "Sei connesso."
},
"verify": {
"title": "Verifica utente",
"description": "Inserisci il codice fornito nell'email di verifica.",
"userIdMissing": "Nessun userId fornito!",
"resendCode": "Invia di nuovo il codice",
"submit": "Continua"
"verify": {
"title": "Verifica utente",
"description": "Inserisci il codice fornito nell'email di verifica.",
"resendCode": "Invia di nuovo il codice",
"submit": "Continua"
},
"setup": {
"title": "Seleziona metodo di autenticazione",
"description": "Seleziona il metodo con cui desideri autenticarti"
}
},
"error": {
"unknownContext": "Impossibile ottenere il contesto dell'utente. Assicurati di inserire prima il nome utente o di fornire un loginName come parametro di ricerca.",

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 467 467" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<svg width="100%" height="100%" viewBox="0 0 467 467" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;strokeLinejoin:round;stroke-miterlimit:2;">
<g id="zitadel-logo-solo-darkdesign" transform="matrix(0.564847,0,0,0.659318,-1282.85,0)">
<rect x="2271.15" y="0" width="826.773" height="708.241" style="fill:none;"/>
<g transform="matrix(4.96737,-1.14029,1.331,4.25561,-5923.46,-2258.26)">

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 467 468" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<svg width="100%" height="100%" viewBox="0 0 467 468" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;strokeLinejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,0,-492)">
<g id="zitadel-logo-solo-lightdesign" transform="matrix(0.564847,0,0,0.659318,-1282.85,492.925)">
<rect x="2271.15" y="0" width="826.773" height="708.241" style="fill:none;"/>

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 295 81" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<svg width="100%" height="100%" viewBox="0 0 295 81" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;strokeLinejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,0,-107)">
<g id="zitadel-logo-dark" transform="matrix(1,0,0,1,-20.9181,18.2562)">
<rect x="20.918" y="89.57" width="294.943" height="79.632" style="fill:none;"/>

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 295 80" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<svg width="100%" height="100%" viewBox="0 0 295 80" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;strokeLinejoin:round;stroke-miterlimit:2;">
<g id="zitadel-logo-light" transform="matrix(1,0,0,1,-20.9181,-89.5699)">
<rect x="20.918" y="89.57" width="294.943" height="79.632" style="fill:none;"/>
<g transform="matrix(2.73883,0,0,1.55076,-35267,23.6366)">

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -4,6 +4,7 @@ import { InviteForm } from "@/components/invite-form";
import {
getBrandingSettings,
getDefaultOrg,
getLoginSettings,
getPasswordComplexitySettings,
} from "@/lib/zitadel";
import { getLocale, getTranslations } from "next-intl/server";
@@ -27,6 +28,8 @@ export default async function Page({
organization = org.id;
}
const loginSettings = await getLoginSettings(organization);
const passwordComplexitySettings =
await getPasswordComplexitySettings(organization);
@@ -38,9 +41,13 @@ export default async function Page({
<h1>{t("title")}</h1>
<p className="ztdl-p">{t("description")}</p>
<Alert type={AlertType.INFO}>{t("info")}</Alert>
{!loginSettings?.allowRegister ? (
<Alert type={AlertType.ALERT}>{t("notAllowed")}</Alert>
) : (
<Alert type={AlertType.INFO}>{t("info")}</Alert>
)}
{passwordComplexitySettings && (
{passwordComplexitySettings && loginSettings?.allowRegister && (
<InviteForm
organization={organization}
firstname={firstname}

View File

@@ -1,6 +1,11 @@
import { Alert } from "@/components/alert";
import { AuthenticatorMethods } from "@/components/authenticator-methods";
import { DynamicTheme } from "@/components/dynamic-theme";
import { VerifyEmailForm } from "@/components/verify-email-form";
import { getBrandingSettings, getLoginSettings } from "@/lib/zitadel";
import { UserAvatar } from "@/components/user-avatar";
import { VerifyForm } from "@/components/verify-form";
import { verifyUser } from "@/lib/server/email";
import { getBrandingSettings, getUserByID } from "@/lib/zitadel";
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { getLocale, getTranslations } from "next-intl/server";
export default async function Page({ searchParams }: { searchParams: any }) {
@@ -20,21 +25,86 @@ export default async function Page({ searchParams }: { searchParams: any }) {
const branding = await getBrandingSettings(organization);
const loginSettings = await getLoginSettings(organization);
let verifyResponse, error;
if (code && userId) {
verifyResponse = await verifyUser({
code,
userId,
isInvite: invite === "true",
}).catch(() => {
error = "Could not verify user";
});
}
let user: User | undefined;
let human: HumanUser | undefined;
if (userId) {
const userResponse = await getUserByID(userId);
if (userResponse) {
user = userResponse.user;
if (user?.type.case === "human") {
human = user.type.value as HumanUser;
}
}
}
const params = new URLSearchParams({
userId: userId,
initial: "true", // defines that a code is not required and is therefore not shown in the UI
});
if (organization) {
params.set("organization", organization);
}
if (authRequestId) {
params.set("authRequest", authRequestId);
}
if (sessionId) {
params.set("sessionId", sessionId);
}
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<VerifyEmailForm
userId={userId}
loginName={loginName}
code={code}
organization={organization}
authRequestId={authRequestId}
sessionId={sessionId}
loginSettings={loginSettings}
isInvite={invite === "true"}
/>
{!userId && (
<>
<h1>{t("verify.title")}</h1>
<p className="ztdl-p mb-6 block">{t("verify.description")}</p>
<div className="py-4">
<Alert>{tError("unknownContext")}</Alert>
</div>
</>
)}
{!verifyResponse || !verifyResponse.authMethodTypes ? (
<VerifyForm
userId={userId}
loginName={loginName}
code={!error ? code : ""}
organization={organization}
authRequestId={authRequestId}
sessionId={sessionId}
isInvite={invite === "true"}
/>
) : (
<>
<h1>{t("setup.title")}</h1>
<p className="ztdl-p mb-6 block">{t("setup.description")}</p>
{user && (
<UserAvatar
loginName={user.preferredLoginName}
displayName={human?.profile?.displayName}
showDropdown={false}
/>
)}
<AuthenticatorMethods
authMethods={verifyResponse.authMethodTypes}
params={params}
/>
</>
)}
</div>
</DynamicTheme>
);

View File

@@ -139,7 +139,7 @@ export const EMAIL = (alreadyAdded: boolean, link: string) => {
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={1.5}
strokeWidth={1.5}
stroke="currentColor"
>
<path
@@ -174,12 +174,12 @@ export const SMS = (alreadyAdded: boolean, link: string) => {
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
strokeWidth="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
strokeLinecap="round"
strokeLinejoin="round"
d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3"
/>
</svg>
@@ -207,13 +207,13 @@ export const PASSKEYS = (alreadyAdded: boolean, link: string) => {
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
strokeWidth="1.5"
stroke="currentColor"
className="w-8 h-8 mr-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
strokeLinecap="round"
strokeLinejoin="round"
d="M7.864 4.243A7.5 7.5 0 0119.5 10.5c0 2.92-.556 5.709-1.568 8.268M5.742 6.364A7.465 7.465 0 004.5 10.5a7.464 7.464 0 01-1.15 3.993m1.989 3.559A11.209 11.209 0 008.25 10.5a3.75 3.75 0 117.5 0c0 .527-.021 1.049-.064 1.565M12 10.5a14.94 14.94 0 01-3.6 9.75m6.633-4.596a18.666 18.666 0 01-2.485 5.33"
/>
</svg>

View File

@@ -0,0 +1,18 @@
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { PASSKEYS, PASSWORD } from "./auth-methods";
type Props = {
authMethods: AuthenticationMethodType[];
params: URLSearchParams;
};
export function AuthenticatorMethods({ authMethods, params }: Props) {
return (
<div className="grid grid-cols-1 gap-5 w-full pt-4">
{!authMethods.includes(AuthenticationMethodType.PASSWORD) &&
PASSWORD(false, "/password/set?" + params)}
{!authMethods.includes(AuthenticationMethodType.PASSKEY) &&
PASSKEYS(false, "/passkeys/set?" + params)}
</div>
);
}

View File

@@ -77,6 +77,8 @@ export function ChangePasswordForm({
return;
}
await new Promise((resolve) => setTimeout(resolve, 1000)); // wait for a second, to prevent eventual consistency issues
const passwordResponse = await sendPassword({
loginName,
organization,

View File

@@ -59,7 +59,7 @@ export function SetPasswordForm({
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
async function submitRegister(values: Inputs) {
async function submitPassword(values: Inputs) {
setLoading(true);
let payload: { userId: string; password: string; code?: string } = {
userId: userId,
@@ -73,16 +73,19 @@ export function SetPasswordForm({
const changeResponse = await changePassword(payload).catch(() => {
setError("Could not set password");
setLoading(false);
return;
});
if (changeResponse && "error" in changeResponse) {
setError(changeResponse.error);
}
setLoading(false);
if (changeResponse && "error" in changeResponse) {
setError(changeResponse.error);
return;
}
if (!changeResponse) {
setError("Could not register user");
setError("Could not set password");
return;
}
@@ -95,6 +98,8 @@ export function SetPasswordForm({
params.append("organization", organization);
}
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait for a second to avoid eventual consistency issues with an initial password being set
const passwordResponse = await sendPassword({
loginName,
organization,
@@ -213,7 +218,7 @@ export function SetPasswordForm({
!formState.isValid ||
watchPassword !== watchConfirmPassword
}
onClick={handleSubmit(submitRegister)}
onClick={handleSubmit(submitPassword)}
>
{loading && <Spinner className="h-5 w-5 mr-2" />}
{t("set.submit")}

View File

@@ -2,13 +2,12 @@
import { Alert } from "@/components/alert";
import { resendVerification, verifyUser } from "@/lib/server/email";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { PASSKEYS, PASSWORD } from "./auth-methods";
import { AuthenticatorMethods } from "./authenticator-methods";
import { Button, ButtonVariants } from "./button";
import { TextInput } from "./input";
import { Spinner } from "./spinner";
@@ -18,25 +17,25 @@ type Inputs = {
};
type Props = {
userId?: string;
userId: string;
loginName: string;
code: string;
organization?: string;
authRequestId?: string;
sessionId?: string;
loginSettings?: LoginSettings;
isInvite: boolean;
verifyError?: string;
};
export function VerifyEmailForm({
export function VerifyForm({
userId,
loginName,
code,
organization,
authRequestId,
sessionId,
loginSettings,
isInvite,
verifyError,
}: Props) {
const t = useTranslations("verify");
const tError = useTranslations("error");
@@ -52,30 +51,19 @@ export function VerifyEmailForm({
AuthenticationMethodType[] | null
>(null);
useEffect(() => {
if (code && userId) {
// When we navigate to this page, we always want to be redirected if submit is true and the parameters are valid.
// For programmatic verification, the /verifyemail API should be used.
submitCodeAndContinue({ code });
}
}, []);
const [error, setError] = useState<string>("");
const [error, setError] = useState<string>(verifyError || "");
const [loading, setLoading] = useState<boolean>(false);
const router = useRouter();
const params = new URLSearchParams({});
if (userId) {
params.append("userId", userId);
}
const params = new URLSearchParams({
userId: userId,
});
if (isInvite) {
params.append("initial", "true");
}
if (loginName) {
params.append("loginName", loginName);
}
@@ -131,13 +119,10 @@ export function VerifyEmailForm({
// if auth methods fall trough, we complete to login
const params = new URLSearchParams({
userId: userId,
initial: "true", // defines that a code is not required and is therefore not shown in the UI
});
if (userId) {
params.set("userId", userId);
}
if (organization) {
params.set("organization", organization);
}
@@ -153,14 +138,8 @@ export function VerifyEmailForm({
return !authMethods ? (
<>
<h1>{t("title")}</h1>
<p className="ztdl-p mb-6 block">{t("description")}</p>
{!userId && (
<div className="py-4">
<Alert>{tError("unknownContext")}</Alert>
</div>
)}
<h1>{t("verify.title")}</h1>
<p className="ztdl-p mb-6 block">{t("verify.description")}</p>
<form className="w-full">
<div className="">
@@ -169,7 +148,6 @@ export function VerifyEmailForm({
autoComplete="one-time-code"
{...register("code", { required: "This field is required" })}
label="Code"
// error={errors.username?.message as string}
/>
</div>
@@ -185,7 +163,7 @@ export function VerifyEmailForm({
onClick={() => resendCode()}
variant={ButtonVariants.Secondary}
>
{t("resendCode")}
{t("verify.resendCode")}
</Button>
<span className="flex-grow"></span>
<Button
@@ -196,22 +174,17 @@ export function VerifyEmailForm({
onClick={handleSubmit(submitCodeAndContinue)}
>
{loading && <Spinner className="h-5 w-5 mr-2" />}
{t("submit")}
{t("verify.submit")}
</Button>
</div>
</form>
</>
) : (
<>
<h1>{t("title")}</h1>
<p className="ztdl-p mb-6 block">{t("description")}</p>
<h1>{t("setup.title")}</h1>
<p className="ztdl-p mb-6 block">{t("setup.description")}</p>
<div className="grid grid-cols-1 gap-5 w-full pt-4">
{!authMethods.includes(AuthenticationMethodType.PASSWORD) &&
PASSWORD(false, "/password/set?" + params)}
{!authMethods.includes(AuthenticationMethodType.PASSKEY) &&
PASSKEYS(false, "/passkeys/set?" + params)}
</div>
<AuthenticatorMethods authMethods={authMethods} params={params} />
</>
);
}

View File

@@ -7,7 +7,7 @@ import {
getSession,
setSession,
} from "@/lib/zitadel";
import { timestampDate } from "@zitadel/client";
import { timestampMs } from "@zitadel/client";
import {
Challenges,
RequestChallenges,
@@ -43,13 +43,13 @@ export async function createSessionAndUpdateCookie(
id: createdSession.sessionId,
token: createdSession.sessionToken,
creationDate: response.session.creationDate
? `${timestampDate(response.session.creationDate).toDateString()}`
? `${timestampMs(response.session.creationDate)}`
: "",
expirationDate: response.session.expirationDate
? `${timestampDate(response.session.expirationDate).toDateString()}`
? `${timestampMs(response.session.expirationDate)}`
: "",
changeDate: response.session.changeDate
? `${timestampDate(response.session.changeDate).toDateString()}`
? `${timestampMs(response.session.changeDate)}`
: "",
loginName: response.session.factors.user.loginName ?? "",
};
@@ -98,13 +98,13 @@ export async function createSessionForIdpAndUpdateCookie(
id: createdSession.sessionId,
token: createdSession.sessionToken,
creationDate: response.session.creationDate
? `${timestampDate(response.session.creationDate).toDateString()}`
? `${timestampMs(response.session.creationDate)}`
: "",
expirationDate: response.session.expirationDate
? `${timestampDate(response.session.expirationDate).toDateString()}`
? `${timestampMs(response.session.expirationDate)}`
: "",
changeDate: response.session.changeDate
? `${timestampDate(response.session.changeDate).toDateString()}`
? `${timestampMs(response.session.changeDate)}`
: "",
loginName: response.session.factors.user.loginName ?? "",
organization: response.session.factors.user.organizationId ?? "",
@@ -155,7 +155,7 @@ export async function setSessionAndUpdateCookie(
expirationDate: recentCookie.expirationDate,
// just overwrite the changeDate with the new one
changeDate: updatedSession.details?.changeDate
? `${timestampDate(updatedSession.details.changeDate).toDateString()}`
? `${timestampMs(updatedSession.details.changeDate)}`
: "",
loginName: recentCookie.loginName,
organization: recentCookie.organization,
@@ -178,7 +178,7 @@ export async function setSessionAndUpdateCookie(
expirationDate: sessionCookie.expirationDate,
// just overwrite the changeDate with the new one
changeDate: updatedSession.details?.changeDate
? `${timestampDate(updatedSession.details.changeDate).toDateString()}`
? `${timestampMs(updatedSession.details.changeDate)}`
: "",
loginName: session.factors?.user?.loginName ?? "",
organization: session.factors?.user?.organizationId ?? "",

View File

@@ -139,6 +139,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
if (users.result[0].state === UserState.INITIAL) {
const params = new URLSearchParams({
loginName: session.factors?.user?.loginName,
initial: "true", // this does not require a code to be set
});
if (command.organization || session.factors?.user?.organizationId) {

View File

@@ -284,5 +284,5 @@ export async function changePassword(command: {
}
const userId = user.userId;
return setPassword(userId, command.password, command.code);
return setPassword(userId, command.password, user, command.code);
}

View File

@@ -37,7 +37,11 @@ import {
SearchQuery,
SearchQuerySchema,
} from "@zitadel/proto/zitadel/user/v2/query_pb";
import { SendInviteCodeSchema } from "@zitadel/proto/zitadel/user/v2/user_pb";
import {
SendInviteCodeSchema,
User,
UserState,
} from "@zitadel/proto/zitadel/user/v2/user_pb";
import { unstable_cache } from "next/cache";
import { PROVIDER_MAPPING } from "./idp";
@@ -327,7 +331,7 @@ export async function createInviteCode(userId: string, host: string | null) {
if (host) {
medium = {
...medium,
urlTemplate: `https://${host}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true`,
urlTemplate: `${host.includes("localhost") ? "http://" : "https://"}${host}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true`,
};
}
@@ -564,7 +568,7 @@ export async function passwordReset(userId: string, host: string | null) {
if (host) {
medium = {
...medium,
urlTemplate: `https://${host}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}`,
urlTemplate: `${host.includes("localhost") ? "http://" : "https://"}${host}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}`,
};
}
@@ -590,6 +594,7 @@ export async function passwordReset(userId: string, host: string | null) {
export async function setPassword(
userId: string,
password: string,
user: User,
code?: string,
) {
let payload = create(SetPasswordRequestSchema, {
@@ -605,9 +610,8 @@ export async function setPassword(
// if the user has no authmethods set, we can set a password otherwise we need a code
if (
!authmethods ||
!authmethods.authMethodTypes ||
authmethods.authMethodTypes.length === 0
!(authmethods.authMethodTypes.length === 0) &&
user.state !== UserState.INITIAL
) {
return { error: "Provide a code to set a password" };
}
@@ -623,7 +627,14 @@ export async function setPassword(
};
}
return userService.setPassword(payload, {});
return userService.setPassword(payload, {}).catch((error) => {
// throw error if failed precondition (ex. User is not yet initialized)
if (error.code === 9 && error.message) {
return { error: error.message };
} else {
throw error;
}
});
}
/**

View File

@@ -3,5 +3,5 @@ export { NewAuthorizationBearerInterceptor } from "./interceptors";
// TODO: Move this to `./protobuf.ts` and export it from there
export { create, fromJson, toJson } from "@bufbuild/protobuf";
export { TimestampSchema, timestampDate, timestampFromDate } from "@bufbuild/protobuf/wkt";
export { TimestampSchema, timestampDate, timestampFromDate, timestampMs } from "@bufbuild/protobuf/wkt";
export type { Timestamp } from "@bufbuild/protobuf/wkt";