diff --git a/apps/web/main.go b/apps/web/main.go index e5bb532..1a9f19f 100644 --- a/apps/web/main.go +++ b/apps/web/main.go @@ -163,6 +163,9 @@ func main() { 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" diff --git a/internal/handlers/web/generic.go b/internal/handlers/web/generic.go index 6f0882b..eba5b59 100644 --- a/internal/handlers/web/generic.go +++ b/internal/handlers/web/generic.go @@ -1,18 +1,54 @@ package web import ( - root "marmic/servicetrade-toolbox" + "bytes" + "html/template" "net/http" + + root "marmic/servicetrade-toolbox" + "marmic/servicetrade-toolbox/internal/api" + "marmic/servicetrade-toolbox/internal/middleware" + + "github.com/gorilla/csrf" ) func GenericHandler(w http.ResponseWriter, r *http.Request) { + session, ok := r.Context().Value(middleware.SessionKey).(*api.Session) + if !ok { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + tmpl := root.WebTemplates + var csrfCookie string + if c, err := r.Cookie("XSRF-TOKEN"); err == nil { + csrfCookie = c.Value + } else if c, err := r.Cookie("XSRF-TOKEN-VALUE"); err == nil { + csrfCookie = c.Value + } data := map[string]interface{}{ - "Title": "Generic", + "Title": "Generic", + "Session": session, + "CSRFField": csrf.TemplateField(r), + "CSRFToken": csrf.Token(r), + "CSRFCookie": csrfCookie, + } + + if r.Header.Get("HX-Request") == "true" { + if err := tmpl.ExecuteTemplate(w, "generic_content", data); err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } + return + } + + var contentBuf bytes.Buffer + if err := tmpl.ExecuteTemplate(&contentBuf, "generic_content", data); err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return } + data["BodyContent"] = template.HTML(contentBuf.String()) - err := tmpl.Execute(w, data) - if err != nil { + if err := tmpl.ExecuteTemplate(w, "layout.html", data); err != nil { http.Error(w, "Internal Server Error", http.StatusInternalServerError) } } diff --git a/internal/middleware/csrf_simple.go b/internal/middleware/csrf_simple.go index 013920b..c278c22 100644 --- a/internal/middleware/csrf_simple.go +++ b/internal/middleware/csrf_simple.go @@ -3,6 +3,7 @@ package middleware import ( "crypto/rand" "encoding/base64" + "log" "net/http" "strings" "time" @@ -40,9 +41,31 @@ func CSRFSimple(next http.Handler) http.Handler { } // For unsafe methods, require header matches cookie - headerToken := r.Header.Get("X-CSRF-Token") + token := r.Header.Get("X-CSRF-Token") + if token == "" { + contentType := r.Header.Get("Content-Type") + if strings.Contains(contentType, "multipart/form-data") { + if err := r.ParseMultipartForm(10 << 20); err == nil { + token = r.PostFormValue("csrfToken") + } + } else if err := r.ParseForm(); err == nil { + token = r.PostFormValue("csrfToken") + } + } + cookie, err := r.Cookie("XSRF-TOKEN") - if err != nil || headerToken == "" || cookie == nil || cookie.Value == "" || cookie.Value != headerToken { + if err != nil || token == "" || cookie == nil || cookie.Value == "" || cookie.Value != token { + if token == "" { + if headerToken := r.Header.Get("X-CSRF-Token"); headerToken != "" { + token = headerToken + } + } + log.Printf("CSRF validation failed (simple mode): header=%q form=%q cookie=%q err=%v", r.Header.Get("X-CSRF-Token"), token, func() string { + if cookie != nil { + return cookie.Value + } + return "" + }(), err) http.Error(w, "Forbidden - CSRF", http.StatusForbidden) return } diff --git a/templates/generic.html b/templates/generic.html index 70cdb92..1119af8 100644 --- a/templates/generic.html +++ b/templates/generic.html @@ -7,12 +7,73 @@
-

Generic tools and utilities will be implemented here.

-
-
🔧
-

Coming Soon

-

Generic tools and utilities are under development.

-
+ +
+

Invoice Clock Events Report (Proof of Concept)

+

+ Upload a CSV containing invoice numbers to generate a downloadable report that aggregates + related job information, assigned technicians, and enroute/onsite clock events. +

+ +
+ {{if .CSRFField}} + {{.CSRFField}} + {{end}} + +
+ + +
+

+ The CSV may include a header named invoice_number; if no header is present, the first + column + is used. Each invoice number is processed individually. +

+ +
+
+ {{end}} \ No newline at end of file