package handlers import ( "aggios-app/backend/internal/api/middleware" "aggios-app/backend/internal/domain" "aggios-app/backend/internal/repository" "aggios-app/backend/internal/service" "encoding/json" "log" "net/http" "time" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" ) // CollaboratorHandler handles agency collaborator management type CollaboratorHandler struct { userRepo *repository.UserRepository agencyServ *service.AgencyService } // NewCollaboratorHandler creates a new collaborator handler func NewCollaboratorHandler(userRepo *repository.UserRepository, agencyServ *service.AgencyService) *CollaboratorHandler { return &CollaboratorHandler{ userRepo: userRepo, agencyServ: agencyServ, } } // AddCollaboratorRequest representa a requisição para adicionar um colaborador type AddCollaboratorRequest struct { Email string `json:"email"` Name string `json:"name"` } // CollaboratorResponse representa um colaborador type CollaboratorResponse struct { ID string `json:"id"` Email string `json:"email"` Name string `json:"name"` AgencyRole string `json:"agency_role"` // owner ou collaborator CreatedAt time.Time `json:"created_at"` CollaboratorCreatedAt *time.Time `json:"collaborator_created_at,omitempty"` } // ListCollaborators lista todos os colaboradores da agência (apenas owner pode ver) func (h *CollaboratorHandler) ListCollaborators(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } ownerID, _ := r.Context().Value(middleware.UserIDKey).(string) tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string) agencyRole, _ := r.Context().Value("agency_role").(string) // Apenas owner pode listar colaboradores if agencyRole != "owner" { log.Printf("❌ COLLABORATOR ACCESS BLOCKED: User %s tried to list collaborators", ownerID) http.Error(w, "Only agency owners can manage collaborators", http.StatusForbidden) return } // Buscar todos os usuários da agência tenantUUID := parseUUID(tenantID) if tenantUUID == nil { http.Error(w, "Invalid tenant ID", http.StatusBadRequest) return } users, err := h.userRepo.ListByTenantID(*tenantUUID) if err != nil { log.Printf("Error fetching collaborators: %v", err) http.Error(w, "Error fetching collaborators", http.StatusInternalServerError) return } // Formatar resposta collaborators := make([]CollaboratorResponse, 0) for _, user := range users { collaborators = append(collaborators, CollaboratorResponse{ ID: user.ID.String(), Email: user.Email, Name: user.Name, AgencyRole: user.AgencyRole, CreatedAt: user.CreatedAt, CollaboratorCreatedAt: user.CollaboratorCreatedAt, }) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "collaborators": collaborators, }) } // InviteCollaborator convida um novo colaborador para a agência (apenas owner pode fazer isso) func (h *CollaboratorHandler) InviteCollaborator(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } ownerID, _ := r.Context().Value(middleware.UserIDKey).(string) tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string) agencyRole, _ := r.Context().Value("agency_role").(string) // Apenas owner pode convidar colaboradores if agencyRole != "owner" { log.Printf("❌ COLLABORATOR INVITE BLOCKED: User %s tried to invite collaborator", ownerID) http.Error(w, "Only agency owners can invite collaborators", http.StatusForbidden) return } var req AddCollaboratorRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } // Validar email if req.Email == "" { http.Error(w, "Email is required", http.StatusBadRequest) return } // Validar se email já existe exists, err := h.userRepo.EmailExists(req.Email) if err != nil { log.Printf("Error checking email: %v", err) http.Error(w, "Error processing request", http.StatusInternalServerError) return } if exists { http.Error(w, "Email already registered", http.StatusConflict) return } // Gerar senha temporária (8 caracteres aleatórios) tempPassword := generateTempPassword() hashedPassword, err := bcrypt.GenerateFromPassword([]byte(tempPassword), bcrypt.DefaultCost) if err != nil { log.Printf("Error hashing password: %v", err) http.Error(w, "Error processing request", http.StatusInternalServerError) return } // Criar novo colaborador ownerUUID := parseUUID(ownerID) tenantUUID := parseUUID(tenantID) now := time.Now() collaborator := &domain.User{ TenantID: tenantUUID, Email: req.Email, Password: string(hashedPassword), Name: req.Name, Role: "ADMIN_AGENCIA", AgencyRole: "collaborator", CreatedBy: ownerUUID, CollaboratorCreatedAt: &now, } if err := h.userRepo.Create(collaborator); err != nil { log.Printf("Error creating collaborator: %v", err) http.Error(w, "Error creating collaborator", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(map[string]interface{}{ "message": "Collaborator invited successfully", "temporary_password": tempPassword, "collaborator": CollaboratorResponse{ ID: collaborator.ID.String(), Email: collaborator.Email, Name: collaborator.Name, AgencyRole: collaborator.AgencyRole, CreatedAt: collaborator.CreatedAt, CollaboratorCreatedAt: collaborator.CollaboratorCreatedAt, }, }) } // RemoveCollaborator remove um colaborador da agência (apenas owner pode fazer isso) func (h *CollaboratorHandler) RemoveCollaborator(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } ownerID, _ := r.Context().Value(middleware.UserIDKey).(string) tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string) agencyRole, _ := r.Context().Value("agency_role").(string) // Apenas owner pode remover colaboradores if agencyRole != "owner" { log.Printf("❌ COLLABORATOR REMOVE BLOCKED: User %s tried to remove collaborator", ownerID) http.Error(w, "Only agency owners can remove collaborators", http.StatusForbidden) return } collaboratorID := r.URL.Query().Get("id") if collaboratorID == "" { http.Error(w, "Collaborator ID is required", http.StatusBadRequest) return } // Converter ID para UUID collaboratorUUID := parseUUID(collaboratorID) if collaboratorUUID == nil { http.Error(w, "Invalid collaborator ID", http.StatusBadRequest) return } // Buscar o colaborador collaborator, err := h.userRepo.GetByID(*collaboratorUUID) if err != nil { http.Error(w, "Collaborator not found", http.StatusNotFound) return } // Verificar se o colaborador pertence à mesma agência if collaborator.TenantID == nil || collaborator.TenantID.String() != tenantID { http.Error(w, "Collaborator not found in this agency", http.StatusForbidden) return } // Não permitir remover o owner if collaborator.AgencyRole == "owner" { http.Error(w, "Cannot remove the agency owner", http.StatusBadRequest) return } // Remover colaborador if err := h.userRepo.Delete(*collaboratorUUID); err != nil { log.Printf("Error removing collaborator: %v", err) http.Error(w, "Error removing collaborator", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{ "message": "Collaborator removed successfully", }) } // generateTempPassword gera uma senha temporária func generateTempPassword() string { const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*" return randomString(12, charset) } // randomString gera uma string aleatória func randomString(length int, charset string) string { b := make([]byte, length) for i := range b { b[i] = charset[i%len(charset)] } return string(b) } // parseUUID converte string para UUID func parseUUID(s string) *uuid.UUID { u, err := uuid.Parse(s) if err != nil { return nil } return &u }