tests, session using timestamp
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
@@ -152,13 +153,19 @@
|
||||
"title": "Willkommen {user}!",
|
||||
"description": "Sie sind angemeldet."
|
||||
},
|
||||
"verify": {
|
||||
"userIdMissing": "Keine Benutzer-ID angegeben!",
|
||||
"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"
|
||||
},
|
||||
"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.",
|
||||
"sessionExpired": "Ihre aktuelle Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.",
|
||||
|
||||
@@ -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",
|
||||
@@ -152,13 +153,19 @@
|
||||
"title": "Welcome {user}!",
|
||||
"description": "You are signed in."
|
||||
},
|
||||
"verify": {
|
||||
"userIdMissing": "No userId provided!",
|
||||
"verify": {
|
||||
"title": "Verify user",
|
||||
"description": "Enter the Code provided in the verification email.",
|
||||
"userIdMissing": "No userId provided!",
|
||||
"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.",
|
||||
"sessionExpired": "Your current session has expired. Please login again.",
|
||||
|
||||
@@ -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!",
|
||||
@@ -152,13 +153,19 @@
|
||||
"title": "¡Bienvenido {user}!",
|
||||
"description": "Has iniciado sesión."
|
||||
},
|
||||
"verify": {
|
||||
"userIdMissing": "¡No se proporcionó userId!",
|
||||
"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"
|
||||
},
|
||||
"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.",
|
||||
"sessionExpired": "Tu sesión actual ha expirado. Por favor, inicia sesión de nuevo.",
|
||||
|
||||
@@ -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",
|
||||
@@ -152,13 +153,19 @@
|
||||
"title": "Benvenuto {user}!",
|
||||
"description": "Sei connesso."
|
||||
},
|
||||
"verify": {
|
||||
"userIdMissing": "Nessun userId fornito!",
|
||||
"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"
|
||||
},
|
||||
"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.",
|
||||
"sessionExpired": "La tua sessione attuale è scaduta. Effettua nuovamente l'accesso.",
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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>
|
||||
|
||||
{!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}
|
||||
|
||||
@@ -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 && (
|
||||
<>
|
||||
<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={code}
|
||||
code={!error ? code : ""}
|
||||
organization={organization}
|
||||
authRequestId={authRequestId}
|
||||
sessionId={sessionId}
|
||||
loginSettings={loginSettings}
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
18
apps/login/src/components/authenticator-methods.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 ?? "",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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";
|
||||
|
||||