Update to not logout from server when switching servers, force users to re-login if using old auth

This commit is contained in:
advplyr 2025-07-05 11:21:20 -05:00
parent 44613e12f1
commit f6e2e4010f
7 changed files with 105 additions and 45 deletions

View file

@ -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()

View file

@ -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)

View file

@ -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))) {

View file

@ -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')
}
},

View file

@ -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) {

View file

@ -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') {

View file

@ -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()