Update ereaders to handle refreshing, epubjs to use custom request method, separate accessToken in store
Some checks are pending
Run Component Tests / Run Component Tests (push) Waiting to run
Integration Test / build and test (push) Waiting to run
Run Unit Tests / Run Unit Tests (push) Waiting to run

This commit is contained in:
advplyr 2025-07-10 16:54:28 -05:00
parent 25fe4dee3a
commit d3402e30c2
9 changed files with 67 additions and 46 deletions

View file

@ -311,7 +311,7 @@ export default {
if (data.user.id === this.user.id && data.user.accessToken !== this.user.accessToken) { if (data.user.id === this.user.id && data.user.accessToken !== this.user.accessToken) {
console.log('Current user access token was updated') console.log('Current user access token was updated')
this.$store.commit('user/setUserToken', data.user.accessToken) this.$store.commit('user/setAccessToken', data.user.accessToken)
} }
this.$toast.success(this.$strings.ToastAccountUpdateSuccess) this.$toast.success(this.$strings.ToastAccountUpdateSuccess)

View file

@ -104,9 +104,6 @@ export default {
} }
}, },
computed: { computed: {
userToken() {
return this.$store.getters['user/getToken']
},
libraryItemId() { libraryItemId() {
return this.libraryItem?.id return this.libraryItem?.id
}, },
@ -234,10 +231,7 @@ export default {
async extract() { async extract() {
this.loading = true this.loading = true
var buff = await this.$axios.$get(this.ebookUrl, { var buff = await this.$axios.$get(this.ebookUrl, {
responseType: 'blob', responseType: 'blob'
headers: {
Authorization: `Bearer ${this.userToken}`
}
}) })
const archive = await Archive.open(buff) const archive = await Archive.open(buff)
const originalFilesObject = await archive.getFilesObject() const originalFilesObject = await archive.getFilesObject()

View file

@ -57,9 +57,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

View file

@ -26,9 +26,6 @@ export default {
return {} return {}
}, },
computed: { computed: {
userToken() {
return this.$store.getters['user/getToken']
},
libraryItemId() { libraryItemId() {
return this.libraryItem?.id return this.libraryItem?.id
}, },
@ -96,11 +93,8 @@ export default {
}, },
async initMobi() { async initMobi() {
// Fetch mobi file as blob // Fetch mobi file as blob
var buff = await this.$axios.$get(this.ebookUrl, { const buff = await this.$axios.$get(this.ebookUrl, {
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) => {

View file

@ -55,7 +55,8 @@ export default {
loadedRatio: 0, loadedRatio: 0,
page: 1, page: 1,
numPages: 0, numPages: 0,
pdfDocInitParams: null pdfDocInitParams: null,
isRefreshing: false
} }
}, },
computed: { computed: {
@ -152,7 +153,34 @@ export default {
this.page++ this.page++
this.updateProgress() this.updateProgress()
}, },
error(err) { async refreshToken() {
if (this.isRefreshing) return
this.isRefreshing = true
const newAccessToken = await this.$store.dispatch('user/refreshToken').catch((error) => {
console.error('Failed to refresh token', error)
return null
})
if (!newAccessToken) {
// Redirect to login on failed refresh
this.$router.push('/login')
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)
}, },
resize() { resize() {

View file

@ -189,7 +189,7 @@ export default {
this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId) this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId)
this.$store.commit('user/setUser', user) this.$store.commit('user/setUser', user)
this.$store.commit('user/setUserToken', user.accessToken) this.$store.commit('user/setAccessToken', user.accessToken)
this.$store.dispatch('user/loadUserSettings') this.$store.dispatch('user/loadUserSettings')
}, },

View file

@ -45,7 +45,7 @@ export default function ({ $axios, store, $root, app }) {
if (originalRequest.url === '/auth/refresh' || originalRequest.url === '/login') { if (originalRequest.url === '/auth/refresh' || originalRequest.url === '/login') {
// Refresh failed or login failed, redirect to login // Refresh failed or login failed, redirect to login
store.commit('user/setUser', null) store.commit('user/setUser', null)
store.commit('user/setUserToken', null) store.commit('user/setAccessToken', null)
app.router.push('/login') app.router.push('/login')
return Promise.reject(error) return Promise.reject(error)
} }
@ -72,23 +72,13 @@ export default function ({ $axios, store, $root, app }) {
try { try {
// Attempt to refresh the token // Attempt to refresh the token
const response = await $axios.$post('/auth/refresh') // Updates store if successful, otherwise clears store and throw error
const newAccessToken = response.user.accessToken const newAccessToken = await store.dispatch('user/refreshToken')
if (!newAccessToken) { if (!newAccessToken) {
console.error('No new access token received') console.error('No new access token received')
return Promise.reject(error) return Promise.reject(error)
} }
// Update the token in store and localStorage
store.commit('user/setUser', response.user)
store.commit('user/setUserToken', newAccessToken)
// 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 = {}
@ -106,9 +96,7 @@ export default function ({ $axios, store, $root, app }) {
// Process queued requests with error // Process queued requests with error
processQueue(refreshError, null) processQueue(refreshError, null)
// Clear user data and redirect to login // Redirect to login
store.commit('user/setUser', null)
store.commit('user/setUserToken', null)
app.router.push('/login') app.router.push('/login')
return Promise.reject(refreshError) return Promise.reject(refreshError)

View file

@ -1,5 +1,6 @@
export const state = () => ({ export const state = () => ({
user: null, user: null,
accessToken: null,
settings: { settings: {
orderBy: 'media.metadata.title', orderBy: 'media.metadata.title',
orderDesc: false, orderDesc: false,
@ -25,7 +26,7 @@ export const getters = {
getIsRoot: (state) => state.user && state.user.type === 'root', getIsRoot: (state) => state.user && state.user.type === 'root',
getIsAdminOrUp: (state) => state.user && (state.user.type === 'admin' || state.user.type === 'root'), getIsAdminOrUp: (state) => state.user && (state.user.type === 'admin' || state.user.type === 'root'),
getToken: (state) => { getToken: (state) => {
return state.user?.accessToken || null return state.accessToken || null
}, },
getUserMediaProgress: getUserMediaProgress:
(state) => (state) =>
@ -145,6 +146,27 @@ export const actions = {
} catch (error) { } catch (error) {
console.error('Failed to load userSettings from local storage', error) console.error('Failed to load userSettings from local storage', error)
} }
},
refreshToken({ state, commit }) {
return this.$axios
.$post('/auth/refresh')
.then(async (response) => {
const newAccessToken = response.user.accessToken
commit('setUser', response.user)
commit('setAccessToken', newAccessToken)
// Emit event used to re-authenticate socket in default.vue since $root is not available here
if (this.$eventBus) {
this.$eventBus.$emit('token_refreshed', newAccessToken)
}
return newAccessToken
})
.catch((error) => {
console.error('Failed to refresh token', error)
commit('setUser', null)
commit('setAccessToken', null)
// Calling function handles redirect to login
throw error
})
} }
} }
@ -152,14 +174,12 @@ export const mutations = {
setUser(state, user) { setUser(state, user) {
state.user = user state.user = user
}, },
setUserToken(state, token) { setAccessToken(state, token) {
if (!token) { if (!token) {
localStorage.removeItem('token') localStorage.removeItem('token')
if (state.user) { state.accessToken = null
state.user.accessToken = null } else {
} state.accessToken = token
} else if (state.user) {
state.user.accessToken = token
localStorage.setItem('token', token) localStorage.setItem('token', token)
} }
}, },

View file

@ -23,7 +23,7 @@ class RateLimiterFactory {
windowMs = parseInt(process.env.RATE_LIMIT_AUTH_WINDOW) windowMs = parseInt(process.env.RATE_LIMIT_AUTH_WINDOW)
} }
let max = 20 // 20 attempts default let max = 40 // 40 attempts default
if (parseInt(process.env.RATE_LIMIT_AUTH_MAX) > 0) { if (parseInt(process.env.RATE_LIMIT_AUTH_MAX) > 0) {
max = parseInt(process.env.RATE_LIMIT_AUTH_MAX) max = parseInt(process.env.RATE_LIMIT_AUTH_MAX)
} }