package handlers import ( "encoding/json" "io" "log" "net/http" "strings" "aggios-app/backend/internal/api/middleware" "aggios-app/backend/internal/domain" "aggios-app/backend/internal/service" ) // AuthHandler handles authentication endpoints type AuthHandler struct { authService *service.AuthService } // NewAuthHandler creates a new auth handler func NewAuthHandler(authService *service.AuthService) *AuthHandler { return &AuthHandler{ authService: authService, } } // Register handles user registration func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req domain.CreateUserRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } user, err := h.authService.Register(req) if err != nil { switch err { case service.ErrEmailAlreadyExists: http.Error(w, err.Error(), http.StatusConflict) case service.ErrWeakPassword: http.Error(w, err.Error(), http.StatusBadRequest) default: http.Error(w, "Internal server error", http.StatusInternalServerError) } return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(user) } // Login handles user login func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { log.Printf("🔐 LOGIN HANDLER CALLED - Method: %s", r.Method) if r.Method != http.MethodPost { log.Printf("❌ Method not allowed: %s", r.Method) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } bodyBytes, err := io.ReadAll(r.Body) if err != nil { log.Printf("❌ Failed to read body: %v", err) http.Error(w, "Failed to read request body", http.StatusBadRequest) return } defer r.Body.Close() log.Printf("đŸ“„ Raw body: %s", string(bodyBytes)) // Trim whitespace to avoid decode errors caused by BOM or stray chars sanitized := strings.TrimSpace(string(bodyBytes)) var req domain.LoginRequest if err := json.Unmarshal([]byte(sanitized), &req); err != nil { log.Printf("❌ JSON parse error: %v", err) http.Error(w, "Invalid request body", http.StatusBadRequest) return } log.Printf("📧 Login attempt for email: %s", req.Email) response, err := h.authService.Login(req) if err != nil { log.Printf("❌ authService.Login error: %v", err) if err == service.ErrInvalidCredentials { http.Error(w, err.Error(), http.StatusUnauthorized) } else { http.Error(w, "Internal server error", http.StatusInternalServerError) } return } // VALIDAÇÃO DE SEGURANÇA: Verificar se o tenant do usuĂĄrio corresponde ao subdomain acessado tenantIDFromContext := "" if ctxTenantID := r.Context().Value(middleware.TenantIDKey); ctxTenantID != nil { tenantIDFromContext, _ = ctxTenantID.(string) } // Se foi detectado um tenant no contexto (nĂŁo Ă© superadmin ou site institucional) if tenantIDFromContext != "" && response.User.TenantID != nil { userTenantID := response.User.TenantID.String() if userTenantID != tenantIDFromContext { log.Printf("❌ LOGIN BLOCKED: User from tenant %s tried to login in tenant %s subdomain", userTenantID, tenantIDFromContext) http.Error(w, "Forbidden: Invalid credentials for this tenant", http.StatusForbidden) return } log.Printf("✅ TENANT LOGIN VALIDATION PASSED: %s", userTenantID) } log.Printf("✅ Login successful for %s, role=%s", response.User.Email, response.User.Role) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // ChangePasswordRequest represents a password change request type ChangePasswordRequest struct { CurrentPassword string `json:"currentPassword"` NewPassword string `json:"newPassword"` } // ChangePassword handles password change func (h *AuthHandler) ChangePassword(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Get user ID from context (set by auth middleware) userID, ok := r.Context().Value("userID").(string) if !ok { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } var req ChangePasswordRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } if req.CurrentPassword == "" || req.NewPassword == "" { http.Error(w, "Current password and new password are required", http.StatusBadRequest) return } // Call auth service to change password if err := h.authService.ChangePassword(userID, req.CurrentPassword, req.NewPassword); err != nil { if err == service.ErrInvalidCredentials { http.Error(w, "Current password is incorrect", http.StatusUnauthorized) } else if err == service.ErrWeakPassword { http.Error(w, "New password is too weak", http.StatusBadRequest) } else { http.Error(w, "Error changing password", http.StatusInternalServerError) } return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{ "message": "Password changed successfully", }) } // UnifiedLogin handles login for all user types (agency, customer, superadmin) func (h *AuthHandler) UnifiedLogin(w http.ResponseWriter, r *http.Request) { log.Printf("🔐 UNIFIED LOGIN HANDLER CALLED - Method: %s", r.Method) if r.Method != http.MethodPost { log.Printf("❌ Method not allowed: %s", r.Method) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } bodyBytes, err := io.ReadAll(r.Body) if err != nil { log.Printf("❌ Failed to read body: %v", err) http.Error(w, "Failed to read request body", http.StatusBadRequest) return } defer r.Body.Close() log.Printf("đŸ“„ Raw body: %s", string(bodyBytes)) sanitized := strings.TrimSpace(string(bodyBytes)) var req domain.UnifiedLoginRequest if err := json.Unmarshal([]byte(sanitized), &req); err != nil { log.Printf("❌ JSON parse error: %v", err) http.Error(w, "Invalid request body", http.StatusBadRequest) return } log.Printf("📧 Unified login attempt for email: %s", req.Email) response, err := h.authService.UnifiedLogin(req) if err != nil { log.Printf("❌ authService.UnifiedLogin error: %v", err) if err == service.ErrInvalidCredentials || strings.Contains(err.Error(), "nĂŁo autorizado") { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{ "error": err.Error(), }) } else { http.Error(w, "Internal server error", http.StatusInternalServerError) } return } // VALIDAÇÃO DE SEGURANÇA: Verificar se o tenant corresponde ao subdomain acessado tenantIDFromContext := "" if ctxTenantID := r.Context().Value(middleware.TenantIDKey); ctxTenantID != nil { tenantIDFromContext, _ = ctxTenantID.(string) } // Se foi detectado um tenant no contexto E o usuĂĄrio tem tenant if tenantIDFromContext != "" && response.TenantID != "" { if response.TenantID != tenantIDFromContext { log.Printf("❌ LOGIN BLOCKED: User from tenant %s tried to login in tenant %s subdomain", response.TenantID, tenantIDFromContext) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusForbidden) json.NewEncoder(w).Encode(map[string]string{ "error": "Credenciais invĂĄlidas para esta agĂȘncia", }) return } log.Printf("✅ TENANT LOGIN VALIDATION PASSED: %s", response.TenantID) } log.Printf("✅ Unified login successful: email=%s, type=%s, role=%s", response.Email, response.UserType, response.Role) // Montar resposta compatĂ­vel com frontend antigo E com novos campos compatibleResponse := map[string]interface{}{ "token": response.Token, "user": map[string]interface{}{ "id": response.UserID, "email": response.Email, "name": response.Name, "role": response.Role, "tenant_id": response.TenantID, "user_type": response.UserType, }, // Campos adicionais do sistema unificado "user_type": response.UserType, "user_id": response.UserID, "subdomain": response.Subdomain, "tenant_id": response.TenantID, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(compatibleResponse) }