package handlers import ( "fmt" "net/http" "git.linuxforward.com/byop/byop-engine/dbstore" "git.linuxforward.com/byop/byop-engine/models" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" "golang.org/x/crypto/bcrypt" ) // UserHandler handles user-related operations type UserHandler struct { Store *dbstore.SQLiteStore Validate *validator.Validate } // NewUserHandler creates a new UserHandler func NewUserHandler(store *dbstore.SQLiteStore) *UserHandler { return &UserHandler{ Store: store, Validate: validator.New(), } } // RegisterUserRoutes registers routes for user operations func (h *UserHandler) RegisterUserRoutes(rg *gin.RouterGroup) { rg.POST("/", h.CreateUser) rg.GET("/:id", h.GetUser) rg.PUT("/:id", h.UpdateUser) rg.DELETE("/:id", h.DeleteUser) rg.GET("/", h.ListUsers) rg.GET("/:id/deployments", h.GetUserDeployments) } // CreateUserInput defines the input for creating a user type CreateUserInput struct { Email string `json:"email" validate:"required,email"` Password string `json:"password" validate:"required,min=8"` Name string `json:"name" validate:"required,min=2"` Role string `json:"role" validate:"omitempty,oneof=user admin editor"` Active *bool `json:"active"` } // CreateUser creates a new user func (h *UserHandler) CreateUser(c *gin.Context) { ctx := c.Request.Context() var input CreateUserInput if err := c.ShouldBindJSON(&input); err != nil { appErr := models.NewErrValidation("invalid_user_input_format", map[string]string{"body": "Invalid request body"}, err) models.RespondWithError(c, appErr) return } if err := h.Validate.StructCtx(ctx, input); err != nil { errors := models.ExtractValidationErrors(err) appErr := models.NewErrValidation("user_validation_failed", errors, err) models.RespondWithError(c, appErr) return } hashedPassword, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost) if err != nil { appErr := models.NewErrInternalServer("password_hash_failed", fmt.Errorf("Failed to hash password: %w", err)) models.RespondWithError(c, appErr) return } userRole := models.RoleUser if input.Role != "" { userRole = input.Role } userActive := true if input.Active != nil { userActive = *input.Active } user := models.User{ Email: input.Email, Password: string(hashedPassword), Name: input.Name, Role: userRole, Active: userActive, } err = h.Store.CreateUser(ctx, &user) if err != nil { if models.IsErrConflict(err) { models.RespondWithError(c, err) return } appErr := models.NewErrInternalServer("failed_to_create_user", fmt.Errorf("Failed to create user: %w", err)) models.RespondWithError(c, appErr) return } // GORM automatically sets the ID after creation // Clear the password before sending the response createdUser := user createdUser.Password = "" c.JSON(http.StatusCreated, createdUser) } // GetUser retrieves a user by ID func (h *UserHandler) GetUser(c *gin.Context) { ctx := c.Request.Context() id, err := parseUintID(c, "id") if err != nil { models.RespondWithError(c, err) return } user, err := h.Store.GetUserByID(ctx, id) if err != nil { models.RespondWithError(c, err) return } user.Password = "" c.JSON(http.StatusOK, user) } // UpdateUserInput defines the input for updating a user type UpdateUserInput struct { Email *string `json:"email,omitempty" validate:"omitempty,email"` Password *string `json:"password,omitempty" validate:"omitempty,min=8"` Name *string `json:"name,omitempty" validate:"omitempty,min=2"` Role *string `json:"role,omitempty" validate:"omitempty,oneof=user admin editor"` Active *bool `json:"active,omitempty"` } // UpdateUser updates an existing user func (h *UserHandler) UpdateUser(c *gin.Context) { ctx := c.Request.Context() id, err := parseUintID(c, "id") if err != nil { models.RespondWithError(c, err) return } var input UpdateUserInput if err := c.ShouldBindJSON(&input); err != nil { appErr := models.NewErrValidation("invalid_update_user_input_format", map[string]string{"body": "Invalid request body"}, err) models.RespondWithError(c, appErr) return } if err := h.Validate.StructCtx(ctx, input); err != nil { errors := models.ExtractValidationErrors(err) appErr := models.NewErrValidation("update_user_validation_failed", errors, err) models.RespondWithError(c, appErr) return } user, err := h.Store.GetUserByID(ctx, id) if err != nil { models.RespondWithError(c, err) return } updated := false if input.Email != nil { user.Email = *input.Email updated = true } if input.Password != nil { hashedPassword, err := bcrypt.GenerateFromPassword([]byte(*input.Password), bcrypt.DefaultCost) if err != nil { appErr := models.NewErrInternalServer("update_password_hash_failed", fmt.Errorf("Failed to hash new password: %w", err)) models.RespondWithError(c, appErr) return } user.Password = string(hashedPassword) updated = true } if input.Name != nil { user.Name = *input.Name updated = true } if input.Role != nil { user.Role = *input.Role updated = true } if input.Active != nil { user.Active = *input.Active updated = true } if !updated { user.Password = "" c.JSON(http.StatusOK, user) return } if err := h.Store.UpdateUser(ctx, user); err != nil { models.RespondWithError(c, err) return } user.Password = "" c.JSON(http.StatusOK, user) } // DeleteUser deletes a user by ID func (h *UserHandler) DeleteUser(c *gin.Context) { ctx := c.Request.Context() id, err := parseUintID(c, "id") if err != nil { models.RespondWithError(c, err) return } if err := h.Store.DeleteUser(ctx, id); err != nil { models.RespondWithError(c, err) return } c.Status(http.StatusNoContent) } // ListUsers retrieves all users func (h *UserHandler) ListUsers(c *gin.Context) { ctx := c.Request.Context() users, err := h.Store.GetUsers(ctx) if err != nil { appErr := models.NewErrInternalServer("failed_to_list_users", fmt.Errorf("Failed to list users: %w", err)) models.RespondWithError(c, appErr) return } for i := range users { users[i].Password = "" } c.JSON(http.StatusOK, users) } // GetUserDeployments returns all deployments for a specific user func (h *UserHandler) GetUserDeployments(c *gin.Context) { ctx := c.Request.Context() userID, err := parseUintID(c, "id") if err != nil { models.RespondWithError(c, err) return } _, err = h.Store.GetUserByID(ctx, userID) if err != nil { models.RespondWithError(c, err) return } deployments, err := h.Store.GetDeploymentsByUserID(ctx, userID) if err != nil { appErr := models.NewErrInternalServer("failed_to_get_user_deployments", fmt.Errorf("Failed to get deployments for user %d: %w", userID, err)) models.RespondWithError(c, appErr) return } if deployments == nil { deployments = []*models.Deployment{} } c.JSON(http.StatusOK, deployments) }