package handlers import ( "aggios-app/backend/internal/domain" "aggios-app/backend/internal/repository" "aggios-app/backend/internal/service" "aggios-app/backend/internal/config" "aggios-app/backend/internal/api/middleware" "context" "database/sql" "encoding/json" "fmt" "log" "net/http" "path/filepath" "strings" "time" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" "golang.org/x/crypto/bcrypt" ) type CustomerPortalHandler struct { crmRepo *repository.CRMRepository authService *service.AuthService cfg *config.Config minioClient *minio.Client } func NewCustomerPortalHandler(crmRepo *repository.CRMRepository, authService *service.AuthService, cfg *config.Config) *CustomerPortalHandler { // Initialize MinIO client minioClient, err := minio.New(cfg.Minio.Endpoint, &minio.Options{ Creds: credentials.NewStaticV4(cfg.Minio.RootUser, cfg.Minio.RootPassword, ""), Secure: cfg.Minio.UseSSL, }) if err != nil { log.Printf("❌ Failed to create MinIO client for CustomerPortalHandler: %v", err) } return &CustomerPortalHandler{ crmRepo: crmRepo, authService: authService, cfg: cfg, minioClient: minioClient, } } // CustomerLoginRequest representa a requisição de login do cliente type CustomerLoginRequest struct { Email string `json:"email"` Password string `json:"password"` } // CustomerLoginResponse representa a resposta de login do cliente type CustomerLoginResponse struct { Token string `json:"token"` Customer *CustomerPortalInfo `json:"customer"` } // CustomerPortalInfo representa informações seguras do cliente para o portal type CustomerPortalInfo struct { ID string `json:"id"` Name string `json:"name"` Email string `json:"email"` Company string `json:"company"` HasPortalAccess bool `json:"has_portal_access"` TenantID string `json:"tenant_id"` } // Login autentica um cliente e retorna um token JWT func (h *CustomerPortalHandler) Login(w http.ResponseWriter, r *http.Request) { var req CustomerLoginRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]string{ "error": "Invalid request body", }) return } // Validar entrada if req.Email == "" || req.Password == "" { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]string{ "error": "Email e senha são obrigatórios", }) return } // Buscar cliente por email customer, err := h.crmRepo.GetCustomerByEmail(req.Email) if err != nil { if err == sql.ErrNoRows { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{ "error": "Credenciais inválidas", }) return } log.Printf("Error fetching customer: %v", err) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]string{ "error": "Erro ao processar login", }) return } // Verificar se tem acesso ao portal if !customer.HasPortalAccess { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusForbidden) json.NewEncoder(w).Encode(map[string]string{ "error": "Acesso ao portal não autorizado. Entre em contato com o administrador.", }) return } // Verificar senha if err := bcrypt.CompareHashAndPassword([]byte(customer.PasswordHash), []byte(req.Password)); err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{ "error": "Credenciais inválidas", }) return } // Atualizar último login if err := h.crmRepo.UpdateCustomerLastLogin(customer.ID); err != nil { log.Printf("Warning: Failed to update last login for customer %s: %v", customer.ID, err) } // Gerar token JWT token, err := h.authService.GenerateCustomerToken(customer.ID, customer.TenantID, customer.Email) if err != nil { log.Printf("Error generating token: %v", err) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]string{ "error": "Erro ao gerar token de autenticação", }) return } // Resposta de sucesso w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(CustomerLoginResponse{ Token: token, Customer: &CustomerPortalInfo{ ID: customer.ID, Name: customer.Name, Email: customer.Email, Company: customer.Company, HasPortalAccess: customer.HasPortalAccess, TenantID: customer.TenantID, }, }) } // GetPortalDashboard retorna dados do dashboard para o cliente autenticado func (h *CustomerPortalHandler) GetPortalDashboard(w http.ResponseWriter, r *http.Request) { customerID, _ := r.Context().Value(middleware.CustomerIDKey).(string) tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string) // Buscar leads do cliente leads, err := h.crmRepo.GetLeadsByCustomerID(customerID) if err != nil { log.Printf("Error fetching leads: %v", err) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]string{ "error": "Erro ao buscar leads", }) return } // Buscar informações do cliente customer, err := h.crmRepo.GetCustomerByID(customerID, tenantID) if err != nil { log.Printf("Error fetching customer: %v", err) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]string{ "error": "Erro ao buscar informações do cliente", }) return } // Calcular estatísticas rawStats := calculateLeadStats(leads) stats := map[string]interface{}{ "total_leads": rawStats["total"], "active_leads": rawStats["novo"].(int) + rawStats["qualificado"].(int) + rawStats["negociacao"].(int), "converted": rawStats["convertido"], } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "customer": CustomerPortalInfo{ ID: customer.ID, Name: customer.Name, Email: customer.Email, Company: customer.Company, HasPortalAccess: customer.HasPortalAccess, TenantID: customer.TenantID, }, "leads": leads, "stats": stats, }) } // GetPortalLeads retorna apenas os leads do cliente func (h *CustomerPortalHandler) GetPortalLeads(w http.ResponseWriter, r *http.Request) { customerID, _ := r.Context().Value(middleware.CustomerIDKey).(string) leads, err := h.crmRepo.GetLeadsByCustomerID(customerID) if err != nil { log.Printf("Error fetching leads: %v", err) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]string{ "error": "Erro ao buscar leads", }) return } if leads == nil { leads = []domain.CRMLead{} } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "leads": leads, }) } // GetPortalLists retorna as listas que possuem leads do cliente func (h *CustomerPortalHandler) GetPortalLists(w http.ResponseWriter, r *http.Request) { customerID, _ := r.Context().Value(middleware.CustomerIDKey).(string) lists, err := h.crmRepo.GetListsByCustomerID(customerID) if err != nil { log.Printf("Error fetching portal lists: %v", err) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]string{ "error": "Erro ao buscar listas", }) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "lists": lists, }) } // GetPortalProfile retorna o perfil completo do cliente func (h *CustomerPortalHandler) GetPortalProfile(w http.ResponseWriter, r *http.Request) { customerID, _ := r.Context().Value(middleware.CustomerIDKey).(string) tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string) // Buscar informações do cliente customer, err := h.crmRepo.GetCustomerByID(customerID, tenantID) if err != nil { log.Printf("Error fetching customer: %v", err) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]string{ "error": "Erro ao buscar perfil", }) return } // Buscar leads para estatísticas leads, err := h.crmRepo.GetLeadsByCustomerID(customerID) if err != nil { log.Printf("Error fetching leads for stats: %v", err) leads = []domain.CRMLead{} } // Calcular estatísticas stats := calculateLeadStats(leads) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "customer": map[string]interface{}{ "id": customer.ID, "name": customer.Name, "email": customer.Email, "phone": customer.Phone, "company": customer.Company, "logo_url": customer.LogoURL, "portal_last_login": customer.PortalLastLogin, "created_at": customer.CreatedAt, "total_leads": len(leads), "converted_leads": stats["convertido"].(int), }, }) } // ChangePasswordRequest representa a requisição de troca de senha type CustomerChangePasswordRequest struct { CurrentPassword string `json:"current_password"` NewPassword string `json:"new_password"` } // ChangePassword altera a senha do cliente func (h *CustomerPortalHandler) ChangePassword(w http.ResponseWriter, r *http.Request) { customerID, _ := r.Context().Value(middleware.CustomerIDKey).(string) tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string) var req CustomerChangePasswordRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]string{ "error": "Invalid request body", }) return } // Validar entrada if req.CurrentPassword == "" || req.NewPassword == "" { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]string{ "error": "Senha atual e nova senha são obrigatórias", }) return } if len(req.NewPassword) < 6 { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]string{ "error": "A nova senha deve ter no mínimo 6 caracteres", }) return } // Buscar cliente customer, err := h.crmRepo.GetCustomerByID(customerID, tenantID) if err != nil { log.Printf("Error fetching customer: %v", err) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]string{ "error": "Erro ao processar solicitação", }) return } // Verificar senha atual if err := bcrypt.CompareHashAndPassword([]byte(customer.PasswordHash), []byte(req.CurrentPassword)); err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{ "error": "Senha atual incorreta", }) return } // Gerar hash da nova senha hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost) if err != nil { log.Printf("Error hashing password: %v", err) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]string{ "error": "Erro ao processar nova senha", }) return } // Atualizar senha no banco if err := h.crmRepo.UpdateCustomerPassword(customerID, string(hashedPassword)); err != nil { log.Printf("Error updating password: %v", err) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]string{ "error": "Erro ao atualizar senha", }) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{ "message": "Senha alterada com sucesso", }) } // UploadLogo faz o upload do logo do cliente func (h *CustomerPortalHandler) UploadLogo(w http.ResponseWriter, r *http.Request) { customerID, _ := r.Context().Value(middleware.CustomerIDKey).(string) tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string) if h.minioClient == nil { http.Error(w, "Storage service unavailable", http.StatusServiceUnavailable) return } // Parse multipart form (2MB max) const maxLogoSize = 2 * 1024 * 1024 if err := r.ParseMultipartForm(maxLogoSize); err != nil { http.Error(w, "File too large", http.StatusBadRequest) return } file, header, err := r.FormFile("logo") if err != nil { http.Error(w, "Failed to read file", http.StatusBadRequest) return } defer file.Close() // Validate file type contentType := header.Header.Get("Content-Type") if !strings.HasPrefix(contentType, "image/") { http.Error(w, "Only images are allowed", http.StatusBadRequest) return } // Generate unique filename ext := filepath.Ext(header.Filename) if ext == "" { ext = ".png" // Default extension } filename := fmt.Sprintf("logo-%d%s", time.Now().Unix(), ext) objectPath := fmt.Sprintf("customers/%s/%s", customerID, filename) // Upload to MinIO ctx := context.Background() bucketName := h.cfg.Minio.BucketName _, err = h.minioClient.PutObject(ctx, bucketName, objectPath, file, header.Size, minio.PutObjectOptions{ ContentType: contentType, }) if err != nil { log.Printf("Error uploading to MinIO: %v", err) http.Error(w, "Failed to upload file", http.StatusInternalServerError) return } // Generate public URL logoURL := fmt.Sprintf("%s/api/files/%s/%s", h.cfg.Minio.PublicURL, bucketName, objectPath) // Update customer in database err = h.crmRepo.UpdateCustomerLogo(customerID, tenantID, logoURL) if err != nil { log.Printf("Error updating customer logo in DB: %v", err) http.Error(w, "Failed to update profile", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{ "logo_url": logoURL, }) }