diff --git a/apps/web/main.go b/apps/web/main.go index d92e6be..d02642e 100644 --- a/apps/web/main.go +++ b/apps/web/main.go @@ -32,7 +32,7 @@ func main() { protected.HandleFunc("/contacts", handlers.ContactsHandler).Methods("GET") protected.HandleFunc("/contracts", handlers.ContractsHandler).Methods("GET") protected.HandleFunc("/generic", handlers.GenericHandler).Methods("GET") - protected.HandleFunc("/invoices", handlers.InvoicesHandler).Methods("GET") + protected.HandleFunc("/invoices", handlers.InvoicesHandler).Methods("GET", "POST") protected.HandleFunc("/locations", handlers.LocationsHandler).Methods("GET") protected.HandleFunc("/notifications", handlers.NotificationsHandler).Methods("GET") protected.HandleFunc("/quotes", handlers.QuotesHandler).Methods("GET") diff --git a/internal/api/api.go b/internal/api/api.go index 51e80d2..7152b1c 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -6,16 +6,45 @@ import ( "fmt" "io" "net/http" + "regexp" + "strconv" "strings" + "time" ) type Session struct { - Client *http.Client - Cookie string + Client *http.Client + Cookie string + LastAccessed time.Time } func NewSession() *Session { - return &Session{Client: &http.Client{}} + return &Session{ + Client: &http.Client{}, + LastAccessed: time.Now(), + } +} + +func (s *Session) ValidateSession() error { + url := "https://api.servicetrade.com/api/auth/validate" + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return err + } + req.Header.Set("Cookie", s.Cookie) + + resp, err := s.Client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("session validation failed") + } + + s.LastAccessed = time.Now() + return nil } func (s *Session) Login(email, password string) error { @@ -194,3 +223,57 @@ func (s *Session) GetDeficiencyById(deficiencyId string) (map[string]interface{} return result, nil } + +func (s *Session) GetInvoice(identifier string) (map[string]interface{}, error) { + var url string + + // Regular expression to check if the identifier starts with a letter + isInvoiceNumber, _ := regexp.MatchString(`^[A-Za-z]`, identifier) + + if isInvoiceNumber { + url = fmt.Sprintf("https://api.servicetrade.com/api/invoice?invoiceNumber=%s", identifier) + } else { + // Check if the identifier is a valid number + if _, err := strconv.Atoi(identifier); err != nil { + return nil, fmt.Errorf("invalid invoice identifier: %s", identifier) + } + url = fmt.Sprintf("https://api.servicetrade.com/api/invoice/%s", identifier) + } + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + req.Header.Set("Cookie", s.Cookie) + + resp, err := s.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error sending request: %v", err) + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != 200 { + return nil, fmt.Errorf("failed to get invoice info: %s, response: %s", resp.Status, string(body)) + } + + var result map[string]interface{} + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("error unmarshalling response: %v, body: %s", err, string(body)) + } + + // Check if the response contains a 'data' field + if data, ok := result["data"].(map[string]interface{}); ok { + // If 'invoices' field exists, it's a search by invoice number + if invoices, ok := data["invoices"].([]interface{}); ok && len(invoices) > 0 { + if invoice, ok := invoices[0].(map[string]interface{}); ok { + return invoice, nil + } + } else { + // If 'invoices' doesn't exist, it's a direct invoice lookup by ID + return data, nil + } + } + + return nil, fmt.Errorf("no invoice found in the response") +} diff --git a/internal/api/session_store.go b/internal/api/session_store.go new file mode 100644 index 0000000..425a3b9 --- /dev/null +++ b/internal/api/session_store.go @@ -0,0 +1,46 @@ +package api + +import ( + "sync" + "time" +) + +type SessionStore struct { + sessions map[string]*Session + mu sync.RWMutex +} + +func NewSessionStore() *SessionStore { + return &SessionStore{ + sessions: make(map[string]*Session), + } +} + +func (store *SessionStore) Set(sessionID string, session *Session) { + store.mu.Lock() + defer store.mu.Unlock() + store.sessions[sessionID] = session +} + +func (store *SessionStore) Get(sessionID string) (*Session, bool) { + store.mu.RLock() + defer store.mu.RUnlock() + session, ok := store.sessions[sessionID] + return session, ok +} + +func (store *SessionStore) Delete(sessionID string) { + store.mu.Lock() + defer store.mu.Unlock() + delete(store.sessions, sessionID) +} + +func (store *SessionStore) CleanupSessions() { + store.mu.Lock() + defer store.mu.Unlock() + for id, session := range store.sessions { + if time.Since(session.LastAccessed) > 24*time.Hour { + delete(store.sessions, id) + } + } +} diff --git a/internal/handlers/dashboard.go b/internal/handlers/dashboard.go index 895cb0f..ecd687a 100644 --- a/internal/handlers/dashboard.go +++ b/internal/handlers/dashboard.go @@ -21,7 +21,11 @@ func DashboardHandler(w http.ResponseWriter, r *http.Request) { data := struct{}{} // Empty struct as data - err = tmpl.ExecuteTemplate(w, "layout.html", data) + if r.Header.Get("HX-Request") == "true" { + err = tmpl.ExecuteTemplate(w, "content", data) + } else { + err = tmpl.ExecuteTemplate(w, "layout.html", data) + } if err != nil { log.Printf("Template execution error: %v", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) diff --git a/internal/handlers/invoices.go b/internal/handlers/invoices.go index a233738..0b4f995 100644 --- a/internal/handlers/invoices.go +++ b/internal/handlers/invoices.go @@ -2,10 +2,54 @@ package handlers import ( "html/template" + "log" + "marmic/servicetrade-toolbox/internal/api" "net/http" ) func InvoicesHandler(w http.ResponseWriter, r *http.Request) { + session, ok := r.Context().Value("session").(*api.Session) + if !ok { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + // Handle the search request + if r.Method == "GET" && r.URL.Query().Get("search") != "" { + handleInvoiceSearch(w, r, session) + return + } + + // Handle the initial page load tmpl := template.Must(template.ParseFiles("templates/layout.html", "templates/invoices.html")) - tmpl.Execute(w, nil) + err := tmpl.Execute(w, nil) + if err != nil { + log.Printf("Error executing template: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } +} + +func handleInvoiceSearch(w http.ResponseWriter, r *http.Request, session *api.Session) { + invoiceIdentifier := r.URL.Query().Get("search") + + if invoiceIdentifier == "" { + w.Write([]byte("")) + return + } + + invoice, err := session.GetInvoice(invoiceIdentifier) + if err != nil { + log.Printf("Error fetching invoice: %v", err) + w.WriteHeader(http.StatusInternalServerError) + tmpl := template.Must(template.ParseFiles("templates/partials/invoice_search_results.html")) + tmpl.ExecuteTemplate(w, "invoice_search_results", map[string]interface{}{"Error": err.Error()}) + return + } + + tmpl := template.Must(template.ParseFiles("templates/partials/invoice_search_results.html")) + err = tmpl.ExecuteTemplate(w, "invoice_search_results", invoice) + if err != nil { + log.Printf("Error executing template: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } } diff --git a/internal/handlers/login.go b/internal/handlers/login.go index 6c278fd..3f31a4e 100644 --- a/internal/handlers/login.go +++ b/internal/handlers/login.go @@ -4,12 +4,12 @@ import ( "html/template" "log" "marmic/servicetrade-toolbox/internal/api" + "marmic/servicetrade-toolbox/internal/middleware" "net/http" "strings" ) func LoginHandler(w http.ResponseWriter, r *http.Request) { - if r.Method == "GET" { tmpl := template.Must(template.ParseFiles("templates/login.html")) tmpl.Execute(w, nil) @@ -30,12 +30,15 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { } return } + cookieParts := strings.Split(session.Cookie, ";") - sessionId := strings.TrimPrefix(cookieParts[0], "PHPSESSID=") - // Set session cookie + sessionID := strings.TrimPrefix(cookieParts[0], "PHPSESSID=") + + middleware.SessionStore.Set(sessionID, session) + http.SetCookie(w, &http.Cookie{ Name: "PHPSESSID", - Value: sessionId, + Value: sessionID, Path: "/", HttpOnly: true, Secure: r.TLS != nil, @@ -56,20 +59,17 @@ func LogoutHandler(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("PHPSESSID") if err != nil { log.Printf("No session cookie found: %v", err) - - // Check if the request is an HTMX request - if r.Header.Get("HX-Request") != "" { - // Use HX-Redirect to redirect the entire page to the login page - w.Header().Set("HX-Redirect", "/login") - w.WriteHeader(http.StatusOK) - } else { - http.Redirect(w, r, "/login", http.StatusSeeOther) - } + redirectToLogin(w, r) return } - session := api.NewSession() - session.Cookie = "PHPSESSID=" + cookie.Value + sessionID := cookie.Value + session, exists := middleware.SessionStore.Get(sessionID) + if !exists { + log.Println("No session found in store") + redirectToLogin(w, r) + return + } err = session.Logout() if err != nil { @@ -78,7 +78,8 @@ func LogoutHandler(w http.ResponseWriter, r *http.Request) { return } - // Clear the session cookie + middleware.SessionStore.Delete(sessionID) + http.SetCookie(w, &http.Cookie{ Name: "PHPSESSID", Value: "", @@ -90,14 +91,14 @@ func LogoutHandler(w http.ResponseWriter, r *http.Request) { }) log.Println("Logout successful, redirecting to login page") + redirectToLogin(w, r) +} - // Check if the request is an HTMX request +func redirectToLogin(w http.ResponseWriter, r *http.Request) { if r.Header.Get("HX-Request") != "" { - // Use HX-Redirect to ensure the entire page is redirected to the login page w.Header().Set("HX-Redirect", "/login") w.WriteHeader(http.StatusOK) } else { - // If not an HTMX request, perform a full-page redirect http.Redirect(w, r, "/login", http.StatusSeeOther) } } diff --git a/internal/middleware/auth_middleware.go b/internal/middleware/auth_middleware.go index d7272b5..461bc97 100644 --- a/internal/middleware/auth_middleware.go +++ b/internal/middleware/auth_middleware.go @@ -1,10 +1,13 @@ package middleware import ( + "context" "marmic/servicetrade-toolbox/internal/api" "net/http" ) +var SessionStore = api.NewSessionStore() + func AuthMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("PHPSESSID") @@ -13,12 +16,21 @@ func AuthMiddleware(next http.Handler) http.Handler { return } - session := api.NewSession() - session.Cookie = "PHPSESSID=" + cookie.Value + sessionID := cookie.Value + session, exists := SessionStore.Get(sessionID) + if !exists { + session = api.NewSession() + session.Cookie = "PHPSESSID=" + sessionID + + if err := session.ValidateSession(); err != nil { + http.Redirect(w, r, "/login", http.StatusSeeOther) + return + } - // You might want to add a method to validate the session token - // For now, we'll assume if the cookie exists, the session is valid + SessionStore.Set(sessionID, session) + } - next.ServeHTTP(w, r) + ctx := context.WithValue(r.Context(), "session", session) + next.ServeHTTP(w, r.WithContext(ctx)) }) } diff --git a/templates/layout.html b/templates/layout.html index 72a5303..692e5f4 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -13,6 +13,7 @@