From 57047e3b324302f74056e77d0b5b2ca4435b0f16 Mon Sep 17 00:00:00 2001 From: Finley Ghosh Date: Sun, 7 Dec 2025 12:27:41 +1100 Subject: [PATCH] Centralising auth, making pdf retrieval via authenticated request --- go/internal/cmc/auth/auth.go | 109 ++++++++++++++++++++++ go/internal/cmc/handlers/pages.go | 36 +++---- go/internal/cmc/handlers/quotes/quotes.go | 85 ++++++++++------- 3 files changed, 173 insertions(+), 57 deletions(-) create mode 100644 go/internal/cmc/auth/auth.go diff --git a/go/internal/cmc/auth/auth.go b/go/internal/cmc/auth/auth.go new file mode 100644 index 00000000..f02b96bd --- /dev/null +++ b/go/internal/cmc/auth/auth.go @@ -0,0 +1,109 @@ +package auth + +import ( + "context" + "net/http" + "strings" +) + +// ContextKey is a type for context keys to avoid collisions +type ContextKey string + +const ( + // ContextKeyUsername is the context key for storing the authenticated username + ContextKeyUsername ContextKey = "username" + // ContextKeyAuthUser is the context key for storing the raw auth username + ContextKeyAuthUser ContextKey = "auth_user" + // ContextKeyAuthPass is the context key for storing the raw auth password + ContextKeyAuthPass ContextKey = "auth_pass" +) + +// Credentials holds authentication credentials +type Credentials struct { + Username string + Password string +} + +// GetCredentials extracts authentication credentials from the request +// This is the single point where auth mechanism is defined +func GetCredentials(r *http.Request) (*Credentials, bool) { + username, password, ok := r.BasicAuth() + if !ok || username == "" { + return nil, false + } + return &Credentials{ + Username: username, + Password: password, + }, true +} + +// GetUsername extracts and formats the username for display +func GetUsername(r *http.Request) string { + creds, ok := GetCredentials(r) + if !ok { + return "Guest" + } + // Capitalize the username for display + return strings.Title(creds.Username) +} + +// GetUsernameFromContext retrieves the username from the request context +func GetUsernameFromContext(ctx context.Context) string { + if username, ok := ctx.Value(ContextKeyUsername).(string); ok { + return username + } + return "Guest" +} + +// GetCredentialsFromContext retrieves credentials from the request context +func GetCredentialsFromContext(ctx context.Context) (*Credentials, bool) { + username, okUser := ctx.Value(ContextKeyAuthUser).(string) + password, okPass := ctx.Value(ContextKeyAuthPass).(string) + if !okUser || !okPass { + return nil, false + } + return &Credentials{ + Username: username, + Password: password, + }, true +} + +// AddAuthToRequest adds authentication credentials to an HTTP request +// This should be used when making authenticated requests to internal services +func AddAuthToRequest(req *http.Request, creds *Credentials) { + if creds != nil { + req.SetBasicAuth(creds.Username, creds.Password) + } +} + +// NewAuthenticatedRequest creates a new HTTP request with authentication from the source request +// This is a convenience method that extracts auth from sourceReq and applies it to the new request +func NewAuthenticatedRequest(method, url string, sourceReq *http.Request) (*http.Request, error) { + req, err := http.NewRequest(method, url, nil) + if err != nil { + return nil, err + } + + // Copy authentication from source request + if creds, ok := GetCredentials(sourceReq); ok { + AddAuthToRequest(req, creds) + } + + return req, nil +} + +// Middleware adds authentication information to the request context +// This allows handlers to access auth info without parsing headers repeatedly +func Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + creds, ok := GetCredentials(r) + if ok { + ctx := r.Context() + ctx = context.WithValue(ctx, ContextKeyUsername, strings.Title(creds.Username)) + ctx = context.WithValue(ctx, ContextKeyAuthUser, creds.Username) + ctx = context.WithValue(ctx, ContextKeyAuthPass, creds.Password) + r = r.WithContext(ctx) + } + next.ServeHTTP(w, r) + }) +} diff --git a/go/internal/cmc/handlers/pages.go b/go/internal/cmc/handlers/pages.go index 73417f2a..f6e119d7 100644 --- a/go/internal/cmc/handlers/pages.go +++ b/go/internal/cmc/handlers/pages.go @@ -7,11 +7,10 @@ import ( "strconv" "time" + "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/auth" "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db" "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/templates" "github.com/gorilla/mux" - "golang.org/x/text/cases" - "golang.org/x/text/language" ) type PageHandler struct { @@ -30,12 +29,7 @@ func NewPageHandler(queries *db.Queries, tmpl *templates.TemplateManager, databa // Helper function to get the username from the request func getUsername(r *http.Request) string { - username, _, ok := r.BasicAuth() - if ok && username != "" { - caser := cases.Title(language.English) - return caser.String(username) // Capitalise the username for display - } - return "Guest" + return auth.GetUsername(r) } // Home page @@ -839,7 +833,7 @@ func (h *PageHandler) EmailsIndex(w http.ResponseWriter, r *http.Request) { u.email as user_email, u.first_name, u.last_name FROM emails e LEFT JOIN users u ON e.user_id = u.id` - + var args []interface{} var conditions []string @@ -882,16 +876,16 @@ func (h *PageHandler) EmailsIndex(w http.ResponseWriter, r *http.Request) { defer rows.Close() type EmailWithUser struct { - ID int32 `json:"id"` - Subject string `json:"subject"` - UserID int32 `json:"user_id"` - Created time.Time `json:"created"` - GmailMessageID *string `json:"gmail_message_id"` - AttachmentCount int32 `json:"attachment_count"` - IsDownloaded *bool `json:"is_downloaded"` - UserEmail *string `json:"user_email"` - FirstName *string `json:"first_name"` - LastName *string `json:"last_name"` + ID int32 `json:"id"` + Subject string `json:"subject"` + UserID int32 `json:"user_id"` + Created time.Time `json:"created"` + GmailMessageID *string `json:"gmail_message_id"` + AttachmentCount int32 `json:"attachment_count"` + IsDownloaded *bool `json:"is_downloaded"` + UserEmail *string `json:"user_email"` + FirstName *string `json:"first_name"` + LastName *string `json:"last_name"` } var emails []EmailWithUser @@ -1080,7 +1074,7 @@ func (h *PageHandler) EmailsShow(w http.ResponseWriter, r *http.Request) { var attachments []EmailAttachment hasStoredAttachments := false - + if attachmentRows != nil { defer attachmentRows.Close() for attachmentRows.Next() { @@ -1179,7 +1173,7 @@ func (h *PageHandler) EmailsShow(w http.ResponseWriter, r *http.Request) { func (h *PageHandler) EmailsSearch(w http.ResponseWriter, r *http.Request) { _ = r.URL.Query().Get("search") // TODO: Implement search functionality - + // Empty result for now - would need proper implementation emails := []interface{}{} diff --git a/go/internal/cmc/handlers/quotes/quotes.go b/go/internal/cmc/handlers/quotes/quotes.go index 849e0ed2..4a661dfc 100644 --- a/go/internal/cmc/handlers/quotes/quotes.go +++ b/go/internal/cmc/handlers/quotes/quotes.go @@ -12,19 +12,14 @@ import ( "strings" "time" + "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/auth" "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db" "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/templates" ) -// Import getUsername from pages.go -// If you move getUsername to a shared utils package, update the import accordingly. +// getUsername is a wrapper around auth.GetUsername for backwards compatibility func getUsername(r *http.Request) string { - username, _, ok := r.BasicAuth() - if ok && username != "" { - // Capitalise the username for display - return strings.Title(username) - } - return "Guest" + return auth.GetUsername(r) } // Helper: returns date string or empty if zero @@ -490,7 +485,9 @@ func (h *QuotesHandler) SendQuoteReminderEmail(ctx context.Context, quoteID int3 } // SendQuoteReminderEmailWithAttachment is like SendQuoteReminderEmail but includes a PDF attachment -func (h *QuotesHandler) SendQuoteReminderEmailWithAttachment(ctx context.Context, quoteID int32, reminderType QuoteReminderType, recipient string, subject string, templateName string, templateData map[string]interface{}, ccs []string, username *string, pdfPath string, pdfFilename string) error { +func (h *QuotesHandler) SendQuoteReminderEmailWithAttachment(ctx context.Context, quoteID int32, reminderType QuoteReminderType, recipient string, subject string, templateName string, templateData map[string]interface{}, ccs []string, username *string, pdfPath string, pdfFilename string, sourceReq *http.Request) error { + log.Printf("SendQuoteReminderEmailWithAttachment called for quote %d, recipient: %s, PDF URL: %s", quoteID, recipient, pdfPath) + // Safeguard: check for valid recipient if strings.TrimSpace(recipient) == "" { return fmt.Errorf("recipient email is required") @@ -506,43 +503,53 @@ func (h *QuotesHandler) SendQuoteReminderEmailWithAttachment(ctx context.Context // Download PDF from URL and prepare attachment if available var attachments []interface{} - resp, err := http.Get(pdfPath) - if err == nil && resp.StatusCode == 200 { - defer resp.Body.Close() - // Create temporary file for the PDF - tmpFile, err := os.CreateTemp("", "quote_*.pdf") - if err == nil { - defer os.Remove(tmpFile.Name()) - defer tmpFile.Close() + // Create authenticated request for PDF download + req, err := auth.NewAuthenticatedRequest("GET", pdfPath, sourceReq) + if err != nil { + log.Printf("Failed to create PDF download request for quote %d: %v", quoteID, err) + } else { + client := &http.Client{} + resp, err := client.Do(req) + if err == nil && resp.StatusCode == 200 { + defer resp.Body.Close() - // Copy PDF content to temp file - if _, err := io.Copy(tmpFile, resp.Body); err == nil { - type Attachment struct { - Filename string - FilePath string - } - attachments = []interface{}{ - Attachment{ - Filename: pdfFilename, - FilePath: tmpFile.Name(), - }, + // Create temporary file for the PDF + tmpFile, err := os.CreateTemp("", "quote_*.pdf") + if err == nil { + defer os.Remove(tmpFile.Name()) + defer tmpFile.Close() + + // Copy PDF content to temp file + if _, err := io.Copy(tmpFile, resp.Body); err == nil { + type Attachment struct { + Filename string + FilePath string + } + attachments = []interface{}{ + Attachment{ + Filename: pdfFilename, + FilePath: tmpFile.Name(), + }, + } + log.Printf("Successfully downloaded and attached PDF for quote %d: %s", quoteID, pdfFilename) + } else { + log.Printf("Failed to copy PDF content for quote %d: %v", quoteID, err) } } else { - log.Printf("Failed to copy PDF content for quote %d: %v", quoteID, err) + log.Printf("Failed to create temporary file for quote %d PDF: %v", quoteID, err) } - } else { - log.Printf("Failed to create temporary file for quote %d PDF: %v", quoteID, err) + } else if err != nil { + log.Printf("Failed to download PDF from %s for quote %d: %v", pdfPath, quoteID, err) + } else if resp != nil { + defer resp.Body.Close() + log.Printf("PDF download returned status %d for quote %d at %s", resp.StatusCode, quoteID, pdfPath) } - } else if err != nil { - log.Printf("Failed to download PDF from %s for quote %d: %v", pdfPath, quoteID, err) - } else if resp != nil { - defer resp.Body.Close() - log.Printf("PDF download returned status %d for quote %d at %s", resp.StatusCode, quoteID, pdfPath) } // If PDF download fails, just send email without attachment // Send the email (with or without attachment) + log.Printf("Sending email for quote %d with %d attachment(s)", quoteID, len(attachments)) err = h.emailService.SendTemplateEmailWithAttachments( recipient, subject, @@ -577,6 +584,8 @@ func (h *QuotesHandler) SendQuoteReminderEmailWithAttachment(ctx context.Context // SendManualReminder handles POST requests to manually send a quote reminder func (h *QuotesHandler) SendManualReminder(w http.ResponseWriter, r *http.Request) { + log.Printf("SendManualReminder handler called") + if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return @@ -643,6 +652,7 @@ func (h *QuotesHandler) SendManualReminder(w http.ResponseWriter, r *http.Reques // Attach PDF quote from URL pdfURL := fmt.Sprintf("https://stg.cmctechnologies.com.au/pdf/%s.pdf", enquiryRef) pdfFilename := fmt.Sprintf("%s.pdf", enquiryRef) + log.Printf("Manual reminder for quote %d (%s): recipient=%s, pdfURL=%s", quoteID, enquiryRef, customerEmail, pdfURL) // Get username from request username := getUsername(r) @@ -661,13 +671,16 @@ func (h *QuotesHandler) SendManualReminder(w http.ResponseWriter, r *http.Reques usernamePtr, pdfURL, pdfFilename, + r, ) if err != nil { + log.Printf("Failed to send manual reminder for quote %d: %v", quoteID, err) http.Error(w, fmt.Sprintf("Failed to send reminder: %v", err), http.StatusInternalServerError) return } + log.Printf("Manual reminder sent successfully for quote %d (%s) to %s", quoteID, enquiryRef, customerEmail) // Redirect back to quotes page http.Redirect(w, r, "/go/quotes", http.StatusSeeOther) }