mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-27 03:31:46 +00:00
Add /me/sessions endpoint and show sessions table on account page
This commit is contained in:
parent
4c71a076e7
commit
35a2c5e87c
5 changed files with 130 additions and 1 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="page-wrapper" class="page p-6 overflow-y-auto relative" :class="streamLibraryItem ? 'streaming' : ''">
|
<div id="page-wrapper" class="page p-6 overflow-y-auto relative" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
<div class="w-full max-w-xl mx-auto">
|
<div class="w-full max-w-2xl mx-auto">
|
||||||
<h1 class="text-2xl">{{ $strings.HeaderAccount }}</h1>
|
<h1 class="text-2xl">{{ $strings.HeaderAccount }}</h1>
|
||||||
|
|
||||||
<div class="my-4">
|
<div class="my-4">
|
||||||
|
|
@ -69,6 +69,38 @@
|
||||||
</app-settings-content>
|
</app-settings-content>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!isGuest">
|
||||||
|
<div class="w-full h-px bg-white/10 my-4" />
|
||||||
|
|
||||||
|
<app-settings-content :header-text="$strings.HeaderSessions">
|
||||||
|
<table v-if="authSessions.length" class="tracksTable mt-4 table-fixed w-full">
|
||||||
|
<tr>
|
||||||
|
<th class="text-left">{{ $strings.LabelDeviceInfo }}</th>
|
||||||
|
<th class="text-left hidden sm:table-cell w-36">{{ $strings.LabelIpAddress }}</th>
|
||||||
|
<th class="text-left w-32 sm:w-40">{{ $strings.LabelLastUpdate }}</th>
|
||||||
|
</tr>
|
||||||
|
<tr v-for="session in authSessions" :key="session.id">
|
||||||
|
<td class="max-w-0">
|
||||||
|
<div class="flex items-center gap-0.5 min-w-0">
|
||||||
|
<span class="text-sm text-gray-100 truncate min-w-0" :title="session.userAgent">{{ getSessionDeviceLabel(session) }}</span>
|
||||||
|
<ui-tooltip v-if="session.current" direction="top" :text="$strings.LabelCurrent" class="inline-flex shrink-0">
|
||||||
|
<span class="material-symbols text-success text-sm leading-none">check</span>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="hidden sm:table-cell w-36">
|
||||||
|
<p class="text-sm text-gray-100 truncate" :title="session.ipAddress">{{ session.ipAddress || '-' }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="w-32 sm:w-40">
|
||||||
|
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDatetime(session.updatedAt, dateFormat, timeFormat)">
|
||||||
|
<p class="text-xs sm:text-sm text-gray-100">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</app-settings-content>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="py-4 mt-8 flex flex-wrap gap-2">
|
<div class="py-4 mt-8 flex flex-wrap gap-2">
|
||||||
<ui-btn v-if="!isGuest" color="bg-primary flex items-center text-lg" :disabled="loggingOut" @click="logout(true)"> <span class="material-symbols mr-4 icon-text">devices</span>{{ $strings.ButtonLogoutAllDevices }} </ui-btn>
|
<ui-btn v-if="!isGuest" color="bg-primary flex items-center text-lg" :disabled="loggingOut" @click="logout(true)"> <span class="material-symbols mr-4 icon-text">devices</span>{{ $strings.ButtonLogoutAllDevices }} </ui-btn>
|
||||||
<ui-btn color="bg-primary flex items-center text-lg" :disabled="loggingOut" @click="logout"><span class="material-symbols mr-4 icon-text">logout</span>{{ $strings.ButtonLogout }}</ui-btn>
|
<ui-btn color="bg-primary flex items-center text-lg" :disabled="loggingOut" @click="logout"><span class="material-symbols mr-4 icon-text">logout</span>{{ $strings.ButtonLogout }}</ui-btn>
|
||||||
|
|
@ -89,6 +121,7 @@ export default {
|
||||||
confirmPassword: null,
|
confirmPassword: null,
|
||||||
changingPassword: false,
|
changingPassword: false,
|
||||||
loggingOut: false,
|
loggingOut: false,
|
||||||
|
authSessions: [],
|
||||||
selectedLanguage: '',
|
selectedLanguage: '',
|
||||||
newEReaderDevice: {
|
newEReaderDevice: {
|
||||||
name: '',
|
name: '',
|
||||||
|
|
@ -131,6 +164,12 @@ export default {
|
||||||
},
|
},
|
||||||
revisedEreaderDevices() {
|
revisedEreaderDevices() {
|
||||||
return this.ereaderDevices.filter((device) => device.users?.length === 1)
|
return this.ereaderDevices.filter((device) => device.users?.length === 1)
|
||||||
|
},
|
||||||
|
dateFormat() {
|
||||||
|
return this.$store.getters['getServerSetting']('dateFormat')
|
||||||
|
},
|
||||||
|
timeFormat() {
|
||||||
|
return this.$store.getters['getServerSetting']('timeFormat')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -245,11 +284,34 @@ export default {
|
||||||
},
|
},
|
||||||
ereaderDevicesUpdated(ereaderDevices) {
|
ereaderDevicesUpdated(ereaderDevices) {
|
||||||
this.ereaderDevices = ereaderDevices
|
this.ereaderDevices = ereaderDevices
|
||||||
|
},
|
||||||
|
loadAuthSessions() {
|
||||||
|
this.$axios
|
||||||
|
.$get('/api/me/sessions')
|
||||||
|
.then((data) => {
|
||||||
|
this.authSessions = data.sessions || []
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to load sessions', error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getSessionDeviceLabel(session) {
|
||||||
|
const deviceInfo = session.deviceInfo
|
||||||
|
if (!deviceInfo) return session.userAgent || '-'
|
||||||
|
|
||||||
|
const parts = []
|
||||||
|
if (deviceInfo.model) parts.push(deviceInfo.model)
|
||||||
|
if (deviceInfo.osName) parts.push(`${deviceInfo.osName}${deviceInfo.osVersion ? ` ${deviceInfo.osVersion}` : ''}`)
|
||||||
|
if (deviceInfo.browserName) parts.push(deviceInfo.browserName)
|
||||||
|
return parts.join(' / ') || session.userAgent || '-'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.selectedLanguage = this.$languageCodes.current
|
this.selectedLanguage = this.$languageCodes.current
|
||||||
this.ereaderDevices = this.$store.state.libraries.ereaderDevices || []
|
this.ereaderDevices = this.$store.state.libraries.ereaderDevices || []
|
||||||
|
if (!this.isGuest) {
|
||||||
|
this.loadAuthSessions()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,7 @@
|
||||||
"HeaderScheduleEpisodeDownloads": "Schedule Automatic Episode Downloads",
|
"HeaderScheduleEpisodeDownloads": "Schedule Automatic Episode Downloads",
|
||||||
"HeaderScheduleLibraryScans": "Schedule Automatic Library Scans",
|
"HeaderScheduleLibraryScans": "Schedule Automatic Library Scans",
|
||||||
"HeaderSession": "Session",
|
"HeaderSession": "Session",
|
||||||
|
"HeaderSessions": "Sessions",
|
||||||
"HeaderSetBackupSchedule": "Set Backup Schedule",
|
"HeaderSetBackupSchedule": "Set Backup Schedule",
|
||||||
"HeaderSettings": "Settings",
|
"HeaderSettings": "Settings",
|
||||||
"HeaderSettingsDisplay": "Display",
|
"HeaderSettingsDisplay": "Display",
|
||||||
|
|
@ -416,6 +417,7 @@
|
||||||
"LabelIntervalEveryHour": "Every hour",
|
"LabelIntervalEveryHour": "Every hour",
|
||||||
"LabelIntervalEveryMinute": "Every minute",
|
"LabelIntervalEveryMinute": "Every minute",
|
||||||
"LabelInvert": "Invert",
|
"LabelInvert": "Invert",
|
||||||
|
"LabelIpAddress": "IP Address",
|
||||||
"LabelItem": "Item",
|
"LabelItem": "Item",
|
||||||
"LabelJumpBackwardAmount": "Jump backward amount",
|
"LabelJumpBackwardAmount": "Jump backward amount",
|
||||||
"LabelJumpForwardAmount": "Jump forward amount",
|
"LabelJumpForwardAmount": "Jump forward amount",
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
const { Request, Response } = require('express')
|
const { Request, Response } = require('express')
|
||||||
|
const { Op } = require('sequelize')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const SocketAuthority = require('../SocketAuthority')
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
const Database = require('../Database')
|
const Database = require('../Database')
|
||||||
const { sort } = require('../libs/fastSort')
|
const { sort } = require('../libs/fastSort')
|
||||||
const { toNumber, isNullOrNaN } = require('../utils/index')
|
const { toNumber, isNullOrNaN } = require('../utils/index')
|
||||||
const userStats = require('../utils/queries/userStats')
|
const userStats = require('../utils/queries/userStats')
|
||||||
|
const parseUserAgent = require('../utils/parsers/parseUserAgent')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef RequestUserObject
|
* @typedef RequestUserObject
|
||||||
|
|
@ -26,6 +28,40 @@ class MeController {
|
||||||
res.json(req.user.toOldJSONForBrowser())
|
res.json(req.user.toOldJSONForBrowser())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET: /api/me/sessions
|
||||||
|
*
|
||||||
|
* @param {RequestWithUser} req
|
||||||
|
* @param {Response} res
|
||||||
|
*/
|
||||||
|
async getSessions(req, res) {
|
||||||
|
if (req.user.isGuest) {
|
||||||
|
return res.json({ sessions: [] })
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshToken = req.cookies.refresh_token || req.headers['x-refresh-token']
|
||||||
|
const sessions = await Database.sessionModel.findAll({
|
||||||
|
where: {
|
||||||
|
userId: req.user.id,
|
||||||
|
expiresAt: { [Op.gt]: new Date() }
|
||||||
|
},
|
||||||
|
order: [['updatedAt', 'DESC']]
|
||||||
|
})
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
sessions: sessions.map((session) => ({
|
||||||
|
id: session.id,
|
||||||
|
ipAddress: session.ipAddress,
|
||||||
|
userAgent: session.userAgent,
|
||||||
|
// For display convenience
|
||||||
|
deviceInfo: parseUserAgent(session.userAgent),
|
||||||
|
createdAt: session.createdAt?.valueOf() ?? null,
|
||||||
|
updatedAt: session.updatedAt?.valueOf() ?? null,
|
||||||
|
current: !!refreshToken && (session.refreshToken === refreshToken || session.lastRefreshToken === refreshToken)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET: /api/me/progress
|
* GET: /api/me/progress
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,7 @@ class ApiRouter {
|
||||||
// Current User Routes (Me)
|
// Current User Routes (Me)
|
||||||
//
|
//
|
||||||
this.router.get('/me', MeController.getCurrentUser.bind(this))
|
this.router.get('/me', MeController.getCurrentUser.bind(this))
|
||||||
|
this.router.get('/me/sessions', MeController.getSessions.bind(this))
|
||||||
this.router.get('/me/progress', MeController.getAllMediaProgress.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', MeController.getAllBookmarks.bind(this))
|
||||||
this.router.get('/me/bookmarks/:libraryItemId', MeController.getBookmarksForLibraryItem.bind(this))
|
this.router.get('/me/bookmarks/:libraryItemId', MeController.getBookmarksForLibraryItem.bind(this))
|
||||||
|
|
|
||||||
28
server/utils/parsers/parseUserAgent.js
Normal file
28
server/utils/parsers/parseUserAgent.js
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
const uaParserJs = require('../../libs/uaParser')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string|null|undefined} userAgent
|
||||||
|
* @returns {{ browserName?:string, browserVersion?:string, osName?:string, osVersion?:string, deviceType?:string, model?:string, vendor?:string }|null}
|
||||||
|
*/
|
||||||
|
function parseUserAgent(userAgent) {
|
||||||
|
if (!userAgent) return null
|
||||||
|
|
||||||
|
const ua = uaParserJs(userAgent)
|
||||||
|
const deviceInfo = {
|
||||||
|
browserName: ua?.browser?.name || undefined,
|
||||||
|
browserVersion: ua?.browser?.version || undefined,
|
||||||
|
osName: ua?.os?.name || undefined,
|
||||||
|
osVersion: ua?.os?.version || undefined,
|
||||||
|
deviceType: ua?.device?.type || undefined,
|
||||||
|
model: ua?.device?.model || undefined,
|
||||||
|
vendor: ua?.device?.vendor || undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in deviceInfo) {
|
||||||
|
if (deviceInfo[key] === undefined) delete deviceInfo[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(deviceInfo).length ? deviceInfo : null
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = parseUserAgent
|
||||||
Loading…
Add table
Add a link
Reference in a new issue