i18n(app): 本地化认证与邮箱申请提示

- 将登录、注册、双因素认证和邮箱申请相关错误提示改为简体中文。
- 统一优化校验失败、权限不足、资源冲突等场景的反馈文案。
This commit is contained in:
LanQin_
2026-06-16 10:40:37 +08:00
parent 556389b734
commit 061311d524
3 changed files with 35 additions and 35 deletions
+23 -23
View File
@@ -24,52 +24,52 @@ func (a *App) handleLogin(w http.ResponseWriter, r *http.Request) {
if strings.TrimSpace(req.ChallengeToken) != "" { if strings.TrimSpace(req.ChallengeToken) != "" {
challenge, err := a.loginChallengeByToken(r.Context(), req.ChallengeToken) challenge, err := a.loginChallengeByToken(r.Context(), req.ChallengeToken)
if err != nil { if err != nil {
respondError(w, http.StatusUnauthorized, "invalid verification challenge") respondError(w, http.StatusUnauthorized, "验证已过期,请重新登录")
return return
} }
user, secret, err := a.loadUserAuthByID(r.Context(), challenge.UserID) user, secret, err := a.loadUserAuthByID(r.Context(), challenge.UserID)
if err != nil || user.Disabled || !user.TwoFactorEnabled || strings.TrimSpace(secret) == "" { if err != nil || user.Disabled || !user.TwoFactorEnabled || strings.TrimSpace(secret) == "" {
a.deleteLoginChallenge(r.Context(), challenge.ID) a.deleteLoginChallenge(r.Context(), challenge.ID)
respondError(w, http.StatusUnauthorized, "invalid verification challenge") respondError(w, http.StatusUnauthorized, "验证已过期,请重新登录")
return return
} }
if !verifyTOTP(secret, req.TwoFactorCode, a.now().UTC()) { if !verifyTOTP(secret, req.TwoFactorCode, a.now().UTC()) {
respondError(w, http.StatusUnauthorized, "invalid verification code") respondError(w, http.StatusUnauthorized, "验证码错误")
return return
} }
a.deleteLoginChallenge(r.Context(), challenge.ID) a.deleteLoginChallenge(r.Context(), challenge.ID)
if err := a.issueSession(w, r, user.ID); err != nil { if err := a.issueSession(w, r, user.ID); err != nil {
respondError(w, http.StatusInternalServerError, "failed to create session") respondError(w, http.StatusInternalServerError, "登录失败,请稍后重试")
return return
} }
respondJSON(w, http.StatusOK, map[string]any{"user": user}) respondJSON(w, http.StatusOK, map[string]any{"user": user})
return return
} }
if err := a.verifyTurnstile(r.Context(), req.TurnstileToken, r.RemoteAddr); err != nil { if err := a.verifyTurnstile(r.Context(), req.TurnstileToken, r.RemoteAddr); err != nil {
respondError(w, http.StatusUnauthorized, "human verification failed") respondError(w, http.StatusUnauthorized, "人机验证失败,请重试")
return return
} }
email := normalizeEmail(req.Email) email := normalizeEmail(req.Email)
user, passwordHash, err := a.userByEmail(r.Context(), email) user, passwordHash, err := a.userByEmail(r.Context(), email)
if err != nil || user.Disabled { if err != nil || user.Disabled {
respondError(w, http.StatusUnauthorized, "invalid email or password") respondError(w, http.StatusUnauthorized, "邮箱或密码错误")
return return
} }
if err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(req.Password)); err != nil { if err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(req.Password)); err != nil {
respondError(w, http.StatusUnauthorized, "invalid email or password") respondError(w, http.StatusUnauthorized, "邮箱或密码错误")
return return
} }
if a.cfg.TwoFactorEnabled && user.TwoFactorEnabled { if a.cfg.TwoFactorEnabled && user.TwoFactorEnabled {
challengeToken, err := a.createLoginChallenge(r.Context(), user.ID) challengeToken, err := a.createLoginChallenge(r.Context(), user.ID)
if err != nil { if err != nil {
respondError(w, http.StatusInternalServerError, "failed to create verification challenge") respondError(w, http.StatusInternalServerError, "验证码生成失败,请稍后重试")
return return
} }
respondJSON(w, http.StatusOK, map[string]any{"twoFactorRequired": true, "challengeToken": challengeToken}) respondJSON(w, http.StatusOK, map[string]any{"twoFactorRequired": true, "challengeToken": challengeToken})
return return
} }
if err := a.issueSession(w, r, user.ID); err != nil { if err := a.issueSession(w, r, user.ID); err != nil {
respondError(w, http.StatusInternalServerError, "failed to create session") respondError(w, http.StatusInternalServerError, "登录失败,请稍后重试")
return return
} }
respondJSON(w, http.StatusOK, map[string]any{"user": user}) respondJSON(w, http.StatusOK, map[string]any{"user": user})
@@ -77,7 +77,7 @@ func (a *App) handleLogin(w http.ResponseWriter, r *http.Request) {
func (a *App) handleRegister(w http.ResponseWriter, r *http.Request) { func (a *App) handleRegister(w http.ResponseWriter, r *http.Request) {
if !a.cfg.OpenRegistration { if !a.cfg.OpenRegistration {
respondError(w, http.StatusForbidden, "registration is closed") respondError(w, http.StatusForbidden, "当前未开放注册")
return return
} }
var req struct { var req struct {
@@ -93,16 +93,16 @@ func (a *App) handleRegister(w http.ResponseWriter, r *http.Request) {
return return
} }
if err := a.verifyTurnstile(r.Context(), req.TurnstileToken, r.RemoteAddr); err != nil { if err := a.verifyTurnstile(r.Context(), req.TurnstileToken, r.RemoteAddr); err != nil {
respondError(w, http.StatusUnauthorized, "human verification failed") respondError(w, http.StatusUnauthorized, "人机验证失败,请重试")
return return
} }
email := normalizeEmail(req.Email) email := normalizeEmail(req.Email)
if email == "" || !strings.Contains(email, "@") { if email == "" || !strings.Contains(email, "@") {
badRequest(w, errors.New("invalid email")) badRequest(w, errors.New("邮箱地址无效"))
return return
} }
if len(req.Password) < 8 { if len(req.Password) < 8 {
badRequest(w, errors.New("password must be at least 8 characters")) badRequest(w, errors.New("密码至少需要 8 个字符"))
return return
} }
displayName := strings.TrimSpace(req.DisplayName) displayName := strings.TrimSpace(req.DisplayName)
@@ -110,11 +110,11 @@ func (a *App) handleRegister(w http.ResponseWriter, r *http.Request) {
displayName = strings.Split(email, "@")[0] displayName = strings.Split(email, "@")[0]
} }
if len([]rune(displayName)) > 80 { if len([]rune(displayName)) > 80 {
badRequest(w, errors.New("displayName must be at most 80 characters")) badRequest(w, errors.New("显示名称不能超过 80 个字符"))
return return
} }
if _, _, err := a.userByEmail(r.Context(), email); err == nil { if _, _, err := a.userByEmail(r.Context(), email); err == nil {
respondError(w, http.StatusConflict, "email already registered") respondError(w, http.StatusConflict, "该邮箱已被注册")
return return
} else if !errors.Is(err, errNotFound) { } else if !errors.Is(err, errNotFound) {
respondError(w, http.StatusInternalServerError, "failed to check user") respondError(w, http.StatusInternalServerError, "failed to check user")
@@ -130,10 +130,10 @@ func (a *App) handleRegister(w http.ResponseWriter, r *http.Request) {
if _, err := a.db.ExecContext(r.Context(), `INSERT INTO users(id,email,display_name,role,password_hash,disabled,created_at,updated_at) if _, err := a.db.ExecContext(r.Context(), `INSERT INTO users(id,email,display_name,role,password_hash,disabled,created_at,updated_at)
VALUES(?,?,?,?,?,?,?,?)`, userID, email, displayName, "user", string(passwordHash), 0, now, now); err != nil { VALUES(?,?,?,?,?,?,?,?)`, userID, email, displayName, "user", string(passwordHash), 0, now, now); err != nil {
if strings.Contains(strings.ToLower(err.Error()), "unique") { if strings.Contains(strings.ToLower(err.Error()), "unique") {
respondError(w, http.StatusConflict, "email already registered") respondError(w, http.StatusConflict, "该邮箱已被注册")
return return
} }
respondError(w, http.StatusInternalServerError, "failed to create user") respondError(w, http.StatusInternalServerError, "注册失败,请稍后重试")
return return
} }
user, err := a.userByID(r.Context(), userID) user, err := a.userByID(r.Context(), userID)
@@ -142,7 +142,7 @@ func (a *App) handleRegister(w http.ResponseWriter, r *http.Request) {
return return
} }
if err := a.issueSession(w, r, user.ID); err != nil { if err := a.issueSession(w, r, user.ID); err != nil {
respondError(w, http.StatusInternalServerError, "failed to create session") respondError(w, http.StatusInternalServerError, "登录失败,请稍后重试")
return return
} }
@@ -169,7 +169,7 @@ func (a *App) handleRegister(w http.ResponseWriter, r *http.Request) {
reserved[item] = true reserved[item] = true
} }
if reserved[mailboxLocalPart] { if reserved[mailboxLocalPart] {
respondError(w, http.StatusForbidden, "localPart is reserved") respondError(w, http.StatusForbidden, "该前缀已被保留,请使用其他前缀")
return return
} }
if _, mbErr := a.createMailboxWithPasswordHash(r.Context(), user.ID, mailboxDomainID, mailboxLocalPart, displayName, string(passwordHash), 1024, "active"); mbErr != nil { if _, mbErr := a.createMailboxWithPasswordHash(r.Context(), user.ID, mailboxDomainID, mailboxLocalPart, displayName, string(passwordHash), 1024, "active"); mbErr != nil {
@@ -203,11 +203,11 @@ func (a *App) handleUpdateProfile(w http.ResponseWriter, r *http.Request) {
} }
displayName := strings.TrimSpace(req.DisplayName) displayName := strings.TrimSpace(req.DisplayName)
if displayName == "" { if displayName == "" {
badRequest(w, errors.New("displayName is required")) badRequest(w, errors.New("请输入显示名称"))
return return
} }
if len([]rune(displayName)) > 80 { if len([]rune(displayName)) > 80 {
badRequest(w, errors.New("displayName must be at most 80 characters")) badRequest(w, errors.New("显示名称不能超过 80 个字符"))
return return
} }
_, err := a.db.ExecContext(r.Context(), `UPDATE users SET display_name=?, updated_at=? WHERE id=?`, _, err := a.db.ExecContext(r.Context(), `UPDATE users SET display_name=?, updated_at=? WHERE id=?`,
@@ -235,7 +235,7 @@ func (a *App) handleChangePassword(w http.ResponseWriter, r *http.Request) {
return return
} }
if len(req.NewPassword) < 8 { if len(req.NewPassword) < 8 {
badRequest(w, errors.New("newPassword must be at least 8 characters")) badRequest(w, errors.New("新密码至少需要 8 个字符"))
return return
} }
row := a.db.QueryRowContext(r.Context(), `SELECT password_hash FROM users WHERE id=?`, user.ID) row := a.db.QueryRowContext(r.Context(), `SELECT password_hash FROM users WHERE id=?`, user.ID)
@@ -245,7 +245,7 @@ func (a *App) handleChangePassword(w http.ResponseWriter, r *http.Request) {
return return
} }
if err := bcrypt.CompareHashAndPassword([]byte(currentHash), []byte(req.CurrentPassword)); err != nil { if err := bcrypt.CompareHashAndPassword([]byte(currentHash), []byte(req.CurrentPassword)); err != nil {
respondError(w, http.StatusUnauthorized, "current password is incorrect") respondError(w, http.StatusUnauthorized, "当前密码错误")
return return
} }
newHash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost) newHash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
+7 -7
View File
@@ -33,7 +33,7 @@ func (a *App) handleMailboxApplyOptions(w http.ResponseWriter, r *http.Request)
func (a *App) handleApplyMailbox(w http.ResponseWriter, r *http.Request) { func (a *App) handleApplyMailbox(w http.ResponseWriter, r *http.Request) {
if !a.cfg.UserMailboxApplyEnabled { if !a.cfg.UserMailboxApplyEnabled {
respondError(w, http.StatusForbidden, "mailbox application is disabled") respondError(w, http.StatusForbidden, "当前未开放邮箱申请")
return return
} }
user := currentUser(r) user := currentUser(r)
@@ -48,7 +48,7 @@ func (a *App) handleApplyMailbox(w http.ResponseWriter, r *http.Request) {
} }
domainID := strings.TrimSpace(req.DomainID) domainID := strings.TrimSpace(req.DomainID)
if domainID == "" { if domainID == "" {
badRequest(w, errors.New("domainId is required")) badRequest(w, errors.New("请选择域名"))
return return
} }
allowed, err := a.mailboxApplyDomainAllowed(r.Context(), domainID) allowed, err := a.mailboxApplyDomainAllowed(r.Context(), domainID)
@@ -57,17 +57,17 @@ func (a *App) handleApplyMailbox(w http.ResponseWriter, r *http.Request) {
return return
} }
if !allowed { if !allowed {
respondError(w, http.StatusForbidden, "domain is not available") respondError(w, http.StatusForbidden, "该域名不可用")
return return
} }
localPart := normalizeLocalPart(req.LocalPart) localPart := normalizeLocalPart(req.LocalPart)
if localPart == "" { if localPart == "" {
badRequest(w, errors.New("localPart is required")) badRequest(w, errors.New("请输入邮箱前缀"))
return return
} }
if len(localPart) > 64 { if len(localPart) > 64 {
badRequest(w, errors.New("localPart is too long")) badRequest(w, errors.New("邮箱前缀过长"))
return return
} }
reserved := map[string]bool{} reserved := map[string]bool{}
@@ -84,7 +84,7 @@ func (a *App) handleApplyMailbox(w http.ResponseWriter, r *http.Request) {
return return
} }
if exists > 0 { if exists > 0 {
respondError(w, http.StatusConflict, "mailbox already exists") respondError(w, http.StatusConflict, "该邮箱地址已被占用")
return return
} }
@@ -101,7 +101,7 @@ func (a *App) handleApplyMailbox(w http.ResponseWriter, r *http.Request) {
mailboxID, err := a.createMailboxWithPasswordHash(r.Context(), user.ID, domainID, localPart, displayName, passwordHash, 1024, "active") mailboxID, err := a.createMailboxWithPasswordHash(r.Context(), user.ID, domainID, localPart, displayName, passwordHash, 1024, "active")
if err != nil { if err != nil {
if strings.Contains(strings.ToLower(err.Error()), "unique") { if strings.Contains(strings.ToLower(err.Error()), "unique") {
respondError(w, http.StatusConflict, "mailbox already exists") respondError(w, http.StatusConflict, "该邮箱地址已被占用")
return return
} }
badRequest(w, err) badRequest(w, err)
+5 -5
View File
@@ -140,12 +140,12 @@ func (a *App) loadUserAuthByID(ctx context.Context, id string) (*User, string, e
func (a *App) handleTwoFactorSetup(w http.ResponseWriter, r *http.Request) { func (a *App) handleTwoFactorSetup(w http.ResponseWriter, r *http.Request) {
if !a.cfg.TwoFactorEnabled { if !a.cfg.TwoFactorEnabled {
respondError(w, http.StatusBadRequest, "two-factor authentication is disabled") respondError(w, http.StatusBadRequest, "双因素认证已关闭")
return return
} }
user := currentUser(r) user := currentUser(r)
if user == nil { if user == nil {
respondError(w, http.StatusUnauthorized, "authentication required") respondError(w, http.StatusUnauthorized, "需要登录后才能操作")
return return
} }
current, _, err := a.loadUserAuthByID(r.Context(), user.ID) current, _, err := a.loadUserAuthByID(r.Context(), user.ID)
@@ -175,12 +175,12 @@ func (a *App) handleTwoFactorSetup(w http.ResponseWriter, r *http.Request) {
func (a *App) handleTwoFactorEnable(w http.ResponseWriter, r *http.Request) { func (a *App) handleTwoFactorEnable(w http.ResponseWriter, r *http.Request) {
if !a.cfg.TwoFactorEnabled { if !a.cfg.TwoFactorEnabled {
respondError(w, http.StatusBadRequest, "two-factor authentication is disabled") respondError(w, http.StatusBadRequest, "双因素认证已关闭")
return return
} }
user := currentUser(r) user := currentUser(r)
if user == nil { if user == nil {
respondError(w, http.StatusUnauthorized, "authentication required") respondError(w, http.StatusUnauthorized, "需要登录后才能操作")
return return
} }
var req struct { var req struct {
@@ -222,7 +222,7 @@ func (a *App) handleTwoFactorEnable(w http.ResponseWriter, r *http.Request) {
func (a *App) handleTwoFactorDisable(w http.ResponseWriter, r *http.Request) { func (a *App) handleTwoFactorDisable(w http.ResponseWriter, r *http.Request) {
user := currentUser(r) user := currentUser(r)
if user == nil { if user == nil {
respondError(w, http.StatusUnauthorized, "authentication required") respondError(w, http.StatusUnauthorized, "需要登录后才能操作")
return return
} }
var req struct { var req struct {