Files
MyClub/internal/controllers/qr_controller.go
T
Tomas Dvorak dfc079288f hot fix #1
2026-01-26 08:13:18 +01:00

391 lines
11 KiB
Go

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]
}