diff --git a/_backups/app.html.cc3_post_profile.1777935003 b/_backups/app.html.cc3_post_profile.1777935003
new file mode 100644
index 0000000..05fd6e4
--- /dev/null
+++ b/_backups/app.html.cc3_post_profile.1777935003
@@ -0,0 +1,1705 @@
+
+
+
+
+
+
Two-factor authentication (2FA) moj račun
+
+ Učitavam…
+
+
+
+
+
+
+
+
Skenirajte QR u aplikaciji (Google Authenticator, Authy, 1Password, …) ili upišite secret ručno:
+
+
Kodovi za oporavak (sačuvajte ih sigurno):
+
+
+
+
+
+
+
+
+
+
Zaključani / failed-login računi
| E-mail | Uloga | Pokušaja | Zaključan do | Akcije |
|---|
@@ -692,8 +716,43 @@ async function loadSecurity() {
`).join('') || '
| Nema zaključanih računa |
';
+ load2FAStatus();
}
+// 2FA UI
+async function load2FAStatus() {
+ const r = await apiJson('/auth/2fa/status');
+ const enabled = !!(r && r.enabled);
+ $('#twofaStatus').className = 'badge ' + (enabled ? 'green' : 'gray');
+ $('#twofaStatus').textContent = enabled ? '✓ Omogućen' : 'Onemogućen';
+ $('#btnEnable2FA').style.display = enabled ? 'none' : '';
+ $('#btnDisable2FA').style.display = enabled ? '' : 'none';
+ $('#twofaSetup').style.display = 'none';
+}
+$('#btnEnable2FA').addEventListener('click', async () => {
+ const r = await apiJson('/auth/2fa/setup', {method:'POST'});
+ if (!r || !r.qr_png) return toast(r?.detail || 'Greška', 'error');
+ $('#twofaQr').src = r.qr_png;
+ $('#twofaSecret').textContent = r.secret;
+ $('#twofaRecovery').innerHTML = (r.recovery_codes||[]).map(c => `
${c}`).join('');
+ $('#twofaSetup').style.display = '';
+ $('#twofaConfirm').focus();
+});
+$('#btnVerify2FA').addEventListener('click', async () => {
+ const code = ($('#twofaConfirm').value || '').trim().replace(/\s/g,'');
+ if (!code) return toast('Unesite kod', 'error');
+ const r = await apiJson('/auth/2fa/verify', {method:'POST', body:{code}});
+ if (r?.status === 'ok') { toast('2FA omogućen ✓'); load2FAStatus(); }
+ else toast(r?.detail || 'Neispravan kod', 'error');
+});
+$('#btnDisable2FA').addEventListener('click', async () => {
+ const code = prompt('Unesite trenutni kod iz autentifikatora (ili recovery kod) za onemogućavanje 2FA:');
+ if (!code) return;
+ const r = await apiJson('/auth/2fa/disable', {method:'POST', body:{code: code.trim()}});
+ if (r?.status === 'ok') { toast('2FA onemogućen'); load2FAStatus(); }
+ else toast(r?.detail || 'Greška', 'error');
+});
+
// GDPR
async function loadGdpr() {
const er = await apiJson('/admin/gdpr/erasure-requests');
diff --git a/static/app.html b/static/app.html
index f7e2d10..05fd6e4 100644
--- a/static/app.html
+++ b/static/app.html
@@ -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 ? `
})
`
+ : (u.google_picture ? `
})
` : 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 `
+
+
+
+ ${av}
+
📷 Promijeni sliku
+
+
+
${esc(name)}
+
${esc(roleLabel)} · ${esc(u.tenant_name || u.tenant_type || '')}
+
+ ${esc(u.user_type||'')}
+ ${u.aktivan!==false ? 'Aktivan' : 'Suspended'}
+ ${u.two_factor_enabled ? '2FA ON' : '2FA OFF'}
+ ${gdpr ? `GDPR ${esc(gdpr)}` : ''}
+
+
+
+
+
+
+
+
+
+
Osobni podaci ✏ Uredi sva polja
+
+
Ime
+
${esc(u.ime||'')||'—'}
+
+
+
+
Prezime
+
${esc(u.prezime||'')||'—'}
+
+
+
+
Puno ime
+
${esc(u.full_name||'')||'—'}
+
+
+
+
Email
+
${esc(u.email||'—')}
+
read-only
+
+
+
Telefon
+
${esc(u.telefon||u.phone||'')||'—'}
+
+
+
+
OIB
+
${esc(u.oib||'')||'—'}
+
+
+
+
Jezik sučelja
+
${esc(u.preferred_language||'hr')}
+
+
+
+
+
+
Tenant i ovlasti
+
Tenant
${esc(u.tenant_name || '—')}
+
Tip tenanta
${esc(u.tenant_type || '—')}
+
Tier
${u.tier!=null?u.tier:'—'} ${u.tier===0?'(PGŽ)':u.tier===1?'(savez)':u.tier===2?'(klub)':''}
+
User type
${esc(u.user_type || '—')}
+
Dodatne uloge
${(u.roles||[]).map(r => `${esc(r.code)}`).join('')||'—'}
+
+
+
+
Sigurnost 🔑 Promijeni lozinku
+
2FA
${u.two_factor_enabled?'Uključeno':'Nije postavljeno'}
+
Mora promijeniti lozinku
${u.must_change_pwd?'DA':'NE'}
+
GDPR pristanak
${gdpr || 'Nije zabilježen'}
${gdpr?'':''}
+
Status računa
${u.aktivan===false?'Suspended':'Aktivan'}
+
+
+
+
Aktivnost
+
Zadnji login
${esc(lastLogin)}
+
Račun kreiran
${esc(created)}
+
+
+
+
+
GDPR i podaci
+
+ Imaš pravo na pristup, izmjenu i brisanje svojih osobnih podataka prema GDPR uredbi (čl. 15–17, 20).
+
+
+
+
+
+
+
+
`;
+}
+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 = '
⏳
';
+ 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', `
+
`);
+}
+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', `
`);
+ } 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 () => {
`;
const reqHtml = MOCK.zahtjevi_pending.map(z => `
-