Merge e329667ab073c9e393b6a0cd25e0eb465687b714 into 5f2b7ee1050a177785864ab50db4c1c8c118ea20

This commit is contained in:
Deluan Quintão 2026-06-30 08:10:57 +02:00 committed by GitHub
commit 6268db88a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 122 additions and 6 deletions

View File

@ -26,6 +26,11 @@ function storeAuthenticationInfo(authInfo) {
localStorage.setItem('is-authenticated', 'true')
}
const isNetworkError = (error) => {
const msg = error?.message || ''
return msg === 'Failed to fetch' || msg.includes('NetworkError')
}
const authProvider = {
login: ({ username, password }) => {
let url = baseUrl('/auth/login')
@ -53,10 +58,7 @@ const authProvider = {
return response
})
.catch((error) => {
if (
error.message === 'Failed to fetch' ||
error.stack === 'TypeError: Failed to fetch'
) {
if (isNetworkError(error)) {
throw new Error('errors.network_error')
}
@ -78,11 +80,22 @@ const authProvider = {
? Promise.resolve()
: Promise.reject(),
checkError: ({ status }) => {
if (status === 401) {
checkError: (error) => {
if (error?.status === 401) {
removeItems()
return Promise.reject()
}
if (config.extAuthLogoutURL && isNetworkError(error)) {
const now = Date.now()
const lastReload =
Number(sessionStorage.getItem('ext-auth-reload-ts')) || 0
if (now - lastReload > 30000) {
sessionStorage.setItem('ext-auth-reload-ts', String(now))
removeItems()
window.location.reload()
return new Promise(() => {})
}
}
return Promise.resolve()
},

100
ui/src/authProvider.test.js Normal file
View File

@ -0,0 +1,100 @@
import { describe, it, beforeEach, afterEach, expect } from 'vitest'
import config from './config'
import authProvider from './authProvider'
describe('authProvider', () => {
beforeEach(() => {
localStorage.clear()
sessionStorage.clear()
localStorage.setItem('is-authenticated', 'true')
localStorage.setItem('token', 'test-token')
localStorage.setItem('userId', 'test-user')
localStorage.setItem('role', 'admin')
config.extAuthLogoutURL = ''
})
afterEach(() => {
config.extAuthLogoutURL = ''
})
describe('checkError', () => {
it('rejects and clears storage on 401', async () => {
await expect(authProvider.checkError({ status: 401 })).rejects.toBe(
undefined,
)
expect(localStorage.getItem('is-authenticated')).toBeNull()
})
it('resolves on non-401 HTTP errors', async () => {
await expect(
authProvider.checkError({ status: 500 }),
).resolves.toBeUndefined()
expect(localStorage.getItem('is-authenticated')).toBe('true')
})
it('resolves on network error without extAuth configured', async () => {
config.extAuthLogoutURL = ''
await expect(
authProvider.checkError(new TypeError('Failed to fetch')),
).resolves.toBeUndefined()
expect(localStorage.getItem('is-authenticated')).toBe('true')
})
it('clears auth and sets reload guard on TypeError with extAuth', () => {
config.extAuthLogoutURL = 'https://auth.example.com/logout'
// window.location.reload throws in jsdom, so we catch it
try {
authProvider.checkError(new TypeError('Failed to fetch'))
} catch {
// jsdom "Not implemented: navigation" is expected
}
expect(localStorage.getItem('is-authenticated')).toBeNull()
expect(sessionStorage.getItem('ext-auth-reload-ts')).not.toBeNull()
})
it('clears auth on Firefox NetworkError with extAuth', () => {
config.extAuthLogoutURL = 'https://auth.example.com/logout'
const error = new Error('NetworkError when attempting to fetch resource')
try {
authProvider.checkError(error)
} catch {
// jsdom "Not implemented: navigation" is expected
}
expect(localStorage.getItem('is-authenticated')).toBeNull()
expect(sessionStorage.getItem('ext-auth-reload-ts')).not.toBeNull()
})
it('does not reload-loop within 30 seconds', async () => {
config.extAuthLogoutURL = 'https://auth.example.com/logout'
sessionStorage.setItem('ext-auth-reload-ts', String(Date.now() - 10000))
await expect(
authProvider.checkError(new TypeError('Failed to fetch')),
).resolves.toBeUndefined()
expect(localStorage.getItem('is-authenticated')).toBe('true')
})
it('allows reload again after 30 seconds', () => {
config.extAuthLogoutURL = 'https://auth.example.com/logout'
sessionStorage.setItem('ext-auth-reload-ts', String(Date.now() - 31000))
try {
authProvider.checkError(new TypeError('Failed to fetch'))
} catch {
// jsdom "Not implemented: navigation" is expected
}
expect(localStorage.getItem('is-authenticated')).toBeNull()
const ts = parseInt(sessionStorage.getItem('ext-auth-reload-ts'), 10)
expect(Date.now() - ts).toBeLessThan(5000)
})
})
describe('checkAuth', () => {
it('resolves when authenticated', async () => {
await expect(authProvider.checkAuth()).resolves.toBeUndefined()
})
it('rejects when not authenticated', async () => {
localStorage.removeItem('is-authenticated')
await expect(authProvider.checkAuth()).rejects.toBe(undefined)
})
})
})

View File

@ -14,6 +14,9 @@ const localStorageMock = (function () {
setItem: function (key, value) {
store[key] = value.toString()
},
removeItem: function (key) {
delete store[key]
},
clear: function () {
store = {}
},