diff --git a/components/app/SideDrawer.vue b/components/app/SideDrawer.vue index 7173f790..f0dfed36 100644 --- a/components/app/SideDrawer.vue +++ b/components/app/SideDrawer.vue @@ -179,7 +179,7 @@ export default { this.show = false }, async logout() { - await this.$store.dispatch('user/logout', {}) + await this.$store.dispatch('user/logout') }, async disconnect() { await this.$hapticsImpact() diff --git a/components/connection/ServerConnectForm.vue b/components/connection/ServerConnectForm.vue index 35f8e680..7f1a461b 100644 --- a/components/connection/ServerConnectForm.vue +++ b/components/connection/ServerConnectForm.vue @@ -439,6 +439,7 @@ export default { const payload = await this.authenticateToken() if (payload) { + // Will NOT include access token and refresh token this.setUserAndConnection(payload) } else { this.showAuth = true @@ -770,26 +771,6 @@ export default { prependProtocolIfNeeded(address) { return address.startsWith('http://') || address.startsWith('https://') ? address : `https://${address}` }, - /** - * Compares two semantic versioning strings to determine if the current version meets - * or exceeds the minimum version requirement. - * - * @param {string} currentVersion - The current version string to compare, e.g., "1.2.3". - * @param {string} minVersion - The minimum version string required, e.g., "1.0.0". - * @returns {boolean} - Returns true if the current version is greater than or equal - * to the minimum version, false otherwise. - */ - isValidVersion(currentVersion, minVersion) { - const currentParts = currentVersion.split('.').map(Number) - const minParts = minVersion.split('.').map(Number) - - for (let i = 0; i < minParts.length; i++) { - if (currentParts[i] > minParts[i]) return true - if (currentParts[i] < minParts[i]) return false - } - - return true - }, async submitAuth() { if (!this.networkConnected) return if (!this.serverConfig.username) { @@ -809,6 +790,7 @@ export default { const payload = await this.requestServerLogin() this.processing = false if (payload) { + // Will include access token and refresh token this.setUserAndConnection(payload) } }, @@ -821,20 +803,27 @@ export default { this.$store.commit('libraries/setEReaderDevices', ereaderDevices) this.$setServerLanguageCode(serverSettings.language) - // Set library - Use last library if set and available fallback to default user library - var lastLibraryId = await this.$localStore.getLastLibraryId() - if (lastLibraryId && (!user.librariesAccessible.length || user.librariesAccessible.includes(lastLibraryId))) { - this.$store.commit('libraries/setCurrentLibrary', lastLibraryId) - } else if (userDefaultLibraryId) { - this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId) - } - this.serverConfig.userId = user.id this.serverConfig.username = user.username - // Tokens only returned from /login endpoint - if (user.accessToken) { - this.serverConfig.token = user.accessToken - this.serverConfig.refreshToken = user.refreshToken + + if (this.$isValidVersion(serverSettings.version, '2.26.0')) { + // Tokens only returned from /login endpoint + if (user.accessToken) { + this.serverConfig.token = user.accessToken + this.serverConfig.refreshToken = user.refreshToken + } else { + // Detect if the connection config is using the old token. If so, force re-login + if (this.serverConfig.token === user.token) { + this.setForceReloginForNewAuth() + return + } + + // If the token was updated during a refresh (in nativeHttp.js) it gets updated in the store, so refetch + this.serverConfig.token = this.$store.getters['user/getToken'] || this.serverConfig.token + } + } else { + // Server version before new JWT auth, use old user.token + this.serverConfig.token = user.token } this.serverConfig.version = serverSettings.version @@ -854,6 +843,14 @@ export default { } } + // Set library - Use last library if set and available fallback to default user library + const lastLibraryId = await this.$localStore.getLastLibraryId() + if (lastLibraryId && (!user.librariesAccessible.length || user.librariesAccessible.includes(lastLibraryId))) { + this.$store.commit('libraries/setCurrentLibrary', lastLibraryId) + } else if (userDefaultLibraryId) { + this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId) + } + this.$store.commit('user/setUser', user) this.$store.commit('user/setAccessToken', serverConnectionConfig.token) this.$store.commit('user/setServerConnectionConfig', serverConnectionConfig) @@ -871,8 +868,13 @@ export default { this.error = null this.processing = true - // TODO: Handle refresh token - const authRes = await this.postRequest(`${this.serverConfig.address}/api/authorize`, null, { Authorization: `Bearer ${this.serverConfig.token}` }).catch((error) => { + const nativeHttpOptions = { + headers: { + Authorization: `Bearer ${this.serverConfig.token}` + }, + serverConnectionConfig: this.serverConfig + } + const authRes = await this.$nativeHttp.post(`${this.serverConfig.address}/api/authorize`, null, nativeHttpOptions).catch((error) => { console.error('[ServerConnectForm] Server auth failed', error) const errorMsg = error.message || error this.error = 'Failed to authorize' @@ -881,13 +883,25 @@ export default { } return false }) - console.log('[ServerConnectForm] authRes=', authRes) this.processing = false return authRes }, + setForceReloginForNewAuth() { + this.error = 'A new authentication system was added in server v2.26.0. Re-login is required for this server connection.' + this.showAuth = true + }, init() { + // Handle force re-login for servers using new JWT auth but still using an old token in the server config + if (this.$route.query.error === 'oldAuthToken' && this.$route.query.serverConnectionConfigId) { + this.serverConfig = this.serverConnectionConfigs.find((scc) => scc.id === this.$route.query.serverConnectionConfigId) + if (this.serverConfig) { + this.setForceReloginForNewAuth() + return + } + } + if (this.lastServerConnectionConfig) { console.log('[ServerConnectForm] init with lastServerConnectionConfig', this.lastServerConnectionConfig) this.connectToServer(this.lastServerConnectionConfig) diff --git a/layouts/default.vue b/layouts/default.vue index e895d81e..990da2c5 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -151,6 +151,22 @@ export default { this.$store.commit('setServerSettings', serverSettings) this.$store.commit('libraries/setEReaderDevices', ereaderDevices) + if (this.$isValidVersion(serverSettings.version, '2.26.0')) { + // Check if the server is using the new JWT auth and is still using an old token in the server config + // If so, redirect to /connect and request to re-login + if (serverConfig.token === user.token) { + this.attemptingConnection = false + AbsLogger.info({ tag: 'default', message: `attemptConnection: Server is using new JWT auth but is still using an old token (server version: ${serverSettings.version}) (${serverConfig.name})` }) + // Clear last server config + await this.$store.dispatch('user/logout') + this.$router.push(`/connect?error=oldAuthToken&serverConnectionConfigId=${serverConfig.id}`) + return + } + + // Token may have been refreshed during the authorize call so refetch from store + serverConfig.token = this.$store.getters['user/getToken'] || serverConfig.token + } + // Set library - Use last library if set and available fallback to default user library const lastLibraryId = await this.$localStore.getLastLibraryId() if (lastLibraryId && (!user.librariesAccessible.length || user.librariesAccessible.includes(lastLibraryId))) { diff --git a/pages/account.vue b/pages/account.vue index 78c16b8b..ceb65842 100644 --- a/pages/account.vue +++ b/pages/account.vue @@ -48,7 +48,7 @@ export default { methods: { async logout() { await this.$hapticsImpact() - await this.$store.dispatch('user/logout', {}) + await this.$store.dispatch('user/logout') this.$router.push('/connect') } }, diff --git a/plugins/init.client.js b/plugins/init.client.js index aa90dd4e..e54f85b2 100644 --- a/plugins/init.client.js +++ b/plugins/init.client.js @@ -245,10 +245,35 @@ Vue.prototype.$sanitizeSlug = (str) => { return str } +/** + * Compares two semantic versioning strings to determine if the current version meets + * or exceeds the minimum version requirement. + * Only supports 3 part versions, e.g. "1.2.3" + * + * @param {string} currentVersion - The current version string to compare, e.g., "1.2.3". + * @param {string} minVersion - The minimum version string required, e.g., "1.0.0". + * @returns {boolean} - Returns true if the current version is greater than or equal + * to the minimum version, false otherwise. + */ +function isValidVersion(currentVersion, minVersion) { + if (!currentVersion || !minVersion) return false + const currentParts = currentVersion.split('.').map(Number) + const minParts = minVersion.split('.').map(Number) + + for (let i = 0; i < minParts.length; i++) { + if (currentParts[i] > minParts[i]) return true + if (currentParts[i] < minParts[i]) return false + } + + return true +} + export default ({ store, app }, inject) => { const eventBus = new Vue() inject('eventBus', eventBus) + inject('isValidVersion', isValidVersion) + // Set theme app.$localStore?.getTheme()?.then((theme) => { if (theme) { diff --git a/plugins/nativeHttp.js b/plugins/nativeHttp.js index f00b5837..49d3f3f1 100644 --- a/plugins/nativeHttp.js +++ b/plugins/nativeHttp.js @@ -28,6 +28,7 @@ export default function ({ store, $db }, inject) { delete options.headers } console.log(`[nativeHttp] Making ${method} request to ${url}`) + return CapacitorHttp.request({ method, url, @@ -177,7 +178,7 @@ export default function ({ store, $db }, inject) { refreshToken: tokens.refreshToken } - // Save updated config to secure storage + // Save updated config to secure storage, persists refresh token in secure storage const savedConfig = await $db.setServerConnectionConfig(updatedConfig) // Update the store @@ -204,7 +205,12 @@ export default function ({ store, $db }, inject) { console.log('[nativeHttp] Handling refresh failure - logging out user') // Logout from server and clear store - await store.dispatch('user/logout', { serverConnectionConfigId }) + await store.dispatch('user/logout') + + if (serverConnectionConfigId) { + // Clear refresh token for server connection config + await $db.clearRefreshToken(serverConnectionConfigId) + } // Redirect to login page if (window.location.pathname !== '/connect') { diff --git a/store/user.js b/store/user.js index fcb7a83b..63161bb0 100644 --- a/store/user.js +++ b/store/user.js @@ -140,8 +140,11 @@ export const actions = { console.error('Error opening browser', error) } }, - async logout({ state, commit }, { serverConnectionConfigId }) { - if (state.serverConnectionConfig) { + async logout({ state, commit }, logoutFromServer = false) { + // Logging out from server deletes the session so the refresh token is no longer valid + // Currently this is not being used to support switching servers without logging back in (assuming refresh token is still valid) + // We may want to make this change in the future + if (state.serverConnectionConfig && logoutFromServer) { const refreshToken = await this.$db.getRefreshToken(state.serverConnectionConfig.id) const options = {} if (refreshToken) { @@ -154,10 +157,6 @@ export const actions = { await this.$nativeHttp.post('/logout', null, options).catch((error) => { console.error('Failed to logout', error) }) - await this.$db.clearRefreshToken(state.serverConnectionConfig.id) - } else if (serverConnectionConfigId) { - // When refresh fails before a server connection config is set, clear refresh token for server connection config - await this.$db.clearRefreshToken(serverConnectionConfigId) } await this.$db.logout()