package handler import ( "baron-sso-backend/internal/domain" "errors" "strings" "time" "github.com/gofiber/fiber/v2" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) type UserHandler struct { DB *gorm.DB } func NewUserHandler(db *gorm.DB) *UserHandler { return &UserHandler{DB: db} } type userSummary struct { ID string `json:"id"` Email string `json:"email"` Name string `json:"name"` Phone string `json:"phone"` Role string `json:"role"` Status string `json:"status"` CompanyCode string `json:"companyCode"` Department string `json:"department"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` } type userListResponse struct { Items []userSummary `json:"items"` Limit int `json:"limit"` Offset int `json:"offset"` Total int64 `json:"total"` } func (h *UserHandler) ListUsers(c *fiber.Ctx) error { if h.DB == nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"}) } limit := c.QueryInt("limit", 50) offset := c.QueryInt("offset", 0) search := strings.TrimSpace(c.Query("search")) if limit <= 0 { limit = 50 } if offset < 0 { offset = 0 } query := h.DB.Model(&domain.User{}) if search != "" { like := "%" + search + "%" query = query.Where("email ILIKE ? OR name ILIKE ?", like, like) } var total int64 if err := query.Count(&total).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } var users []domain.User if err := query.Order("created_at desc").Limit(limit).Offset(offset).Find(&users).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } items := make([]userSummary, 0, len(users)) for _, u := range users { items = append(items, mapUserSummary(u)) } return c.JSON(userListResponse{Items: items, Limit: limit, Offset: offset, Total: total}) } func (h *UserHandler) GetUser(c *fiber.Ctx) error { if h.DB == nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"}) } userID := strings.TrimSpace(c.Params("id")) if userID == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "user id is required"}) } var user domain.User if err := h.DB.First(&user, "id = ?", userID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "user not found"}) } return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(mapUserSummary(user)) } func (h *UserHandler) CreateUser(c *fiber.Ctx) error { if h.DB == nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"}) } var req struct { Email string `json:"email"` Password string `json:"password"` Name string `json:"name"` Phone string `json:"phone"` Role string `json:"role"` CompanyCode string `json:"companyCode"` Department string `json:"department"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) } email := strings.TrimSpace(req.Email) if email == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "email is required"}) } password := req.Password if password == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "password is required"}) } name := strings.TrimSpace(req.Name) if name == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "name is required"}) } // Check duplicates var exists domain.User if err := h.DB.Where("email = ?", email).First(&exists).Error; err == nil { return c.Status(fiber.StatusConflict).JSON(fiber.Map{"error": "email already exists"}) } hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to hash password"}) } user := domain.User{ Email: email, PasswordHash: string(hashedPassword), Name: name, Phone: req.Phone, Role: req.Role, // default "user" handled by GORM if empty, but struct default usually works on zero value? GORM default tag works for zero value. CompanyCode: req.CompanyCode, Department: req.Department, Status: "active", AffiliationType: "internal", // Defaulting for now } if user.Role == "" { user.Role = "user" } if err := h.DB.Create(&user).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.Status(fiber.StatusCreated).JSON(mapUserSummary(user)) } func (h *UserHandler) UpdateUser(c *fiber.Ctx) error { if h.DB == nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"}) } userID := strings.TrimSpace(c.Params("id")) if userID == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "user id is required"}) } var user domain.User if err := h.DB.First(&user, "id = ?", userID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "user not found"}) } return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } var req struct { Password *string `json:"password"` Name *string `json:"name"` Phone *string `json:"phone"` Role *string `json:"role"` Status *string `json:"status"` CompanyCode *string `json:"companyCode"` Department *string `json:"department"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) } if req.Name != nil { user.Name = strings.TrimSpace(*req.Name) } if req.Phone != nil { user.Phone = strings.TrimSpace(*req.Phone) } if req.Role != nil { user.Role = strings.TrimSpace(*req.Role) } if req.Status != nil { status := strings.ToLower(strings.TrimSpace(*req.Status)) if status == "active" || status == "inactive" || status == "blocked" { user.Status = status } } if req.CompanyCode != nil { user.CompanyCode = strings.TrimSpace(*req.CompanyCode) } if req.Department != nil { user.Department = strings.TrimSpace(*req.Department) } if req.Password != nil && *req.Password != "" { hashedPassword, err := bcrypt.GenerateFromPassword([]byte(*req.Password), bcrypt.DefaultCost) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to hash password"}) } user.PasswordHash = string(hashedPassword) } if err := h.DB.Save(&user).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(mapUserSummary(user)) } func (h *UserHandler) DeleteUser(c *fiber.Ctx) error { if h.DB == nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"}) } userID := strings.TrimSpace(c.Params("id")) if userID == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "user id is required"}) } // Soft delete if err := h.DB.Delete(&domain.User{}, "id = ?", userID).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.SendStatus(fiber.StatusNoContent) } func mapUserSummary(u domain.User) userSummary { return userSummary{ ID: u.ID, Email: u.Email, Name: u.Name, Phone: u.Phone, Role: u.Role, Status: u.Status, CompanyCode: u.CompanyCode, Department: u.Department, CreatedAt: u.CreatedAt.Format(time.RFC3339), UpdatedAt: u.UpdatedAt.Format(time.RFC3339), } }