package handlers import ( "encoding/json" "fmt" "log" "net/http" "os" "path/filepath" "regexp" "strconv" "strings" "text/template" "time" "unicode" "landing/internal/config" "landing/internal/database" "landing/internal/email" ) type Handler struct { db *database.DB cfg *config.Config email *email.Sender templatesPath string logger *log.Logger } func New(db *database.DB, cfg *config.Config) *Handler { templatesPath := "templates" if _, err := os.Stat(templatesPath); os.IsNotExist(err) { templatesPath = "./templates" } return &Handler{ db: db, cfg: cfg, email: email.New(cfg), templatesPath: templatesPath, logger: log.New(os.Stdout, "", log.LstdFlags), } } // loggingMiddleware logs HTTP requests func (h *Handler) loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { userAgent := r.UserAgent() // Block malicious bots and common attack patterns blockedPatterns := []string{ "python-requests", "curl", "wget", "sqlmap", "nikto", ".php", ".env", ".git", "wp-admin", "xmlrpc", "backup", "config", } for _, pattern := range blockedPatterns { if strings.Contains(strings.ToLower(r.RequestURI), strings.ToLower(pattern)) { w.WriteHeader(http.StatusForbidden) fmt.Fprintf(w, "Access Denied") h.logger.Printf("BLOCKED attack: %s %s from %s", r.Method, r.RequestURI, r.RemoteAddr) return } if strings.Contains(strings.ToLower(userAgent), strings.ToLower(pattern)) { w.WriteHeader(http.StatusForbidden) fmt.Fprintf(w, "Access Denied") h.logger.Printf("BLOCKED bot: %s from %s", userAgent, r.RemoteAddr) return } } start := time.Now() wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK} next.ServeHTTP(wrapped, r) duration := time.Since(start) statusColor := getStatusColor(wrapped.statusCode) methodColor := getMethodColor(r.Method) h.logger.Printf( "%s %s %s %s %s %d %s", methodColor+r.Method+"\033[0m", r.RequestURI, statusColor+fmt.Sprintf("%d", wrapped.statusCode)+"\033[0m", duration.String(), r.RemoteAddr, wrapped.contentLength, userAgent, ) }) } type responseWriter struct { http.ResponseWriter statusCode int contentLength int } func (rw *responseWriter) WriteHeader(code int) { rw.statusCode = code rw.ResponseWriter.WriteHeader(code) } func (rw *responseWriter) Write(b []byte) (int, error) { rw.contentLength = len(b) return rw.ResponseWriter.Write(b) } // Color codes for terminal output func getStatusColor(statusCode int) string { switch { case statusCode >= 200 && statusCode < 300: return "\033[32m" // Green case statusCode >= 300 && statusCode < 400: return "\033[36m" // Cyan case statusCode >= 400 && statusCode < 500: return "\033[33m" // Yellow case statusCode >= 500: return "\033[31m" // Red default: return "\033[37m" // White } } func getMethodColor(method string) string { switch method { case "GET": return "\033[34m" // Blue case "POST": return "\033[32m" // Green case "PUT": return "\033[33m" // Yellow case "DELETE": return "\033[31m" // Red default: return "\033[37m" // White } } func (h *Handler) Start(host, port string) error { mux := http.NewServeMux() // Serve static files mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))) mux.HandleFunc("/", h.indexHandler) mux.HandleFunc("/subscribe", h.subscribeHandler) mux.HandleFunc("/unsubscribe", h.unsubscribeHandler) mux.HandleFunc("/newsletters", h.newslettersHandler) mux.HandleFunc("/newsletter/", h.newsletterDetailHandler) mux.HandleFunc("/contact", h.contactHandler) mux.HandleFunc("/about", h.aboutHandler) // Wrap with logging middleware handler := h.loggingMiddleware(mux) h.logger.Printf("\033[36m▶ Starting server on http://%s:%s\033[0m", host, port) return http.ListenAndServe(host+":"+port, handler) } func (h *Handler) getTemplatePath(name string) string { return filepath.Join(h.templatesPath, name) } func (h *Handler) indexHandler( w http.ResponseWriter, r *http.Request, ) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } tmpl, err := template.ParseFiles( h.getTemplatePath("base.html"), h.getTemplatePath("index.html"), ) if err != nil { http.Error(w, fmt.Sprintf("Failed to parse template: %v", err), http.StatusInternalServerError) return } data := map[string]interface{}{ "IsHome": true, } tmpl.ExecuteTemplate(w, "base.html", data) } func (h *Handler) subscribeHandler( w http.ResponseWriter, r *http.Request, ) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req struct { Email string `json:"email"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request", http.StatusBadRequest) return } if req.Email == "" { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]string{ "error": "Email is required", }) return } if err := h.db.AddSubscriber(r.Context(), req.Email); err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]string{ "error": "Email already exists", }) return } unsubscribeLink := fmt.Sprintf( "%s/unsubscribe?email=%s", getBaseURL(r), req.Email, ) if err := h.email.SendConfirmationEmail( req.Email, unsubscribeLink, ); err != nil { h.logger.Printf("❌ Failed to send confirmation email to %s: %v", req.Email, err) } else { h.logger.Printf("✓ Confirmation email sent to %s", req.Email) } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(map[string]string{ "message": "Email has been added", }) } func (h *Handler) unsubscribeHandler( w http.ResponseWriter, r *http.Request, ) { email := r.URL.Query().Get("email") if email == "" { http.Error(w, "No email specified", http.StatusBadRequest) return } if err := h.db.RemoveSubscriber(r.Context(), email); err != nil { http.Error( w, fmt.Sprintf( "Email %s was not found or already unsubscribed", email, ), http.StatusBadRequest, ) return } h.logger.Printf("✓ Unsubscribed %s", email) w.Header().Set("Content-Type", "text/plain") fmt.Fprintf(w, "The email %s has been unsubscribed.", email) } func (h *Handler) newslettersHandler( w http.ResponseWriter, r *http.Request, ) { newsletters, err := h.db.GetNewsletters(r.Context()) if err != nil { http.Error(w, "Failed to fetch newsletters", 500) return } tmpl, err := template.ParseFiles( h.getTemplatePath("base.html"), h.getTemplatePath("newsletters.html"), ) if err != nil { http.Error(w, fmt.Sprintf("Failed to parse template: %v", err), http.StatusInternalServerError) return } tmpl.ExecuteTemplate(w, "base.html", newsletters) } func (h *Handler) newsletterDetailHandler( w http.ResponseWriter, r *http.Request, ) { idStr := r.URL.Path[len("/newsletter/"):] id, err := strconv.Atoi(idStr) if err != nil { http.Error(w, "Invalid newsletter ID", http.StatusBadRequest) return } newsletter, err := h.db.GetNewsletter(r.Context(), id) if err != nil { http.Error(w, "Newsletter not found", http.StatusNotFound) return } tmpl, err := template.ParseFiles( h.getTemplatePath("base.html"), h.getTemplatePath("newsletter_detail.html"), ) if err != nil { http.Error(w, fmt.Sprintf("Failed to parse template: %v", err), http.StatusInternalServerError) return } tmpl.ExecuteTemplate(w, "base.html", newsletter) } func (h *Handler) contactHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet && r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } if r.Method == http.MethodGet { tmpl, err := template.ParseFiles( h.getTemplatePath("base.html"), h.getTemplatePath("contact.html"), ) if err != nil { http.Error(w, fmt.Sprintf("Failed to parse template: %v", err), http.StatusInternalServerError) return } data := map[string]interface{}{ "IsContact": true, } tmpl.ExecuteTemplate(w, "base.html", data) return } if r.Method == http.MethodPost { h.handleContactSubmission(w, r) } } // isEnglishText checks if text is primarily in English func isEnglishText(text string) bool { if len(text) == 0 { return true } englishCharCount := 0 nonASCIICount := 0 totalCharCount := 0 // Common English words to boost score commonEnglish := []string{ "the ", "and ", "is ", "to ", "of ", "for ", "that ", "with ", "this ", "have ", "from ", "would ", "could ", "about ", "more ", "which ", "been ", "their ", } lowerText := strings.ToLower(text) englishWordBoost := 0 for _, word := range commonEnglish { if strings.Contains(lowerText, word) { englishWordBoost += 10 } } for _, r := range text { if unicode.IsLetter(r) || unicode.IsNumber(r) || unicode.IsSpace(r) || unicode.IsPunct(r) { totalCharCount++ if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == ' ' || r == '.' || r == ',' || r == '!' || r == '?' || r == '-' || r == '\'' || r == '"' || r == ';' || r == ':' || r == '(' || r == ')' || r == '\n' || r == '\t' { englishCharCount++ } else if r > 127 { nonASCIICount++ } } } if totalCharCount == 0 { return true } // If more than 3 non-ASCII characters, likely spam/bot if nonASCIICount > 3 { return false } englishPercentage := float64(englishCharCount) / float64(totalCharCount) // Stricter requirements with word boost return englishPercentage >= 0.75 || (englishPercentage >= 0.65 && englishWordBoost > 0) } // isSpamMessage checks if a message looks like spam func isSpamMessage(message string) bool { // Convert to lowercase for checks lowerMsg := strings.ToLower(message) // Check for common spam patterns spamPatterns := []string{ "viagra", "cialis", "casino", "lottery", "prize", "click here", "buy now", "limited time", "congratulations", "you have won", "claim your", "bitcoin", "crypto", "forex", "trading bot", "free money", "make money fast", "work from home", "nigerian", "inheritance", "transfer funds", "