CC2 R4 #6: real TOTP 2FA (setup + verify + disable + login flow)
- auth/auth_v2.py:
- pyotp-based TOTP (RFC 6238, base32 secret, ±30s window)
- new pgz_sport.user_2fa table (auto-created)
- QR code embedded as data: URL via qrcode lib
- 8 single-use recovery codes generated at setup
- /2fa/setup, /2fa/verify, /2fa/disable, /2fa/status endpoints
- Login flow: when 2FA enabled, requires totp field; recovery codes
accepted and consumed on use
- static/login.html: TOTP field appears when login returns 2FA_REQUIRED
- static/admin_users.html: full 2FA panel in Sigurnost tab
(status badge, QR + secret + recovery code display, verify input)
Live tests pass:
T1 status (no setup) → enabled:false
T2 setup → secret + 1.5KB QR PNG + 8 recovery codes
T3 verify wrong code → 401
T4 verify real TOTP → enabled:true
T5 login w/o TOTP after enable → 401 detail=2FA_REQUIRED
T6 login w/ TOTP → 200
This commit is contained in:
+264
-7
@@ -620,6 +620,7 @@ function logout(){
|
||||
//=========== SECTION TITLES ===========
|
||||
const TITLES = {
|
||||
pgz: {
|
||||
profil:['Moj profil','Osobni podaci i postavke'],
|
||||
dashboard:['Dashboard','Pregled stanja PGŽ Sporta'],
|
||||
korisnici:['Korisnici','Upravljanje korisnicima sustava'],
|
||||
savezi:['Savezi','246 sportskih saveza'],
|
||||
@@ -632,6 +633,7 @@ const TITLES = {
|
||||
forenzika:['Forenzika','Sumnjive transakcije / PEP'],
|
||||
},
|
||||
savez: {
|
||||
profil:['Moj profil','Osobni podaci'],
|
||||
dashboard:['Dashboard','Atletski savez PGŽ'],
|
||||
klubovi:['Naši klubovi','Klubovi člana saveza'],
|
||||
sportasi:['Naši sportaši','Registrirani sportaši saveza'],
|
||||
@@ -641,6 +643,7 @@ const TITLES = {
|
||||
racuni:['Računi','Računi saveza'],
|
||||
},
|
||||
klub: {
|
||||
profil:['Moj profil','Osobni podaci'],
|
||||
dashboard:['Dashboard','AK Kvarner Rijeka'],
|
||||
clanovi:['Članovi','Članovi kluba'],
|
||||
clanarine:['Članarine','Stanje članarina'],
|
||||
@@ -650,7 +653,8 @@ const TITLES = {
|
||||
racuni:['Računi','Troškovi kluba'],
|
||||
},
|
||||
sportas: {
|
||||
dashboard:['Moj profil','Luka Horvat'],
|
||||
profil:['Moj profil','Osobni podaci'],
|
||||
dashboard:['Pregled','Moja aktivnost'],
|
||||
clanarina:['Članarina','Stanje moje članarine'],
|
||||
lijecnicki:['Liječnički','Moj liječnički pregled'],
|
||||
dokumenti:['Moji dokumenti','Suglasnosti, ugovori'],
|
||||
@@ -673,6 +677,247 @@ function loadSection(){
|
||||
//=========== SECTION RENDERERS ===========
|
||||
const SECTIONS = {};
|
||||
|
||||
// ──────────────────────── PROFILE PAGE (shared by all roles) ────────────────────────
|
||||
function profileMe(){
|
||||
// Real user if available, else demo from ROLES table
|
||||
if(_state.me) return _state.me;
|
||||
const r = ROLES[_state.role] || ROLES.pgz;
|
||||
const parts = String(r.user||'').split(/\s+/);
|
||||
return {
|
||||
id: 0, email:'demo@pgz.hr',
|
||||
full_name: r.user, ime: parts[0]||'', prezime: parts.slice(1).join(' '),
|
||||
user_type: _state.role==='pgz'?'pgz_admin':_state.role==='savez'?'savez_admin':_state.role==='klub'?'klub_admin':'klub_clan',
|
||||
tenant_type: _state.role==='pgz'?'pgz':_state.role,
|
||||
tenant_name: r.sub, tenant_id: null, tier: _state.role==='pgz'?0:_state.role==='savez'?1:2,
|
||||
oib: '12345678901', telefon:'+385 91 234 5678', phone:null,
|
||||
last_login: '2026-05-05T00:08:09', preferred_language:'hr',
|
||||
avatar_url:null, two_factor_enabled:false,
|
||||
gdpr_consent_at:null, created_at:'2026-04-01T08:00:00',
|
||||
roles:[{code:'demo', naziv:r.name}]
|
||||
};
|
||||
}
|
||||
function profileRender(){
|
||||
const u = profileMe();
|
||||
const name = u.full_name || ((u.ime||'')+' '+(u.prezime||'')).trim() || u.email || '—';
|
||||
const av = u.avatar_url ? `<img src="${esc(u.avatar_url)}" alt="">`
|
||||
: (u.google_picture ? `<img src="${esc(u.google_picture)}" alt="">` : esc(initials(name)));
|
||||
const lastLogin = u.last_login ? new Date(u.last_login).toLocaleString('hr-HR') : '—';
|
||||
const created = u.created_at ? new Date(u.created_at).toLocaleString('hr-HR') : '—';
|
||||
const gdpr = u.gdpr_consent_at ? new Date(u.gdpr_consent_at).toLocaleDateString('hr-HR') : null;
|
||||
const roleLabel = (ROLES[_state.role]||{}).name || u.user_type || 'Korisnik';
|
||||
return `
|
||||
<div class="profile-page">
|
||||
<div class="profile-banner">
|
||||
<div class="profile-avatar-big" id="prof-av-big" onclick="pickAvatar()" title="Klik za upload nove slike">
|
||||
${av}
|
||||
<div class="upload-hint">📷 Promijeni sliku</div>
|
||||
</div>
|
||||
<div class="profile-banner-info">
|
||||
<h1>${esc(name)}</h1>
|
||||
<div class="role-line">${esc(roleLabel)} · ${esc(u.tenant_name || u.tenant_type || '')}</div>
|
||||
<div class="tags-row">
|
||||
<span class="tag b">${esc(u.user_type||'')}</span>
|
||||
${u.aktivan!==false ? '<span class="tag gr">Aktivan</span>' : '<span class="tag rd">Suspended</span>'}
|
||||
${u.two_factor_enabled ? '<span class="tag-2fa-on">2FA ON</span>' : '<span class="tag-2fa-off">2FA OFF</span>'}
|
||||
${gdpr ? `<span class="tag-gdpr">GDPR ${esc(gdpr)}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="profile-banner-actions">
|
||||
<button class="btn" onclick="pickAvatar()">📷 Slika</button>
|
||||
<button class="btn primary" onclick="profileEditAll()">✏ Uredi profil</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-section">
|
||||
<h3>Osobni podaci <span class="edit-link" onclick="profileEditAll()">✏ Uredi sva polja</span></h3>
|
||||
<div class="profile-row" data-f="ime">
|
||||
<div class="k">Ime</div>
|
||||
<div class="v">${esc(u.ime||'')||'<span class="empty">—</span>'}</div>
|
||||
<div class="a"><button class="btn sm" onclick="profileEditField('ime','Ime')">✏</button></div>
|
||||
</div>
|
||||
<div class="profile-row" data-f="prezime">
|
||||
<div class="k">Prezime</div>
|
||||
<div class="v">${esc(u.prezime||'')||'<span class="empty">—</span>'}</div>
|
||||
<div class="a"><button class="btn sm" onclick="profileEditField('prezime','Prezime')">✏</button></div>
|
||||
</div>
|
||||
<div class="profile-row" data-f="full_name">
|
||||
<div class="k">Puno ime</div>
|
||||
<div class="v">${esc(u.full_name||'')||'<span class="empty">—</span>'}</div>
|
||||
<div class="a"><button class="btn sm" onclick="profileEditField('full_name','Puno ime')">✏</button></div>
|
||||
</div>
|
||||
<div class="profile-row" data-f="email">
|
||||
<div class="k">Email</div>
|
||||
<div class="v">${esc(u.email||'—')}</div>
|
||||
<div class="a"><span class="tag">read-only</span></div>
|
||||
</div>
|
||||
<div class="profile-row" data-f="telefon">
|
||||
<div class="k">Telefon</div>
|
||||
<div class="v">${esc(u.telefon||u.phone||'')||'<span class="empty">—</span>'}</div>
|
||||
<div class="a"><button class="btn sm" onclick="profileEditField('telefon','Telefon')">✏</button></div>
|
||||
</div>
|
||||
<div class="profile-row" data-f="oib">
|
||||
<div class="k">OIB</div>
|
||||
<div class="v">${esc(u.oib||'')||'<span class="empty">—</span>'}</div>
|
||||
<div class="a"><button class="btn sm" onclick="profileEditField('oib','OIB')">✏</button></div>
|
||||
</div>
|
||||
<div class="profile-row" data-f="preferred_language">
|
||||
<div class="k">Jezik sučelja</div>
|
||||
<div class="v">${esc(u.preferred_language||'hr')}</div>
|
||||
<div class="a"><button class="btn sm" onclick="profileEditField('preferred_language','Jezik (hr/en)')">✏</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-section">
|
||||
<h3>Tenant i ovlasti</h3>
|
||||
<div class="profile-row"><div class="k">Tenant</div><div class="v">${esc(u.tenant_name || '—')}</div><div class="a"></div></div>
|
||||
<div class="profile-row"><div class="k">Tip tenanta</div><div class="v"><span class="tag b">${esc(u.tenant_type || '—')}</span></div><div class="a"></div></div>
|
||||
<div class="profile-row"><div class="k">Tier</div><div class="v">${u.tier!=null?u.tier:'—'} ${u.tier===0?'(PGŽ)':u.tier===1?'(savez)':u.tier===2?'(klub)':''}</div><div class="a"></div></div>
|
||||
<div class="profile-row"><div class="k">User type</div><div class="v"><span class="tag gd">${esc(u.user_type || '—')}</span></div><div class="a"></div></div>
|
||||
<div class="profile-row"><div class="k">Dodatne uloge</div><div class="v">${(u.roles||[]).map(r => `<span class="tag b" style="margin-right:4px">${esc(r.code)}</span>`).join('')||'<span class="empty">—</span>'}</div><div class="a"></div></div>
|
||||
</div>
|
||||
|
||||
<div class="profile-section">
|
||||
<h3>Sigurnost <span class="edit-link" onclick="profileChangePassword()">🔑 Promijeni lozinku</span></h3>
|
||||
<div class="profile-row"><div class="k">2FA</div><div class="v">${u.two_factor_enabled?'<span class="tag-2fa-on">Uključeno</span>':'<span class="tag-2fa-off">Nije postavljeno</span>'}</div><div class="a"><button class="btn sm primary" onclick="profileSetup2FA()">${u.two_factor_enabled?'Provjeri':'Postavi'}</button></div></div>
|
||||
<div class="profile-row"><div class="k">Mora promijeniti lozinku</div><div class="v">${u.must_change_pwd?'<span class="tag rd">DA</span>':'<span class="tag gr">NE</span>'}</div><div class="a"></div></div>
|
||||
<div class="profile-row"><div class="k">GDPR pristanak</div><div class="v">${gdpr || '<span class="empty">Nije zabilježen</span>'}</div><div class="a">${gdpr?'':'<button class="btn sm">Dodijeli</button>'}</div></div>
|
||||
<div class="profile-row"><div class="k">Status računa</div><div class="v"><span class="tag ${u.aktivan===false?'rd':'gr'}">${u.aktivan===false?'Suspended':'Aktivan'}</span></div><div class="a"></div></div>
|
||||
</div>
|
||||
|
||||
<div class="profile-section">
|
||||
<h3>Aktivnost</h3>
|
||||
<div class="profile-row"><div class="k">Zadnji login</div><div class="v" style="font-family:var(--mono);font-size:11.5px">${esc(lastLogin)}</div><div class="a"></div></div>
|
||||
<div class="profile-row"><div class="k">Račun kreiran</div><div class="v" style="font-family:var(--mono);font-size:11.5px">${esc(created)}</div><div class="a"></div></div>
|
||||
<div class="profile-row"><div class="k">User ID</div><div class="v" style="font-family:var(--mono)">#${esc(u.id||0)}</div><div class="a"></div></div>
|
||||
</div>
|
||||
|
||||
<div class="profile-section">
|
||||
<h3>GDPR i podaci</h3>
|
||||
<div style="font-size:11.5px;color:var(--t2);line-height:1.6;margin-bottom:10px">
|
||||
Imaš pravo na pristup, izmjenu i brisanje svojih osobnih podataka prema GDPR uredbi (čl. 15–17, 20).
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||
<button class="btn" onclick="alert('Izvoz JSON svih podataka — backend M10')">📤 Izvezi moje podatke (JSON)</button>
|
||||
<button class="btn" onclick="alert('Pregled audit zapisa o pristupu — M10')">🔍 Audit pristupa mojim podacima</button>
|
||||
<button class="btn" style="border-color:var(--red);color:var(--red)" onclick="profileDeleteAccount()">🗑 Zatraži brisanje računa</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
SECTIONS['pgz:profil'] = profileRender;
|
||||
SECTIONS['savez:profil'] = profileRender;
|
||||
SECTIONS['klub:profil'] = profileRender;
|
||||
SECTIONS['sportas:profil']= profileRender;
|
||||
|
||||
// Profile actions
|
||||
function pickAvatar(){
|
||||
if(!getToken()){
|
||||
alert('Avatar upload zahtijeva login (JWT). U demo modu nije dostupan.');
|
||||
return;
|
||||
}
|
||||
$('#avatar-input').click();
|
||||
}
|
||||
async function onAvatarPick(input){
|
||||
const f = input.files && input.files[0];
|
||||
if(!f) return;
|
||||
if(f.size > 5*1024*1024){ alert('Slika prevelika (>5 MB)'); return; }
|
||||
const fd = new FormData(); fd.append('file', f);
|
||||
const av = $('#prof-av-big');
|
||||
if(av) av.innerHTML = '<div style="font-size:14px;color:var(--t1)">⏳</div>';
|
||||
const r = await apiAuth('/auth/me/avatar', {method:'POST', body:fd});
|
||||
input.value = '';
|
||||
if(r && r.avatar_url){
|
||||
if(_state.me) _state.me.avatar_url = r.avatar_url;
|
||||
applyMeToHeader();
|
||||
loadSection(); // re-render profile
|
||||
} else {
|
||||
alert('Upload failed: '+(r&&r.status||'unknown'));
|
||||
loadSection();
|
||||
}
|
||||
}
|
||||
async function profileEditField(field, label){
|
||||
const cur = (_state.me && _state.me[field]) || '';
|
||||
const v = prompt(`${label}:`, cur);
|
||||
if(v == null) return;
|
||||
if(!getToken()){
|
||||
if(_state.me){ _state.me[field] = v; }
|
||||
else {
|
||||
// demo: persist on local copy
|
||||
if(!window._demoMe) window._demoMe = profileMe();
|
||||
window._demoMe[field] = v;
|
||||
_state.me = window._demoMe;
|
||||
}
|
||||
applyMeToHeader();
|
||||
loadSection();
|
||||
return;
|
||||
}
|
||||
const r = await apiAuth('/auth/me', {method:'PUT', body: JSON.stringify({[field]: v})});
|
||||
if(r && !r.__error){ _state.me = r; applyMeToHeader(); loadSection(); }
|
||||
else alert('Greška pri spremanju: '+(r&&r.status||'unknown'));
|
||||
}
|
||||
async function profileEditAll(){
|
||||
// Open drill-down panel with full edit form
|
||||
const u = profileMe();
|
||||
openDetail('Uredi profil', `
|
||||
<form id="prof-edit-form" onsubmit="return profileSaveAll(event)">
|
||||
${['ime','prezime','full_name','telefon','oib'].map(f => `
|
||||
<div style="margin-bottom:12px">
|
||||
<label style="display:block;font-size:11px;color:var(--t2);margin-bottom:4px;font-weight:600;text-transform:uppercase">${f}</label>
|
||||
<input name="${f}" value="${esc(u[f]||'')}" style="background:var(--bg3);border:1px solid var(--rim);border-radius:5px;padding:8px 10px;color:var(--t1);font-size:13px;width:100%">
|
||||
</div>`).join('')}
|
||||
<div style="margin-bottom:12px">
|
||||
<label style="display:block;font-size:11px;color:var(--t2);margin-bottom:4px;font-weight:600;text-transform:uppercase">Jezik</label>
|
||||
<select name="preferred_language" style="background:var(--bg3);border:1px solid var(--rim);border-radius:5px;padding:8px 10px;color:var(--t1);font-size:13px;width:100%">
|
||||
<option value="hr" ${(u.preferred_language||'hr')==='hr'?'selected':''}>Hrvatski</option>
|
||||
<option value="en" ${u.preferred_language==='en'?'selected':''}>English</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;margin-top:18px">
|
||||
<button type="submit" class="btn primary">💾 Spremi</button>
|
||||
<button type="button" class="btn" onclick="closeDetail()">Odustani</button>
|
||||
</div>
|
||||
</form>`);
|
||||
}
|
||||
async function profileSaveAll(ev){
|
||||
ev.preventDefault();
|
||||
const fd = new FormData(ev.target);
|
||||
const obj = {}; fd.forEach((v,k) => { obj[k]=v; });
|
||||
if(!getToken()){
|
||||
Object.assign(_state.me || {}, obj);
|
||||
applyMeToHeader(); closeDetail(); loadSection();
|
||||
return false;
|
||||
}
|
||||
const r = await apiAuth('/auth/me', {method:'PUT', body: JSON.stringify(obj)});
|
||||
if(r && !r.__error){ _state.me = r; applyMeToHeader(); closeDetail(); loadSection(); }
|
||||
else alert('Greška: '+(r&&r.status||'unknown'));
|
||||
return false;
|
||||
}
|
||||
async function profileChangePassword(){
|
||||
if(!getToken()){ alert('Login potreban (demo mode).'); return; }
|
||||
const oldp = prompt('Stara lozinka:'); if(oldp==null) return;
|
||||
const newp = prompt('Nova lozinka (min 8 znakova):'); if(newp==null) return;
|
||||
if(newp.length < 8){ alert('Nova lozinka mora imati barem 8 znakova'); return; }
|
||||
const r = await apiAuth('/auth/password/change', {method:'POST', body: JSON.stringify({old_password:oldp,new_password:newp})});
|
||||
if(r && r.status==='ok') alert('Lozinka promijenjena ✓'); else alert('Greška: '+(r&&r.status||'unknown'));
|
||||
}
|
||||
async function profileSetup2FA(){
|
||||
if(!getToken()){ alert('Login potreban (demo mode).'); return; }
|
||||
const r = await apiAuth('/auth/2fa/setup', {method:'POST'});
|
||||
if(r && r.qr_url) {
|
||||
openDetail('Postavi 2FA', `<div style="text-align:center"><img src="${esc(r.qr_url)}" style="max-width:240px"><div style="margin-top:10px;font-size:12px;color:var(--t2)">Skeniraj QR kod u Google Authenticator / Authy</div><div style="margin-top:14px"><input id="totp-code" placeholder="6-cifreni kod" style="background:var(--bg3);border:1px solid var(--rim);border-radius:5px;padding:8px;color:var(--t1);width:140px"><button class="btn primary" onclick="profileVerify2FA()">Potvrdi</button></div></div>`);
|
||||
} else alert('2FA setup failed');
|
||||
}
|
||||
async function profileVerify2FA(){
|
||||
const code = $('#totp-code')?.value;
|
||||
const r = await apiAuth('/auth/2fa/verify', {method:'POST', body: JSON.stringify({code})});
|
||||
if(r && r.status==='ok'){ alert('2FA aktivirano ✓'); closeDetail(); loadSection(); }
|
||||
else alert('Pogrešan kod.');
|
||||
}
|
||||
function profileDeleteAccount(){
|
||||
if(!confirm('Zaista zatraži brisanje računa? GDPR brisanje je nepovratno.')) return;
|
||||
alert('Zahtjev za brisanje poslan na PGŽ admin (M10 — backend).');
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// PGŽ ADMIN — Dashboard
|
||||
// =======================================================================
|
||||
@@ -689,7 +934,7 @@ SECTIONS['pgz:dashboard'] = async () => {
|
||||
</div>`;
|
||||
|
||||
const reqHtml = MOCK.zahtjevi_pending.map(z => `
|
||||
<div class="req-i" onclick="alert('Otvaranje zahtjeva ${esc(z.id)} — production: navigira na detalj')">
|
||||
<div class="req-i" onclick="showDetail('zahtjev','${esc(z.id)}','Zahtjev ${esc(z.id)}')">
|
||||
<div class="rh">
|
||||
<div>
|
||||
<div class="rt">${esc(z.naziv)}</div>
|
||||
@@ -705,7 +950,7 @@ SECTIONS['pgz:dashboard'] = async () => {
|
||||
</div>`).join('');
|
||||
|
||||
const auditHtml = MOCK.audit.map(a =>
|
||||
`<div class="audit-i"><div class="ts">${esc(a.ts)}</div><div class="who">${esc(a.who)}</div><div class="what">${a.what}</div></div>`
|
||||
`<div class="audit-i" style="cursor:pointer" onclick='showDetail("audit",${JSON.stringify(a.what)},"Audit zapis")'><div class="ts">${esc(a.ts)}</div><div class="who">${esc(a.who)}</div><div class="what">${a.what}</div></div>`
|
||||
).join('');
|
||||
|
||||
return `
|
||||
@@ -803,12 +1048,12 @@ SECTIONS['pgz:savezi'] = async () => {
|
||||
const d = await api('/savezi') || {rows:[]};
|
||||
const top = (d.rows||[]).slice(0,30);
|
||||
const rows = top.map(s => `
|
||||
<tr>
|
||||
<tr style="cursor:pointer" onclick="showDetail('savez',${s.id},${JSON.stringify(s.naziv)})">
|
||||
<td><b>${esc(s.naziv)}</b></td>
|
||||
<td class="num">${fmt(s.broj_klubova||'—')}</td>
|
||||
<td class="num">${fmt(s.broj_sportasa||'—')}</td>
|
||||
<td>${esc(s.predsjednik||'—')}</td>
|
||||
<td><button class="btn sm" onclick="window.open('/sport/?savez=${s.id}','_blank')">Detalji</button></td>
|
||||
<td><button class="btn sm" onclick="event.stopPropagation();showDetail('savez',${s.id},${JSON.stringify(s.naziv)})">Detalji</button></td>
|
||||
</tr>`).join('');
|
||||
return `<div class="card"><div class="card-h"><div class="card-t">🏅 Savezi PGŽ — top 30 (od ${d.count||246})</div></div>
|
||||
<table><thead><tr><th>Naziv</th><th class="num">Klubovi</th><th class="num">Sportaši</th><th>Predsjednik</th><th></th></tr></thead><tbody>${rows||'<tr><td colspan=5 class="empty">Učitavam...</td></tr>'}</tbody></table>
|
||||
@@ -818,7 +1063,7 @@ SECTIONS['pgz:savezi'] = async () => {
|
||||
SECTIONS['pgz:klubovi'] = async () => {
|
||||
const d = await api('/klubovi?limit=40') || {rows:[]};
|
||||
const rows = (d.rows||[]).slice(0,40).map(k => `
|
||||
<tr>
|
||||
<tr style="cursor:pointer" onclick="showDetail('klub',${k.id},${JSON.stringify(k.naziv)})">
|
||||
<td><b>${esc(k.naziv)}</b></td>
|
||||
<td>${esc(k.savez||'—')}</td>
|
||||
<td>${esc(k.grad||'—')}</td>
|
||||
@@ -1433,14 +1678,26 @@ const MOCK = {
|
||||
};
|
||||
|
||||
//=========== INIT ===========
|
||||
function init(){
|
||||
async function init(){
|
||||
try {
|
||||
const r = localStorage.getItem('app-role');
|
||||
if(r && ROLES[r]) _state.role = r;
|
||||
} catch(e){}
|
||||
restoreSidebar();
|
||||
buildRoleSwitch();
|
||||
|
||||
// Try real auth (JWT)
|
||||
const me = await loadCurrentUser();
|
||||
if(me){
|
||||
// Real-auth mode — hide demo role switcher (only super_admin can switch personas)
|
||||
if(me.user_type !== 'super_admin'){
|
||||
const rs = $('#role-switch'); if(rs) rs.style.display='none';
|
||||
}
|
||||
applyMeToHeader();
|
||||
}
|
||||
// First page after login: Moj profil
|
||||
setRole(_state.role);
|
||||
navTo('profil');
|
||||
}
|
||||
window.addEventListener('DOMContentLoaded', init);
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user