diff --git a/docs/docs.go b/docs/docs.go index 64fc98e3..bbcdeca1 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -6286,10 +6286,15 @@ const docTemplate = `{ "user.createUserRequest": { "type": "object", "required": [ + "email", "password", "username" ], "properties": { + "email": { + "type": "string", + "maxLength": 255 + }, "is_active": { "type": "boolean" }, diff --git a/docs/swagger.json b/docs/swagger.json index 6528f81d..bfaffed1 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -6279,10 +6279,15 @@ "user.createUserRequest": { "type": "object", "required": [ + "email", "password", "username" ], "properties": { + "email": { + "type": "string", + "maxLength": 255 + }, "is_active": { "type": "boolean" }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index a699f518..57454a17 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -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 diff --git a/frontend/components/common/admin/create-user-modal.tsx b/frontend/components/common/admin/create-user-modal.tsx index 74e5cd4d..71da2a82 100644 --- a/frontend/components/common/admin/create-user-modal.tsx +++ b/frontend/components/common/admin/create-user-modal.tsx @@ -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({ /> +
+ + setForm((prev) => ({ ...prev, email: e.target.value }))} + placeholder="请输入邮箱地址" + /> + {errors.email && ( +

{errors.email}

+ )} +
+
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{}, diff --git a/internal/apps/admin/user/routers_test.go b/internal/apps/admin/user/routers_test.go index 3590bd06..d499e9f9 100644 --- a/internal/apps/admin/user/routers_test.go +++ b/internal/apps/admin/user/routers_test.go @@ -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) {