Files
ld 63ede5c237 refactor(api): enforce protocol-neutral service boundaries
- refactor: move HTTP status and DTO concerns out of model and service
  layers into API and handler code.
- feat: add admin service boundary and route auth through services
  instead of direct repository access.
- test: add architecture and error tests covering package boundaries,
  redaction, and service behavior.
- docs: record the protocol-neutral boundary decision and update
  architecture and roadmap notes.
2026-07-05 23:30:17 +08:00

112 lines
2.8 KiB
Go

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/service"
)
// AdminHandler handles admin-only endpoints for user management.
type AdminHandler struct {
adminService *service.AdminService
}
// NewAdminHandler creates an AdminHandler.
func NewAdminHandler(adminService *service.AdminService) *AdminHandler {
return &AdminHandler{adminService: adminService}
}
// 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.adminService.DeleteUser(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.adminService.GetUser(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.adminService.ListUsers(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,
})
}