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>
129 lines
4.3 KiB
Go
129 lines
4.3 KiB
Go
package responses
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/shopspring/decimal"
|
|
|
|
"github.com/treytartt/honeydue-api/internal/models"
|
|
)
|
|
|
|
// DocumentUserResponse represents a user in document context
|
|
type DocumentUserResponse struct {
|
|
ID uint `json:"id"`
|
|
Username string `json:"username"`
|
|
FirstName string `json:"first_name"`
|
|
LastName string `json:"last_name"`
|
|
}
|
|
|
|
// DocumentImageResponse represents an image in a document
|
|
type DocumentImageResponse struct {
|
|
ID uint `json:"id"`
|
|
ImageURL string `json:"image_url"`
|
|
MediaURL string `json:"media_url"` // Authenticated endpoint: /api/media/document-image/{id}
|
|
Caption string `json:"caption"`
|
|
Error string `json:"error,omitempty"` // Non-empty when the image could not be resolved
|
|
}
|
|
|
|
// DocumentResponse represents a document in the API response
|
|
type DocumentResponse struct {
|
|
ID uint `json:"id"`
|
|
ResidenceID uint `json:"residence_id"`
|
|
Residence uint `json:"residence"` // Alias for residence_id (KMM compatibility)
|
|
CreatedByID uint `json:"created_by_id"`
|
|
CreatedBy *DocumentUserResponse `json:"created_by,omitempty"`
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
DocumentType models.DocumentType `json:"document_type"`
|
|
MediaURL string `json:"media_url"` // Authenticated endpoint: /api/media/document/{id}
|
|
FileName string `json:"file_name"`
|
|
FileSize *int64 `json:"file_size"`
|
|
MimeType string `json:"mime_type"`
|
|
PurchaseDate *time.Time `json:"purchase_date"`
|
|
ExpiryDate *time.Time `json:"expiry_date"`
|
|
PurchasePrice *decimal.Decimal `json:"purchase_price"`
|
|
Vendor string `json:"vendor"`
|
|
SerialNumber string `json:"serial_number"`
|
|
ModelNumber string `json:"model_number"`
|
|
TaskID *uint `json:"task_id"`
|
|
IsActive bool `json:"is_active"`
|
|
Images []DocumentImageResponse `json:"images"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// Note: Pagination removed - list endpoints now return arrays directly
|
|
|
|
// === Factory Functions ===
|
|
|
|
// NewDocumentUserResponse creates a DocumentUserResponse from a User model
|
|
func NewDocumentUserResponse(u *models.User) *DocumentUserResponse {
|
|
if u == nil {
|
|
return nil
|
|
}
|
|
return &DocumentUserResponse{
|
|
ID: u.ID,
|
|
Username: u.Username,
|
|
FirstName: u.FirstName,
|
|
LastName: u.LastName,
|
|
}
|
|
}
|
|
|
|
// NewDocumentResponse creates a DocumentResponse from a Document model
|
|
func NewDocumentResponse(d *models.Document) DocumentResponse {
|
|
resp := DocumentResponse{
|
|
ID: d.ID,
|
|
ResidenceID: d.ResidenceID,
|
|
Residence: d.ResidenceID, // Alias for KMM compatibility
|
|
CreatedByID: d.CreatedByID,
|
|
Title: d.Title,
|
|
Description: d.Description,
|
|
DocumentType: d.DocumentType,
|
|
MediaURL: fmt.Sprintf("/api/media/document/%d", d.ID), // Authenticated endpoint
|
|
FileName: d.FileName,
|
|
FileSize: d.FileSize,
|
|
MimeType: d.MimeType,
|
|
PurchaseDate: d.PurchaseDate,
|
|
ExpiryDate: d.ExpiryDate,
|
|
PurchasePrice: d.PurchasePrice,
|
|
Vendor: d.Vendor,
|
|
SerialNumber: d.SerialNumber,
|
|
ModelNumber: d.ModelNumber,
|
|
TaskID: d.TaskID,
|
|
IsActive: d.IsActive,
|
|
Images: make([]DocumentImageResponse, 0),
|
|
CreatedAt: d.CreatedAt,
|
|
UpdatedAt: d.UpdatedAt,
|
|
}
|
|
|
|
if d.CreatedBy.ID != 0 {
|
|
resp.CreatedBy = NewDocumentUserResponse(&d.CreatedBy)
|
|
}
|
|
|
|
// Convert images with authenticated media URLs
|
|
for _, img := range d.Images {
|
|
imgResp := DocumentImageResponse{
|
|
ID: img.ID,
|
|
ImageURL: img.ImageURL,
|
|
MediaURL: fmt.Sprintf("/api/media/document-image/%d", img.ID), // Authenticated endpoint
|
|
Caption: img.Caption,
|
|
}
|
|
if img.ImageURL == "" {
|
|
imgResp.Error = "image source URL is missing"
|
|
}
|
|
resp.Images = append(resp.Images, imgResp)
|
|
}
|
|
|
|
return resp
|
|
}
|
|
|
|
// NewDocumentListResponse creates a list of document responses
|
|
func NewDocumentListResponse(documents []models.Document) []DocumentResponse {
|
|
results := make([]DocumentResponse, len(documents))
|
|
for i, d := range documents {
|
|
results[i] = NewDocumentResponse(&d)
|
|
}
|
|
return results
|
|
}
|