CRISIS V3: definitive apiAuth + mobile hamburger + Playwright E2E test
apiAuth in app.html: - Pre-checks JWT exp client-side BEFORE making request - On expired: clears localStorage + redirects /login?reason=expired - On 401 from server: clears + redirects /login?reason=unauthorized - Single-flight redirect via window.__pgz_redirecting flag login.html: - Toast for ?reason=expired (red) / ?reason=unauthorized (orange) app.html mobile: - Hamburger button injected into topbar (.tb) - Mobile CSS: sidebar slide-in -280→0, backdrop overlay, full-width drill-down - toggleMobileSidebar() global function - @media (max-width:768px) display:inline-flex, sidebar fixed pos scripts/playwright_e2e.py: - Desktop test (1280x800): login, JWT persist, profile, logo, logout - Mobile test (375x812 iPhone X): viewport, login flow, hamburger, no h-scroll - Output: _audit/playwright_<TS>/results.json + screenshots/*.png Reproducible: TS=YYYYmmdd_HHMM python3 scripts/playwright_e2e.py
This commit is contained in:
+65
-2
@@ -309,6 +309,38 @@ table tbody tr:hover{background:var(--bg3)}
|
||||
@media (max-width: 768px) {
|
||||
.sb-backdrop.show { display: block; }
|
||||
}
|
||||
|
||||
/* Hamburger button visibility (CRISIS V3) */
|
||||
.mobile-menu-btn {
|
||||
display: none;
|
||||
background: var(--bg2,#1a1a1e);
|
||||
color: var(--t1,#fff);
|
||||
border: 1px solid var(--rim,#2a2a2e);
|
||||
padding: 6px 10px;
|
||||
font-size: 18px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-right: 8px;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.mobile-menu-btn { display: inline-flex !important; align-items: center; justify-content: center; }
|
||||
.sb {
|
||||
position: fixed !important; left: -280px !important; top: 0 !important;
|
||||
width: 260px !important; height: 100vh !important; z-index: 1000 !important;
|
||||
transition: left 0.3s ease !important;
|
||||
}
|
||||
.sb.mobile-open { left: 0 !important; }
|
||||
.main { margin-left: 0 !important; }
|
||||
.sb-backdrop {
|
||||
display: none; position: fixed; inset: 0;
|
||||
background: rgba(0,0,0,.55); z-index: 999;
|
||||
}
|
||||
.sb-backdrop.show { display: block !important; }
|
||||
|
||||
/* Center mobile content */
|
||||
.content, .main { padding: 12px !important; }
|
||||
.tb { padding: 8px 12px !important; }
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/shared/sidebar.css">
|
||||
<script src="/static/shared/sidebar.js" defer data-active="profil"></script>
|
||||
@@ -337,6 +369,7 @@ table tbody tr:hover{background:var(--bg3)}
|
||||
|
||||
<main class="main">
|
||||
<div class="tb">
|
||||
<button class="mobile-menu-btn" onclick="toggleMobileSidebar()" aria-label="Menu" type="button">☰</button>
|
||||
<div>
|
||||
<div class="tb-t" id="tb-t">Dashboard</div>
|
||||
<div class="tb-s" id="tb-s">Pregled stanja</div>
|
||||
@@ -398,11 +431,41 @@ function getToken(){
|
||||
async function apiAuth(path, opts){
|
||||
opts = opts || {};
|
||||
const h = Object.assign({}, opts.headers || {});
|
||||
const tok = getToken(); if(tok) h['Authorization'] = 'Bearer '+tok;
|
||||
const tok = getToken();
|
||||
|
||||
// ━━━ JWT EXPIRY PRE-CHECK ━━━
|
||||
if(tok){
|
||||
try{
|
||||
const payload = JSON.parse(atob(tok.split('.')[1]));
|
||||
if(payload.exp && payload.exp * 1000 < Date.now()){
|
||||
console.warn('[apiAuth] JWT expired client-side, redirecting');
|
||||
['pgz_access','pgz_refresh','pgz_user','jwt','access_token'].forEach(k => {
|
||||
try{localStorage.removeItem(k); sessionStorage.removeItem(k);}catch(e){}
|
||||
});
|
||||
if(!window.__pgz_redirecting){ window.__pgz_redirecting = true; window.location.href = '/login?reason=expired'; }
|
||||
return {__unauthorized:true, status:401};
|
||||
}
|
||||
}catch(e){ /* token not parseable, continue and let server respond */ }
|
||||
h['Authorization'] = 'Bearer '+tok;
|
||||
}
|
||||
|
||||
if(opts.body && !(opts.body instanceof FormData) && !h['Content-Type']) h['Content-Type'] = 'application/json';
|
||||
try {
|
||||
const r = await fetch(API+path, Object.assign({}, opts, {headers:h}));
|
||||
if(r.status === 401){ return {__unauthorized:true, status:401}; }
|
||||
if(r.status === 401){
|
||||
// ━━━ GLOBAL 401 HANDLER — clear + redirect ━━━
|
||||
console.warn('[apiAuth] 401 from server, clearing localStorage + redirecting');
|
||||
['pgz_access','pgz_refresh','pgz_user','jwt','access_token'].forEach(k => {
|
||||
try{localStorage.removeItem(k); sessionStorage.removeItem(k);}catch(e){}
|
||||
});
|
||||
// Don't redirect from /login itself; allow profile page to handle
|
||||
const onLogin = location.pathname.includes('/login');
|
||||
if(!onLogin && !window.__pgz_redirecting){
|
||||
window.__pgz_redirecting = true;
|
||||
window.location.href = '/login?reason=unauthorized';
|
||||
}
|
||||
return {__unauthorized:true, status:401};
|
||||
}
|
||||
if(!r.ok) return {__error:true, status:r.status};
|
||||
if(r.headers.get('content-type')?.includes('application/json')) return await r.json();
|
||||
return {__ok:true};
|
||||
|
||||
@@ -582,5 +582,22 @@ $('#cookieMore').addEventListener('click', e => { e.preventDefault(); $('#privac
|
||||
}, 100);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// Auto-toast za reason=expired / unauthorized (CRISIS V3)
|
||||
(function(){
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const reason = params.get('reason');
|
||||
if(!reason) return;
|
||||
setTimeout(() => {
|
||||
const div = document.createElement('div');
|
||||
div.id = 'pgz-reason-toast';
|
||||
div.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);background:'+(reason==='expired'?'#c0392b':'#e67e22')+';color:#fff;padding:12px 20px;border-radius:6px;z-index:9999;font-size:14px;box-shadow:0 4px 12px rgba(0,0,0,.3);font-family:system-ui,sans-serif';
|
||||
div.textContent = reason==='expired' ? 'Sesija je istekla. Molim prijavi se ponovno.' : (reason==='unauthorized' ? 'Sesija je nevažeća. Prijavi se opet.' : 'Potrebna prijava.');
|
||||
document.body.appendChild(div);
|
||||
setTimeout(() => div.remove(), 6000);
|
||||
}, 100);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user