mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
391 lines
11 KiB
Go
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]
|
|
}
|