Compare commits

...

1 Commits

Author SHA1 Message Date
01zulfi
adebb59df4 web: show active login sessions
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-11-19 16:42:00 +05:00
8 changed files with 275 additions and 6 deletions

View File

@@ -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);

View File

@@ -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(),

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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 (
<Dialog
isOpen={true}
title={strings.loginSessions()}
description={strings.loginSessionsDesc()}
onClose={() => props.onClose(false)}
negativeButton={{
text: "Close",
onClick: () => props.onClose(false)
}}
width={500}
>
<ScrollContainer
style={{
display: "flex",
flexDirection: "column",
maxHeight: 400
}}
>
{sessions.status === "pending" ? (
<Loader title={strings.loading()} />
) : sessions.status === "rejected" ? (
<ErrorText error={sessions.reason} />
) : !sessions.value ? (
<Flex
sx={{
flexDirection: "column",
alignItems: "center",
py: 4,
color: "paragraph"
}}
>
<Text variant="body">{strings.somethingWentWrong()}</Text>
</Flex>
) : (
<Flex sx={{ flexDirection: "row", gap: 2, flexWrap: "wrap" }}>
{sessions.value.map((session) => (
<SessionItem
key={session.sessionKey}
session={session}
onLogout={sessions.refresh}
/>
))}
</Flex>
)}
</ScrollContainer>
</Dialog>
);
}
);
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 (
<Flex
sx={{
flexDirection: "column",
gap: 1,
p: 3,
borderRadius: "default",
border: "1px solid",
borderColor: "border"
}}
>
<Flex sx={{ alignItems: "center", gap: 1 }}>
{type === "desktop" ? (
<Monitor size={16} />
) : type === "mobile" ? (
<Cellphone size={16} />
) : (
<FileWebClip size={16} />
)}
<Text
variant="subtitle"
sx={{
fontWeight: "bold",
color: session.isCurrentDevice ? "accent" : "heading"
}}
>
{session.browser || "Unknown Browser"}
</Text>
{session.isCurrentDevice && (
<Text
variant="subBody"
sx={{
py: 0.8,
px: 1,
color: "accent",
border: "1px solid",
borderColor: "accent",
fontWeight: "bold",
borderRadius: "default"
}}
>
Current Device
</Text>
)}
</Flex>
{session.platform && (
<Text variant="body" sx={{ color: "paragraph" }}>
{session.platform}
</Text>
)}
<Flex sx={{ gap: 2 }}>
<Text variant="subBody" sx={{ color: "fontTertiary" }}>
Created: {getFormattedDate(session.createdAt, "date-time")}
</Text>
</Flex>
{!session.isCurrentDevice && (
<Button
variant="errorSecondary"
onClick={async () => {
try {
setIsLoggingOut(true);
await db.user.logoutSession(session.sessionKey);
showToast("success", strings.sessionLoggedOut());
onLogout();
} catch (e) {
if (e instanceof Error) {
logger.error(e, "Failed to logout session:");
}
showToast("error", strings.failedToLogoutSession());
} finally {
setIsLoggingOut(false);
}
}}
disabled={isLoggingOut}
>
{isLoggingOut ? strings.loggingOut() : strings.logout()}
</Button>
)}
</Flex>
);
}

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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<UserSession[]> {
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);
}

View File

@@ -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;

View File

@@ -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"

View File

@@ -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 ""

View File

@@ -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.`
};