mirror of
https://github.com/sudoxnym/audiobookshelf-atv.git
synced 2026-04-14 03:26:22 +00:00
Update readers to handle token refresh
This commit is contained in:
parent
80ee88b488
commit
f4e0a6121f
7 changed files with 243 additions and 42 deletions
|
|
@ -77,9 +77,6 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
userToken() {
|
||||
return this.$store.getters['user/getToken']
|
||||
},
|
||||
libraryItemId() {
|
||||
return this.libraryItem?.id
|
||||
},
|
||||
|
|
@ -247,10 +244,7 @@ export default {
|
|||
|
||||
// TODO: Handle JWT auth refresh
|
||||
const buff = await this.$axios.$get(this.url, {
|
||||
responseType: 'blob',
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.userToken}`
|
||||
}
|
||||
responseType: 'blob'
|
||||
})
|
||||
|
||||
const archive = await Archive.open(buff)
|
||||
|
|
|
|||
|
|
@ -49,9 +49,6 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
userToken() {
|
||||
return this.$store.getters['user/getToken']
|
||||
},
|
||||
/** @returns {string} */
|
||||
libraryItemId() {
|
||||
return this.libraryItem?.id
|
||||
|
|
@ -298,15 +295,26 @@ export default {
|
|||
|
||||
/** @type {EpubReader} */
|
||||
const reader = this
|
||||
|
||||
// Use axios to make request because we have token refresh logic in interceptor
|
||||
const customRequest = async (url) => {
|
||||
try {
|
||||
return this.$axios.$get(url, {
|
||||
responseType: 'arraybuffer'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('EpubReader.initEpub customRequest failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[EpubReader] initEpub', reader.url)
|
||||
/** @type {ePub.Book} */
|
||||
reader.book = new ePub(reader.url, {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight - this.readerHeightOffset,
|
||||
openAs: 'epub',
|
||||
requestHeaders: {
|
||||
Authorization: `Bearer ${this.userToken}`
|
||||
}
|
||||
requestMethod: this.isLocal ? null : customRequest
|
||||
})
|
||||
|
||||
/** @type {ePub.Rendition} */
|
||||
|
|
|
|||
|
|
@ -22,11 +22,7 @@ export default {
|
|||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
userToken() {
|
||||
return this.$store.getters['user/getToken']
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
addHtmlCss() {
|
||||
let iframe = document.getElementsByTagName('iframe')[0]
|
||||
|
|
@ -86,10 +82,7 @@ export default {
|
|||
// Fetch mobi file as blob
|
||||
// TODO: Handle JWT auth refresh
|
||||
var buff = await this.$axios.$get(this.url, {
|
||||
responseType: 'blob',
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.userToken}`
|
||||
}
|
||||
responseType: 'blob'
|
||||
})
|
||||
var reader = new FileReader()
|
||||
reader.onload = async (event) => {
|
||||
|
|
@ -134,4 +127,4 @@ export default {
|
|||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -49,7 +49,8 @@ export default {
|
|||
numPages: 0,
|
||||
windowWidth: 0,
|
||||
windowHeight: 0,
|
||||
pdfDocInitParams: null
|
||||
pdfDocInitParams: null,
|
||||
isRefreshing: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -109,6 +110,10 @@ export default {
|
|||
},
|
||||
isPlayerOpen() {
|
||||
return this.$store.getters['getIsPlayerOpen']
|
||||
},
|
||||
ebookUrl() {
|
||||
const serverAddress = this.$store.getters['user/getServerAddress']
|
||||
return this.isLocal ? this.url : `${serverAddress}${this.url}`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -164,7 +169,54 @@ export default {
|
|||
this.page++
|
||||
this.updateProgress()
|
||||
},
|
||||
error(err) {
|
||||
async handleRefreshFailure() {
|
||||
try {
|
||||
console.log('[PdfReader] Handling refresh failure - logging out user')
|
||||
|
||||
// Clear store
|
||||
await this.$store.dispatch('user/logout')
|
||||
|
||||
if (this.$store.getters['user/getServerConnectionConfigId']) {
|
||||
// Clear refresh token for server connection config
|
||||
await this.$db.clearRefreshToken(this.$store.getters['user/getServerConnectionConfigId'])
|
||||
}
|
||||
|
||||
if (window.location.pathname !== '/connect') {
|
||||
window.location.href = '/connect'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[PdfReader] Failed to handle refresh failure:', error)
|
||||
}
|
||||
},
|
||||
async refreshToken() {
|
||||
if (this.isRefreshing) return
|
||||
this.isRefreshing = true
|
||||
// Cannot use axios with this pdf reader so we need to handle the refresh separately
|
||||
// Should work on migrating to a different pdf reader in the future
|
||||
const newAccessToken = await this.$store.dispatch('user/refreshToken').catch((error) => {
|
||||
console.error('Failed to refresh token', error)
|
||||
return null
|
||||
})
|
||||
if (!newAccessToken) {
|
||||
this.handleRefreshFailure()
|
||||
return
|
||||
}
|
||||
|
||||
// Force Vue to re-render the PDF component by creating a new object
|
||||
this.pdfDocInitParams = {
|
||||
url: this.ebookUrl,
|
||||
httpHeaders: {
|
||||
Authorization: `Bearer ${newAccessToken}`
|
||||
}
|
||||
}
|
||||
this.isRefreshing = false
|
||||
},
|
||||
async error(err) {
|
||||
if (err && err.status === 401) {
|
||||
console.log('Received 401 error, refreshing token')
|
||||
await this.refreshToken()
|
||||
return
|
||||
}
|
||||
console.error(err)
|
||||
},
|
||||
screenOrientationChange() {
|
||||
|
|
@ -173,7 +225,7 @@ export default {
|
|||
},
|
||||
init() {
|
||||
this.pdfDocInitParams = {
|
||||
url: this.url,
|
||||
url: this.ebookUrl,
|
||||
httpHeaders: {
|
||||
Authorization: `Bearer ${this.userToken}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -305,11 +305,11 @@ export default {
|
|||
if (this.localContentUrl) {
|
||||
return Capacitor.convertFileSrc(this.localContentUrl)
|
||||
}
|
||||
const serverAddress = this.$store.getters['user/getServerAddress']
|
||||
|
||||
if (this.ebookFileId) {
|
||||
return `${serverAddress}/api/items/${this.selectedLibraryItem.id}/ebook/${this.ebookFileId}`
|
||||
return `/api/items/${this.selectedLibraryItem.id}/ebook/${this.ebookFileId}`
|
||||
}
|
||||
return `${serverAddress}/api/items/${this.selectedLibraryItem.id}/ebook`
|
||||
return `/api/items/${this.selectedLibraryItem.id}/ebook`
|
||||
},
|
||||
isPlayerOpen() {
|
||||
return this.$store.getters['getIsPlayerOpen']
|
||||
|
|
|
|||
121
plugins/axios.js
121
plugins/axios.js
|
|
@ -1,17 +1,50 @@
|
|||
export default function ({ $axios, store }) {
|
||||
export default function ({ $axios, store, $db }) {
|
||||
// Track if we're currently refreshing to prevent multiple refresh attempts
|
||||
let isRefreshing = false
|
||||
let failedQueue = []
|
||||
|
||||
const processQueue = (error, token = null) => {
|
||||
failedQueue.forEach(({ resolve, reject }) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(token)
|
||||
}
|
||||
})
|
||||
failedQueue = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the case when token refresh fails
|
||||
* @param {string} [serverConnectionConfigId]
|
||||
* @returns {Promise} - Promise that resolves when logout is complete
|
||||
*/
|
||||
const handleRefreshFailure = async (serverConnectionConfigId) => {
|
||||
try {
|
||||
console.log('[axios] Handling refresh failure - logging out user')
|
||||
|
||||
// Clear store
|
||||
await store.dispatch('user/logout')
|
||||
|
||||
if (serverConnectionConfigId) {
|
||||
// Clear refresh token for server connection config
|
||||
await $db.clearRefreshToken(serverConnectionConfigId)
|
||||
}
|
||||
|
||||
if (window.location.pathname !== '/connect') {
|
||||
window.location.href = '/connect'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[axios] Failed to handle refresh failure:', error)
|
||||
}
|
||||
}
|
||||
|
||||
$axios.onRequest((config) => {
|
||||
console.log('[Axios] Making request to ' + config.url)
|
||||
if (config.url.startsWith('http:') || config.url.startsWith('https:') || config.url.startsWith('capacitor:')) {
|
||||
return
|
||||
}
|
||||
|
||||
const customHeaders = store.getters['user/getCustomHeaders']
|
||||
if (customHeaders) {
|
||||
for (const key in customHeaders) {
|
||||
config.headers.common[key] = customHeaders[key]
|
||||
}
|
||||
}
|
||||
|
||||
const bearerToken = store.getters['user/getToken']
|
||||
if (bearerToken) {
|
||||
config.headers.common['Authorization'] = `Bearer ${bearerToken}`
|
||||
|
|
@ -26,7 +59,75 @@ export default function ({ $axios, store }) {
|
|||
console.log('[Axios] Request out', config.url)
|
||||
})
|
||||
|
||||
$axios.onError((error) => {
|
||||
console.error('Axios error code', error)
|
||||
$axios.onError(async (error) => {
|
||||
const originalRequest = error.config
|
||||
const code = parseInt(error.response && error.response.status)
|
||||
const message = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error'
|
||||
|
||||
console.error('Axios error', code, message)
|
||||
|
||||
// Handle 401 Unauthorized (token expired)
|
||||
if (code === 401 && !originalRequest._retry) {
|
||||
// Skip refresh for auth endpoints to prevent infinite loops
|
||||
if (originalRequest.url.endsWith('/auth/refresh') || originalRequest.url.endsWith('/login')) {
|
||||
await handleRefreshFailure(store.getters['user/getServerConnectionConfigId'])
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
if (isRefreshing) {
|
||||
// If already refreshing, queue this request
|
||||
return new Promise((resolve, reject) => {
|
||||
failedQueue.push({ resolve, reject })
|
||||
})
|
||||
.then((token) => {
|
||||
if (!originalRequest.headers) {
|
||||
originalRequest.headers = {}
|
||||
}
|
||||
originalRequest.headers['Authorization'] = `Bearer ${token}`
|
||||
return $axios(originalRequest)
|
||||
})
|
||||
.catch((err) => {
|
||||
return Promise.reject(err)
|
||||
})
|
||||
}
|
||||
|
||||
originalRequest._retry = true
|
||||
isRefreshing = true
|
||||
|
||||
try {
|
||||
// Attempt to refresh the token
|
||||
// Updates store if successful, otherwise clears store and throw error
|
||||
const newAccessToken = await store.dispatch('user/refreshToken')
|
||||
if (!newAccessToken) {
|
||||
console.error('No new access token received')
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
// Update the original request with new token
|
||||
if (!originalRequest.headers) {
|
||||
originalRequest.headers = {}
|
||||
}
|
||||
originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`
|
||||
|
||||
// Process any queued requests
|
||||
processQueue(null, newAccessToken)
|
||||
|
||||
// Retry the original request
|
||||
return $axios(originalRequest)
|
||||
} catch (refreshError) {
|
||||
console.error('Token refresh failed:', refreshError)
|
||||
|
||||
// Process queued requests with error
|
||||
processQueue(refreshError, null)
|
||||
|
||||
await handleRefreshFailure(store.getters['user/getServerConnectionConfigId'])
|
||||
|
||||
return Promise.reject(refreshError)
|
||||
} finally {
|
||||
isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Browser } from '@capacitor/browser'
|
||||
import { AbsLogger } from '@/plugins/capacitor'
|
||||
import { CapacitorHttp } from '@capacitor/core'
|
||||
|
||||
export const state = () => ({
|
||||
user: null,
|
||||
|
|
@ -30,9 +31,6 @@ export const getters = {
|
|||
getServerConfigName: (state) => {
|
||||
return state.serverConnectionConfig?.name || null
|
||||
},
|
||||
getCustomHeaders: (state) => {
|
||||
return state.serverConnectionConfig?.customHeaders || null
|
||||
},
|
||||
getUserMediaProgress:
|
||||
(state) =>
|
||||
(libraryItemId, episodeId = null) => {
|
||||
|
|
@ -165,6 +163,61 @@ export const actions = {
|
|||
commit('logout')
|
||||
commit('libraries/setCurrentLibrary', null, { root: true })
|
||||
await AbsLogger.info({ tag: 'user', message: `Logged out from server ${state.serverConnectionConfig?.name || 'Not connected'}` })
|
||||
},
|
||||
async refreshToken({ getters, commit, state }) {
|
||||
const refreshToken = await this.$db.getRefreshToken(getters.getServerConnectionConfigId)
|
||||
if (!refreshToken) {
|
||||
console.error('No refresh token found')
|
||||
return null
|
||||
}
|
||||
|
||||
const serverAddress = getters.getServerAddress
|
||||
|
||||
const response = await CapacitorHttp.post({
|
||||
url: `${serverAddress}/auth/refresh`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-refresh-token': refreshToken
|
||||
},
|
||||
data: {}
|
||||
})
|
||||
|
||||
if (response.status !== 200) {
|
||||
console.error('[user] Token refresh request failed:', response.status)
|
||||
return null
|
||||
}
|
||||
|
||||
const userResponseData = response.data
|
||||
if (!userResponseData.user?.accessToken) {
|
||||
console.error('[user] No access token in refresh response')
|
||||
return null
|
||||
}
|
||||
|
||||
// Update the config with new tokens
|
||||
const updatedConfig = {
|
||||
...state.serverConnectionConfig,
|
||||
token: userResponseData.user.accessToken,
|
||||
refreshToken: userResponseData.user.refreshToken
|
||||
}
|
||||
|
||||
// Save updated config to secure storage, persists refresh token in secure storage
|
||||
const savedConfig = await this.$db.setServerConnectionConfig(updatedConfig)
|
||||
|
||||
// Update the store
|
||||
commit('setAccessToken', userResponseData.user.accessToken)
|
||||
|
||||
// Re-authenticate socket if necessary
|
||||
if (this.$socket?.connected && !this.$socket.isAuthenticated) {
|
||||
this.$socket.sendAuthenticate()
|
||||
} else if (!this.$socket) {
|
||||
console.warn('[user] Socket not available, cannot re-authenticate')
|
||||
}
|
||||
|
||||
if (savedConfig) {
|
||||
commit('setServerConnectionConfig', savedConfig)
|
||||
}
|
||||
|
||||
return userResponseData.user.accessToken
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue