package main import ( "database/sql" "encoding/json" "fmt" "log" "net/http" "os" "strings" "time" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" _ "github.com/lib/pq" "golang.org/x/crypto/bcrypt" ) var db *sql.DB // jwtSecret carrega o secret do ambiente ou usa fallback (NUNCA use fallback em produção) var jwtSecret = []byte(getEnvOrDefault("JWT_SECRET", "INSECURE-fallback-secret-CHANGE-THIS")) // Rate limiting simples (IP -> timestamp das últimas tentativas) var loginAttempts = make(map[string][]time.Time) var registerAttempts = make(map[string][]time.Time) const maxAttemptsPerMinute = 5 // corsMiddleware adiciona headers CORS func corsMiddleware(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // CORS - apenas domínios permitidos allowedOrigins := map[string]bool{ "http://localhost": true, // Dev local "http://dash.localhost": true, // Dashboard dev "http://aggios.local": true, // Institucional dev "http://dash.aggios.local": true, // Dashboard dev alternativo "https://aggios.app": true, // Institucional prod "https://dash.aggios.app": true, // Dashboard prod "https://www.aggios.app": true, // Institucional prod www } origin := r.Header.Get("Origin") if allowedOrigins[origin] { w.Header().Set("Access-Control-Allow-Origin", origin) } w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") w.Header().Set("Access-Control-Allow-Credentials", "true") // Headers de segurança w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("X-XSS-Protection", "1; mode=block") w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") // Handle preflight if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } // Log da requisição (sem dados sensíveis) log.Printf("📥 %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr) next(w, r) } } // RegisterRequest representa os dados completos de registro type RegisterRequest struct { // Step 1 - Dados Pessoais Email string `json:"email"` Password string `json:"password"` FullName string `json:"fullName"` Newsletter bool `json:"newsletter"` // Step 2 - Empresa CompanyName string `json:"companyName"` CNPJ string `json:"cnpj"` RazaoSocial string `json:"razaoSocial"` Description string `json:"description"` Website string `json:"website"` Industry string `json:"industry"` TeamSize string `json:"teamSize"` // Step 3 - Localização CEP string `json:"cep"` State string `json:"state"` City string `json:"city"` Neighborhood string `json:"neighborhood"` Street string `json:"street"` Number string `json:"number"` Complement string `json:"complement"` Contacts []struct { ID int `json:"id"` WhatsApp string `json:"whatsapp"` } `json:"contacts"` // Step 4 - Domínio Subdomain string `json:"subdomain"` // Step 5 - Personalização PrimaryColor string `json:"primaryColor"` SecondaryColor string `json:"secondaryColor"` LogoURL string `json:"logoUrl"` } // RegisterResponse representa a resposta do registro type RegisterResponse struct { Token string `json:"token"` ID string `json:"id"` Email string `json:"email"` Name string `json:"name"` TenantID string `json:"tenantId"` Company string `json:"company"` Subdomain string `json:"subdomain"` CreatedAt string `json:"created_at"` } // ErrorResponse representa uma resposta de erro type ErrorResponse struct { Error string `json:"error"` Message string `json:"message"` } // LoginRequest representa os dados de login type LoginRequest struct { Email string `json:"email"` Password string `json:"password"` } // LoginResponse representa a resposta do login type LoginResponse struct { Token string `json:"token"` User UserPayload `json:"user"` } // UserPayload representa os dados do usuário no token type UserPayload struct { ID string `json:"id"` Email string `json:"email"` Name string `json:"name"` TenantID string `json:"tenantId"` Company string `json:"company"` Subdomain string `json:"subdomain"` } // Claims customizado para JWT type Claims struct { UserID string `json:"userId"` Email string `json:"email"` TenantID string `json:"tenantId"` jwt.RegisteredClaims } // getEnvOrDefault retorna variável de ambiente ou valor padrão func getEnvOrDefault(key, defaultValue string) string { if value := os.Getenv(key); value != "" { return value } return defaultValue } // checkRateLimit verifica se IP excedeu limite de tentativas func checkRateLimit(ip string, attempts map[string][]time.Time) bool { now := time.Now() cutoff := now.Add(-1 * time.Minute) // Limpar tentativas antigas if timestamps, exists := attempts[ip]; exists { var recent []time.Time for _, t := range timestamps { if t.After(cutoff) { recent = append(recent, t) } } attempts[ip] = recent // Verificar se excedeu limite if len(recent) >= maxAttemptsPerMinute { return false } } // Adicionar nova tentativa attempts[ip] = append(attempts[ip], now) return true } // validateEmail valida formato de email func validateEmail(email string) bool { if len(email) < 3 || len(email) > 254 { return false } // Regex simples para validação return strings.Contains(email, "@") && strings.Contains(email, ".") } func initDB() error { connStr := fmt.Sprintf( "host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", os.Getenv("DB_HOST"), os.Getenv("DB_PORT"), os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"), os.Getenv("DB_NAME"), ) var err error db, err = sql.Open("postgres", connStr) if err != nil { return fmt.Errorf("erro ao abrir conexão: %v", err) } if err = db.Ping(); err != nil { return fmt.Errorf("erro ao conectar ao banco: %v", err) } log.Println("✅ Conectado ao PostgreSQL") return nil } func main() { // Inicializar banco de dados if err := initDB(); err != nil { log.Fatalf("❌ Erro ao inicializar banco: %v", err) } defer db.Close() // Health check handlers http.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) fmt.Fprintf(w, `{"status":"healthy","version":"1.0.0","database":"pending","redis":"pending","minio":"pending"}`) }) http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) fmt.Fprintf(w, `{"status":"ok"}`) }) // Auth routes (com CORS) http.HandleFunc("/api/auth/register", corsMiddleware(handleRegister)) http.HandleFunc("/api/auth/login", corsMiddleware(handleLogin)) http.HandleFunc("/api/me", corsMiddleware(authMiddleware(handleMe))) port := os.Getenv("SERVER_PORT") if port == "" { port = "8080" } addr := fmt.Sprintf(":%s", port) log.Printf("🚀 Server starting on %s", addr) log.Printf("📍 Health check: http://localhost:%s/health", port) log.Printf("🔗 API: http://localhost:%s/api/health", port) log.Printf("👤 Register: http://localhost:%s/api/auth/register", port) log.Printf("🔐 Login: http://localhost:%s/api/auth/login", port) log.Printf("👤 Me: http://localhost:%s/api/me", port) if err := http.ListenAndServe(addr, nil); err != nil { log.Fatalf("❌ Server error: %v", err) } } // handleRegister handler para criar novo usuário func handleRegister(w http.ResponseWriter, r *http.Request) { // Apenas POST if r.Method != http.MethodPost { sendError(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Rate limiting ip := strings.Split(r.RemoteAddr, ":")[0] if !checkRateLimit(ip, registerAttempts) { sendError(w, "Too many registration attempts. Please try again later.", http.StatusTooManyRequests) return } // Parse JSON var req RegisterRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { sendError(w, "Invalid JSON", http.StatusBadRequest) return } // Validações básicas if !validateEmail(req.Email) { sendError(w, "Invalid email format", http.StatusBadRequest) return } if req.Password == "" { sendError(w, "Password is required", http.StatusBadRequest) return } if len(req.Password) < 8 { sendError(w, "Password must be at least 8 characters", http.StatusBadRequest) return } if req.FullName == "" { sendError(w, "Full name is required", http.StatusBadRequest) return } if req.CompanyName == "" { sendError(w, "Company name is required", http.StatusBadRequest) return } if req.Subdomain == "" { sendError(w, "Subdomain is required", http.StatusBadRequest) return } // Verificar se email já existe var exists bool err := db.QueryRow("SELECT EXISTS(SELECT 1 FROM users WHERE email = $1)", req.Email).Scan(&exists) if err != nil { sendError(w, "Database error", http.StatusInternalServerError) log.Printf("Erro ao verificar email: %v", err) return } if exists { sendError(w, "Email already registered", http.StatusConflict) return } // Hash da senha com bcrypt hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { sendError(w, "Error processing password", http.StatusInternalServerError) log.Printf("Erro ao hash senha: %v", err) return } // Criar Tenant (empresa) tenantID := uuid.New().String() domain := fmt.Sprintf("%s.aggios.app", req.Subdomain) createdAt := time.Now() _, err = db.Exec( "INSERT INTO tenants (id, name, domain, subdomain, is_active, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7)", tenantID, req.CompanyName, domain, req.Subdomain, true, createdAt, createdAt, ) if err != nil { sendError(w, "Error creating company", http.StatusInternalServerError) log.Printf("Erro ao criar tenant: %v", err) return } log.Printf("✅ Tenant criado: %s (%s)", req.CompanyName, tenantID) // Criar Usuário (administrador do tenant) userID := uuid.New().String() firstName := req.FullName lastName := "" _, err = db.Exec( "INSERT INTO users (id, tenant_id, email, password_hash, first_name, last_name, is_active, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", userID, tenantID, req.Email, string(hashedPassword), firstName, lastName, true, createdAt, createdAt, ) if err != nil { sendError(w, "Error creating user", http.StatusInternalServerError) log.Printf("Erro ao inserir usuário: %v", err) return } log.Printf("✅ Usuário criado: %s (%s)", req.Email, userID) // Gerar token JWT para login automático token, err := generateToken(userID, req.Email, tenantID) if err != nil { sendError(w, "Error generating token", http.StatusInternalServerError) log.Printf("Erro ao gerar token: %v", err) return } response := RegisterResponse{ Token: token, ID: userID, Email: req.Email, Name: req.FullName, TenantID: tenantID, Company: req.CompanyName, Subdomain: req.Subdomain, CreatedAt: createdAt.Format(time.RFC3339), } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(response) } // sendError envia uma resposta de erro padronizada func sendError(w http.ResponseWriter, message string, statusCode int) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(statusCode) json.NewEncoder(w).Encode(ErrorResponse{ Error: http.StatusText(statusCode), Message: message, }) } // generateToken gera um JWT token para o usuário func generateToken(userID, email, tenantID string) (string, error) { claims := Claims{ UserID: userID, Email: email, TenantID: tenantID, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), IssuedAt: jwt.NewNumericDate(time.Now()), Issuer: "aggios-api", }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(jwtSecret) } // authMiddleware verifica o token JWT func authMiddleware(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { authHeader := r.Header.Get("Authorization") if authHeader == "" { sendError(w, "Authorization header required", http.StatusUnauthorized) return } tokenString := strings.TrimPrefix(authHeader, "Bearer ") if tokenString == authHeader { sendError(w, "Invalid authorization format", http.StatusUnauthorized) return } claims := &Claims{} token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { return jwtSecret, nil }) if err != nil || !token.Valid { sendError(w, "Invalid or expired token", http.StatusUnauthorized) return } // Adicionar claims ao contexto (simplificado: usar headers) r.Header.Set("X-User-ID", claims.UserID) r.Header.Set("X-User-Email", claims.Email) r.Header.Set("X-Tenant-ID", claims.TenantID) next(w, r) } } // handleLogin handler para fazer login func handleLogin(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { sendError(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Rate limiting ip := strings.Split(r.RemoteAddr, ":")[0] if !checkRateLimit(ip, loginAttempts) { sendError(w, "Too many login attempts. Please try again later.", http.StatusTooManyRequests) return } var req LoginRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { sendError(w, "Invalid JSON", http.StatusBadRequest) return } if !validateEmail(req.Email) || req.Password == "" { sendError(w, "Invalid credentials", http.StatusBadRequest) return } // Buscar usuário no banco var userID, email, passwordHash, firstName, tenantID string var tenantName, subdomain string err := db.QueryRow(` SELECT u.id, u.email, u.password_hash, u.first_name, u.tenant_id, t.name, t.subdomain FROM users u INNER JOIN tenants t ON u.tenant_id = t.id WHERE u.email = $1 AND u.is_active = true `, req.Email).Scan(&userID, &email, &passwordHash, &firstName, &tenantID, &tenantName, &subdomain) if err == sql.ErrNoRows { sendError(w, "Invalid credentials", http.StatusUnauthorized) return } if err != nil { sendError(w, "Database error", http.StatusInternalServerError) log.Printf("Erro ao buscar usuário: %v", err) return } // Verificar senha if err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(req.Password)); err != nil { sendError(w, "Invalid credentials", http.StatusUnauthorized) return } // Gerar token JWT token, err := generateToken(userID, email, tenantID) if err != nil { sendError(w, "Error generating token", http.StatusInternalServerError) log.Printf("Erro ao gerar token: %v", err) return } log.Printf("✅ Login bem-sucedido: %s", email) response := LoginResponse{ Token: token, User: UserPayload{ ID: userID, Email: email, Name: firstName, TenantID: tenantID, Company: tenantName, Subdomain: subdomain, }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // handleMe retorna dados do usuário autenticado func handleMe(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { sendError(w, "Method not allowed", http.StatusMethodNotAllowed) return } userID := r.Header.Get("X-User-ID") tenantID := r.Header.Get("X-Tenant-ID") var email, firstName, lastName string var tenantName, subdomain string err := db.QueryRow(` SELECT u.email, u.first_name, u.last_name, t.name, t.subdomain FROM users u INNER JOIN tenants t ON u.tenant_id = t.id WHERE u.id = $1 AND u.tenant_id = $2 `, userID, tenantID).Scan(&email, &firstName, &lastName, &tenantName, &subdomain) if err != nil { sendError(w, "User not found", http.StatusNotFound) log.Printf("Erro ao buscar usuário: %v", err) return } fullName := firstName if lastName != "" { fullName += " " + lastName } response := UserPayload{ ID: userID, Email: email, Name: fullName, TenantID: tenantID, Company: tenantName, Subdomain: subdomain, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) }