ts for cookie, loginname to verification

This commit is contained in:
peintnermax
2024-10-24 10:02:12 +02:00
parent f45c5304a7
commit c4da6fd077
12 changed files with 107 additions and 58 deletions

View File

@@ -155,6 +155,7 @@
}, },
"verify": { "verify": {
"userIdMissing": "Keine Benutzer-ID angegeben!", "userIdMissing": "Keine Benutzer-ID angegeben!",
"success": "Erfolgreich verifiziert",
"verify": { "verify": {
"title": "Benutzer verifizieren", "title": "Benutzer verifizieren",
"description": "Geben Sie den Code ein, der in der Bestätigungs-E-Mail angegeben ist.", "description": "Geben Sie den Code ein, der in der Bestätigungs-E-Mail angegeben ist.",

View File

@@ -155,6 +155,7 @@
}, },
"verify": { "verify": {
"userIdMissing": "No userId provided!", "userIdMissing": "No userId provided!",
"success": "The user has been verified successfully.",
"verify": { "verify": {
"title": "Verify user", "title": "Verify user",
"description": "Enter the Code provided in the verification email.", "description": "Enter the Code provided in the verification email.",

View File

@@ -155,6 +155,7 @@
}, },
"verify": { "verify": {
"userIdMissing": "¡No se proporcionó userId!", "userIdMissing": "¡No se proporcionó userId!",
"success": "¡Verificación exitosa!",
"verify": { "verify": {
"title": "Verificar usuario", "title": "Verificar usuario",
"description": "Introduce el código proporcionado en el correo electrónico de verificación.", "description": "Introduce el código proporcionado en el correo electrónico de verificación.",

View File

@@ -155,6 +155,7 @@
}, },
"verify": { "verify": {
"userIdMissing": "Nessun userId fornito!", "userIdMissing": "Nessun userId fornito!",
"success": "Verifica effettuata con successo!",
"verify": { "verify": {
"title": "Verifica utente", "title": "Verifica utente",
"description": "Inserisci il codice fornito nell'email di verifica.", "description": "Inserisci il codice fornito nell'email di verifica.",

View File

@@ -1,6 +1,5 @@
import { Alert } from "@/components/alert"; import { Alert } from "@/components/alert";
import { BackButton } from "@/components/back-button"; import { BackButton } from "@/components/back-button";
import { ChooseAuthenticatorToSetup } from "@/components/choose-authenticator-to-setup";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getSessionCookieById } from "@/lib/cookies"; import { getSessionCookieById } from "@/lib/cookies";
@@ -41,13 +40,14 @@ export default async function Page({
const t = await getTranslations({ locale, namespace: "authenticator" }); const t = await getTranslations({ locale, namespace: "authenticator" });
const tError = await getTranslations({ locale, namespace: "error" }); const tError = await getTranslations({ locale, namespace: "error" });
const { loginName, checkAfter, authRequestId, organization, sessionId } = const { loginName, authRequestId, organization, sessionId } = searchParams;
searchParams;
const sessionWithData = sessionId const sessionWithData = sessionId
? await loadSessionById(sessionId, organization) ? await loadSessionById(sessionId, organization)
: await loadSessionByLoginname(loginName, organization); : await loadSessionByLoginname(loginName, organization);
console.log("sessionWithData", sessionWithData);
async function getAuthMethodsAndUser(session?: Session) { async function getAuthMethodsAndUser(session?: Session) {
const userId = session?.factors?.user?.id; const userId = session?.factors?.user?.id;
@@ -93,7 +93,9 @@ export default async function Page({
}); });
} }
const branding = await getBrandingSettings(organization); const branding = await getBrandingSettings(
sessionWithData.factors?.user?.organizationId,
);
const loginSettings = await getLoginSettings( const loginSettings = await getLoginSettings(
sessionWithData.factors?.user?.organizationId, sessionWithData.factors?.user?.organizationId,
@@ -141,14 +143,14 @@ export default async function Page({
{!valid && <Alert>{tError("sessionExpired")}</Alert>} {!valid && <Alert>{tError("sessionExpired")}</Alert>}
{loginSettings && sessionWithData && ( {/* {loginSettings && sessionWithData && (
<ChooseAuthenticatorToSetup <ChooseAuthenticatorToSetup
authMethods={sessionWithData.authMethods} authMethods={sessionWithData.authMethods}
sessionFactors={sessionWithData.factors} sessionFactors={sessionWithData.factors}
loginSettings={loginSettings} loginSettings={loginSettings}
params={params} params={params}
></ChooseAuthenticatorToSetup> ></ChooseAuthenticatorToSetup>
)} )} */}
<div className="mt-8 flex w-full flex-row items-center"> <div className="mt-8 flex w-full flex-row items-center">
<BackButton /> <BackButton />

View File

@@ -1,5 +1,6 @@
import { Alert } from "@/components/alert"; import { Alert, AlertType } from "@/components/alert";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { UserAvatar } from "@/components/user-avatar";
import { VerifyForm } from "@/components/verify-form"; import { VerifyForm } from "@/components/verify-form";
import { getBrandingSettings, getUserByID } from "@/lib/zitadel"; import { getBrandingSettings, getUserByID } from "@/lib/zitadel";
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
@@ -47,6 +48,9 @@ export default async function Page({ searchParams }: { searchParams: any }) {
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>{t("verify.title")}</h1>
<p className="ztdl-p mb-6 block">{t("verify.description")}</p>
{!userId && ( {!userId && (
<> <>
<h1>{t("verify.title")}</h1> <h1>{t("verify.title")}</h1>
@@ -58,12 +62,24 @@ export default async function Page({ searchParams }: { searchParams: any }) {
</> </>
)} )}
{user && (
<UserAvatar
loginName={user.preferredLoginName}
displayName={human?.profile?.displayName}
showDropdown={false}
/>
)}
{human?.email?.isVerified ? (
<Alert type={AlertType.INFO}>{t("success")}</Alert>
) : (
// check if auth methods are set
<VerifyForm <VerifyForm
userId={userId} userId={userId}
code={code} code={code}
isInvite={invite === "true"} isInvite={invite === "true"}
params={params} params={params}
/> />
)}
</div> </div>
</DynamicTheme> </DynamicTheme>
); );

View File

@@ -1,13 +1,10 @@
"use client"; "use client";
import { Alert } from "@/components/alert"; import { Alert } from "@/components/alert";
import { import { resendVerification, sendVerification } from "@/lib/server/email";
resendVerification,
verifyUserAndCreateSession,
} from "@/lib/server/email";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { Button, ButtonVariants } from "./button"; import { Button, ButtonVariants } from "./button";
import { TextInput } from "./input"; import { TextInput } from "./input";
@@ -41,6 +38,12 @@ export function VerifyForm({ userId, code, isInvite, params }: Props) {
const router = useRouter(); const router = useRouter();
useEffect(() => {
if (code) {
submitCodeAndContinue({ code });
}
}, []);
async function resendCode() { async function resendCode() {
setLoading(true); setLoading(true);
@@ -60,7 +63,7 @@ export function VerifyForm({ userId, code, isInvite, params }: Props) {
async function submitCodeAndContinue(value: Inputs): Promise<boolean | void> { async function submitCodeAndContinue(value: Inputs): Promise<boolean | void> {
setLoading(true); setLoading(true);
const verifyResponse = await verifyUserAndCreateSession({ const verifyResponse = await sendVerification({
code: value.code, code: value.code,
userId, userId,
isInvite: isInvite, isInvite: isInvite,
@@ -83,9 +86,6 @@ export function VerifyForm({ userId, code, isInvite, params }: Props) {
return ( return (
<> <>
<h1>{t("verify.title")}</h1>
<p className="ztdl-p mb-6 block">{t("verify.description")}</p>
<form className="w-full"> <form className="w-full">
<div className=""> <div className="">
<TextInput <TextInput

View File

@@ -1,5 +1,6 @@
"use server"; "use server";
import { timestampDate, timestampFromMs } from "@zitadel/client";
import { cookies } from "next/headers"; import { cookies } from "next/headers";
import { LANGUAGE_COOKIE_NAME } from "./i18n"; import { LANGUAGE_COOKIE_NAME } from "./i18n";
@@ -11,9 +12,9 @@ export type Cookie = {
token: string; token: string;
loginName: string; loginName: string;
organization?: string; organization?: string;
creationDate: string; creationTs: string;
expirationDate: string; expirationTs: string;
changeDate: string; changeTs: string;
authRequestId?: string; // if its linked to an OIDC flow authRequestId?: string; // if its linked to an OIDC flow
}; };
@@ -66,13 +67,17 @@ export async function addSessionToCookie<T>(
// TODO: improve cookie handling // TODO: improve cookie handling
// this replaces the first session (oldest) with the new one // this replaces the first session (oldest) with the new one
currentSessions = [session].concat(currentSessions.slice(1)); currentSessions = [session].concat(currentSessions.slice(1));
} else {
currentSessions = [session].concat(currentSessions);
} }
} }
if (cleanup) { if (cleanup) {
const now = new Date(); const now = new Date();
const filteredSessions = currentSessions.filter((session) => const filteredSessions = currentSessions.filter((session) =>
session.expirationDate ? new Date(session.expirationDate) > now : true, session.expirationTs
? timestampDate(timestampFromMs(Number(session.expirationTs))) > now
: true,
); );
return setSessionHttpOnlyCookie(filteredSessions); return setSessionHttpOnlyCookie(filteredSessions);
} else { } else {
@@ -99,7 +104,9 @@ export async function updateSessionCookie<T>(
if (cleanup) { if (cleanup) {
const now = new Date(); const now = new Date();
const filteredSessions = sessions.filter((session) => const filteredSessions = sessions.filter((session) =>
session.expirationDate ? new Date(session.expirationDate) > now : true, session.expirationTs
? timestampDate(timestampFromMs(Number(session.expirationTs))) > now
: true,
); );
return setSessionHttpOnlyCookie(filteredSessions); return setSessionHttpOnlyCookie(filteredSessions);
} else { } else {
@@ -125,7 +132,9 @@ export async function removeSessionFromCookie<T>(
if (cleanup) { if (cleanup) {
const now = new Date(); const now = new Date();
const filteredSessions = reducedSessions.filter((session) => const filteredSessions = reducedSessions.filter((session) =>
session.expirationDate ? new Date(session.expirationDate) > now : true, session.expirationTs
? timestampDate(timestampFromMs(Number(session.expirationTs))) > now
: true,
); );
return setSessionHttpOnlyCookie(filteredSessions); return setSessionHttpOnlyCookie(filteredSessions);
} else { } else {
@@ -141,10 +150,7 @@ export async function getMostRecentSessionCookie<T>(): Promise<any> {
const sessions: SessionCookie<T>[] = JSON.parse(stringifiedCookie?.value); const sessions: SessionCookie<T>[] = JSON.parse(stringifiedCookie?.value);
const latest = sessions.reduce((prev, current) => { const latest = sessions.reduce((prev, current) => {
return new Date(prev.changeDate).getTime() > return prev.changeTs > current.changeTs ? prev : current;
new Date(current.changeDate).getTime()
? prev
: current;
}); });
return latest; return latest;
@@ -226,8 +232,8 @@ export async function getAllSessionCookieIds<T>(
const now = new Date(); const now = new Date();
return sessions return sessions
.filter((session) => .filter((session) =>
session.expirationDate session.expirationTs
? new Date(session.expirationDate) > now ? timestampDate(timestampFromMs(Number(session.expirationTs))) > now
: true, : true,
) )
.map((session) => session.id); .map((session) => session.id);
@@ -256,7 +262,9 @@ export async function getAllSessions<T>(
if (cleanup) { if (cleanup) {
const now = new Date(); const now = new Date();
return sessions.filter((session) => return sessions.filter((session) =>
session.expirationDate ? new Date(session.expirationDate) > now : true, session.expirationTs
? timestampDate(timestampFromMs(Number(session.expirationTs))) > now
: true,
); );
} else { } else {
return sessions; return sessions;
@@ -297,10 +305,7 @@ export async function getMostRecentCookieWithLoginname<T>({
const latest = const latest =
filtered && filtered.length filtered && filtered.length
? filtered.reduce((prev, current) => { ? filtered.reduce((prev, current) => {
return new Date(prev.changeDate).getTime() > return prev.changeTs > current.changeTs ? prev : current;
new Date(current.changeDate).getTime()
? prev
: current;
}) })
: undefined; : undefined;

View File

@@ -20,9 +20,9 @@ type CustomCookieData = {
token: string; token: string;
loginName: string; loginName: string;
organization?: string; organization?: string;
creationDate: string; creationTs: string;
expirationDate: string; expirationTs: string;
changeDate: string; changeTs: string;
authRequestId?: string; // if its linked to an OIDC flow authRequestId?: string; // if its linked to an OIDC flow
}; };
@@ -42,13 +42,13 @@ export async function createSessionAndUpdateCookie(
const sessionCookie: CustomCookieData = { const sessionCookie: CustomCookieData = {
id: createdSession.sessionId, id: createdSession.sessionId,
token: createdSession.sessionToken, token: createdSession.sessionToken,
creationDate: response.session.creationDate creationTs: response.session.creationDate
? `${timestampMs(response.session.creationDate)}` ? `${timestampMs(response.session.creationDate)}`
: "", : "",
expirationDate: response.session.expirationDate expirationTs: response.session.expirationDate
? `${timestampMs(response.session.expirationDate)}` ? `${timestampMs(response.session.expirationDate)}`
: "", : "",
changeDate: response.session.changeDate changeTs: response.session.changeDate
? `${timestampMs(response.session.changeDate)}` ? `${timestampMs(response.session.changeDate)}`
: "", : "",
loginName: response.session.factors.user.loginName ?? "", loginName: response.session.factors.user.loginName ?? "",
@@ -97,13 +97,13 @@ export async function createSessionForIdpAndUpdateCookie(
const sessionCookie: CustomCookieData = { const sessionCookie: CustomCookieData = {
id: createdSession.sessionId, id: createdSession.sessionId,
token: createdSession.sessionToken, token: createdSession.sessionToken,
creationDate: response.session.creationDate creationTs: response.session.creationDate
? `${timestampMs(response.session.creationDate)}` ? `${timestampMs(response.session.creationDate)}`
: "", : "",
expirationDate: response.session.expirationDate expirationTs: response.session.expirationDate
? `${timestampMs(response.session.expirationDate)}` ? `${timestampMs(response.session.expirationDate)}`
: "", : "",
changeDate: response.session.changeDate changeTs: response.session.changeDate
? `${timestampMs(response.session.changeDate)}` ? `${timestampMs(response.session.changeDate)}`
: "", : "",
loginName: response.session.factors.user.loginName ?? "", loginName: response.session.factors.user.loginName ?? "",
@@ -151,10 +151,10 @@ export async function setSessionAndUpdateCookie(
const sessionCookie: CustomCookieData = { const sessionCookie: CustomCookieData = {
id: recentCookie.id, id: recentCookie.id,
token: updatedSession.sessionToken, token: updatedSession.sessionToken,
creationDate: recentCookie.creationDate, creationTs: recentCookie.creationTs,
expirationDate: recentCookie.expirationDate, expirationTs: recentCookie.expirationTs,
// just overwrite the changeDate with the new one // just overwrite the changeDate with the new one
changeDate: updatedSession.details?.changeDate changeTs: updatedSession.details?.changeDate
? `${timestampMs(updatedSession.details.changeDate)}` ? `${timestampMs(updatedSession.details.changeDate)}`
: "", : "",
loginName: recentCookie.loginName, loginName: recentCookie.loginName,
@@ -174,10 +174,10 @@ export async function setSessionAndUpdateCookie(
const newCookie: CustomCookieData = { const newCookie: CustomCookieData = {
id: sessionCookie.id, id: sessionCookie.id,
token: updatedSession.sessionToken, token: updatedSession.sessionToken,
creationDate: sessionCookie.creationDate, creationTs: sessionCookie.creationTs,
expirationDate: sessionCookie.expirationDate, expirationTs: sessionCookie.expirationTs,
// just overwrite the changeDate with the new one // just overwrite the changeDate with the new one
changeDate: updatedSession.details?.changeDate changeTs: updatedSession.details?.changeDate
? `${timestampMs(updatedSession.details.changeDate)}` ? `${timestampMs(updatedSession.details.changeDate)}`
: "", : "",
loginName: session.factors?.user?.loginName ?? "", loginName: session.factors?.user?.loginName ?? "",

View File

@@ -20,9 +20,7 @@ type VerifyUserByEmailCommand = {
authRequestId?: string; authRequestId?: string;
}; };
export async function verifyUserAndCreateSession( export async function sendVerification(command: VerifyUserByEmailCommand) {
command: VerifyUserByEmailCommand,
) {
const verifyResponse = command.isInvite const verifyResponse = command.isInvite
? await verifyInviteCode(command.userId, command.code).catch((error) => { ? await verifyInviteCode(command.userId, command.code).catch((error) => {
return { error: "Could not verify invite" }; return { error: "Could not verify invite" };

View File

@@ -7,6 +7,7 @@ import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_se
import { headers } from "next/headers"; import { headers } from "next/headers";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp"; import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp";
import { import {
getActiveIdentityProviders, getActiveIdentityProviders,
getIDPByID, getIDPByID,
@@ -161,6 +162,29 @@ export async function sendLoginname(command: SendLoginnameCommand) {
); );
if (!methods.authMethodTypes || !methods.authMethodTypes.length) { if (!methods.authMethodTypes || !methods.authMethodTypes.length) {
if (
users.result[0].type.case === "human" &&
users.result[0].type.value.email &&
!users.result[0].type.value.email.isVerified
) {
const paramsVerify = new URLSearchParams({
loginName: session.factors?.user?.loginName,
userId: session.factors?.user?.id, // verify needs user id
});
if (command.organization || session.factors?.user?.organizationId) {
paramsVerify.append(
"organization",
command.organization ?? session.factors?.user?.organizationId,
);
}
if (command.authRequestId) {
paramsVerify.append("authRequestId", command.authRequestId);
}
redirect("/verify?" + paramsVerify);
}
return { return {
error: error:
"User has no available authentication methods. Contact your administrator to setup authentication for the requested user.", "User has no available authentication methods. Contact your administrator to setup authentication for the requested user.",

View File

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