From adebb59df4d138cb4c079f197aaf42c8fb6b5435 Mon Sep 17 00:00:00 2001
From: 01zulfi <85733202+01zulfi@users.noreply.github.com>
Date: Wed, 19 Nov 2025 16:42:00 +0500
Subject: [PATCH] web: show active login sessions Signed-off-by: 01zulfi
<85733202+01zulfi@users.noreply.github.com>
---
apps/web/src/components/icons/index.tsx | 4 +-
.../src/dialogs/settings/profile-settings.ts | 16 ++
apps/web/src/dialogs/user-sessions-dialog.tsx | 187 ++++++++++++++++++
packages/core/src/api/user-manager.ts | 19 +-
packages/core/src/types.ts | 8 +
packages/intl/locale/en.po | 20 ++
packages/intl/locale/pseudo-LOCALE.po | 20 ++
packages/intl/src/strings.ts | 7 +-
8 files changed, 275 insertions(+), 6 deletions(-)
create mode 100644 apps/web/src/dialogs/user-sessions-dialog.tsx
diff --git a/apps/web/src/components/icons/index.tsx b/apps/web/src/components/icons/index.tsx
index 3a636ed44..babb73447 100644
--- a/apps/web/src/components/icons/index.tsx
+++ b/apps/web/src/components/icons/index.tsx
@@ -231,7 +231,8 @@ import {
mdiNotePlus,
mdiNoteEditOutline,
mdiArrowUp,
- mdiInbox
+ mdiInbox,
+ mdiMonitor
} from "@mdi/js";
import { useTheme } from "@emotion/react";
import { Theme } from "@notesnook/theme";
@@ -586,3 +587,4 @@ export const ExpandSidebar = createIcon(mdiArrowCollapseRight);
export const HamburgerMenu = createIcon(mdiMenu);
export const ArrowUp = createIcon(mdiArrowUp);
export const Inbox = createIcon(mdiInbox);
+export const Monitor = createIcon(mdiMonitor);
diff --git a/apps/web/src/dialogs/settings/profile-settings.ts b/apps/web/src/dialogs/settings/profile-settings.ts
index 41ea73096..735df506b 100644
--- a/apps/web/src/dialogs/settings/profile-settings.ts
+++ b/apps/web/src/dialogs/settings/profile-settings.ts
@@ -35,6 +35,7 @@ import { EmailChangeDialog } from "../email-change-dialog";
import { RecoveryKeyDialog } from "../recovery-key-dialog";
import { UserProfile } from "./components/user-profile";
import { SettingsGroup } from "./types";
+import { UserSessionsDialog } from "../user-sessions-dialog";
export const ProfileSettings: SettingsGroup[] = [
{
@@ -131,6 +132,21 @@ export const ProfileSettings: SettingsGroup[] = [
},
isHidden: () => !useUserStore.getState().isLoggedIn,
settings: [
+ {
+ key: "login-sessions",
+ title: strings.loginSessions(),
+ description: strings.loginSessionsDesc(),
+ components: [
+ {
+ type: "button",
+ title: strings.viewSessions(),
+ variant: "secondary",
+ action: async () => {
+ await UserSessionsDialog.show({});
+ }
+ }
+ ]
+ },
{
key: "logout",
title: strings.logout(),
diff --git a/apps/web/src/dialogs/user-sessions-dialog.tsx b/apps/web/src/dialogs/user-sessions-dialog.tsx
new file mode 100644
index 000000000..a2a6ba31b
--- /dev/null
+++ b/apps/web/src/dialogs/user-sessions-dialog.tsx
@@ -0,0 +1,187 @@
+/*
+This file is part of the Notesnook project (https://notesnook.com/)
+
+Copyright (C) 2023 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+import { getFormattedDate, usePromise } from "@notesnook/common";
+import { DialogManager } from "../common/dialog-manager";
+import { db } from "../common/db";
+import Dialog from "../components/dialog";
+import { Button, Flex, Text } from "@theme-ui/components";
+import { Loader } from "../components/loader";
+import { ScrollContainer } from "@notesnook/ui";
+import { ErrorText } from "../components/error-text";
+import { Cellphone, FileWebClip, Monitor } from "../components/icons";
+import { logger, UserSession } from "@notesnook/core";
+import { showToast } from "../utils/toast";
+import { useState } from "react";
+import { strings } from "@notesnook/intl";
+
+export const UserSessionsDialog = DialogManager.register(
+ function UserSessionsDialog(props) {
+ const sessions = usePromise(() => db.user.getSessions());
+
+ return (
+
+ );
+ }
+);
+
+function SessionItem({
+ session,
+ onLogout
+}: {
+ session: UserSession;
+ onLogout: () => void;
+}) {
+ const [isLoggingOut, setIsLoggingOut] = useState(false);
+ const type = session.browser?.toLowerCase().includes("electron")
+ ? "desktop"
+ : session.platform?.toLowerCase().includes("ios") ||
+ session.platform?.toLowerCase().includes("android")
+ ? "mobile"
+ : "web";
+
+ return (
+
+
+ {type === "desktop" ? (
+
+ ) : type === "mobile" ? (
+
+ ) : (
+
+ )}
+
+ {session.browser || "Unknown Browser"}
+
+ {session.isCurrentDevice && (
+
+ Current Device
+
+ )}
+
+
+ {session.platform && (
+
+ {session.platform}
+
+ )}
+
+
+
+ Created: {getFormattedDate(session.createdAt, "date-time")}
+
+
+
+ {!session.isCurrentDevice && (
+
+ )}
+
+ );
+}
diff --git a/packages/core/src/api/user-manager.ts b/packages/core/src/api/user-manager.ts
index bd6875833..81c3874bf 100644
--- a/packages/core/src/api/user-manager.ts
+++ b/packages/core/src/api/user-manager.ts
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
-import { User } from "../types.js";
+import { User, UserSession } from "../types.js";
import http from "../utils/http.js";
import constants from "../utils/constants.js";
import TokenManager from "./token-manager.js";
@@ -243,10 +243,10 @@ class UserManager {
EV.publish(EVENTS.userLoggedIn, user);
}
- async getSessions() {
+ async getSessions(): Promise {
const token = await this.tokenManager.getAccessToken();
- if (!token) return;
- await http.get(`${constants.AUTH_HOST}/account/sessions`, token);
+ if (!token) return [];
+ return await http.get(`${constants.AUTH_HOST}/account/sessions`, token);
}
async clearSessions(all = false) {
@@ -286,6 +286,17 @@ class UserManager {
}
}
+ async logoutSession(sessionKey: string) {
+ const token = await this.tokenManager.getAccessToken();
+ if (!token) return [];
+
+ await http.post(
+ `${constants.AUTH_HOST}/account/clear-session`,
+ { sessionKey },
+ token
+ );
+ }
+
setUser(user: User) {
return this.db.kv().write("user", user);
}
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index 5b3403b76..69fa493d5 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -615,6 +615,14 @@ export type User = {
};
};
+export type UserSession = {
+ sessionKey: string;
+ createdAt: number;
+ isCurrentDevice: boolean;
+ browser?: string;
+ platform?: string;
+};
+
export type Profile = {
fullName?: string;
profilePicture?: string;
diff --git a/packages/intl/locale/en.po b/packages/intl/locale/en.po
index 43a2b8f5d..c80ceb506 100644
--- a/packages/intl/locale/en.po
+++ b/packages/intl/locale/en.po
@@ -2737,6 +2737,10 @@ msgstr "Failed to download file"
msgid "Failed to install theme."
msgstr "Failed to install theme."
+#: src/strings.ts:2613
+msgid "Failed to log out session."
+msgstr "Failed to log out session."
+
#: src/strings.ts:947
msgid "Failed to open"
msgstr "Failed to open"
@@ -3691,6 +3695,10 @@ msgstr "Login failed"
msgid "Login required"
msgstr "Login required"
+#: src/strings.ts:2609
+msgid "Login sessions"
+msgstr "Login sessions"
+
#: src/strings.ts:778
msgid "Login successful"
msgstr "Login successful"
@@ -5778,6 +5786,10 @@ msgstr "Servers configuration"
msgid "Session expired"
msgstr "Session expired"
+#: src/strings.ts:2612
+msgid "Session logged out"
+msgstr "Session logged out"
+
#: src/strings.ts:2190
msgid "Sessions"
msgstr "Sessions"
@@ -7010,6 +7022,10 @@ msgstr "Videos"
msgid "View all linked notebooks"
msgstr "View all linked notebooks"
+#: src/strings.ts:2610
+msgid "View and manage your active login sessions."
+msgstr "View and manage your active login sessions."
+
#: src/strings.ts:1269
msgid "View and share debug logs"
msgstr "View and share debug logs"
@@ -7022,6 +7038,10 @@ msgstr "View receipt"
msgid "View recovery codes"
msgstr "View recovery codes"
+#: src/strings.ts:2611
+msgid "View sessions"
+msgstr "View sessions"
+
#: src/strings.ts:2151
msgid "View source code"
msgstr "View source code"
diff --git a/packages/intl/locale/pseudo-LOCALE.po b/packages/intl/locale/pseudo-LOCALE.po
index 2b257cbea..f256f5299 100644
--- a/packages/intl/locale/pseudo-LOCALE.po
+++ b/packages/intl/locale/pseudo-LOCALE.po
@@ -2726,6 +2726,10 @@ msgstr ""
msgid "Failed to install theme."
msgstr ""
+#: src/strings.ts:2613
+msgid "Failed to log out session."
+msgstr ""
+
#: src/strings.ts:947
msgid "Failed to open"
msgstr ""
@@ -3671,6 +3675,10 @@ msgstr ""
msgid "Login required"
msgstr ""
+#: src/strings.ts:2609
+msgid "Login sessions"
+msgstr ""
+
#: src/strings.ts:778
msgid "Login successful"
msgstr ""
@@ -5752,6 +5760,10 @@ msgstr ""
msgid "Session expired"
msgstr ""
+#: src/strings.ts:2612
+msgid "Session logged out"
+msgstr ""
+
#: src/strings.ts:2190
msgid "Sessions"
msgstr ""
@@ -6961,6 +6973,10 @@ msgstr ""
msgid "View all linked notebooks"
msgstr ""
+#: src/strings.ts:2610
+msgid "View and manage your active login sessions."
+msgstr ""
+
#: src/strings.ts:1269
msgid "View and share debug logs"
msgstr ""
@@ -6973,6 +6989,10 @@ msgstr ""
msgid "View recovery codes"
msgstr ""
+#: src/strings.ts:2611
+msgid "View sessions"
+msgstr ""
+
#: src/strings.ts:2151
msgid "View source code"
msgstr ""
diff --git a/packages/intl/src/strings.ts b/packages/intl/src/strings.ts
index f9f9e0223..5220c8e25 100644
--- a/packages/intl/src/strings.ts
+++ b/packages/intl/src/strings.ts
@@ -2605,5 +2605,10 @@ Use this if changes from other devices are not appearing on this device. This wi
clickToDirectlyClaimPromo: () =>
t`Click here to directly claim the promotion.`,
loginToUploadAttachments: () =>
- t`Login to upload attachments. [Read more](https://help.notesnook.com/faqs/login-to-upload-attachments)`
+ t`Login to upload attachments. [Read more](https://help.notesnook.com/faqs/login-to-upload-attachments)`,
+ loginSessions: () => t`Login sessions`,
+ loginSessionsDesc: () => t`View and manage your active login sessions.`,
+ viewSessions: () => t`View sessions`,
+ sessionLoggedOut: () => t`Session logged out`,
+ failedToLogoutSession: () => t`Failed to log out session.`
};