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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
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
6 changes: 6 additions & 0 deletions apps/login/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
"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": "Entfernen"
},
"loginname": {
"title": "Willkommen zurück!",
"description": "Geben Sie Ihre Anmeldedaten ein.",
Expand Down
6 changes: 6 additions & 0 deletions apps/login/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
"addAnother": "Add another account",
"noResults": "No accounts found"
},
"logout": {
"title": "Logout",
"description": "Click the accounts you want to clear",
"noResults": "No accounts found",
"clear": "Clear"
},
"loginname": {
"title": "Welcome back!",
"description": "Enter your login data.",
Expand Down
6 changes: 6 additions & 0 deletions apps/login/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
"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"
},
"loginname": {
"title": "¡Bienvenido de nuevo!",
"description": "Introduce tus datos de acceso.",
Expand Down
6 changes: 6 additions & 0 deletions apps/login/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
"addAnother": "Aggiungi un altro account",
"noResults": "Nessun account trovato"
},
"logout": {
"title": "Esci",
"description": "Seleziona l'account che desideri uscire",
"noResults": "Nessun account trovato",
"clear": "Rimuovi"
},
"loginname": {
"title": "Bentornato!",
"description": "Inserisci i tuoi dati di accesso.",
Expand Down
6 changes: 6 additions & 0 deletions apps/login/locales/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
"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ń"
},
"loginname": {
"title": "Witamy ponownie!",
"description": "Wprowadź dane logowania.",
Expand Down
6 changes: 6 additions & 0 deletions apps/login/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
"addAnother": "Добавить другой аккаунт",
"noResults": "Аккаунты не найдены"
},
"logout": {
"title": "Выход",
"description": "Выберите аккаунт, который хотите удалить",
"noResults": "Аккаунты не найдены",
"clear": "Удалить"
},
"loginname": {
"title": "С возвращением!",
"description": "Введите свои данные для входа.",
Expand Down
6 changes: 6 additions & 0 deletions apps/login/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
"addAnother": "添加另一个账户",
"noResults": "未找到账户"
},
"logout": {
"title": "注销",
"description": "选择您想注销的账户",
"noResults": "未找到账户",
"clear": "清除"
},
"loginname": {
"title": "欢迎回来!",
"description": "请输入您的登录信息。",
Expand Down
56 changes: 39 additions & 17 deletions apps/login/src/app/(login)/idp/[provider]/success/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { linkingFailed } from "@/components/idps/pages/linking-failed";
import { linkingSuccess } from "@/components/idps/pages/linking-success";
import { loginFailed } from "@/components/idps/pages/login-failed";
import { loginSuccess } from "@/components/idps/pages/login-success";
import { idpTypeToIdentityProviderType } from "@/lib/idp";
import { getServiceUrlFromHeaders } from "@/lib/service";
import {
addHuman,
Expand All @@ -16,10 +15,13 @@ import {
listUsers,
retrieveIDPIntent,
} from "@/lib/zitadel";
import { create } from "@zitadel/client";
import { ConnectError, create } from "@zitadel/client";
import { AutoLinkingOption } from "@zitadel/proto/zitadel/idp/v2/idp_pb";
import { OrganizationSchema } from "@zitadel/proto/zitadel/object/v2/object_pb";
import { AddHumanUserRequestSchema } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import {
AddHumanUserRequest,
AddHumanUserRequestSchema,
} from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers";

Expand Down Expand Up @@ -83,8 +85,6 @@ export default async function Page(props: {
throw new Error("IDP not found");
}

const providerType = idpTypeToIdentityProviderType(idp.type);

if (link) {
if (!options?.isLinkingAllowed) {
// linking was probably disallowed since the invitation was created
Expand Down Expand Up @@ -205,20 +205,42 @@ export default async function Page(props: {
}
}

if (addHumanUser && orgToRegisterOn) {
const organizationSchema = create(OrganizationSchema, {
org: { case: "orgId", value: orgToRegisterOn },
});
if (addHumanUser) {
let addHumanUserWithOrganization: AddHumanUserRequest;
if (orgToRegisterOn) {
const organizationSchema = create(OrganizationSchema, {
org: { case: "orgId", value: orgToRegisterOn },
});

const addHumanUserWithOrganization = create(AddHumanUserRequestSchema, {
...addHumanUser,
organization: organizationSchema,
});
addHumanUserWithOrganization = create(AddHumanUserRequestSchema, {
...addHumanUser,
organization: organizationSchema,
});
} else {
addHumanUserWithOrganization = create(
AddHumanUserRequestSchema,
addHumanUser,
);
}

newUser = await addHuman({
serviceUrl,
request: addHumanUserWithOrganization,
});
try {
newUser = await addHuman({
serviceUrl,
request: addHumanUserWithOrganization,
});
} catch (error: unknown) {
console.error(
"An error occurred while creating the user:",
error,
addHumanUser,
);
return loginFailed(
branding,
(error as ConnectError).message
? (error as ConnectError).message
: "Could not create user",
);
}
}

if (newUser) {
Expand Down
80 changes: 80 additions & 0 deletions apps/login/src/app/(login)/logout/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { DynamicTheme } from "@/components/dynamic-theme";
import { SessionsClearList } from "@/components/sessions-clear-list";
import { getAllSessionCookieIds } from "@/lib/cookies";
import { getServiceUrlFromHeaders } from "@/lib/service";
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 _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}
postLogoutRedirectUri={postLogoutRedirectUri}
/>
</div>
</div>
</DynamicTheme>
);
}
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
100 changes: 100 additions & 0 deletions apps/login/src/components/session-clear-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"use client";

import { cleanupSession } from "@/lib/server/session";
import { timestampDate } from "@zitadel/client";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import moment from "moment";
import { useLocale, useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { Avatar } from "./avatar";
import { isSessionValid } from "./session-item";

export function SessionClearItem({
session,
reload,
}: {
session: Session;
reload: () => void;
}) {
const t = useTranslations("logout");

const currentLocale = useLocale();
moment.locale(currentLocale === "zh" ? "zh-cn" : currentLocale);

const [loading, setLoading] = useState<boolean>(false);

async function clearSession(id: string) {
setLoading(true);
const response = await cleanupSession({
sessionId: id,
})
.catch((error) => {
setError(error.message);
return;
})
.finally(() => {
setLoading(false);
});

return response;
}

const { valid, verifiedAt } = isSessionValid(session);

const [error, setError] = useState<string | null>(null);

const router = useRouter();

return (
<button
onClick={async () => {
clearSession(session.id).then(() => {
reload();
});
}}
className="group flex flex-row items-center bg-background-light-400 dark:bg-background-dark-400 border border-divider-light hover:shadow-lg dark:hover:bg-white/10 py-2 px-4 rounded-md transition-all"
>
<div className="pr-4">
<Avatar
size="small"
loginName={session.factors?.user?.loginName as string}
name={session.factors?.user?.displayName ?? ""}
/>
</div>

<div className="flex flex-col items-start overflow-hidden">
<span className="">{session.factors?.user?.displayName}</span>
<span className="text-xs opacity-80 text-ellipsis">
{session.factors?.user?.loginName}
</span>
{valid ? (
<span className="text-xs opacity-80 text-ellipsis">
{verifiedAt && moment(timestampDate(verifiedAt)).fromNow()}
</span>
) : (
verifiedAt && (
<span className="text-xs opacity-80 text-ellipsis">
expired{" "}
{session.expirationDate &&
moment(timestampDate(session.expirationDate)).fromNow()}
</span>
)
)}
</div>

<span className="flex-grow"></span>
<div className="relative flex flex-row items-center">
<div className="mr-6 px-2 py-[2px] text-xs hidden group-hover:block transition-all text-warn-light-500 dark:text-warn-dark-500 bg-[#ff0000]/10 dark:bg-[#ff0000]/10 rounded-full flex items-center justify-center">
{t("clear")}
</div>

{valid ? (
<div className="absolute h-2 w-2 bg-green-500 rounded-full mx-2 transform right-0 transition-all"></div>
) : (
<div className="absolute h-2 w-2 bg-red-500 rounded-full mx-2 transform right-0 transition-all"></div>
)}
</div>
</button>
);
}
Loading
Loading