Merge remote-tracking branch 'origin/master'
This commit is contained in:
2
ui/dist/spa/index.html
vendored
2
ui/dist/spa/index.html
vendored
@@ -1 +1 @@
|
||||
<!DOCTYPE html><html><head><title>Baggi SS</title><meta charset=utf-8><meta name=description content="A Quasar Project"><meta name=format-detection content="telephone=no"><meta name=msapplication-tap-highlight content=no><meta name=viewport content="user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1,width=device-width"><link rel=icon type=image/png sizes=128x128 href=/icons/favicon-128x128.png><link rel=icon type=image/png sizes=96x96 href=/icons/favicon-96x96.png><link rel=icon type=image/png sizes=32x32 href=/icons/favicon-32x32.png><link rel=icon type=image/png sizes=16x16 href=/icons/favicon-16x16.png><link rel=icon type=image/ico href=/favicon.ico><script defer src=/js/vendor.9ea1812a.js></script><script defer src=/js/app.bcfe0c60.js></script><link href=/css/vendor.724dcfab.css rel=stylesheet><link href=/css/app.53116624.css rel=stylesheet></head><body><div id=q-app></div></body></html>
|
||||
<!DOCTYPE html><html><head><title>Baggi SS</title><meta charset=utf-8><meta name=description content="A Quasar Project"><meta name=format-detection content="telephone=no"><meta name=msapplication-tap-highlight content=no><meta name=viewport content="user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1,width=device-width"><link rel=icon type=image/png sizes=128x128 href=/icons/favicon-128x128.png><link rel=icon type=image/png sizes=96x96 href=/icons/favicon-96x96.png><link rel=icon type=image/png sizes=32x32 href=/icons/favicon-32x32.png><link rel=icon type=image/png sizes=16x16 href=/icons/favicon-16x16.png><link rel=icon type=image/ico href=/favicon.ico><script defer src=/js/vendor.9ea1812a.js></script><script defer src=/js/app.d0936c73.js></script><link href=/css/vendor.724dcfab.css rel=stylesheet><link href=/css/app.53116624.css rel=stylesheet></head><body><div id=q-app></div></body></html>
|
||||
@@ -3,6 +3,7 @@ import qs from 'qs'
|
||||
import { useAuthStore } from 'stores/authStore'
|
||||
|
||||
export const API_BASE_URL = '/api'
|
||||
const AUTH_REFRESH_PATH = '/auth/refresh'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
@@ -12,17 +13,69 @@ const api = axios.create({
|
||||
withCredentials: true
|
||||
})
|
||||
|
||||
function isPublicPath(url) {
|
||||
return (
|
||||
url.startsWith('/auth/login') ||
|
||||
url.startsWith(AUTH_REFRESH_PATH) ||
|
||||
url.startsWith('/password/forgot') ||
|
||||
url.startsWith('/password/reset')
|
||||
)
|
||||
}
|
||||
|
||||
function extractToken(payload) {
|
||||
return (
|
||||
payload?.token ||
|
||||
payload?.access_token ||
|
||||
payload?.data?.token ||
|
||||
payload?.data?.access_token ||
|
||||
''
|
||||
)
|
||||
}
|
||||
|
||||
function isHtmlLike(value) {
|
||||
const text = String(value || '').trim().toLowerCase()
|
||||
return text.startsWith('<!doctype html') || text.startsWith('<html')
|
||||
}
|
||||
|
||||
function sanitizeApiErrorDetail(detail, status) {
|
||||
const normalized = String(detail || '').trim()
|
||||
|
||||
if (!normalized) {
|
||||
if (status === 504) {
|
||||
return 'Gateway timeout: origin server did not respond in time.'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
if (isHtmlLike(normalized)) {
|
||||
if (status === 504) {
|
||||
return 'Gateway timeout: origin server did not respond in time.'
|
||||
}
|
||||
if (status >= 500) {
|
||||
return `Upstream server error (${status}).`
|
||||
}
|
||||
return `Unexpected HTML error response (${status || '-'})`
|
||||
}
|
||||
|
||||
const compact = normalized.replace(/\s+/g, ' ').trim()
|
||||
if (compact.length > 320) {
|
||||
return `${compact.slice(0, 320)}...`
|
||||
}
|
||||
|
||||
return compact
|
||||
}
|
||||
|
||||
function redirectToLogin() {
|
||||
if (typeof window === 'undefined') return
|
||||
if (window.location.hash === '#/login') return
|
||||
window.location.hash = '/login'
|
||||
}
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
const auth = useAuthStore()
|
||||
const url = config.url || ''
|
||||
|
||||
const isPublic =
|
||||
url.startsWith('/auth/login') ||
|
||||
url.startsWith('/auth/refresh') ||
|
||||
url.startsWith('/password/forgot') ||
|
||||
url.startsWith('/password/reset')
|
||||
|
||||
if (!isPublic && auth?.token) {
|
||||
if (!isPublicPath(url) && auth?.token) {
|
||||
config.headers ||= {}
|
||||
config.headers.Authorization = `Bearer ${auth.token}`
|
||||
}
|
||||
@@ -31,33 +84,106 @@ api.interceptors.request.use((config) => {
|
||||
})
|
||||
|
||||
let isLoggingOut = false
|
||||
let refreshPromise = null
|
||||
|
||||
async function refreshAccessToken() {
|
||||
if (refreshPromise) {
|
||||
return refreshPromise
|
||||
}
|
||||
|
||||
refreshPromise = (async () => {
|
||||
const auth = useAuthStore()
|
||||
const response = await api.post(
|
||||
AUTH_REFRESH_PATH,
|
||||
{},
|
||||
{
|
||||
skipAuthRefresh: true,
|
||||
skipAutoLogout: true
|
||||
}
|
||||
)
|
||||
|
||||
const rawToken = extractToken(response?.data)
|
||||
const token = typeof rawToken === 'string' ? rawToken.trim() : ''
|
||||
const isJwt = token.split('.').length === 3
|
||||
|
||||
if (!token || !isJwt) {
|
||||
throw new Error('Invalid refresh token response')
|
||||
}
|
||||
|
||||
auth.setSession({
|
||||
token,
|
||||
user: auth.user || null
|
||||
})
|
||||
|
||||
return token
|
||||
})().finally(() => {
|
||||
refreshPromise = null
|
||||
})
|
||||
|
||||
return refreshPromise
|
||||
}
|
||||
|
||||
function clearSessionAndRedirect() {
|
||||
if (isLoggingOut) return
|
||||
|
||||
isLoggingOut = true
|
||||
try {
|
||||
useAuthStore().clearSession()
|
||||
} finally {
|
||||
isLoggingOut = false
|
||||
redirectToLogin()
|
||||
}
|
||||
}
|
||||
|
||||
api.interceptors.response.use(
|
||||
r => r,
|
||||
async (error) => {
|
||||
const requestConfig = error?.config || {}
|
||||
const status = error?.response?.status
|
||||
const requestUrl = String(error?.config?.url || '')
|
||||
const requestUrl = String(requestConfig.url || '')
|
||||
const hasBlob = typeof Blob !== 'undefined' && error?.response?.data instanceof Blob
|
||||
const isPasswordChangeRequest =
|
||||
requestUrl.startsWith('/password/change') ||
|
||||
requestUrl.startsWith('/me/password')
|
||||
const isPublicRequest = isPublicPath(requestUrl)
|
||||
|
||||
if ((status >= 500 || hasBlob) && error) {
|
||||
const method = String(error?.config?.method || 'GET').toUpperCase()
|
||||
const detail = await extractApiErrorDetail(error)
|
||||
const method = String(requestConfig.method || 'GET').toUpperCase()
|
||||
const detail = sanitizeApiErrorDetail(
|
||||
await extractApiErrorDetail(error),
|
||||
status
|
||||
)
|
||||
error.parsedMessage = detail
|
||||
console.error(`API ${status || '-'} ${method} ${requestUrl}: ${detail}`)
|
||||
}
|
||||
|
||||
const shouldTryRefresh =
|
||||
status === 401 &&
|
||||
!requestConfig._retry &&
|
||||
!requestConfig.skipAuthRefresh &&
|
||||
!isPasswordChangeRequest &&
|
||||
!isPublicRequest
|
||||
|
||||
if (shouldTryRefresh) {
|
||||
requestConfig._retry = true
|
||||
try {
|
||||
const newToken = await refreshAccessToken()
|
||||
requestConfig.headers ||= {}
|
||||
requestConfig.headers.Authorization = `Bearer ${newToken}`
|
||||
return api(requestConfig)
|
||||
} catch {
|
||||
// fall through to logout
|
||||
}
|
||||
}
|
||||
|
||||
// Password change endpoints may return 401 for business reasons
|
||||
// (for example current password mismatch). Keep session in that case.
|
||||
if (status === 401 && !isPasswordChangeRequest && !isLoggingOut) {
|
||||
isLoggingOut = true
|
||||
try {
|
||||
useAuthStore().clearSession()
|
||||
} finally {
|
||||
isLoggingOut = false
|
||||
}
|
||||
if (
|
||||
status === 401 &&
|
||||
!isPasswordChangeRequest &&
|
||||
!requestConfig.skipAutoLogout
|
||||
) {
|
||||
clearSessionAndRedirect()
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
@@ -114,6 +240,7 @@ async function parseBlobErrorMessage(data) {
|
||||
}
|
||||
|
||||
export async function extractApiErrorDetail(err) {
|
||||
const status = err?.response?.status
|
||||
let detail =
|
||||
err?.parsedMessage ||
|
||||
err?.response?.data?.detail ||
|
||||
@@ -129,7 +256,7 @@ export async function extractApiErrorDetail(err) {
|
||||
detail = err?.message || 'Request failed'
|
||||
}
|
||||
|
||||
return detail
|
||||
return sanitizeApiErrorDetail(detail, status)
|
||||
}
|
||||
|
||||
export const download = async (u, p = {}, c = {}) => {
|
||||
|
||||
Reference in New Issue
Block a user