mirror of
https://github.com/sudoxnym/audiobookshelf-atv.git
synced 2026-04-14 11:36:27 +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: {
|
computed: {
|
||||||
userToken() {
|
|
||||||
return this.$store.getters['user/getToken']
|
|
||||||
},
|
|
||||||
libraryItemId() {
|
libraryItemId() {
|
||||||
return this.libraryItem?.id
|
return this.libraryItem?.id
|
||||||
},
|
},
|
||||||
|
|
@ -247,10 +244,7 @@ export default {
|
||||||
|
|
||||||
// TODO: Handle JWT auth refresh
|
// TODO: Handle JWT auth refresh
|
||||||
const buff = await this.$axios.$get(this.url, {
|
const buff = await this.$axios.$get(this.url, {
|
||||||
responseType: 'blob',
|
responseType: 'blob'
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${this.userToken}`
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const archive = await Archive.open(buff)
|
const archive = await Archive.open(buff)
|
||||||
|
|
|
||||||
|
|
@ -49,9 +49,6 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
userToken() {
|
|
||||||
return this.$store.getters['user/getToken']
|
|
||||||
},
|
|
||||||
/** @returns {string} */
|
/** @returns {string} */
|
||||||
libraryItemId() {
|
libraryItemId() {
|
||||||
return this.libraryItem?.id
|
return this.libraryItem?.id
|
||||||
|
|
@ -298,15 +295,26 @@ export default {
|
||||||
|
|
||||||
/** @type {EpubReader} */
|
/** @type {EpubReader} */
|
||||||
const reader = this
|
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)
|
console.log('[EpubReader] initEpub', reader.url)
|
||||||
/** @type {ePub.Book} */
|
/** @type {ePub.Book} */
|
||||||
reader.book = new ePub(reader.url, {
|
reader.book = new ePub(reader.url, {
|
||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
height: window.innerHeight - this.readerHeightOffset,
|
height: window.innerHeight - this.readerHeightOffset,
|
||||||
openAs: 'epub',
|
openAs: 'epub',
|
||||||
requestHeaders: {
|
requestMethod: this.isLocal ? null : customRequest
|
||||||
Authorization: `Bearer ${this.userToken}`
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/** @type {ePub.Rendition} */
|
/** @type {ePub.Rendition} */
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,7 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {},
|
||||||
userToken() {
|
|
||||||
return this.$store.getters['user/getToken']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
addHtmlCss() {
|
addHtmlCss() {
|
||||||
let iframe = document.getElementsByTagName('iframe')[0]
|
let iframe = document.getElementsByTagName('iframe')[0]
|
||||||
|
|
@ -86,10 +82,7 @@ export default {
|
||||||
// Fetch mobi file as blob
|
// Fetch mobi file as blob
|
||||||
// TODO: Handle JWT auth refresh
|
// TODO: Handle JWT auth refresh
|
||||||
var buff = await this.$axios.$get(this.url, {
|
var buff = await this.$axios.$get(this.url, {
|
||||||
responseType: 'blob',
|
responseType: 'blob'
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${this.userToken}`
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
var reader = new FileReader()
|
var reader = new FileReader()
|
||||||
reader.onload = async (event) => {
|
reader.onload = async (event) => {
|
||||||
|
|
@ -134,4 +127,4 @@ export default {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,8 @@ export default {
|
||||||
numPages: 0,
|
numPages: 0,
|
||||||
windowWidth: 0,
|
windowWidth: 0,
|
||||||
windowHeight: 0,
|
windowHeight: 0,
|
||||||
pdfDocInitParams: null
|
pdfDocInitParams: null,
|
||||||
|
isRefreshing: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -109,6 +110,10 @@ export default {
|
||||||
},
|
},
|
||||||
isPlayerOpen() {
|
isPlayerOpen() {
|
||||||
return this.$store.getters['getIsPlayerOpen']
|
return this.$store.getters['getIsPlayerOpen']
|
||||||
|
},
|
||||||
|
ebookUrl() {
|
||||||
|
const serverAddress = this.$store.getters['user/getServerAddress']
|
||||||
|
return this.isLocal ? this.url : `${serverAddress}${this.url}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -164,7 +169,54 @@ export default {
|
||||||
this.page++
|
this.page++
|
||||||
this.updateProgress()
|
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)
|
console.error(err)
|
||||||
},
|
},
|
||||||
screenOrientationChange() {
|
screenOrientationChange() {
|
||||||
|
|
@ -173,7 +225,7 @@ export default {
|
||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
this.pdfDocInitParams = {
|
this.pdfDocInitParams = {
|
||||||
url: this.url,
|
url: this.ebookUrl,
|
||||||
httpHeaders: {
|
httpHeaders: {
|
||||||
Authorization: `Bearer ${this.userToken}`
|
Authorization: `Bearer ${this.userToken}`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -305,11 +305,11 @@ export default {
|
||||||
if (this.localContentUrl) {
|
if (this.localContentUrl) {
|
||||||
return Capacitor.convertFileSrc(this.localContentUrl)
|
return Capacitor.convertFileSrc(this.localContentUrl)
|
||||||
}
|
}
|
||||||
const serverAddress = this.$store.getters['user/getServerAddress']
|
|
||||||
if (this.ebookFileId) {
|
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() {
|
isPlayerOpen() {
|
||||||
return this.$store.getters['getIsPlayerOpen']
|
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) => {
|
$axios.onRequest((config) => {
|
||||||
console.log('[Axios] Making request to ' + config.url)
|
console.log('[Axios] Making request to ' + config.url)
|
||||||
if (config.url.startsWith('http:') || config.url.startsWith('https:') || config.url.startsWith('capacitor:')) {
|
if (config.url.startsWith('http:') || config.url.startsWith('https:') || config.url.startsWith('capacitor:')) {
|
||||||
return
|
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']
|
const bearerToken = store.getters['user/getToken']
|
||||||
if (bearerToken) {
|
if (bearerToken) {
|
||||||
config.headers.common['Authorization'] = `Bearer ${bearerToken}`
|
config.headers.common['Authorization'] = `Bearer ${bearerToken}`
|
||||||
|
|
@ -26,7 +59,75 @@ export default function ({ $axios, store }) {
|
||||||
console.log('[Axios] Request out', config.url)
|
console.log('[Axios] Request out', config.url)
|
||||||
})
|
})
|
||||||
|
|
||||||
$axios.onError((error) => {
|
$axios.onError(async (error) => {
|
||||||
console.error('Axios error code', 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 { Browser } from '@capacitor/browser'
|
||||||
import { AbsLogger } from '@/plugins/capacitor'
|
import { AbsLogger } from '@/plugins/capacitor'
|
||||||
|
import { CapacitorHttp } from '@capacitor/core'
|
||||||
|
|
||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
user: null,
|
user: null,
|
||||||
|
|
@ -30,9 +31,6 @@ export const getters = {
|
||||||
getServerConfigName: (state) => {
|
getServerConfigName: (state) => {
|
||||||
return state.serverConnectionConfig?.name || null
|
return state.serverConnectionConfig?.name || null
|
||||||
},
|
},
|
||||||
getCustomHeaders: (state) => {
|
|
||||||
return state.serverConnectionConfig?.customHeaders || null
|
|
||||||
},
|
|
||||||
getUserMediaProgress:
|
getUserMediaProgress:
|
||||||
(state) =>
|
(state) =>
|
||||||
(libraryItemId, episodeId = null) => {
|
(libraryItemId, episodeId = null) => {
|
||||||
|
|
@ -165,6 +163,61 @@ export const actions = {
|
||||||
commit('logout')
|
commit('logout')
|
||||||
commit('libraries/setCurrentLibrary', null, { root: true })
|
commit('libraries/setCurrentLibrary', null, { root: true })
|
||||||
await AbsLogger.info({ tag: 'user', message: `Logged out from server ${state.serverConnectionConfig?.name || 'Not connected'}` })
|
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