514 lines
13 KiB
Go
514 lines
13 KiB
Go
package handlers
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"log"
|
||
"net/http"
|
||
"os"
|
||
"path/filepath"
|
||
"strconv"
|
||
"strings"
|
||
"text/template"
|
||
"time"
|
||
|
||
"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)
|
||
}
|
||
}
|
||
|
||
func (h *Handler) handleContactSubmission(w http.ResponseWriter, r *http.Request) {
|
||
// Parse form data
|
||
if err := r.ParseForm(); err != nil {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Failed to parse form data",
|
||
})
|
||
return
|
||
}
|
||
|
||
// Extract form fields
|
||
name := strings.TrimSpace(r.FormValue("name"))
|
||
email := strings.TrimSpace(r.FormValue("email"))
|
||
subject := strings.TrimSpace(r.FormValue("subject"))
|
||
message := strings.TrimSpace(r.FormValue("message"))
|
||
subscribe := r.FormValue("subscribe") == "on"
|
||
|
||
// Validate required fields
|
||
if name == "" || email == "" || subject == "" || message == "" {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "All fields are required",
|
||
})
|
||
return
|
||
}
|
||
|
||
// Validate email format (basic validation)
|
||
if !strings.Contains(email, "@") || !strings.Contains(email, ".") {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Invalid email address",
|
||
})
|
||
return
|
||
}
|
||
|
||
// Validate message length (prevent spam)
|
||
if len(message) < 10 {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Message must be at least 10 characters",
|
||
})
|
||
return
|
||
}
|
||
|
||
if len(message) > 5000 {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"error": "Message must be less than 5000 characters",
|
||
})
|
||
return
|
||
}
|
||
|
||
// If subscribe checkbox is checked, add to subscribers
|
||
if subscribe {
|
||
if err := h.db.AddSubscriber(r.Context(), email); err != nil {
|
||
// Log but don't fail the contact submission
|
||
h.logger.Printf(
|
||
"ℹ Subscriber %s already exists or failed to add: %v",
|
||
email,
|
||
err,
|
||
)
|
||
} else {
|
||
h.logger.Printf("✓ New subscriber added: %s", email)
|
||
}
|
||
}
|
||
|
||
// Send confirmation email to the user
|
||
if err := h.email.SendContactConfirmation(email, name); err != nil {
|
||
h.logger.Printf(
|
||
"❌ Failed to send contact confirmation to %s: %v",
|
||
email,
|
||
err,
|
||
)
|
||
} else {
|
||
h.logger.Printf("✓ Contact confirmation email sent to %s", email)
|
||
}
|
||
|
||
// Send notification email to admin
|
||
adminEmail := h.cfg.AdminEmail
|
||
if adminEmail != "" {
|
||
if err := h.email.SendContactNotification(
|
||
adminEmail,
|
||
name,
|
||
email,
|
||
subject,
|
||
message,
|
||
); err != nil {
|
||
h.logger.Printf(
|
||
"❌ Failed to send contact notification to admin: %v",
|
||
err,
|
||
)
|
||
} else {
|
||
h.logger.Printf("✓ Contact notification sent to admin: %s", adminEmail)
|
||
}
|
||
}
|
||
|
||
// Save contact message to database (optional - add to your DB interface)
|
||
if err := h.db.AddContactMessage(r.Context(), name, email, subject, message); err != nil {
|
||
h.logger.Printf(
|
||
"⚠ Failed to save contact message: %v",
|
||
err,
|
||
)
|
||
}
|
||
|
||
h.logger.Printf("✓ Contact form submitted by %s (%s)", name, email)
|
||
|
||
// Return success response
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusCreated)
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"message": "Thank you for your message. We'll get back to you soon!",
|
||
})
|
||
}
|
||
|
||
func (h *Handler) aboutHandler(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("about.html"),
|
||
)
|
||
if err != nil {
|
||
http.Error(w, fmt.Sprintf("Failed to parse template: %v", err), http.StatusInternalServerError)
|
||
h.logger.Printf("❌ Template parse error: %v", err)
|
||
return
|
||
}
|
||
|
||
data := map[string]interface{}{
|
||
"IsAbout": true,
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "text/html")
|
||
tmpl.ExecuteTemplate(w, "base.html", data)
|
||
}
|
||
|
||
func getBaseURL(r *http.Request) string {
|
||
scheme := "http"
|
||
if r.TLS != nil {
|
||
scheme = "https"
|
||
}
|
||
return fmt.Sprintf("%s://%s", scheme, r.Host)
|
||
} |