feat: /api/v2/analiza/* endpoints - sport analytics backend
This commit is contained in:
+84
-2
@@ -104,6 +104,7 @@ table tbody tr:hover{background:var(--bg3)}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/shared/sidebar.css">
|
||||
<script src="/static/shared/sidebar.js" defer data-active="erp_full"></script>
|
||||
<script src="/static/shared/sortable.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -623,11 +624,92 @@ table tbody tr:hover{background:var(--bg3)}
|
||||
|
||||
<script>
|
||||
const API = '/api/v2/erp';
|
||||
const AUTH = () => ({ 'Authorization': 'Bearer ' + (localStorage.getItem('jwt') || localStorage.getItem('access_token') || 'admin-pgz-2026') });
|
||||
// ━━━ MZ4-04 (2026-05-10): hardened auth helper — no 'admin-pgz-2026' fallback ━━━
|
||||
function getToken(){
|
||||
try {
|
||||
return localStorage.getItem('pgz_access')
|
||||
|| sessionStorage.getItem('pgz_access')
|
||||
|| localStorage.getItem('jwt')
|
||||
|| localStorage.getItem('access_token')
|
||||
|| '';
|
||||
} catch(e){ return ''; }
|
||||
}
|
||||
function isJwtExpired(tok){
|
||||
if(!tok) return true;
|
||||
try {
|
||||
const payload = JSON.parse(atob(tok.split('.')[1]));
|
||||
return !!payload.exp && payload.exp * 1000 < Date.now();
|
||||
} catch(e){ return false; } // not parseable — let server decide
|
||||
}
|
||||
function clearAuthAndRedirect(reason){
|
||||
['pgz_access','pgz_refresh','pgz_user','jwt','access_token','refresh_token'].forEach(k => {
|
||||
try{ localStorage.removeItem(k); sessionStorage.removeItem(k); }catch(e){}
|
||||
});
|
||||
if(!window.__pgz_redirecting){
|
||||
window.__pgz_redirecting = true;
|
||||
window.location.href = '/sport/login?reason='+(reason||'unauthorized');
|
||||
}
|
||||
}
|
||||
async function tryRefreshToken(){
|
||||
const rt = localStorage.getItem('pgz_refresh') || localStorage.getItem('refresh_token');
|
||||
if(!rt) return null;
|
||||
try {
|
||||
const r = await fetch('/api/auth/refresh', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({refresh_token: rt})
|
||||
});
|
||||
if(!r.ok) return null;
|
||||
const d = await r.json();
|
||||
if(d.access_token){
|
||||
localStorage.setItem('pgz_access', d.access_token);
|
||||
localStorage.setItem('access_token', d.access_token);
|
||||
return d.access_token;
|
||||
}
|
||||
} catch(e){}
|
||||
return null;
|
||||
}
|
||||
function AUTH(){
|
||||
const tok = getToken();
|
||||
return tok ? { 'Authorization': 'Bearer ' + tok } : {};
|
||||
}
|
||||
const fmt = n => (Number(n||0)).toLocaleString('hr-HR',{minimumFractionDigits:2,maximumFractionDigits:2});
|
||||
|
||||
async function api(path, opts={}) {
|
||||
const r = await fetch(API + path, { headers: { 'Content-Type':'application/json', ...AUTH() }, ...opts });
|
||||
let tok = getToken();
|
||||
if(tok && isJwtExpired(tok)){
|
||||
console.warn('[erp.api] access_token expired client-side; refreshing');
|
||||
const fresh = await tryRefreshToken();
|
||||
if(!fresh){ clearAuthAndRedirect('expired'); throw new Error('401: token expired, refresh failed'); }
|
||||
tok = fresh;
|
||||
}
|
||||
const headers = Object.assign(
|
||||
{ 'Content-Type':'application/json' },
|
||||
tok ? {'Authorization':'Bearer '+tok} : {},
|
||||
(opts.headers||{})
|
||||
);
|
||||
const r = await fetch(API + path, Object.assign({}, opts, {headers}));
|
||||
if (r.status === 401) {
|
||||
// One-shot refresh + retry
|
||||
const fresh = await tryRefreshToken();
|
||||
if(fresh){
|
||||
const retryHeaders = Object.assign(
|
||||
{ 'Content-Type':'application/json' },
|
||||
{ 'Authorization':'Bearer '+fresh },
|
||||
(opts.headers||{})
|
||||
);
|
||||
const r2 = await fetch(API + path, Object.assign({}, opts, {headers: retryHeaders}));
|
||||
if (r2.ok) {
|
||||
if (r2.headers.get('content-type')?.includes('application/json')) return r2.json();
|
||||
return {__ok:true};
|
||||
}
|
||||
if (r2.status === 401) { clearAuthAndRedirect('unauthorized'); throw new Error('401: refresh did not restore auth'); }
|
||||
const detail2 = await r2.text();
|
||||
throw new Error(`${r2.status}: ${detail2}`);
|
||||
}
|
||||
clearAuthAndRedirect('unauthorized');
|
||||
throw new Error('401: not authenticated');
|
||||
}
|
||||
if (!r.ok) {
|
||||
let detail = await r.text();
|
||||
throw new Error(`${r.status}: ${detail}`);
|
||||
|
||||
Reference in New Issue
Block a user