diff --git a/internal/server/docs/docs.go b/internal/server/docs/docs.go index a8b2eb2..6586499 100644 --- a/internal/server/docs/docs.go +++ b/internal/server/docs/docs.go @@ -23,154 +23,6 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { - "/admin/users": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Admin can list all users with optional status filter and pagination.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Admin" - ], - "summary": "List all users", - "parameters": [ - { - "enum": [ - "pending", - "active", - "blocked" - ], - "type": "string", - "description": "Filter by status", - "name": "status", - "in": "query" - }, - { - "enum": [ - "admin", - "user" - ], - "type": "string", - "description": "Filter by role", - "name": "role", - "in": "query" - }, - { - "type": "integer", - "default": 20, - "description": "Page limit", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "default": 0, - "description": "Page offset", - "name": "offset", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Users list", - "schema": { - "$ref": "#/definitions/users.UserListResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/fiberfx.ErrorResponse" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/fiberfx.ErrorResponse" - } - } - } - } - }, - "/admin/users/{id}": { - "patch": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Admin can update user status (active/blocked/pending) and role (admin/user).", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Admin" - ], - "summary": "Update user", - "parameters": [ - { - "type": "integer", - "format": "int64", - "description": "User ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Update data", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/users.UpdateUserRequest" - } - } - ], - "responses": { - "200": { - "description": "Updated user", - "schema": { - "$ref": "#/definitions/users.UserResponse" - } - }, - "400": { - "description": "Validation error", - "schema": { - "$ref": "#/definitions/fiberfx.ErrorResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/fiberfx.ErrorResponse" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/fiberfx.ErrorResponse" - } - }, - "404": { - "description": "User not found", - "schema": { - "$ref": "#/definitions/fiberfx.ErrorResponse" - } - } - } - } - }, "/auth/change-password": { "post": { "security": [ @@ -612,6 +464,154 @@ const docTemplate = `{ } } } + }, + "/users": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Admin can list all users with optional status filter and pagination.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin" + ], + "summary": "List all users", + "parameters": [ + { + "enum": [ + "pending", + "active", + "blocked" + ], + "type": "string", + "description": "Filter by status", + "name": "status", + "in": "query" + }, + { + "enum": [ + "admin", + "user" + ], + "type": "string", + "description": "Filter by role", + "name": "role", + "in": "query" + }, + { + "type": "integer", + "default": 20, + "description": "Page limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Page offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Users list", + "schema": { + "$ref": "#/definitions/users.ListResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/fiberfx.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/fiberfx.ErrorResponse" + } + } + } + } + }, + "/users/{id}": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Admin can update user status (active/blocked/pending) and role (admin/user).", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin" + ], + "summary": "Update user", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update data", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/users.UpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "Updated user", + "schema": { + "$ref": "#/definitions/users.GetResponse" + } + }, + "400": { + "description": "Validation error", + "schema": { + "$ref": "#/definitions/fiberfx.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/fiberfx.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/fiberfx.ErrorResponse" + } + }, + "404": { + "description": "User not found", + "schema": { + "$ref": "#/definitions/fiberfx.ErrorResponse" + } + } + } + } } }, "definitions": { @@ -785,34 +785,22 @@ const docTemplate = `{ } } }, - "users.Role": { - "type": "string", - "enum": [ - "admin", - "user" - ], - "x-enum-varnames": [ - "RoleAdmin", - "RoleUser" - ] - }, - "users.Status": { - "type": "string", - "enum": [ - "pending", - "active", - "blocked" - ], - "x-enum-varnames": [ - "StatusPending", - "StatusActive", - "StatusBlocked" - ] - }, - "users.UpdateUserRequest": { - "description": "Admin can update user status (active/blocked/pending) and role (admin/user).", + "users.GetResponse": { + "description": "User data returned in admin API responses (password excluded).", "type": "object", "properties": { + "created_at": { + "type": "string", + "example": "2026-04-06T10:00:00Z" + }, + "email": { + "type": "string", + "example": "user@example.com" + }, + "id": { + "type": "integer", + "example": 42 + }, "role": { "enum": [ "admin", @@ -822,7 +810,8 @@ const docTemplate = `{ { "$ref": "#/definitions/users.Role" } - ] + ], + "example": "user" }, "status": { "enum": [ @@ -834,18 +823,23 @@ const docTemplate = `{ { "$ref": "#/definitions/users.Status" } - ] + ], + "example": "active" + }, + "updated_at": { + "type": "string", + "example": "2026-04-06T10:00:00Z" } } }, - "users.UserListResponse": { + "users.ListResponse": { "description": "Paginated response containing users list and total count.", "type": "object", "properties": { "items": { "type": "array", "items": { - "$ref": "#/definitions/users.UserResponse" + "$ref": "#/definitions/users.GetResponse" } }, "total": { @@ -854,22 +848,34 @@ const docTemplate = `{ } } }, - "users.UserResponse": { - "description": "User data returned in admin API responses (password excluded).", + "users.Role": { + "type": "string", + "enum": [ + "admin", + "user" + ], + "x-enum-varnames": [ + "RoleAdmin", + "RoleUser" + ] + }, + "users.Status": { + "type": "string", + "enum": [ + "pending", + "active", + "blocked" + ], + "x-enum-varnames": [ + "StatusPending", + "StatusActive", + "StatusBlocked" + ] + }, + "users.UpdateRequest": { + "description": "Admin can update user status (active/blocked/pending) and role (admin/user).", "type": "object", "properties": { - "created_at": { - "type": "string", - "example": "2026-04-06T10:00:00Z" - }, - "email": { - "type": "string", - "example": "user@example.com" - }, - "id": { - "type": "integer", - "example": 42 - }, "role": { "enum": [ "admin", @@ -879,8 +885,7 @@ const docTemplate = `{ { "$ref": "#/definitions/users.Role" } - ], - "example": "user" + ] }, "status": { "enum": [ @@ -892,12 +897,7 @@ const docTemplate = `{ { "$ref": "#/definitions/users.Status" } - ], - "example": "active" - }, - "updated_at": { - "type": "string", - "example": "2026-04-06T10:00:00Z" + ] } } } diff --git a/internal/server/module.go b/internal/server/module.go index b65a39c..c1ec12e 100644 --- a/internal/server/module.go +++ b/internal/server/module.go @@ -1,11 +1,11 @@ package server import ( - "github.com/bit-issues/backend/internal/server/admin/users" "github.com/bit-issues/backend/internal/server/auth" "github.com/bit-issues/backend/internal/server/docs" "github.com/bit-issues/backend/internal/server/middlewares/jwtauth" "github.com/bit-issues/backend/internal/server/projects" + "github.com/bit-issues/backend/internal/server/users" "github.com/go-core-fx/fiberfx" "github.com/go-core-fx/fiberfx/handler" "github.com/go-core-fx/fiberfx/health" diff --git a/internal/server/admin/users/dto.go b/internal/server/users/dto.go similarity index 73% rename from internal/server/admin/users/dto.go rename to internal/server/users/dto.go index 6b33090..5e266bf 100644 --- a/internal/server/admin/users/dto.go +++ b/internal/server/users/dto.go @@ -10,28 +10,28 @@ const ( defaultLimit = 20 ) -// AdminUserFilter represents query parameters for filtering users. +// ListFilter represents query parameters for filtering users. // // @Description Query parameters for filtering and paginating users list. -type AdminUserFilter struct { +type ListFilter struct { Status *users.Status `query:"status" validate:"omitempty,oneof=pending active blocked" enums:"pending,active,blocked"` Role *users.Role `query:"role" validate:"omitempty,oneof=admin user" enums:"admin,user"` Limit int `query:"limit" validate:"min=1,max=100" default:"20"` Offset int `query:"offset" validate:"min=0" default:"0"` } -// UpdateUserRequest represents admin request to update user status/role. +// UpdateRequest represents admin request to update user status/role. // // @Description Admin can update user status (active/blocked/pending) and role (admin/user). -type UpdateUserRequest struct { +type UpdateRequest struct { Status *users.Status `json:"status,omitempty" validate:"omitempty,oneof=pending active blocked" enums:"pending,active,blocked"` Role *users.Role `json:"role,omitempty" validate:"omitempty,oneof=admin user" enums:"admin,user"` } -// UserResponse represents user data in admin responses (without password). +// GetResponse represents user data in admin responses (without password). // // @Description User data returned in admin API responses (password excluded). -type UserResponse struct { +type GetResponse struct { ID int64 `json:"id" example:"42"` Email string `json:"email" example:"user@example.com"` Role users.Role `json:"role" example:"user" enums:"admin,user"` @@ -40,16 +40,16 @@ type UserResponse struct { UpdatedAt time.Time `json:"updated_at" example:"2026-04-06T10:00:00Z"` } -// UserListResponse represents paginated list of users. +// ListResponse represents paginated list of users. // // @Description Paginated response containing users list and total count. -type UserListResponse struct { - Items []UserResponse `json:"items"` - Total int `json:"total" example:"42"` +type ListResponse struct { + Items []GetResponse `json:"items"` + Total int `json:"total" example:"42"` } -func defaultAdminUserFilter() AdminUserFilter { - return AdminUserFilter{ +func defaultListFilter() ListFilter { + return ListFilter{ Status: nil, Role: nil, Limit: defaultLimit, @@ -57,9 +57,9 @@ func defaultAdminUserFilter() AdminUserFilter { } } -// toUserResponse converts domain User to admin UserResponse. -func toUserResponse(u *users.User) UserResponse { - return UserResponse{ +// toGetResponse converts domain User to admin UserResponse. +func toGetResponse(u *users.User) GetResponse { + return GetResponse{ ID: u.ID, Email: u.Email, Role: u.Role, diff --git a/internal/server/admin/users/handler.go b/internal/server/users/handler.go similarity index 86% rename from internal/server/admin/users/handler.go rename to internal/server/users/handler.go index 3e641c0..fbcd44e 100644 --- a/internal/server/admin/users/handler.go +++ b/internal/server/users/handler.go @@ -34,15 +34,15 @@ func NewHandler(usersSvc *users.Service, jwtSvc *jwt.Service, validate *validato func (h *Handler) Register(r fiber.Router) { admin := r.Group( - "/admin/users", + "/users", h.errorsHandler, jwtauth.WithRole(users.RoleAdmin), ) - // GET /admin/users - list all users with optional filters + // GET /users - list all users with optional filters admin.Get("/", h.handleList) - // PATCH /admin/users/{id} - update user status/role + // PATCH /users/{id} - update user status/role admin.Patch("/:id", validation.DecorateWithBodyEx(h.Validator, h.handleUpdate)) } @@ -58,12 +58,12 @@ func (h *Handler) Register(r fiber.Router) { // @Param role query users.Role false "Filter by role" // @Param limit query int false "Page limit" default(20) // @Param offset query int false "Page offset" default(0) -// @Success 200 {object} UserListResponse "Users list" +// @Success 200 {object} ListResponse "Users list" // @Failure 401 {object} fiberfx.ErrorResponse "Unauthorized" // @Failure 403 {object} fiberfx.ErrorResponse "Forbidden" -// @Router /admin/users [get] +// @Router /users [get] func (h *Handler) handleList(c *fiber.Ctx) error { - filter := defaultAdminUserFilter() + filter := defaultListFilter() if err := h.QueryParserValidator(c, &filter); err != nil { return fmt.Errorf("failed to parse query: %w", err) @@ -82,12 +82,12 @@ func (h *Handler) handleList(c *fiber.Ctx) error { } // Convert to response DTOs - items := make([]UserResponse, 0, len(usersList)) + items := make([]GetResponse, 0, len(usersList)) for _, u := range usersList { - items = append(items, toUserResponse(&u)) + items = append(items, toGetResponse(&u)) } - return c.JSON(UserListResponse{ + return c.JSON(ListResponse{ Items: items, Total: int(total), }) @@ -102,14 +102,14 @@ func (h *Handler) handleList(c *fiber.Ctx) error { // @Produce json // @Security BearerAuth // @Param id path int64 true "User ID" -// @Param request body UpdateUserRequest true "Update data" -// @Success 200 {object} UserResponse "Updated user" +// @Param request body UpdateRequest true "Update data" +// @Success 200 {object} GetResponse "Updated user" // @Failure 400 {object} fiberfx.ErrorResponse "Validation error" // @Failure 401 {object} fiberfx.ErrorResponse "Unauthorized" // @Failure 403 {object} fiberfx.ErrorResponse "Forbidden" // @Failure 404 {object} fiberfx.ErrorResponse "User not found" -// @Router /admin/users/{id} [patch] -func (h *Handler) handleUpdate(c *fiber.Ctx, req *UpdateUserRequest) error { +// @Router /users/{id} [patch] +func (h *Handler) handleUpdate(c *fiber.Ctx, req *UpdateRequest) error { // Parse user ID from path idStr := c.Params("id") userID, err := strconv.ParseInt(idStr, 10, 64) @@ -140,7 +140,7 @@ func (h *Handler) handleUpdate(c *fiber.Ctx, req *UpdateUserRequest) error { return fmt.Errorf("failed to fetch updated user: %w", err) } - return c.JSON(toUserResponse(updatedUser)) + return c.JSON(toGetResponse(updatedUser)) } // errorsHandler converts service errors to HTTP errors. diff --git a/requests.http b/requests.http index 3ac8b0c..43d2c51 100644 --- a/requests.http +++ b/requests.http @@ -62,13 +62,13 @@ Authorization: Bearer {{accessToken}} # List all users (admin only) # Requires admin JWT token in Authorization header # Optional query parameters: status (pending/active/blocked), role (admin/user), limit, offset -GET {{baseURL}}/admin/users?status=pending +GET {{baseURL}}/users?status=pending Authorization: Bearer {{adminAccessToken}} ### # Update user status/role (admin only) # Requires admin JWT token in Authorization header -PATCH {{baseURL}}/admin/users/{{userID}} +PATCH {{baseURL}}/users/{{userID}} Content-Type: application/json Authorization: Bearer {{adminAccessToken}} @@ -79,7 +79,7 @@ Authorization: Bearer {{adminAccessToken}} ### # Example: Activate a pending user -PATCH {{baseURL}}/admin/users/2 +PATCH {{baseURL}}/users/2 Content-Type: application/json Authorization: Bearer {{adminAccessToken}} @@ -89,7 +89,7 @@ Authorization: Bearer {{adminAccessToken}} ### # Example: Block a user -PATCH {{baseURL}}/admin/users/3 +PATCH {{baseURL}}/users/3 Content-Type: application/json Authorization: Bearer {{adminAccessToken}} @@ -99,7 +99,7 @@ Authorization: Bearer {{adminAccessToken}} ### # Example: Promote user to admin -PATCH {{baseURL}}/admin/users/4 +PATCH {{baseURL}}/users/4 Content-Type: application/json Authorization: Bearer {{adminAccessToken}}