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
+28 -1
View File
@@ -28,7 +28,8 @@ const (
ActionAdminUserSetKOL = "admin_user.set_kol"
ActionAdminUserDisable = "admin_user.disable"
ActionAdminUserEnable = "admin_user.enable"
ActionAdminUserResetPassword = "admin_user.reset_password"
ActionAdminUserResetPassword = "admin_user.reset_password"
ActionAdminUserResetLoginLock = "admin_user.reset_login_lock"
)
var phonePattern = regexp.MustCompile(`^\+?[0-9]{6,20}$`)
@@ -36,6 +37,7 @@ var phonePattern = regexp.MustCompile(`^\+?[0-9]{6,20}$`)
type AdminUserService struct {
users *repository.AdminUserRepository
audits *AuditService
loginGuard *sharedauth.LoginGuard
configMu sync.RWMutex
defaultPlanCode string
}
@@ -52,6 +54,11 @@ func NewAdminUserService(users *repository.AdminUserRepository, audits *AuditSer
}
}
func (s *AdminUserService) WithLoginGuard(g *sharedauth.LoginGuard) *AdminUserService {
s.loginGuard = g
return s
}
func (s *AdminUserService) UpdateDefaultPlanCode(defaultPlanCode string) {
if s == nil {
return
@@ -596,6 +603,26 @@ func (s *AdminUserService) ResetPassword(ctx context.Context, actor *Actor, id i
return nil
}
func (s *AdminUserService) ResetLoginLock(ctx context.Context, actor *Actor, id int64) error {
user, err := s.users.GetByID(ctx, id)
if err != nil {
if errors.Is(err, repository.ErrAdminUserNotFound) {
return response.ErrNotFound(40430, "user_not_found", "用户不存在")
}
return response.ErrInternal(50034, "query_failed", "failed to get user")
}
if s.loginGuard != nil {
_ = s.loginGuard.Unlock(ctx, user.Phone)
if user.Email != nil && *user.Email != "" {
_ = s.loginGuard.Unlock(ctx, *user.Email)
}
}
if actor != nil {
_ = s.audits.Append(ctx, actor.audit(ActionAdminUserResetLoginLock, "admin_user", id, nil))
}
return nil
}
func (s *AdminUserService) mapWriteError(err error) error {
if errors.Is(err, repository.ErrDefaultPlanNotFound) {
return response.ErrInternal(50034, "default_plan_missing", "默认套餐未配置")
@@ -284,3 +284,18 @@ func resetAdminUserPasswordHandler(svc *app.AdminUserService) gin.HandlerFunc {
response.Success(c, gin.H{"id": id})
}
}
func resetAdminUserLoginLockHandler(svc *app.AdminUserService) gin.HandlerFunc {
return func(c *gin.Context) {
id, err := parseIDParam(c)
if err != nil {
response.Error(c, err)
return
}
if err := svc.ResetLoginLock(c.Request.Context(), actorFromGin(c), id); err != nil {
response.Error(c, err)
return
}
response.Success(c, gin.H{"id": id})
}
}
+1
View File
@@ -81,6 +81,7 @@ func RegisterRoutes(d Deps) {
authed.POST("/admin-users/:id/kol", setAdminUserKOLHandler(d.AdminUsers))
authed.POST("/admin-users/:id/status", setAdminUserStatusHandler(d.AdminUsers))
authed.POST("/admin-users/:id/password", resetAdminUserPasswordHandler(d.AdminUsers))
authed.POST("/admin-users/:id/reset-login-lock", resetAdminUserLoginLockHandler(d.AdminUsers))
authed.GET("/kol/packages", listKolSubscriptionPackagesHandler(d.KolSubs))
authed.GET("/kol/subscriptions", listKolSubscriptionsHandler(d.KolSubs))