From b429bcd24fecb34d7f5d450c0cc888522108c9ee Mon Sep 17 00:00:00 2001 From: "n.nakhostin" Date: Wed, 17 Sep 2025 10:15:14 +0330 Subject: [PATCH] Add Role Assignment Feature for Customers - Introduced a new role assignment feature for customers, allowing roles to be assigned as either 'admin' or 'analyst'. - Updated the Customer entity to include a Role field, enhancing the data model to support role management. - Created new request and response forms for role assignment, ensuring proper validation and structured responses. - Implemented the AssignRole method in the customer service layer, handling role assignment logic and error management. - Added a new AssignRole handler to process role assignment requests via the API, complete with Swagger documentation for clarity. - Updated routes to include the new role assignment endpoint, improving the administrative capabilities of the system. - Enhanced logging for role assignment operations to ensure traceability and debugging ease. --- cmd/web/router/routes.go | 1 + internal/customer/entity.go | 9 ++++++ internal/customer/form.go | 15 +++++++++ internal/customer/handler.go | 37 +++++++++++++++++++++ internal/customer/service.go | 63 +++++++++++++++++++++++++++++++++++- 5 files changed, 124 insertions(+), 1 deletion(-) diff --git a/cmd/web/router/routes.go b/cmd/web/router/routes.go index 6569872..f159cef 100644 --- a/cmd/web/router/routes.go +++ b/cmd/web/router/routes.go @@ -80,6 +80,7 @@ func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler customersGP.PUT("/:id", customerHandler.UpdateCustomer) customersGP.DELETE("/:id", customerHandler.DeleteCustomer) customersGP.PUT("/:id/status", customerHandler.UpdateStatus) + customersGP.PATCH("/:id/role", customerHandler.AssignRole) } // Admin Tenders Routes diff --git a/internal/customer/entity.go b/internal/customer/entity.go index c39f637..bf636c4 100644 --- a/internal/customer/entity.go +++ b/internal/customer/entity.go @@ -24,6 +24,14 @@ const ( CustomerTypeGovernment CustomerType = "government" ) +// CustomerRole represents the role of customer +type CustomerRole string + +const ( + CustomerRoleAdmin CustomerRole = "admin" + CustomerRoleAnalyst CustomerRole = "analyst" +) + // Customer represents a customer in the tender management system // A customer can have an associated company for login purposes type Customer struct { @@ -34,6 +42,7 @@ type Customer struct { Password string `bson:"password"` Status CustomerStatus `bson:"status"` Type CustomerType `bson:"type"` + Role CustomerRole `bson:"role"` Phone *string `bson:"phone"` CompanyIDs []string `bson:"company_ids"` LastLoginAt *int64 `bson:"last_login_at"` diff --git a/internal/customer/form.go b/internal/customer/form.go index b5e42fb..7ab105a 100644 --- a/internal/customer/form.go +++ b/internal/customer/form.go @@ -6,6 +6,7 @@ import "tm/pkg/response" type CreateCustomerForm struct { FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)" example:"User"` Type string `json:"type" valid:"required,in(individual|company|government)" example:"individual"` + Role string `json:"role" valid:"required,in(admin|analyst)" example:"analyst"` CompanyIDs []string `json:"company_ids,omitempty" valid:"optional"` Username string `json:"username" valid:"required,alphanum,length(3|30)" example:"user"` Email string `json:"email" valid:"required,email" example:"user@opplens.com"` @@ -17,6 +18,7 @@ type CreateCustomerForm struct { type UpdateCustomerForm struct { FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)" example:"User"` Type string `json:"type" valid:"required,in(individual|company|government)" example:"individual"` + Role string `json:"role" valid:"required,in(admin|analyst)" example:"analyst"` CompanyIDs []string `json:"company_ids,omitempty" valid:"optional"` Username string `json:"username" valid:"required,alphanum,length(3|30)" example:"user"` Email string `json:"email" valid:"required,email" example:"user@opplens.com"` @@ -52,6 +54,7 @@ type CustomerResponse struct { Email string `json:"email"` Status string `json:"status"` Type string `json:"type"` + Role string `json:"role"` Phone *string `json:"phone"` CompanyIDs []string `json:"company_ids"` UpdatedAt int64 `json:"updated_at"` @@ -69,6 +72,7 @@ func (c *Customer) ToResponse(companies []*CompanySummary) *CustomerResponse { Email: c.Email, Type: string(c.Type), Status: string(c.Status), + Role: string(c.Role), Phone: c.Phone, CompanyIDs: c.CompanyIDs, UpdatedAt: c.UpdatedAt, @@ -154,3 +158,14 @@ type ResetPasswordResponse struct { Message string `json:"message" example:"Password reset successfully"` Success bool `json:"success" example:"true"` } + +// AssignRoleForm represents the form for assigning a role to a customer +type AssignRoleForm struct { + Role string `json:"role" valid:"required,in(admin|analyst)" example:"analyst"` +} + +// AssignRoleResponse represents the response for role assignment +type AssignRoleResponse struct { + Message string `json:"message" example:"Role assigned successfully"` + Success bool `json:"success" example:"true"` +} diff --git a/internal/customer/handler.go b/internal/customer/handler.go index 85253e7..9d3c8d2 100644 --- a/internal/customer/handler.go +++ b/internal/customer/handler.go @@ -228,6 +228,43 @@ func (h *Handler) UpdateStatus(c echo.Context) error { }, "Customer status updated successfully") } +// AssignRole assigns a role to a customer (Web Panel) +// @Summary Assign role to customer +// @Description Assign a role (admin or analyst) to a customer +// @Tags Admin-Customers +// @Accept json +// @Produce json +// @Param id path string true "Customer ID" +// @Param role body AssignRoleForm true "Role assignment information" +// @Success 200 {object} response.APIResponse{data=AssignRoleResponse} "Role assigned successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data" +// @Failure 404 {object} response.APIResponse "Not found - Customer not found" +// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/customers/{id}/role [patch] +func (h *Handler) AssignRole(c echo.Context) error { + id := c.Param("id") + + form, err := response.Parse[AssignRoleForm](c) + if err != nil { + return response.ValidationError(c, "Invalid request data", err.Error()) + } + + result, err := h.service.AssignRole(c.Request().Context(), id, form) + if err != nil { + if err.Error() == "customer not found" { + return response.NotFound(c, "Customer not found") + } + if err.Error() == "invalid role. Must be 'admin' or 'analyst'" { + return response.BadRequest(c, err.Error(), "") + } + return response.InternalServerError(c, "Failed to assign role to customer") + } + + return response.Success(c, result, "Role assigned successfully") +} + // ************************************************** // * PROFILE HANDLERS * // ************************************************** diff --git a/internal/customer/service.go b/internal/customer/service.go index dcf2ff2..a744804 100644 --- a/internal/customer/service.go +++ b/internal/customer/service.go @@ -42,6 +42,9 @@ type Service interface { RequestResetPassword(ctx context.Context, form *RequestResetPasswordForm) (*RequestResetPasswordResponse, error) VerifyOTP(ctx context.Context, form *VerifyOTPForm) (*VerifyOTPResponse, error) ResetPassword(ctx context.Context, form *ResetPasswordForm) (*ResetPasswordResponse, error) + + // Role assignment operations + AssignRole(ctx context.Context, customerID string, form *AssignRoleForm) (*AssignRoleResponse, error) } // customerService implements the Service interface @@ -102,6 +105,7 @@ func (s *customerService) Register(ctx context.Context, form *CreateCustomerForm // Create customer customer := &Customer{ Type: CustomerType(form.Type), + Role: CustomerRole(form.Role), Status: CustomerStatusActive, CompanyIDs: companyIDs, FullName: form.FullName, @@ -249,6 +253,11 @@ func (s *customerService) Update(ctx context.Context, id string, form *UpdateCus customer.Phone = form.Phone } + // Update role if provided + if form.Role != "" { + customer.Role = CustomerRole(form.Role) + } + // Update in database err = s.repository.Update(ctx, customer) if err != nil { @@ -509,7 +518,7 @@ func (s *customerService) Login(ctx context.Context, form *LoginForm) (*AuthResp tokenPair, err := s.authService.GenerateTokenPair( customer.ID.Hex(), customer.Email, - "customer", // Role for customers + string(customer.Role), // Use customer's actual role companyID, ) if err != nil { @@ -1055,3 +1064,55 @@ func (s *customerService) loadCompaniesForCustomer(ctx context.Context, customer return companySummaries, nil } + +// AssignRole assigns a role to a customer +func (s *customerService) AssignRole(ctx context.Context, customerID string, form *AssignRoleForm) (*AssignRoleResponse, error) { + s.logger.Info("Assigning role to customer", map[string]interface{}{ + "customer_id": customerID, + "role": form.Role, + }) + + // Get customer to check if exists + customer, err := s.repository.GetByID(ctx, customerID) + if err != nil { + s.logger.Error("Failed to get customer for role assignment", map[string]interface{}{ + "error": err.Error(), + "customer_id": customerID, + }) + return nil, errors.New("customer not found") + } + + // Validate role + role := CustomerRole(form.Role) + if role != CustomerRoleAdmin && role != CustomerRoleAnalyst { + s.logger.Error("Invalid role provided", map[string]interface{}{ + "customer_id": customerID, + "role": form.Role, + }) + return nil, errors.New("invalid role. Must be 'admin' or 'analyst'") + } + + // Update customer role + customer.Role = role + customer.UpdatedAt = time.Now().Unix() + + err = s.repository.Update(ctx, customer) + if err != nil { + s.logger.Error("Failed to update customer role", map[string]interface{}{ + "error": err.Error(), + "customer_id": customerID, + "role": form.Role, + }) + return nil, errors.New("failed to assign role to customer") + } + + s.logger.Info("Role assigned to customer successfully", map[string]interface{}{ + "customer_id": customerID, + "role": form.Role, + }) + + return &AssignRoleResponse{ + Message: "Role assigned successfully", + Success: true, + }, nil +}