-
-
Notifications
You must be signed in to change notification settings - Fork 4
Development Guide
Comprehensive guide for developers working on Source-License, including setup, architecture patterns, coding standards, and contribution guidelines.
- Ruby: 3.4.7 or higher
- Git: Version control
- Database: MySQL, PostgreSQL, or SQLite for development
- Text Editor: VS Code, RubyMine, or your preferred editor
- Terminal: Command line access
-
Clone and Setup:
git clone https://github.com/PixelRidgeSoftworks/Source-License.git cd Source-License bundle install -
Environment Configuration:
cp .env.example .env # Edit .env with development settings -
Database Setup:
# For SQLite (development only) DATABASE_ADAPTER=sqlite DATABASE_NAME=source_license_dev.db # Run migrations ruby -r './lib/database.rb' -e "Database.setup; Database.migrate"
-
Start Development Server:
ruby launch.rb
-
Run Tests:
ruby run_tests.rb
# RuboCop - Ruby style guide enforcement
bundle exec rubocop
# Auto-fix style issues
bundle exec rubocop -A
# Run with specific configuration
bundle exec rubocop --config .rubocop.yml# Run tests with coverage
ruby run_tests.rb
# View coverage report
open coverage/index.html # macOS
xdg-open coverage/index.html # LinuxSource-License follows a modular MVC pattern with clear separation of concerns:
Controllers (lib/controllers/) → Handle HTTP requests and routing
Models (lib/models.rb) → Database entities and business logic
Views (views/) → ERB templates for HTML rendering
# lib/controllers/base_controller.rb
module BaseController
def self.included(base)
base.extend(ClassMethods)
end
# Common helper methods
def authenticated?
session[:admin_id] || session[:user_id]
end
def require_admin_auth
redirect '/admin/login' unless admin_authenticated?
end
module ClassMethods
def setup_routes(app)
# Define routes in each controller
end
end
end# lib/controllers/admin_controller.rb
module AdminController
include BaseController
def self.setup_routes(app)
app.get '/admin' do
require_admin_auth
erb :'admin/dashboard', layout: :'layouts/admin_layout'
end
app.post '/admin/login' do
# Authentication logic
end
end
end# In lib/models.rb
module BaseModelMethods
def self.included(base)
base.extend(ClassMethods)
end
def before_update
super
self.updated_at = Time.now if respond_to?(:updated_at)
end
def to_hash_for_api
values.reject { |k, _| k.to_s.include?('password') }
end
module ClassMethods
# Class-level methods
end
endclass User < Sequel::Model
include BaseModelMethods
set_dataset :users
one_to_many :licenses
# Instance methods
def active_licenses
licenses_dataset.where(status: 'active')
end
end# lib/license_generator.rb
class LicenseGenerator
def self.generate_license(product, customer_email, options = {})
# Generate cryptographically secure license key
license_key = generate_key(options[:format])
# Create license record
license = License.create(
license_key: license_key,
product_id: product.id,
customer_email: customer_email,
max_activations: options[:max_activations] || product.max_activations,
expires_at: calculate_expiration(product, options)
)
# Send notification email if configured
send_license_email(license) if options[:send_email]
license
end
private
def self.generate_key(format = 'XXXX-XXXX-XXXX-XXXX')
case format
when 'XXXX-XXXX-XXXX-XXXX'
generate_segmented_key
when 'UUID'
SecureRandom.uuid
else
generate_custom_key(format)
end
end
end# lib/auth.rb
module AuthHelpers
def admin_authenticated?
session[:admin_id] && Admin.where(id: session[:admin_id], status: 'active').first
end
def current_admin
@current_admin ||= Admin[session[:admin_id]] if session[:admin_id]
end
def require_admin_auth
unless admin_authenticated?
session[:return_to] = request.path_info
redirect '/admin/login'
end
end
def check_admin_permissions(required_permission)
admin = current_admin
halt 403 unless admin&.has_permission?(required_permission)
end
end# lib/enhanced_auth.rb
module EnhancedAuth
def authenticate_api_request
token = extract_jwt_token(request)
return halt 401, json(error: 'Authentication required') unless token
payload = decode_jwt_token(token)
@current_user = User[payload['user_id']] || Admin[payload['admin_id']]
halt 401, json(error: 'Invalid token') unless @current_user
rescue JWT::DecodeError
halt 401, json(error: 'Invalid token')
end
private
def extract_jwt_token(request)
auth_header = request.env['HTTP_AUTHORIZATION']
auth_header&.match(/Bearer (.+)/)&.[](1)
end
end# lib/migrations.rb
class DatabaseMigrator
MIGRATIONS = [
{
version: 1,
description: 'Create initial tables',
up: -> {
create_table :users do
primary_key :id
String :email, null: false, unique: true
String :password_hash, null: false
String :name
String :status, default: 'active'
Boolean :email_verified, default: false
DateTime :created_at, default: Sequel::CURRENT_TIMESTAMP
DateTime :updated_at, default: Sequel::CURRENT_TIMESTAMP
end
},
down: -> {
drop_table :users
}
}
].freeze
def self.run_migrations
# Migration execution logic
end
endclass Product < Sequel::Model
include BaseModelMethods
def validate
super
errors.add(:name, 'cannot be empty') if !name || name.strip.empty?
errors.add(:price, 'must be greater than or equal to 0') if !price || price.negative?
errors.add(:license_type, 'must be one_time or subscription') unless %w[one_time subscription].include?(license_type)
end
def before_save
super
self.name = name.strip if name
self.created_at ||= Time.now
end
end# Good: Use dataset methods for filtering
def active_licenses_for_product(product_id)
License.where(product_id: product_id, status: 'active')
.order(:created_at)
.limit(100)
end
# Good: Use joins for related data
def licenses_with_products
License.join(:products, id: :product_id)
.select_all(:licenses)
.select_append(:products__name___product_name)
end
# Avoid: N+1 queries
licenses = License.all
licenses.each { |license| puts license.product.name } # Bad
# Better: Eager loading
licenses = License.eager(:product).all
licenses.each { |license| puts license.product.name } # GoodSource-License uses Minitest for testing with the following structure:
test/
├── test_helper.rb # Test configuration and helpers
├── factories.rb # Test data factories
├── app_test.rb # Application integration tests
├── models_test.rb # Model unit tests
├── auth_test.rb # Authentication tests
├── security_test.rb # Security tests
└── html_reports/ # Coverage reports
# test/test_helper.rb
require 'minitest/autorun'
require 'minitest/pride'
require 'rack/test'
require 'database_cleaner'
# Set test environment
ENV['APP_ENV'] = 'test'
ENV['DATABASE_ADAPTER'] = 'sqlite'
ENV['DATABASE_NAME'] = ':memory:'
# Load application
require_relative '../app'
class MiniTest::Test
include Rack::Test::Methods
def app
SourceLicenseApp
end
def setup
DatabaseCleaner.start
end
def teardown
DatabaseCleaner.clean
end
end# test/models_test.rb
require_relative 'test_helper'
class ModelsTest < Minitest::Test
def test_user_creation
user = User.new(
email: 'test@example.com',
password: 'secure_password'
)
assert user.valid?
user.save
assert user.id
assert user.password_matches?('secure_password')
end
def test_license_validation
product = create_product
license = License.new(
license_key: 'TEST-KEY-123',
product_id: product.id,
customer_email: 'customer@example.com',
max_activations: 3
)
assert license.valid?
assert_equal 3, license.remaining_activations
end
end# test/app_test.rb
require_relative 'test_helper'
class AppTest < Minitest::Test
def test_homepage_loads
get '/'
assert_equal 200, last_response.status
assert_includes last_response.body, 'Source-License'
end
def test_admin_login_required
get '/admin'
assert_equal 302, last_response.status
follow_redirect!
assert_includes last_request.url, '/admin/login'
end
def test_license_validation_api
license = create_license
get "/api/license/#{license.license_key}/validate"
assert_equal 200, last_response.status
data = JSON.parse(last_response.body)
assert_equal true, data['valid']
assert_equal 'active', data['status']
end
end# test/security_test.rb
require_relative 'test_helper'
class SecurityTest < Minitest::Test
def test_csrf_protection
# Test CSRF token validation
post '/admin/products', {
name: 'Test Product',
price: 99.99
}
assert_equal 403, last_response.status
end
def test_sql_injection_protection
# Test parameterized queries
get "/api/license/'; DROP TABLE licenses; --/validate"
assert_equal 404, last_response.status
end
def test_xss_protection
user = create_user(name: '<script>alert("xss")</script>')
get "/admin/customers/#{user.id}"
refute_includes last_response.body, '<script>'
assert_includes last_response.body, '<script>'
end
end# test/factories.rb
module TestFactories
def create_user(attributes = {})
defaults = {
email: "user#{rand(10000)}@example.com",
password: 'test_password',
name: 'Test User',
status: 'active'
}
User.create(defaults.merge(attributes))
end
def create_product(attributes = {})
defaults = {
name: 'Test Product',
description: 'Test product description',
price: 99.99,
license_type: 'one_time',
max_activations: 3
}
Product.create(defaults.merge(attributes))
end
def create_license(attributes = {})
product = attributes[:product] || create_product
defaults = {
license_key: "TEST-#{SecureRandom.hex(4)}".upcase,
product_id: product.id,
customer_email: 'customer@example.com',
status: 'active',
max_activations: 3,
activation_count: 0
}
License.create(defaults.merge(attributes))
end
end
# Include in test files
class Minitest::Test
include TestFactories
end# lib/settings/settings_schema.rb
class SettingsSchema
SCHEMA = {
'app' => {
'site_name' => { type: 'string', default: 'Source-License' },
'site_url' => { type: 'string', required: true },
'support_email' => { type: 'email', required: true }
},
'license' => {
'default_format' => { type: 'string', default: 'XXXX-XXXX-XXXX-XXXX' },
'default_max_activations' => { type: 'integer', default: 3, min: 1 },
'auto_generate' => { type: 'boolean', default: true }
},
'payment' => {
'stripe_publishable_key' => { type: 'string', required: true },
'stripe_secret_key' => { type: 'string', required: true, secret: true },
'paypal_client_id' => { type: 'string' },
'paypal_client_secret' => { type: 'string', secret: true }
}
}.freeze
end# lib/settings_manager.rb
class SettingsManager
def self.get(key, default = nil)
category, setting_key = key.split('.', 2)
return default unless category && setting_key
# Check environment variable override
env_key = "#{category.upcase}_#{setting_key.upcase}"
return ENV[env_key] if ENV.key?(env_key)
# Get from database/cache
stored_value = get_stored_setting(category, setting_key)
return stored_value unless stored_value.nil?
# Return schema default
schema_default(category, setting_key) || default
end
def self.set(key, value)
category, setting_key = key.split('.', 2)
validate_setting(category, setting_key, value)
store_setting(category, setting_key, value)
end
private
def self.validate_setting(category, key, value)
schema = SettingsSchema::SCHEMA.dig(category, key)
return unless schema
case schema[:type]
when 'integer'
raise ArgumentError, "Value must be an integer" unless value.is_a?(Integer)
raise ArgumentError, "Value below minimum" if schema[:min] && value < schema[:min]
when 'email'
raise ArgumentError, "Invalid email format" unless value =~ /\A[\w+\-.]+@[a-z\d-]+(\.[a-z\d-]+)*\.[a-z]+\z/i
end
end
end# Development environment
configure :development do
set :logging, true
set :show_exceptions, true
set :dump_errors, true
end
# Production environment
configure :production do
set :logging, false
set :show_exceptions, false
set :dump_errors, false
# Enable security middleware
use SecurityMiddleware
end
# Test environment
configure :test do
set :logging, false
set :show_exceptions, false
endSource-License uses ERB (Embedded Ruby) templates for rendering HTML:
<!-- views/layouts/main_layout.erb -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= @page_title || 'Source-License' %></title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom CSS -->
<style>
:root {
--primary-color: <%= customization_get('primary_color', '#007bff') %>;
--secondary-color: <%= customization_get('secondary_color', '#6c757d') %>;
}
</style>
</head>
<body>
<%= erb :'partials/_navigation' %>
<main class="container mt-4">
<% if @flash_message %>
<div class="alert alert-<%= @flash_type || 'info' %> alert-dismissible fade show">
<%= @flash_message %>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<% end %>
<%= yield %>
</main>
<%= erb :'partials/_footer' %>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html># lib/helpers.rb
module TemplateHelpers
def format_currency(amount, currency = 'USD')
case currency.upcase
when 'USD'
"$#{format('%.2f', amount)}"
when 'EUR'
"€#{format('%.2f', amount)}"
else
"#{amount} #{currency}"
end
end
def format_date(date, format = :short)
return 'Never' unless date
case format
when :short
date.strftime('%m/%d/%Y')
when :long
date.strftime('%B %d, %Y at %I:%M %p')
when :iso
date.iso8601
else
date.strftime(format.to_s)
end
end
def status_badge(status)
classes = {
'active' => 'success',
'inactive' => 'secondary',
'suspended' => 'warning',
'revoked' => 'danger',
'expired' => 'dark'
}
badge_class = classes[status.to_s] || 'secondary'
"<span class=\"badge bg-#{badge_class}\">#{status.to_s.capitalize}</span>"
end
def card(title = nil, options = {}, &block)
css_class = "card #{options[:class]}"
content = "<div class=\"#{css_class}\">"
if title
content << "<div class=\"card-header\">"
content << "<h5 class=\"card-title mb-0\">#{title}</h5>"
content << "</div>"
end
content << "<div class=\"card-body\">"
content << capture(&block) if block_given?
content << "</div>"
content << "</div>"
content
end
end# lib/customization.rb
class Customization
def self.get_css_variables
{
'--primary-color' => get_setting('appearance.primary_color', '#007bff'),
'--secondary-color' => get_setting('appearance.secondary_color', '#6c757d'),
'--font-family' => get_setting('appearance.font_family', 'system-ui'),
'--border-radius' => get_setting('appearance.border_radius', '0.375rem')
}
end
def self.generate_custom_css
variables = get_css_variables
css = ":root {\n"
variables.each do |property, value|
css << " #{property}: #{value};\n"
end
css << "}\n"
# Additional custom styles
css << custom_component_styles
css
end
private
def self.custom_component_styles
"""
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.navbar-brand {
font-family: var(--font-family);
}
.card {
border-radius: var(--border-radius);
}
"""
end
end# Strong parameter validation
def validate_product_params(params)
required_fields = %w[name price license_type max_activations]
missing_fields = required_fields.select { |field| params[field].nil? || params[field].to_s.strip.empty? }
raise ArgumentError, "Missing required fields: #{missing_fields.join(', ')}" unless missing_fields.empty?
# Validate data types
raise ArgumentError, "Price must be a valid number" unless valid_decimal?(params['price'])
raise ArgumentError, "Max activations must be a positive integer" unless valid_positive_integer?(params['max_activations'])
# Sanitize strings
{
name: sanitize_string(params['name']),
description: sanitize_html(params['description']),
price: params['price'].to_f,
license_type: params['license_type'],
max_activations: params['max_activations'].to_i
}
end
def sanitize_string(input)
return '' unless input
input.to_s.strip.gsub(/[<>"]/, '')
end
def sanitize_html(input)
return '' unless input
# Use a proper HTML sanitizer in production
input.to_s.gsub(/<script.*?>.*?<\/script>/mi, '')
end# Always use parameterized queries with Sequel
def find_licenses_by_email(email)
# Good - parameterized query
License.where(customer_email: email).all
end
def search_licenses(search_term)
# Good - parameterized LIKE query
License.where(Sequel.ilike(:customer_email, "%#{search_term}%")).all
end
# Never do this - vulnerable to SQL injection
def bad_search(term)
DB["SELECT * FROM licenses WHERE customer_email LIKE '%#{term}%'"].all
endclass User < Sequel::Model
def password=(new_password)
# Validate password strength
validate_password_strength(new_password)
# Use BCrypt with appropriate cost
self.password_hash = BCrypt::Password.create(new_password, cost: 12)
self.password_changed_at = Time.now
end
def password_matches?(password)
BCrypt::Password.new(password_hash) == password
rescue BCrypt::Errors::InvalidHash
false
end
private
def validate_password_strength(password)
errors = []
errors << "Password must be at least 12 characters long" if password.length < 12
errors << "Password must contain uppercase letters" unless password.match?(/[A-Z]/)
errors << "Password must contain lowercase letters" unless password.match?(/[a-z]/)
errors << "Password must contain numbers" unless password.match?(/\d/)
errors << "Password must contain special characters" unless password.match?(/[!@#$%^&*]/)
raise ArgumentError, errors.join('; ') unless errors.empty?
end
endconfigure do
# Secure session configuration
use Rack::Session::Cookie, {
key: '_source_license_session',
secret: ENV.fetch('APP_SECRET'),
secure: ENV['APP_ENV'] == 'production', # HTTPS only in production
httponly: true, # Prevent XSS
same_site: :strict, # CSRF protection
expire_after: 24 * 60 * 60 # 24 hours
}
endSource-License follows the Ruby Style Guide with these specific conventions:
# Use snake_case for variables, methods, and files
user_email = 'user@example.com'
def calculate_license_expiry(product)
end
# Use PascalCase for classes and modules
class LicenseGenerator
end
module PaymentProcessor
end
# Use SCREAMING_SNAKE_CASE for constants
MAX_LICENSE_ACTIVATIONS = 10
DEFAULT_LICENSE_FORMAT = 'XXXX-XXXX-XXXX-XXXX'class License < Sequel::Model
# Class methods first
def self.generate_for_product(product, customer_email)
end
# Instance methods grouped by functionality
# Public methods first
def activate!(machine_fingerprint)
end
def deactivate!(machine_fingerprint)
end
def valid?
end
# Private methods last
private
def calculate_expiration
end
def send_notification_email
end
end# Use YARD documentation format
class LicenseGenerator
# Generates a new license for the specified product and customer
#
# @param product [Product] The product to generate a license for
# @param customer_email [String] The customer's email address
# @param options [Hash] Additional options for license generation
# @option options [Integer] :max_activations Override product default
# @option options [Time] :expires_at Custom expiration date
# @option options [String] :format License key format
#
# @return [License] The generated license
# @raise [ArgumentError] If product or email is invalid
def self.generate_license(product, customer_email, options = {})
validate_inputs(product, customer_email)
# Implementation...
end
end# Use descriptive migration names with version numbers
{
version: 1,
description: 'Create initial user and admin tables'
}
{
version: 2,
description: 'Add email verification to users'
}
{
version: 3,
description: 'Create product and license tables'
}class Product < Sequel::Model
def validate
super
# Always validate required fields
errors.add(:name, 'cannot be empty') if !name || name.strip.empty?
errors.add(:price, 'must be present') if price.nil?
# Validate data types and ranges
errors.add(:price, 'must be greater than or equal to 0') if price&.negative?
errors.add(:max_activations, 'must be a positive integer') if max_activations && max_activations <= 0
# Validate enums and allowed values
valid_license_types = %w[one_time subscription]
errors.add(:license_type, "must be one of: #{valid_license_types.join(', ')}") unless valid_license_types.include?(license_type)
end
end# Production environment settings
configure :production do
# Disable detailed error pages
set :show_exceptions, false
set :dump_errors, false
# Enable security middleware
use SecurityMiddleware
# Configure secure sessions
use Rack::Session::Cookie, {
secure: true, # HTTPS only
httponly: true, # No JavaScript access
same_site: :strict # CSRF protection
}
end# Production database configuration
configure :production do
# Connection pooling
DB.extension :connection_validator
DB.pool.connection_validation_timeout = 3600
# Query logging (disable in production)
DB.loggers = [] unless ENV['DEBUG_SQL']
# Prepared statements
DB.extension :pg_auto_parameterize if DB.adapter_scheme == :postgres
end# Simple memory cache for settings
class SettingsCache
def self.get(key)
@cache ||= {}
@cache[key] ||= SettingsManager.get_from_db(key)
end
def self.invalidate(key = nil)
if key
@cache&.delete(key)
else
@cache = {}
end
end
end# Use database indexes for frequent queries
DB.add_index :licenses, :customer_email
DB.add_index :licenses, [:product_id, :status]
DB.add_index :license_activations, [:license_id, :active]
# Optimize common queries
def recent_orders(limit = 50)
Order.where(created_at: (Time.now - 30*24*60*60)..Time.now)
.order(Sequel.desc(:created_at))
.limit(limit)
.eager(:order_items)
end-
Fork and Clone:
git clone https://github.com/your-username/Source-License.git cd Source-License git remote add upstream https://github.com/PixelRidgeSoftworks/Source-License.git -
Create Feature Branch:
git checkout -b feature/new-feature-name
-
Development:
# Make changes bundle exec rubocop # Check style ruby run_tests.rb # Run tests
-
Commit and Push:
git add . git commit -m "Add new feature: description" git push origin feature/new-feature-name
-
Pull Request:
- Create PR on GitHub
- Include description of changes
- Reference any related issues
- Code follows style guidelines
- Tests are included and passing
- Documentation is updated
- Security considerations addressed
- Performance impact considered
- Database migrations are reversible
# Good: Constructive feedback
# Consider using a constant for this magic number
MAX_RETRIES = 3
# Suggestion: Extract this logic into a helper method
def validate_license_key_format(key)
# Validation logic here
end
# Question: Should we handle the case where customer_email is nil?# Update version in multiple places
# app.rb
APP_VERSION = '1.1.0'
# Gemfile
# Add version to gem specification
# CHANGELOG.md
## [1.1.0] - 2024-01-15
### Added
- New license validation API endpoint
- Improved error handling for payment processing
### Fixed
- Bug in subscription renewal process
- Security issue with admin authenticationThis development guide provides comprehensive information for developers working on Source-License. For deployment and production considerations, refer to the Deployment Guide. For API integration, see the API Reference.