Nachrichten: Senden-Button entfernt, Login-Sicherheit: IPs bei Lockout, Attempts-Bereinigung

This commit is contained in:
2026-05-28 14:12:49 +02:00
parent 70d9eeab78
commit 34fa756291
4 changed files with 31 additions and 10 deletions

View File

@@ -97,7 +97,7 @@ router.put('/users/:id/reset-password', authenticate, requireAdmin, (req, res) =
const getSetting = k => db.prepare('SELECT value FROM admin_settings WHERE key=?').get(k)?.value; const getSetting = k => db.prepare('SELECT value FROM admin_settings WHERE key=?').get(k)?.value;
// GET gesperrte Konten // GET gesperrte Konten mit IPs
router.get('/lockouts', authenticate, requireAdmin, (req, res) => { router.get('/lockouts', authenticate, requireAdmin, (req, res) => {
const locked = db.prepare(` const locked = db.prepare(`
SELECT id, username, failed_attempts, locked_until, last_failed_at SELECT id, username, failed_attempts, locked_until, last_failed_at
@@ -105,11 +105,25 @@ router.get('/lockouts', authenticate, requireAdmin, (req, res) => {
WHERE locked_until IS NOT NULL WHERE locked_until IS NOT NULL
ORDER BY locked_until DESC ORDER BY locked_until DESC
`).all(); `).all();
res.json(locked);
// IPs aus login_attempts hinzufügen
const result = locked.map(u => {
const ips = db.prepare(`
SELECT DISTINCT ip FROM login_attempts
WHERE username=? AND success=0
ORDER BY created_at DESC LIMIT 10
`).all(u.username).map(r => r.ip).filter(Boolean);
return { ...u, ips };
});
res.json(result);
}); });
// DELETE Sperre aufheben // DELETE Sperre aufheben + Login-Versuche löschen
router.delete('/lockouts/:userId', authenticate, requireAdmin, (req, res) => { router.delete('/lockouts/:userId', authenticate, requireAdmin, (req, res) => {
const user = db.prepare('SELECT username FROM users WHERE id=?').get(req.params.userId);
if (user) {
db.prepare('DELETE FROM login_attempts WHERE username=?').run(user.username);
}
db.prepare('UPDATE users SET failed_attempts=0, locked_until=NULL, last_failed_at=NULL WHERE id=?') db.prepare('UPDATE users SET failed_attempts=0, locked_until=NULL, last_failed_at=NULL WHERE id=?')
.run(req.params.userId); .run(req.params.userId);
res.json({ ok: true }); res.json({ ok: true });

View File

@@ -72,8 +72,9 @@ router.post('/login', (req, res) => {
lockedUntil: until.toISOString(), lockedUntil: until.toISOString(),
}); });
} }
// Abgelaufen // Abgelaufen → zurücksetzen + Attempts löschen
db.prepare('UPDATE users SET failed_attempts=0, locked_until=NULL, last_failed_at=NULL WHERE id=?').run(user.id); db.prepare('UPDATE users SET failed_attempts=0, locked_until=NULL, last_failed_at=NULL WHERE id=?').run(user.id);
db.prepare('DELETE FROM login_attempts WHERE username=?').run(username);
} }
// Passwort prüfen // Passwort prüfen
@@ -96,8 +97,9 @@ router.post('/login', (req, res) => {
}); });
} }
// Erfolgreich → Zähler zurücksetzen // Erfolgreich → Zähler zurücksetzen + Attempts löschen
db.prepare('UPDATE users SET failed_attempts=0, locked_until=NULL, last_failed_at=NULL WHERE id=?').run(user.id); db.prepare('UPDATE users SET failed_attempts=0, locked_until=NULL, last_failed_at=NULL WHERE id=?').run(user.id);
db.prepare('DELETE FROM login_attempts WHERE username=?').run(username);
const token = jwt.sign( const token = jwt.sign(
{ id: user.id, username: user.username, role: user.role }, { id: user.id, username: user.username, role: user.role },

View File

@@ -1068,6 +1068,15 @@ function SecuritySettingsAdmin({ toast }) {
Letzter Versuch: {fmtDate(u.last_failed_at)} Letzter Versuch: {fmtDate(u.last_failed_at)}
</div> </div>
)} )}
{u.ips?.length > 0 && (
<div style={{ marginTop:4, display:'flex', flexWrap:'wrap', gap:4 }}>
{u.ips.map((ip,i) => (
<span key={i} style={{ background:'rgba(255,107,157,0.1)', border:'1px solid rgba(255,107,157,0.2)',
borderRadius:5, padding:'1px 7px', color:'rgba(255,107,157,0.7)',
fontFamily:'monospace', fontSize:9 }}>{ip}</span>
))}
</div>
)}
</div> </div>
<button onClick={()=>unlock(u.id)} <button onClick={()=>unlock(u.id)}
style={{ ...S.btn('#4ecdc4', true), flexShrink:0, fontSize:11 }}> style={{ ...S.btn('#4ecdc4', true), flexShrink:0, fontSize:11 }}>

View File

@@ -479,11 +479,7 @@ export default function Nachrichten({ toast, mobile }) {
background: showPicker ? 'rgba(78,205,196,0.12)' : 'rgba(255,255,255,0.05)', background: showPicker ? 'rgba(78,205,196,0.12)' : 'rgba(255,255,255,0.05)',
border:`1px solid ${showPicker ? 'rgba(78,205,196,0.3)' : 'rgba(255,255,255,0.1)'}`, border:`1px solid ${showPicker ? 'rgba(78,205,196,0.3)' : 'rgba(255,255,255,0.1)'}`,
borderRadius:8, cursor:'pointer', fontSize:17, lineHeight:1, borderRadius:8, cursor:'pointer', fontSize:17, lineHeight:1,
padding:'5px 8px', flexShrink:0 }}>😊</button> padding:'5px 8px', flexShrink:0, alignSelf:'flex-end' }}>😊</button>
<button onClick={send} disabled={sending || !text.trim()} style={{
...S.btn('#4ecdc4'), flex:1, padding:'0 14px',
opacity: sending || !text.trim() ? 0.3 : 1, fontSize:17, lineHeight:1,
}}></button>
</div> </div>
</div> </div>
)} )}