<% @categories.each do |category| %>
<%= link_to category_path(category), class: "block" do %>
<%= category.name %>
<%= pluralize(category.posts_count, 'post') %>
<% end %>
<% end %>
=end
# ═══════════════════════════════════════════════════════════════════════════════
# 8. AUTHORIZATION WITH PUNDIT
# ═══════════════════════════════════════════════════════════════════════════════
# app/policies/application_policy.rb
class ApplicationPolicy
attr_reader :user, :record
def initialize(user, record)
@user = user
@record = record
end
def index?
true
end
def show?
true
end
def create?
user.present?
end
def new?
create?
end
def update?
user.present? && (user == record.user || user.admin?)
end
def edit?
update?
end
def destroy?
user.present? && (user == record.user || user.admin?)
end
class Scope
def initialize(user, scope)
@user = user
@scope = scope
end
def resolve
raise NotImplementedError, "You must define #resolve in #{self.class}"
end
private
attr_reader :user, :scope
end
end
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
def index?
true
end
def show?
record.published? || user == record.user || user&.can_moderate?
end
def create?
user.present?
end
def update?
user == record.user || user&.can_moderate?
end
def destroy?
user == record.user || user&.admin?
end
def publish?
user == record.user || user&.can_moderate?
end
def unpublish?
publish?
end
def toggle_featured?
user&.can_moderate?
end
class Scope < Scope
def resolve
if user&.admin?
scope.all
elsif user&.moderator?
scope.where("status = ? OR user_id = ?", Post.statuses[:published], user.id)
elsif user.present?
scope.where("status = ? OR user_id = ?", Post.statuses[:published], user.id)
else
scope.published
end
end
end
end
# app/policies/comment_policy.rb
class CommentPolicy < ApplicationPolicy
def create?
user.present?
end
def update?
user == record.user || user&.can_moderate?
end
def destroy?
user == record.user || user&.can_moderate?
end
def approve?
user&.can_moderate?
end
def reject?
user&.can_moderate?
end
end
# app/policies/user_policy.rb
class UserPolicy < ApplicationPolicy
def index?
user&.can_moderate?
end
def show?
true
end
def update?
user == record || user&.admin?
end
def destroy?
user&.admin? && user != record
end
def activate?
user&.admin?
end
def deactivate?
user&.admin? && user != record
end
end
# ═══════════════════════════════════════════════════════════════════════════════
# 9. BACKGROUND JOBS AND MAILERS
# ═══════════════════════════════════════════════════════════════════════════════
# app/jobs/application_job.rb
class ApplicationJob < ActiveJob::Base
# Automatically retry jobs that encountered a deadlock
retry_on ActiveRecord::Deadlocked
# Most jobs are safe to ignore if the underlying records are no longer available
discard_on ActiveJob::DeserializationError
end
# app/jobs/notify_subscribers_job.rb
class NotifySubscribersJob < ApplicationJob
queue_as :default
def perform(post)
# Get all subscribers (this would be a real model in your app)
subscribers = User.where(subscribed: true)
subscribers.find_each do |subscriber|
PostMailer.new_post_notification(subscriber, post).deliver_now
end
end
end
# app/jobs/comment_notification_job.rb
class CommentNotificationJob < ApplicationJob
queue_as :default
def perform(comment)
return unless comment.post.user.email_notifications?
CommentMailer.new_comment_notification(comment).deliver_now
end
end
# app/jobs/cleanup_old_posts_job.rb
class CleanupOldPostsJob < ApplicationJob
queue_as :low_priority
def perform
# Archive posts older than 2 years
Post.where('created_at < ?', 2.years.ago)
.where(status: :published)
.update_all(status: :archived)
end
end
# app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
default from: 'noreply@mywebapp.com'
layout 'mailer'
private
def mail_with_name(to_user, subject)
mail(
to: "#{to_user.full_name} <#{to_user.email}>",
subject: subject
)
end
end
# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
def welcome(user)
@user = user
@login_url = new_user_session_url
mail_with_name(@user, 'Welcome to My Web App!')
end
def password_reset(user)
@user = user
@reset_url = edit_user_password_url(@user, reset_password_token: @user.reset_password_token)
mail_with_name(@user, 'Password Reset Instructions')
end
end
# app/mailers/post_mailer.rb
class PostMailer < ApplicationMailer
def new_post_notification(subscriber, post)
@subscriber = subscriber
@post = post
@post_url = post_url(@post)
@unsubscribe_url = unsubscribe_url(token: @subscriber.unsubscribe_token)
mail_with_name(@subscriber, "New post: #{@post.title}")
end
end
# app/mailers/comment_mailer.rb
class CommentMailer < ApplicationMailer
def new_comment_notification(comment)
@comment = comment
@post = comment.post
@author = @post.user
@post_url = post_url(@post, anchor: "comment-#{@comment.id}")
mail_with_name(@author, "New comment on your post: #{@post.title}")
end
end
# ═══════════════════════════════════════════════════════════════════════════════
# 10. TESTING WITH RSPEC
# ═══════════════════════════════════════════════════════════════════════════════
# spec/rails_helper.rb
=begin
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'
abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'rspec/rails'
# Add additional requires below this line. Rails is not loaded until this point!
require 'factory_bot_rails'
require 'faker'
# Checks for pending migrations and applies them before tests are run.
begin
ActiveRecord::Migration.maintain_test_schema!
rescue ActiveRecord::PendingMigrationError => e
abort e.to_s.strip
end
RSpec.configure do |config|
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
config.fixture_path = "#{::Rails.root}/spec/fixtures"
# If you're not using ActiveRecord, or you'd prefer not to run each of your
# examples within a transaction, remove the following line or assign false
# instead of true
config.use_transactional_fixtures = true
# You can uncomment this line to turn off ActiveRecord support entirely.
# config.use_active_record = false
# RSpec Rails can automatically mix in different behaviours to your tests
# based on their file location, for example enabling you to call `get` and
# `post` in specs under `spec/controllers`.
config.infer_spec_type_from_file_location!
# Filter lines from Rails gems in backtraces.
config.filter_rails_from_backtrace!
# Include FactoryBot methods
config.include FactoryBot::Syntax::Methods
# Include Devise test helpers
config.include Devise::Test::ControllerHelpers, type: :controller
config.include Devise::Test::IntegrationHelpers, type: :request
# Database cleaner
config.before(:suite) do
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with(:truncation)
end
config.around(:each) do |example|
DatabaseCleaner.cleaning do
example.run
end
end
end
=end
# spec/factories/users.rb
FactoryBot.define do
factory :user do
first_name { Faker::Name.first_name }
last_name { Faker::Name.last_name }
username { Faker::Internet.unique.username(specifier: 5..15) }
email { Faker::Internet.unique.email }
password { 'password123' }
password_confirmation { 'password123' }
bio { Faker::Lorem.paragraph }
website { Faker::Internet.url }
location { Faker::Address.city }
confirmed_at { Time.current }
trait :admin do
role { :admin }
end
trait :moderator do
role { :moderator }
end
trait :with_avatar do
after(:build) do |user|
user.avatar.attach(
io: File.open(Rails.root.join('spec', 'fixtures', 'files', 'avatar.jpg')),
filename: 'avatar.jpg',
content_type: 'image/jpeg'
)
end
end
end
end
# spec/factories/categories.rb
FactoryBot.define do
factory :category do
name { Faker::Book.genre }
slug { name.parameterize }
description { Faker::Lorem.paragraph }
color { %w[#3B82F6 #EF4444 #10B981 #F59E0B #8B5CF6].sample }
active { true }
end
end
# spec/factories/posts.rb
FactoryBot.define do
factory :post do
title { Faker::Lorem.sentence(word_count: 4) }
slug { title.parameterize }
excerpt { Faker::Lorem.paragraph }
content { Faker::Lorem.paragraphs(number: 5).join("\n\n") }
status { :published }
visibility { :public }
published_at { Time.current }
association :user
association :category
trait :draft do
status { :draft }
published_at { nil }
end
trait :featured do
featured { true }
end
trait :with_image do
after(:build) do |post|
post.featured_image.attach(
io: File.open(Rails.root.join('spec', 'fixtures', 'files', 'featured_image.jpg')),
filename: 'featured_image.jpg',
content_type: 'image/jpeg'
)
end
end
end
end
# spec/factories/comments.rb
FactoryBot.define do
factory :comment do
content { Faker::Lorem.paragraph }
status { :approved }
association :post
association :user
trait :pending do
status { :pending }
end
trait :reply do
association :parent, factory: :comment
end
end
end
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
describe 'validations' do
subject { build(:user) }
it { should validate_presence_of(:first_name) }
it { should validate_presence_of(:last_name) }
it { should validate_presence_of(:username) }
it { should validate_presence_of(:email) }
it { should validate_uniqueness_of(:username).case_insensitive }
it { should validate_uniqueness_of(:email).case_insensitive }
end
describe 'associations' do
it { should have_many(:posts).dependent(:destroy) }
it { should have_many(:comments).dependent(:destroy) }
it { should have_one_attached(:avatar) }
end
describe 'enums' do
it { should define_enum_for(:role).with_values(user: 0, moderator: 1, admin: 2) }
it { should define_enum_for(:status).with_values(inactive: 0, active: 1, suspended: 2) }
end
describe 'scopes' do
let!(:active_user) { create(:user, status: :active) }
let!(:inactive_user) { create(:user, status: :inactive) }
it 'returns only active users' do
expect(User.active).to include(active_user)
expect(User.active).not_to include(inactive_user)
end
end
describe 'instance methods' do
let(:user) { create(:user, first_name: 'John', last_name: 'Doe') }
describe '#full_name' do
it 'returns the full name' do
expect(user.full_name).to eq('John Doe')
end
end
describe '#admin?' do
it 'returns true for admin users' do
admin = create(:user, :admin)
expect(admin.admin?).to be true
end
it 'returns false for non-admin users' do
expect(user.admin?).to be false
end
end
describe '#can_moderate?' do
it 'returns true for admin users' do
admin = create(:user, :admin)
expect(admin.can_moderate?).to be true
end
it 'returns true for moderator users' do
moderator = create(:user, :moderator)
expect(moderator.can_moderate?).to be true
end
it 'returns false for regular users' do
expect(user.can_moderate?).to be false
end
end
end
describe 'callbacks' do
it 'normalizes username before save' do
user = build(:user, username: 'TestUser123')
user.save
expect(user.username).to eq('testuser123')
end
it 'sends welcome email after create' do
expect {
create(:user)
}.to have_enqueued_job(ActionMailer::MailDeliveryJob)
end
end
end
# spec/models/post_spec.rb
require 'rails_helper'
RSpec.describe Post, type: :model do
describe 'validations' do
subject { build(:post) }
it { should validate_presence_of(:title) }
it { should validate_presence_of(:content) }
it { should validate_presence_of(:slug) }
it { should validate_uniqueness_of(:slug) }
it { should validate_length_of(:title).is_at_most(255) }
it { should validate_length_of(:excerpt).is_at_most(500) }
end
describe 'associations' do
it { should belong_to(:user) }
it { should belong_to(:category) }
it { should have_many(:comments).dependent(:destroy) }
it { should have_many(:post_tags).dependent(:destroy) }
it { should have_many(:tags).through(:post_tags) }
end
describe 'enums' do
it { should define_enum_for(:status).with_values(draft: 0, published: 1, archived: 2) }
it { should define_enum_for(:visibility).with_values(public: 0, private: 1, protected: 2) }
end
describe 'scopes' do
let!(:published_post) { create(:post, status: :published) }
let!(:draft_post) { create(:post, :draft) }
let!(:featured_post) { create(:post, featured: true) }
it 'returns only published posts' do
expect(Post.published).to include(published_post)
expect(Post.published).not_to include(draft_post)
end
it 'returns only draft posts' do
expect(Post.drafts).to include(draft_post)
expect(Post.drafts).not_to include(published_post)
end
it 'returns only featured posts' do
expect(Post.featured).to include(featured_post)
end
end
describe 'instance methods' do
let(:post) { create(:post) }
describe '#published?' do
it 'returns true for published posts' do
expect(post.published?).to be true
end
it 'returns false for draft posts' do
draft_post = create(:post, :draft)
expect(draft_post.published?).to be false
end
end
describe '#reading_time' do
it 'calculates reading time based on content' do
# Create post with 400 words (should be 2 minutes at 200 words/minute)
content = (1..400).map { 'word' }.join(' ')
post = create(:post, content: content)
expect(post.reading_time).to eq(2)
end
end
describe '#publish!' do
let(:draft_post) { create(:post, :draft) }
it 'publishes a draft post' do
expect {
draft_post.publish!
}.to change(draft_post, :status).from('draft').to('published')
.and change(draft_post, :published_at).from(nil)
end
end
describe '#unpublish!' do
it 'unpublishes a published post' do
expect {
post.unpublish!
}.to change(post, :status).from('published').to('draft')
.and change(post, :published_at).to(nil)
end
end
end
describe 'callbacks' do
it 'generates slug before validation' do
post = build(:post, title: 'Test Post Title', slug: nil)
post.valid?
expect(post.slug).to eq('test-post-title')
end
it 'generates unique slug if title already exists' do
create(:post, title: 'Test Title', slug: 'test-title')
post = build(:post, title: 'Test Title')
post.valid?
expect(post.slug).to eq('test-title-1')
end
it 'generates excerpt if not provided' do
content = 'This is a very long content that should be truncated' * 10
post = build(:post, content: content, excerpt: nil)
post.save
expect(post.excerpt).to be_present
expect(post.excerpt.length).to be <= 200
end
end
end
# spec/controllers/posts_controller_spec.rb
require 'rails_helper'
RSpec.describe PostsController, type: :controller do
let(:user) { create(:user) }
let(:admin) { create(:user, :admin) }
let(:category) { create(:category) }
let(:post_instance) { create(:post, user: user, category: category) }
let(:draft_post) { create(:post, :draft, user: user, category: category) }
describe 'GET #index' do
before do
create_list(:post, 3, category: category)
create_list(:post, 2, :draft, category: category)
end
it 'returns published posts only' do
get :index
expect(assigns(:posts).map(&:status).uniq).to eq(['published'])
end
it 'filters by category when specified' do
other_category = create(:category)
other_post = create(:post, category: other_category)
get :index, params: { category: category.slug }
expect(assigns(:posts)).not_to include(other_post)
end
it 'searches posts when search term provided' do
searchable_post = create(:post, title: 'Unique Searchable Title')
get :index, params: { search: 'Unique Searchable' }
expect(assigns(:posts)).to include(searchable_post)
end
end
describe 'GET #show' do
context 'when post is published' do
it 'shows the post to anyone' do
get :show, params: { id: post_instance.slug }
expect(response).to have_http_status(:success)
expect(assigns(:post)).to eq(post_instance)
end
it 'increments views count' do
expect {
get :show, params: { id: post_instance.slug }
}.to change { post_instance.reload.views_count }.by(1)
end
end
context 'when post is draft' do
it 'shows the post to the author' do
sign_in user
get :show, params: { id: draft_post.slug }
expect(response).to have_http_status(:success)
end
it 'denies access to other users' do
other_user = create(:user)
sign_in other_user
expect {
get :show, params: { id: draft_post.slug }
}.to raise_error(Pundit::NotAuthorizedError)
end
end
end
describe 'POST #create' do
let(:valid_attributes) do
{
title: 'New Post',
content: 'Post content',
category_id: category.id
}
end
context 'when user is signed in' do
before { sign_in user }
it 'creates a new post' do
expect {
post :create, params: { post: valid_attributes }
}.to change(Post, :count).by(1)
end
it 'assigns the post to current user' do
post :create, params: { post: valid_attributes }
expect(assigns(:post).user).to eq(user)
end
it 'redirects to the post on success' do
post :create, params: { post: valid_attributes }
expect(response).to redirect_to(assigns(:post))
end
end
context 'when user is not signed in' do
it 'redirects to sign in' do
post :create, params: { post: valid_attributes }
expect(response).to redirect_to(new_user_session_path)
end
end
end
describe 'PATCH #update' do
context 'when user owns the post' do
before { sign_in user }
it 'updates the post' do
patch :update, params: {
id: post_instance.slug,
post: { title: 'Updated Title' }
}
expect(post_instance.reload.title).to eq('Updated Title')
end
it 'redirects to the post on success' do
patch :update, params: {
id: post_instance.slug,
post: { title: 'Updated Title' }
}
expect(response).to redirect_to(post_instance)
end
end
context 'when user does not own the post' do
let(:other_user) { create(:user) }
before { sign_in other_user }
it 'denies access' do
expect {
patch :update, params: {
id: post_instance.slug,
post: { title: 'Updated Title' }
}
}.to raise_error(Pundit::NotAuthorizedError)
end
end
end
describe 'DELETE #destroy' do
context 'when user owns the post' do
before { sign_in user }
it 'deletes the post' do
post_to_delete = create(:post, user: user)
expect {
delete :destroy, params: { id: post_to_delete.slug }
}.to change(Post, :count).by(-1)
end
it 'redirects to posts index' do
delete :destroy, params: { id: post_instance.slug }
expect(response).to redirect_to(posts_path)
end
end
context 'when admin user' do
before { sign_in admin }
it 'allows deletion of any post' do
expect {
delete :destroy, params: { id: post_instance.slug }
}.to change(Post, :count).by(-1)
end
end
end
describe 'PATCH #publish' do
before { sign_in user }
it 'publishes a draft post' do
patch :publish, params: { id: draft_post.slug }
expect(draft_post.reload.status).to eq('published')
end
it 'redirects with success notice' do
patch :publish, params: { id: draft_post.slug }
expect(response).to redirect_to(draft_post)
expect(flash[:notice]).to eq('Post was successfully published.')
end
end
end
# spec/requests/api/v1/posts_spec.rb
require 'rails_helper'
RSpec.describe 'Api::V1::Posts', type: :request do
let(:user) { create(:user) }
let(:admin) { create(:user, :admin) }
let(:category) { create(:category) }
let!(:published_posts) { create_list(:post, 3, category: category) }
let!(:draft_posts) { create_list(:post, 2, :draft, category: category) }
describe 'GET /api/v1/posts' do
it 'returns published posts' do
get '/api/v1/posts'
expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body)
expect(json_response['posts']['data'].length).to eq(3)
end
it 'includes pagination metadata' do
get '/api/v1/posts'
json_response = JSON.parse(response.body)
expect(json_response['meta']).to include(
'current_page',
'total_pages',
'total_count'
)
end
it 'filters by category' do
other_category = create(:category)
create(:post, category: other_category)
get "/api/v1/posts?category=#{category.slug}"
json_response = JSON.parse(response.body)
expect(json_response['posts']['data'].length).to eq(3)
end
it 'searches posts' do
searchable_post = create(:post, title: 'Unique Search Term')
get '/api/v1/posts?search=Unique Search'
json_response = JSON.parse(response.body)
expect(json_response['posts']['data'].length).to eq(1)
end
end
describe 'GET /api/v1/posts/:id' do
let(:post_instance) { published_posts.first }
it 'returns the post' do
get "/api/v1/posts/#{post_instance.slug}"
expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body)
expect(json_response['data']['attributes']['title']).to eq(post_instance.title)
end
it 'returns 404 for non-existent post' do
get '/api/v1/posts/non-existent'
expect(response).to have_http_status(:not_found)
end
end
describe 'POST /api/v1/posts' do
let(:valid_attributes) do
{
title: 'New API Post',
content: 'Post content via API',
category_id: category.id
}
end
context 'with valid authentication' do
before { authenticate_api_user(user) }
it 'creates a new post' do
expect {
post '/api/v1/posts', params: { post: valid_attributes }
}.to change(Post, :count).by(1)
expect(response).to have_http_status(:created)
end
it 'returns the created post' do
post '/api/v1/posts', params: { post: valid_attributes }
json_response = JSON.parse(response.body)
expect(json_response['data']['attributes']['title']).to eq('New API Post')
end
end
context 'without authentication' do
it 'returns unauthorized' do
post '/api/v1/posts', params: { post: valid_attributes }
expect(response).to have_http_status(:unauthorized)
end
end
context 'with invalid attributes' do
before { authenticate_api_user(user) }
it 'returns validation errors' do
post '/api/v1/posts', params: { post: { title: '' } }
expect(response).to have_http_status(:unprocessable_entity)
json_response = JSON.parse(response.body)
expect(json_response['errors']).to be_present
end
end
end
describe 'PUT /api/v1/posts/:id' do
let(:post_instance) { create(:post, user: user) }
before { authenticate_api_user(user) }
it 'updates the post' do
put "/api/v1/posts/#{post_instance.slug}",
params: { post: { title: 'Updated Title' } }
expect(response).to have_http_status(:success)
expect(post_instance.reload.title).to eq('Updated Title')
end
it 'returns the updated post' do
put "/api/v1/posts/#{post_instance.slug}",
params: { post: { title: 'Updated Title' } }
json_response = JSON.parse(response.body)
expect(json_response['data']['attributes']['title']).to eq('Updated Title')
end
end
describe 'DELETE /api/v1/posts/:id' do
let(:post_instance) { create(:post, user: user) }
before { authenticate_api_user(user) }
it 'deletes the post' do
delete "/api/v1/posts/#{post_instance.slug}"
expect(response).to have_http_status(:no_content)
expect(Post.exists?(post_instance.id)).to be false
end
end
end
# Helper method for API authentication in tests
def authenticate_api_user(user)
token = JWT.encode(
{ user_id: user.id, exp: 24.hours.from_now.to_i },
Rails.application.secrets.secret_key_base,
'HS256'
)
request.headers['Authorization'] = "Bearer #{token}"
end
# ═══════════════════════════════════════════════════════════════════════════════
# 11. PERFORMANCE OPTIMIZATION
# ═══════════════════════════════════════════════════════════════════════════════
# config/application.rb - Performance configurations
module MyWebApp
class Application < Rails::Application
# ... existing configuration ...
# Asset pipeline optimizations
config.assets.compile = false
config.assets.digest = true
config.assets.compress = true
# Gzip compression
config.middleware.use Rack::Deflater
# Cache store configuration
config.cache_store = :redis_cache_store, {
url: ENV['REDIS_URL'],
namespace: 'myapp_cache'
}
# Session store with Redis
config.session_store :redis_store,
servers: [ENV['REDIS_URL']],
expire_after: 1.week,
key: '_myapp_session'
end
end
# app/models/concerns/cacheable.rb
module Cacheable
extend ActiveSupport::Concern
class_methods do
def cached_find(id, expires_in: 1.hour)
Rails.cache.fetch("#{self.name.downcase}/#{id}", expires_in: expires_in) do
find(id)
end
end
def cached_count(expires_in: 10.minutes)
Rails.cache.fetch("#{self.name.downcase}/count", expires_in: expires_in) do
count
end
end
end
def cache_key_with_version
"#{super}/#{updated_at.to_i}"
end
def expire_cache
Rails.cache.delete("#{self.class.name.downcase}/#{id}")
Rails.cache.delete("#{self.class.name.downcase}/count")
end
end
# Include in models that need caching
class Post < ApplicationRecord
include Cacheable
after_update :expire_cache
after_destroy :expire_cache
# ... rest of model ...
end
# app/controllers/concerns/caching.rb
module Caching
extend ActiveSupport::Concern
private
def cache_page(key, expires_in: 1.hour, &block)
Rails.cache.fetch(key, expires_in: expires_in) do
yield
end
end
def set_cache_headers(max_age: 1.hour)
response.cache_control[:max_age] = max_age.to_i
response.cache_control[:public] = true
end
end
# Database optimization examples
class OptimizedPostsQuery
def self.homepage_posts
Post.includes(:user, :category, :tags)
.published
.featured
.select(:id, :title, :slug, :excerpt, :published_at, :user_id, :category_id)
.recent
.limit(3)
end
def self.posts_with_stats
Post.joins(:user, :category)
.select('posts.*, users.first_name, users.last_name, categories.name as category_name')
.where(status: :published)
.order(published_at: :desc)
end
def self.popular_posts(limit: 10)
Rails.cache.fetch("popular_posts/#{limit}", expires_in: 1.hour) do
Post.published
.select(:id, :title, :slug, :views_count)
.order(views_count: :desc)
.limit(limit)
end
end
end
# Background job for cache warming
class CacheWarmupJob < ApplicationJob
queue_as :low_priority
def perform
# Warm up popular posts cache
OptimizedPostsQuery.popular_posts
# Warm up categories cache
Category.active.includes(:posts).order(:name)
# Warm up homepage data
OptimizedPostsQuery.homepage_posts
end
end
# Database indexes for performance
=begin
# Add these to your migrations for better query performance
class AddIndexesForPerformance < ActiveRecord::Migration[7.1]
def change
# Posts indexes
add_index :posts, [:status, :published_at]
add_index :posts, [:category_id, :status]
add_index :posts, [:user_id, :status]
add_index :posts, [:featured, :status]
add_index :posts, :views_count
# Comments indexes
add_index :comments, [:post_id, :status, :created_at]
add_index :comments, [:parent_id]
# Users indexes
add_index :users, [:status, :created_at]
add_index :users, :username
# Full-text search indexes (PostgreSQL)
add_index :posts, :title, using: :gin, opclass: :gin_trgm_ops
add_index :posts, :excerpt, using: :gin, opclass: :gin_trgm_ops
end
end
=end
# ═══════════════════════════════════════════════════════════════════════════════
# 12. DEPLOYMENT AND PRODUCTION
# ═══════════════════════════════════════════════════════════════════════════════
# config/environments/production.rb
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# Code is not reloaded between requests.
config.cache_classes = true
# Eager load code on boot.
config.eager_load = true
# Full error reports are disabled and caching is turned on.
config.consider_all_requests_local = false
config.action_controller.perform_caching = true
# Ensures that a master key has been made available
config.require_master_key = true
# Disable serving static files from the `/public` folder by default since
# Apache or NGINX already handles this.
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
# Compress CSS using a preprocessor.
config.assets.css_compressor = :sass
# Do not fallback to assets pipeline if a precompiled asset is missed.
config.assets.compile = false
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
config.asset_host = ENV['ASSET_HOST'] if ENV['ASSET_HOST'].present?
# Specifies the header that your server uses for sending files.
config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
# Store uploaded files on the local file system (see config/storage.yml for options).
config.active_storage.variant_processor = :mini_magick
# Mount Action Cable outside main process or domain.
# config.action_cable.mount_path = nil
# config.action_cable.url = 'wss://example.com/cable'
# config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
# Force all access to the app over SSL
config.force_ssl = true
# Include generic and useful information about system operation
config.log_level = :info
# Prepend all log lines with the following tags.
config.log_tags = [ :request_id ]# RUBY ON RAILS WEB DEVELOPMENT - Comprehensive Reference - by Richard Rembert
# Ruby on Rails enables rapid prototyping and development of full-stack web applications
# with convention over configuration, built-in security, and powerful abstractions
# ═══════════════════════════════════════════════════════════════════════════════
# 13. SECURITY BEST PRACTICES
# ═══════════════════════════════════════════════════════════════════════════════
# config/application.rb - Security configurations
module MyWebApp
class Application < Rails::Application
# ... existing configuration ...
# Security headers
config.force_ssl = true
config.ssl_options = {
redirect: { exclude: ->(request) { request.path =~ /health/ } },
secure_cookies: true,
hsts: {
expires: 1.year,
subdomains: true,
preload: true
}
}
# Content Security Policy
config.content_security_policy do |policy|
policy.default_src :self, :https
policy.font_src :self, :https, :data
policy.img_src :self, :https, :data, 'https://picsum.photos'
policy.object_src :none
policy.script_src :self, :https, :unsafe_inline, :unsafe_eval
policy.style_src :self, :https, :unsafe_inline
# Specify URI for violation reports
policy.report_uri "/csp-violation-report-endpoint"
end
# Referrer Policy
config.referrer_policy = "strict-origin-when-cross-origin"
# Feature Policy
config.permissions_policy = {
camera: :none,
microphone: :none,
geolocation: :self,
payment: :none
}
end
end
# app/controllers/concerns/security.rb
module Security
extend ActiveSupport::Concern
included do
before_action :set_security_headers
before_action :validate_request_origin
before_action :rate_limit_requests
end
private
def set_security_headers
response.headers['X-Frame-Options'] = 'DENY'
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-XSS-Protection'] = '1; mode=block'
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
end
def validate_request_origin
return unless Rails.env.production?
allowed_origins = ['https://mywebapp.com', 'https://www.mywebapp.com']
origin = request.headers['Origin']
if origin.present? && !allowed_origins.include?(origin)
render json: { error: 'Invalid origin' }, status: :forbidden
end
end
def rate_limit_requests
# Simple rate limiting (consider using Rack::Attack for production)
key = "rate_limit:#{request.remote_ip}"
requests = Rails.cache.read(key) || 0
if requests > 100 # 100 requests per minute
render json: { error: 'Rate limit exceeded' }, status: :too_many_requests
return
end
Rails.cache.write(key, requests + 1, expires_in: 1.minute)
end
end
# app/models/concerns/secure_token.rb
module SecureToken
extend ActiveSupport::Concern
class_methods do
def has_secure_token(attribute = :token, length: 32)
define_method("regenerate_#{attribute}") do
update!(attribute => generate_token(length))
end
before_create do
self.send("#{attribute}=", generate_token(length)) if self.send(attribute).blank?
end
end
end
private
def generate_token(length)
SecureRandom.alphanumeric(length)
end
end
# Input sanitization and validation
class SecureValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.blank?
# Remove potentially dangerous HTML tags
sanitized_value = ActionController::Base.helpers.sanitize(
value,
tags: %w[p br strong em ul ol li h1 h2 h3 h4 h5 h6 blockquote],
attributes: %w[href title]
)
# Check for SQL injection patterns
sql_patterns = [
/(\bunion\b.*\bselect\b)/i,
/(\bselect\b.*\bfrom\b)/i,
/(\binsert\b.*\binto\b)/i,
/(\bupdate\b.*\bset\b)/i,
/(\bdelete\b.*\bfrom\b)/i,
/(\bdrop\b.*\btable\b)/i
]
if sql_patterns.any? { |pattern| sanitized_value.match?(pattern) }
record.errors.add(attribute, 'contains potentially unsafe content')
end
# Update the value with sanitized version
record.send("#{attribute}=", sanitized_value)
end
end
# Usage in models
class Post < ApplicationRecord
validates :content, secure: true
validates :excerpt, secure: true
end
# Password security
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:confirmable, :lockable, :trackable
# Strong password validation
validate :password_complexity
# Rate limiting for login attempts
devise :lockable, lock_strategy: :failed_attempts,
unlock_strategy: :time, maximum_attempts: 5,
unlock_in: 30.minutes
private
def password_complexity
return if password.blank?
rules = [
[/.{8,}/, 'must be at least 8 characters long'],
[/[A-Z]/, 'must contain at least one uppercase letter'],
[/[a-z]/, 'must contain at least one lowercase letter'],
[/\d/, 'must contain at least one number'],
[/[^A-Za-z\d]/, 'must contain at least one special character']
]
rules.each do |rule, message|
unless password.match?(rule)
errors.add(:password, message)
end
end
end
end
# File upload security
class ApplicationController < ActionController::Base
before_action :validate_file_uploads
private
def validate_file_uploads
return unless params[:post] && params[:post][:featured_image]
file = params[:post][:featured_image]
# Check file size (max 5MB)
if file.size > 5.megabytes
flash[:alert] = 'File size must be less than 5MB'
redirect_back(fallback_location: root_path)
return
end
# Check file type
allowed_types = %w[image/jpeg image/png image/gif image/webp]
unless allowed_types.include?(file.content_type)
flash[:alert] = 'Only image files are allowed'
redirect_back(fallback_location: root_path)
return
end
# Scan file content for malicious code
if file.read.include?('