an updated and hopefully faster version of the ST Toolbox
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

188 lines
7.8 KiB

package main
import (
"log"
"net/http"
"os"
"strings"
"time"
root "marmic/servicetrade-toolbox"
"marmic/servicetrade-toolbox/internal/handlers/web"
"marmic/servicetrade-toolbox/internal/middleware"
"github.com/gorilla/csrf"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
)
func main() {
err := root.InitializeWebTemplates() // Note the change here
if err != nil {
log.Fatalf("Failed to initialize web templates: %v", err)
}
r := mux.NewRouter()
// Make app proxy-aware (X-Forwarded-Proto/Host) so downstream middlewares see correct scheme/host
r.Use(handlers.ProxyHeaders)
// Global security headers middleware
r.Use(middleware.SecurityHeaders)
// Serve embedded static files
staticFS, err := root.GetStaticFS()
if err != nil {
log.Fatalf("Failed to get static filesystem: %v", err)
}
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))
// Serve static files
// r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
// Auth routes (login remains outside CSRF)
r.HandleFunc("/login", web.LoginHandler).Methods("GET", "POST")
// CSRF protection for state-changing routes
csrfKey := os.Getenv("CSRF_AUTH_KEY")
if len(csrfKey) < 32 {
log.Println("WARNING: CSRF_AUTH_KEY is not set or too short; using insecure default for development only")
csrfKey = "this-is-a-very-insecure-dev-key-please-set-env"
}
secureCookies := os.Getenv("APP_SECURE_COOKIES") != "false"
disableCSRF := os.Getenv("CSRF_DISABLE") == "true"
// Trusted origins for reverse-proxy/HTTPS setups (comma-separated env)
var trustedOrigins []string
if envTO := os.Getenv("CSRF_TRUSTED_ORIGINS"); envTO != "" {
for _, part := range strings.Split(envTO, ",") {
o := strings.TrimSpace(part)
if o != "" {
trustedOrigins = append(trustedOrigins, o)
}
}
} else {
// Sensible defaults for your known hosts; override via CSRF_TRUSTED_ORIGINS in prod
trustedOrigins = []string{
"https://dev.toolbox.nicpatterson.info",
"https://toolbox.nicpatterson.info",
"http://localhost:8080",
"http://127.0.0.1:8080",
}
}
csrfOpts := []csrf.Option{
csrf.Secure(secureCookies),
csrf.SameSite(csrf.SameSiteLaxMode),
csrf.CookieName("XSRF-TOKEN"),
csrf.HttpOnly(false), // allow htmx script to read and send the token
csrf.Path("/"),
csrf.ErrorHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Log the specific CSRF failure reason to help debugging
origin := r.Header.Get("Origin")
referer := r.Header.Get("Referer")
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
if xfProto := r.Header.Get("X-Forwarded-Proto"); xfProto != "" {
scheme = xfProto
}
if reason := csrf.FailureReason(r); reason != nil {
log.Printf("CSRF validation failed: %v | host=%s scheme=%s origin=%s referer=%s", reason, r.Host, scheme, origin, referer)
} else {
log.Printf("CSRF validation failed: unknown reason | host=%s scheme=%s origin=%s referer=%s", r.Host, scheme, origin, referer)
}
http.Error(w, "Forbidden - CSRF", http.StatusForbidden)
})),
}
if len(trustedOrigins) > 0 {
log.Printf("CSRF trusted origins: %v", trustedOrigins)
csrfOpts = append(csrfOpts, csrf.TrustedOrigins(trustedOrigins))
}
csrfMw := csrf.Protect([]byte(csrfKey), csrfOpts...)
// Protected routes
protected := r.PathPrefix("/").Subrouter()
protected.Use(middleware.AuthMiddleware)
if disableCSRF {
log.Println("WARNING: CSRF is DISABLED by environment (CSRF_DISABLE=true) - DO NOT USE IN PRODUCTION")
} else {
// Environment-guarded CSRF mode selection
appEnv := os.Getenv("APP_ENV") // expected: production, staging, dev, local
useSimple := os.Getenv("CSRF_MODE") == "simple"
if appEnv == "production" && useSimple {
log.Fatal("CSRF_MODE=simple is not allowed in production")
}
if useSimple && appEnv != "production" {
log.Println("INFO: Using simple CSRF mode (double-submit cookie) for non-production environment")
protected.Use(middleware.CSRFSimple)
} else {
protected.Use(csrfMw)
protected.Use(middleware.CSRFExposeToken)
}
}
protected.HandleFunc("/", web.DashboardHandler).Methods("GET")
protected.HandleFunc("/logout", web.LogoutHandler).Methods("POST")
protected.HandleFunc("/jobs", web.JobsHandler).Methods("GET", "POST")
protected.HandleFunc("/invoices", web.InvoicesHandler).Methods("GET", "POST")
protected.HandleFunc("/ok-invoice/{id}", web.UpdateInvoiceStatusHandler).Methods("PUT")
protected.HandleFunc("/draft-invoice/{id}", web.UpdateInvoiceStatusHandler).Methods("PUT")
protected.HandleFunc("/failed-invoice/{id}", web.UpdateInvoiceStatusHandler).Methods("PUT")
protected.HandleFunc("/pending_accounting-invoice/{id}", web.UpdateInvoiceStatusHandler).Methods("PUT")
protected.HandleFunc("/processed-invoice/{id}", web.UpdateInvoiceStatusHandler).Methods("PUT")
protected.HandleFunc("/void-invoice/{id}", web.UpdateInvoiceStatusHandler).Methods("PUT")
protected.HandleFunc("/admin", web.AdminHandler).Methods("GET")
protected.HandleFunc("/assets", web.AssetsHandler).Methods("GET")
protected.HandleFunc("/companies", web.CompaniesHandler).Methods("GET")
protected.HandleFunc("/contacts", web.ContactsHandler).Methods("GET")
protected.HandleFunc("/contracts", web.ContractsHandler).Methods("GET")
protected.HandleFunc("/generic", web.GenericHandler).Methods("GET")
protected.HandleFunc("/locations", web.LocationsHandler).Methods("GET")
protected.HandleFunc("/notifications", web.NotificationsHandler).Methods("GET")
protected.HandleFunc("/quotes", web.QuotesHandler).Methods("GET")
protected.HandleFunc("/services", web.ServicesHandler).Methods("GET")
protected.HandleFunc("/tags", web.TagsHandler).Methods("GET")
protected.HandleFunc("/users", web.UsersHandler).Methods("GET")
protected.HandleFunc("/users/upload", web.UsersUploadHandler).Methods("POST")
protected.HandleFunc("/users/update", web.UsersUpdateHandler).Methods("GET")
protected.HandleFunc("/users/update/upload", web.UsersUpdateUploadHandler).Methods("POST")
// Document upload routes
protected.HandleFunc("/documents", web.DocumentsHandler).Methods("GET")
protected.HandleFunc("/process-csv", web.ProcessCSVHandler).Methods("POST")
protected.HandleFunc("/upload-documents", web.UploadDocumentsHandler).Methods("POST")
protected.HandleFunc("/documents/upload/results", web.UploadResultsHandler).Methods("GET")
protected.HandleFunc("/documents/upload/job/file", web.UploadJobFileHandler).Methods("GET")
// Document removal routes
protected.HandleFunc("/documents/remove", web.DocumentRemoveHandler).Methods("GET")
protected.HandleFunc("/documents/remove/process-csv", web.ProcessRemoveCSVHandler).Methods("POST")
protected.HandleFunc("/documents/remove/job-selection", web.JobSelectionHandler).Methods("POST")
protected.HandleFunc("/documents/remove/job/file", web.RemovalJobFileHandler).Methods("GET")
protected.HandleFunc("/documents/remove/job/{jobID}", web.GetJobAttachmentsHandler).Methods("GET")
protected.HandleFunc("/documents/remove/attachments/{jobID}", web.RemoveJobAttachmentsHandler).Methods("POST")
protected.HandleFunc("/documents/remove/bulk", web.BulkRemoveDocumentsHandler).Methods("POST")
protected.HandleFunc("/documents/remove/results", web.RemovalResultsHandler).Methods("GET")
// Reports & utilities
protected.HandleFunc("/reports/invoice-clock", web.InvoiceClockReportHandler).Methods("POST")
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Println("Server starting on :" + port)
// Create a custom server with appropriate timeouts
server := &http.Server{
Addr: ":" + port,
Handler: r,
ReadTimeout: 2 * time.Hour, // Large timeout for big file uploads
WriteTimeout: 2 * time.Hour, // Large timeout for big file responses
IdleTimeout: 120 * time.Second, // How long to wait for the next request
}
log.Fatal(server.ListenAndServe())
}