Files
honeyDueAPI/internal/services/pdf_service.go
Trey t 42a5533a56 Fix 113 hardening issues across entire Go backend
Security:
- Replace all binding: tags with validate: + c.Validate() in admin handlers
- Add rate limiting to auth endpoints (login, register, password reset)
- Add security headers (HSTS, XSS protection, nosniff, frame options)
- Wire Google Pub/Sub token verification into webhook handler
- Replace ParseUnverified with proper OIDC/JWKS key verification
- Verify inner Apple JWS signatures in webhook handler
- Add io.LimitReader (1MB) to all webhook body reads
- Add ownership verification to file deletion
- Move hardcoded admin credentials to env vars
- Add uniqueIndex to User.Email
- Hide ConfirmationCode from JSON serialization
- Mask confirmation codes in admin responses
- Use http.DetectContentType for upload validation
- Fix path traversal in storage service
- Replace os.Getenv with Viper in stripe service
- Sanitize Redis URLs before logging
- Separate DEBUG_FIXED_CODES from DEBUG flag
- Reject weak SECRET_KEY in production
- Add host check on /_next/* proxy routes
- Use explicit localhost CORS origins in debug mode
- Replace err.Error() with generic messages in all admin error responses

Critical fixes:
- Rewrite FCM to HTTP v1 API with OAuth 2.0 service account auth
- Fix user_customuser -> auth_user table names in raw SQL
- Fix dashboard verified query to use UserProfile model
- Add escapeLikeWildcards() to prevent SQL wildcard injection

Bug fixes:
- Add bounds checks for days/expiring_soon query params (1-3650)
- Add receipt_data/transaction_id empty-check to RestoreSubscription
- Change Active bool -> *bool in device handler
- Check all unchecked GORM/FindByIDWithProfile errors
- Add validation for notification hour fields (0-23)
- Add max=10000 validation on task description updates

Transactions & data integrity:
- Wrap registration flow in transaction
- Wrap QuickComplete in transaction
- Move image creation inside completion transaction
- Wrap SetSpecialties in transaction
- Wrap GetOrCreateToken in transaction
- Wrap completion+image deletion in transaction

Performance:
- Batch completion summaries (2 queries vs 2N)
- Reuse single http.Client in IAP validation
- Cache dashboard counts (30s TTL)
- Batch COUNT queries in admin user list
- Add Limit(500) to document queries
- Add reminder_stage+due_date filters to reminder queries
- Parse AllowedTypes once at init
- In-memory user cache in auth middleware (30s TTL)
- Timezone change detection cache
- Optimize P95 with per-endpoint sorted buffers
- Replace crypto/md5 with hash/fnv for ETags

Code quality:
- Add sync.Once to all monitoring Stop()/Close() methods
- Replace 8 fmt.Printf with zerolog in auth service
- Log previously discarded errors
- Standardize delete response shapes
- Route hardcoded English through i18n
- Remove FileURL from DocumentResponse (keep MediaURL only)
- Thread user timezone through kanban board responses
- Initialize empty slices to prevent null JSON
- Extract shared field map for task Update/UpdateTx
- Delete unused SoftDeleteModel, min(), formatCron, legacy handlers

Worker & jobs:
- Wire Asynq email infrastructure into worker
- Register HandleReminderLogCleanup with daily 3AM cron
- Use per-user timezone in HandleSmartReminder
- Replace direct DB queries with repository calls
- Delete legacy reminder handlers (~200 lines)
- Delete unused task type constants

Dependencies:
- Replace archived jung-kurt/gofpdf with go-pdf/fpdf
- Replace unmaintained gomail.v2 with wneessen/go-mail
- Add TODO for Echo jwt v3 transitive dep removal

Test infrastructure:
- Fix MakeRequest/SeedLookupData error handling
- Replace os.Exit(0) with t.Skip() in scope/consistency tests
- Add 11 new FCM v1 tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:13 -05:00

183 lines
4.9 KiB
Go

package services
import (
"bytes"
"fmt"
"time"
"github.com/go-pdf/fpdf"
)
// PDFService handles PDF generation
type PDFService struct{}
// NewPDFService creates a new PDF service
func NewPDFService() *PDFService {
return &PDFService{}
}
// GenerateTasksReportPDF generates a PDF report from task report data
func (s *PDFService) GenerateTasksReportPDF(report *TasksReportResponse) ([]byte, error) {
pdf := fpdf.New("P", "mm", "A4", "")
pdf.SetMargins(15, 15, 15)
pdf.AddPage()
// Translate UTF-8 strings to Windows-1252 for built-in fonts
tr := pdf.UnicodeTranslatorFromDescriptor("")
// Header
pdf.SetFont("Arial", "B", 20)
pdf.SetTextColor(51, 51, 51)
pdf.Cell(0, 12, "Tasks Report")
pdf.Ln(14)
// Residence name
pdf.SetFont("Arial", "", 14)
pdf.SetTextColor(102, 102, 102)
pdf.Cell(0, 8, tr(report.ResidenceName))
pdf.Ln(10)
// Generated date
pdf.SetFont("Arial", "", 10)
pdf.Cell(0, 6, fmt.Sprintf("Generated: %s", report.GeneratedAt.Format("January 2, 2006 at 3:04 PM")))
pdf.Ln(12)
// Summary section
pdf.SetFont("Arial", "B", 14)
pdf.SetTextColor(51, 51, 51)
pdf.Cell(0, 8, "Summary")
pdf.Ln(10)
// Summary box
pdf.SetFillColor(248, 249, 250)
pdf.Rect(15, pdf.GetY(), 180, 30, "F")
y := pdf.GetY() + 5
pdf.SetXY(20, y)
pdf.SetFont("Arial", "B", 12)
pdf.SetTextColor(51, 51, 51)
// Summary columns
colWidth := 45.0
pdf.Cell(colWidth, 6, "Total Tasks")
pdf.Cell(colWidth, 6, "Completed")
pdf.Cell(colWidth, 6, "Pending")
pdf.Cell(colWidth, 6, "Overdue")
pdf.Ln(8)
pdf.SetX(20)
pdf.SetFont("Arial", "", 16)
pdf.Cell(colWidth, 8, fmt.Sprintf("%d", report.TotalTasks))
pdf.SetTextColor(40, 167, 69) // Green
pdf.Cell(colWidth, 8, fmt.Sprintf("%d", report.Completed))
pdf.SetTextColor(255, 193, 7) // Yellow/Orange
pdf.Cell(colWidth, 8, fmt.Sprintf("%d", report.Pending))
pdf.SetTextColor(220, 53, 69) // Red
pdf.Cell(colWidth, 8, fmt.Sprintf("%d", report.Overdue))
pdf.Ln(25)
// Tasks table
pdf.SetTextColor(51, 51, 51)
pdf.SetFont("Arial", "B", 14)
pdf.Cell(0, 8, "Tasks")
pdf.Ln(10)
if len(report.Tasks) == 0 {
pdf.SetFont("Arial", "I", 11)
pdf.SetTextColor(128, 128, 128)
pdf.Cell(0, 8, "No tasks found for this residence.")
} else {
// Table header
pdf.SetFont("Arial", "B", 10)
pdf.SetFillColor(233, 236, 239)
pdf.SetTextColor(51, 51, 51)
pdf.CellFormat(70, 8, "Title", "1", 0, "L", true, 0, "")
pdf.CellFormat(30, 8, "Category", "1", 0, "C", true, 0, "")
pdf.CellFormat(25, 8, "Priority", "1", 0, "C", true, 0, "")
pdf.CellFormat(25, 8, "Status", "1", 0, "C", true, 0, "")
pdf.CellFormat(30, 8, "Due Date", "1", 0, "C", true, 0, "")
pdf.Ln(-1)
// Table rows
pdf.SetFont("Arial", "", 9)
for _, task := range report.Tasks {
// Check if we need a new page
if pdf.GetY() > 270 {
pdf.AddPage()
// Repeat header
pdf.SetFont("Arial", "B", 10)
pdf.SetFillColor(233, 236, 239)
pdf.CellFormat(70, 8, "Title", "1", 0, "L", true, 0, "")
pdf.CellFormat(30, 8, "Category", "1", 0, "C", true, 0, "")
pdf.CellFormat(25, 8, "Priority", "1", 0, "C", true, 0, "")
pdf.CellFormat(25, 8, "Status", "1", 0, "C", true, 0, "")
pdf.CellFormat(30, 8, "Due Date", "1", 0, "C", true, 0, "")
pdf.Ln(-1)
pdf.SetFont("Arial", "", 9)
}
// Determine row color based on status
if task.IsCancelled {
pdf.SetFillColor(248, 215, 218) // Light red
} else if task.IsCompleted {
pdf.SetFillColor(212, 237, 218) // Light green
} else if task.IsArchived {
pdf.SetFillColor(226, 227, 229) // Light gray
} else {
pdf.SetFillColor(255, 255, 255) // White
}
// Title (truncate if too long, use runes to avoid cutting multi-byte UTF-8 characters)
title := task.Title
titleRunes := []rune(title)
if len(titleRunes) > 35 {
title = string(titleRunes[:32]) + "..."
}
// Status text
var status string
if task.IsCancelled {
status = "Cancelled"
} else if task.IsCompleted {
status = "Completed"
} else if task.IsArchived {
status = "Archived"
} else {
status = task.Status
}
// Due date
dueDate := "-"
if task.DueDate != nil {
dueDate = task.DueDate.Format("Jan 2, 2006")
}
pdf.SetTextColor(51, 51, 51)
pdf.CellFormat(70, 7, tr(title), "1", 0, "L", true, 0, "")
pdf.CellFormat(30, 7, tr(task.Category), "1", 0, "C", true, 0, "")
pdf.CellFormat(25, 7, tr(task.Priority), "1", 0, "C", true, 0, "")
pdf.CellFormat(25, 7, tr(status), "1", 0, "C", true, 0, "")
pdf.CellFormat(30, 7, dueDate, "1", 0, "C", true, 0, "")
pdf.Ln(-1)
}
}
// Footer
pdf.SetY(-25)
pdf.SetFont("Arial", "I", 9)
pdf.SetTextColor(128, 128, 128)
pdf.Cell(0, 10, tr(fmt.Sprintf("honeyDue - Tasks Report for %s", report.ResidenceName)))
pdf.Ln(5)
pdf.Cell(0, 10, fmt.Sprintf("Generated on %s", time.Now().UTC().Format("2006-01-02 15:04:05 UTC")))
// Output to buffer
var buf bytes.Buffer
if err := pdf.Output(&buf); err != nil {
return nil, fmt.Errorf("failed to generate PDF: %w", err)
}
return buf.Bytes(), nil
}