From 170e54495d2f13212e226c2a9c89782ed77aea14 Mon Sep 17 00:00:00 2001 From: Huxley Date: Sat, 4 Jul 2026 17:18:23 +0800 Subject: [PATCH] feat(handler): add AdminHandler with admin routes --- internal/handler/admin.go | 111 ++++++++++++++++++++++++++++ internal/server/routes_protected.go | 9 +++ 2 files changed, 120 insertions(+) create mode 100644 internal/handler/admin.go diff --git a/internal/handler/admin.go b/internal/handler/admin.go new file mode 100644 index 0000000..36575ef --- /dev/null +++ b/internal/handler/admin.go @@ -0,0 +1,111 @@ +package handler + +import ( + "net/http" + "strconv" + "time" + + "github.com/gin-gonic/gin" + + "github.com/dhao2001/mygo/internal/api" + "github.com/dhao2001/mygo/internal/model" + "github.com/dhao2001/mygo/internal/repository" +) + +// AdminHandler handles admin-only endpoints for user management. +type AdminHandler struct { + userRepo repository.UserRepository +} + +// NewAdminHandler creates an AdminHandler. +func NewAdminHandler(userRepo repository.UserRepository) *AdminHandler { + return &AdminHandler{userRepo: userRepo} +} + +// adminUserResponse exposes all user fields, including status, for admin views. +type adminUserResponse struct { + ID string `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + IsAdmin bool `json:"is_admin"` + Status string `json:"status"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// adminUserListResponse wraps a paginated list of admin user views. +type adminUserListResponse struct { + Users []adminUserResponse `json:"users"` + Total int64 `json:"total"` +} + +func toAdminResponse(u *model.User) adminUserResponse { + return adminUserResponse{ + ID: u.ID, + Username: u.Username, + Email: u.Email, + IsAdmin: u.IsAdmin, + Status: u.Status, + CreatedAt: u.CreatedAt, + UpdatedAt: u.UpdatedAt, + } +} + +// DeleteUser handles DELETE /api/v1/admin/users/:id — soft-deletes a user. +func (h *AdminHandler) DeleteUser(c *gin.Context) { + id := c.Param("id") + + if err := h.userRepo.Delete(c.Request.Context(), id); err != nil { + api.RespondError(c, err) + return + } + + c.Status(http.StatusNoContent) +} + +// GetUser handles GET /api/v1/admin/users/:id — returns a user including deleted. +func (h *AdminHandler) GetUser(c *gin.Context) { + id := c.Param("id") + + user, err := h.userRepo.FindByIDIncludeDeleted(c.Request.Context(), id) + if err != nil { + api.RespondError(c, err) + return + } + + c.JSON(http.StatusOK, toAdminResponse(user)) +} + +// ListUsers handles GET /api/v1/admin/users — returns a paginated list of all users. +func (h *AdminHandler) ListUsers(c *gin.Context) { + offset, err := strconv.Atoi(c.DefaultQuery("offset", "0")) + if err != nil || offset < 0 { + api.Error(c, http.StatusBadRequest, "invalid offset") + return + } + + limit, err := strconv.Atoi(c.DefaultQuery("limit", "50")) + if err != nil || limit < 1 { + api.Error(c, http.StatusBadRequest, "invalid limit") + return + } + if limit > 200 { + limit = 200 + } + + users, total, err := h.userRepo.ListIncludeDeleted(c.Request.Context(), offset, limit) + if err != nil { + api.RespondError(c, err) + return + } + + items := make([]adminUserResponse, 0, len(users)) + for i := range users { + items = append(items, toAdminResponse(&users[i])) + } + + c.JSON(http.StatusOK, adminUserListResponse{ + Users: items, + Total: total, + }) +} diff --git a/internal/server/routes_protected.go b/internal/server/routes_protected.go index 6e53b9e..4c889c1 100644 --- a/internal/server/routes_protected.go +++ b/internal/server/routes_protected.go @@ -12,6 +12,7 @@ func setupProtectedRoutes(rg *gin.RouterGroup, webApp *app.WebApp) { jwtSecret := []byte(webApp.Config.JWT.Secret) accountHandler := handler.NewAccountHandler(webApp.AuthService) fileHandler := handler.NewFileHandler(webApp.FileService) + adminHandler := handler.NewAdminHandler(webApp.UserRepo) rg.Use(middleware.AuthRequired(jwtSecret)) @@ -36,4 +37,12 @@ func setupProtectedRoutes(rg *gin.RouterGroup, webApp *app.WebApp) { files.PUT("/:id", fileHandler.Update) files.DELETE("/:id", fileHandler.Delete) } + + admin := rg.Group("/admin") + admin.Use(middleware.AdminRequired(webApp.UserRepo)) + { + admin.GET("/users", adminHandler.ListUsers) + admin.GET("/users/:id", adminHandler.GetUser) + admin.DELETE("/users/:id", adminHandler.DeleteUser) + } }