mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 15:07:32 +00:00
logout success page
This commit is contained in:
@@ -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!",
|
||||||
|
@@ -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!",
|
||||||
|
@@ -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!",
|
||||||
|
@@ -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!",
|
||||||
|
@@ -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!",
|
||||||
|
@@ -12,7 +12,11 @@
|
|||||||
"title": "Выход",
|
"title": "Выход",
|
||||||
"description": "Выберите аккаунт, который хотите удалить",
|
"description": "Выберите аккаунт, который хотите удалить",
|
||||||
"noResults": "Аккаунты не найдены",
|
"noResults": "Аккаунты не найдены",
|
||||||
"clear": "Удалить"
|
"clear": "Удалить",
|
||||||
|
"success": {
|
||||||
|
"title": "Выход выполнен успешно",
|
||||||
|
"description": "Вы успешно вышли из системы."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"loginname": {
|
"loginname": {
|
||||||
"title": "С возвращением!",
|
"title": "С возвращением!",
|
||||||
|
@@ -12,7 +12,11 @@
|
|||||||
"title": "注销",
|
"title": "注销",
|
||||||
"description": "选择您想注销的账户",
|
"description": "选择您想注销的账户",
|
||||||
"noResults": "未找到账户",
|
"noResults": "未找到账户",
|
||||||
"clear": "清除"
|
"clear": "清除",
|
||||||
|
"success": {
|
||||||
|
"title": "注销成功",
|
||||||
|
"description": "您已成功注销。"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"loginname": {
|
"loginname": {
|
||||||
"title": "欢迎回来!",
|
"title": "欢迎回来!",
|
||||||
|
@@ -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>
|
||||||
|
41
apps/login/src/app/(login)/logout/success/page.tsx
Normal file
41
apps/login/src/app/(login)/logout/success/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@@ -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();
|
||||||
|
@@ -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();
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
@@ -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();
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
@@ -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
|
||||||
|
@@ -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({
|
||||||
|
Reference in New Issue
Block a user