From 1876bfbfe74a9de5d8d99b7c7899c2d83ea1e299 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 26 Jul 2026 17:36:36 -0500 Subject: [PATCH] Add delete auth session endpoint to logout individual session --- client/pages/account.vue | 45 ++++++++++++++++++++++++++++-- client/strings/en-us.json | 1 + server/controllers/MeController.js | 34 +++++++++++++++++++++- server/routers/ApiRouter.js | 1 + 4 files changed, 78 insertions(+), 3 deletions(-) diff --git a/client/pages/account.vue b/client/pages/account.vue index 398a21c88..c371a969d 100644 --- a/client/pages/account.vue +++ b/client/pages/account.vue @@ -77,7 +77,8 @@ {{ $strings.LabelDeviceInfo }} {{ $strings.LabelIpAddress }} - {{ $strings.LabelLastUpdate }} + {{ $strings.LabelLastUpdate }} + @@ -91,11 +92,16 @@

{{ session.ipAddress || '-' }}

- +

{{ $dateDistanceFromNow(session.updatedAt) }}

+ +
+ +
+
@@ -131,6 +137,7 @@ export default { authSessionsNumPages: 0, authSessionsItemsPerPage: 10, loadingAuthSessions: false, + deletingSessionId: null, selectedLanguage: '', newEReaderDevice: { name: '', @@ -322,6 +329,40 @@ export default { if (this.authSessionsPage >= this.authSessionsNumPages - 1) return this.loadAuthSessions(this.authSessionsPage + 1) }, + deleteAuthSessionClick(session) { + this.$store.commit('globals/setConfirmPrompt', { + message: this.$getString('MessageConfirmLogoutDevice', [this.getSessionDeviceLabel(session)]), + callback: (confirmed) => { + if (confirmed) this.deleteAuthSession(session) + }, + type: 'yesNo' + }) + }, + deleteAuthSession(session) { + // Call logout instead for current session + if (session.current) { + this.logout(false) + return + } + + this.deletingSessionId = session.id + this.$axios + .$delete(`/api/me/sessions/${session.id}`) + .then(() => { + if (this.authSessions.length === 1 && this.authSessionsPage > 0) { + this.loadAuthSessions(this.authSessionsPage - 1) + } else { + this.loadAuthSessions(this.authSessionsPage) + } + }) + .catch((error) => { + console.error('Failed to delete session', error) + this.$toast.error(this.$strings.ToastFailedToDelete) + }) + .finally(() => { + this.deletingSessionId = null + }) + }, getSessionDeviceLabel(session) { const deviceInfo = session.deviceInfo if (!deviceInfo) return session.userAgent || '-' diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 684927124..d9d6325e4 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -777,6 +777,7 @@ "MessageConfirmDeleteSession": "Are you sure you want to delete this session?", "MessageConfirmEmbedMetadataInAudioFiles": "Are you sure you want to embed metadata in {0} audio files?", "MessageConfirmForceReScan": "Are you sure you want to force re-scan?", + "MessageConfirmLogoutDevice": "Are you sure you want to log out of \"{0}\"?", "MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?", "MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?", "MessageConfirmMarkItemFinished": "Are you sure you want to mark \"{0}\" as finished?", diff --git a/server/controllers/MeController.js b/server/controllers/MeController.js index cdf3034a1..73e9fec53 100644 --- a/server/controllers/MeController.js +++ b/server/controllers/MeController.js @@ -4,7 +4,7 @@ const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') const { sort } = require('../libs/fastSort') -const { toNumber, isNullOrNaN } = require('../utils/index') +const { toNumber, isNullOrNaN, isUUID } = require('../utils/index') const userStats = require('../utils/queries/userStats') const parseUserAgent = require('../utils/parsers/parseUserAgent') @@ -71,6 +71,38 @@ class MeController { }) } + /** + * DELETE: /api/me/sessions/:id + * + * @param {RequestWithUser} req + * @param {Response} res + */ + async deleteSession(req, res) { + if (req.user.isGuest) { + return res.sendStatus(403) + } + + if (!isUUID(req.params.id)) { + return res.sendStatus(400) + } + + const session = await Database.sessionModel.findOne({ + where: { + id: req.params.id, + userId: req.user.id + } + }) + + if (!session) { + return res.sendStatus(404) + } + + await Database.sessionModel.destroy({ where: { id: session.id } }) + Logger.info(`[MeController] User ${req.user.username} deleted auth session ${session.id}`) + + res.json({ success: true }) + } + /** * GET: /api/me/progress * diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 3ade851bb..7c89126b2 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -172,6 +172,7 @@ class ApiRouter { // this.router.get('/me', MeController.getCurrentUser.bind(this)) this.router.get('/me/sessions', MeController.getSessions.bind(this)) + this.router.delete('/me/sessions/:id', MeController.deleteSession.bind(this)) this.router.get('/me/progress', MeController.getAllMediaProgress.bind(this)) this.router.get('/me/bookmarks', MeController.getAllBookmarks.bind(this)) this.router.get('/me/bookmarks/:libraryItemId', MeController.getBookmarksForLibraryItem.bind(this))