mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-07-13 19:04:57 +02:00
Handle socket re-authentication, fix socket toast to be re-usable, socket cleanup
This commit is contained in:
parent
a24dae5262
commit
e201247d69
5 changed files with 66 additions and 16 deletions
|
@ -778,10 +778,6 @@ export default {
|
|||
windowResize() {
|
||||
this.executeRebuild()
|
||||
},
|
||||
socketInit() {
|
||||
// Server settings are set on socket init
|
||||
this.executeRebuild()
|
||||
},
|
||||
initListeners() {
|
||||
window.addEventListener('resize', this.windowResize)
|
||||
|
||||
|
@ -794,7 +790,6 @@ export default {
|
|||
})
|
||||
|
||||
this.$eventBus.$on('bookshelf_clear_selection', this.clearSelectedEntities)
|
||||
this.$eventBus.$on('socket_init', this.socketInit)
|
||||
this.$eventBus.$on('user-settings', this.settingsUpdated)
|
||||
|
||||
if (this.$root.socket) {
|
||||
|
@ -826,7 +821,6 @@ export default {
|
|||
}
|
||||
|
||||
this.$eventBus.$off('bookshelf_clear_selection', this.clearSelectedEntities)
|
||||
this.$eventBus.$off('socket_init', this.socketInit)
|
||||
this.$eventBus.$off('user-settings', this.settingsUpdated)
|
||||
|
||||
if (this.$root.socket) {
|
||||
|
|
|
@ -33,6 +33,7 @@ export default {
|
|||
return {
|
||||
socket: null,
|
||||
isSocketConnected: false,
|
||||
isSocketAuthenticated: false,
|
||||
isFirstSocketConnection: true,
|
||||
socketConnectionToastId: null,
|
||||
currentLang: null,
|
||||
|
@ -81,9 +82,28 @@ export default {
|
|||
document.body.classList.add('app-bar')
|
||||
}
|
||||
},
|
||||
tokenRefreshed(newAccessToken) {
|
||||
if (this.isSocketConnected && !this.isSocketAuthenticated) {
|
||||
console.log('[SOCKET] Re-authenticating socket after token refresh')
|
||||
this.socket.emit('auth', newAccessToken)
|
||||
}
|
||||
},
|
||||
updateSocketConnectionToast(content, type, timeout) {
|
||||
if (this.socketConnectionToastId !== null && this.socketConnectionToastId !== undefined) {
|
||||
this.$toast.update(this.socketConnectionToastId, { content: content, options: { timeout: timeout, type: type, closeButton: false, position: 'bottom-center', onClose: () => null, closeOnClick: timeout !== null } }, false)
|
||||
const toastUpdateOptions = {
|
||||
content: content,
|
||||
options: {
|
||||
timeout: timeout,
|
||||
type: type,
|
||||
closeButton: false,
|
||||
position: 'bottom-center',
|
||||
onClose: () => {
|
||||
this.socketConnectionToastId = null
|
||||
},
|
||||
closeOnClick: timeout !== null
|
||||
}
|
||||
}
|
||||
this.$toast.update(this.socketConnectionToastId, toastUpdateOptions, false)
|
||||
} else {
|
||||
this.socketConnectionToastId = this.$toast[type](content, { position: 'bottom-center', timeout: timeout, closeButton: false, closeOnClick: timeout !== null })
|
||||
}
|
||||
|
@ -109,7 +129,7 @@ export default {
|
|||
this.updateSocketConnectionToast(this.$strings.ToastSocketDisconnected, 'error', null)
|
||||
},
|
||||
reconnect() {
|
||||
console.error('[SOCKET] reconnected')
|
||||
console.log('[SOCKET] reconnected')
|
||||
},
|
||||
reconnectAttempt(val) {
|
||||
console.log(`[SOCKET] reconnect attempt ${val}`)
|
||||
|
@ -120,6 +140,10 @@ export default {
|
|||
reconnectFailed() {
|
||||
console.error('[SOCKET] reconnect failed')
|
||||
},
|
||||
authFailed(payload) {
|
||||
console.error('[SOCKET] auth failed', payload.message)
|
||||
this.isSocketAuthenticated = false
|
||||
},
|
||||
init(payload) {
|
||||
console.log('Init Payload', payload)
|
||||
|
||||
|
@ -127,7 +151,7 @@ export default {
|
|||
this.$store.commit('users/setUsersOnline', payload.usersOnline)
|
||||
}
|
||||
|
||||
this.$eventBus.$emit('socket_init')
|
||||
this.isSocketAuthenticated = true
|
||||
},
|
||||
streamOpen(stream) {
|
||||
if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.streamOpen(stream)
|
||||
|
@ -354,6 +378,15 @@ export default {
|
|||
this.$store.commit('scanners/removeCustomMetadataProvider', provider)
|
||||
},
|
||||
initializeSocket() {
|
||||
if (this.$root.socket) {
|
||||
// Can happen in dev due to hot reload
|
||||
console.warn('Socket already initialized')
|
||||
this.socket = this.$root.socket
|
||||
this.isSocketConnected = this.$root.socket?.connected
|
||||
this.isFirstSocketConnection = false
|
||||
this.socketConnectionToastId = null
|
||||
return
|
||||
}
|
||||
this.socket = this.$nuxtSocket({
|
||||
name: process.env.NODE_ENV === 'development' ? 'dev' : 'prod',
|
||||
persist: 'main',
|
||||
|
@ -364,6 +397,7 @@ export default {
|
|||
path: `${this.$config.routerBasePath}/socket.io`
|
||||
})
|
||||
this.$root.socket = this.socket
|
||||
this.isSocketAuthenticated = false
|
||||
console.log('Socket initialized')
|
||||
|
||||
// Pre-defined socket events
|
||||
|
@ -377,6 +411,7 @@ export default {
|
|||
|
||||
// Event received after authorizing socket
|
||||
this.socket.on('init', this.init)
|
||||
this.socket.on('auth_failed', this.authFailed)
|
||||
|
||||
// Stream Listeners
|
||||
this.socket.on('stream_open', this.streamOpen)
|
||||
|
@ -571,6 +606,7 @@ export default {
|
|||
this.updateBodyClass()
|
||||
this.resize()
|
||||
this.$eventBus.$on('change-lang', this.changeLanguage)
|
||||
this.$eventBus.$on('token_refreshed', this.tokenRefreshed)
|
||||
window.addEventListener('resize', this.resize)
|
||||
window.addEventListener('keydown', this.keyDown)
|
||||
|
||||
|
@ -594,6 +630,7 @@ export default {
|
|||
},
|
||||
beforeDestroy() {
|
||||
this.$eventBus.$off('change-lang', this.changeLanguage)
|
||||
this.$eventBus.$off('token_refreshed', this.tokenRefreshed)
|
||||
window.removeEventListener('resize', this.resize)
|
||||
window.removeEventListener('keydown', this.keyDown)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export default function ({ $axios, store, $config, app }) {
|
||||
export default function ({ $axios, store, $root, app }) {
|
||||
// Track if we're currently refreshing to prevent multiple refresh attempts
|
||||
let isRefreshing = false
|
||||
let failedQueue = []
|
||||
|
@ -82,6 +82,11 @@ export default function ({ $axios, store, $config, app }) {
|
|||
// Update the token in store and localStorage
|
||||
store.commit('user/setUser', response.user)
|
||||
|
||||
// Emit event used to re-authenticate socket in default.vue since $root is not available here
|
||||
if (app.$eventBus) {
|
||||
app.$eventBus.$emit('token_refreshed', newAccessToken)
|
||||
}
|
||||
|
||||
// Update the original request with new token
|
||||
if (!originalRequest.headers) {
|
||||
originalRequest.headers = {}
|
||||
|
|
|
@ -1054,6 +1054,8 @@ class Auth {
|
|||
|
||||
/**
|
||||
* Function to validate a jwt token for a given user
|
||||
* Used to authenticate socket connections
|
||||
* TODO: Support API keys for web socket connections
|
||||
*
|
||||
* @param {string} token
|
||||
* @returns {Object} tokens data
|
||||
|
|
|
@ -231,6 +231,9 @@ class SocketAuthority {
|
|||
* When setting up a socket connection the user needs to be associated with a socket id
|
||||
* for this the client will send a 'auth' event that includes the users API token
|
||||
*
|
||||
* Sends event 'init' to the socket. For admins this contains an array of users online.
|
||||
* For failed authentication it sends event 'auth_failed' with a message
|
||||
*
|
||||
* @param {SocketIO.Socket} socket
|
||||
* @param {string} token JWT
|
||||
*/
|
||||
|
@ -242,7 +245,7 @@ class SocketAuthority {
|
|||
if (!token_data?.userId) {
|
||||
// Token invalid
|
||||
Logger.error('Cannot validate socket - invalid token')
|
||||
return socket.emit('invalid_token')
|
||||
return socket.emit('auth_failed', { message: 'Invalid token' })
|
||||
}
|
||||
|
||||
// get the user via the id from the decoded jwt.
|
||||
|
@ -250,7 +253,11 @@ class SocketAuthority {
|
|||
if (!user) {
|
||||
// user not found
|
||||
Logger.error('Cannot validate socket - invalid token')
|
||||
return socket.emit('invalid_token')
|
||||
return socket.emit('auth_failed', { message: 'Invalid token' })
|
||||
}
|
||||
if (!user.isActive) {
|
||||
Logger.error('Cannot validate socket - user is not active')
|
||||
return socket.emit('auth_failed', { message: 'Invalid user' })
|
||||
}
|
||||
|
||||
const client = this.clients[socket.id]
|
||||
|
@ -260,13 +267,18 @@ class SocketAuthority {
|
|||
}
|
||||
|
||||
if (client.user !== undefined) {
|
||||
Logger.debug(`[SocketAuthority] Authenticating socket client already has user`, client.user.username)
|
||||
if (client.user.id === user.id) {
|
||||
// Allow re-authentication of a socket to the same user
|
||||
Logger.info(`[SocketAuthority] Authenticating socket already associated to user "${client.user.username}"`)
|
||||
} else {
|
||||
// Allow re-authentication of a socket to a different user but shouldn't happen
|
||||
Logger.warn(`[SocketAuthority] Authenticating socket to user "${user.username}", but is already associated with a different user "${client.user.username}"`)
|
||||
}
|
||||
} else {
|
||||
Logger.debug(`[SocketAuthority] Authenticating socket to user "${user.username}"`)
|
||||
}
|
||||
|
||||
client.user = user
|
||||
|
||||
Logger.debug(`[SocketAuthority] User Online ${client.user.username}`)
|
||||
|
||||
this.adminEmitter('user_online', client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions))
|
||||
|
||||
// Update user lastSeen without firing sequelize bulk update hooks
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue