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 ( + props.onClose(false)} + negativeButton={{ + text: "Close", + onClick: () => props.onClose(false) + }} + width={500} + > + + {sessions.status === "pending" ? ( + + ) : sessions.status === "rejected" ? ( + + ) : !sessions.value ? ( + + {strings.somethingWentWrong()} + + ) : ( + + {sessions.value.map((session) => ( + + ))} + + )} + + + ); + } +); + +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.` };