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": {
"userIdMissing": "Keine Benutzer-ID angegeben!",
"success": "Erfolgreich verifiziert",
"verify": {
"title": "Benutzer verifizieren",
"description": "Geben Sie den Code ein, der in der Bestätigungs-E-Mail angegeben ist.",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,9 +20,7 @@ type VerifyUserByEmailCommand = {
authRequestId?: string;
};
export async function verifyUserAndCreateSession(
command: VerifyUserByEmailCommand,
) {
export async function sendVerification(command: VerifyUserByEmailCommand) {
const verifyResponse = command.isInvite
? await verifyInviteCode(command.userId, command.code).catch((error) => {
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 { redirect } from "next/navigation";
import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp";
import {
getActiveIdentityProviders,
getIDPByID,
@@ -161,6 +162,29 @@ export async function sendLoginname(command: SendLoginnameCommand) {
);
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 {
error:
"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
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";