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() {
|
windowResize() {
|
||||||
this.executeRebuild()
|
this.executeRebuild()
|
||||||
},
|
},
|
||||||
socketInit() {
|
|
||||||
// Server settings are set on socket init
|
|
||||||
this.executeRebuild()
|
|
||||||
},
|
|
||||||
initListeners() {
|
initListeners() {
|
||||||
window.addEventListener('resize', this.windowResize)
|
window.addEventListener('resize', this.windowResize)
|
||||||
|
|
||||||
|
@ -794,7 +790,6 @@ export default {
|
||||||
})
|
})
|
||||||
|
|
||||||
this.$eventBus.$on('bookshelf_clear_selection', this.clearSelectedEntities)
|
this.$eventBus.$on('bookshelf_clear_selection', this.clearSelectedEntities)
|
||||||
this.$eventBus.$on('socket_init', this.socketInit)
|
|
||||||
this.$eventBus.$on('user-settings', this.settingsUpdated)
|
this.$eventBus.$on('user-settings', this.settingsUpdated)
|
||||||
|
|
||||||
if (this.$root.socket) {
|
if (this.$root.socket) {
|
||||||
|
@ -826,7 +821,6 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$eventBus.$off('bookshelf_clear_selection', this.clearSelectedEntities)
|
this.$eventBus.$off('bookshelf_clear_selection', this.clearSelectedEntities)
|
||||||
this.$eventBus.$off('socket_init', this.socketInit)
|
|
||||||
this.$eventBus.$off('user-settings', this.settingsUpdated)
|
this.$eventBus.$off('user-settings', this.settingsUpdated)
|
||||||
|
|
||||||
if (this.$root.socket) {
|
if (this.$root.socket) {
|
||||||
|
|
|
@ -33,6 +33,7 @@ export default {
|
||||||
return {
|
return {
|
||||||
socket: null,
|
socket: null,
|
||||||
isSocketConnected: false,
|
isSocketConnected: false,
|
||||||
|
isSocketAuthenticated: false,
|
||||||
isFirstSocketConnection: true,
|
isFirstSocketConnection: true,
|
||||||
socketConnectionToastId: null,
|
socketConnectionToastId: null,
|
||||||
currentLang: null,
|
currentLang: null,
|
||||||
|
@ -81,9 +82,28 @@ export default {
|
||||||
document.body.classList.add('app-bar')
|
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) {
|
updateSocketConnectionToast(content, type, timeout) {
|
||||||
if (this.socketConnectionToastId !== null && this.socketConnectionToastId !== undefined) {
|
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 {
|
} else {
|
||||||
this.socketConnectionToastId = this.$toast[type](content, { position: 'bottom-center', timeout: timeout, closeButton: false, closeOnClick: timeout !== null })
|
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)
|
this.updateSocketConnectionToast(this.$strings.ToastSocketDisconnected, 'error', null)
|
||||||
},
|
},
|
||||||
reconnect() {
|
reconnect() {
|
||||||
console.error('[SOCKET] reconnected')
|
console.log('[SOCKET] reconnected')
|
||||||
},
|
},
|
||||||
reconnectAttempt(val) {
|
reconnectAttempt(val) {
|
||||||
console.log(`[SOCKET] reconnect attempt ${val}`)
|
console.log(`[SOCKET] reconnect attempt ${val}`)
|
||||||
|
@ -120,6 +140,10 @@ export default {
|
||||||
reconnectFailed() {
|
reconnectFailed() {
|
||||||
console.error('[SOCKET] reconnect failed')
|
console.error('[SOCKET] reconnect failed')
|
||||||
},
|
},
|
||||||
|
authFailed(payload) {
|
||||||
|
console.error('[SOCKET] auth failed', payload.message)
|
||||||
|
this.isSocketAuthenticated = false
|
||||||
|
},
|
||||||
init(payload) {
|
init(payload) {
|
||||||
console.log('Init Payload', payload)
|
console.log('Init Payload', payload)
|
||||||
|
|
||||||
|
@ -127,7 +151,7 @@ export default {
|
||||||
this.$store.commit('users/setUsersOnline', payload.usersOnline)
|
this.$store.commit('users/setUsersOnline', payload.usersOnline)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$eventBus.$emit('socket_init')
|
this.isSocketAuthenticated = true
|
||||||
},
|
},
|
||||||
streamOpen(stream) {
|
streamOpen(stream) {
|
||||||
if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.streamOpen(stream)
|
if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.streamOpen(stream)
|
||||||
|
@ -354,6 +378,15 @@ export default {
|
||||||
this.$store.commit('scanners/removeCustomMetadataProvider', provider)
|
this.$store.commit('scanners/removeCustomMetadataProvider', provider)
|
||||||
},
|
},
|
||||||
initializeSocket() {
|
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({
|
this.socket = this.$nuxtSocket({
|
||||||
name: process.env.NODE_ENV === 'development' ? 'dev' : 'prod',
|
name: process.env.NODE_ENV === 'development' ? 'dev' : 'prod',
|
||||||
persist: 'main',
|
persist: 'main',
|
||||||
|
@ -364,6 +397,7 @@ export default {
|
||||||
path: `${this.$config.routerBasePath}/socket.io`
|
path: `${this.$config.routerBasePath}/socket.io`
|
||||||
})
|
})
|
||||||
this.$root.socket = this.socket
|
this.$root.socket = this.socket
|
||||||
|
this.isSocketAuthenticated = false
|
||||||
console.log('Socket initialized')
|
console.log('Socket initialized')
|
||||||
|
|
||||||
// Pre-defined socket events
|
// Pre-defined socket events
|
||||||
|
@ -377,6 +411,7 @@ export default {
|
||||||
|
|
||||||
// Event received after authorizing socket
|
// Event received after authorizing socket
|
||||||
this.socket.on('init', this.init)
|
this.socket.on('init', this.init)
|
||||||
|
this.socket.on('auth_failed', this.authFailed)
|
||||||
|
|
||||||
// Stream Listeners
|
// Stream Listeners
|
||||||
this.socket.on('stream_open', this.streamOpen)
|
this.socket.on('stream_open', this.streamOpen)
|
||||||
|
@ -571,6 +606,7 @@ export default {
|
||||||
this.updateBodyClass()
|
this.updateBodyClass()
|
||||||
this.resize()
|
this.resize()
|
||||||
this.$eventBus.$on('change-lang', this.changeLanguage)
|
this.$eventBus.$on('change-lang', this.changeLanguage)
|
||||||
|
this.$eventBus.$on('token_refreshed', this.tokenRefreshed)
|
||||||
window.addEventListener('resize', this.resize)
|
window.addEventListener('resize', this.resize)
|
||||||
window.addEventListener('keydown', this.keyDown)
|
window.addEventListener('keydown', this.keyDown)
|
||||||
|
|
||||||
|
@ -594,6 +630,7 @@ export default {
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$eventBus.$off('change-lang', this.changeLanguage)
|
this.$eventBus.$off('change-lang', this.changeLanguage)
|
||||||
|
this.$eventBus.$off('token_refreshed', this.tokenRefreshed)
|
||||||
window.removeEventListener('resize', this.resize)
|
window.removeEventListener('resize', this.resize)
|
||||||
window.removeEventListener('keydown', this.keyDown)
|
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
|
// Track if we're currently refreshing to prevent multiple refresh attempts
|
||||||
let isRefreshing = false
|
let isRefreshing = false
|
||||||
let failedQueue = []
|
let failedQueue = []
|
||||||
|
@ -82,6 +82,11 @@ export default function ({ $axios, store, $config, app }) {
|
||||||
// Update the token in store and localStorage
|
// Update the token in store and localStorage
|
||||||
store.commit('user/setUser', response.user)
|
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
|
// Update the original request with new token
|
||||||
if (!originalRequest.headers) {
|
if (!originalRequest.headers) {
|
||||||
originalRequest.headers = {}
|
originalRequest.headers = {}
|
||||||
|
|
|
@ -1054,6 +1054,8 @@ class Auth {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to validate a jwt token for a given user
|
* 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
|
* @param {string} token
|
||||||
* @returns {Object} tokens data
|
* @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
|
* 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
|
* 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 {SocketIO.Socket} socket
|
||||||
* @param {string} token JWT
|
* @param {string} token JWT
|
||||||
*/
|
*/
|
||||||
|
@ -242,7 +245,7 @@ class SocketAuthority {
|
||||||
if (!token_data?.userId) {
|
if (!token_data?.userId) {
|
||||||
// Token invalid
|
// Token invalid
|
||||||
Logger.error('Cannot validate socket - invalid token')
|
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.
|
// get the user via the id from the decoded jwt.
|
||||||
|
@ -250,7 +253,11 @@ class SocketAuthority {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
// user not found
|
// user not found
|
||||||
Logger.error('Cannot validate socket - invalid token')
|
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]
|
const client = this.clients[socket.id]
|
||||||
|
@ -260,13 +267,18 @@ class SocketAuthority {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (client.user !== undefined) {
|
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
|
client.user = user
|
||||||
|
|
||||||
Logger.debug(`[SocketAuthority] User Online ${client.user.username}`)
|
|
||||||
|
|
||||||
this.adminEmitter('user_online', client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions))
|
this.adminEmitter('user_online', client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions))
|
||||||
|
|
||||||
// Update user lastSeen without firing sequelize bulk update hooks
|
// Update user lastSeen without firing sequelize bulk update hooks
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue