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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { SessionsClearList } from "@/components/sessions-clear-list"; import { SessionsClearList } from "@/components/sessions-clear-list";
import { getAllSessionCookieIds } from "@/lib/cookies"; import { getAllSessionCookieIds } from "@/lib/cookies";
import { getServiceUrlFromHeaders } from "@/lib/service"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { import {
getBrandingSettings, getBrandingSettings,
getDefaultOrg, getDefaultOrg,
@@ -35,6 +35,7 @@ export default async function Page(props: {
const organization = searchParams?.organization; const organization = searchParams?.organization;
const postLogoutRedirectUri = searchParams?.post_logout_redirect_uri; const postLogoutRedirectUri = searchParams?.post_logout_redirect_uri;
const loginHint = searchParams?.login_hint;
const _headers = await headers(); const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_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"> <div className="flex flex-col w-full space-y-2">
<SessionsClearList <SessionsClearList
sessions={sessions} sessions={sessions}
loginHint={loginHint}
postLogoutRedirectUri={postLogoutRedirectUri} postLogoutRedirectUri={postLogoutRedirectUri}
organization={organization ?? defaultOrganization}
/> />
</div> </div>
</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 { DynamicTheme } from "@/components/dynamic-theme";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getSessionCookieById } from "@/lib/cookies";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { import {
getBrandingSettings, getBrandingSettings,
getLoginSettings, getLoginSettings,
getSession,
getUserByID, getUserByID,
} from "@/lib/zitadel"; } 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";
import { getLocale, getTranslations } from "next-intl/server"; import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers"; 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> }) { export default async function Page(props: { searchParams: Promise<any> }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale(); const locale = getLocale();

View File

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

View File

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

View File

@@ -1,23 +1,66 @@
"use client"; "use client";
import { clearSession } from "@/lib/server/session";
import { timestampDate } from "@zitadel/client"; import { timestampDate } from "@zitadel/client";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { redirect, useRouter } from "next/navigation";
import { useState } from "react"; import { useEffect, useState } from "react";
import { Alert, AlertType } from "./alert"; import { Alert, AlertType } from "./alert";
import { SessionClearItem } from "./session-clear-item"; import { SessionClearItem } from "./session-clear-item";
type Props = { type Props = {
sessions: Session[]; sessions: Session[];
postLogoutRedirectUri?: string; 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 t = useTranslations("logout");
const [list, setList] = useState<Session[]>(sessions); const [list, setList] = useState<Session[]>(sessions);
const router = useRouter(); 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 ? ( return sessions ? (
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
{list {list

View File

@@ -202,30 +202,6 @@ export async function clearSession(options: ClearSessionOptions) {
const { sessionId } = options; 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 sessionCookie = await getSessionCookieById({ sessionId });
const deleteResponse = await deleteSession({ const deleteResponse = await deleteSession({