Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -26,51 +26,74 @@ func GetStatements(params models.StatementParams) ([]models.StatementHeader, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
query := fmt.Sprintf(`
|
query := fmt.Sprintf(`
|
||||||
|
|
||||||
;WITH Opening AS (
|
;WITH Opening AS (
|
||||||
SELECT
|
SELECT
|
||||||
b.CurrAccCode AS Cari_Kod,
|
b.CurrAccCode AS Cari_Kod,
|
||||||
b.DocCurrencyCode AS Para_Birimi,
|
b.DocCurrencyCode AS Para_Birimi,
|
||||||
SUM(c.Debit - c.Credit) AS Devir_Bakiyesi
|
SUM(c.Debit - c.Credit) AS Devir_Bakiyesi
|
||||||
|
|
||||||
FROM trCurrAccBook b
|
FROM trCurrAccBook b
|
||||||
|
|
||||||
LEFT JOIN trCurrAccBookCurrency c
|
LEFT JOIN trCurrAccBookCurrency c
|
||||||
ON c.CurrAccBookID = b.CurrAccBookID
|
ON c.CurrAccBookID = b.CurrAccBookID
|
||||||
AND c.CurrencyCode = b.DocCurrencyCode
|
AND c.CurrencyCode = b.DocCurrencyCode
|
||||||
|
|
||||||
WHERE b.CurrAccCode LIKE '%%' + @Carikod + '%%'
|
WHERE b.CurrAccCode LIKE '%%' + @Carikod + '%%'
|
||||||
AND b.DocumentDate < @startdate
|
AND b.DocumentDate < @startdate
|
||||||
|
|
||||||
AND EXISTS (
|
AND EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM CurrAccBookATAttributesFilter f2
|
FROM CurrAccBookATAttributesFilter f2
|
||||||
WHERE f2.CurrAccBookID = b.CurrAccBookID
|
WHERE f2.CurrAccBookID = b.CurrAccBookID
|
||||||
AND f2.ATAtt01 IN (%s)
|
AND f2.ATAtt01 IN (%s)
|
||||||
)
|
)
|
||||||
GROUP BY b.CurrAccCode, b.DocCurrencyCode
|
|
||||||
|
GROUP BY
|
||||||
|
b.CurrAccCode,
|
||||||
|
b.DocCurrencyCode
|
||||||
),
|
),
|
||||||
|
|
||||||
Movements AS (
|
Movements AS (
|
||||||
SELECT
|
SELECT
|
||||||
b.CurrAccCode AS Cari_Kod,
|
b.CurrAccCode AS Cari_Kod,
|
||||||
d.CurrAccDescription AS Cari_Isim,
|
d.CurrAccDescription AS Cari_Isim,
|
||||||
|
|
||||||
CONVERT(varchar(10), b.DocumentDate, 23) AS Belge_Tarihi,
|
CONVERT(varchar(10), b.DocumentDate, 23) AS Belge_Tarihi,
|
||||||
CONVERT(varchar(10), b.DueDate, 23) AS Vade_Tarihi,
|
CONVERT(varchar(10), b.DueDate, 23) AS Vade_Tarihi,
|
||||||
|
|
||||||
b.RefNumber AS Belge_No,
|
b.RefNumber AS Belge_No,
|
||||||
b.BaseApplicationCode AS Islem_Tipi,
|
b.BaseApplicationCode AS Islem_Tipi,
|
||||||
b.LineDescription AS Aciklama,
|
b.LineDescription AS Aciklama,
|
||||||
|
|
||||||
b.DocCurrencyCode AS Para_Birimi,
|
b.DocCurrencyCode AS Para_Birimi,
|
||||||
|
|
||||||
c.Debit AS Borc,
|
c.Debit AS Borc,
|
||||||
c.Credit AS Alacak,
|
c.Credit AS Alacak,
|
||||||
|
|
||||||
SUM(c.Debit - c.Credit)
|
SUM(c.Debit - c.Credit)
|
||||||
OVER (PARTITION BY b.CurrAccCode, c.CurrencyCode
|
OVER (
|
||||||
ORDER BY b.DocumentDate, b.CurrAccBookID) AS Hareket_Bakiyesi,
|
PARTITION BY b.CurrAccCode, c.CurrencyCode
|
||||||
|
ORDER BY b.DocumentDate, b.CurrAccBookID
|
||||||
|
) AS Hareket_Bakiyesi,
|
||||||
|
|
||||||
f.ATAtt01 AS Parislemtipi
|
f.ATAtt01 AS Parislemtipi
|
||||||
|
|
||||||
FROM trCurrAccBook b
|
FROM trCurrAccBook b
|
||||||
|
|
||||||
LEFT JOIN cdCurrAccDesc d
|
LEFT JOIN cdCurrAccDesc d
|
||||||
ON b.CurrAccCode = d.CurrAccCode
|
ON b.CurrAccCode = d.CurrAccCode
|
||||||
|
|
||||||
LEFT JOIN trCurrAccBookCurrency c
|
LEFT JOIN trCurrAccBookCurrency c
|
||||||
ON b.CurrAccBookID = c.CurrAccBookID
|
ON b.CurrAccBookID = c.CurrAccBookID
|
||||||
AND b.DocCurrencyCode = c.CurrencyCode
|
AND b.DocCurrencyCode = c.CurrencyCode
|
||||||
|
|
||||||
LEFT JOIN CurrAccBookATAttributesFilter f
|
LEFT JOIN CurrAccBookATAttributesFilter f
|
||||||
ON b.CurrAccBookID = f.CurrAccBookID
|
ON b.CurrAccBookID = f.CurrAccBookID
|
||||||
|
|
||||||
WHERE b.CurrAccCode LIKE '%%' + @Carikod + '%%'
|
WHERE b.CurrAccCode LIKE '%%' + @Carikod + '%%'
|
||||||
AND b.DocumentDate BETWEEN @startdate AND @enddate
|
AND b.DocumentDate BETWEEN @startdate AND @enddate
|
||||||
|
|
||||||
AND EXISTS (
|
AND EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM CurrAccBookATAttributesFilter f2
|
FROM CurrAccBookATAttributesFilter f2
|
||||||
@@ -78,6 +101,7 @@ Movements AS (
|
|||||||
AND f2.ATAtt01 IN (%s)
|
AND f2.ATAtt01 IN (%s)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
m.Cari_Kod,
|
m.Cari_Kod,
|
||||||
m.Cari_Isim,
|
m.Cari_Isim,
|
||||||
@@ -87,54 +111,66 @@ SELECT
|
|||||||
m.Islem_Tipi,
|
m.Islem_Tipi,
|
||||||
m.Aciklama,
|
m.Aciklama,
|
||||||
m.Para_Birimi,
|
m.Para_Birimi,
|
||||||
|
|
||||||
m.Borc,
|
m.Borc,
|
||||||
m.Alacak,
|
m.Alacak,
|
||||||
|
|
||||||
ISNULL(o.Devir_Bakiyesi,0) + m.Hareket_Bakiyesi AS Bakiye,
|
ISNULL(o.Devir_Bakiyesi,0) + m.Hareket_Bakiyesi AS Bakiye,
|
||||||
|
|
||||||
m.Parislemtipi AS Parislemler
|
m.Parislemtipi AS Parislemler
|
||||||
|
|
||||||
FROM Movements m
|
FROM Movements m
|
||||||
|
|
||||||
LEFT JOIN Opening o
|
LEFT JOIN Opening o
|
||||||
ON o.Cari_Kod = m.Cari_Kod
|
ON o.Cari_Kod = m.Cari_Kod
|
||||||
AND o.Para_Birimi = m.Para_Birimi
|
AND o.Para_Birimi = m.Para_Birimi
|
||||||
|
|
||||||
|
|
||||||
UNION ALL
|
UNION ALL
|
||||||
|
|
||||||
-- Devir satırı
|
|
||||||
SELECT
|
|
||||||
@Carikod AS Cari_Kod,
|
|
||||||
MAX(d.CurrAccDescription) AS Cari_Isim,
|
|
||||||
CONVERT(varchar(10), @startdate, 23) AS Belge_Tarihi,
|
|
||||||
CONVERT(varchar(10), @startdate, 23) AS Vade_Tarihi,
|
|
||||||
'Baslangic_devir' AS Belge_No,
|
|
||||||
'Devir' AS Islem_Tipi,
|
|
||||||
'Devir Bakiyesi' AS Aciklama,
|
|
||||||
b.DocCurrencyCode AS Para_Birimi,
|
|
||||||
SUM(c.Debit) AS Borc,
|
|
||||||
SUM(c.Credit) AS Alacak,
|
|
||||||
SUM(c.Debit) - SUM(c.Credit) AS Bakiye,
|
|
||||||
(
|
|
||||||
SELECT STRING_AGG(x.ATAtt01, ',')
|
|
||||||
FROM (
|
|
||||||
SELECT DISTINCT f2.ATAtt01
|
|
||||||
FROM CurrAccBookATAttributesFilter f2
|
|
||||||
INNER JOIN trCurrAccBook bb
|
|
||||||
ON f2.CurrAccBookID = bb.CurrAccBookID
|
|
||||||
WHERE bb.CurrAccCode LIKE '%%' + @Carikod + '%%'
|
|
||||||
AND bb.DocumentDate < @startdate
|
|
||||||
AND f2.ATAtt01 IN (%s)
|
|
||||||
) x
|
|
||||||
) AS Parislemler
|
|
||||||
FROM trCurrAccBook b
|
|
||||||
LEFT JOIN cdCurrAccDesc d
|
|
||||||
ON b.CurrAccCode = d.CurrAccCode
|
|
||||||
LEFT JOIN trCurrAccBookCurrency c
|
|
||||||
ON b.CurrAccBookID = c.CurrAccBookID
|
|
||||||
AND b.DocCurrencyCode = c.CurrencyCode
|
|
||||||
WHERE b.CurrAccCode LIKE '%%' + @Carikod + '%%'
|
|
||||||
AND b.DocumentDate < @startdate
|
|
||||||
GROUP BY b.DocCurrencyCode
|
|
||||||
|
|
||||||
ORDER BY Para_Birimi, Belge_Tarihi;
|
/* ✅ Devir satırı sadece Opening’den */
|
||||||
`, parislemFilter, parislemFilter, parislemFilter)
|
SELECT
|
||||||
|
o.Cari_Kod,
|
||||||
|
d.CurrAccDescription,
|
||||||
|
|
||||||
|
CONVERT(varchar(10), @startdate, 23),
|
||||||
|
CONVERT(varchar(10), @startdate, 23),
|
||||||
|
|
||||||
|
'Baslangic_devir',
|
||||||
|
'Devir',
|
||||||
|
'Devir Bakiyesi',
|
||||||
|
|
||||||
|
o.Para_Birimi,
|
||||||
|
|
||||||
|
CASE
|
||||||
|
WHEN o.Devir_Bakiyesi >= 0 THEN o.Devir_Bakiyesi
|
||||||
|
ELSE 0
|
||||||
|
END,
|
||||||
|
|
||||||
|
CASE
|
||||||
|
WHEN o.Devir_Bakiyesi < 0 THEN ABS(o.Devir_Bakiyesi)
|
||||||
|
ELSE 0
|
||||||
|
END,
|
||||||
|
|
||||||
|
o.Devir_Bakiyesi,
|
||||||
|
|
||||||
|
'%s'
|
||||||
|
|
||||||
|
FROM Opening o
|
||||||
|
|
||||||
|
LEFT JOIN cdCurrAccDesc d
|
||||||
|
ON d.CurrAccCode = o.Cari_Kod
|
||||||
|
|
||||||
|
|
||||||
|
ORDER BY
|
||||||
|
Para_Birimi,
|
||||||
|
Belge_Tarihi;
|
||||||
|
`,
|
||||||
|
parislemFilter,
|
||||||
|
parislemFilter,
|
||||||
|
parislemFilter,
|
||||||
|
)
|
||||||
|
|
||||||
rows, err := db.MssqlDB.Query(query,
|
rows, err := db.MssqlDB.Query(query,
|
||||||
sql.Named("startdate", params.StartDate),
|
sql.Named("startdate", params.StartDate),
|
||||||
|
|||||||
@@ -32,8 +32,16 @@ SELECT
|
|||||||
a.ItemCode AS Urun_Kodu,
|
a.ItemCode AS Urun_Kodu,
|
||||||
a.ColorCode AS Urun_Rengi,
|
a.ColorCode AS Urun_Rengi,
|
||||||
SUM(a.Qty1) AS Toplam_Adet,
|
SUM(a.Qty1) AS Toplam_Adet,
|
||||||
SUM(ABS(a.Doc_Price)) AS Toplam_Fiyat,
|
|
||||||
CAST(SUM(a.Qty1 * ABS(a.Doc_Price)) AS numeric(18,2)) AS Toplam_Tutar
|
CAST(
|
||||||
|
SUM(a.Qty1 * ABS(a.Doc_Price))
|
||||||
|
/ NULLIF(SUM(a.Qty1),0)
|
||||||
|
AS numeric(18,4)) AS Doviz_Fiyat,
|
||||||
|
|
||||||
|
CAST(
|
||||||
|
SUM(a.Qty1 * ABS(a.Doc_Price))
|
||||||
|
AS numeric(18,2)) AS Toplam_Tutar
|
||||||
|
|
||||||
FROM AllInvoicesWithAttributes a
|
FROM AllInvoicesWithAttributes a
|
||||||
LEFT JOIN prItemAttribute AnaGrup
|
LEFT JOIN prItemAttribute AnaGrup
|
||||||
ON a.ItemCode = AnaGrup.ItemCode AND AnaGrup.AttributeTypeCode = 1
|
ON a.ItemCode = AnaGrup.ItemCode AND AnaGrup.AttributeTypeCode = 1
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package routes
|
|||||||
import (
|
import (
|
||||||
"bssapp-backend/auth"
|
"bssapp-backend/auth"
|
||||||
"bssapp-backend/internal/auditlog"
|
"bssapp-backend/internal/auditlog"
|
||||||
|
"bssapp-backend/internal/security"
|
||||||
"bssapp-backend/models"
|
"bssapp-backend/models"
|
||||||
"bssapp-backend/queries"
|
"bssapp-backend/queries"
|
||||||
"bssapp-backend/repository"
|
"bssapp-backend/repository"
|
||||||
@@ -208,6 +209,22 @@ func writeLoginResponse(w http.ResponseWriter, db *sql.DB, user *models.MkUser)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refreshPlain, refreshHash, err := security.GenerateRefreshToken()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Refresh token üretilemedi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshExp := time.Now().Add(14 * 24 * time.Hour)
|
||||||
|
rtRepo := repository.NewRefreshTokenRepository(db)
|
||||||
|
if err := rtRepo.IssueRefreshToken(user.ID, refreshHash, refreshExp); err != nil {
|
||||||
|
log.Printf("refresh token store failed user=%d err=%v", user.ID, err)
|
||||||
|
http.Error(w, "Session başlatılamadı", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setRefreshCookie(w, refreshPlain, refreshExp)
|
||||||
|
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
"token": token,
|
"token": token,
|
||||||
"user": map[string]any{
|
"user": map[string]any{
|
||||||
|
|||||||
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'
|
import { useAuthStore } from 'stores/authStore'
|
||||||
|
|
||||||
export const API_BASE_URL = '/api'
|
export const API_BASE_URL = '/api'
|
||||||
|
const AUTH_REFRESH_PATH = '/auth/refresh'
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: API_BASE_URL,
|
baseURL: API_BASE_URL,
|
||||||
@@ -12,17 +13,69 @@ const api = axios.create({
|
|||||||
withCredentials: true
|
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) => {
|
api.interceptors.request.use((config) => {
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const url = config.url || ''
|
const url = config.url || ''
|
||||||
|
|
||||||
const isPublic =
|
if (!isPublicPath(url) && auth?.token) {
|
||||||
url.startsWith('/auth/login') ||
|
|
||||||
url.startsWith('/auth/refresh') ||
|
|
||||||
url.startsWith('/password/forgot') ||
|
|
||||||
url.startsWith('/password/reset')
|
|
||||||
|
|
||||||
if (!isPublic && auth?.token) {
|
|
||||||
config.headers ||= {}
|
config.headers ||= {}
|
||||||
config.headers.Authorization = `Bearer ${auth.token}`
|
config.headers.Authorization = `Bearer ${auth.token}`
|
||||||
}
|
}
|
||||||
@@ -31,33 +84,106 @@ api.interceptors.request.use((config) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
let isLoggingOut = false
|
let isLoggingOut = false
|
||||||
|
let refreshPromise = null
|
||||||
|
|
||||||
api.interceptors.response.use(
|
async function refreshAccessToken() {
|
||||||
r => r,
|
if (refreshPromise) {
|
||||||
async (error) => {
|
return refreshPromise
|
||||||
const status = error?.response?.status
|
|
||||||
const requestUrl = String(error?.config?.url || '')
|
|
||||||
const hasBlob = typeof Blob !== 'undefined' && error?.response?.data instanceof Blob
|
|
||||||
const isPasswordChangeRequest =
|
|
||||||
requestUrl.startsWith('/password/change') ||
|
|
||||||
requestUrl.startsWith('/me/password')
|
|
||||||
|
|
||||||
if ((status >= 500 || hasBlob) && error) {
|
|
||||||
const method = String(error?.config?.method || 'GET').toUpperCase()
|
|
||||||
const detail = await extractApiErrorDetail(error)
|
|
||||||
error.parsedMessage = detail
|
|
||||||
console.error(`API ${status || '-'} ${method} ${requestUrl}: ${detail}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Password change endpoints may return 401 for business reasons
|
refreshPromise = (async () => {
|
||||||
// (for example current password mismatch). Keep session in that case.
|
const auth = useAuthStore()
|
||||||
if (status === 401 && !isPasswordChangeRequest && !isLoggingOut) {
|
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
|
isLoggingOut = true
|
||||||
try {
|
try {
|
||||||
useAuthStore().clearSession()
|
useAuthStore().clearSession()
|
||||||
} finally {
|
} finally {
|
||||||
isLoggingOut = false
|
isLoggingOut = false
|
||||||
|
redirectToLogin()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
api.interceptors.response.use(
|
||||||
|
r => r,
|
||||||
|
async (error) => {
|
||||||
|
const requestConfig = error?.config || {}
|
||||||
|
const status = error?.response?.status
|
||||||
|
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(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 &&
|
||||||
|
!requestConfig.skipAutoLogout
|
||||||
|
) {
|
||||||
|
clearSessionAndRedirect()
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(error)
|
return Promise.reject(error)
|
||||||
@@ -114,6 +240,7 @@ async function parseBlobErrorMessage(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function extractApiErrorDetail(err) {
|
export async function extractApiErrorDetail(err) {
|
||||||
|
const status = err?.response?.status
|
||||||
let detail =
|
let detail =
|
||||||
err?.parsedMessage ||
|
err?.parsedMessage ||
|
||||||
err?.response?.data?.detail ||
|
err?.response?.data?.detail ||
|
||||||
@@ -129,7 +256,7 @@ export async function extractApiErrorDetail(err) {
|
|||||||
detail = err?.message || 'Request failed'
|
detail = err?.message || 'Request failed'
|
||||||
}
|
}
|
||||||
|
|
||||||
return detail
|
return sanitizeApiErrorDetail(detail, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const download = async (u, p = {}, c = {}) => {
|
export const download = async (u, p = {}, c = {}) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user