193 lines
5.2 KiB
Go
193 lines
5.2 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sno/internal/models"
|
|
"sno/internal/repository"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
)
|
|
|
|
// QRService defines the interface for QR code processing logic.
|
|
type QRService interface {
|
|
ValidatePayload(ctx context.Context, userID int64, payload string) (*models.QRValidateResponse, error)
|
|
GenerateUniqueToken(ctx context.Context, qrType string, value string) (string, error)
|
|
ValidateUniqueToken(ctx context.Context, token string) (*models.QRTokenData, error)
|
|
}
|
|
|
|
// qrService implements the QRService interface.
|
|
type qrService struct {
|
|
qrScanRepo repository.QRScanRepository
|
|
adminSvc AdminService
|
|
quizSvc QuizService
|
|
redis *redis.Client
|
|
}
|
|
|
|
// NewQRService creates a new instance of a QR service.
|
|
func NewQRService(qrRepo repository.QRScanRepository, adminSvc AdminService, quizSvc QuizService, redisClient *redis.Client) QRService {
|
|
return &qrService{
|
|
qrScanRepo: qrRepo,
|
|
adminSvc: adminSvc,
|
|
quizSvc: quizSvc,
|
|
redis: redisClient,
|
|
}
|
|
}
|
|
|
|
// generateRandomToken generates a secure random token
|
|
func (s *qrService) generateRandomToken() (string, error) {
|
|
b := make([]byte, 16) // 128 bits
|
|
_, err := rand.Read(b)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("%x", b), nil
|
|
}
|
|
|
|
// GenerateUniqueToken creates a new unique QR token and stores it in Redis
|
|
func (s *qrService) GenerateUniqueToken(ctx context.Context, qrType string, value string) (string, error) {
|
|
token, err := s.generateRandomToken()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to generate token: %w", err)
|
|
}
|
|
|
|
tokenData := &models.QRTokenData{
|
|
Type: qrType,
|
|
Value: value,
|
|
Used: false,
|
|
}
|
|
|
|
// Store in Redis with 30 day expiration
|
|
data, err := json.Marshal(tokenData)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to marshal token data: %w", err)
|
|
}
|
|
|
|
key := fmt.Sprintf("qr_token:%s", token)
|
|
err = s.redis.Set(ctx, key, data, 30*24*time.Hour).Err()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to store token in Redis: %w", err)
|
|
}
|
|
|
|
return token, nil
|
|
}
|
|
|
|
// ValidateUniqueToken validates a QR token and marks it as used
|
|
func (s *qrService) ValidateUniqueToken(ctx context.Context, token string) (*models.QRTokenData, error) {
|
|
key := fmt.Sprintf("qr_token:%s", token)
|
|
|
|
// Get token data from Redis
|
|
data, err := s.redis.Get(ctx, key).Bytes()
|
|
if err != nil {
|
|
if err == redis.Nil {
|
|
return nil, errors.New("invalid or expired token")
|
|
}
|
|
return nil, fmt.Errorf("failed to get token from Redis: %w", err)
|
|
}
|
|
|
|
var tokenData models.QRTokenData
|
|
if err := json.Unmarshal(data, &tokenData); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal token data: %w", err)
|
|
}
|
|
|
|
// Check if token is already used
|
|
if tokenData.Used {
|
|
return nil, errors.New("token has already been used")
|
|
}
|
|
|
|
// Mark token as used
|
|
tokenData.Used = true
|
|
updatedData, err := json.Marshal(tokenData)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal updated token data: %w", err)
|
|
}
|
|
|
|
err = s.redis.Set(ctx, key, updatedData, 30*24*time.Hour).Err()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to update token in Redis: %w", err)
|
|
}
|
|
|
|
return &tokenData, nil
|
|
}
|
|
|
|
// ValidatePayload validates a QR token and performs the corresponding action.
|
|
func (s *qrService) ValidatePayload(ctx context.Context, userID int64, payload string) (*models.QRValidateResponse, error) {
|
|
// Validate as unique token
|
|
tokenData, err := s.ValidateUniqueToken(ctx, payload)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Process the validated token data
|
|
return s.processTokenData(ctx, userID, tokenData, payload)
|
|
}
|
|
|
|
// processTokenData processes validated token data and returns response
|
|
func (s *qrService) processTokenData(ctx context.Context, userID int64, tokenData *models.QRTokenData, payload string) (*models.QRValidateResponse, error) {
|
|
scanLog := &models.QRScan{
|
|
UserID: userID,
|
|
Source: models.InApp,
|
|
Value: &payload,
|
|
}
|
|
|
|
switch tokenData.Type {
|
|
case "reward":
|
|
scanLog.Type = models.QRReward
|
|
amount, err := strconv.Atoi(tokenData.Value)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid reward amount: %s", tokenData.Value)
|
|
}
|
|
|
|
if err := s.adminSvc.GrantStars(ctx, userID, amount); err != nil {
|
|
return nil, fmt.Errorf("failed to grant reward: %w", err)
|
|
}
|
|
|
|
// Log the successful scan
|
|
_, _ = s.qrScanRepo.Create(ctx, scanLog)
|
|
|
|
return &models.QRValidateResponse{
|
|
Type: "REWARD",
|
|
Data: map[string]int{"amount": amount},
|
|
}, nil
|
|
|
|
case "quiz":
|
|
scanLog.Type = models.QRQuiz
|
|
quizID, err := strconv.Atoi(tokenData.Value)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid quiz ID: %s", tokenData.Value)
|
|
}
|
|
|
|
quiz, err := s.quizSvc.GetQuizByID(ctx, quizID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get quiz: %w", err)
|
|
}
|
|
|
|
// Log the successful scan
|
|
_, _ = s.qrScanRepo.Create(ctx, scanLog)
|
|
|
|
return &models.QRValidateResponse{
|
|
Type: "OPEN_QUIZ",
|
|
Data: quiz,
|
|
}, nil
|
|
|
|
case "shop":
|
|
scanLog.Type = models.QRShop
|
|
// Shop QR codes can be used for various shop-related actions
|
|
// For now, just log the scan and return success
|
|
_, _ = s.qrScanRepo.Create(ctx, scanLog)
|
|
|
|
return &models.QRValidateResponse{
|
|
Type: "SHOP_ACTION",
|
|
Data: map[string]string{"action": tokenData.Value},
|
|
}, nil
|
|
|
|
default:
|
|
return nil, fmt.Errorf("unknown token type: %s", tokenData.Type)
|
|
}
|
|
}
|