feat: /api/v2/analiza/* endpoints - sport analytics backend

This commit is contained in:
Damir Radulic
2026-05-16 00:28:12 +02:00
parent 7ca5d7d94e
commit aca5051418
1355 changed files with 321891 additions and 4128 deletions
+84 -2
View File
@@ -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}`);