diff --git a/internal/consts/consts.go b/internal/consts/consts.go index 555dae8..ee454fb 100644 --- a/internal/consts/consts.go +++ b/internal/consts/consts.go @@ -170,6 +170,9 @@ const ( CodeModelImportInvalidFile = 10087 CodeModelImportBadVersion = 10088 + // Username validation + CodeInvalidUsername = 10089 + // Demo mode CodeDemoModeRestricted = 10403 @@ -276,7 +279,7 @@ const ( // Setup errors MsgSetupCompleted = "系统已完成初始化" - MsgSetupInvalidUsername = "用户名格式无效,仅支持字母、数字和下划线,长度3-20" + MsgSetupInvalidUsername = "用户名格式无效,仅支持字母和数字,长度3-20" MsgSetupPasswordMismatch = "两次输入的密码不一致" MsgSetupNotInitialized = "系统未初始化,请先完成设置" @@ -290,6 +293,9 @@ const ( // Model import errors MsgModelImportInvalidFile = "导入文件格式无效" MsgModelImportBadVersion = "导入文件版本不兼容" + + // Username validation + MsgInvalidUsername = "用户名仅支持英文字母和数字,不能为纯数字" ) // Demo mode messages diff --git a/internal/logic/admin/admin_user.go b/internal/logic/admin/admin_user.go index 61d7227..7771c21 100644 --- a/internal/logic/admin/admin_user.go +++ b/internal/logic/admin/admin_user.go @@ -72,6 +72,11 @@ func (s *sAdmin) ListUsers(ctx context.Context, req *v1.AdminUserListReq) (*v1.A func (s *sAdmin) CreateUser(ctx context.Context, req *v1.AdminUserCreateReq) (*v1.AdminUserCreateRes, error) { username := strings.TrimSpace(req.Username) + // Validate username format + if err := common.ValidateUsername(username); err != nil { + return nil, common.NewBusinessError(consts.CodeInvalidUsername, err.Error()) + } + // Check username uniqueness count, err := dao.SysAdminUsers.Ctx(ctx). Where("username", username).Count() diff --git a/internal/logic/admin/setup.go b/internal/logic/admin/setup.go index 3bc31a7..596b558 100644 --- a/internal/logic/admin/setup.go +++ b/internal/logic/admin/setup.go @@ -15,7 +15,7 @@ import ( "github.com/qianfree/team-api/internal/utility/crypto" ) -var usernameRegex = regexp.MustCompile(`^[a-zA-Z0-9_]{3,20}$`) +var usernameRegex = regexp.MustCompile(`^[a-zA-Z0-9]{3,20}$`) // AdminExists checks whether any admin user exists in the database. func AdminExists(ctx context.Context) (bool, error) { @@ -31,6 +31,17 @@ func ValidateSetupUsername(username string) error { if !usernameRegex.MatchString(username) { return common.NewBusinessError(consts.CodeSetupInvalidUsername, consts.MsgSetupInvalidUsername) } + // Disallow pure numeric usernames + allDigit := true + for _, c := range username { + if c < '0' || c > '9' { + allDigit = false + break + } + } + if allDigit { + return common.NewBusinessError(consts.CodeSetupInvalidUsername, "用户名不能为纯数字") + } return nil } diff --git a/internal/logic/admin/tenant.go b/internal/logic/admin/tenant.go index 8ef5079..e5dd80f 100644 --- a/internal/logic/admin/tenant.go +++ b/internal/logic/admin/tenant.go @@ -75,6 +75,11 @@ func (s *sAdmin) CreateTenant(ctx context.Context, req *v1.TenantCreateReq) (*v1 username := strings.TrimSpace(req.Username) email := strings.TrimSpace(strings.ToLower(req.Email)) + // Validate username format + if err := common.ValidateUsername(username); err != nil { + return nil, common.NewBusinessError(consts.CodeInvalidUsername, err.Error()) + } + count, err := dao.TntTenants.Ctx(ctx). Where("code", tenantCode).Count() if err != nil { diff --git a/internal/logic/common/validation.go b/internal/logic/common/validation.go index 3cc127e..39f0ec6 100644 --- a/internal/logic/common/validation.go +++ b/internal/logic/common/validation.go @@ -10,6 +10,27 @@ import ( "github.com/qianfree/team-api/internal/consts" ) +// ValidateUsername 校验用户名格式:仅允许英文字母和数字,不能为纯数字,长度3-50 +func ValidateUsername(username string) error { + if len(username) < 3 || len(username) > 50 { + return gerror.New("用户名长度为3-50位") + } + allDigit := true + for _, c := range username { + if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' { + allDigit = false + } else if c >= '0' && c <= '9' { + // digits are ok + } else { + return gerror.New("用户名仅支持英文字母和数字,不能包含特殊字符或中文") + } + } + if allDigit { + return gerror.New("用户名不能为纯数字") + } + return nil +} + // ValidatePassword 校验密码强度:至少8位,包含大写、小写、数字 func ValidatePassword(password string) error { if len(password) < 8 { diff --git a/internal/logic/tenant/auth.go b/internal/logic/tenant/auth.go index bf4f6f7..43ffb13 100644 --- a/internal/logic/tenant/auth.go +++ b/internal/logic/tenant/auth.go @@ -35,6 +35,11 @@ func (s *sTenant) Register(ctx context.Context, req *v1.TenantRegisterReq) (*v1. tenantCode := strings.TrimSpace(strings.ToLower(req.TenantCode)) username := strings.TrimSpace(req.Username) + // Validate username format + if err := common.ValidateUsername(username); err != nil { + return nil, common.NewBusinessError(consts.CodeInvalidUsername, err.Error()) + } + // 根据配置选择验证方式 emailVerificationEnabled := common.Config().GetBool(ctx, "register_email_verification") if emailVerificationEnabled { diff --git a/internal/logic/tenant/member.go b/internal/logic/tenant/member.go index a962cda..02707e5 100644 --- a/internal/logic/tenant/member.go +++ b/internal/logic/tenant/member.go @@ -229,6 +229,11 @@ func (s *sTenant) JoinByInvite(ctx context.Context, req *v1.TenantMemberJoinReq) username := strings.TrimSpace(req.Username) tenantID := invitation.TenantID + // Validate username format + if err := common.ValidateUsername(username); err != nil { + return nil, common.NewBusinessError(consts.CodeInvalidUsername, err.Error()) + } + var userID int64 err = dao.TntTenants.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { @@ -381,6 +386,12 @@ func (s *sTenant) CreateMember(ctx context.Context, req *v1.TenantMemberCreateRe username := strings.TrimSpace(req.Username) email := strings.TrimSpace(strings.ToLower(req.Email)) + + // Validate username format + if err := common.ValidateUsername(username); err != nil { + return nil, common.NewBusinessError(consts.CodeInvalidUsername, err.Error()) + } + displayName := req.DisplayName if displayName == "" { displayName = username diff --git a/web/tenant/src/views/auth/JoinPage.vue b/web/tenant/src/views/auth/JoinPage.vue index 9b7fa50..374cd07 100644 --- a/web/tenant/src/views/auth/JoinPage.vue +++ b/web/tenant/src/views/auth/JoinPage.vue @@ -41,10 +41,13 @@ v-model="form.username" type="text" required - placeholder="请输入用户名" + placeholder="仅支持英文字母和数字,不能为纯数字" class="input pl-11" + :class="{ 'input-error': usernameError }" + @input="validateUsername" /> +

{{ usernameError }}

@@ -193,6 +196,25 @@ const form = ref({ password: '', }) +const usernameError = ref('') + +function validateUsername() { + const val = form.value.username + if (!val) { + usernameError.value = '' + return + } + if (/[^a-zA-Z0-9]/.test(val)) { + usernameError.value = '用户名仅支持英文字母和数字' + } else if (/^\d+$/.test(val)) { + usernameError.value = '用户名不能为纯数字' + } else if (val.length < 3) { + usernameError.value = '用户名长度至少 3 位' + } else { + usernameError.value = '' + } +} + onMounted(async () => { const code = route.query.code as string if (!code) { @@ -218,6 +240,9 @@ onMounted(async () => { }) async function handleJoin() { + validateUsername() + if (usernameError.value) return + loading.value = true try { const res = await fetch('/api/tenant/members/join', { diff --git a/web/tenant/src/views/auth/RegisterPage.vue b/web/tenant/src/views/auth/RegisterPage.vue index 7b898ce..24468c8 100644 --- a/web/tenant/src/views/auth/RegisterPage.vue +++ b/web/tenant/src/views/auth/RegisterPage.vue @@ -89,6 +89,10 @@ function validateUser(): boolean { if (!userForm.username.trim()) { userErrors.username = '请输入用户名' + } else if (/[^a-zA-Z0-9]/.test(userForm.username)) { + userErrors.username = '用户名仅支持英文字母和数字' + } else if (/^\d+$/.test(userForm.username)) { + userErrors.username = '用户名不能为纯数字' } else if (userForm.username.length < 3) { userErrors.username = '用户名长度至少 3 位' } @@ -96,6 +100,23 @@ function validateUser(): boolean { return Object.keys(userErrors).length === 0 } +function validateUsernameRealtime() { + const val = userForm.username + if (!val) { + delete userErrors.username + return + } + if (/[^a-zA-Z0-9]/.test(val)) { + userErrors.username = '用户名仅支持英文字母和数字' + } else if (/^\d+$/.test(val)) { + userErrors.username = '用户名不能为纯数字' + } else if (val.length < 3) { + userErrors.username = '用户名长度至少 3 位' + } else { + delete userErrors.username + } +} + function goNext() { if (!validateOrg()) return step.value = 2 @@ -292,10 +313,11 @@ async function handleRegister() { + @input="validateUsernameRealtime" + />

{{ userErrors.username }}