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.
+
+
+
+
+
{{end}}
\ No newline at end of file