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"
|
||||
"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{}{}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue