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
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())
|
|
}
|
|
|