feat: add IP-based login brute-force protection (#531)
* feat: add IP-based login brute-force protection - Per-IP rate limiting: 3 failed login attempts locks the IP for 1 hour - Separate counters for password login and token auth - Global safety net: 20 req/min, hard lock after 50 total failures - Persistent lock state to ~/.hermes-web-ui/.login-lock.json (survives restarts) - Manual unlock: edit or delete the lock file - Frontend handles 429/503 responses with localized error messages - i18n support for 8 languages * feat: add locked IP management endpoint and UI - GET /api/auth/locked-ips: list all currently locked IPs (protected) - DELETE /api/auth/locked-ips/:ip: unlock a specific IP (protected) - DELETE /api/auth/locked-ips: unlock all IPs (protected) - AccountSettings: shows locked IPs with remaining time, unlock buttons - i18n support for 8 languages - Clean up stale .js artifacts, add .gitignore rule * fix: cross-type IP lock and IPv6-compatible unlock route - Password and token login now share IP lock state: if an IP is locked by either method, ALL auth methods are blocked for that IP - Changed unlock endpoint from path param to query param (?ip=xxx) to support IPv6 addresses containing colons - Merged unlockIp and unlockAll into a single handler * chore: increase global login rate limit from 20 to 100 requests per minute Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: ekko <fqsy1416@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ export default {
|
||||
passwordPlaceholder: 'Passwort',
|
||||
credentialsRequired: 'Bitte Benutzername und Passwort eingeben',
|
||||
invalidCredentials: 'Ungultiger Benutzername oder Passwort',
|
||||
tooManyAttempts: 'Zu viele fehlgeschlagene Versuche, bitte versuchen Sie es spater erneut',
|
||||
passwordMismatch: 'Passworter stimmen nicht uberein',
|
||||
passwordTooShort: 'Passwort muss mindestens 6 Zeichen lang sein',
|
||||
setupSuccess: 'Passwort-Login erfolgreich konfiguriert',
|
||||
@@ -519,6 +520,16 @@ jobTriggered: 'Job ausgelost',
|
||||
cors: 'CORS-Ursprunge',
|
||||
corsHint: 'Erlaubte Cross-Origin-Quellen',
|
||||
},
|
||||
lockedIps: {
|
||||
title: 'Gesperrte IPs',
|
||||
count: '{count} gesperrt',
|
||||
empty: 'Keine gesperrten IPs',
|
||||
unlock: 'Entsperren',
|
||||
unlockAll: 'Alle entsperren',
|
||||
unlockAllConfirm: 'Alle gesperrten IPs entsperren?',
|
||||
unlocked: 'IP entsperrt',
|
||||
allUnlocked: '{count} IPs entsperrt',
|
||||
},
|
||||
},
|
||||
|
||||
// Platform channel settings
|
||||
|
||||
@@ -14,6 +14,7 @@ export default {
|
||||
passwordPlaceholder: 'Password',
|
||||
credentialsRequired: 'Please enter username and password',
|
||||
invalidCredentials: 'Invalid username or password',
|
||||
tooManyAttempts: 'Too many failed attempts, please try again later',
|
||||
passwordMismatch: 'Passwords do not match',
|
||||
passwordTooShort: 'Password must be at least 6 characters',
|
||||
setupSuccess: 'Password login configured successfully',
|
||||
@@ -53,6 +54,7 @@ export default {
|
||||
copied: 'Copied',
|
||||
copy: 'Copy',
|
||||
noData: 'No data',
|
||||
expired: 'Expired',
|
||||
fetch: 'Fetch',
|
||||
add: 'Add',
|
||||
enable: 'Enable',
|
||||
@@ -655,6 +657,16 @@ export default {
|
||||
cors: 'CORS Origins',
|
||||
corsHint: 'Allowed cross-origin sources',
|
||||
},
|
||||
lockedIps: {
|
||||
title: 'Locked IPs',
|
||||
count: '{count} locked',
|
||||
empty: 'No locked IPs',
|
||||
unlock: 'Unlock',
|
||||
unlockAll: 'Unlock All',
|
||||
unlockAllConfirm: 'Unlock all locked IPs?',
|
||||
unlocked: 'IP unlocked',
|
||||
allUnlocked: '{count} IPs unlocked',
|
||||
},
|
||||
},
|
||||
|
||||
// Platform channel settings
|
||||
|
||||
@@ -14,6 +14,7 @@ export default {
|
||||
passwordPlaceholder: 'Contrasena',
|
||||
credentialsRequired: 'Por favor, introduzca nombre de usuario y contrasena',
|
||||
invalidCredentials: 'Nombre de usuario o contrasena incorrectos',
|
||||
tooManyAttempts: 'Demasiados intentos fallidos, por favor intente mas tarde',
|
||||
passwordMismatch: 'Las contrasenas no coinciden',
|
||||
passwordTooShort: 'La contrasena debe tener al menos 6 caracteres',
|
||||
setupSuccess: 'Login con contrasena configurado correctamente',
|
||||
@@ -519,6 +520,16 @@ jobTriggered: 'Job ejecutado',
|
||||
cors: 'Origenes CORS',
|
||||
corsHint: 'Fuentes cross-origin permitidas',
|
||||
},
|
||||
lockedIps: {
|
||||
title: 'IPs bloqueadas',
|
||||
count: '{count} bloqueadas',
|
||||
empty: 'Sin IPs bloqueadas',
|
||||
unlock: 'Desbloquear',
|
||||
unlockAll: 'Desbloquear todo',
|
||||
unlockAllConfirm: 'Desbloquear todas las IPs?',
|
||||
unlocked: 'IP desbloqueada',
|
||||
allUnlocked: '{count} IPs desbloqueadas',
|
||||
},
|
||||
},
|
||||
|
||||
// Platform channel settings
|
||||
|
||||
@@ -14,6 +14,7 @@ export default {
|
||||
passwordPlaceholder: 'Mot de passe',
|
||||
credentialsRequired: 'Veuillez entrer le nom d\'utilisateur et le mot de passe',
|
||||
invalidCredentials: 'Nom d\'utilisateur ou mot de passe incorrect',
|
||||
tooManyAttempts: 'Trop de tentatives echouees, veuillez reessayer plus tard',
|
||||
passwordMismatch: 'Les mots de passe ne correspondent pas',
|
||||
passwordTooShort: 'Le mot de passe doit contenir au moins 6 caracteres',
|
||||
setupSuccess: 'Login par mot de passe configure avec succes',
|
||||
@@ -519,6 +520,16 @@ jobTriggered: 'Job declenche',
|
||||
cors: 'Origines CORS',
|
||||
corsHint: 'Sources cross-origin autorisees',
|
||||
},
|
||||
lockedIps: {
|
||||
title: 'IPs bloquees',
|
||||
count: '{count} bloquees',
|
||||
empty: 'Aucune IP bloquee',
|
||||
unlock: 'Debloquer',
|
||||
unlockAll: 'Tout debloquer',
|
||||
unlockAllConfirm: 'Debloquer toutes les IPs?',
|
||||
unlocked: 'IP debloquee',
|
||||
allUnlocked: '{count} IPs debloquees',
|
||||
},
|
||||
},
|
||||
|
||||
// Platform channel settings
|
||||
|
||||
@@ -14,6 +14,7 @@ export default {
|
||||
passwordPlaceholder: 'パスワード',
|
||||
credentialsRequired: 'ユーザー名とパスワードを入力してください',
|
||||
invalidCredentials: 'ユーザー名またはパスワードが正しくありません',
|
||||
tooManyAttempts: 'ログイン試行回数が多すぎます。しばらくしてからお試しください',
|
||||
passwordMismatch: 'パスワードが一致しません',
|
||||
passwordTooShort: 'パスワードは6文字以上必要です',
|
||||
setupSuccess: 'パスワードログインが設定されました',
|
||||
@@ -519,6 +520,16 @@ export default {
|
||||
cors: 'CORS 許可元',
|
||||
corsHint: '許可するクロスオリジン',
|
||||
},
|
||||
lockedIps: {
|
||||
title: 'ロック済みIP管理',
|
||||
count: '{count}件ロック中',
|
||||
empty: 'ロック済みIPなし',
|
||||
unlock: 'ロック解除',
|
||||
unlockAll: '全て解除',
|
||||
unlockAllConfirm: '全てのロック済みIPを解除しますか?',
|
||||
unlocked: 'IPをロック解除しました',
|
||||
allUnlocked: '{count}件のIPをロック解除しました',
|
||||
},
|
||||
},
|
||||
|
||||
// プラットフォームチャンネル設定
|
||||
|
||||
@@ -14,6 +14,7 @@ export default {
|
||||
passwordPlaceholder: '비밀번호',
|
||||
credentialsRequired: '사용자 이름과 비밀번호를 입력해 주세요',
|
||||
invalidCredentials: '사용자 이름 또는 비밀번호가 올바르지 않습니다',
|
||||
tooManyAttempts: '로그인 시도 횟수가 너무 많습니다. 잠시 후 다시 시도해 주세요',
|
||||
passwordMismatch: '비밀번호가 일치하지 않습니다',
|
||||
passwordTooShort: '비밀번호는 6자 이상이어야 합니다',
|
||||
setupSuccess: '비밀번호 로그인이 설정되었습니다',
|
||||
@@ -519,6 +520,16 @@ export default {
|
||||
cors: 'CORS 출처',
|
||||
corsHint: '허용된 교차 출처',
|
||||
},
|
||||
lockedIps: {
|
||||
title: '잠긴 IP 관리',
|
||||
count: '{count}개 잠김',
|
||||
empty: '잠긴 IP 없음',
|
||||
unlock: '잠금 해제',
|
||||
unlockAll: '전체 해제',
|
||||
unlockAllConfirm: '모든 잠긴 IP를 해제하시겠습니까?',
|
||||
unlocked: 'IP 잠금 해제됨',
|
||||
allUnlocked: '{count}개 IP 잠금 해제됨',
|
||||
},
|
||||
},
|
||||
|
||||
// 플랫폼 채널 설정
|
||||
|
||||
@@ -14,6 +14,7 @@ export default {
|
||||
passwordPlaceholder: 'Senha',
|
||||
credentialsRequired: 'Por favor, insira nome de usuario e senha',
|
||||
invalidCredentials: 'Nome de usuario ou senha incorretos',
|
||||
tooManyAttempts: 'Muitas tentativas falhadas, por favor tente novamente mais tarde',
|
||||
passwordMismatch: 'As senhas nao conferem',
|
||||
passwordTooShort: 'A senha deve ter pelo menos 6 caracteres',
|
||||
setupSuccess: 'Login por senha configurado com sucesso',
|
||||
@@ -519,6 +520,16 @@ jobTriggered: 'Job acionado',
|
||||
cors: 'Origens CORS',
|
||||
corsHint: 'Fontes cross-origin permitidas',
|
||||
},
|
||||
lockedIps: {
|
||||
title: 'IPs bloqueadas',
|
||||
count: '{count} bloqueadas',
|
||||
empty: 'Nenhuma IP bloqueada',
|
||||
unlock: 'Desbloquear',
|
||||
unlockAll: 'Desbloquear tudo',
|
||||
unlockAllConfirm: 'Desbloquear todas as IPs?',
|
||||
unlocked: 'IP desbloqueada',
|
||||
allUnlocked: '{count} IPs desbloqueadas',
|
||||
},
|
||||
},
|
||||
|
||||
// Platform channel settings
|
||||
|
||||
@@ -14,6 +14,7 @@ export default {
|
||||
passwordPlaceholder: '密码',
|
||||
credentialsRequired: '请输入用户名和密码',
|
||||
invalidCredentials: '用户名或密码错误',
|
||||
tooManyAttempts: '登录失败次数过多,请稍后重试',
|
||||
passwordMismatch: '两次密码不一致',
|
||||
passwordTooShort: '密码长度至少 6 个字符',
|
||||
setupSuccess: '密码登录配置成功',
|
||||
@@ -53,6 +54,7 @@ export default {
|
||||
update: '更新',
|
||||
create: '创建',
|
||||
noData: '暂无数据',
|
||||
expired: '已过期',
|
||||
fetch: '获取',
|
||||
add: '添加',
|
||||
enable: '启用',
|
||||
@@ -647,6 +649,16 @@ export default {
|
||||
cors: 'CORS 来源',
|
||||
corsHint: '允许的跨域来源',
|
||||
},
|
||||
lockedIps: {
|
||||
title: '锁定 IP 管理',
|
||||
count: '{count} 个 IP 被锁定',
|
||||
empty: '暂无锁定 IP',
|
||||
unlock: '解锁',
|
||||
unlockAll: '全部解锁',
|
||||
unlockAllConfirm: '确认解锁所有锁定的 IP?',
|
||||
unlocked: 'IP 已解锁',
|
||||
allUnlocked: '已解锁 {count} 个 IP',
|
||||
},
|
||||
},
|
||||
|
||||
// 平台频道设置
|
||||
|
||||
Reference in New Issue
Block a user