mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 17:37:32 +00:00
Merge commit '097adb9ab7c0e66d680614548137119c34af3ee4' into add-login-subtree
This commit is contained in:
@@ -65,7 +65,7 @@ You can already use the current state, and extend it with your needs.
|
|||||||
- [x] Apple
|
- [x] Apple
|
||||||
- [x] Generic OIDC
|
- [x] Generic OIDC
|
||||||
- [x] Generic OAuth
|
- [x] Generic OAuth
|
||||||
- [ ] Generic JWT
|
- [x] Generic JWT
|
||||||
- [ ] LDAP
|
- [ ] LDAP
|
||||||
- [ ] SAML SP
|
- [ ] SAML SP
|
||||||
- Multifactor Registration an Login
|
- Multifactor Registration an Login
|
||||||
@@ -73,7 +73,7 @@ You can already use the current state, and extend it with your needs.
|
|||||||
- [x] TOTP
|
- [x] TOTP
|
||||||
- [x] OTP: Email Code
|
- [x] OTP: Email Code
|
||||||
- [x] OTP: SMS Code
|
- [x] OTP: SMS Code
|
||||||
- [ ] Password Change/Reset
|
- [x] Password Change/Reset
|
||||||
- [x] Domain Discovery
|
- [x] Domain Discovery
|
||||||
- [x] Branding
|
- [x] Branding
|
||||||
- OIDC Standard
|
- OIDC Standard
|
||||||
|
@@ -8,6 +8,17 @@
|
|||||||
"addAnother": "Ein weiteres Konto hinzufügen",
|
"addAnother": "Ein weiteres Konto hinzufügen",
|
||||||
"noResults": "Keine Konten gefunden"
|
"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": {
|
"loginname": {
|
||||||
"title": "Willkommen zurück!",
|
"title": "Willkommen zurück!",
|
||||||
"description": "Geben Sie Ihre Anmeldedaten ein.",
|
"description": "Geben Sie Ihre Anmeldedaten ein.",
|
||||||
|
@@ -8,6 +8,17 @@
|
|||||||
"addAnother": "Add another account",
|
"addAnother": "Add another account",
|
||||||
"noResults": "No accounts found"
|
"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": {
|
"loginname": {
|
||||||
"title": "Welcome back!",
|
"title": "Welcome back!",
|
||||||
"description": "Enter your login data.",
|
"description": "Enter your login data.",
|
||||||
|
@@ -8,6 +8,17 @@
|
|||||||
"addAnother": "Agregar otra cuenta",
|
"addAnother": "Agregar otra cuenta",
|
||||||
"noResults": "No se encontraron cuentas"
|
"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": {
|
"loginname": {
|
||||||
"title": "¡Bienvenido de nuevo!",
|
"title": "¡Bienvenido de nuevo!",
|
||||||
"description": "Introduce tus datos de acceso.",
|
"description": "Introduce tus datos de acceso.",
|
||||||
|
@@ -8,6 +8,17 @@
|
|||||||
"addAnother": "Aggiungi un altro account",
|
"addAnother": "Aggiungi un altro account",
|
||||||
"noResults": "Nessun account trovato"
|
"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": {
|
"loginname": {
|
||||||
"title": "Bentornato!",
|
"title": "Bentornato!",
|
||||||
"description": "Inserisci i tuoi dati di accesso.",
|
"description": "Inserisci i tuoi dati di accesso.",
|
||||||
|
@@ -8,6 +8,17 @@
|
|||||||
"addAnother": "Dodaj kolejne konto",
|
"addAnother": "Dodaj kolejne konto",
|
||||||
"noResults": "Nie znaleziono kont"
|
"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": {
|
"loginname": {
|
||||||
"title": "Witamy ponownie!",
|
"title": "Witamy ponownie!",
|
||||||
"description": "Wprowadź dane logowania.",
|
"description": "Wprowadź dane logowania.",
|
||||||
|
@@ -8,6 +8,17 @@
|
|||||||
"addAnother": "Добавить другой аккаунт",
|
"addAnother": "Добавить другой аккаунт",
|
||||||
"noResults": "Аккаунты не найдены"
|
"noResults": "Аккаунты не найдены"
|
||||||
},
|
},
|
||||||
|
"logout": {
|
||||||
|
"title": "Выход",
|
||||||
|
"description": "Выберите аккаунт, который хотите удалить",
|
||||||
|
"noResults": "Аккаунты не найдены",
|
||||||
|
"clear": "Удалить сессию",
|
||||||
|
"verifiedAt": "Последняя активность: {time}",
|
||||||
|
"success": {
|
||||||
|
"title": "Выход выполнен успешно",
|
||||||
|
"description": "Вы успешно вышли из системы."
|
||||||
|
}
|
||||||
|
},
|
||||||
"loginname": {
|
"loginname": {
|
||||||
"title": "С возвращением!",
|
"title": "С возвращением!",
|
||||||
"description": "Введите свои данные для входа.",
|
"description": "Введите свои данные для входа.",
|
||||||
|
@@ -8,6 +8,17 @@
|
|||||||
"addAnother": "添加另一个账户",
|
"addAnother": "添加另一个账户",
|
||||||
"noResults": "未找到账户"
|
"noResults": "未找到账户"
|
||||||
},
|
},
|
||||||
|
"logout": {
|
||||||
|
"title": "注销",
|
||||||
|
"description": "选择您想注销的账户",
|
||||||
|
"noResults": "未找到账户",
|
||||||
|
"clear": "注销会话",
|
||||||
|
"verifiedAt": "最后活动时间:{time}",
|
||||||
|
"success": {
|
||||||
|
"title": "注销成功",
|
||||||
|
"description": "您已成功注销。"
|
||||||
|
}
|
||||||
|
},
|
||||||
"loginname": {
|
"loginname": {
|
||||||
"title": "欢迎回来!",
|
"title": "欢迎回来!",
|
||||||
"description": "请输入您的登录信息。",
|
"description": "请输入您的登录信息。",
|
||||||
|
@@ -120,7 +120,7 @@ export default async function Page(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// search for potential user via username, then link
|
// search for potential user via username, then link
|
||||||
if (options?.isLinkingAllowed) {
|
if (options?.autoLinking) {
|
||||||
let foundUser;
|
let foundUser;
|
||||||
const email = addHumanUser?.email?.email;
|
const email = addHumanUser?.email?.email;
|
||||||
|
|
||||||
@@ -176,7 +176,7 @@ export default async function Page(props: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.isCreationAllowed && options.isAutoCreation) {
|
if (options?.isAutoCreation) {
|
||||||
let orgToRegisterOn: string | undefined = organization;
|
let orgToRegisterOn: string | undefined = organization;
|
||||||
let newUser;
|
let newUser;
|
||||||
|
|
||||||
|
84
apps/login/src/app/(login)/logout/page.tsx
Normal file
84
apps/login/src/app/(login)/logout/page.tsx
Normal file
@@ -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
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();
|
||||||
|
@@ -43,7 +43,7 @@ export function LanguageSwitcher() {
|
|||||||
>
|
>
|
||||||
{selected.name}
|
{selected.name}
|
||||||
<ChevronDownIcon
|
<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"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</ListboxButton>
|
</ListboxButton>
|
||||||
@@ -61,7 +61,7 @@ export function LanguageSwitcher() {
|
|||||||
value={lang}
|
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"
|
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">
|
<div className="text-sm/6 text-black dark:text-white">
|
||||||
{lang.name}
|
{lang.name}
|
||||||
</div>
|
</div>
|
||||||
|
103
apps/login/src/components/session-clear-item.tsx
Normal file
103
apps/login/src/components/session-clear-item.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
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";
|
||||||
|
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 clearSessionId(id: string) {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await clearSession({
|
||||||
|
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 () => {
|
||||||
|
clearSessionId(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 &&
|
||||||
|
t("verfiedAt", {
|
||||||
|
time: 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>
|
||||||
|
);
|
||||||
|
}
|
@@ -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();
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
106
apps/login/src/components/sessions-clear-list.tsx
Normal file
106
apps/login/src/components/sessions-clear-list.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
"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 { 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;
|
||||||
|
logoutHint?: string;
|
||||||
|
organization?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SessionsClearList({
|
||||||
|
sessions,
|
||||||
|
logoutHint,
|
||||||
|
postLogoutRedirectUri,
|
||||||
|
organization,
|
||||||
|
}: Props) {
|
||||||
|
const t = useTranslations("logout");
|
||||||
|
const [list, setList] = useState<Session[]>(sessions);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
async function clearHintedSession() {
|
||||||
|
console.log("Clearing session for login hint:", logoutHint);
|
||||||
|
// If a login hint is provided, we logout that specific session
|
||||||
|
const sessionIdToBeCleared = sessions.find((session) => {
|
||||||
|
return session.factors?.user?.loginName === logoutHint;
|
||||||
|
})?.id;
|
||||||
|
|
||||||
|
if (sessionIdToBeCleared) {
|
||||||
|
const clearSessionResponse = await clearSession({
|
||||||
|
sessionId: sessionIdToBeCleared,
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("Error clearing session:", error);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!clearSessionResponse) {
|
||||||
|
console.error("Failed to clear session for login hint:", logoutHint);
|
||||||
|
}
|
||||||
|
|
||||||
|
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: ${logoutHint}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (logoutHint) {
|
||||||
|
clearHintedSession();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return sessions ? (
|
||||||
|
<div className="flex flex-col space-y-2">
|
||||||
|
{list
|
||||||
|
.filter((session) => session?.factors?.user?.loginName)
|
||||||
|
// sort by change date descending
|
||||||
|
.sort((a, b) => {
|
||||||
|
const dateA = a.changeDate
|
||||||
|
? timestampDate(a.changeDate).getTime()
|
||||||
|
: 0;
|
||||||
|
const dateB = b.changeDate
|
||||||
|
? timestampDate(b.changeDate).getTime()
|
||||||
|
: 0;
|
||||||
|
return dateB - dateA;
|
||||||
|
})
|
||||||
|
// TODO: add sorting to move invalid sessions to the bottom
|
||||||
|
.map((session, index) => {
|
||||||
|
return (
|
||||||
|
<SessionClearItem
|
||||||
|
session={session}
|
||||||
|
reload={() => {
|
||||||
|
setList(list.filter((s) => s.id !== session.id));
|
||||||
|
if (postLogoutRedirectUri) {
|
||||||
|
router.push(postLogoutRedirectUri);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
key={"session-" + index}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{list.length === 0 && (
|
||||||
|
<Alert type={AlertType.INFO}>{t("noResults")}</Alert>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Alert>{t("noResults")}</Alert>
|
||||||
|
);
|
||||||
|
}
|
@@ -53,6 +53,7 @@ export function SignInWithIdp({
|
|||||||
[IdentityProviderType.GITLAB]: SignInWithGitlab,
|
[IdentityProviderType.GITLAB]: SignInWithGitlab,
|
||||||
[IdentityProviderType.GITLAB_SELF_HOSTED]: SignInWithGitlab,
|
[IdentityProviderType.GITLAB_SELF_HOSTED]: SignInWithGitlab,
|
||||||
[IdentityProviderType.SAML]: SignInWithGeneric,
|
[IdentityProviderType.SAML]: SignInWithGeneric,
|
||||||
|
[IdentityProviderType.JWT]: SignInWithGeneric,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Component = components[type];
|
const Component = components[type];
|
||||||
|
@@ -141,7 +141,7 @@ export async function removeSessionFromCookie<T>({
|
|||||||
session: SessionCookie<T>;
|
session: SessionCookie<T>;
|
||||||
cleanup?: boolean;
|
cleanup?: boolean;
|
||||||
sameSite?: boolean | "lax" | "strict" | "none" | undefined;
|
sameSite?: boolean | "lax" | "strict" | "none" | undefined;
|
||||||
}): Promise<any> {
|
}) {
|
||||||
const cookiesList = await cookies();
|
const cookiesList = await cookies();
|
||||||
const stringifiedCookie = cookiesList.get("sessions");
|
const stringifiedCookie = cookiesList.get("sessions");
|
||||||
|
|
||||||
|
@@ -24,6 +24,8 @@ export function idpTypeToSlug(idpType: IdentityProviderType) {
|
|||||||
return "oauth";
|
return "oauth";
|
||||||
case IdentityProviderType.OIDC:
|
case IdentityProviderType.OIDC:
|
||||||
return "oidc";
|
return "oidc";
|
||||||
|
case IdentityProviderType.JWT:
|
||||||
|
return "jwt";
|
||||||
default:
|
default:
|
||||||
throw new Error("Unknown identity provider type");
|
throw new Error("Unknown identity provider type");
|
||||||
}
|
}
|
||||||
@@ -64,6 +66,9 @@ export function idpTypeToIdentityProviderType(
|
|||||||
case IDPType.IDP_TYPE_OIDC:
|
case IDPType.IDP_TYPE_OIDC:
|
||||||
return IdentityProviderType.OIDC;
|
return IdentityProviderType.OIDC;
|
||||||
|
|
||||||
|
case IDPType.IDP_TYPE_JWT:
|
||||||
|
return IdentityProviderType.JWT;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error("Unknown identity provider type");
|
throw new Error("Unknown identity provider type");
|
||||||
}
|
}
|
||||||
|
@@ -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