333 lines
8.9 KiB
Go
333 lines
8.9 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Configuration holds all website configuration
|
|
type Config struct {
|
|
CompanyName string
|
|
KVK string
|
|
Email string
|
|
Phone string
|
|
Street string
|
|
PostalCode string
|
|
Village string
|
|
Domain string
|
|
Port string
|
|
TelegramBotToken string
|
|
TelegramChatID string
|
|
}
|
|
|
|
// PageData holds data for template rendering
|
|
type PageData struct {
|
|
Config
|
|
Title string
|
|
CurrentPage string
|
|
CurrentYear int
|
|
ErrorMessage string
|
|
SuccessMessage string
|
|
FormData ContactForm
|
|
}
|
|
|
|
// ContactForm holds form data
|
|
type ContactForm struct {
|
|
Name string
|
|
Email string
|
|
Phone string
|
|
Computer string
|
|
Service string
|
|
Message string
|
|
}
|
|
|
|
// Server holds the application state
|
|
type Server struct {
|
|
config Config
|
|
templates *template.Template
|
|
}
|
|
|
|
// Rate limiting
|
|
var (
|
|
mu sync.Mutex
|
|
lastSubmissionTime = make(map[string]time.Time)
|
|
submissionCooldown = 5 * time.Minute
|
|
)
|
|
|
|
// getEnv returns environment variable or default value
|
|
func getEnv(key, defaultValue string) string {
|
|
if value := os.Getenv(key); value != "" {
|
|
return value
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
// NewServer creates a new server instance
|
|
func NewServer() *Server {
|
|
// Configuration - can be overridden with environment variables
|
|
config := Config{
|
|
CompanyName: getEnv("COMPANY_NAME", "Hogeland Linux"),
|
|
KVK: getEnv("KVK", "12345678"), // Replace with actual KVK number
|
|
Email: getEnv("EMAIL", "info@hogelandlinux.nl"),
|
|
Phone: getEnv("PHONE", "+31 6 12345678"),
|
|
Street: getEnv("STREET", "Voorstraat 123"),
|
|
PostalCode: getEnv("POSTAL_CODE", "9967 AA"),
|
|
Village: getEnv("VILLAGE", "Eenrum"),
|
|
Domain: getEnv("DOMAIN", "hogelandlinux.nl"), // Replace with actual domain
|
|
Port: ":" + getEnv("PORT", "8080"),
|
|
TelegramBotToken: getEnv("TELEGRAM_BOT_TOKEN", ""), // Set this in environment
|
|
TelegramChatID: getEnv("TELEGRAM_CHAT_ID", ""), // Set this in environment
|
|
}
|
|
|
|
// Parse templates with error handling
|
|
templates, err := template.ParseGlob("templates/*.html")
|
|
if err != nil {
|
|
log.Fatalf("Failed to parse templates: %v", err)
|
|
}
|
|
|
|
return &Server{
|
|
config: config,
|
|
templates: templates,
|
|
}
|
|
}
|
|
|
|
// createPageData creates PageData with the given title and current page
|
|
func (s *Server) createPageData(title, currentPage string) PageData {
|
|
return PageData{
|
|
Config: s.config,
|
|
Title: title,
|
|
CurrentPage: currentPage,
|
|
CurrentYear: time.Now().Year(),
|
|
}
|
|
}
|
|
|
|
// renderTemplate renders a template with error handling
|
|
func (s *Server) renderTemplate(w http.ResponseWriter, templateName string, data PageData) {
|
|
if err := s.templates.ExecuteTemplate(w, templateName, data); err != nil {
|
|
log.Printf("Template execution error: %v", err)
|
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// homeHandler handles the home page
|
|
func (s *Server) homeHandler(w http.ResponseWriter, r *http.Request) {
|
|
data := s.createPageData("Linux Migratie Service - Uw Computer Nieuw Leven Geven", "home")
|
|
s.renderTemplate(w, "index.html", data)
|
|
}
|
|
|
|
// contactHandler handles the contact page
|
|
func (s *Server) contactHandler(w http.ResponseWriter, r *http.Request) {
|
|
data := s.createPageData("Contact - "+s.config.CompanyName, "contact")
|
|
|
|
if r.Method == "POST" {
|
|
s.handleContactForm(w, r, &data)
|
|
return
|
|
}
|
|
|
|
s.renderTemplate(w, "contact.html", data)
|
|
}
|
|
|
|
// handleContactForm processes the contact form submission
|
|
func (s *Server) handleContactForm(w http.ResponseWriter, r *http.Request, data *PageData) {
|
|
// Get client IP for rate limiting
|
|
ip := getClientIP(r)
|
|
|
|
// Check rate limiting
|
|
mu.Lock()
|
|
if lastTime, exists := lastSubmissionTime[ip]; exists {
|
|
if time.Since(lastTime) < submissionCooldown {
|
|
mu.Unlock()
|
|
data.ErrorMessage = "U heeft recent al een bericht verstuurd. Probeer het over een paar minuten opnieuw."
|
|
s.renderTemplate(w, "contact.html", *data)
|
|
return
|
|
}
|
|
}
|
|
mu.Unlock()
|
|
|
|
// Parse form data
|
|
err := r.ParseForm()
|
|
if err != nil {
|
|
log.Printf("Error parsing form: %v", err)
|
|
data.ErrorMessage = "Er is een fout opgetreden bij het verwerken van uw bericht."
|
|
s.renderTemplate(w, "contact.html", *data)
|
|
return
|
|
}
|
|
|
|
// Extract form data
|
|
form := ContactForm{
|
|
Name: strings.TrimSpace(r.FormValue("name")),
|
|
Email: strings.TrimSpace(r.FormValue("email")),
|
|
Phone: strings.TrimSpace(r.FormValue("phone")),
|
|
Computer: strings.TrimSpace(r.FormValue("computer")),
|
|
Service: strings.TrimSpace(r.FormValue("service")),
|
|
Message: strings.TrimSpace(r.FormValue("message")),
|
|
}
|
|
|
|
// Store form data for re-rendering on error
|
|
data.FormData = form
|
|
|
|
// Validate required fields
|
|
if form.Name == "" || form.Email == "" || form.Message == "" {
|
|
data.ErrorMessage = "Vul alle verplichte velden in (naam, email, bericht)."
|
|
s.renderTemplate(w, "contact.html", *data)
|
|
return
|
|
}
|
|
|
|
// Send to Telegram if configured
|
|
if s.config.TelegramBotToken != "" && s.config.TelegramChatID != "" {
|
|
err := s.sendToTelegram(form)
|
|
if err != nil {
|
|
log.Printf("Error sending message to Telegram: %v", err)
|
|
data.ErrorMessage = "Er is een fout opgetreden bij het versturen van uw bericht. Probeer het later opnieuw."
|
|
s.renderTemplate(w, "contact.html", *data)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Update last submission time on success
|
|
mu.Lock()
|
|
lastSubmissionTime[ip] = time.Now()
|
|
mu.Unlock()
|
|
|
|
// On success, render success message
|
|
data.SuccessMessage = "Bedankt voor uw bericht! Wij nemen zo snel mogelijk contact met u op."
|
|
data.FormData = ContactForm{} // Clear form data
|
|
s.renderTemplate(w, "contact.html", *data)
|
|
}
|
|
|
|
// sendToTelegram sends the contact form data to Telegram
|
|
func (s *Server) sendToTelegram(form ContactForm) error {
|
|
// Format message
|
|
message := fmt.Sprintf(
|
|
"🔔 Nieuw contactformulier\n\n"+
|
|
"👤 *Naam:* %s\n"+
|
|
"📧 *Email:* %s\n"+
|
|
"📞 *Telefoon:* %s\n"+
|
|
"💻 *Computer:* %s\n"+
|
|
"🛠️ *Service:* %s\n\n"+
|
|
"💬 *Bericht:*\n%s",
|
|
escapeMarkdown(form.Name),
|
|
escapeMarkdown(form.Email),
|
|
escapeMarkdown(form.Phone),
|
|
escapeMarkdown(form.Computer),
|
|
escapeMarkdown(form.Service),
|
|
escapeMarkdown(form.Message),
|
|
)
|
|
|
|
// Prepare Telegram API request
|
|
telegramURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", s.config.TelegramBotToken)
|
|
|
|
payload := map[string]interface{}{
|
|
"chat_id": s.config.TelegramChatID,
|
|
"text": message,
|
|
"parse_mode": "Markdown",
|
|
}
|
|
|
|
jsonData, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal JSON: %v", err)
|
|
}
|
|
|
|
// Send HTTP request
|
|
resp, err := http.Post(telegramURL, "application/json", bytes.NewBuffer(jsonData))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to send HTTP request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
|
return fmt.Errorf("Telegram API error: %s, Status Code: %d", string(bodyBytes), resp.StatusCode)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// escapeMarkdown escapes special characters for Telegram Markdown
|
|
func escapeMarkdown(text string) string {
|
|
if text == "" {
|
|
return "N/A"
|
|
}
|
|
// Escape special Markdown characters
|
|
replacer := strings.NewReplacer(
|
|
"*", "\\*",
|
|
"_", "\\_",
|
|
"`", "\\`",
|
|
"[", "\\[",
|
|
"]", "\\]",
|
|
"(", "\\(",
|
|
")", "\\)",
|
|
"~", "\\~",
|
|
">", "\\>",
|
|
"#", "\\#",
|
|
"+", "\\+",
|
|
"-", "\\-",
|
|
"=", "\\=",
|
|
"|", "\\|",
|
|
"{", "\\{",
|
|
"}", "\\}",
|
|
".", "\\.",
|
|
"!", "\\!",
|
|
)
|
|
return replacer.Replace(text)
|
|
}
|
|
|
|
// getClientIP extracts the client IP address from the request
|
|
func getClientIP(r *http.Request) string {
|
|
// Check X-Forwarded-For header first (for reverse proxies)
|
|
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
|
// X-Forwarded-For can contain multiple IPs, take the first one
|
|
if idx := strings.Index(xff, ","); idx != -1 {
|
|
return strings.TrimSpace(xff[:idx])
|
|
}
|
|
return strings.TrimSpace(xff)
|
|
}
|
|
|
|
// Check X-Real-IP header
|
|
if xri := r.Header.Get("X-Real-IP"); xri != "" {
|
|
return strings.TrimSpace(xri)
|
|
}
|
|
|
|
// Fall back to RemoteAddr
|
|
if idx := strings.LastIndex(r.RemoteAddr, ":"); idx != -1 {
|
|
return r.RemoteAddr[:idx]
|
|
}
|
|
return r.RemoteAddr
|
|
}
|
|
|
|
// healthHandler provides a simple health check endpoint
|
|
func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"status":"healthy","service":"linuxservice"}`))
|
|
}
|
|
|
|
// setupRoutes configures all HTTP routes
|
|
func (s *Server) setupRoutes() {
|
|
// Static files
|
|
fs := http.FileServer(http.Dir("static/"))
|
|
http.Handle("/static/", http.StripPrefix("/static/", fs))
|
|
|
|
// Page routes
|
|
http.HandleFunc("/", s.homeHandler)
|
|
http.HandleFunc("/contact", s.contactHandler)
|
|
http.HandleFunc("/health", s.healthHandler)
|
|
}
|
|
|
|
func main() {
|
|
server := NewServer()
|
|
server.setupRoutes()
|
|
|
|
log.Printf("Server starting on %s", server.config.Port)
|
|
log.Fatal(http.ListenAndServe(server.config.Port, nil))
|
|
}
|