Login-Sicherheit: Account-Lockout, konfigurierbar, Admin-Verwaltung, .env für JWT-Secret
This commit is contained in:
@@ -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}/>
|
||||
|
||||
Reference in New Issue
Block a user