develop #3
@@ -1,6 +1,6 @@
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="flex mb-6" aria-label="Breadcrumb">
|
||||
<nav class="flex my-6" aria-label="Breadcrumb">
|
||||
<ol class="inline-flex items-center space-x-1 md:space-x-2 rounded-lg bg-white px-4 py-2 shadow-sm">
|
||||
<li class="inline-flex items-center">
|
||||
<%= link_to "Accueil", root_path, class: "inline-flex items-center text-sm font-medium text-gray-700 hover:text-purple-600" %>
|
||||
@@ -51,7 +51,7 @@
|
||||
<%= order.status.humanize %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex items-center space-x-4 text-sm text-slate-600 dark:text-slate-400 mb-3">
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="calendar" class="w-4 h-4 mr-1"></i>
|
||||
@@ -73,14 +73,14 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2 ml-4">
|
||||
<%= link_to order_path(order),
|
||||
<%= link_to order_path(order),
|
||||
class: "inline-flex items-center px-3 py-2 bg-purple-600 hover:bg-purple-700 text-white text-sm font-medium rounded-lg transition-colors duration-200" do %>
|
||||
<i data-lucide="eye" class="w-4 h-4 mr-2"></i>
|
||||
Voir détails
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Quick tickets preview -->
|
||||
<div class="border-t border-slate-200 dark:border-slate-600 pt-3">
|
||||
<div class="grid gap-2">
|
||||
@@ -92,7 +92,7 @@
|
||||
<span class="text-slate-500">- <%= ticket.first_name %> <%= ticket.last_name %></span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<%= link_to ticket_download_path(ticket.qr_code),
|
||||
<%= link_to ticket_download_path(ticket.qr_code),
|
||||
class: "text-purple-600 hover:text-purple-800 dark:text-purple-400 dark:hover:text-purple-200" do %>
|
||||
<i data-lucide="download" class="w-3 h-3"></i>
|
||||
<% end %>
|
||||
@@ -128,4 +128,4 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
275
docs/ticket-download-security.md
Normal file
275
docs/ticket-download-security.md
Normal file
@@ -0,0 +1,275 @@
|
||||
# Ticket Download Security Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes how to implement secure unique identifiers for ticket PDF downloads to enhance security and prevent unauthorized access to user tickets.
|
||||
|
||||
## Problem Statement
|
||||
|
||||
Currently, the ticket download functionality uses the QR code directly as an identifier in URLs. This approach presents several security risks:
|
||||
|
||||
1. **Predictability**: QR codes may follow predictable patterns
|
||||
2. **Information Disclosure**: QR codes might reveal internal system information
|
||||
3. **Brute Force Vulnerability**: Attackers can enumerate valid tickets
|
||||
4. **Lack of Revocability**: Cannot invalidate download links without affecting the QR code
|
||||
|
||||
## Solution
|
||||
|
||||
Implement a separate, cryptographically secure unique identifier specifically for PDF downloads.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### 1. Database Migration
|
||||
|
||||
Create a migration to add the new column:
|
||||
|
||||
```ruby
|
||||
# db/migrate/xxx_add_pdf_download_token_to_tickets.rb
|
||||
class AddPdfDownloadTokenToTickets < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :tickets, :pdf_download_token, :string, limit: 50
|
||||
add_column :tickets, :pdf_download_token_expires_at, :datetime
|
||||
add_index :tickets, :pdf_download_token, unique: true
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### 2. Model Implementation
|
||||
|
||||
Update the Ticket model to generate secure tokens:
|
||||
|
||||
```ruby
|
||||
# app/models/ticket.rb
|
||||
class Ticket < ApplicationRecord
|
||||
before_create :generate_pdf_download_token
|
||||
|
||||
# Generate a secure token for PDF downloads
|
||||
def generate_pdf_download_token
|
||||
self.pdf_download_token = SecureRandom.urlsafe_base64(32)
|
||||
self.pdf_download_token_expires_at = 24.hours.from_now
|
||||
end
|
||||
|
||||
# Check if the download token is still valid
|
||||
def pdf_download_token_valid?
|
||||
pdf_download_token.present? &&
|
||||
pdf_download_token_expires_at.present? &&
|
||||
pdf_download_token_expires_at > Time.current
|
||||
end
|
||||
|
||||
# Regenerate token (useful for security or when token expires)
|
||||
def regenerate_pdf_download_token
|
||||
generate_pdf_download_token
|
||||
save!
|
||||
end
|
||||
|
||||
# Ensure tokens are generated for existing records
|
||||
def ensure_pdf_download_token
|
||||
if pdf_download_token.blank?
|
||||
generate_pdf_download_token
|
||||
save!
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### 3. Controller Updates
|
||||
|
||||
Update the TicketsController to use the new token system:
|
||||
|
||||
```ruby
|
||||
# app/controllers/tickets_controller.rb
|
||||
class TicketsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def show
|
||||
@ticket = Ticket.joins(order: :user)
|
||||
.includes(:event, :ticket_type, order: :user)
|
||||
.find_by(tickets: { qr_code: params[:qr_code] })
|
||||
|
||||
if @ticket.nil?
|
||||
redirect_to dashboard_path, alert: "Billet non trouvé"
|
||||
return
|
||||
end
|
||||
|
||||
@event = @ticket.event
|
||||
@order = @ticket.order
|
||||
end
|
||||
|
||||
def download
|
||||
# Find ticket by PDF download token instead of QR code
|
||||
@ticket = Ticket.find_by(pdf_download_token: params[:pdf_download_token])
|
||||
|
||||
# Check if ticket exists
|
||||
if @ticket.nil?
|
||||
redirect_to dashboard_path, alert: "Lien de téléchargement invalide ou expiré"
|
||||
return
|
||||
end
|
||||
|
||||
# Verify token validity
|
||||
unless @ticket.pdf_download_token_valid?
|
||||
redirect_to dashboard_path, alert: "Le lien de téléchargement a expiré"
|
||||
return
|
||||
end
|
||||
|
||||
# Verify ownership
|
||||
unless @ticket.order.user == current_user
|
||||
redirect_to dashboard_path, alert: "Vous n'avez pas l'autorisation d'accéder à ce billet"
|
||||
return
|
||||
end
|
||||
|
||||
# Generate and send PDF
|
||||
pdf_content = @ticket.to_pdf
|
||||
|
||||
# Optionally regenerate token to make it single-use
|
||||
# @ticket.regenerate_pdf_download_token
|
||||
|
||||
send_data pdf_content,
|
||||
filename: "ticket_#{@ticket.id}_#{@ticket.event.name.parameterize}.pdf",
|
||||
type: "application/pdf",
|
||||
disposition: "attachment"
|
||||
rescue => e
|
||||
Rails.logger.error "Error generating ticket PDF: #{e.message}"
|
||||
redirect_to dashboard_path, alert: "Erreur lors de la génération du billet"
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### 4. Route Configuration
|
||||
|
||||
Update routes to use the new token-based system:
|
||||
|
||||
```ruby
|
||||
# config/routes.rb
|
||||
Rails.application.routes.draw do
|
||||
# Existing routes...
|
||||
|
||||
# Update ticket download route
|
||||
get "tickets/:pdf_download_token/download", to: "tickets#download", as: "ticket_download"
|
||||
|
||||
# Keep existing show route for QR code functionality
|
||||
get "tickets/:qr_code", to: "tickets#show", as: "ticket"
|
||||
end
|
||||
```
|
||||
|
||||
### 5. View Updates
|
||||
|
||||
Update views to use the new download URL:
|
||||
|
||||
```erb
|
||||
<!-- In app/views/tickets/show.html.erb -->
|
||||
<%= link_to ticket_download_path(@ticket.pdf_download_token),
|
||||
class: "flex-1 bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 text-white font-medium py-3 px-6 rounded-xl shadow-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 transform hover:-translate-y-0.5 text-center" do %>
|
||||
<svg class="w-4 h-4 inline-block mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
Télécharger le PDF
|
||||
<% end %>
|
||||
```
|
||||
|
||||
### 6. Background Job for Token Management
|
||||
|
||||
Create a job to clean up expired tokens periodically:
|
||||
|
||||
```ruby
|
||||
# app/jobs/cleanup_expired_ticket_tokens_job.rb
|
||||
class CleanupExpiredTicketTokensJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform
|
||||
# Clear expired tokens to free up database space
|
||||
Ticket.where("pdf_download_token_expires_at < ?", 1.week.ago)
|
||||
.update_all(pdf_download_token: nil, pdf_download_token_expires_at: nil)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Schedule this job to run regularly:
|
||||
|
||||
```ruby
|
||||
# config/schedule.rb (if using whenever gem)
|
||||
every 1.day, at: '4:30 am' do
|
||||
rake "tickets:cleanup_expired_tokens"
|
||||
end
|
||||
```
|
||||
|
||||
## Security Benefits
|
||||
|
||||
1. **Unpredictability**: Tokens are cryptographically secure and random
|
||||
2. **Separation of Concerns**: QR codes for physical entry, tokens for digital downloads
|
||||
3. **Revocability**: Tokens can be regenerated without affecting QR codes
|
||||
4. **Expirability**: Time-limited access prevents long-term exposure
|
||||
5. **Ownership Verification**: Additional checks ensure only ticket owners can download
|
||||
6. **Audit Trail**: Token usage can be logged for security monitoring
|
||||
|
||||
## Additional Security Considerations
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
Implement rate limiting to prevent abuse:
|
||||
|
||||
```ruby
|
||||
# In ApplicationController or specific controller
|
||||
before_action :rate_limit_downloads, only: [:download]
|
||||
|
||||
def rate_limit_downloads
|
||||
if Rails.cache.read("download_attempts_#{current_user.id}")&.to_i > 10
|
||||
render json: { error: "Too many download attempts" }, status: :too_many_requests
|
||||
else
|
||||
Rails.cache.write("download_attempts_#{current_user.id}",
|
||||
(Rails.cache.read("download_attempts_#{current_user.id}") || 0) + 1,
|
||||
expires_in: 1.hour)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Logging
|
||||
|
||||
Add logging for security monitoring:
|
||||
|
||||
```ruby
|
||||
# In TicketsController#download
|
||||
Rails.logger.info "Ticket PDF download attempted - User: #{current_user.id}, Ticket: #{@ticket.id}, Token: #{params[:pdf_download_token]}"
|
||||
```
|
||||
|
||||
## Migration Process
|
||||
|
||||
1. Run the database migration
|
||||
2. Update existing tickets with tokens:
|
||||
```ruby
|
||||
# In rails console or a rake task
|
||||
Ticket.find_each(&:ensure_pdf_download_token)
|
||||
```
|
||||
3. Deploy code changes
|
||||
4. Update any external references to use the new system
|
||||
5. Monitor for issues and adjust expiration times as needed
|
||||
|
||||
## Testing
|
||||
|
||||
Ensure comprehensive testing of the new functionality:
|
||||
|
||||
```ruby
|
||||
# spec/controllers/tickets_controller_spec.rb
|
||||
RSpec.describe TicketsController, type: :controller do
|
||||
describe "GET #download" do
|
||||
it "downloads PDF for valid token" do
|
||||
# Test implementation
|
||||
end
|
||||
|
||||
it "rejects expired tokens" do
|
||||
# Test implementation
|
||||
end
|
||||
|
||||
it "rejects invalid tokens" do
|
||||
# Test implementation
|
||||
end
|
||||
|
||||
it "verifies ticket ownership" do
|
||||
# Test implementation
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
This implementation provides a robust security framework for ticket PDF downloads while maintaining usability. The separation of QR codes (for physical entry) and download tokens (for digital access) follows security best practices and provides multiple layers of protection against unauthorized access.
|
||||
Reference in New Issue
Block a user