feat: improve login UX and add admin login lock reset

- Translate all login error messages to Chinese (network errors, HTTP
  status codes, URL validation errors)
- Add password visibility toggle (eye icon) to login form
- Add live countdown timer when login is locked, disable login button
  during cooldown, auto-clear error when countdown ends
- Add X button red hover feedback in server settings dialog
- Add LoginGuard.Unlock() method to clear Redis lock/failure keys
- Expose POST /admin-users/:id/reset-login-lock endpoint in ops API
- Add "重置登录保护" button in ops-web user management table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Xu Liang
2026-05-02 19:25:15 +08:00
parent 40c877ee2d
commit 6dd3bd8e9d
9 changed files with 212 additions and 23 deletions
+26 -7
View File
@@ -74,8 +74,8 @@ func DefaultLoginGuardConfig(scope string) LoginGuardConfig {
RequestPairWindow: time.Minute,
FailureIdentifierLimit: 8,
FailurePairLimit: 5,
FailureWindow: 15 * time.Minute,
FailureLockBaseTTL: 5 * time.Minute,
FailureWindow: 5 * time.Minute,
FailureLockBaseTTL: 2 * time.Minute,
FailureLockMaxTTL: time.Hour,
ConcurrentAttemptLockTTL: 15 * time.Second,
}
@@ -83,11 +83,11 @@ func DefaultLoginGuardConfig(scope string) LoginGuardConfig {
if strings.EqualFold(strings.TrimSpace(scope), "ops") {
cfg.RequestIPLimit = 30
cfg.RequestIdentifierLimit = 8
cfg.RequestPairLimit = 5
cfg.FailureIdentifierLimit = 5
cfg.FailurePairLimit = 3
cfg.FailureLockBaseTTL = 10 * time.Minute
cfg.FailureLockMaxTTL = 2 * time.Hour
cfg.RequestPairLimit = 6
cfg.FailureIdentifierLimit = 10
cfg.FailurePairLimit = 5
cfg.FailureLockBaseTTL = 2 * time.Minute
cfg.FailureLockMaxTTL = 30 * time.Minute
}
return cfg
@@ -256,6 +256,18 @@ func (p *LoginPermit) Release(ctx context.Context) {
_, _ = loginGuardReleaseScript.Run(ctx, p.guard.rdb, []string{p.keys.inflightIdentifier}, p.token).Result()
}
func (g *LoginGuard) Unlock(ctx context.Context, identifier string) error {
if g == nil || !g.cfg.Enabled {
return nil
}
keys := g.keysFor(LoginGuardSubject{Identifier: identifier})
if g.rdb == nil {
g.fallback.unlock(keys)
return nil
}
return g.rdb.Del(ctx, keys.lockIdentifier, keys.failureIdentifier).Err()
}
type loginGuardKeys struct {
requestIP string
requestIdentifier string
@@ -582,6 +594,13 @@ func (g *memoryLoginGuard) bumpFailureLocked(failureKey, lockKey string, thresho
g.locks[lockKey] = now.Add(lockTTL)
}
func (g *memoryLoginGuard) unlock(keys loginGuardKeys) {
g.mu.Lock()
defer g.mu.Unlock()
delete(g.locks, keys.lockIdentifier)
delete(g.failures, keys.failureIdentifier)
}
func (g *memoryLoginGuard) releaseLocked(key, token string) {
current, ok := g.inflight[key]
if !ok || current.token != token {