Login-Sicherheit: Account-Lockout, konfigurierbar, Admin-Verwaltung, .env für JWT-Secret

This commit is contained in:
2026-05-28 14:02:32 +02:00
parent 2da72f1246
commit 70d9eeab78
7 changed files with 276 additions and 27 deletions

View File

@@ -998,6 +998,88 @@ function QuickLinksSettings({ toast }) {
}
// ── Benutzerverwaltung ────────────────────────────────────────────────────────
// ── Login-Sicherheit Admin ────────────────────────────────────────────────────
function SecuritySettingsAdmin({ toast }) {
const [s, setS] = useState({ login_max_attempts:'5', login_lockout_minutes:'30' });
const [locked, setLocked] = useState([]);
const [busy, setBusy] = useState(false);
const load = () => {
api('/admin/security-settings').then(setS).catch(()=>{});
api('/admin/lockouts').then(setLocked).catch(()=>{});
};
useEffect(()=>{ load(); },[]);
const save = async () => {
setBusy(true);
try { await api('/admin/security-settings',{method:'PUT',body:s}); toast('Gespeichert ✓'); }
catch(e) { toast(e.message,'error'); }
setBusy(false);
};
const unlock = async uid => {
try { await api(`/admin/lockouts/${uid}`,{method:'DELETE'}); toast('Sperre aufgehoben'); setLocked(p=>p.filter(l=>l.id!==uid)); }
catch(e) { toast(e.message,'error'); }
};
const fmtDate = s => { if (!s) return ''; const d = new Date(s.replace(' ','T')); return isNaN(d)?'':d.toLocaleString('de-DE',{day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}); };
return (
<div>
<Sec title="LOGIN-SCHUTZ EINSTELLUNGEN">
<div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:10, marginBottom:12 }}>
<div>
<label style={{ ...S.head, display:'block', marginBottom:4 }}>MAX. FEHLVERSUCHE</label>
<input type="number" min="1" max="20" value={s.login_max_attempts}
onChange={e=>setS(p=>({...p,login_max_attempts:e.target.value}))}
style={{ ...S.inp, fontSize:15 }}/>
</div>
<div>
<label style={{ ...S.head, display:'block', marginBottom:4 }}>SPERRZEIT (MINUTEN)</label>
<input type="number" min="1" max="1440" value={s.login_lockout_minutes}
onChange={e=>setS(p=>({...p,login_lockout_minutes:e.target.value}))}
style={{ ...S.inp, fontSize:15 }}/>
</div>
</div>
<button onClick={save} disabled={busy}
style={{ ...S.btn('#4ecdc4'), width:'100%', textAlign:'center', padding:'10px 0', opacity:busy?0.5:1 }}>
{busy?'…':'✓ Speichern'}
</button>
</Sec>
<Sec title={`GESPERRTE KONTEN ${locked.length>0?`(${locked.length})`:''}`}>
{locked.length===0 ? (
<div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:11 }}>
Keine gesperrten Konten
</div>
) : locked.map(u => (
<div key={u.id} style={{ display:'flex', alignItems:'center', gap:10,
padding:'10px 0', borderBottom:'1px solid rgba(255,255,255,0.05)' }}>
<div style={{ flex:1, minWidth:0 }}>
<div style={{ color:'#ff6b9d', fontFamily:'monospace', fontSize:13, fontWeight:700 }}>
🔒 {u.username}
</div>
<div style={{ color:'rgba(255,255,255,0.35)', fontFamily:'monospace', fontSize:10, marginTop:3, lineHeight:1.7 }}>
{u.failed_attempts} Fehlversuch{u.failed_attempts!==1?'e':''} ·
Gesperrt bis: {fmtDate(u.locked_until)}
</div>
{u.last_failed_at && (
<div style={{ color:'rgba(255,255,255,0.2)', fontFamily:'monospace', fontSize:9 }}>
Letzter Versuch: {fmtDate(u.last_failed_at)}
</div>
)}
</div>
<button onClick={()=>unlock(u.id)}
style={{ ...S.btn('#4ecdc4', true), flexShrink:0, fontSize:11 }}>
🔓 Entsperren
</button>
</div>
))}
</Sec>
</div>
);
}
function UserManagement({ toast }) {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
@@ -1458,6 +1540,7 @@ function AdminPanel({ toast, mobile, user }) {
{section === 'benutzer' && user?.role==='admin' && (
<div>
<SecuritySettingsAdmin toast={toast}/>
<UserManagement toast={toast}/>
<Sec title="DATEI-MANAGER LIMITS">
<DateiSettings toast={toast}/>