logout success page

This commit is contained in:
Max Peintner
2025-06-05 10:01:09 +02:00
parent 34d8a64d6c
commit db3c2d9729
14 changed files with 134 additions and 62 deletions

View File

@@ -12,7 +12,11 @@
"title": "Logout",
"description": "Wählen Sie den Account aus, das Sie entfernen möchten",
"noResults": "Keine Konten gefunden",
"clear": "Entfernen"
"clear": "Entfernen",
"success": {
"title": "Logout erfolgreich",
"description": "Sie haben sich erfolgreich abgemeldet."
}
},
"loginname": {
"title": "Willkommen zurück!",

View File

@@ -12,7 +12,11 @@
"title": "Logout",
"description": "Click the accounts you want to clear",
"noResults": "No accounts found",
"clear": "Clear"
"clear": "Clear",
"success": {
"title": "Logout successful",
"description": "You have successfully logged out."
}
},
"loginname": {
"title": "Welcome back!",

View File

@@ -12,7 +12,11 @@
"title": "Cerrar sesión",
"description": "Selecciona la cuenta que deseas eliminar",
"noResults": "No se encontraron cuentas",
"clear": "Eliminar"
"clear": "Eliminar",
"success": {
"title": "Cierre de sesión exitoso",
"description": "Has cerrado sesión correctamente."
}
},
"loginname": {
"title": "¡Bienvenido de nuevo!",

View File

@@ -12,7 +12,11 @@
"title": "Esci",
"description": "Seleziona l'account che desideri uscire",
"noResults": "Nessun account trovato",
"clear": "Rimuovi"
"clear": "Rimuovi",
"success": {
"title": "Uscita riuscita",
"description": "Hai effettuato l'uscita con successo."
}
},
"loginname": {
"title": "Bentornato!",

View File

@@ -12,7 +12,11 @@
"title": "Wyloguj się",
"description": "Wybierz konto, które chcesz usunąć",
"noResults": "Nie znaleziono kont",
"clear": "Usuń"
"clear": "Usuń",
"success": {
"title": "Wylogowanie udane",
"description": "Pomyślnie się wylogowałeś."
}
},
"loginname": {
"title": "Witamy ponownie!",

View File

@@ -12,7 +12,11 @@
"title": "Выход",
"description": "Выберите аккаунт, который хотите удалить",
"noResults": "Аккаунты не найдены",
"clear": "Удалить"
"clear": "Удалить",
"success": {
"title": "Выход выполнен успешно",
"description": "Вы успешно вышли из системы."
}
},
"loginname": {
"title": "С возвращением!",

View File

@@ -12,7 +12,11 @@
"title": "注销",
"description": "选择您想注销的账户",
"noResults": "未找到账户",
"clear": "清除"
"clear": "清除",
"success": {
"title": "注销成功",
"description": "您已成功注销。"
}
},
"loginname": {
"title": "欢迎回来!",

View File

@@ -1,7 +1,7 @@
import { DynamicTheme } from "@/components/dynamic-theme";
import { SessionsClearList } from "@/components/sessions-clear-list";
import { getAllSessionCookieIds } from "@/lib/cookies";
import { getServiceUrlFromHeaders } from "@/lib/service";
import { getServiceUrlFromHeaders } from "@/lib/service-url";
import {
getBrandingSettings,
getDefaultOrg,
@@ -35,6 +35,7 @@ export default async function Page(props: {
const organization = searchParams?.organization;
const postLogoutRedirectUri = searchParams?.post_logout_redirect_uri;
const loginHint = searchParams?.login_hint;
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
@@ -71,7 +72,9 @@ export default async function Page(props: {
<div className="flex flex-col w-full space-y-2">
<SessionsClearList
sessions={sessions}
loginHint={loginHint}
postLogoutRedirectUri={postLogoutRedirectUri}
organization={organization ?? defaultOrganization}
/>
</div>
</div>

View File

@@ -0,0 +1,41 @@
import { DynamicTheme } from "@/components/dynamic-theme";
import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { getBrandingSettings, getDefaultOrg } from "@/lib/zitadel";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers";
export default async function Page(props: { searchParams: Promise<any> }) {
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "logout" });
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { login_hint, organization } = searchParams;
let defaultOrganization;
if (!organization) {
const org: Organization | null = await getDefaultOrg({
serviceUrl,
});
if (org) {
defaultOrganization = org.id;
}
}
const branding = await getBrandingSettings({
serviceUrl,
organization,
});
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>{t("success.title")}</h1>
<p className="ztdl-p mb-6 block">{t("success.description")}</p>
</div>
</DynamicTheme>
);
}

View File

@@ -1,35 +1,16 @@
import { DynamicTheme } from "@/components/dynamic-theme";
import { UserAvatar } from "@/components/user-avatar";
import { getSessionCookieById } from "@/lib/cookies";
import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session";
import {
getBrandingSettings,
getLoginSettings,
getSession,
getUserByID,
} from "@/lib/zitadel";
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers";
async function loadSessionById(
serviceUrl: string,
sessionId: string,
organization?: string,
) {
const recent = await getSessionCookieById({ sessionId, organization });
return getSession({
serviceUrl,
sessionId: recent.id,
sessionToken: recent.token,
}).then((response) => {
if (response?.session) {
return response.session;
}
});
}
export default async function Page(props: { searchParams: Promise<any> }) {
const searchParams = await props.searchParams;
const locale = getLocale();

View File

@@ -1,6 +1,6 @@
"use client";
import { cleanupSession } from "@/lib/server/session";
import { clearSession } from "@/lib/server/session";
import { timestampDate } from "@zitadel/client";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import moment from "moment";
@@ -24,9 +24,9 @@ export function SessionClearItem({
const [loading, setLoading] = useState<boolean>(false);
async function clearSession(id: string) {
async function clearSessionId(id: string) {
setLoading(true);
const response = await cleanupSession({
const response = await clearSession({
sessionId: id,
})
.catch((error) => {
@@ -49,7 +49,7 @@ export function SessionClearItem({
return (
<button
onClick={async () => {
clearSession(session.id).then(() => {
clearSessionId(session.id).then(() => {
reload();
});
}}

View File

@@ -1,7 +1,7 @@
"use client";
import { sendLoginname } from "@/lib/server/loginname";
import { cleanupSession, continueWithSession } from "@/lib/server/session";
import { clearSession, continueWithSession } from "@/lib/server/session";
import { XCircleIcon } from "@heroicons/react/24/outline";
import { Timestamp, timestampDate } from "@zitadel/client";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
@@ -43,9 +43,9 @@ export function SessionItem({
const [loading, setLoading] = useState<boolean>(false);
async function clearSession(id: string) {
async function clearSessionId(id: string) {
setLoading(true);
const response = await cleanupSession({
const response = await clearSession({
sessionId: id,
})
.catch((error) => {
@@ -145,7 +145,7 @@ export function SessionItem({
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
clearSession(session.id).then(() => {
clearSessionId(session.id).then(() => {
reload();
});
}}

View File

@@ -1,23 +1,66 @@
"use client";
import { clearSession } from "@/lib/server/session";
import { timestampDate } from "@zitadel/client";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { redirect, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { Alert, AlertType } from "./alert";
import { SessionClearItem } from "./session-clear-item";
type Props = {
sessions: Session[];
postLogoutRedirectUri?: string;
loginHint?: string;
organization?: string;
};
export function SessionsClearList({ sessions, postLogoutRedirectUri }: Props) {
export function SessionsClearList({
sessions,
loginHint,
postLogoutRedirectUri,
organization,
}: Props) {
const t = useTranslations("logout");
const [list, setList] = useState<Session[]>(sessions);
const router = useRouter();
async function clearHintedSession() {
// If a login hint is provided, we logout that specific session
const sessionIdToBeCleared = sessions.find((session) => {
return session.factors?.user?.loginName === loginHint;
})?.id;
if (sessionIdToBeCleared) {
const clearSessionResponse = await clearSession({
sessionId: sessionIdToBeCleared,
});
if (!clearSessionResponse) {
console.error("Failed to clear session for login hint:", loginHint);
}
if (postLogoutRedirectUri) {
return redirect(postLogoutRedirectUri);
}
const params = new URLSearchParams();
if (organization) {
params.set("organization", organization);
}
return router.push("/logout/success?" + params);
} else {
console.warn(`No session found for login hint: ${loginHint}`);
}
}
useEffect(() => {
clearHintedSession();
}, []);
return sessions ? (
<div className="flex flex-col space-y-2">
{list

View File

@@ -202,30 +202,6 @@ export async function clearSession(options: ClearSessionOptions) {
const { sessionId } = options;
const session = await getSessionCookieById({ sessionId });
const deletedSession = await deleteSession({
serviceUrl,
sessionId: session.id,
sessionToken: session.token,
});
const securitySettings = await getSecuritySettings({ serviceUrl });
const sameSite = securitySettings?.embeddedIframe?.enabled ? "none" : true;
if (deletedSession) {
return removeSessionFromCookie({ session, sameSite });
}
}
type CleanupSessionCommand = {
sessionId: string;
};
export async function cleanupSession({ sessionId }: CleanupSessionCommand) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const sessionCookie = await getSessionCookieById({ sessionId });
const deleteResponse = await deleteSession({