|
| 1 | +package users |
| 2 | + |
| 3 | +import ( |
| 4 | + "errors" |
| 5 | + "fmt" |
| 6 | + "strconv" |
| 7 | + |
| 8 | + "github.com/bit-issues/backend/internal/jwt" |
| 9 | + "github.com/bit-issues/backend/internal/server/middlewares/jwtauth" |
| 10 | + "github.com/bit-issues/backend/internal/users" |
| 11 | + "github.com/go-core-fx/fiberfx/handler" |
| 12 | + "github.com/go-core-fx/fiberfx/validation" |
| 13 | + "github.com/go-playground/validator/v10" |
| 14 | + "github.com/gofiber/fiber/v2" |
| 15 | +) |
| 16 | + |
| 17 | +type Handler struct { |
| 18 | + handler.Base |
| 19 | + |
| 20 | + usersSvc *users.Service |
| 21 | + |
| 22 | + jwtSvc *jwt.Service |
| 23 | +} |
| 24 | + |
| 25 | +func NewHandler(usersSvc *users.Service, jwtSvc *jwt.Service, validate *validator.Validate) handler.Handler { |
| 26 | + return &Handler{ |
| 27 | + Base: handler.Base{Validator: validate}, |
| 28 | + |
| 29 | + usersSvc: usersSvc, |
| 30 | + |
| 31 | + jwtSvc: jwtSvc, |
| 32 | + } |
| 33 | +} |
| 34 | + |
| 35 | +func (h *Handler) Register(r fiber.Router) { |
| 36 | + admin := r.Group( |
| 37 | + "/admin/users", |
| 38 | + h.errorsHandler, |
| 39 | + jwtauth.New(h.jwtSvc, h.usersSvc), |
| 40 | + jwtauth.WithRole(users.RoleAdmin), |
| 41 | + ) |
| 42 | + |
| 43 | + // GET /admin/users - list all users with optional filters |
| 44 | + admin.Get("/", h.handleList) |
| 45 | + |
| 46 | + // PATCH /admin/users/{id} - update user status/role |
| 47 | + admin.Patch("/:id", validation.DecorateWithBodyEx(h.Validator, h.handleUpdate)) |
| 48 | +} |
| 49 | + |
| 50 | +// handleList returns a paginated list of users with optional status filter. |
| 51 | +// |
| 52 | +// @Summary List all users |
| 53 | +// @Description Admin can list all users with optional status filter and pagination. |
| 54 | +// @Tags Admin |
| 55 | +// @Accept json |
| 56 | +// @Produce json |
| 57 | +// @Security BearerAuth |
| 58 | +// @Param status query users.Status false "Filter by status" |
| 59 | +// @Param role query users.Role false "Filter by role" |
| 60 | +// @Param limit query int false "Page limit" default(20) |
| 61 | +// @Param offset query int false "Page offset" default(0) |
| 62 | +// @Success 200 {object} UserListResponse "Users list" |
| 63 | +// @Failure 401 {object} fiberfx.ErrorResponse "Unauthorized" |
| 64 | +// @Failure 403 {object} fiberfx.ErrorResponse "Forbidden" |
| 65 | +// @Router /admin/users [get] |
| 66 | +func (h *Handler) handleList(c *fiber.Ctx) error { |
| 67 | + filter := defaultAdminUserFilter() |
| 68 | + |
| 69 | + if err := h.QueryParserValidator(c, &filter); err != nil { |
| 70 | + return fmt.Errorf("failed to parse query: %w", err) |
| 71 | + } |
| 72 | + |
| 73 | + // Get users from service |
| 74 | + usersList, err := h.usersSvc.List(c.Context(), filter.Status, filter.Role, filter.Limit, filter.Offset) |
| 75 | + if err != nil { |
| 76 | + return fmt.Errorf("failed to list users: %w", err) |
| 77 | + } |
| 78 | + |
| 79 | + // Get total count for pagination |
| 80 | + total, err := h.usersSvc.Count(c.Context(), filter.Status, filter.Role) |
| 81 | + if err != nil { |
| 82 | + return fmt.Errorf("failed to count users: %w", err) |
| 83 | + } |
| 84 | + |
| 85 | + // Convert to response DTOs |
| 86 | + items := make([]UserResponse, 0, len(usersList)) |
| 87 | + for _, u := range usersList { |
| 88 | + items = append(items, toUserResponse(&u)) |
| 89 | + } |
| 90 | + |
| 91 | + return c.JSON(UserListResponse{ |
| 92 | + Items: items, |
| 93 | + Total: int(total), |
| 94 | + }) |
| 95 | +} |
| 96 | + |
| 97 | +// handleUpdate updates user status and/or role by admin. |
| 98 | +// |
| 99 | +// @Summary Update user |
| 100 | +// @Description Admin can update user status (active/blocked/pending) and role (admin/user). |
| 101 | +// @Tags Admin |
| 102 | +// @Accept json |
| 103 | +// @Produce json |
| 104 | +// @Security BearerAuth |
| 105 | +// @Param id path int64 true "User ID" |
| 106 | +// @Param request body UpdateUserRequest true "Update data" |
| 107 | +// @Success 200 {object} UserResponse "Updated user" |
| 108 | +// @Failure 400 {object} fiberfx.ErrorResponse "Validation error" |
| 109 | +// @Failure 401 {object} fiberfx.ErrorResponse "Unauthorized" |
| 110 | +// @Failure 403 {object} fiberfx.ErrorResponse "Forbidden" |
| 111 | +// @Failure 404 {object} fiberfx.ErrorResponse "User not found" |
| 112 | +// @Router /admin/users/{id} [patch] |
| 113 | +func (h *Handler) handleUpdate(c *fiber.Ctx, req *UpdateUserRequest) error { |
| 114 | + // Parse user ID from path |
| 115 | + idStr := c.Params("id") |
| 116 | + userID, err := strconv.ParseInt(idStr, 10, 64) |
| 117 | + if err != nil { |
| 118 | + return fiber.NewError(fiber.StatusBadRequest, "invalid user id") |
| 119 | + } |
| 120 | + |
| 121 | + // Validate at least one field is provided |
| 122 | + if req.Status == nil && req.Role == nil { |
| 123 | + return fiber.NewError(fiber.StatusBadRequest, "at least one of status or role must be provided") |
| 124 | + } |
| 125 | + |
| 126 | + // Perform update |
| 127 | + if updErr := h.usersSvc.Update( |
| 128 | + c.Context(), |
| 129 | + userID, |
| 130 | + users.UserUpdate{Status: req.Status, Role: req.Role}, |
| 131 | + ); updErr != nil { |
| 132 | + if errors.Is(updErr, users.ErrNotFound) { |
| 133 | + return fiber.NewError(fiber.StatusNotFound, updErr.Error()) |
| 134 | + } |
| 135 | + return fmt.Errorf("failed to update user: %w", updErr) |
| 136 | + } |
| 137 | + |
| 138 | + // Fetch updated user |
| 139 | + updatedUser, err := h.usersSvc.GetByID(c.Context(), userID) |
| 140 | + if err != nil { |
| 141 | + return fmt.Errorf("failed to fetch updated user: %w", err) |
| 142 | + } |
| 143 | + |
| 144 | + return c.JSON(toUserResponse(updatedUser)) |
| 145 | +} |
| 146 | + |
| 147 | +// errorsHandler converts service errors to HTTP errors. |
| 148 | +func (h *Handler) errorsHandler(c *fiber.Ctx) error { |
| 149 | + err := c.Next() |
| 150 | + if err == nil { |
| 151 | + return nil |
| 152 | + } |
| 153 | + |
| 154 | + switch { |
| 155 | + case errors.Is(err, users.ErrEmailAlreadyUsed): |
| 156 | + return fiber.NewError(fiber.StatusConflict, err.Error()) |
| 157 | + case errors.Is(err, users.ErrNotFound): |
| 158 | + return fiber.NewError(fiber.StatusNotFound, err.Error()) |
| 159 | + case errors.Is(err, users.ErrInvalidCredential): |
| 160 | + return fiber.NewError(fiber.StatusUnauthorized, err.Error()) |
| 161 | + case errors.Is(err, users.ErrNotActive): |
| 162 | + return fiber.NewError(fiber.StatusForbidden, err.Error()) |
| 163 | + |
| 164 | + default: |
| 165 | + return err //nolint:wrapcheck // err is already wrapped |
| 166 | + } |
| 167 | +} |
0 commit comments