feat(settings): 用户名校验强化、个性签名编辑、字符计数器及提示优化

- 用户名校验改用白名单正则(中英文/数字/下划线/连字符),字符数计法统一
- 简介→个性签名,新增 textarea 可编辑,上限 128 字符
- 用户名+签名各增加实时字符计数器(x/16、x/128)
- API 合并为 PUT /api/settings/profile,一次提交两字段,按变更写入
- Toast 移至 NAV 下方,独立样式,5 秒自动消失
- 移除用户 ID 灰色底色,提示改为「个人资料已更新」
- 纯文本存储 + Go 模板自动转义防 XSS
This commit is contained in:
2026-05-27 00:03:09 +08:00
parent 3937945b43
commit debaa17127
6 changed files with 347 additions and 12 deletions

View File

@ -2,6 +2,7 @@ package service
import (
"regexp"
"unicode/utf8"
"metazone.cc/metalab/internal/common"
"metazone.cc/metalab/internal/config"
@ -41,6 +42,9 @@ var (
var pwLetter = regexp.MustCompile(`[a-zA-Z]`)
var pwDigit = regexp.MustCompile(`\d`)
// usernamePattern 用户名合法字符:中文、英文大小写、数字、下划线、连字符
var usernamePattern = regexp.MustCompile(`^[\p{Han}a-zA-Z0-9_-]+$`)
// dummyHash 防时序攻击:当用户不存在时,仍对虚拟哈希执行完整的 bcrypt 比对
// 在 init() 中按当前 bcrypt 成本生成,确保时序与真实校验一致
var dummyHash []byte
@ -184,3 +188,52 @@ func (s *AuthService) GetProfile(userID uint) (*model.User, error) {
func (s *AuthService) InvalidateSessions(userID uint) error {
return s.userRepo.IncrementTokenVersion(userID)
}
// UpdateProfile 修改个人资料(用户名 + 个性签名)
// 用户名校验非空、1-16 字符、白名单、去重
// 个性签名校验0-128 字符纯文本Go 模板自动 HTML 转义防 XSS
// 任一字段无变更时跳过该字段的写库操作
func (s *AuthService) UpdateProfile(userID uint, username, bio string) error {
// 查当前用户
user, err := s.userRepo.FindByID(userID)
if err != nil {
return common.ErrUserNotFound
}
needsUpdate := false
// 用户名:校验 + 更新
if n := utf8.RuneCountInString(username); n == 0 {
return common.ErrUsernameInvalid
} else if n > 16 {
return common.ErrUsernameInvalid
}
if !usernamePattern.MatchString(username) {
return common.ErrUsernameInvalid
}
if username != user.Username {
exists, err := s.userRepo.ExistsByUsername(username)
if err != nil {
return err
}
if exists {
return common.ErrUsernameTaken
}
user.Username = username
needsUpdate = true
}
// 个性签名:长度校验 + 更新
if utf8.RuneCountInString(bio) > 128 {
return common.ErrBioTooLong
}
if bio != user.Bio {
user.Bio = bio
needsUpdate = true
}
if !needsUpdate {
return nil
}
return s.userRepo.Update(user)
}