Skip to content

feat: Logout page #436

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jun 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions apps/login/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@
"addAnother": "Ein weiteres Konto hinzufügen",
"noResults": "Keine Konten gefunden"
},
"logout": {
"title": "Logout",
"description": "Wählen Sie den Account aus, das Sie entfernen möchten",
"noResults": "Keine Konten gefunden",
"clear": "Session beenden",
"verifiedAt": "Zuletzt aktiv: {time}",
"success": {
"title": "Logout erfolgreich",
"description": "Sie haben sich erfolgreich abgemeldet."
}
},
"loginname": {
"title": "Willkommen zurück!",
"description": "Geben Sie Ihre Anmeldedaten ein.",
Expand Down
11 changes: 11 additions & 0 deletions apps/login/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@
"addAnother": "Add another account",
"noResults": "No accounts found"
},
"logout": {
"title": "Logout",
"description": "Click an account to end the session",
"noResults": "No accounts found",
"clear": "End Session",
"verifiedAt": "Last active: {time}",
"success": {
"title": "Logout successful",
"description": "You have successfully logged out."
}
},
"loginname": {
"title": "Welcome back!",
"description": "Enter your login data.",
Expand Down
11 changes: 11 additions & 0 deletions apps/login/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@
"addAnother": "Agregar otra cuenta",
"noResults": "No se encontraron cuentas"
},
"logout": {
"title": "Cerrar sesión",
"description": "Selecciona la cuenta que deseas eliminar",
"noResults": "No se encontraron cuentas",
"clear": "Eliminar sesión",
"verifiedAt": "Última actividad: {time}",
"success": {
"title": "Cierre de sesión exitoso",
"description": "Has cerrado sesión correctamente."
}
},
"loginname": {
"title": "¡Bienvenido de nuevo!",
"description": "Introduce tus datos de acceso.",
Expand Down
11 changes: 11 additions & 0 deletions apps/login/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@
"addAnother": "Aggiungi un altro account",
"noResults": "Nessun account trovato"
},
"logout": {
"title": "Esci",
"description": "Seleziona l'account che desideri uscire",
"noResults": "Nessun account trovato",
"clear": "Elimina sessione",
"verifiedAt": "Ultima attività: {time}",
"success": {
"title": "Uscita riuscita",
"description": "Hai effettuato l'uscita con successo."
}
},
"loginname": {
"title": "Bentornato!",
"description": "Inserisci i tuoi dati di accesso.",
Expand Down
11 changes: 11 additions & 0 deletions apps/login/locales/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@
"addAnother": "Dodaj kolejne konto",
"noResults": "Nie znaleziono kont"
},
"logout": {
"title": "Wyloguj się",
"description": "Wybierz konto, które chcesz usunąć",
"noResults": "Nie znaleziono kont",
"clear": "Usuń sesję",
"verifiedAt": "Ostatnia aktywność: {time}",
"success": {
"title": "Wylogowanie udane",
"description": "Pomyślnie się wylogowałeś."
}
},
"loginname": {
"title": "Witamy ponownie!",
"description": "Wprowadź dane logowania.",
Expand Down
11 changes: 11 additions & 0 deletions apps/login/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@
"addAnother": "Добавить другой аккаунт",
"noResults": "Аккаунты не найдены"
},
"logout": {
"title": "Выход",
"description": "Выберите аккаунт, который хотите удалить",
"noResults": "Аккаунты не найдены",
"clear": "Удалить сессию",
"verifiedAt": "Последняя активность: {time}",
"success": {
"title": "Выход выполнен успешно",
"description": "Вы успешно вышли из системы."
}
},
"loginname": {
"title": "С возвращением!",
"description": "Введите свои данные для входа.",
Expand Down
11 changes: 11 additions & 0 deletions apps/login/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@
"addAnother": "添加另一个账户",
"noResults": "未找到账户"
},
"logout": {
"title": "注销",
"description": "选择您想注销的账户",
"noResults": "未找到账户",
"clear": "注销会话",
"verifiedAt": "最后活动时间:{time}",
"success": {
"title": "注销成功",
"description": "您已成功注销。"
}
},
"loginname": {
"title": "欢迎回来!",
"description": "请输入您的登录信息。",
Expand Down
84 changes: 84 additions & 0 deletions apps/login/src/app/(login)/logout/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { DynamicTheme } from "@/components/dynamic-theme";
import { SessionsClearList } from "@/components/sessions-clear-list";
import { getAllSessionCookieIds } from "@/lib/cookies";
import { getServiceUrlFromHeaders } from "@/lib/service-url";
import {
getBrandingSettings,
getDefaultOrg,
listSessions,
} 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";

async function loadSessions({ serviceUrl }: { serviceUrl: string }) {
const ids: (string | undefined)[] = await getAllSessionCookieIds();

if (ids && ids.length) {
const response = await listSessions({
serviceUrl,
ids: ids.filter((id) => !!id) as string[],
});
return response?.sessions ?? [];
} else {
console.info("No session cookie found.");
return [];
}
}

export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) {
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "logout" });

const organization = searchParams?.organization;
const postLogoutRedirectUri = searchParams?.post_logout_redirect_uri;
const logoutHint = searchParams?.logout_hint;
const UILocales = searchParams?.ui_locales; // TODO implement with new translation service

const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);

let defaultOrganization;
if (!organization) {
const org: Organization | null = await getDefaultOrg({
serviceUrl,
});
if (org) {
defaultOrganization = org.id;
}
}

let sessions = await loadSessions({ serviceUrl });

const branding = await getBrandingSettings({
serviceUrl,
organization: organization ?? defaultOrganization,
});

const params = new URLSearchParams();

if (organization) {
params.append("organization", organization);
}

return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>{t("title")}</h1>
<p className="ztdl-p mb-6 block">{t("description")}</p>

<div className="flex flex-col w-full space-y-2">
<SessionsClearList
sessions={sessions}
logoutHint={logoutHint}
postLogoutRedirectUri={postLogoutRedirectUri}
organization={organization ?? defaultOrganization}
/>
</div>
</div>
</DynamicTheme>
);
}
41 changes: 41 additions & 0 deletions apps/login/src/app/(login)/logout/success/page.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
19 changes: 0 additions & 19 deletions apps/login/src/app/(login)/verify/success/page.tsx
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
4 changes: 2 additions & 2 deletions apps/login/src/components/language-switcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function LanguageSwitcher() {
>
{selected.name}
<ChevronDownIcon
className="group pointer-events-none absolute top-2.5 right-2.5 size-4 fill-white/60"
className="group pointer-events-none absolute top-2.5 right-2.5 size-4"
aria-hidden="true"
/>
</ListboxButton>
Expand All @@ -61,7 +61,7 @@ export function LanguageSwitcher() {
value={lang}
className="group flex cursor-default items-center gap-2 rounded-lg py-1.5 px-3 select-none data-[focus]:bg-black/10 dark:data-[focus]:bg-white/10"
>
<CheckIcon className="invisible size-4 fill-white group-data-[selected]:visible" />
<CheckIcon className="invisible size-4 group-data-[selected]:visible" />
<div className="text-sm/6 text-black dark:text-white">
{lang.name}
</div>
Expand Down
Loading
Loading