Browse Source

updated auth and added session store; got invoice search working

cli-archive
nic 2 years ago
parent
commit
119251db79
  1. 2
      apps/web/main.go
  2. 89
      internal/api/api.go
  3. 46
      internal/api/session_store.go
  4. 6
      internal/handlers/dashboard.go
  5. 46
      internal/handlers/invoices.go
  6. 39
      internal/handlers/login.go
  7. 22
      internal/middleware/auth_middleware.go
  8. 1
      templates/layout.html
  9. 1
      templates/partials/invoice_search.html
  10. 25
      templates/partials/invoice_search_results.html
  11. 2
      templates/partials/jobs.html

2
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")

89
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")
}

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

6
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)

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

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

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

1
templates/layout.html

@ -13,6 +13,7 @@
<h1 class="title">ServiceTrade Tools</h1>
<nav>
<ul>
<li><a href="/" hx-get="/" hx-target="#content">Dashboard</a></li>
<li><a href="/jobs" hx-get="/jobs" hx-target="#content">Jobs</a></li>
<li><a href="/assets" hx-get="/assets" hx-target="#content">Assets</a></li>
<li><a href="/companies" hx-get="/companies" hx-target="#content">Companies</a></li>

1
templates/partials/invoice_search.html

@ -4,6 +4,7 @@
type="text"
name="search"
id="invoice-search-input"
hx-get="/invoices"
hx-trigger="keyup changed delay:300ms"
hx-target="#invoice-search-results"
hx-indicator="#invoice-search-spinner" />

25
templates/partials/invoice_search_results.html

@ -1,8 +1,23 @@
{{define "invoice_search_results"}} {{if .Error}}
<p class="error">Error: {{.Error}}</p>
{{else if .invoiceNumber}}
<h3>Invoice Details</h3>
<p>Invoice Number: {{.invoiceNumber}}</p>
<p>Total Price: ${{.totalPrice}}</p>
<p>Status: {{.status}}</p>
{{with .customer}}
<p>Customer: {{.name}}</p>
{{end}} {{with .job}}
<p>Job: {{.name}}</p>
{{end}} {{with .location}}
<p>Location: {{.name}}</p>
{{end}} {{if .items}}
<h4>Items:</h4>
<ul>
{{range .}}
<li>
<!-- Customize this based on your invoice data structure -->
Invoice: {{.Number}} - Amount: {{.Amount}}
</li>
{{range .items}}
<li>{{.description}} - ${{.totalPrice}}</li>
{{end}}
</ul>
{{end}} {{else}}
<p>No invoice found with the given identifier.</p>
{{end}} {{end}}

2
templates/partials/jobs.html

@ -1,3 +1,4 @@
{{define "content"}}
<div class="submenu-container">
<h2 class="submenu-header">Jobs</h2>
<div class="submenu-grid">
@ -24,3 +25,4 @@
</div>
</div>
</div>
{{end}}

Loading…
Cancel
Save