Centralising auth, making pdf retrieval via authenticated request
This commit is contained in:
parent
46cf098dfa
commit
57047e3b32
109
go/internal/cmc/auth/auth.go
Normal file
109
go/internal/cmc/auth/auth.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue