package controllers import ( "crypto/rand" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "net/http" "strconv" "time" "fotbal-club/internal/models" "github.com/gin-gonic/gin" "github.com/skip2/go-qrcode" "gorm.io/gorm" ) type QRCodeController struct { DB *gorm.DB } func NewQRCodeController(db *gorm.DB) *QRCodeController { return &QRCodeController{DB: db} } // Admin QR Code Management // GetQRCodes retrieves all QR codes func (qrc *QRCodeController) GetQRCodes(c *gin.Context) { var qrCodes []models.QRCode if err := qrc.DB.Order("created_at DESC").Find(&qrCodes).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve QR codes"}) return } c.JSON(http.StatusOK, qrCodes) } // GetQRCode retrieves a single QR code by ID func (qrc *QRCodeController) GetQRCode(c *gin.Context) { id := c.Param("id") var qrCode models.QRCode if err := qrc.DB.First(&qrCode, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "QR code not found"}) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve QR code"}) } return } c.JSON(http.StatusOK, qrCode) } // CreateQRCode creates a new QR code func (qrc *QRCodeController) CreateQRCode(c *gin.Context) { type CreateQRRequest struct { Name string `json:"name" binding:"required"` Description string `json:"description"` TargetURL string `json:"target_url" binding:"required"` } var req CreateQRRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Generate QR code qrCode, err := qrcode.Encode(req.TargetURL, qrcode.Medium, 256) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate QR code"}) return } // Convert to base64 data URL dataURL := fmt.Sprintf("data:image/png;base64,%s", base64.StdEncoding.EncodeToString(qrCode)) // Create QR code record qrCodeRecord := models.QRCode{ Name: req.Name, Description: req.Description, TargetURL: req.TargetURL, QRCodeURL: dataURL, ScanCount: 0, IsActive: true, } if err := qrc.DB.Create(&qrCodeRecord).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create QR code"}) return } c.JSON(http.StatusCreated, qrCodeRecord) } // UpdateQRCode updates an existing QR code func (qrc *QRCodeController) UpdateQRCode(c *gin.Context) { id := c.Param("id") var qrCode models.QRCode if err := qrc.DB.First(&qrCode, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "QR code not found"}) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve QR code"}) } return } type UpdateQRRequest struct { Name string `json:"name" binding:"required"` Description string `json:"description"` TargetURL string `json:"target_url" binding:"required"` IsActive *bool `json:"is_active"` } var req UpdateQRRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Regenerate QR code if URL changed if req.TargetURL != qrCode.TargetURL { newQRCode, err := qrcode.Encode(req.TargetURL, qrcode.Medium, 256) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate QR code"}) return } qrCode.QRCodeURL = fmt.Sprintf("data:image/png;base64,%s", base64.StdEncoding.EncodeToString(newQRCode)) } // Update fields qrCode.Name = req.Name qrCode.Description = req.Description qrCode.TargetURL = req.TargetURL if req.IsActive != nil { qrCode.IsActive = *req.IsActive } if err := qrc.DB.Save(&qrCode).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update QR code"}) return } c.JSON(http.StatusOK, qrCode) } // DeleteQRCode deletes a QR code func (qrc *QRCodeController) DeleteQRCode(c *gin.Context) { id := c.Param("id") if err := qrc.DB.Delete(&models.QRCode{}, id).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete QR code"}) return } c.JSON(http.StatusOK, gin.H{"message": "QR code deleted successfully"}) } // TicketQRData represents the data encoded in the QR code type TicketQRData struct { TicketID int64 `json:"id"` Barcode string `json:"barcode"` Holder string `json:"holder"` Email string `json:"email"` Event string `json:"event"` Type string `json:"type"` Qty int `json:"qty"` Price string `json:"price"` Date string `json:"date,omitempty"` Venue string `json:"venue,omitempty"` Generated string `json:"generated"` Checksum string `json:"checksum"` } // GET /api/v1/tickets/:id/qr - Generate QR code for a ticket func (qrc *QRCodeController) GenerateTicketQR(c *gin.Context) { ticketID := c.Param("id") var ticket models.Ticket if err := qrc.DB.Preload("Campaign").Preload("TicketType").First(&ticket, ticketID).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Ticket not found"}) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch ticket"}) } return } // Only allow QR codes for paid tickets if ticket.Status != "paid" { c.JSON(http.StatusBadRequest, gin.H{"error": "QR codes only available for paid tickets"}) return } // Generate QR data qrData := TicketQRData{ TicketID: int64(ticket.ID), Barcode: ticket.Barcode, Holder: ticket.HolderName, Email: ticket.HolderEmail, Event: ticket.Campaign.Title, Type: ticket.TicketType.Name, Qty: ticket.Quantity, Price: fmt.Sprintf("%.2f Kč", float64(ticket.TotalPriceCents)/100), Generated: time.Now().Format(time.RFC3339), Checksum: generateChecksum(ticket), } if ticket.Campaign.MatchDateTime != nil { qrData.Date = ticket.Campaign.MatchDateTime.Format(time.RFC3339) } if ticket.Campaign.Venue != nil { qrData.Venue = *ticket.Campaign.Venue } // Generate QR code as PNG qrJSON, err := json.Marshal(qrData) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to serialize QR data"}) return } qrCode, err := qrcode.Encode(string(qrJSON), qrcode.Medium, 256) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate QR code"}) return } // Return as base64 data URL dataURL := fmt.Sprintf("data:image/png;base64,%s", base64.StdEncoding.EncodeToString(qrCode)) c.JSON(http.StatusOK, gin.H{ "qr_code": dataURL, "data": qrData, }) } // POST /api/v1/tickets/validate-qr - Validate ticket from QR code data func (qrc *QRCodeController) ValidateTicketFromQR(c *gin.Context) { type ValidateQRRequest struct { QRData string `json:"qr_data" binding:"required"` UsedBy string `json:"used_by"` } var req ValidateQRRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Parse QR data (simplified - in production, you'd parse the actual JSON) var qrData TicketQRData if err := parseQRData(req.QRData, &qrData); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid QR code data"}) return } // Validate checksum var ticket models.Ticket if err := qrc.DB.First(&ticket, qrData.TicketID).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Ticket not found"}) return } expectedChecksum := generateChecksum(ticket) if expectedChecksum != qrData.Checksum { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid QR code checksum"}) return } // Validate ticket status if ticket.Status != "paid" { c.JSON(http.StatusBadRequest, gin.H{"error": "Ticket is not paid"}) return } if ticket.UsedAt != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "Ticket already used", "used_at": ticket.UsedAt, }) return } // Mark ticket as used now := time.Now() if err := qrc.DB.Model(&ticket).Updates(map[string]interface{}{ "used_at": now, "used_by": req.UsedBy, "status": "used", }).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to validate ticket"}) return } c.JSON(http.StatusOK, gin.H{ "message": "Ticket validated successfully", "ticket": ticket, "used_at": now, }) } // GET /api/v1/tickets/:id/qr-download - Download QR code as image func (qrc *QRCodeController) DownloadTicketQR(c *gin.Context) { ticketID := c.Param("id") var ticket models.Ticket if err := qrc.DB.Preload("Campaign").Preload("TicketType").First(&ticket, ticketID).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Ticket not found"}) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch ticket"}) } return } // Only allow QR codes for paid tickets if ticket.Status != "paid" { c.JSON(http.StatusBadRequest, gin.H{"error": "QR codes only available for paid tickets"}) return } // Generate QR data qrData := TicketQRData{ TicketID: int64(ticket.ID), Barcode: ticket.Barcode, Holder: ticket.HolderName, Email: ticket.HolderEmail, Event: ticket.Campaign.Title, Type: ticket.TicketType.Name, Qty: ticket.Quantity, Price: fmt.Sprintf("%.2f Kč", float64(ticket.TotalPriceCents)/100), Generated: time.Now().Format(time.RFC3339), Checksum: generateChecksum(ticket), } if ticket.Campaign.MatchDateTime != nil { qrData.Date = ticket.Campaign.MatchDateTime.Format(time.RFC3339) } if ticket.Campaign.Venue != nil { qrData.Venue = *ticket.Campaign.Venue } // Generate QR code as PNG qrJSON, err := json.Marshal(qrData) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to serialize QR data"}) return } qrCode, err := qrcode.Encode(string(qrJSON), qrcode.Medium, 256) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate QR code"}) return } // Set headers for file download filename := fmt.Sprintf("vstupenka-%d-%s.png", ticket.ID, ticket.Barcode) c.Header("Content-Type", "image/png") c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) c.Header("Content-Length", strconv.Itoa(len(qrCode))) c.Data(http.StatusOK, "image/png", qrCode) } // Helper functions func generateChecksum(ticket models.Ticket) string { // Simple checksum for basic validation checksumString := fmt.Sprintf("%d%s%s", ticket.ID, ticket.Barcode, ticket.HolderEmail) hash := 0 for _, char := range checksumString { hash = ((hash << 5) - hash) + int(char) hash = hash & hash // Convert to 32-bit integer } return fmt.Sprintf("%x", hash) } func parseQRData(data string, qrData *TicketQRData) error { // Parse JSON data from QR code if err := json.Unmarshal([]byte(data), qrData); err != nil { return fmt.Errorf("failed to parse QR data: %w", err) } return nil } // Generate random string for additional security func generateRandomString(length int) string { bytes := make([]byte, length) rand.Read(bytes) return hex.EncodeToString(bytes)[:length] }