Files
pgz-sport/_backups/sidebar.css.cc3_pre_redesign.1777937786
Damir Radulić f9ebcddf28 CC2 R6: middleware-wide JWT, avatar demo mode, mock mailer, login rate limit
#1 JWT middleware extended:
- Was: /api/admin/* only
- Now: any POST/PUT/PATCH/DELETE under /api/* requires Bearer JWT
- Whitelist (still anonymous): /api/auth/login, /refresh, /forgot-password,
  /password/reset, /reset-password, /setup-password, /google;
  /api/gdpr/consent; any path ending /avatar
- 14 mutating endpoints verified to return 401 without token

#2 Avatar upload demo mode (routers/clan_panel_router.py):
- Anonymous → returns {demo_mode:true, slika_url:null,
  message:'Demo mode — slika nije spremljena. Prijavite se za pravu pohranu.'},
  no FS write, no DB write
- Authenticated (valid JWT, allowed role) → real save as before
- Auth check now uses auth.auth_v2.decode_token (proper secret + revocation)
  instead of the broken local _resolve_role

#3 Mock mailer (auth/mailer.py):
- send_email writes RFC 822 .eml to /tmp/pgz_mailbox + appends to INDEX.jsonl
- send_password_reset, send_invite helpers with HR text + HTML alt
- Real SMTP active when PGZ_SMTP_HOST is set (env-driven, off by default)
- forgot-password and admin invite both call mailer; audit logs mail status

#5 Rate limiting on /api/auth/login:
- Per-user: 5 wrong attempts → 5-minute DB-backed lockout
  (was 5 → 15 min). Configurable via PGZ_LOGIN_LOCK_THRESHOLD/MINUTES.
- Per-IP: 10 fails / 5-min sliding window in-memory → HTTP 429
  Configurable via PGZ_LOGIN_IP_THRESHOLD/WINDOW_SEC. Successful
  login clears the IP counter.
- Failed attempts respond '(N/5) — račun je zaključan na 5 minuta'
- New audit actions: login.ratelimit.ip; login.fail meta now
  includes fails count, locked, lock_minutes

#4 Live test report: 46/46 across 6 demo users — login, JWT gate on 14
   mutating endpoints, public path whitelist, demo-mode avatar +
   real save, forgot-password e-mail to mailbox, no-leak unknown email,
   5-fail lockout, 423 during lockout, audit coverage.
2026-05-05 01:42:53 +02:00

149 lines
7.1 KiB
Plaintext

/* PGŽ SPORT — Unified Sidebar v1.0
* dradulic@outlook.com / damir@rinet.one — 2026-05-05
* Used by: sport2.html, app.html, admin.html, crm.html, erp.html, audit.html, kpi.html, login.html
* Reference: app.rinet.one/klasik/control
*/
:root{
--pgz-blue:#003087; --pgz-blue2:#004CC4; --pgz-gold:#F4C430;
--bg0:#08090e; --bg1:#0d1021; --bg2:#111628; --bg3:#161d35; --bg4:#1c2542;
--rim:#1e2a50; --rim2:#283560;
--t0:#fff; --t1:#e2e6f0; --t2:#8a95b4; --t4:#4e5a7a;
--green:#00e88f; --red:#ff2d55; --amber:#f59e0b; --cyan:#00c8e8;
--sb-w-exp:230px; --sb-w-col:58px;
}
#pgz-sb{
position:fixed; top:0; left:0; bottom:0; width:var(--sb-w-exp);
background:linear-gradient(180deg,var(--bg1) 0%,var(--bg0) 100%);
border-right:1px solid var(--rim);
display:flex; flex-direction:column; z-index:100;
font-family:'Inter',sans-serif; font-size:13px; color:var(--t1);
transition:width .22s ease, transform .22s ease;
}
#pgz-sb *{box-sizing:border-box}
#pgz-sb a{text-decoration:none;color:inherit}
/* Header */
.pgz-sb-h{padding:18px 18px 14px;border-bottom:1px solid var(--rim);position:relative;flex-shrink:0}
.pgz-sb-h .pgz-logo{font-weight:800;font-size:14px;color:var(--t0);letter-spacing:.5px;white-space:nowrap;overflow:hidden}
.pgz-sb-h .pgz-logo .g{color:var(--pgz-gold)}
.pgz-sb-h .pgz-sub{font-size:10px;color:var(--t2);margin-top:4px;text-transform:uppercase;letter-spacing:1px;white-space:nowrap;overflow:hidden}
.pgz-sb-toggle{
position:absolute;top:14px;right:8px;width:24px;height:24px;
display:flex;align-items:center;justify-content:center;cursor:pointer;
color:var(--t2);background:var(--bg2);border:1px solid var(--rim);
border-radius:5px;font-size:14px;font-weight:700;
transition:all .15s;user-select:none;
}
.pgz-sb-toggle:hover{background:var(--bg3);color:var(--pgz-gold);border-color:var(--pgz-gold)}
/* Section label / separator */
.pgz-sb-sep{padding:14px 14px 4px 14px;font-size:9.5px;color:var(--t4);
text-transform:uppercase;letter-spacing:1.2px;font-weight:700;
white-space:nowrap;overflow:hidden}
/* Nav */
.pgz-sb-nav{flex:1;padding:6px 8px;overflow-y:auto;overflow-x:hidden}
.pgz-sb-nav::-webkit-scrollbar{width:6px}
.pgz-sb-nav::-webkit-scrollbar-thumb{background:var(--rim2);border-radius:3px}
.pgz-nav-i{
padding:9px 12px;border-radius:6px;color:var(--t2);
cursor:pointer;display:flex;align-items:center;gap:10px;
font-size:12.5px;margin-bottom:2px;white-space:nowrap;
transition:background .15s,color .15s;position:relative;
}
.pgz-nav-i:hover{background:var(--bg2);color:var(--t1)}
.pgz-nav-i.active{
background:linear-gradient(90deg,var(--pgz-blue) 0%,var(--pgz-blue2) 100%);
color:#fff;font-weight:600;
}
.pgz-nav-i .ic{width:20px;text-align:center;font-size:14px;flex-shrink:0}
.pgz-nav-i .lbl{overflow:hidden;text-overflow:ellipsis;flex:1;min-width:0}
.pgz-nav-i .badge{margin-left:auto;background:var(--red);color:#fff;font-size:9px;font-weight:700;padding:1px 6px;border-radius:8px;flex-shrink:0}
.pgz-nav-ext{color:var(--cyan)}
.pgz-nav-ext::after{content:"↗";font-size:10px;opacity:.5;margin-left:auto;flex-shrink:0}
.pgz-nav-ext:hover{color:var(--pgz-gold);background:var(--bg2)}
.pgz-nav-ext.active{background:linear-gradient(90deg,var(--pgz-blue) 0%,var(--pgz-blue2) 100%);color:#fff}
.pgz-nav-ext.active::after{opacity:.85}
/* Footer (user) */
.pgz-sb-foot{padding:10px 12px;border-top:1px solid var(--rim);
display:flex;align-items:center;gap:8px;
white-space:nowrap;overflow:hidden;flex-shrink:0}
.pgz-sb-foot .av{
width:30px;height:30px;border-radius:50%;
background:linear-gradient(135deg,var(--pgz-blue),var(--pgz-gold));
color:#fff;font-weight:800;display:flex;align-items:center;justify-content:center;
font-size:11px;flex-shrink:0;overflow:hidden;
}
.pgz-sb-foot .av img{width:100%;height:100%;object-fit:cover}
.pgz-sb-foot .ui{flex:1;min-width:0;overflow:hidden}
.pgz-sb-foot .un{font-size:11.5px;color:var(--t1);font-weight:600;line-height:1.2;overflow:hidden;text-overflow:ellipsis}
.pgz-sb-foot .ur{font-size:9.5px;color:var(--t4);text-transform:uppercase;letter-spacing:.5px;line-height:1.2;overflow:hidden;text-overflow:ellipsis}
.pgz-sb-foot .lo{cursor:pointer;color:var(--t4);font-size:14px;
padding:6px 8px;border-radius:5px;transition:all .15s;flex-shrink:0}
.pgz-sb-foot .lo:hover{background:rgba(255,45,85,.15);color:var(--red)}
/* Mobile burger (shown <768px when sidebar is offscreen) */
.pgz-sb-burger{
position:fixed;top:10px;left:10px;z-index:99;
width:36px;height:36px;display:none;align-items:center;justify-content:center;
background:var(--bg2);border:1px solid var(--rim);border-radius:6px;
color:var(--t1);font-size:18px;cursor:pointer;
}
.pgz-sb-burger:hover{background:var(--bg3);color:var(--pgz-gold)}
/* Mobile X (shown <768px when sidebar is open) */
.pgz-sb-mx{display:none;cursor:pointer;color:var(--t2);font-size:18px;
width:24px;height:24px;align-items:center;justify-content:center;
border-radius:5px;transition:all .15s}
.pgz-sb-mx:hover{background:var(--bg3);color:var(--red)}
/* ─── Collapsed state ─── */
#pgz-sb.pgz-collapsed{width:var(--sb-w-col)}
#pgz-sb.pgz-collapsed .pgz-sb-h{padding:18px 6px 14px;text-align:center}
#pgz-sb.pgz-collapsed .pgz-sb-h .pgz-logo{font-size:0}
#pgz-sb.pgz-collapsed .pgz-sb-h .pgz-logo::before{content:"PG";font-size:13px;color:var(--pgz-gold);font-weight:800}
#pgz-sb.pgz-collapsed .pgz-sb-h .pgz-sub{display:none}
#pgz-sb.pgz-collapsed .pgz-sb-toggle{position:static;margin:6px auto 0;display:flex}
#pgz-sb.pgz-collapsed .pgz-sb-sep{font-size:0;padding:6px 0;text-align:center;border-top:1px dashed var(--rim);margin:6px 8px 4px}
#pgz-sb.pgz-collapsed .pgz-nav-i{justify-content:center;padding:10px 6px}
#pgz-sb.pgz-collapsed .pgz-nav-i .lbl,
#pgz-sb.pgz-collapsed .pgz-nav-i .badge,
#pgz-sb.pgz-collapsed .pgz-nav-ext::after{display:none}
#pgz-sb.pgz-collapsed .pgz-sb-foot{padding:10px 6px;justify-content:center}
#pgz-sb.pgz-collapsed .pgz-sb-foot .ui,
#pgz-sb.pgz-collapsed .pgz-sb-foot .lo{display:none}
/* Tooltip when collapsed */
#pgz-sb.pgz-collapsed .pgz-nav-i:hover::after{
content:attr(data-label);
position:absolute;left:calc(var(--sb-w-col) - 4px);top:50%;transform:translateY(-50%);
background:var(--bg3);color:var(--t0);
padding:5px 10px;border-radius:4px;
font-size:11.5px;white-space:nowrap;
border:1px solid var(--rim);font-weight:600;
box-shadow:2px 2px 10px rgba(0,0,0,.45);
pointer-events:none;z-index:200;
}
/* Layout helper — apply on body to push content right of sidebar */
body.pgz-has-sb{padding-left:var(--sb-w-exp);transition:padding-left .22s ease}
body.pgz-has-sb.pgz-sb-col{padding-left:var(--sb-w-col)}
/* Mobile: <768px */
@media (max-width:768px){
#pgz-sb{transform:translateX(-100%)}
#pgz-sb.pgz-mobile-open{transform:translateX(0)}
#pgz-sb.pgz-collapsed{width:var(--sb-w-exp)} /* full width on mobile when open */
body.pgz-has-sb,body.pgz-has-sb.pgz-sb-col{padding-left:0}
.pgz-sb-burger{display:flex}
.pgz-sb-mx{display:flex}
.pgz-sb-toggle{display:none}
/* overlay backdrop */
body.pgz-mobile-sb-open::before{
content:"";position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:99;backdrop-filter:blur(2px)
}
}