Centralising auth, making pdf retrieval via authenticated request

This commit is contained in:
Finley Ghosh 2025-12-07 12:27:41 +11:00
parent 46cf098dfa
commit 57047e3b32
3 changed files with 173 additions and 57 deletions

View file

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

View file

@ -7,11 +7,10 @@ import (
"strconv" "strconv"
"time" "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/db"
"code.springupsoftware.com/cmc/cmc-sales/internal/cmc/templates" "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/templates"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"golang.org/x/text/cases"
"golang.org/x/text/language"
) )
type PageHandler struct { 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 // Helper function to get the username from the request
func getUsername(r *http.Request) string { func getUsername(r *http.Request) string {
username, _, ok := r.BasicAuth() return auth.GetUsername(r)
if ok && username != "" {
caser := cases.Title(language.English)
return caser.String(username) // Capitalise the username for display
}
return "Guest"
} }
// Home page // Home page

View file

@ -12,19 +12,14 @@ import (
"strings" "strings"
"time" "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/db"
"code.springupsoftware.com/cmc/cmc-sales/internal/cmc/templates" "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/templates"
) )
// Import getUsername from pages.go // getUsername is a wrapper around auth.GetUsername for backwards compatibility
// If you move getUsername to a shared utils package, update the import accordingly.
func getUsername(r *http.Request) string { func getUsername(r *http.Request) string {
username, _, ok := r.BasicAuth() return auth.GetUsername(r)
if ok && username != "" {
// Capitalise the username for display
return strings.Title(username)
}
return "Guest"
} }
// Helper: returns date string or empty if zero // 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 // 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 // Safeguard: check for valid recipient
if strings.TrimSpace(recipient) == "" { if strings.TrimSpace(recipient) == "" {
return fmt.Errorf("recipient email is required") return fmt.Errorf("recipient email is required")
@ -506,7 +503,14 @@ func (h *QuotesHandler) SendQuoteReminderEmailWithAttachment(ctx context.Context
// Download PDF from URL and prepare attachment if available // Download PDF from URL and prepare attachment if available
var attachments []interface{} var attachments []interface{}
resp, err := http.Get(pdfPath)
// 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 { if err == nil && resp.StatusCode == 200 {
defer resp.Body.Close() defer resp.Body.Close()
@ -528,6 +532,7 @@ func (h *QuotesHandler) SendQuoteReminderEmailWithAttachment(ctx context.Context
FilePath: tmpFile.Name(), FilePath: tmpFile.Name(),
}, },
} }
log.Printf("Successfully downloaded and attached PDF for quote %d: %s", quoteID, pdfFilename)
} else { } else {
log.Printf("Failed to copy PDF content for quote %d: %v", quoteID, err) log.Printf("Failed to copy PDF content for quote %d: %v", quoteID, err)
} }
@ -540,9 +545,11 @@ func (h *QuotesHandler) SendQuoteReminderEmailWithAttachment(ctx context.Context
defer resp.Body.Close() defer resp.Body.Close()
log.Printf("PDF download returned status %d for quote %d at %s", resp.StatusCode, quoteID, pdfPath) 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 // If PDF download fails, just send email without attachment
// Send the email (with or 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( err = h.emailService.SendTemplateEmailWithAttachments(
recipient, recipient,
subject, subject,
@ -577,6 +584,8 @@ func (h *QuotesHandler) SendQuoteReminderEmailWithAttachment(ctx context.Context
// SendManualReminder handles POST requests to manually send a quote reminder // SendManualReminder handles POST requests to manually send a quote reminder
func (h *QuotesHandler) SendManualReminder(w http.ResponseWriter, r *http.Request) { func (h *QuotesHandler) SendManualReminder(w http.ResponseWriter, r *http.Request) {
log.Printf("SendManualReminder handler called")
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return return
@ -643,6 +652,7 @@ func (h *QuotesHandler) SendManualReminder(w http.ResponseWriter, r *http.Reques
// Attach PDF quote from URL // Attach PDF quote from URL
pdfURL := fmt.Sprintf("https://stg.cmctechnologies.com.au/pdf/%s.pdf", enquiryRef) pdfURL := fmt.Sprintf("https://stg.cmctechnologies.com.au/pdf/%s.pdf", enquiryRef)
pdfFilename := fmt.Sprintf("%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 // Get username from request
username := getUsername(r) username := getUsername(r)
@ -661,13 +671,16 @@ func (h *QuotesHandler) SendManualReminder(w http.ResponseWriter, r *http.Reques
usernamePtr, usernamePtr,
pdfURL, pdfURL,
pdfFilename, pdfFilename,
r,
) )
if err != nil { 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) http.Error(w, fmt.Sprintf("Failed to send reminder: %v", err), http.StatusInternalServerError)
return return
} }
log.Printf("Manual reminder sent successfully for quote %d (%s) to %s", quoteID, enquiryRef, customerEmail)
// Redirect back to quotes page // Redirect back to quotes page
http.Redirect(w, r, "/go/quotes", http.StatusSeeOther) http.Redirect(w, r, "/go/quotes", http.StatusSeeOther)
} }