mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-30 22:26:38 +08:00
用户详情
This commit is contained in:
@@ -21,7 +21,10 @@ package user
|
||||
const (
|
||||
userNotFound = "用户不存在"
|
||||
cannotDisable = "不能禁用管理员用户"
|
||||
cannotDelete = "不能删除管理员用户"
|
||||
cannotDeleteSelf = "不能删除当前登录用户"
|
||||
updateUserFailed = "更新用户状态失败"
|
||||
deleteUserFailed = "删除用户失败"
|
||||
usernameExists = "用户名已存在"
|
||||
usernameRequired = "用户名不能为空"
|
||||
passwordTooShort = "密码长度不能少于 8 位"
|
||||
|
||||
@@ -19,9 +19,11 @@ package user
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/db/idgen"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
@@ -45,9 +47,15 @@ type user struct {
|
||||
ID uint64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Nickname string `json:"nickname"`
|
||||
Email string `json:"email"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
IsActive bool `json:"is_active"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
Bio string `json:"bio"`
|
||||
Phone string `json:"phone"`
|
||||
Gender string `json:"gender"`
|
||||
Website string `json:"website"`
|
||||
Location string `json:"location"`
|
||||
LastLoginAt time.Time `json:"last_login_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
@@ -59,6 +67,35 @@ type listUsersResponse struct {
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
func parseUserID(c *gin.Context) (uint64, bool) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.JSON(http.StatusBadRequest, util.Err(userNotFound))
|
||||
return 0, false
|
||||
}
|
||||
return id, true
|
||||
}
|
||||
|
||||
func toUser(u model.User) user {
|
||||
return user{
|
||||
ID: u.ID,
|
||||
Username: u.Username,
|
||||
Nickname: u.Nickname,
|
||||
Email: u.Email,
|
||||
AvatarURL: u.AvatarURL,
|
||||
IsActive: u.IsActive,
|
||||
IsAdmin: u.IsAdmin,
|
||||
Bio: u.Bio,
|
||||
Phone: u.Phone,
|
||||
Gender: u.Gender,
|
||||
Website: u.Website,
|
||||
Location: u.Location,
|
||||
LastLoginAt: u.LastLoginAt,
|
||||
CreatedAt: u.CreatedAt,
|
||||
UpdatedAt: u.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// ListUsers 获取用户列表
|
||||
// @Summary 获取用户列表
|
||||
// @Description 分页返回用户列表,支持按用户 ID 和用户名筛选,需要管理员权限
|
||||
@@ -118,6 +155,43 @@ func ListUsers(c *gin.Context) {
|
||||
}))
|
||||
}
|
||||
|
||||
// GetUser 获取用户详情
|
||||
// @Summary 获取用户详情
|
||||
// @Description 返回指定用户的完整个人资料和系统状态,需要管理员权限,不返回密码等敏感字段
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param id path int true "用户 ID"
|
||||
// @Success 200 {object} util.ResponseAny{data=user.user} "用户详情"
|
||||
// @Failure 400 {object} util.ResponseAny "参数错误"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 404 {object} util.ResponseAny "用户不存在"
|
||||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||
// @Router /api/v1/admin/users/{id} [get]
|
||||
func GetUser(c *gin.Context) {
|
||||
id, ok := parseUserID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var targetUser model.User
|
||||
if err := db.DB(c.Request.Context()).
|
||||
Select("id, username, nickname, email, avatar_url, is_active, is_admin, "+
|
||||
"bio, phone, gender, website, location, last_login_at, created_at, updated_at").
|
||||
Where("id = ?", id).
|
||||
First(&targetUser).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, util.Err(userNotFound))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(toUser(targetUser)))
|
||||
}
|
||||
|
||||
// updateUserStatusRequest 更新用户状态请求
|
||||
type updateUserStatusRequest struct {
|
||||
IsActive bool `json:"is_active"`
|
||||
@@ -146,7 +220,10 @@ func UpdateUserStatus(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
id := c.Param("id")
|
||||
id, ok := parseUserID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var targetUser struct {
|
||||
ID uint64 `gorm:"column:id"`
|
||||
@@ -181,6 +258,70 @@ func UpdateUserStatus(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, util.OKNil())
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户
|
||||
// @Summary 删除用户
|
||||
// @Description 删除指定非管理员用户,需要管理员权限,不能删除当前登录用户
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param id path int true "用户 ID"
|
||||
// @Success 200 {object} util.ResponseAny{data=string} "删除成功"
|
||||
// @Failure 400 {object} util.ResponseAny "参数错误"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限、尝试删除管理员或当前用户"
|
||||
// @Failure 404 {object} util.ResponseAny "用户不存在"
|
||||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||
// @Router /api/v1/admin/users/{id} [delete]
|
||||
func DeleteUser(c *gin.Context) {
|
||||
id, ok := parseUserID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
currUser, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
if currUser != nil && currUser.ID == id {
|
||||
c.JSON(http.StatusForbidden, util.Err(cannotDeleteSelf))
|
||||
return
|
||||
}
|
||||
|
||||
var targetUser struct {
|
||||
ID uint64 `gorm:"column:id"`
|
||||
IsAdmin bool `gorm:"column:is_admin"`
|
||||
}
|
||||
if err := db.DB(c.Request.Context()).
|
||||
Table("users").
|
||||
Select("id, is_admin").
|
||||
Where("id = ?", id).
|
||||
First(&targetUser).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, util.Err(userNotFound))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if targetUser.IsAdmin {
|
||||
c.JSON(http.StatusForbidden, util.Err(cannotDelete))
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.DB(c.Request.Context()).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("user_id = ?", id).Delete(&model.AccessToken{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("user_id = ?", id).Delete(&model.ExternalAccount{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Where("id = ?", id).Delete(&model.User{}).Error
|
||||
}); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(deleteUserFailed))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OKNil())
|
||||
}
|
||||
|
||||
// createUserRequest 创建用户请求
|
||||
type createUserRequest struct {
|
||||
Username string `json:"username" binding:"required,min=3,max=64"`
|
||||
@@ -257,17 +398,5 @@ func CreateUser(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
res := user{
|
||||
ID: newUser.ID,
|
||||
Username: newUser.Username,
|
||||
Nickname: newUser.Nickname,
|
||||
AvatarURL: newUser.AvatarURL,
|
||||
IsActive: newUser.IsActive,
|
||||
IsAdmin: newUser.IsAdmin,
|
||||
LastLoginAt: newUser.LastLoginAt,
|
||||
CreatedAt: newUser.CreatedAt,
|
||||
UpdatedAt: newUser.UpdatedAt,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(res))
|
||||
c.JSON(http.StatusOK, util.OK(toUser(newUser)))
|
||||
}
|
||||
|
||||
@@ -47,7 +47,9 @@ func setupTestRouter(authUser *model.User) *gin.Engine {
|
||||
|
||||
adminGroup.GET("/users", ListUsers)
|
||||
adminGroup.POST("/users", CreateUser)
|
||||
adminGroup.GET("/users/:id", GetUser)
|
||||
adminGroup.PUT("/users/:id/status", UpdateUserStatus)
|
||||
adminGroup.DELETE("/users/:id", DeleteUser)
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -170,6 +172,72 @@ func TestListUsers(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetUser(t *testing.T) {
|
||||
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
targetUser := model.User{
|
||||
ID: 1001,
|
||||
Username: "alice",
|
||||
Password: "secret-hash",
|
||||
Nickname: "Alice Nickname",
|
||||
Email: "alice@example.com",
|
||||
AvatarURL: "https://example.com/avatar.png",
|
||||
IsActive: true,
|
||||
IsAdmin: false,
|
||||
Bio: "hello",
|
||||
Phone: "123456",
|
||||
Gender: "female",
|
||||
Website: "https://example.com",
|
||||
Location: "Shanghai",
|
||||
}
|
||||
if err := dbConn.Create(&targetUser).Error; err != nil {
|
||||
t.Fatalf("failed to seed user: %v", err)
|
||||
}
|
||||
|
||||
adminUser := &model.User{ID: 1003, Username: "charlie", IsAdmin: true}
|
||||
router := setupTestRouter(adminUser)
|
||||
|
||||
t.Run("get full user profile", func(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "/api/v1/admin/users/1001", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 OK, got %d. Body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var resp util.ResponseAny
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("failed to parse response: %v", err)
|
||||
}
|
||||
|
||||
dataBytes, _ := json.Marshal(resp.Data)
|
||||
var resUser user
|
||||
if err := json.Unmarshal(dataBytes, &resUser); err != nil {
|
||||
t.Fatalf("failed to parse response data: %v", err)
|
||||
}
|
||||
|
||||
if resUser.Email != targetUser.Email || resUser.Bio != targetUser.Bio || resUser.Phone != targetUser.Phone ||
|
||||
resUser.Gender != targetUser.Gender || resUser.Website != targetUser.Website || resUser.Location != targetUser.Location {
|
||||
t.Errorf("profile fields were not returned correctly: %+v", resUser)
|
||||
}
|
||||
if bytes.Contains(dataBytes, []byte("secret-hash")) {
|
||||
t.Error("response should not include password")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get non-existent user", func(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "/api/v1/admin/users/9999", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected 404 Not Found, got %d. Body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateUserStatus(t *testing.T) {
|
||||
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
defer cleanup()
|
||||
@@ -349,3 +417,117 @@ func TestCreateUser(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteUser(t *testing.T) {
|
||||
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
regularUser := model.User{
|
||||
ID: 1001,
|
||||
Username: "alice",
|
||||
IsActive: true,
|
||||
IsAdmin: false,
|
||||
}
|
||||
adminUser := model.User{
|
||||
ID: 1002,
|
||||
Username: "bob",
|
||||
IsActive: true,
|
||||
IsAdmin: true,
|
||||
}
|
||||
selfUser := model.User{
|
||||
ID: 1003,
|
||||
Username: "charlie",
|
||||
IsActive: true,
|
||||
IsAdmin: false,
|
||||
}
|
||||
|
||||
if err := dbConn.Create(®ularUser).Error; err != nil {
|
||||
t.Fatalf("failed to seed regular user: %v", err)
|
||||
}
|
||||
if err := dbConn.Create(&adminUser).Error; err != nil {
|
||||
t.Fatalf("failed to seed admin user: %v", err)
|
||||
}
|
||||
if err := dbConn.Create(&selfUser).Error; err != nil {
|
||||
t.Fatalf("failed to seed self user: %v", err)
|
||||
}
|
||||
if err := dbConn.Create(&model.AccessToken{
|
||||
UserID: regularUser.ID,
|
||||
Name: "api",
|
||||
TokenHash: "hash-for-delete-user-test",
|
||||
MaskedToken: "at_****test",
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("failed to seed access token: %v", err)
|
||||
}
|
||||
if err := dbConn.Create(&model.ExternalAccount{
|
||||
ID: 5001,
|
||||
AuthSourceID: 1,
|
||||
UserID: regularUser.ID,
|
||||
ExternalID: "external-alice",
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("failed to seed external account: %v", err)
|
||||
}
|
||||
|
||||
router := setupTestRouter(&selfUser)
|
||||
|
||||
t.Run("delete regular user successfully", func(t *testing.T) {
|
||||
req, _ := http.NewRequest("DELETE", "/api/v1/admin/users/1001", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 OK, got %d. Body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var count int64
|
||||
if err := dbConn.Model(&model.User{}).Where("id = ?", 1001).Count(&count).Error; err != nil {
|
||||
t.Fatalf("failed to count deleted user: %v", err)
|
||||
}
|
||||
if count != 0 {
|
||||
t.Errorf("expected deleted user count 0, got %d", count)
|
||||
}
|
||||
|
||||
if err := dbConn.Model(&model.AccessToken{}).Where("user_id = ?", 1001).Count(&count).Error; err != nil {
|
||||
t.Fatalf("failed to count deleted access tokens: %v", err)
|
||||
}
|
||||
if count != 0 {
|
||||
t.Errorf("expected deleted access token count 0, got %d", count)
|
||||
}
|
||||
|
||||
if err := dbConn.Model(&model.ExternalAccount{}).Where("user_id = ?", 1001).Count(&count).Error; err != nil {
|
||||
t.Fatalf("failed to count deleted external accounts: %v", err)
|
||||
}
|
||||
if count != 0 {
|
||||
t.Errorf("expected deleted external account count 0, got %d", count)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cannot delete admin user", func(t *testing.T) {
|
||||
req, _ := http.NewRequest("DELETE", "/api/v1/admin/users/1002", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Fatalf("expected 403 Forbidden, got %d. Body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cannot delete current user", func(t *testing.T) {
|
||||
req, _ := http.NewRequest("DELETE", "/api/v1/admin/users/1003", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Fatalf("expected 403 Forbidden, got %d. Body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("delete non-existent user", func(t *testing.T) {
|
||||
req, _ := http.NewRequest("DELETE", "/api/v1/admin/users/9999", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected 404 Not Found, got %d. Body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -215,7 +215,9 @@ func Serve() {
|
||||
// Users
|
||||
adminRouter.GET("/users", admin_user.ListUsers)
|
||||
adminRouter.POST("/users", admin_user.CreateUser)
|
||||
adminRouter.GET("/users/:id", admin_user.GetUser)
|
||||
adminRouter.PUT("/users/:id/status", admin_user.UpdateUserStatus)
|
||||
adminRouter.DELETE("/users/:id", admin_user.DeleteUser)
|
||||
|
||||
// System Config
|
||||
adminRouter.POST("/system-configs", system_config.CreateSystemConfig)
|
||||
|
||||
Reference in New Issue
Block a user