My opinionated ruby on rails template
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

inital version

Jasper Mayone 19cb9a24

+407
+407
template.rb
··· 1 + # template.rb 2 + 3 + 4 + # Add gems 5 + gem "devise" # Authentication 6 + gem "okcomputer" 7 + gem "lockbox" 8 + gem "blind_index" 9 + gem "mission_control-jobs" 10 + gem "pg_search" 11 + gem "paper_trail" 12 + gem "strong_migrations" 13 + gem "hashid-rails" 14 + gem "friendly_id" 15 + gem "aasm" 16 + gem "premailer-rails" 17 + gem "email_reply_parser" 18 + gem "invisible_captcha" 19 + gem "browser" 20 + gem "ahoy_matey" # Analytics 21 + gem "ahoy_email" # Email analytics 22 + gem "mailkick" 23 + gem "blazer" # BI dashboard 24 + gem "statsd-instrument" # StatsD metrics 25 + gem "audits1984" # Audit logging 26 + gem "console1984" # Console access auditing 27 + gem "tailwindcss-rails" # Tailwind CSS 28 + gem "pundit" # Authorization 29 + gem "acts_as_paranoid" 30 + gem "pg", "~> 1.6.2" 31 + gem 'rails_performance' 32 + 33 + 34 + ############################################################################### 35 + # FEATURE FLAGS & CONFIGURATION 36 + ############################################################################### 37 + gem "flipper" # Feature flags 38 + gem "flipper-active_record" # ActiveRecord adapter for Flipper 39 + gem "flipper-ui" # UI for Flipper 40 + gem "flipper-active_support_cache_store" 41 + 42 + ############################################################################### 43 + # ENVIRONMENT VARIABLES 44 + ############################################################################### 45 + gem "dotenv-rails" # Environment variables 46 + 47 + gem_group :development, :test do 48 + gem "rspec-rails", "~> 7.1" # Testing framework 49 + gem "factory_bot_rails" # Test data factories 50 + gem "faker" # Fake data generation 51 + gem "shoulda-matchers" # RSpec matchers 52 + gem "rubocop-capybara", "~> 2.22", ">= 2.22.1" 53 + gem "rubocop-rspec", "~> 3.6" 54 + gem "rubocop-rspec_rails", "~> 2.31" 55 + gem "relaxed-rubocop" 56 + gem "query_count" # SQL query counter 57 + gem "bullet" # N+1 query detection 58 + end 59 + 60 + gem_group :development do 61 + gem "actual_db_schema" # Rolls back phantom migrations 62 + gem "annotaterb" # Annotate models 63 + gem "listen", "~> 3.9" # File watcher 64 + gem "letter_opener_web" # Preview emails 65 + gem "foreman" # Process manager 66 + gem "awesome_print" # Pretty print objects 67 + gem "rack-mini-profiler", "~> 3.3", require: false # Performance profiling 68 + gem "stackprof" # Used by rack-mini-profiler for flamegraphs 69 + end 70 + 71 + # Configure database to use PostgreSQL 72 + remove_file "config/database.yml" 73 + create_file "config/database.yml", <<~YAML 74 + default: &default 75 + adapter: postgresql 76 + encoding: unicode 77 + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 78 + 79 + development: 80 + <<: *default 81 + database: #{app_name}_development 82 + 83 + test: 84 + <<: *default 85 + database: #{app_name}_test 86 + 87 + production: 88 + <<: *default 89 + database: #{app_name}_production 90 + username: #{app_name} 91 + password: <%= ENV["#{app_name.upcase}_DATABASE_PASSWORD"] %> 92 + YAML 93 + 94 + after_bundle do 95 + # Generate Devise 96 + generate "devise:install" 97 + generate "devise", "User" 98 + 99 + # Add fields to User 100 + generate "migration", "AddFieldsToUsers first_name:string last_name:string access_level:integer" 101 + 102 + # Update the migration to add default and null constraint for access_level 103 + in_root do 104 + migration_file = Dir.glob("db/migrate/*_add_fields_to_users.rb").first 105 + if migration_file 106 + # Replace the access_level line with one that has default and null constraint 107 + gsub_file migration_file, 108 + /add_column :users, :access_level, :integer$/, 109 + "add_column :users, :access_level, :integer, default: 0, null: false" 110 + end 111 + end 112 + 113 + # Generate Lockbox master key using Lockbox.generate_key 114 + # This needs to run after bundle install, so Lockbox is available 115 + lockbox_key = `rails runner "require 'lockbox'; puts Lockbox.generate_key"`.strip 116 + 117 + # Add lockbox key to credentials programmatically 118 + # Create a Ruby script that directly writes to credentials 119 + create_file 'tmp/add_lockbox_to_credentials.rb', <<~RUBY, force: true 120 + require 'active_support/encrypted_configuration' 121 + require 'yaml' 122 + 123 + # Path to credentials files 124 + credentials_path = Rails.root.join('config/credentials.yml.enc') 125 + key_path = Rails.root.join('config/master.key') 126 + 127 + # Read the key 128 + key = File.read(key_path).strip 129 + 130 + # Create encrypted configuration instance 131 + credentials = ActiveSupport::EncryptedConfiguration.new( 132 + config_path: credentials_path, 133 + key_path: key_path, 134 + env_key: 'RAILS_MASTER_KEY', 135 + raise_if_missing_key: true 136 + ) 137 + 138 + # Read existing credentials 139 + current_config = credentials.config 140 + 141 + # Parse as YAML if it's a string, and ensure we have a hash with string keys 142 + if current_config.is_a?(String) 143 + current_config = YAML.safe_load(current_config, permitted_classes: [Symbol]) || {} 144 + end 145 + 146 + # Convert all keys to strings recursively 147 + current_config = current_config.deep_stringify_keys if current_config.respond_to?(:deep_stringify_keys) 148 + 149 + # Add lockbox config if not present 150 + unless current_config.key?('lockbox') 151 + current_config['lockbox'] = { 'master_key' => '#{lockbox_key}' } 152 + 153 + # Write back to credentials - ensure clean YAML format 154 + yaml_content = current_config.to_yaml 155 + # Remove the YAML document separator for cleaner output 156 + yaml_content = yaml_content.sub(/^---\\n/, '') 157 + # Add blank line before lockbox section for better readability 158 + yaml_content = yaml_content.sub(/^lockbox:/, "\\nlockbox:") 159 + 160 + credentials.write(yaml_content) 161 + puts "✓ Added lockbox master_key to credentials" 162 + else 163 + puts "⚠ Lockbox config already exists in credentials" 164 + end 165 + RUBY 166 + 167 + rails_command "runner tmp/add_lockbox_to_credentials.rb" 168 + remove_file 'tmp/add_lockbox_to_credentials.rb' 169 + 170 + initializer 'lockbox.rb', <<~RUBY 171 + # Set Lockbox master key from credentials 172 + if Rails.application.credentials.lockbox&.key?(:master_key) 173 + Lockbox.master_key = Rails.application.credentials.lockbox[:master_key] 174 + else 175 + Rails.logger.warn "Lockbox master_key not found in credentials. Please add it by running: rails credentials:edit" 176 + end 177 + RUBY 178 + 179 + initializer 'okcomputer.rb', <<~RUBY 180 + # frozen_string_literal: true 181 + 182 + # https://github.com/jphenow/okcomputer#registering-additional-checks 183 + # 184 + # class MyCustomCheck < OKComputer::Check 185 + # def call 186 + # if rand(10).even? 187 + # "Even is great!" 188 + # else 189 + # mark_failure 190 + # "We don't like odd numbers" 191 + # end 192 + # end 193 + # end 194 + 195 + OkComputer::Registry.register "database", OkComputer::ActiveRecordCheck.new 196 + OkComputer::Registry.register "cache", OkComputer::CacheCheck.new 197 + 198 + OkComputer::Registry.register "app_version", OkComputer::AppVersionCheck.new 199 + OkComputer::Registry.register "action_mailer", OkComputer::ActionMailerCheck.new 200 + 201 + # Run checks in parallel 202 + OkComputer.check_in_parallel = true 203 + 204 + # Log when health checks are run 205 + OkComputer.logger = Rails.logger 206 + RUBY 207 + 208 + initializer 'rails_performance.rb', <<~RUBY 209 + RailsPerformance.setup do |config| 210 + config.redis = Redis.new(url: ENV["REDIS_URL"].presence || "redis://127.0.0.1:6379/0") 211 + config.duration = 4.hours 212 + 213 + config.enabled = true 214 + 215 + # protect with authentication 216 + config.verify_access_proc = proc { |controller| 217 + controller.current_user&.admin_access? 218 + } 219 + 220 + # Ignore admin and performance paths 221 + config.ignored_paths = ['/admin', '/rails/performance'] 222 + 223 + config.home_link = '/' 224 + config.skipable_rake_tasks = ['webpacker:compile'] 225 + end if defined?(RailsPerformance) 226 + RUBY 227 + 228 + inject_into_file 'app/models/application_record.rb', 229 + " include PgSearch::Model\n", 230 + after: "primary_abstract_class\n" 231 + 232 + inject_into_file 'app/models/application_record.rb', 233 + " has_paper_trail\n", 234 + after: "include PgSearch::Model\n" 235 + 236 + inject_into_file 'app/models/application_record.rb', 237 + " has_paper_trail\n", 238 + after: "# include Hashid::Rails\n" 239 + 240 + 241 + generare "rails_performance:install" 242 + generate "devise:views" 243 + generate "pg_search:migration:multisearch" 244 + generate "lockbox:audits" 245 + generate "pundit:install" 246 + generate "ahoy:install" 247 + generate "ahoy:messages --encryption=lockbox" 248 + generate "ahoy:clicks" 249 + generate "annotate_rb:install" 250 + generate "blazer:install" 251 + generate "flipper:setup" 252 + generate "bullet:install" 253 + generate "strong_migrations:install" 254 + generate "solid_queue:install" 255 + generate "paper_trail:install" 256 + generate "mailkick:install" 257 + generate "mailkick:views" 258 + 259 + # Set up RSpec 260 + generate "rspec:install" 261 + 262 + # Create and run migrations 263 + rails_command "db:create" 264 + rails_command "db:migrate" 265 + 266 + # Create a custom controller 267 + # generate :controller, "pages", "home" 268 + # route "root to: 'pages#home'" 269 + 270 + # Add enum and has_subscriptions to User model 271 + inject_into_file 'app/models/user.rb', after: "class User < ApplicationRecord\n" do 272 + <<~RUBY 273 + enum :access_level, { 274 + user: 0, 275 + admin: 1, 276 + super_admin: 2, 277 + owner: 3 278 + }, default: :user, null: false 279 + 280 + has_subscriptions 281 + 282 + # Helper method to check if user has any admin access 283 + def admin_access? 284 + admin? || super_admin? || owner? 285 + end 286 + 287 + # Return full name 288 + def full_name 289 + "\#{first_name} \#{last_name}".strip 290 + end 291 + 292 + RUBY 293 + end 294 + 295 + file 'app/controllers/admin/application_controller.rb', <<~RUBY 296 + module Admin 297 + class ApplicationController < ::ApplicationController 298 + include Pundit 299 + rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized 300 + before_action :require_admin 301 + 302 + # Shared admin logic here 303 + def index 304 + @current_user = current_user 305 + end 306 + 307 + private 308 + 309 + def require_admin 310 + unless current_user&.admin? || current_user&.super_admin? || current_user&.owner? 311 + redirect_to root_path, alert: "You are not authorized to access this area." 312 + end 313 + end 314 + 315 + def user_not_authorized 316 + flash[:alert] = "You are not authorized to perform this action." 317 + redirect_to(request.referrer || root_path) 318 + end 319 + end 320 + end 321 + RUBY 322 + 323 + # Create Admin Policy 324 + file 'app/policies/admin_policy.rb', <<~RUBY 325 + class AdminPolicy < ApplicationPolicy 326 + def blazer? 327 + user&.admin? || user&.super_admin? || user&.owner? 328 + end 329 + 330 + def flipper? 331 + user&.super_admin? || user&.owner? 332 + end 333 + 334 + def access_admin_endpoints? 335 + user&.admin? || user&.super_admin? || user&.owner? 336 + end 337 + end 338 + RUBY 339 + 340 + 341 + # Configure admin routes 342 + route <<~RUBY 343 + namespace :admin do 344 + root to: "application#index" 345 + 346 + mount Blazer::Engine, at: "blazer", constraints: ->(request) { 347 + user = User.find_by(id: request.session[:user_id]) 348 + user && AdminPolicy.new(user, :admin).blazer? 349 + } 350 + 351 + mount Flipper::UI.app(Flipper), at: "flipper", constraints: ->(request) { 352 + user = User.find_by(id: request.session[:user_id]) 353 + user && AdminPolicy.new(user, :admin).flipper? 354 + } 355 + 356 + mount RailsPerformance::Engine, at: "performance", constraints: ->(request) { 357 + user = User.find_by(id: request.session[:user_id]) 358 + user && AdminPolicy.new(user, :admin).access_admin_endpoints? 359 + } 360 + 361 + resources :users, shallow: true 362 + end 363 + RUBY 364 + 365 + # Mount OkComputer health checks 366 + route <<~RUBY 367 + mount OkComputer::Engine, at: "/healthchecks" 368 + RUBY 369 + 370 + inject_into_file 'app/mailers/application_mailer.rb', 371 + " has_history\nutm_params\n", 372 + after: "class ApplicationMailer < ActionMailer::Base\n" 373 + 374 + inject_into_file 'app/controllers/application_controller.rb', 375 + " include Pundit::Authorization\n before_action :set_paper_trail_whodunnit\n", 376 + after: "class ApplicationController < ActionController::Base\n" 377 + 378 + # Create GitHub workflows 379 + empty_directory '.github/workflows' 380 + 381 + file '.github/workflows/check-indexes.yml', <<~YAML 382 + name: Check Indexes 383 + on: 384 + pull_request: 385 + paths: 386 + - 'db/migrate/**.rb' 387 + 388 + jobs: 389 + check-indexes: 390 + runs-on: ubuntu-latest 391 + steps: 392 + - uses: actions/checkout@v4 393 + with: 394 + fetch-depth: 0 395 + 396 + - name: Check Migration Indexes 397 + uses: speedshop/ids_must_be_indexed@v1.2.1 398 + YAML 399 + 400 + # Run migrations one final time to catch any remaining pending migrations 401 + rails_command "db:migrate" 402 + 403 + # Git initialization 404 + git :init 405 + git add: "." 406 + git commit: "-m 'Initial commit (from @jaspermayone/rails-template)'" 407 + end