diff --git a/client/components/modals/AccountModal.vue b/client/components/modals/AccountModal.vue index 9293a6d1..6f4b7b67 100644 --- a/client/components/modals/AccountModal.vue +++ b/client/components/modals/AccountModal.vue @@ -311,7 +311,7 @@ export default { if (data.user.id === this.user.id && data.user.accessToken !== this.user.accessToken) { 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) diff --git a/client/components/readers/ComicReader.vue b/client/components/readers/ComicReader.vue index 28d79bf2..fce26939 100644 --- a/client/components/readers/ComicReader.vue +++ b/client/components/readers/ComicReader.vue @@ -104,9 +104,6 @@ export default { } }, computed: { - userToken() { - return this.$store.getters['user/getToken'] - }, libraryItemId() { return this.libraryItem?.id }, @@ -234,10 +231,7 @@ export default { async extract() { this.loading = true var buff = await this.$axios.$get(this.ebookUrl, { - responseType: 'blob', - headers: { - Authorization: `Bearer ${this.userToken}` - } + responseType: 'blob' }) const archive = await Archive.open(buff) const originalFilesObject = await archive.getFilesObject() diff --git a/client/components/readers/EpubReader.vue b/client/components/readers/EpubReader.vue index 795fcd2b..ac8e3397 100644 --- a/client/components/readers/EpubReader.vue +++ b/client/components/readers/EpubReader.vue @@ -57,9 +57,6 @@ export default { } }, computed: { - userToken() { - return this.$store.getters['user/getToken'] - }, /** @returns {string} */ libraryItemId() { return this.libraryItem?.id diff --git a/client/components/readers/MobiReader.vue b/client/components/readers/MobiReader.vue index 3e784f77..459ae55b 100644 --- a/client/components/readers/MobiReader.vue +++ b/client/components/readers/MobiReader.vue @@ -26,9 +26,6 @@ export default { return {} }, computed: { - userToken() { - return this.$store.getters['user/getToken'] - }, libraryItemId() { return this.libraryItem?.id }, @@ -96,11 +93,8 @@ export default { }, async initMobi() { // Fetch mobi file as blob - var buff = await this.$axios.$get(this.ebookUrl, { - responseType: 'blob', - headers: { - Authorization: `Bearer ${this.userToken}` - } + const buff = await this.$axios.$get(this.ebookUrl, { + responseType: 'blob' }) var reader = new FileReader() reader.onload = async (event) => { diff --git a/client/components/readers/PdfReader.vue b/client/components/readers/PdfReader.vue index c05f459c..d9459d76 100644 --- a/client/components/readers/PdfReader.vue +++ b/client/components/readers/PdfReader.vue @@ -55,7 +55,8 @@ export default { loadedRatio: 0, page: 1, numPages: 0, - pdfDocInitParams: null + pdfDocInitParams: null, + isRefreshing: false } }, computed: { @@ -152,7 +153,34 @@ export default { this.page++ 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) }, resize() { diff --git a/client/pages/login.vue b/client/pages/login.vue index 51f60600..242eb93a 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -189,7 +189,7 @@ export default { this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId) 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') }, diff --git a/client/plugins/axios.js b/client/plugins/axios.js index 87eedca2..66a9fa85 100644 --- a/client/plugins/axios.js +++ b/client/plugins/axios.js @@ -45,7 +45,7 @@ export default function ({ $axios, store, $root, app }) { if (originalRequest.url === '/auth/refresh' || originalRequest.url === '/login') { // Refresh failed or login failed, redirect to login store.commit('user/setUser', null) - store.commit('user/setUserToken', null) + store.commit('user/setAccessToken', null) app.router.push('/login') return Promise.reject(error) } @@ -72,23 +72,13 @@ export default function ({ $axios, store, $root, app }) { try { // Attempt to refresh the token - const response = await $axios.$post('/auth/refresh') - const newAccessToken = response.user.accessToken - + // 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 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 if (!originalRequest.headers) { originalRequest.headers = {} @@ -106,9 +96,7 @@ export default function ({ $axios, store, $root, app }) { // Process queued requests with error processQueue(refreshError, null) - // Clear user data and redirect to login - store.commit('user/setUser', null) - store.commit('user/setUserToken', null) + // Redirect to login app.router.push('/login') return Promise.reject(refreshError) diff --git a/client/store/user.js b/client/store/user.js index 04dc8447..a67eae34 100644 --- a/client/store/user.js +++ b/client/store/user.js @@ -1,5 +1,6 @@ export const state = () => ({ user: null, + accessToken: null, settings: { orderBy: 'media.metadata.title', orderDesc: false, @@ -25,7 +26,7 @@ export const getters = { getIsRoot: (state) => state.user && state.user.type === 'root', getIsAdminOrUp: (state) => state.user && (state.user.type === 'admin' || state.user.type === 'root'), getToken: (state) => { - return state.user?.accessToken || null + return state.accessToken || null }, getUserMediaProgress: (state) => @@ -145,6 +146,27 @@ export const actions = { } catch (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) { state.user = user }, - setUserToken(state, token) { + setAccessToken(state, token) { if (!token) { localStorage.removeItem('token') - if (state.user) { - state.user.accessToken = null - } - } else if (state.user) { - state.user.accessToken = token + state.accessToken = null + } else { + state.accessToken = token localStorage.setItem('token', token) } }, diff --git a/server/utils/rateLimiterFactory.js b/server/utils/rateLimiterFactory.js index 6f04d5ac..b5199662 100644 --- a/server/utils/rateLimiterFactory.js +++ b/server/utils/rateLimiterFactory.js @@ -23,7 +23,7 @@ class RateLimiterFactory { 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) { max = parseInt(process.env.RATE_LIMIT_AUTH_MAX) }