mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-30 22:26:38 +08:00
feat(user): require email when admin creates a user
- Add `email` as a required field in `createUserRequest` - Enforce email format verification and database uniqueness checks in the admin user creation handler - Update the admin user creation frontend modal with validation and form field - Update the corresponding backend unit tests and regenerate Swagger docs
This commit is contained in:
@@ -6286,10 +6286,15 @@ const docTemplate = `{
|
||||
"user.createUserRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"password",
|
||||
"username"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
},
|
||||
"is_active": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -6279,10 +6279,15 @@
|
||||
"user.createUserRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"password",
|
||||
"username"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
},
|
||||
"is_active": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -987,6 +987,9 @@ definitions:
|
||||
type: object
|
||||
user.createUserRequest:
|
||||
properties:
|
||||
email:
|
||||
maxLength: 255
|
||||
type: string
|
||||
is_active:
|
||||
type: boolean
|
||||
is_admin:
|
||||
@@ -1003,6 +1006,7 @@ definitions:
|
||||
minLength: 3
|
||||
type: string
|
||||
required:
|
||||
- email
|
||||
- password
|
||||
- username
|
||||
type: object
|
||||
|
||||
@@ -13,6 +13,7 @@ const emptyForm: CreateUserRequest = {
|
||||
username: "",
|
||||
password: "",
|
||||
nickname: "",
|
||||
email: "",
|
||||
is_active: true,
|
||||
is_admin: false,
|
||||
}
|
||||
@@ -46,6 +47,12 @@ export function CreateUserModal({
|
||||
newErrors.username = "用户名长度不能少于 3 位"
|
||||
}
|
||||
|
||||
if (!form.email.trim()) {
|
||||
newErrors.email = "邮箱不能为空"
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email.trim())) {
|
||||
newErrors.email = "邮箱格式不正确"
|
||||
}
|
||||
|
||||
if (!form.password) {
|
||||
newErrors.password = "密码不能为空"
|
||||
} else if (form.password.length < 8) {
|
||||
@@ -66,6 +73,7 @@ export function CreateUserModal({
|
||||
...form,
|
||||
username: form.username.trim(),
|
||||
nickname: form.nickname?.trim() || undefined,
|
||||
email: form.email.trim(),
|
||||
})
|
||||
onClose()
|
||||
} catch {
|
||||
@@ -109,6 +117,20 @@ export function CreateUserModal({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="email">邮箱</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={form.email}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, email: e.target.value }))}
|
||||
placeholder="请输入邮箱地址"
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="text-xs text-destructive">{errors.email}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="password">密码</Label>
|
||||
<Input
|
||||
|
||||
@@ -340,6 +340,8 @@ export interface CreateUserRequest {
|
||||
password: string;
|
||||
/** 昵称 */
|
||||
nickname?: string;
|
||||
/** 邮箱 */
|
||||
email: string;
|
||||
/** 是否激活 */
|
||||
is_active?: boolean;
|
||||
/** 是否管理员 */
|
||||
|
||||
@@ -16,4 +16,6 @@ const (
|
||||
usernameRequired = "用户名不能为空"
|
||||
passwordTooShort = "密码长度不能少于 8 位" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||
createUserFailed = "创建用户失败"
|
||||
emailRequired = "邮箱不能为空"
|
||||
emailExists = "邮箱已被注册"
|
||||
)
|
||||
|
||||
@@ -314,6 +314,7 @@ type createUserRequest struct {
|
||||
Username string `json:"username" binding:"required,min=3,max=64"`
|
||||
Password string `json:"password" binding:"required,min=8,max=64"`
|
||||
Nickname string `json:"nickname" binding:"omitempty,max=64"`
|
||||
Email string `json:"email" binding:"required,email,max=255"`
|
||||
IsActive bool `json:"is_active"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
}
|
||||
@@ -342,11 +343,16 @@ func CreateUser(c *gin.Context) {
|
||||
req.Username = strings.TrimSpace(req.Username)
|
||||
req.Nickname = strings.TrimSpace(req.Nickname)
|
||||
req.Password = strings.TrimSpace(req.Password)
|
||||
req.Email = strings.TrimSpace(req.Email)
|
||||
|
||||
if req.Username == "" {
|
||||
c.JSON(http.StatusBadRequest, util.Err(usernameRequired))
|
||||
return
|
||||
}
|
||||
if req.Email == "" {
|
||||
c.JSON(http.StatusBadRequest, util.Err(emailRequired))
|
||||
return
|
||||
}
|
||||
if len(req.Password) < minPasswordLength {
|
||||
c.JSON(http.StatusBadRequest, util.Err(passwordTooShort))
|
||||
return
|
||||
@@ -363,10 +369,21 @@ func CreateUser(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var emailCount int64
|
||||
if err := db.DB(ctx).Model(&model.User{}).Where("email = ?", req.Email).Count(&emailCount).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
if emailCount > 0 {
|
||||
c.JSON(http.StatusBadRequest, util.Err(emailExists))
|
||||
return
|
||||
}
|
||||
|
||||
newUser := model.User{
|
||||
ID: idgen.NextUint64ID(),
|
||||
Username: req.Username,
|
||||
Nickname: req.Nickname,
|
||||
Email: req.Email,
|
||||
IsActive: req.IsActive,
|
||||
IsAdmin: req.IsAdmin,
|
||||
LastLoginAt: time.Time{},
|
||||
|
||||
@@ -313,6 +313,7 @@ func TestCreateUser(t *testing.T) {
|
||||
Username: "newuser",
|
||||
Password: "newpassword123",
|
||||
Nickname: "New Nickname",
|
||||
Email: "newuser@example.com",
|
||||
IsActive: true,
|
||||
IsAdmin: false,
|
||||
}
|
||||
@@ -350,6 +351,9 @@ func TestCreateUser(t *testing.T) {
|
||||
if err := dbConn.Where("username = ?", "newuser").First(&dbUser).Error; err != nil {
|
||||
t.Fatalf("failed to find user in db: %v", err)
|
||||
}
|
||||
if dbUser.Email != "newuser@example.com" {
|
||||
t.Errorf("expected email 'newuser@example.com', got '%s'", dbUser.Email)
|
||||
}
|
||||
if !dbUser.CheckPassword("newpassword123") {
|
||||
t.Error("password was not hashed correctly")
|
||||
}
|
||||
@@ -361,6 +365,7 @@ func TestCreateUser(t *testing.T) {
|
||||
ID: 2001,
|
||||
Username: "dupuser",
|
||||
Nickname: "Dup User",
|
||||
Email: "dupuser@example.com",
|
||||
}
|
||||
dbConn.Create(&existing)
|
||||
|
||||
@@ -368,6 +373,7 @@ func TestCreateUser(t *testing.T) {
|
||||
Username: "dupuser",
|
||||
Password: "password123",
|
||||
Nickname: "Another Nick",
|
||||
Email: "another@example.com",
|
||||
IsActive: true,
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
@@ -387,10 +393,44 @@ func TestCreateUser(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("create user with duplicate email", func(t *testing.T) {
|
||||
existing := model.User{
|
||||
ID: 2002,
|
||||
Username: "existingemail",
|
||||
Nickname: "Existing Email",
|
||||
Email: "dupemail@example.com",
|
||||
}
|
||||
dbConn.Create(&existing)
|
||||
|
||||
payload := createUserRequest{
|
||||
Username: "newuser2",
|
||||
Password: "password123",
|
||||
Nickname: "New User 2",
|
||||
Email: "dupemail@example.com",
|
||||
IsActive: true,
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
req, _ := http.NewRequest("POST", "/api/v1/admin/users", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400 Bad Request, got %d. Body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var resp util.ResponseAny
|
||||
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
if resp.ErrorMsg != emailExists {
|
||||
t.Errorf("expected error '%s', got '%s'", emailExists, resp.ErrorMsg)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("validation error - password too short", func(t *testing.T) {
|
||||
payload := createUserRequest{
|
||||
Username: "shortpass",
|
||||
Password: "123",
|
||||
Email: "shortpass@example.com",
|
||||
IsActive: true,
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
@@ -403,6 +443,24 @@ func TestCreateUser(t *testing.T) {
|
||||
t.Errorf("expected 400 Bad Request, got %d. Body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("validation error - invalid email format", func(t *testing.T) {
|
||||
payload := map[string]interface{}{
|
||||
"username": "bademail",
|
||||
"password": "password123",
|
||||
"email": "not-an-email",
|
||||
"is_active": true,
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
req, _ := http.NewRequest("POST", "/api/v1/admin/users", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400 Bad Request, got %d. Body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteUser(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user