My opinionated ruby on rails template
0
fork

Configure Feed

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

new template system

+2979 -627
+40
.claude/commands/new-module.md
··· 1 + --- 2 + description: Create a new boxcar module/template 3 + argument-hint: <module-name> 4 + --- 5 + 6 + Create a new boxcar module called `$ARGUMENTS.rb` in this Rails template project. 7 + 8 + ## Context 9 + - This is a Rails application template project (used with `rails new myapp -m template.rb`) 10 + - Modules are Ruby files that get applied via `apply_template('$ARGUMENTS')` in `template.rb` 11 + - Each module should be self-contained and handle one feature/concern 12 + 13 + ## Module Structure 14 + Follow the existing patterns in this project. A module should: 15 + 16 + 1. Start with `# frozen_string_literal: true` 17 + 2. Use `say` with colors for user feedback: 18 + - `:green` for main section headers 19 + - `:cyan` for sub-steps 20 + - `:yellow` for warnings 21 + - `:red` for errors 22 + 3. Use Rails template methods like: 23 + - `gem 'name'` - add gems 24 + - `generate :model, 'Name field:type'` - run generators 25 + - `file 'path', <<~RUBY ... RUBY` - create files 26 + - `inject_into_file`, `gsub_file` - modify files 27 + - `route` - add routes 28 + - `after_bundle do ... end` - run code after bundle install 29 + 30 + ## Reference Files 31 + Look at these existing modules for patterns: 32 + - @auth.rb - authentication module 33 + - @public_identifiable.rb - public IDs module 34 + - @tailwind.rb - Tailwind CSS module 35 + - @template.rb - main template entry point 36 + 37 + ## Task 38 + 1. Ask what the module should do if not clear from the name 39 + 2. Create the module file `$ARGUMENTS.rb` 40 + 3. Show how to add it to `template.rb` using `apply_template('$ARGUMENTS')`
+25
README.md
··· 1 + # boxcar 2 + 3 + my opinionated rails template for my projects. 4 + 5 + ## usage 6 + 7 + ```zsh 8 + rails new myapp \ 9 + --no-rc \ 10 + --skip-kamal \ 11 + --skip-jbuilder \ 12 + --skip-test \ 13 + --skip-system-test \ 14 + --skip-action-mailbox \ 15 + --skip-action-text \ 16 + --skip-active-storage \ 17 + --skip-sprockets \ 18 + --skip-i18n \ 19 + --skip-spring \ 20 + --javascript=bun \ 21 + --css=tailwind \ 22 + -d postgresql \ 23 + -m https://raw.githubusercontent.com/jaspermayone/boxcar/main/template.rb 24 + ``` 25 +
+11
aasm.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say 'Setting up AASM for state machines...', :green 4 + 5 + gem 'aasm' 6 + 7 + say 'AASM configured!', :green 8 + say ' Usage:', :cyan 9 + say ' include AASM in your model', :cyan 10 + say ' Define states and events with aasm block', :cyan 11 + say ' Migration: add_column :table, :state, :string, default: "initial"', :cyan
+60
admin_routes.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say 'Configuring admin routes...', :green 4 + 5 + say ' Creating AdminConstraint for route protection...', :cyan 6 + file 'app/constraints/admin_constraint.rb', <<~RUBY 7 + # frozen_string_literal: true 8 + 9 + class AdminConstraint 10 + def initialize(policy_method) 11 + @policy_method = policy_method 12 + end 13 + 14 + def matches?(request) 15 + token = request.cookie_jar.signed[:session_token] 16 + return false unless token 17 + 18 + session = Session.find_by(token: token) 19 + return false unless session&.user 20 + 21 + AdminPolicy.new(session.user, :admin).public_send(@policy_method) 22 + rescue StandardError 23 + false 24 + end 25 + end 26 + RUBY 27 + 28 + say ' Adding admin namespace routes...', :cyan 29 + route <<~RUBY 30 + namespace :admin do 31 + root to: 'application#index' 32 + 33 + # Blazer BI Dashboard (admin or above) 34 + mount Blazer::Engine, at: 'blazer', constraints: AdminConstraint.new(:blazer?) if defined?(Blazer) 35 + 36 + # Flipper Feature Flags (super_admin or above) 37 + mount Flipper::UI.app(Flipper), at: 'flipper', constraints: AdminConstraint.new(:flipper?) if defined?(Flipper) 38 + 39 + # Rails Performance Dashboard (admin or above) 40 + mount RailsPerformance::Engine, at: 'performance', constraints: AdminConstraint.new(:rails_performance?) if defined?(RailsPerformance) 41 + 42 + # Mission Control Jobs Dashboard (admin or above) 43 + mount MissionControl::Jobs::Engine, at: 'jobs', constraints: AdminConstraint.new(:jobs?) if defined?(MissionControl::Jobs) 44 + 45 + # Console Audits (super_admin or above) 46 + mount Audits1984::Engine, at: 'console_audits', constraints: AdminConstraint.new(:console_audits?) if defined?(Audits1984) 47 + 48 + resources :users 49 + end 50 + RUBY 51 + 52 + say ' Adding health check routes...', :cyan 53 + route <<~RUBY 54 + # Health checks (public endpoint) 55 + mount OkComputer::Engine, at: '/health' if defined?(OkComputer) 56 + RUBY 57 + 58 + say 'Admin routes configured!', :green 59 + say ' Admin dashboard at /admin', :cyan 60 + say ' Health checks at /health', :cyan
+96
analytics.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say 'Setting up Ahoy for analytics...', :green 4 + 5 + gem 'ahoy_matey' 6 + gem 'ahoy_email' 7 + 8 + after_bundle do 9 + say ' Running Ahoy generators...', :cyan 10 + rails_command 'generate ahoy:install' 11 + rails_command 'generate ahoy:messages --encryption=lockbox' 12 + rails_command 'generate ahoy:clicks' 13 + end 14 + 15 + say ' Creating Ahoy initializer...', :cyan 16 + file 'config/initializers/ahoy.rb', <<~RUBY 17 + # frozen_string_literal: true 18 + 19 + class Ahoy::Store < Ahoy::DatabaseStore 20 + end 21 + 22 + Ahoy.api = true 23 + Ahoy.server_side_visits = :when_needed 24 + 25 + # Mask IPs for privacy (GDPR compliance) 26 + Ahoy.mask_ips = true 27 + 28 + # Cookie settings 29 + Ahoy.cookies = :none # or :all for full tracking 30 + 31 + # Visit duration 32 + Ahoy.visit_duration = 30.minutes 33 + 34 + # Geocoding (requires geocoder gem) 35 + # Ahoy.geocode = true 36 + RUBY 37 + 38 + say ' Creating Ahoy Email initializer...', :cyan 39 + file 'config/initializers/ahoy_email.rb', <<~RUBY 40 + # frozen_string_literal: true 41 + 42 + # Configure ahoy_email for email tracking 43 + AhoyEmail.api = true 44 + 45 + # Default tracking options 46 + AhoyEmail.default_options[:message] = true # Store message metadata 47 + AhoyEmail.default_options[:open] = true # Track email opens (via tracking pixel) 48 + AhoyEmail.default_options[:click] = true # Track link clicks 49 + AhoyEmail.default_options[:utm_params] = false # Don't add UTM parameters 50 + 51 + # Register the message subscriber to store messages 52 + AhoyEmail.subscribers << AhoyEmail::MessageSubscriber 53 + 54 + # Configure message model 55 + AhoyEmail.message_model = -> { Ahoy::Message } 56 + 57 + # Track email opens and clicks 58 + # Note: Opens require an image to be loaded, which may be blocked by email clients 59 + # Clicks work by redirecting through the application before going to the final URL 60 + RUBY 61 + 62 + say ' Creating Trackable concern...', :cyan 63 + file 'app/models/concerns/trackable.rb', <<~RUBY 64 + # frozen_string_literal: true 65 + 66 + # Include in ApplicationController for visit/event tracking 67 + # 68 + # Usage in controllers: 69 + # ahoy.track "Viewed Product", product_id: product.id 70 + # 71 + # Usage in views: 72 + # <% ahoy.track "Viewed Page", page: request.path %> 73 + # 74 + module Trackable 75 + extend ActiveSupport::Concern 76 + 77 + included do 78 + # Track visits automatically 79 + # before_action :track_ahoy_visit 80 + end 81 + 82 + def track_event(name, properties = {}) 83 + ahoy.track(name, properties) 84 + end 85 + end 86 + RUBY 87 + 88 + say ' Adding Ahoy to ApplicationController...', :cyan 89 + inject_into_class 'app/controllers/application_controller.rb', 'ApplicationController', <<~RUBY 90 + include Trackable 91 + RUBY 92 + 93 + say 'Ahoy analytics configured!', :green 94 + say ' Track events: ahoy.track "Event Name", key: value', :cyan 95 + say ' Email tracking enabled for opens and clicks', :cyan 96 + say ' Run migrations: rails db:migrate', :yellow
+376
auth.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say 'Setting up custom authentication...', :green 4 + say ' Adding bcrypt gem...', :cyan 5 + 6 + gem 'bcrypt', '~> 3.1' 7 + 8 + say ' Generating User model...', :cyan 9 + generate :model, 'User email:string:uniq password_digest:string role:integer' 10 + 11 + # Add index for email lookup 12 + begin 13 + inject_into_file "db/migrate/#{Dir.entries('db/migrate').grep(/create_users/).first}", 14 + after: "t.string :email\n" do 15 + " t.index :email, unique: true\n" 16 + end 17 + rescue StandardError 18 + nil 19 + end 20 + 21 + # User model with has_secure_password and roles 22 + file 'app/models/user.rb', <<~RUBY, force: true 23 + class User < ApplicationRecord 24 + include PublicIdentifiable 25 + set_public_id_prefix :usr 26 + 27 + has_secure_password 28 + 29 + enum :role, { user: 0, admin: 1, super_admin: 2, owner: 3 }, default: :user 30 + 31 + normalizes :email, with: ->(email) { email.strip.downcase } 32 + 33 + validates :email, presence: true, 34 + uniqueness: { case_sensitive: false }, 35 + format: { with: URI::MailTo::EMAIL_REGEXP } 36 + validates :password, length: { minimum: 8 }, if: -> { password.present? } 37 + validates :role, presence: true 38 + 39 + # Helper method to check if user has any admin access 40 + def admin_or_above? 41 + admin? || super_admin? || owner? 42 + end 43 + 44 + # Helper method to check if user has super admin or owner access 45 + def super_admin_or_above? 46 + super_admin? || owner? 47 + end 48 + end 49 + RUBY 50 + 51 + say ' Generating Session model...', :cyan 52 + generate :model, 'Session user:references token:string:uniq ip_address:string user_agent:string' 53 + 54 + file 'app/models/session.rb', <<~RUBY, force: true 55 + class Session < ApplicationRecord 56 + belongs_to :user 57 + 58 + before_create :generate_token 59 + 60 + validates :token, presence: true, uniqueness: true 61 + 62 + private 63 + 64 + def generate_token 65 + self.token = SecureRandom.urlsafe_base64(32) 66 + end 67 + end 68 + RUBY 69 + 70 + say ' Creating Current model...', :cyan 71 + file 'app/models/current.rb', <<~RUBY 72 + class Current < ActiveSupport::CurrentAttributes 73 + attribute :session, :user 74 + 75 + delegate :user, to: :session, allow_nil: true 76 + end 77 + RUBY 78 + 79 + say ' Creating Authentication concern...', :cyan 80 + file 'app/controllers/concerns/authentication.rb', <<~RUBY 81 + module Authentication 82 + extend ActiveSupport::Concern 83 + 84 + included do 85 + before_action :authenticate 86 + helper_method :signed_in?, :current_user 87 + end 88 + 89 + private 90 + 91 + def authenticate 92 + if (session_record = find_session_by_cookie) 93 + Current.session = session_record 94 + end 95 + end 96 + 97 + def require_authentication 98 + redirect_to sign_in_path, alert: 'Please sign in to continue.' unless signed_in? 99 + end 100 + 101 + def require_admin 102 + require_authentication 103 + return if current_user&.admin_or_above? 104 + 105 + redirect_to root_path, alert: 'You are not authorized to access this page.' 106 + end 107 + 108 + def require_super_admin 109 + require_authentication 110 + return if current_user&.super_admin? 111 + 112 + redirect_to root_path, alert: 'You are not authorized to access this page.' 113 + end 114 + 115 + def signed_in? 116 + Current.session.present? 117 + end 118 + 119 + def current_user 120 + Current.user 121 + end 122 + 123 + def find_session_by_cookie 124 + Session.find_by(token: cookies.signed[:session_token]) 125 + end 126 + 127 + def start_session(user) 128 + session_record = user.sessions.create!( 129 + ip_address: request.remote_ip, 130 + user_agent: request.user_agent 131 + ) 132 + cookies.signed.permanent[:session_token] = { 133 + value: session_record.token, 134 + httponly: true, 135 + same_site: :lax 136 + } 137 + Current.session = session_record 138 + end 139 + 140 + def end_session 141 + Current.session&.destroy 142 + cookies.delete(:session_token) 143 + end 144 + end 145 + RUBY 146 + 147 + say ' Adding to ApplicationController...', :cyan 148 + inject_into_class 'app/controllers/application_controller.rb', 'ApplicationController', <<~RUBY 149 + include Authentication 150 + RUBY 151 + 152 + say ' Creating SessionsController...', :cyan 153 + file 'app/controllers/sessions_controller.rb', <<~RUBY 154 + class SessionsController < ApplicationController 155 + skip_before_action :authenticate, only: %i[new create] 156 + 157 + def new 158 + redirect_to root_path if signed_in? 159 + end 160 + 161 + def create 162 + if (user = User.find_by(email: params[:email])&.authenticate(params[:password])) 163 + start_session(user) 164 + redirect_to root_path, notice: 'Signed in successfully.' 165 + else 166 + flash.now[:alert] = 'Invalid email or password.' 167 + render :new, status: :unprocessable_entity 168 + end 169 + end 170 + 171 + def destroy 172 + end_session 173 + redirect_to root_path, notice: 'Signed out successfully.' 174 + end 175 + end 176 + RUBY 177 + 178 + say ' Creating RegistrationsController...', :cyan 179 + file 'app/controllers/registrations_controller.rb', <<~RUBY 180 + class RegistrationsController < ApplicationController 181 + skip_before_action :authenticate, only: %i[new create] 182 + 183 + def new 184 + redirect_to root_path if signed_in? 185 + @user = User.new 186 + end 187 + 188 + def create 189 + @user = User.new(user_params) 190 + if @user.save 191 + start_session(@user) 192 + redirect_to root_path, notice: 'Account created successfully.' 193 + else 194 + render :new, status: :unprocessable_entity 195 + end 196 + end 197 + 198 + private 199 + 200 + def user_params 201 + params.require(:user).permit(:email, :password, :password_confirmation) 202 + end 203 + end 204 + RUBY 205 + 206 + say ' Creating sign in view...', :cyan 207 + file 'app/views/sessions/new.html.erb', <<~ERB 208 + <div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"> 209 + <div class="max-w-md w-full space-y-8"> 210 + <div> 211 + <h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900"> 212 + Sign in to your account 213 + </h2> 214 + <p class="mt-2 text-center text-sm text-gray-600"> 215 + Or 216 + <%= link_to 'create a new account', sign_up_path, class: 'font-medium text-canopy-green hover:text-fresh-leaf' %> 217 + </p> 218 + </div> 219 + 220 + <%= form_with url: sign_in_path, class: 'mt-8 space-y-6' do |f| %> 221 + <div class="rounded-md shadow-sm -space-y-px"> 222 + <div> 223 + <%= f.label :email, class: 'sr-only' %> 224 + <%= f.email_field :email, required: true, autofocus: true, autocomplete: 'email', 225 + placeholder: 'Email address', 226 + class: 'appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-canopy-green focus:border-canopy-green focus:z-10 sm:text-sm' %> 227 + </div> 228 + <div> 229 + <%= f.label :password, class: 'sr-only' %> 230 + <%= f.password_field :password, required: true, autocomplete: 'current-password', 231 + placeholder: 'Password', 232 + class: 'appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-canopy-green focus:border-canopy-green focus:z-10 sm:text-sm' %> 233 + </div> 234 + </div> 235 + 236 + <div> 237 + <%= f.submit 'Sign in', 238 + class: 'group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-canopy-green hover:bg-bamboo-shadow focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-canopy-green cursor-pointer' %> 239 + </div> 240 + <% end %> 241 + </div> 242 + </div> 243 + ERB 244 + 245 + say ' Creating sign up view...', :cyan 246 + file 'app/views/registrations/new.html.erb', <<~ERB 247 + <div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"> 248 + <div class="max-w-md w-full space-y-8"> 249 + <div> 250 + <h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900"> 251 + Create your account 252 + </h2> 253 + <p class="mt-2 text-center text-sm text-gray-600"> 254 + Already have an account? 255 + <%= link_to 'Sign in', sign_in_path, class: 'font-medium text-canopy-green hover:text-fresh-leaf' %> 256 + </p> 257 + </div> 258 + 259 + <%= form_with model: @user, url: sign_up_path, class: 'mt-8 space-y-6' do |f| %> 260 + <% if @user.errors.any? %> 261 + <div class="rounded-md bg-red-50 p-4"> 262 + <div class="text-sm text-red-700"> 263 + <ul class="list-disc pl-5 space-y-1"> 264 + <% @user.errors.full_messages.each do |message| %> 265 + <li><%= message %></li> 266 + <% end %> 267 + </ul> 268 + </div> 269 + </div> 270 + <% end %> 271 + 272 + <div class="rounded-md shadow-sm -space-y-px"> 273 + <div> 274 + <%= f.label :email, class: 'sr-only' %> 275 + <%= f.email_field :email, required: true, autofocus: true, autocomplete: 'email', 276 + placeholder: 'Email address', 277 + class: 'appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-canopy-green focus:border-canopy-green focus:z-10 sm:text-sm' %> 278 + </div> 279 + <div> 280 + <%= f.label :password, class: 'sr-only' %> 281 + <%= f.password_field :password, required: true, autocomplete: 'new-password', 282 + placeholder: 'Password (min 8 characters)', 283 + class: 'appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-canopy-green focus:border-canopy-green focus:z-10 sm:text-sm' %> 284 + </div> 285 + <div> 286 + <%= f.label :password_confirmation, class: 'sr-only' %> 287 + <%= f.password_field :password_confirmation, required: true, autocomplete: 'new-password', 288 + placeholder: 'Confirm password', 289 + class: 'appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-canopy-green focus:border-canopy-green focus:z-10 sm:text-sm' %> 290 + </div> 291 + </div> 292 + 293 + <div> 294 + <%= f.submit 'Create account', 295 + class: 'group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-canopy-green hover:bg-bamboo-shadow focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-canopy-green cursor-pointer' %> 296 + </div> 297 + <% end %> 298 + </div> 299 + </div> 300 + ERB 301 + 302 + say ' Adding routes...', :cyan 303 + route <<~RUBY 304 + # Authentication 305 + get 'sign_in', to: 'sessions#new' 306 + post 'sign_in', to: 'sessions#create' 307 + delete 'sign_out', to: 'sessions#destroy' 308 + get 'sign_up', to: 'registrations#new' 309 + post 'sign_up', to: 'registrations#create' 310 + RUBY 311 + 312 + say ' Creating Admin namespace...', :cyan 313 + file 'app/controllers/admin/application_controller.rb', <<~RUBY 314 + # frozen_string_literal: true 315 + 316 + module Admin 317 + class ApplicationController < ::ApplicationController 318 + before_action :require_admin 319 + 320 + def index 321 + @current_user = current_user 322 + end 323 + 324 + private 325 + 326 + def require_admin 327 + unless current_user&.admin_or_above? 328 + redirect_to root_path, alert: 'You are not authorized to access this area.' 329 + end 330 + end 331 + end 332 + end 333 + RUBY 334 + 335 + file 'app/controllers/admin/users_controller.rb', <<~RUBY 336 + # frozen_string_literal: true 337 + 338 + module Admin 339 + class UsersController < Admin::ApplicationController 340 + before_action :set_user, only: %i[show edit update destroy] 341 + 342 + def index 343 + @users = User.all 344 + end 345 + 346 + def show; end 347 + 348 + def edit; end 349 + 350 + def update 351 + if @user.update(user_params) 352 + redirect_to admin_user_path(@user), notice: 'User updated successfully.' 353 + else 354 + render :edit, status: :unprocessable_entity 355 + end 356 + end 357 + 358 + def destroy 359 + @user.destroy 360 + redirect_to admin_users_path, notice: 'User deleted successfully.' 361 + end 362 + 363 + private 364 + 365 + def set_user 366 + @user = User.find(params[:id]) 367 + end 368 + 369 + def user_params 370 + params.require(:user).permit(:email, :role) 371 + end 372 + end 373 + end 374 + RUBY 375 + 376 + say 'Custom authentication setup complete!', :green
+69
blazer.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say 'Setting up Blazer for BI dashboard...', :green 4 + 5 + gem 'blazer' 6 + 7 + after_bundle do 8 + say ' Running Blazer install...', :cyan 9 + rails_command 'generate blazer:install' 10 + end 11 + 12 + say ' Creating Blazer initializer...', :cyan 13 + file 'config/initializers/blazer.rb', <<~RUBY 14 + # frozen_string_literal: true 15 + 16 + # Blazer configuration is in config/blazer.yml 17 + # This file handles additional Ruby-based config 18 + 19 + # Ensure Blazer uses the correct database 20 + # Blazer.settings["data_sources"]["main"]["url"] = ENV["DATABASE_URL"] 21 + RUBY 22 + 23 + say ' Creating Blazer config...', :cyan 24 + file 'config/blazer.yml', <<~YAML 25 + # Blazer configuration 26 + # https://github.com/ankane/blazer 27 + 28 + data_sources: 29 + main: 30 + url: <%= ENV["DATABASE_URL"] %> 31 + 32 + # Statement timeout (in seconds) 33 + timeout: 15 34 + 35 + # Caching (requires cache store) 36 + cache: 37 + mode: slow # or "all" 38 + expires_in: 60 # seconds 39 + 40 + # Smart variables (dropdown filters in queries) 41 + smart_variables: 42 + user_id: "SELECT id, email FROM users ORDER BY email" 43 + # state: "SELECT DISTINCT state FROM orders" 44 + 45 + # Linked columns (make values clickable) 46 + linked_columns: 47 + user_id: "/admin/users/{value}" 48 + # order_id: "/admin/orders/{value}" 49 + 50 + # Smart columns (format output) 51 + smart_columns: 52 + created_at: datetime 53 + updated_at: datetime 54 + # amount: currency 55 + 56 + # Audit logging 57 + audit: true 58 + 59 + # Check queries for anomalies 60 + check_schedules: 61 + - "1 day" 62 + - "1 week" 63 + - "1 month" 64 + YAML 65 + 66 + say 'Blazer BI dashboard configured!', :green 67 + say ' Access at /admin/blazer (admin only)', :cyan 68 + say ' Run migrations: rails db:migrate', :yellow 69 + say ' Configure data sources in config/blazer.yml', :cyan
+62
console1984.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say 'Setting up Console1984 for console access auditing...', :green 4 + 5 + gem 'console1984' 6 + gem 'audits1984' 7 + 8 + after_bundle do 9 + say ' Running Console1984 installer...', :cyan 10 + rails_command 'console1984:install' 11 + 12 + say ' Running Audits1984 installer...', :cyan 13 + rails_command 'audits1984:install' 14 + end 15 + 16 + say ' Creating Console1984 initializer...', :cyan 17 + file 'config/initializers/console1984.rb', <<~RUBY 18 + # frozen_string_literal: true 19 + 20 + Console1984.config do |config| 21 + # Require a reason for console access 22 + config.ask_for_session_reason = true 23 + 24 + # Protected URLs that will be flagged when accessed 25 + config.protected_urls = [ 26 + %r{/admin}, 27 + %r{/users/\\d+} 28 + ] 29 + 30 + # Protected environments (production by default) 31 + config.protected_environments = %i[production] 32 + 33 + # Incinerate console sessions after this period 34 + config.incinerate_after = 30.days 35 + 36 + # Enable/disable encryption of console commands 37 + config.encrypt_session_data = true 38 + end 39 + RUBY 40 + 41 + say ' Creating AuditsAuthController...', :cyan 42 + file 'app/controllers/audits_auth_controller.rb', <<~RUBY 43 + # frozen_string_literal: true 44 + 45 + class AuditsAuthController < ApplicationController 46 + before_action :require_super_admin 47 + end 48 + RUBY 49 + 50 + say ' Creating Audits1984 initializer...', :cyan 51 + file 'config/initializers/audits1984.rb', <<~RUBY 52 + # frozen_string_literal: true 53 + 54 + Audits1984.auditor_class = 'User' 55 + Audits1984.base_controller_class = 'AuditsAuthController' 56 + RUBY 57 + 58 + say 'Console1984 + Audits1984 configured!', :green 59 + say ' - Console sessions are logged and encrypted', :cyan 60 + say ' - Access audit logs at /admin/console_audits', :cyan 61 + say ' - Only super_admins can review sessions', :cyan 62 + say ' Run migrations: rails db:migrate', :yellow
+75
database.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say 'Configuring PostgreSQL with multi-database setup...', :green 4 + 5 + say ' Creating database.yml...', :cyan 6 + file 'config/database.yml', <<~YAML, force: true 7 + # PostgreSQL for all environments (with Row Level Security support) 8 + # Ensure PostgreSQL is running locally for development 9 + default: &default 10 + adapter: postgresql 11 + encoding: unicode 12 + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 13 + prepared_statements: true 14 + advisory_locks: true 15 + url: <%= ENV['DATABASE_URL'] %> 16 + 17 + development: 18 + primary: &primary_development 19 + <<: *default 20 + database: #{app_name}_development 21 + queue: 22 + <<: *primary_development 23 + database: #{app_name}_queue_development 24 + migrations_paths: db/queue_migrate 25 + cache: 26 + <<: *primary_development 27 + database: #{app_name}_cache_development 28 + migrations_paths: db/cache_migrate 29 + cable: 30 + <<: *primary_development 31 + database: #{app_name}_cable_development 32 + migrations_paths: db/cable_migrate 33 + 34 + test: 35 + primary: &primary_test 36 + <<: *default 37 + database: #{app_name}_test 38 + queue: 39 + <<: *primary_test 40 + database: #{app_name}_queue_test 41 + migrations_paths: db/queue_migrate 42 + cache: 43 + <<: *primary_test 44 + database: #{app_name}_cache_test 45 + migrations_paths: db/cache_migrate 46 + cable: 47 + <<: *primary_test 48 + database: #{app_name}_cable_test 49 + migrations_paths: db/cable_migrate 50 + 51 + production: 52 + primary: &primary_production 53 + <<: *default 54 + database: #{app_name}_production 55 + username: #{app_name} 56 + password: <%= ENV["#{app_name.upcase}_DATABASE_PASSWORD"] %> 57 + queue: 58 + <<: *primary_production 59 + database: #{app_name}_queue_production 60 + migrations_paths: db/queue_migrate 61 + cache: 62 + <<: *primary_production 63 + database: #{app_name}_cache_production 64 + migrations_paths: db/cache_migrate 65 + cable: 66 + <<: *primary_production 67 + database: #{app_name}_cable_production 68 + migrations_paths: db/cable_migrate 69 + YAML 70 + 71 + say 'Database configuration complete!', :green 72 + say ' Primary database: #{app_name}_[env]', :cyan 73 + say ' Queue database: #{app_name}_queue_[env]', :cyan 74 + say ' Cache database: #{app_name}_cache_[env]', :cyan 75 + say ' Cable database: #{app_name}_cable_[env]', :cyan
+119
flipper.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say 'Setting up Flipper for feature flags...', :green 4 + 5 + gem 'flipper' 6 + gem 'flipper-active_record' 7 + gem 'flipper-ui' 8 + gem 'flipper-active_support_cache_store' 9 + 10 + after_bundle do 11 + say ' Running Flipper setup...', :cyan 12 + rails_command 'generate flipper:setup' 13 + end 14 + 15 + say ' Creating Flipper initializer...', :cyan 16 + file 'config/initializers/flipper.rb', <<~RUBY 17 + # frozen_string_literal: true 18 + 19 + require 'flipper' 20 + require 'flipper/adapters/active_record' 21 + require 'flipper/adapters/active_support_cache_store' 22 + 23 + Flipper.configure do |config| 24 + config.default do 25 + adapter = Flipper::Adapters::ActiveRecord.new 26 + cached_adapter = Flipper::Adapters::ActiveSupportCacheStore.new( 27 + adapter, 28 + Rails.cache, 29 + expires_in: 5.minutes 30 + ) 31 + Flipper.new(cached_adapter) 32 + end 33 + end 34 + 35 + # Register groups 36 + Flipper.register(:staff) do |actor, _context| 37 + actor.respond_to?(:admin_or_above?) && actor.admin_or_above? 38 + end 39 + 40 + Flipper.register(:admins) do |actor, _context| 41 + actor.respond_to?(:admin?) && actor.admin? 42 + end 43 + 44 + Flipper.register(:super_admins) do |actor, _context| 45 + actor.respond_to?(:super_admin?) && actor.super_admin? 46 + end 47 + 48 + # Configure Flipper UI 49 + Flipper::UI.configure do |config| 50 + config.application_breadcrumb_href = '/' 51 + config.feature_creation_enabled = true 52 + config.feature_removal_enabled = true 53 + 54 + if Rails.env.production? 55 + config.banner_text = 'Production Environment' 56 + config.banner_class = 'danger' 57 + elsif Rails.env.staging? 58 + config.banner_text = 'Staging Environment' 59 + config.banner_class = 'warning' 60 + end 61 + end 62 + RUBY 63 + 64 + say ' Creating Featureable concern...', :cyan 65 + file 'app/models/concerns/featureable.rb', <<~RUBY 66 + # frozen_string_literal: true 67 + 68 + # Include in models that can be used as Flipper actors 69 + # 70 + # Usage: 71 + # class User < ApplicationRecord 72 + # include Featureable 73 + # end 74 + # 75 + # Flipper.enable(:new_dashboard, user) 76 + # Flipper.enabled?(:new_dashboard, user) 77 + # 78 + module Featureable 79 + extend ActiveSupport::Concern 80 + 81 + # Uses public_id if available (e.g., "user_abc123"), otherwise falls back to "ClassName;id" 82 + def flipper_id 83 + respond_to?(:public_id) ? public_id : "\#{self.class.name};\#{id}" 84 + end 85 + end 86 + RUBY 87 + 88 + say ' Adding Featureable to User model...', :cyan 89 + inject_into_file 'app/models/user.rb', after: "class User < ApplicationRecord\n" do 90 + " include Featureable\n" 91 + end 92 + 93 + say ' Creating Feature helper...', :cyan 94 + file 'app/helpers/feature_helper.rb', <<~RUBY 95 + # frozen_string_literal: true 96 + 97 + module FeatureHelper 98 + # Check if a feature is enabled for the current user 99 + # 100 + # Usage in views: 101 + # <% if feature_enabled?(:new_dashboard) %> 102 + # <%= render 'new_dashboard' %> 103 + # <% end %> 104 + # 105 + def feature_enabled?(feature, actor = current_user) 106 + Flipper.enabled?(feature, actor) 107 + end 108 + end 109 + RUBY 110 + 111 + say ' Adding Feature helper to ApplicationController...', :cyan 112 + inject_into_class 'app/controllers/application_controller.rb', 'ApplicationController', <<~RUBY 113 + helper FeatureHelper 114 + RUBY 115 + 116 + say 'Flipper feature flags configured!', :green 117 + say ' Dashboard at /admin/flipper (super_admin only)', :cyan 118 + say ' Run migrations: rails db:migrate', :yellow 119 + say ' Usage: Flipper.enable(:feature), Flipper.enabled?(:feature, user)', :cyan
+61
friendly_id.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say 'Setting up FriendlyId for slugs and permalinks...', :green 4 + 5 + gem 'friendly_id' 6 + 7 + after_bundle do 8 + say ' Running FriendlyId generator...', :cyan 9 + rails_command 'generate friendly_id' 10 + end 11 + 12 + say ' Creating Sluggable concern...', :cyan 13 + file 'app/models/concerns/sluggable.rb', <<~RUBY 14 + # frozen_string_literal: true 15 + 16 + # Include this concern in models that need URL slugs 17 + # 18 + # Migration: 19 + # add_column :posts, :slug, :string 20 + # add_index :posts, :slug, unique: true 21 + # 22 + # Usage: 23 + # class Post < ApplicationRecord 24 + # include Sluggable 25 + # slugged_by :title 26 + # end 27 + # 28 + # post = Post.create(title: "Hello World") 29 + # post.slug # => "hello-world" 30 + # Post.friendly.find("hello-world") 31 + # 32 + # With history (redirects old slugs): 33 + # class Post < ApplicationRecord 34 + # include Sluggable 35 + # slugged_by :title, history: true 36 + # end 37 + # 38 + module Sluggable 39 + extend ActiveSupport::Concern 40 + 41 + included do 42 + extend FriendlyId 43 + end 44 + 45 + class_methods do 46 + def slugged_by(attribute, history: false, scope: nil) 47 + options = { use: [:slugged] } 48 + options[:use] << :history if history 49 + options[:use] << :scoped if scope 50 + options[:scope] = scope if scope 51 + 52 + friendly_id attribute, **options 53 + end 54 + end 55 + end 56 + RUBY 57 + 58 + say 'FriendlyId configured!', :green 59 + say ' Include `Sluggable` in models and call `slugged_by :attribute`', :cyan 60 + say ' Add slug column: add_column :table, :slug, :string', :cyan 61 + say ' Find records: Model.friendly.find(params[:id])', :cyan
+60
health_checks.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say 'Setting up OkComputer for health checks...', :green 4 + 5 + gem 'okcomputer' 6 + 7 + say ' Creating OkComputer initializer...', :cyan 8 + initializer 'okcomputer.rb', <<~RUBY 9 + # frozen_string_literal: true 10 + 11 + # OkComputer Health Checks 12 + # https://github.com/jphenow/okcomputer 13 + # 14 + # Custom check example: 15 + # class MyCustomCheck < OkComputer::Check 16 + # def check 17 + # if some_condition 18 + # mark_message "All good!" 19 + # else 20 + # mark_failure 21 + # mark_message "Something went wrong" 22 + # end 23 + # end 24 + # end 25 + 26 + OkComputer.mount_at = 'health' 27 + 28 + # Core checks 29 + OkComputer::Registry.register 'database', OkComputer::ActiveRecordCheck.new 30 + OkComputer::Registry.register 'cache', OkComputer::CacheCheck.new 31 + OkComputer::Registry.register 'app_version', OkComputer::AppVersionCheck.new 32 + OkComputer::Registry.register 'action_mailer', OkComputer::ActionMailerCheck.new 33 + 34 + # Redis check (if using Redis) 35 + if ENV['REDIS_URL'].present? 36 + OkComputer::Registry.register 'redis', OkComputer::RedisCheck.new(url: ENV['REDIS_URL']) 37 + end 38 + 39 + # Run checks in parallel 40 + OkComputer.check_in_parallel = true 41 + 42 + # Log when health checks are run 43 + OkComputer.logger = Rails.logger 44 + 45 + # Require authentication for detailed health info in production 46 + if Rails.env.production? 47 + OkComputer.require_authentication( 48 + ENV.fetch('HEALTH_CHECK_USER', 'health'), 49 + ENV.fetch('HEALTH_CHECK_PASSWORD', 'check'), 50 + except: %w[default] 51 + ) 52 + end 53 + RUBY 54 + 55 + say 'OkComputer health checks configured!', :green 56 + say ' Endpoints:', :cyan 57 + say ' GET /health - all checks', :cyan 58 + say ' GET /health/database - database check', :cyan 59 + say ' GET /health/cache - cache check', :cyan 60 + say ' GET /health/all - all checks as JSON', :cyan
+32
kaminari.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say 'Setting up Kaminari for pagination...', :green 4 + 5 + gem 'kaminari' 6 + 7 + after_bundle do 8 + say ' Running Kaminari config generator...', :cyan 9 + rails_command 'generate kaminari:config' 10 + end 11 + 12 + say ' Creating Kaminari initializer...', :cyan 13 + initializer 'kaminari.rb', <<~RUBY 14 + # frozen_string_literal: true 15 + 16 + Kaminari.configure do |config| 17 + config.default_per_page = 25 18 + config.max_per_page = 100 19 + config.window = 2 20 + config.outer_window = 1 21 + config.left = 0 22 + config.right = 0 23 + # config.page_method_name = :page 24 + # config.param_name = :page 25 + # config.max_pages = nil 26 + # config.params_on_first_page = false 27 + end 28 + RUBY 29 + 30 + say 'Kaminari pagination configured!', :green 31 + say ' Usage: @users = User.page(params[:page]).per(25)', :cyan 32 + say ' View helper: <%= paginate @users %>', :cyan
+36
mailkick.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say 'Setting up Mailkick for email unsubscribes...', :green 4 + 5 + gem 'mailkick' 6 + 7 + after_bundle do 8 + say ' Running Mailkick installer...', :cyan 9 + rails_command 'generate mailkick:install' 10 + 11 + say ' Running Mailkick views generator...', :cyan 12 + rails_command 'generate mailkick:views' 13 + end 14 + 15 + say ' Creating Mailkick initializer...', :cyan 16 + initializer 'mailkick.rb', <<~RUBY 17 + # frozen_string_literal: true 18 + 19 + # Mailkick - Email unsubscribe management 20 + # https://github.com/ankane/mailkick 21 + 22 + Mailkick.secret_token = Rails.application.credentials.dig(:mailkick, :secret_token) || 23 + Rails.application.secret_key_base 24 + 25 + # Optional: Use a custom method to check opt-outs 26 + # Mailkick.user_method = ->(email) { User.find_by(email: email) } 27 + RUBY 28 + 29 + say ' Adding Mailkick to User model...', :cyan 30 + inject_into_file 'app/models/user.rb', after: "class User < ApplicationRecord\n" do 31 + " has_subscriptions\n" 32 + end 33 + 34 + say 'Mailkick configured!', :green 35 + say ' Users can unsubscribe via: mailkick_unsubscribe_url(user)', :cyan 36 + say ' Check opt-out: user.opted_out_of_emails?', :cyan
+123
metrics.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say 'Setting up StatsD metrics...', :green 4 + 5 + gem 'statsd-instrument' 6 + 7 + say ' Creating StatsD initializer...', :cyan 8 + file 'config/initializers/statsd.rb', <<~RUBY 9 + # frozen_string_literal: true 10 + 11 + StatsD.backend = if Rails.env.production? 12 + StatsD::Instrument::Backends::UDPBackend.new( 13 + ENV.fetch('STATSD_ADDR', 'localhost:8125'), 14 + :datadog 15 + ) 16 + else 17 + StatsD::Instrument::Backends::LoggerBackend.new(Rails.logger) 18 + end 19 + 20 + StatsD.prefix = Rails.application.class.module_parent_name.underscore 21 + StatsD.default_tags = [ 22 + "env:\#{Rails.env}", 23 + "app:\#{Rails.application.class.module_parent_name.underscore}" 24 + ] 25 + RUBY 26 + 27 + say ' Creating Metrics module...', :cyan 28 + file 'app/services/metrics.rb', <<~RUBY 29 + # frozen_string_literal: true 30 + 31 + # Centralized metrics helper 32 + # 33 + # Usage: 34 + # Metrics.increment('user.signup') 35 + # Metrics.gauge('queue.size', queue.size) 36 + # Metrics.measure('api.request') { api_call } 37 + # Metrics.histogram('response.size', response.body.size) 38 + # 39 + module Metrics 40 + class << self 41 + # Count occurrences 42 + def increment(name, value = 1, tags: []) 43 + StatsD.increment(name, value, tags: tags) 44 + end 45 + 46 + # Track current value 47 + def gauge(name, value, tags: []) 48 + StatsD.gauge(name, value, tags: tags) 49 + end 50 + 51 + # Measure timing of a block 52 + def measure(name, tags: [], &block) 53 + StatsD.measure(name, tags: tags, &block) 54 + end 55 + 56 + # Track distribution of values 57 + def histogram(name, value, tags: []) 58 + StatsD.histogram(name, value, tags: tags) 59 + end 60 + 61 + # Track unique values 62 + def set(name, value, tags: []) 63 + StatsD.set(name, value, tags: tags) 64 + end 65 + 66 + # Time a block and record 67 + def time(name, tags: []) 68 + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) 69 + result = yield 70 + duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start 71 + histogram("\#{name}.duration", (duration * 1000).round, tags: tags) 72 + result 73 + end 74 + end 75 + end 76 + RUBY 77 + 78 + say ' Creating request metrics middleware...', :cyan 79 + file 'app/middleware/request_metrics.rb', <<~RUBY 80 + # frozen_string_literal: true 81 + 82 + class RequestMetrics 83 + def initialize(app) 84 + @app = app 85 + end 86 + 87 + def call(env) 88 + request = ActionDispatch::Request.new(env) 89 + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) 90 + 91 + status, headers, response = @app.call(env) 92 + 93 + duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start 94 + 95 + tags = [ 96 + "method:\#{request.request_method}", 97 + "status:\#{status}", 98 + "controller:\#{env['action_controller.instance']&.class&.name || 'unknown'}" 99 + ] 100 + 101 + StatsD.histogram('http.request.duration', (duration * 1000).round, tags: tags) 102 + StatsD.increment('http.request.count', tags: tags) 103 + 104 + [status, headers, response] 105 + rescue => e 106 + StatsD.increment('http.request.error', tags: ["error:\#{e.class.name}"]) 107 + raise 108 + end 109 + end 110 + RUBY 111 + 112 + say ' Adding middleware to application...', :cyan 113 + inject_into_file 'config/application.rb', after: "class Application < Rails::Application\n" do 114 + " config.middleware.use RequestMetrics\n" 115 + end 116 + 117 + say ' Adding to .env.development...', :cyan 118 + append_to_file '.env.development', "STATSD_ADDR=localhost:8125\n" 119 + 120 + say 'StatsD metrics configured!', :green 121 + say ' Use Metrics.increment, .gauge, .measure, .histogram', :cyan 122 + say ' Request metrics automatically tracked', :cyan 123 + say ' Set STATSD_ADDR in production', :yellow
+670
old/template.rb
··· 1 + # template.rb 2 + 3 + 4 + # Add gems 5 + gem "devise" # Authentication 6 + gem "pundit" # Authorization 7 + gem "pg", "~> 1.6.2" # PostgreSQL adapter 8 + gem "redis", "~> 5.0" # Redis client 9 + gem "redis-session-store" # Redis-backed session store 10 + gem "rack-attack" # Rate limiting and throttling 11 + 12 + ############################################################################### 13 + # SECURITY & ENCRYPTION 14 + ############################################################################### 15 + gem "lockbox" # Encryption 16 + gem "blind_index" # Encrypted searchable fields 17 + gem "invisible_captcha" # Spam protection 18 + 19 + ############################################################################### 20 + # AUDITING & VERSIONING 21 + ############################################################################### 22 + gem "paper_trail" # Model versioning 23 + gem "audits1984" # Audit logging 24 + gem "console1984" # Console access auditing 25 + gem "acts_as_paranoid" # Soft deletes 26 + 27 + ############################################################################### 28 + # SEARCH & INDEXING 29 + ############################################################################### 30 + gem "pg_search" # PostgreSQL full-text search 31 + gem "hashid-rails" # Obfuscate IDs 32 + gem "friendly_id" # Slugs and permalinks 33 + 34 + ############################################################################### 35 + # STATE MACHINES 36 + ############################################################################### 37 + gem "aasm" # State machines 38 + 39 + ############################################################################### 40 + # ANALYTICS & MONITORING 41 + ############################################################################### 42 + gem "okcomputer" # Health checks 43 + gem "ahoy_matey" # Analytics 44 + gem "ahoy_email" # Email analytics 45 + gem "blazer" # BI dashboard 46 + gem "statsd-instrument" # StatsD metrics 47 + gem "rails_performance" # Performance monitoring 48 + 49 + ############################################################################### 50 + # EMAIL 51 + ############################################################################### 52 + gem "premailer-rails" # Inline CSS for emails 53 + gem "email_reply_parser" # Parse email replies 54 + gem "mailkick" # Email unsubscribe management 55 + 56 + ############################################################################### 57 + # BACKGROUND JOBS 58 + ############################################################################### 59 + gem "mission_control-jobs" # Job dashboard 60 + 61 + ############################################################################### 62 + # UTILITIES 63 + ############################################################################### 64 + gem "browser" # Browser detection 65 + gem "strong_migrations" # Safe migrations 66 + 67 + ############################################################################### 68 + # UI & FRONTEND 69 + ############################################################################### 70 + gem "tailwindcss-rails" # Tailwind CSS 71 + 72 + 73 + ############################################################################### 74 + # FEATURE FLAGS & CONFIGURATION 75 + ############################################################################### 76 + gem "flipper" # Feature flags 77 + gem "flipper-active_record" # ActiveRecord adapter for Flipper 78 + gem "flipper-ui" # UI for Flipper 79 + gem "flipper-active_support_cache_store" 80 + 81 + ############################################################################### 82 + # ENVIRONMENT VARIABLES 83 + ############################################################################### 84 + gem "dotenv-rails" # Environment variables 85 + 86 + gem_group :development, :test do 87 + gem "rspec-rails", "~> 7.1" # Testing framework 88 + gem "factory_bot_rails" # Test data factories 89 + gem "faker" # Fake data generation 90 + gem "shoulda-matchers" # RSpec matchers 91 + gem "rubocop-capybara", "~> 2.22", ">= 2.22.1" 92 + gem "rubocop-rspec", "~> 3.6" 93 + gem "rubocop-rspec_rails", "~> 2.31" 94 + gem "relaxed-rubocop" 95 + gem "query_count" # SQL query counter 96 + gem "bullet" # N+1 query detection 97 + end 98 + 99 + gem_group :development do 100 + gem "actual_db_schema" # Rolls back phantom migrations 101 + gem "annotaterb" # Annotate models 102 + gem "listen", "~> 3.9" # File watcher 103 + gem "letter_opener_web" # Preview emails 104 + gem "foreman" # Process manager 105 + gem "awesome_print" # Pretty print objects 106 + gem "rack-mini-profiler", "~> 3.3", require: false # Performance profiling 107 + gem "stackprof" # Used by rack-mini-profiler for flamegraphs 108 + end 109 + 110 + # Configure database to use PostgreSQL 111 + remove_file "config/database.yml" 112 + create_file "config/database.yml", <<~YAML 113 + default: &default 114 + adapter: postgresql 115 + encoding: unicode 116 + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 117 + 118 + development: 119 + <<: *default 120 + database: #{app_name}_development 121 + 122 + test: 123 + <<: *default 124 + database: #{app_name}_test 125 + 126 + production: 127 + <<: *default 128 + database: #{app_name}_production 129 + username: #{app_name} 130 + password: <%= ENV["#{app_name.upcase}_DATABASE_PASSWORD"] %> 131 + YAML 132 + 133 + after_bundle do 134 + # Generate Devise 135 + generate "devise:install" 136 + generate "devise", "User" 137 + 138 + # Add fields to User 139 + generate "migration", "AddFieldsToUsers first_name:string last_name:string access_level:integer" 140 + 141 + # Update the migration to add default and null constraint for access_level 142 + in_root do 143 + migration_file = Dir.glob("db/migrate/*_add_fields_to_users.rb").first 144 + if migration_file 145 + # Replace the access_level line with one that has default and null constraint 146 + gsub_file migration_file, 147 + /add_column :users, :access_level, :integer$/, 148 + "add_column :users, :access_level, :integer, default: 0, null: false" 149 + end 150 + end 151 + 152 + # Generate Lockbox master key using Lockbox.generate_key 153 + # This needs to run after bundle install, so Lockbox is available 154 + lockbox_key = `rails runner "require 'lockbox'; puts Lockbox.generate_key"`.strip 155 + 156 + # Add lockbox key to credentials programmatically 157 + # Create a Ruby script that directly writes to credentials 158 + create_file 'tmp/add_lockbox_to_credentials.rb', <<~RUBY, force: true 159 + require 'active_support/encrypted_configuration' 160 + require 'yaml' 161 + 162 + # Path to credentials files 163 + credentials_path = Rails.root.join('config/credentials.yml.enc') 164 + key_path = Rails.root.join('config/master.key') 165 + 166 + # Read the key 167 + key = File.read(key_path).strip 168 + 169 + # Create encrypted configuration instance 170 + credentials = ActiveSupport::EncryptedConfiguration.new( 171 + config_path: credentials_path, 172 + key_path: key_path, 173 + env_key: 'RAILS_MASTER_KEY', 174 + raise_if_missing_key: true 175 + ) 176 + 177 + # Read existing credentials 178 + current_config = credentials.config 179 + 180 + # Parse as YAML if it's a string, and ensure we have a hash with string keys 181 + if current_config.is_a?(String) 182 + current_config = YAML.safe_load(current_config, permitted_classes: [Symbol]) || {} 183 + end 184 + 185 + # Convert all keys to strings recursively 186 + current_config = current_config.deep_stringify_keys if current_config.respond_to?(:deep_stringify_keys) 187 + 188 + # Add lockbox config if not present 189 + unless current_config.key?('lockbox') 190 + current_config['lockbox'] = { 'master_key' => '#{lockbox_key}' } 191 + 192 + # Write back to credentials - ensure clean YAML format 193 + yaml_content = current_config.to_yaml 194 + # Remove the YAML document separator for cleaner output 195 + yaml_content = yaml_content.sub(/^---\\n/, '') 196 + # Add blank line before lockbox section for better readability 197 + yaml_content = yaml_content.sub(/^lockbox:/, "\\nlockbox:") 198 + 199 + credentials.write(yaml_content) 200 + puts "✓ Added lockbox master_key to credentials" 201 + else 202 + puts "⚠ Lockbox config already exists in credentials" 203 + end 204 + RUBY 205 + 206 + rails_command "runner tmp/add_lockbox_to_credentials.rb" 207 + remove_file 'tmp/add_lockbox_to_credentials.rb' 208 + 209 + initializer 'lockbox.rb', <<~RUBY 210 + # Set Lockbox master key from credentials 211 + if Rails.application.credentials.lockbox&.key?(:master_key) 212 + Lockbox.master_key = Rails.application.credentials.lockbox[:master_key] 213 + else 214 + Rails.logger.warn "Lockbox master_key not found in credentials. Please add it by running: rails credentials:edit" 215 + end 216 + RUBY 217 + 218 + initializer 'okcomputer.rb', <<~RUBY 219 + # frozen_string_literal: true 220 + 221 + # https://github.com/jphenow/okcomputer#registering-additional-checks 222 + # 223 + # class MyCustomCheck < OKComputer::Check 224 + # def call 225 + # if rand(10).even? 226 + # "Even is great!" 227 + # else 228 + # mark_failure 229 + # "We don't like odd numbers" 230 + # end 231 + # end 232 + # end 233 + 234 + OkComputer::Registry.register "database", OkComputer::ActiveRecordCheck.new 235 + OkComputer::Registry.register "cache", OkComputer::CacheCheck.new 236 + 237 + OkComputer::Registry.register "app_version", OkComputer::AppVersionCheck.new 238 + OkComputer::Registry.register "action_mailer", OkComputer::ActionMailerCheck.new 239 + 240 + # Run checks in parallel 241 + OkComputer.check_in_parallel = true 242 + 243 + # Log when health checks are run 244 + OkComputer.logger = Rails.logger 245 + RUBY 246 + 247 + generate "rails_performance:install" 248 + 249 + initializer 'rails_performance.rb', <<~RUBY 250 + RailsPerformance.setup do |config| 251 + config.redis = Redis.new(url: ENV["REDIS_URL"].presence || "redis://127.0.0.1:6379/0") 252 + config.duration = 4.hours 253 + 254 + config.enabled = true 255 + 256 + # protect with authentication 257 + config.verify_access_proc = proc { |controller| 258 + controller.current_user&.admin_access? 259 + } 260 + 261 + # Ignore admin and performance paths 262 + config.ignored_paths = ['/admin', '/rails/performance'] 263 + 264 + config.home_link = '/' 265 + config.skipable_rake_tasks = ['webpacker:compile'] 266 + end if defined?(RailsPerformance) 267 + RUBY 268 + 269 + initializer 'session_store.rb', <<~RUBY 270 + # Use Redis for session storage 271 + Rails.application.config.session_store :redis_store, 272 + servers: ENV.fetch("REDIS_URL") { "redis://localhost:6379/2/session" }, 273 + expire_after: 90.minutes, 274 + key: "_#{Rails.application.class.module_parent_name.underscore}_session", 275 + threadsafe: true, 276 + signed: true 277 + RUBY 278 + 279 + initializer 'rack_attack.rb', <<~RUBY 280 + # Configure Rack Attack for rate limiting 281 + class Rack::Attack 282 + # Use Redis for Rack Attack storage 283 + Rack::Attack.cache.store = ActiveSupport::Cache::RedisCacheStore.new( 284 + url: ENV.fetch("REDIS_URL") { "redis://localhost:6379/5" } 285 + ) 286 + 287 + # Throttle all requests by IP (300 req/5 minutes) 288 + throttle('req/ip', limit: 300, period: 5.minutes) do |req| 289 + req.ip unless req.path.start_with?('/assets') 290 + end 291 + 292 + # Throttle login attempts by email 293 + throttle('logins/email', limit: 5, period: 20.seconds) do |req| 294 + if req.path == '/users/sign_in' && req.post? 295 + req.params['user']&.dig('email')&.to_s&.downcase&.gsub(/\\s+/, "") 296 + end 297 + end 298 + 299 + # Throttle password reset attempts 300 + throttle('password_resets/email', limit: 3, period: 20.minutes) do |req| 301 + if req.path == '/users/password' && req.post? 302 + req.params['user']&.dig('email')&.to_s&.downcase&.gsub(/\\s+/, "") 303 + end 304 + end 305 + 306 + # Block suspicious requests 307 + blocklist('block suspicious requests') do |req| 308 + # Block requests with suspicious patterns 309 + Rack::Attack::Fail2Ban.filter("pentesters-\#{req.ip}", maxretry: 5, findtime: 10.minutes, bantime: 1.hour) do 310 + # Return true if this is a suspicious request 311 + CGI.unescape(req.query_string) =~ %r{/etc/passwd} || 312 + req.path.include?('/etc/passwd') || 313 + req.path.include?('wp-admin') || 314 + req.path.include?('wp-login') 315 + end 316 + end 317 + 318 + # Always allow requests from localhost in development 319 + safelist('allow from localhost') do |req| 320 + req.ip == '127.0.0.1' || req.ip == '::1' if Rails.env.development? 321 + end 322 + end 323 + RUBY 324 + 325 + # Configure cache store to use Redis in production 326 + inject_into_file 'config/environments/production.rb', after: "Rails.application.configure do\n" do 327 + <<~RUBY 328 + # Use Redis for caching 329 + config.cache_store = :redis_cache_store, { url: ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } } 330 + 331 + # Enable Rack Attack middleware 332 + config.middleware.use Rack::Attack 333 + 334 + RUBY 335 + end 336 + 337 + # Configure cache store to use Redis in development 338 + inject_into_file 'config/environments/development.rb', after: "Rails.application.configure do\n" do 339 + <<~RUBY 340 + # Use Redis for caching (shared across processes) 341 + config.cache_store = :redis_cache_store, { url: ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } } 342 + 343 + # Enable Rack Attack middleware (useful for testing rate limits) 344 + config.middleware.use Rack::Attack 345 + 346 + RUBY 347 + end 348 + 349 + inject_into_file 'app/models/application_record.rb', 350 + " include PgSearch::Model\n", 351 + after: "primary_abstract_class\n" 352 + 353 + inject_into_file 'app/models/application_record.rb', 354 + " has_paper_trail\n", 355 + after: "include PgSearch::Model\n" 356 + 357 + generate "devise:views" 358 + generate "pg_search:migration:multisearch" 359 + generate "lockbox:audits" 360 + generate "pundit:install" 361 + generate "ahoy:install" 362 + generate "ahoy:messages --encryption=lockbox" 363 + generate "ahoy:clicks" 364 + generate "annotate_rb:install" 365 + generate "blazer:install" 366 + generate "flipper:setup" 367 + generate "bullet:install" 368 + generate "strong_migrations:install" 369 + generate "solid_queue:install" 370 + generate "paper_trail:install" 371 + generate "mailkick:install" 372 + generate "mailkick:views" 373 + 374 + # Set up RSpec 375 + generate "rspec:install" 376 + 377 + # Create and run migrations 378 + rails_command "db:create" 379 + rails_command "db:migrate" 380 + 381 + # Create a custom controller 382 + # generate :controller, "pages", "home" 383 + # route "root to: 'pages#home'" 384 + 385 + # Add enum and has_subscriptions to User model 386 + inject_into_file 'app/models/user.rb', after: "class User < ApplicationRecord\n" do 387 + <<~RUBY 388 + enum :access_level, { 389 + user: 0, 390 + admin: 1, 391 + super_admin: 2, 392 + owner: 3 393 + }, default: :user, null: false 394 + 395 + has_subscriptions 396 + 397 + # Helper method to check if user has any admin access 398 + def admin_access? 399 + admin? || super_admin? || owner? 400 + end 401 + 402 + # Return full name 403 + def full_name 404 + "\#{first_name} \#{last_name}".strip 405 + end 406 + 407 + RUBY 408 + end 409 + 410 + file 'app/controllers/admin/application_controller.rb', <<~RUBY 411 + module Admin 412 + class ApplicationController < ::ApplicationController 413 + include Pundit 414 + rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized 415 + before_action :require_admin 416 + 417 + # Shared admin logic here 418 + def index 419 + @current_user = current_user 420 + end 421 + 422 + private 423 + 424 + def require_admin 425 + unless current_user&.admin? || current_user&.super_admin? || current_user&.owner? 426 + redirect_to root_path, alert: "You are not authorized to access this area." 427 + end 428 + end 429 + 430 + def user_not_authorized 431 + flash[:alert] = "You are not authorized to perform this action." 432 + redirect_to(request.referrer || root_path) 433 + end 434 + end 435 + end 436 + RUBY 437 + 438 + # Create Admin index view 439 + file 'app/views/admin/application/index.html.erb', <<~HTML 440 + <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> 441 + <div class="mb-8"> 442 + <h1 class="text-3xl font-bold text-gray-900">Admin Dashboard</h1> 443 + <p class="mt-2 text-sm text-gray-600"> 444 + Welcome back, <%= @current_user.full_name %> 445 + </p> 446 + </div> 447 + 448 + <div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3"> 449 + <!-- Blazer Card --> 450 + <div class="bg-white overflow-hidden shadow rounded-lg hover:shadow-lg transition-shadow"> 451 + <div class="p-6"> 452 + <div class="flex items-center"> 453 + <div class="flex-shrink-0 bg-blue-500 rounded-md p-3"> 454 + <svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 455 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /> 456 + </svg> 457 + </div> 458 + <div class="ml-5 w-0 flex-1"> 459 + <dl> 460 + <dt class="text-sm font-medium text-gray-500 truncate">Blazer</dt> 461 + <dd class="mt-1 text-sm text-gray-900">Business Intelligence & Analytics</dd> 462 + </dl> 463 + </div> 464 + </div> 465 + <div class="mt-4"> 466 + <%= link_to "Open Blazer", "/admin/blazer", class: "text-blue-600 hover:text-blue-800 text-sm font-medium" %> 467 + </div> 468 + </div> 469 + </div> 470 + 471 + <% if @current_user.super_admin? || @current_user.owner? %> 472 + <!-- Flipper Card --> 473 + <div class="bg-white overflow-hidden shadow rounded-lg hover:shadow-lg transition-shadow"> 474 + <div class="p-6"> 475 + <div class="flex items-center"> 476 + <div class="flex-shrink-0 bg-purple-500 rounded-md p-3"> 477 + <svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 478 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" /> 479 + </svg> 480 + </div> 481 + <div class="ml-5 w-0 flex-1"> 482 + <dl> 483 + <dt class="text-sm font-medium text-gray-500 truncate">Flipper</dt> 484 + <dd class="mt-1 text-sm text-gray-900">Feature Flags Management</dd> 485 + </dl> 486 + </div> 487 + </div> 488 + <div class="mt-4"> 489 + <%= link_to "Manage Features", "/admin/flipper", class: "text-purple-600 hover:text-purple-800 text-sm font-medium" %> 490 + </div> 491 + </div> 492 + </div> 493 + <% end %> 494 + 495 + <!-- Performance Card --> 496 + <div class="bg-white overflow-hidden shadow rounded-lg hover:shadow-lg transition-shadow"> 497 + <div class="p-6"> 498 + <div class="flex items-center"> 499 + <div class="flex-shrink-0 bg-green-500 rounded-md p-3"> 500 + <svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 501 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /> 502 + </svg> 503 + </div> 504 + <div class="ml-5 w-0 flex-1"> 505 + <dl> 506 + <dt class="text-sm font-medium text-gray-500 truncate">Performance</dt> 507 + <dd class="mt-1 text-sm text-gray-900">Monitor Application Performance</dd> 508 + </dl> 509 + </div> 510 + </div> 511 + <div class="mt-4"> 512 + <%= link_to "View Performance", "/admin/performance", class: "text-green-600 hover:text-green-800 text-sm font-medium" %> 513 + </div> 514 + </div> 515 + </div> 516 + 517 + <!-- Users Card --> 518 + <div class="bg-white overflow-hidden shadow rounded-lg hover:shadow-lg transition-shadow"> 519 + <div class="p-6"> 520 + <div class="flex items-center"> 521 + <div class="flex-shrink-0 bg-yellow-500 rounded-md p-3"> 522 + <svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 523 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /> 524 + </svg> 525 + </div> 526 + <div class="ml-5 w-0 flex-1"> 527 + <dl> 528 + <dt class="text-sm font-medium text-gray-500 truncate">Users</dt> 529 + <dd class="mt-1 text-sm text-gray-900">Manage User Accounts</dd> 530 + </dl> 531 + </div> 532 + </div> 533 + <div class="mt-4"> 534 + <%= link_to "Manage Users", admin_users_path, class: "text-yellow-600 hover:text-yellow-800 text-sm font-medium" %> 535 + </div> 536 + </div> 537 + </div> 538 + 539 + <!-- Health Checks Card --> 540 + <div class="bg-white overflow-hidden shadow rounded-lg hover:shadow-lg transition-shadow"> 541 + <div class="p-6"> 542 + <div class="flex items-center"> 543 + <div class="flex-shrink-0 bg-red-500 rounded-md p-3"> 544 + <svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 545 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" /> 546 + </svg> 547 + </div> 548 + <div class="ml-5 w-0 flex-1"> 549 + <dl> 550 + <dt class="text-sm font-medium text-gray-500 truncate">Health Checks</dt> 551 + <dd class="mt-1 text-sm text-gray-900">System Health Monitoring</dd> 552 + </dl> 553 + </div> 554 + </div> 555 + <div class="mt-4"> 556 + <%= link_to "View Health", "/healthchecks", class: "text-red-600 hover:text-red-800 text-sm font-medium" %> 557 + </div> 558 + </div> 559 + </div> 560 + 561 + <!-- Mission Control Card --> 562 + <div class="bg-white overflow-hidden shadow rounded-lg hover:shadow-lg transition-shadow"> 563 + <div class="p-6"> 564 + <div class="flex items-center"> 565 + <div class="flex-shrink-0 bg-indigo-500 rounded-md p-3"> 566 + <svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 567 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /> 568 + </svg> 569 + </div> 570 + <div class="ml-5 w-0 flex-1"> 571 + <dl> 572 + <dt class="text-sm font-medium text-gray-500 truncate">Background Jobs</dt> 573 + <dd class="mt-1 text-sm text-gray-900">Monitor Background Jobs</dd> 574 + </dl> 575 + </div> 576 + </div> 577 + <div class="mt-4"> 578 + <%= link_to "View Jobs", "/jobs", class: "text-indigo-600 hover:text-indigo-800 text-sm font-medium" %> 579 + </div> 580 + </div> 581 + </div> 582 + </div> 583 + </div> 584 + HTML 585 + 586 + # Create Admin Policy 587 + file 'app/policies/admin_policy.rb', <<~RUBY 588 + class AdminPolicy < ApplicationPolicy 589 + def blazer? 590 + user&.admin? || user&.super_admin? || user&.owner? 591 + end 592 + 593 + def flipper? 594 + user&.super_admin? || user&.owner? 595 + end 596 + 597 + def access_admin_endpoints? 598 + user&.admin? || user&.super_admin? || user&.owner? 599 + end 600 + end 601 + RUBY 602 + 603 + 604 + # Configure admin routes 605 + route <<~RUBY 606 + namespace :admin do 607 + root to: "application#index" 608 + 609 + mount Blazer::Engine, at: "blazer", constraints: ->(request) { 610 + user = User.find_by(id: request.session[:user_id]) 611 + user && AdminPolicy.new(user, :admin).blazer? 612 + } 613 + 614 + mount Flipper::UI.app(Flipper), at: "flipper", constraints: ->(request) { 615 + user = User.find_by(id: request.session[:user_id]) 616 + user && AdminPolicy.new(user, :admin).flipper? 617 + } 618 + 619 + mount RailsPerformance::Engine, at: "performance", constraints: ->(request) { 620 + user = User.find_by(id: request.session[:user_id]) 621 + user && AdminPolicy.new(user, :admin).access_admin_endpoints? 622 + } 623 + 624 + resources :users, shallow: true 625 + end 626 + RUBY 627 + 628 + # Mount OkComputer health checks 629 + route <<~RUBY 630 + mount OkComputer::Engine, at: "/healthchecks" 631 + RUBY 632 + 633 + inject_into_file 'app/mailers/application_mailer.rb', 634 + " has_history\nutm_params\n", 635 + after: "class ApplicationMailer < ActionMailer::Base\n" 636 + 637 + inject_into_file 'app/controllers/application_controller.rb', 638 + " include Pundit::Authorization\n before_action :set_paper_trail_whodunnit\n", 639 + after: "class ApplicationController < ActionController::Base\n" 640 + 641 + # Create GitHub workflows 642 + empty_directory '.github/workflows' 643 + 644 + file '.github/workflows/check-indexes.yml', <<~YAML 645 + name: Check Indexes 646 + on: 647 + pull_request: 648 + paths: 649 + - 'db/migrate/**.rb' 650 + 651 + jobs: 652 + check-indexes: 653 + runs-on: ubuntu-latest 654 + steps: 655 + - uses: actions/checkout@v4 656 + with: 657 + fetch-depth: 0 658 + 659 + - name: Check Migration Indexes 660 + uses: speedshop/ids_must_be_indexed@v1.2.1 661 + YAML 662 + 663 + # Run migrations one final time to catch any remaining pending migrations 664 + rails_command "db:migrate" 665 + 666 + # Git initialization 667 + git :init 668 + git add: "." 669 + git commit: "-m 'Initial commit (from @jaspermayone/rails-template)'" 670 + end
+67
paper_trail.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say 'Setting up Paper Trail for audit logging...', :green 4 + 5 + gem 'paper_trail' 6 + 7 + say ' Creating initializer...', :cyan 8 + file 'config/initializers/paper_trail.rb', <<~RUBY 9 + PaperTrail.config.enabled = true 10 + PaperTrail.config.has_paper_trail_defaults = { 11 + on: %i[create update destroy] 12 + } 13 + PaperTrail.config.version_limit = nil 14 + RUBY 15 + 16 + say ' Creating Auditable concern...', :cyan 17 + file 'app/models/concerns/auditable.rb', <<~RUBY 18 + # frozen_string_literal: true 19 + 20 + module Auditable 21 + extend ActiveSupport::Concern 22 + 23 + included do 24 + has_paper_trail 25 + end 26 + 27 + def audit_trail 28 + versions.order(created_at: :desc) 29 + end 30 + 31 + def last_modified_by 32 + versions.last&.whodunnit 33 + end 34 + end 35 + RUBY 36 + 37 + say ' Setting up whodunnit tracking...', :cyan 38 + file 'app/controllers/concerns/set_paper_trail_whodunnit.rb', <<~RUBY 39 + # frozen_string_literal: true 40 + 41 + module SetPaperTrailWhodunnit 42 + extend ActiveSupport::Concern 43 + 44 + included do 45 + before_action :set_paper_trail_whodunnit 46 + end 47 + 48 + private 49 + 50 + def user_for_paper_trail 51 + current_user&.id&.to_s || 'system' 52 + end 53 + end 54 + RUBY 55 + 56 + say ' Adding to ApplicationController...', :cyan 57 + inject_into_class 'app/controllers/application_controller.rb', 'ApplicationController', <<~RUBY 58 + include SetPaperTrailWhodunnit 59 + RUBY 60 + 61 + after_bundle do 62 + say ' Running Paper Trail install generator...', :cyan 63 + rails_command 'generate paper_trail:install' 64 + end 65 + 66 + say 'Paper Trail audit logging configured!', :green 67 + say ' Include `Auditable` in models you want to track', :cyan
+138
pg_search.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say 'Setting up pg_search for PostgreSQL full-text search...', :green 4 + 5 + gem 'pg_search' 6 + 7 + after_bundle do 8 + say ' Running pg_search multisearch migration generator...', :cyan 9 + rails_command 'generate pg_search:migration:multisearch' 10 + end 11 + 12 + say ' Creating Searchable concern...', :cyan 13 + file 'app/models/concerns/searchable.rb', <<~RUBY 14 + # frozen_string_literal: true 15 + 16 + # Include this concern in models that need full-text search 17 + # 18 + # Usage: 19 + # class Post < ApplicationRecord 20 + # include Searchable 21 + # searchable_by :title, :body 22 + # end 23 + # 24 + # Post.search("hello world") 25 + # 26 + # Advanced usage with weights: 27 + # class Post < ApplicationRecord 28 + # include Searchable 29 + # searchable_by title: 'A', body: 'B', author_name: 'C' 30 + # end 31 + # 32 + # With associations: 33 + # class Post < ApplicationRecord 34 + # include Searchable 35 + # searchable_by :title, :body, associated: { comments: :content } 36 + # end 37 + # 38 + module Searchable 39 + extend ActiveSupport::Concern 40 + 41 + included do 42 + include PgSearch::Model 43 + end 44 + 45 + class_methods do 46 + def searchable_by(*columns, associated: nil, **weighted_columns) 47 + tsearch_options = { 48 + prefix: true, 49 + dictionary: 'english', 50 + tsvector_column: 'searchable' 51 + } 52 + 53 + against = if weighted_columns.any? 54 + weighted_columns.transform_values { |weight| weight.to_sym } 55 + else 56 + columns 57 + end 58 + 59 + search_config = { 60 + against: against, 61 + using: { 62 + tsearch: tsearch_options 63 + } 64 + } 65 + 66 + if associated 67 + search_config[:associated_against] = associated 68 + end 69 + 70 + pg_search_scope :search, **search_config 71 + 72 + # Also add a ranked search that includes the rank 73 + pg_search_scope :search_with_rank, **search_config.merge(ranked_by: ':tsearch') 74 + end 75 + end 76 + end 77 + RUBY 78 + 79 + say ' Creating multisearch initializer...', :cyan 80 + file 'config/initializers/pg_search.rb', <<~RUBY 81 + # frozen_string_literal: true 82 + 83 + PgSearch.multisearch_options = { 84 + using: { 85 + tsearch: { 86 + prefix: true, 87 + dictionary: 'english' 88 + } 89 + } 90 + } 91 + RUBY 92 + 93 + say ' Creating search generator...', :cyan 94 + file 'lib/generators/search_index/search_index_generator.rb', <<~RUBY 95 + # frozen_string_literal: true 96 + 97 + class SearchIndexGenerator < Rails::Generators::NamedBase 98 + include Rails::Generators::Migration 99 + 100 + source_root File.expand_path('templates', __dir__) 101 + 102 + def self.next_migration_number(_dirname) 103 + Time.now.utc.strftime('%Y%m%d%H%M%S') 104 + end 105 + 106 + def create_migration_file 107 + migration_template 'migration.rb.erb', "db/migrate/add_search_index_to_\#{table_name}.rb" 108 + end 109 + 110 + private 111 + 112 + def table_name 113 + file_name.tableize 114 + end 115 + end 116 + RUBY 117 + 118 + file 'lib/generators/search_index/templates/migration.rb.erb', <<~ERB 119 + class AddSearchIndexTo<%= table_name.camelize %> < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>] 120 + def change 121 + add_column :<%= table_name %>, :searchable, :tsvector 122 + add_index :<%= table_name %>, :searchable, using: :gin 123 + 124 + # Uncomment and customize the trigger for automatic updates: 125 + # execute <<-SQL 126 + # CREATE TRIGGER <%= table_name %>_searchable_update 127 + # BEFORE INSERT OR UPDATE ON <%= table_name %> 128 + # FOR EACH ROW EXECUTE FUNCTION 129 + # tsvector_update_trigger(searchable, 'pg_catalog.english', title, body); 130 + # SQL 131 + end 132 + end 133 + ERB 134 + 135 + say 'pg_search configured!', :green 136 + say ' Include `Searchable` in models and call `searchable_by :columns`', :cyan 137 + say ' Generate index: rails g search_index ModelName', :cyan 138 + say ' Search: Model.search("query")', :cyan
+73
public_identifiable.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say '🔑 Setting up public IDs (hashid-rails)...', :green 4 + 5 + gem 'hashid-rails' 6 + 7 + file 'app/models/concerns/public_identifiable.rb', <<~RUBY 8 + # frozen_string_literal: true 9 + 10 + module PublicIdentifiable 11 + SEPARATOR = '_' 12 + 13 + extend ActiveSupport::Concern 14 + 15 + included do 16 + include Hashid::Rails 17 + class_attribute :public_id_prefix 18 + end 19 + 20 + def public_id 21 + "\#{self.class.get_public_id_prefix}\#{SEPARATOR}\#{hashid}" 22 + end 23 + 24 + module ClassMethods 25 + def set_public_id_prefix(prefix) 26 + self.public_id_prefix = prefix.to_s.downcase 27 + end 28 + 29 + def find_by_public_id(id) 30 + return nil unless id.is_a?(String) && id.include?(SEPARATOR) 31 + 32 + prefix, hash = id.split(SEPARATOR, 2) 33 + return nil unless prefix.downcase == get_public_id_prefix 34 + 35 + find_by_hashid(hash) 36 + end 37 + 38 + def find_by_public_id!(id) 39 + obj = find_by_public_id(id) 40 + raise ActiveRecord::RecordNotFound.new(nil, name) if obj.nil? 41 + 42 + obj 43 + end 44 + 45 + def get_public_id_prefix 46 + return @_public_id_prefix if defined?(@_public_id_prefix) 47 + 48 + if public_id_prefix.present? 49 + @_public_id_prefix = public_id_prefix.downcase 50 + else 51 + raise NotImplementedError, "The \#{name} model includes PublicIdentifiable but set_public_id_prefix hasn't been called." 52 + end 53 + end 54 + end 55 + end 56 + RUBY 57 + 58 + file 'config/initializers/hashid.rb', <<~RUBY 59 + Hashid::Rails.configure do |config| 60 + # Salt from credentials (generate with: SecureRandom.hex(32)) 61 + config.salt = Rails.application.credentials.dig(:hashid, :salt) || Rails.application.secret_key_base 62 + 63 + # Minimum length of the hash (default: 6) 64 + config.min_hash_length = 6 65 + 66 + # Custom alphabet (URL-safe, lowercase for consistency) 67 + config.alphabet = "abcdefghijklmnopqrstuvwxyz0123456789" 68 + end 69 + RUBY 70 + 71 + append_to_file '.env.development', "HASHID_SALT=development_salt_change_in_production\n" 72 + 73 + say '✅ Public IDs configured!', :green
+193
pundit.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say 'Setting up Pundit for authorization...', :green 4 + 5 + gem 'pundit' 6 + 7 + after_bundle do 8 + say ' Running Pundit installer...', :cyan 9 + rails_command 'generate pundit:install' 10 + end 11 + 12 + say ' Creating ApplicationPolicy...', :cyan 13 + file 'app/policies/application_policy.rb', <<~RUBY 14 + # frozen_string_literal: true 15 + 16 + class ApplicationPolicy 17 + attr_reader :user, :record 18 + 19 + def initialize(user, record) 20 + @user = user 21 + @record = record 22 + end 23 + 24 + def index? 25 + false 26 + end 27 + 28 + def show? 29 + false 30 + end 31 + 32 + def create? 33 + false 34 + end 35 + 36 + def new? 37 + create? 38 + end 39 + 40 + def update? 41 + false 42 + end 43 + 44 + def edit? 45 + update? 46 + end 47 + 48 + def destroy? 49 + false 50 + end 51 + 52 + class Scope 53 + def initialize(user, scope) 54 + @user = user 55 + @scope = scope 56 + end 57 + 58 + def resolve 59 + raise NoMethodError, "You must define #resolve in \#{self.class}" 60 + end 61 + 62 + private 63 + 64 + attr_reader :user, :scope 65 + end 66 + end 67 + RUBY 68 + 69 + say ' Creating example UserPolicy...', :cyan 70 + file 'app/policies/user_policy.rb', <<~RUBY 71 + # frozen_string_literal: true 72 + 73 + class UserPolicy < ApplicationPolicy 74 + def show? 75 + user == record 76 + end 77 + 78 + def update? 79 + user == record 80 + end 81 + 82 + def destroy? 83 + user == record 84 + end 85 + 86 + class Scope < ApplicationPolicy::Scope 87 + def resolve 88 + scope.where(id: user.id) 89 + end 90 + end 91 + end 92 + RUBY 93 + 94 + say ' Creating AdminPolicy...', :cyan 95 + file 'app/policies/admin_policy.rb', <<~RUBY 96 + # frozen_string_literal: true 97 + 98 + class AdminPolicy < ApplicationPolicy 99 + def blazer? 100 + user&.admin_or_above? 101 + end 102 + 103 + def flipper? 104 + user&.super_admin_or_above? 105 + end 106 + 107 + def rails_performance? 108 + user&.admin_or_above? 109 + end 110 + 111 + def jobs? 112 + user&.admin_or_above? 113 + end 114 + 115 + def console_audits? 116 + user&.super_admin_or_above? 117 + end 118 + 119 + def access_admin_endpoints? 120 + user&.admin_or_above? 121 + end 122 + end 123 + RUBY 124 + 125 + say ' Creating DefaultPolicy...', :cyan 126 + file 'app/policies/default_policy.rb', <<~RUBY 127 + # frozen_string_literal: true 128 + 129 + # Default policy for records without a specific policy 130 + # Used when Pundit cannot find a policy for a given record 131 + class DefaultPolicy < ApplicationPolicy 132 + def index? 133 + user.present? 134 + end 135 + 136 + def show? 137 + user.present? 138 + end 139 + 140 + def create? 141 + user.present? 142 + end 143 + 144 + def update? 145 + user.present? && (user == record_owner || user.admin_or_above?) 146 + end 147 + 148 + def destroy? 149 + user.present? && (user == record_owner || user.admin_or_above?) 150 + end 151 + 152 + private 153 + 154 + def record_owner 155 + record.respond_to?(:user) ? record.user : nil 156 + end 157 + 158 + class Scope < ApplicationPolicy::Scope 159 + def resolve 160 + if user&.admin_or_above? 161 + scope.all 162 + else 163 + scope.none 164 + end 165 + end 166 + end 167 + end 168 + RUBY 169 + 170 + say ' Adding Pundit to ApplicationController...', :cyan 171 + inject_into_class 'app/controllers/application_controller.rb', 'ApplicationController', <<~RUBY 172 + include Pundit::Authorization 173 + after_action :verify_authorized, except: :index, unless: :skip_pundit? 174 + after_action :verify_policy_scoped, only: :index, unless: :skip_pundit? 175 + 176 + rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized 177 + 178 + private 179 + 180 + def user_not_authorized 181 + flash[:alert] = 'You are not authorized to perform this action.' 182 + redirect_back(fallback_location: root_path) 183 + end 184 + 185 + def skip_pundit? 186 + devise_controller? rescue false || self.class.to_s.start_with?('Sessions', 'Registrations') 187 + end 188 + RUBY 189 + 190 + say 'Pundit authorization configured!', :green 191 + say ' Create policies in app/policies/ for each model', :cyan 192 + say ' Use `authorize @record` in controller actions', :cyan 193 + say ' Use `policy_scope(Model)` for index queries', :cyan
+47
rails_performance.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say 'Setting up Rails Performance monitoring...', :green 4 + 5 + gem 'rails_performance' 6 + 7 + after_bundle do 8 + say ' Running Rails Performance installer...', :cyan 9 + rails_command 'rails_performance:install' 10 + end 11 + 12 + say ' Creating Rails Performance initializer...', :cyan 13 + file 'config/initializers/rails_performance.rb', <<~RUBY 14 + # frozen_string_literal: true 15 + 16 + RailsPerformance.setup do |config| 17 + # Storage backend (Redis required) 18 + config.redis = Redis.new(url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/0')) 19 + 20 + # Data retention 21 + config.duration = 4.hours 22 + 23 + # Enable/disable features 24 + config.enabled = true 25 + 26 + # Skip certain paths from tracking 27 + config.skipable_rake_tasks = %w[assets:precompile] 28 + 29 + # Ignore certain request paths 30 + config.ignored_paths = [ 31 + '/health', 32 + '/assets' 33 + ] 34 + 35 + # Custom event tracking 36 + # config.custom_data_proc = proc { |env| 37 + # { 38 + # user_id: env['warden']&.user&.id 39 + # } 40 + # } 41 + end if defined?(RailsPerformance) 42 + RUBY 43 + 44 + say 'Rails Performance configured!', :green 45 + say ' Dashboard at /admin/performance (admin only)', :cyan 46 + say ' Requires Redis for storage', :yellow 47 + say ' Tracks requests, Sidekiq jobs, rake tasks, and custom events', :cyan
+122
redis.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say 'Setting up Redis...', :green 4 + 5 + gem 'redis' 6 + gem 'redis-session-store' 7 + gem 'rack-attack' 8 + 9 + say ' Creating Redis initializer...', :cyan 10 + initializer 'redis.rb', <<~RUBY 11 + # frozen_string_literal: true 12 + 13 + REDIS = Redis.new(url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/0')) 14 + RUBY 15 + 16 + say ' Configuring Redis session store...', :cyan 17 + initializer 'session_store.rb', <<~RUBY 18 + # frozen_string_literal: true 19 + 20 + # Use Redis for session storage 21 + Rails.application.config.session_store :redis_store, 22 + servers: ENV.fetch('REDIS_URL') { 'redis://localhost:6379/2/session' }, 23 + expire_after: 90.minutes, 24 + key: "_\#{Rails.application.class.module_parent_name.underscore}_session", 25 + threadsafe: true, 26 + signed: true 27 + RUBY 28 + 29 + say ' Configuring Rack::Attack...', :cyan 30 + initializer 'rack_attack.rb', <<~RUBY 31 + # frozen_string_literal: true 32 + 33 + # Configure Rack Attack for rate limiting 34 + class Rack::Attack 35 + # Use Redis for Rack Attack storage 36 + Rack::Attack.cache.store = ActiveSupport::Cache::RedisCacheStore.new( 37 + url: ENV.fetch('REDIS_URL') { 'redis://localhost:6379/5' } 38 + ) 39 + 40 + # Throttle all requests by IP (300 req/5 minutes) 41 + throttle('req/ip', limit: 300, period: 5.minutes) do |req| 42 + req.ip unless req.path.start_with?('/assets') 43 + end 44 + 45 + # Throttle login attempts by email 46 + throttle('logins/email', limit: 5, period: 20.seconds) do |req| 47 + if req.path == '/sign_in' && req.post? 48 + req.params['email']&.to_s&.downcase&.gsub(/\\s+/, '') 49 + end 50 + end 51 + 52 + # Throttle password reset attempts 53 + throttle('password_resets/email', limit: 3, period: 20.minutes) do |req| 54 + if req.path == '/password/reset' && req.post? 55 + req.params['email']&.to_s&.downcase&.gsub(/\\s+/, '') 56 + end 57 + end 58 + 59 + # Block suspicious requests 60 + blocklist('block suspicious requests') do |req| 61 + # Block requests with suspicious patterns 62 + Rack::Attack::Fail2Ban.filter("pentesters-\#{req.ip}", maxretry: 5, findtime: 10.minutes, bantime: 1.hour) do 63 + # Return true if this is a suspicious request 64 + CGI.unescape(req.query_string) =~ %r{/etc/passwd} || 65 + req.path.include?('/etc/passwd') || 66 + req.path.include?('wp-admin') || 67 + req.path.include?('wp-login') 68 + end 69 + end 70 + 71 + # Always allow requests from localhost in development 72 + safelist('allow from localhost') do |req| 73 + req.ip == '127.0.0.1' || req.ip == '::1' if Rails.env.development? 74 + end 75 + 76 + # Custom response for throttled requests 77 + self.throttled_responder = lambda do |request| 78 + match_data = request.env['rack.attack.match_data'] 79 + now = Time.now.utc 80 + 81 + headers = { 82 + 'Content-Type' => 'application/json', 83 + 'Retry-After' => (match_data[:period] - (now.to_i % match_data[:period])).to_s 84 + } 85 + 86 + [429, headers, [{ error: 'Rate limit exceeded. Please try again later.' }.to_json]] 87 + end 88 + end 89 + RUBY 90 + 91 + say ' Configuring cache store...', :cyan 92 + inject_into_file 'config/environments/production.rb', after: "Rails.application.configure do\n" do 93 + <<~RUBY 94 + # Use Redis for caching 95 + config.cache_store = :redis_cache_store, { url: ENV.fetch('REDIS_URL') { 'redis://localhost:6379/1' } } 96 + 97 + # Enable Rack Attack middleware 98 + config.middleware.use Rack::Attack 99 + 100 + RUBY 101 + end 102 + 103 + inject_into_file 'config/environments/development.rb', after: "Rails.application.configure do\n" do 104 + <<~RUBY 105 + # Use Redis for caching (shared across processes) 106 + config.cache_store = :redis_cache_store, { url: ENV.fetch('REDIS_URL') { 'redis://localhost:6379/1' } } 107 + 108 + # Enable Rack Attack middleware (useful for testing rate limits) 109 + config.middleware.use Rack::Attack 110 + 111 + RUBY 112 + end 113 + 114 + say ' Adding Redis URL to .env.development...', :cyan 115 + append_to_file '.env.development', "REDIS_URL=redis://localhost:6379/0\n" 116 + 117 + say 'Redis configured!', :green 118 + say ' - Redis connection available via REDIS constant', :cyan 119 + say ' - Sessions stored in Redis (db 2)', :cyan 120 + say ' - Cache stored in Redis (db 1)', :cyan 121 + say ' - Rate limiting enabled via Rack::Attack (db 5)', :cyan 122 + say ' Make sure Redis is running locally or set REDIS_URL', :yellow
+132
security.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say 'Setting up security gems...', :green 4 + 5 + gem 'lockbox' 6 + gem 'blind_index' 7 + gem 'invisible_captcha' 8 + gem 'strong_migrations' 9 + 10 + after_bundle do 11 + say ' Running Lockbox audits generator...', :cyan 12 + rails_command 'generate lockbox:audits' 13 + 14 + say ' Running Strong Migrations installer...', :cyan 15 + rails_command 'generate strong_migrations:install' 16 + end 17 + 18 + say ' Creating Lockbox initializer...', :cyan 19 + initializer 'lockbox.rb', <<~RUBY 20 + # frozen_string_literal: true 21 + 22 + # Lockbox - Field-level encryption 23 + # https://github.com/ankane/lockbox 24 + # 25 + # Generate key with: Lockbox.generate_key 26 + 27 + if Rails.application.credentials.lockbox&.key?(:master_key) 28 + Lockbox.master_key = Rails.application.credentials.lockbox[:master_key] 29 + elsif ENV['LOCKBOX_MASTER_KEY'].present? 30 + Lockbox.master_key = ENV['LOCKBOX_MASTER_KEY'] 31 + elsif Rails.env.production? 32 + raise 'Lockbox master_key not configured! Run: rails credentials:edit' 33 + else 34 + Rails.logger.warn 'Lockbox master_key not found in credentials. Add it with: rails credentials:edit' 35 + end 36 + RUBY 37 + 38 + say ' Creating BlindIndex initializer...', :cyan 39 + initializer 'blind_index.rb', <<~RUBY 40 + # frozen_string_literal: true 41 + 42 + # Blind Index - Searchable Encryption 43 + # https://github.com/ankane/blind_index 44 + # 45 + # Allows searching encrypted columns without decrypting them 46 + # Generate key with: BlindIndex.generate_key 47 + 48 + if Rails.application.credentials.blind_index&.key?(:master_key) 49 + BlindIndex.master_key = Rails.application.credentials.blind_index[:master_key] 50 + elsif ENV['BLIND_INDEX_MASTER_KEY'].present? 51 + BlindIndex.master_key = ENV['BLIND_INDEX_MASTER_KEY'] 52 + elsif Rails.env.production? 53 + raise 'BlindIndex master_key not configured! Run: rails credentials:edit' 54 + else 55 + Rails.logger.warn 'BlindIndex master_key not found in credentials. Add it with: rails credentials:edit' 56 + end 57 + 58 + # Default options 59 + BlindIndex.default_options[:algorithm] = :argon2id # Most secure, recommended 60 + BlindIndex.default_options[:insecure_key] = false # Require secure keys 61 + RUBY 62 + 63 + say ' Creating InvisibleCaptcha initializer...', :cyan 64 + file 'config/initializers/invisible_captcha.rb', <<~RUBY 65 + # frozen_string_literal: true 66 + 67 + InvisibleCaptcha.setup do |config| 68 + # Minimum time (in seconds) for a human to fill out a form 69 + config.timestamp_threshold = 2 70 + 71 + # Custom honeypot field name (randomized per form by default) 72 + # config.honeypots = ['foo', 'bar'] 73 + 74 + # Flash message when spam is detected 75 + config.timestamp_error_message = 'Something went wrong. Please try again.' 76 + 77 + # Enable visual mode for debugging in development 78 + config.visual_honeypots = Rails.env.development? 79 + end 80 + RUBY 81 + 82 + say ' Creating Encryptable concern...', :cyan 83 + file 'app/models/concerns/encryptable.rb', <<~RUBY 84 + # frozen_string_literal: true 85 + 86 + # Include this concern and use the DSL to encrypt sensitive fields 87 + # 88 + # Example: 89 + # class User < ApplicationRecord 90 + # include Encryptable 91 + # 92 + # encrypts_field :ssn 93 + # encrypts_field :phone, searchable: true 94 + # end 95 + # 96 + # Migration for encrypted fields: 97 + # add_column :users, :ssn_ciphertext, :text 98 + # add_column :users, :phone_ciphertext, :text 99 + # add_column :users, :phone_bidx, :string # for searchable fields 100 + # add_index :users, :phone_bidx 101 + # 102 + module Encryptable 103 + extend ActiveSupport::Concern 104 + 105 + class_methods do 106 + def encrypts_field(field_name, searchable: false) 107 + encrypts field_name 108 + 109 + if searchable 110 + blind_index field_name 111 + end 112 + end 113 + end 114 + end 115 + RUBY 116 + 117 + say ' Adding invisible_captcha to ApplicationController...', :cyan 118 + inject_into_class 'app/controllers/application_controller.rb', 'ApplicationController', <<~RUBY 119 + # Invisible captcha is available in forms via: invisible_captcha 120 + # Add to specific controllers with: invisible_captcha only: [:create], on_spam: :spam_detected 121 + # 122 + # private 123 + # def spam_detected 124 + # redirect_to root_path, alert: 'Spam detected.' 125 + # end 126 + RUBY 127 + 128 + say 'Security gems configured!', :green 129 + say ' - Lockbox: Use `encrypts :field_name` in models', :cyan 130 + say ' - BlindIndex: Use `blind_index :field_name` for searchable encrypted fields', :cyan 131 + say ' - InvisibleCaptcha: Use `invisible_captcha` helper in forms', :cyan 132 + say ' Generate production keys with: Lockbox.generate_key / BlindIndex.generate_key', :yellow
+82
soft_delete.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say 'Setting up soft deletes with acts_as_paranoid...', :green 4 + 5 + gem 'acts_as_paranoid' 6 + 7 + say ' Creating SoftDeletable concern...', :cyan 8 + file 'app/models/concerns/soft_deletable.rb', <<~RUBY 9 + # frozen_string_literal: true 10 + 11 + # Include this concern in models that should support soft deletes 12 + # 13 + # Migration: 14 + # add_column :posts, :deleted_at, :datetime 15 + # add_index :posts, :deleted_at 16 + # 17 + # Usage: 18 + # class Post < ApplicationRecord 19 + # include SoftDeletable 20 + # end 21 + # 22 + # post.destroy # soft deletes (sets deleted_at) 23 + # post.deleted? # => true 24 + # post.recover # restores the record 25 + # post.destroy_fully! # permanently deletes 26 + # 27 + # Post.all # excludes soft-deleted records 28 + # Post.with_deleted # includes soft-deleted records 29 + # Post.only_deleted # only soft-deleted records 30 + # 31 + module SoftDeletable 32 + extend ActiveSupport::Concern 33 + 34 + included do 35 + acts_as_paranoid 36 + end 37 + 38 + # Permanently delete the record 39 + def destroy_fully! 40 + destroy_fully 41 + end 42 + end 43 + RUBY 44 + 45 + say ' Creating migration generator helper...', :cyan 46 + file 'lib/generators/soft_delete/soft_delete_generator.rb', <<~RUBY 47 + # frozen_string_literal: true 48 + 49 + class SoftDeleteGenerator < Rails::Generators::NamedBase 50 + include Rails::Generators::Migration 51 + 52 + source_root File.expand_path('templates', __dir__) 53 + 54 + def self.next_migration_number(_dirname) 55 + Time.now.utc.strftime('%Y%m%d%H%M%S') 56 + end 57 + 58 + def create_migration_file 59 + migration_template 'migration.rb.erb', "db/migrate/add_deleted_at_to_\#{table_name}.rb" 60 + end 61 + 62 + private 63 + 64 + def table_name 65 + file_name.tableize 66 + end 67 + end 68 + RUBY 69 + 70 + file 'lib/generators/soft_delete/templates/migration.rb.erb', <<~ERB 71 + class AddDeletedAtTo<%= table_name.camelize %> < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>] 72 + def change 73 + add_column :<%= table_name %>, :deleted_at, :datetime 74 + add_index :<%= table_name %>, :deleted_at 75 + end 76 + end 77 + ERB 78 + 79 + say 'Soft deletes configured!', :green 80 + say ' Include `SoftDeletable` in models', :cyan 81 + say ' Generate migration: rails g soft_delete ModelName', :cyan 82 + say ' Or manually: add_column :table, :deleted_at, :datetime', :cyan
+56
solid_queue.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say 'Setting up Solid Queue for background jobs...', :green 4 + 5 + gem 'solid_queue' 6 + gem 'mission_control-jobs' 7 + 8 + after_bundle do 9 + say ' Running Solid Queue installer...', :cyan 10 + rails_command 'generate solid_queue:install' 11 + 12 + say ' Creating queue database...', :cyan 13 + rails_command 'db:create:queue' 14 + 15 + say ' Loading Solid Queue schema...', :cyan 16 + rails_command 'db:schema:load:queue' 17 + end 18 + 19 + say ' Creating Solid Queue initializer...', :cyan 20 + file 'config/initializers/solid_queue.rb', <<~RUBY 21 + # frozen_string_literal: true 22 + 23 + # Solid Queue Configuration 24 + # https://github.com/rails/solid_queue 25 + # 26 + # Database-backed Active Job backend that uses PostgreSQL for job storage. 27 + # No need for Redis or external dependencies. 28 + 29 + Rails.application.config.solid_queue.tap do |config| 30 + # Silence polling queries in logs 31 + config.silence_polling = true 32 + 33 + # How often to check for new jobs (default: 0.1 seconds) 34 + # config.polling_interval = 0.1 35 + 36 + # Number of threads per worker (default: 3) 37 + # config.workers_per_process = 3 38 + 39 + # Concurrency per queue 40 + # config.concurrency_maintenance_interval = 600 41 + end 42 + RUBY 43 + 44 + say ' Configuring Active Job to use Solid Queue...', :cyan 45 + inject_into_file 'config/application.rb', after: "class Application < Rails::Application\n" do 46 + " config.active_job.queue_adapter = :solid_queue\n" 47 + end 48 + 49 + say ' Creating Procfile.dev entry...', :cyan 50 + append_to_file 'Procfile.dev', "jobs: bundle exec rake solid_queue:start\n" 51 + 52 + say 'Solid Queue configured!', :green 53 + say ' Dashboard at /admin/jobs (admin only)', :cyan 54 + say ' Jobs will be processed via: bin/jobs or rake solid_queue:start', :cyan 55 + say ' Run migrations: rails db:migrate', :yellow 56 + say ' Configure queues in config/solid_queue.yml', :cyan
+9
tailwind.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say '🎨 Setting up Tailwind CSS...', :green 4 + 5 + after_bundle do 6 + say ' Installing Tailwind...', :cyan 7 + rails_command 'tailwindcss:install' 8 + say '✅ Tailwind CSS configured!', :green 9 + end
+145 -627
template.rb
··· 1 - # template.rb 1 + # frozen_string_literal: true 2 2 3 + say '🚞 boxcar - Rails starter kit', :cyan 3 4 4 - # Add gems 5 - gem "devise" # Authentication 6 - gem "pundit" # Authorization 7 - gem "pg", "~> 1.6.2" # PostgreSQL adapter 8 - gem "redis", "~> 5.0" # Redis client 9 - gem "redis-session-store" # Redis-backed session store 10 - gem "rack-attack" # Rate limiting and throttling 5 + TEMPLATE_ROOT = if __FILE__.start_with?("http") 6 + File.dirname(__FILE__) 7 + else 8 + __dir__ 9 + end 11 10 12 - ############################################################################### 13 - # SECURITY & ENCRYPTION 14 - ############################################################################### 15 - gem "lockbox" # Encryption 16 - gem "blind_index" # Encrypted searchable fields 17 - gem "invisible_captcha" # Spam protection 11 + @post_install_tasks = [] 18 12 19 - ############################################################################### 20 - # AUDITING & VERSIONING 21 - ############################################################################### 22 - gem "paper_trail" # Model versioning 23 - gem "audits1984" # Audit logging 24 - gem "console1984" # Console access auditing 25 - gem "acts_as_paranoid" # Soft deletes 26 - 27 - ############################################################################### 28 - # SEARCH & INDEXING 29 - ############################################################################### 30 - gem "pg_search" # PostgreSQL full-text search 31 - gem "hashid-rails" # Obfuscate IDs 32 - gem "friendly_id" # Slugs and permalinks 13 + def apply_template(template_name, tasks = []) 14 + apply File.join(TEMPLATE_ROOT, "#{template_name}.rb") 15 + @post_install_tasks.concat(tasks) 16 + end 33 17 34 - ############################################################################### 35 - # STATE MACHINES 36 - ############################################################################### 37 - gem "aasm" # State machines 38 - 39 - ############################################################################### 40 - # ANALYTICS & MONITORING 41 - ############################################################################### 42 - gem "okcomputer" # Health checks 43 - gem "ahoy_matey" # Analytics 44 - gem "ahoy_email" # Email analytics 45 - gem "blazer" # BI dashboard 46 - gem "statsd-instrument" # StatsD metrics 47 - gem "rails_performance" # Performance monitoring 48 - 49 - ############################################################################### 50 - # EMAIL 51 - ############################################################################### 52 - gem "premailer-rails" # Inline CSS for emails 53 - gem "email_reply_parser" # Parse email replies 54 - gem "mailkick" # Email unsubscribe management 55 - 56 - ############################################################################### 57 - # BACKGROUND JOBS 58 - ############################################################################### 59 - gem "mission_control-jobs" # Job dashboard 60 - 61 - ############################################################################### 62 - # UTILITIES 63 - ############################################################################### 64 - gem "browser" # Browser detection 65 - gem "strong_migrations" # Safe migrations 66 - 67 - ############################################################################### 68 - # UI & FRONTEND 69 - ############################################################################### 70 - gem "tailwindcss-rails" # Tailwind CSS 71 - 72 - 73 - ############################################################################### 74 - # FEATURE FLAGS & CONFIGURATION 75 - ############################################################################### 76 - gem "flipper" # Feature flags 77 - gem "flipper-active_record" # ActiveRecord adapter for Flipper 78 - gem "flipper-ui" # UI for Flipper 79 - gem "flipper-active_support_cache_store" 80 - 81 - ############################################################################### 82 - # ENVIRONMENT VARIABLES 83 - ############################################################################### 84 - gem "dotenv-rails" # Environment variables 85 - 86 - gem_group :development, :test do 87 - gem "rspec-rails", "~> 7.1" # Testing framework 88 - gem "factory_bot_rails" # Test data factories 89 - gem "faker" # Fake data generation 90 - gem "shoulda-matchers" # RSpec matchers 91 - gem "rubocop-capybara", "~> 2.22", ">= 2.22.1" 92 - gem "rubocop-rspec", "~> 3.6" 93 - gem "rubocop-rspec_rails", "~> 2.31" 94 - gem "relaxed-rubocop" 95 - gem "query_count" # SQL query counter 96 - gem "bullet" # N+1 query detection 97 - end 18 + gem 'jb' 19 + gem 'awesome_print' 20 + gem 'faraday' 21 + gem 'dotenv-rails', groups: %i[development test] 98 22 99 23 gem_group :development do 100 - gem "actual_db_schema" # Rolls back phantom migrations 101 - gem "annotaterb" # Annotate models 102 - gem "listen", "~> 3.9" # File watcher 103 - gem "letter_opener_web" # Preview emails 104 - gem "foreman" # Process manager 105 - gem "awesome_print" # Pretty print objects 106 - gem "rack-mini-profiler", "~> 3.3", require: false # Performance profiling 107 - gem "stackprof" # Used by rack-mini-profiler for flamegraphs 24 + gem 'pry-rails' 25 + gem 'bullet' 26 + gem 'query_count' 27 + gem 'actual_db_schema' 28 + gem 'annotaterb' 29 + gem 'letter_opener_web' 108 30 end 109 31 110 - # Configure database to use PostgreSQL 111 - remove_file "config/database.yml" 112 - create_file "config/database.yml", <<~YAML 113 - default: &default 114 - adapter: postgresql 115 - encoding: unicode 116 - pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 117 - 118 - development: 119 - <<: *default 120 - database: #{app_name}_development 121 - 122 - test: 123 - <<: *default 124 - database: #{app_name}_test 125 - 126 - production: 127 - <<: *default 128 - database: #{app_name}_production 129 - username: #{app_name} 130 - password: <%= ENV["#{app_name.upcase}_DATABASE_PASSWORD"] %> 131 - YAML 132 - 133 - after_bundle do 134 - # Generate Devise 135 - generate "devise:install" 136 - generate "devise", "User" 137 - 138 - # Add fields to User 139 - generate "migration", "AddFieldsToUsers first_name:string last_name:string access_level:integer" 140 - 141 - # Update the migration to add default and null constraint for access_level 142 - in_root do 143 - migration_file = Dir.glob("db/migrate/*_add_fields_to_users.rb").first 144 - if migration_file 145 - # Replace the access_level line with one that has default and null constraint 146 - gsub_file migration_file, 147 - /add_column :users, :access_level, :integer$/, 148 - "add_column :users, :access_level, :integer, default: 0, null: false" 149 - end 150 - end 151 - 152 - # Generate Lockbox master key using Lockbox.generate_key 153 - # This needs to run after bundle install, so Lockbox is available 154 - lockbox_key = `rails runner "require 'lockbox'; puts Lockbox.generate_key"`.strip 155 - 156 - # Add lockbox key to credentials programmatically 157 - # Create a Ruby script that directly writes to credentials 158 - create_file 'tmp/add_lockbox_to_credentials.rb', <<~RUBY, force: true 159 - require 'active_support/encrypted_configuration' 160 - require 'yaml' 161 - 162 - # Path to credentials files 163 - credentials_path = Rails.root.join('config/credentials.yml.enc') 164 - key_path = Rails.root.join('config/master.key') 165 - 166 - # Read the key 167 - key = File.read(key_path).strip 168 - 169 - # Create encrypted configuration instance 170 - credentials = ActiveSupport::EncryptedConfiguration.new( 171 - config_path: credentials_path, 172 - key_path: key_path, 173 - env_key: 'RAILS_MASTER_KEY', 174 - raise_if_missing_key: true 175 - ) 176 - 177 - # Read existing credentials 178 - current_config = credentials.config 179 - 180 - # Parse as YAML if it's a string, and ensure we have a hash with string keys 181 - if current_config.is_a?(String) 182 - current_config = YAML.safe_load(current_config, permitted_classes: [Symbol]) || {} 183 - end 32 + file '.env.development', "" 184 33 185 - # Convert all keys to strings recursively 186 - current_config = current_config.deep_stringify_keys if current_config.respond_to?(:deep_stringify_keys) 34 + # Bullet configuration for N+1 query detection 35 + file 'config/initializers/bullet.rb', <<~RUBY 36 + # frozen_string_literal: true 187 37 188 - # Add lockbox config if not present 189 - unless current_config.key?('lockbox') 190 - current_config['lockbox'] = { 'master_key' => '#{lockbox_key}' } 191 - 192 - # Write back to credentials - ensure clean YAML format 193 - yaml_content = current_config.to_yaml 194 - # Remove the YAML document separator for cleaner output 195 - yaml_content = yaml_content.sub(/^---\\n/, '') 196 - # Add blank line before lockbox section for better readability 197 - yaml_content = yaml_content.sub(/^lockbox:/, "\\nlockbox:") 198 - 199 - credentials.write(yaml_content) 200 - puts "✓ Added lockbox master_key to credentials" 201 - else 202 - puts "⚠ Lockbox config already exists in credentials" 203 - end 204 - RUBY 205 - 206 - rails_command "runner tmp/add_lockbox_to_credentials.rb" 207 - remove_file 'tmp/add_lockbox_to_credentials.rb' 208 - 209 - initializer 'lockbox.rb', <<~RUBY 210 - # Set Lockbox master key from credentials 211 - if Rails.application.credentials.lockbox&.key?(:master_key) 212 - Lockbox.master_key = Rails.application.credentials.lockbox[:master_key] 213 - else 214 - Rails.logger.warn "Lockbox master_key not found in credentials. Please add it by running: rails credentials:edit" 215 - end 216 - RUBY 217 - 218 - initializer 'okcomputer.rb', <<~RUBY 219 - # frozen_string_literal: true 220 - 221 - # https://github.com/jphenow/okcomputer#registering-additional-checks 222 - # 223 - # class MyCustomCheck < OKComputer::Check 224 - # def call 225 - # if rand(10).even? 226 - # "Even is great!" 227 - # else 228 - # mark_failure 229 - # "We don't like odd numbers" 230 - # end 231 - # end 232 - # end 233 - 234 - OkComputer::Registry.register "database", OkComputer::ActiveRecordCheck.new 235 - OkComputer::Registry.register "cache", OkComputer::CacheCheck.new 236 - 237 - OkComputer::Registry.register "app_version", OkComputer::AppVersionCheck.new 238 - OkComputer::Registry.register "action_mailer", OkComputer::ActionMailerCheck.new 239 - 240 - # Run checks in parallel 241 - OkComputer.check_in_parallel = true 242 - 243 - # Log when health checks are run 244 - OkComputer.logger = Rails.logger 245 - RUBY 246 - 247 - generate "rails_performance:install" 248 - 249 - initializer 'rails_performance.rb', <<~RUBY 250 - RailsPerformance.setup do |config| 251 - config.redis = Redis.new(url: ENV["REDIS_URL"].presence || "redis://127.0.0.1:6379/0") 252 - config.duration = 4.hours 253 - 254 - config.enabled = true 255 - 256 - # protect with authentication 257 - config.verify_access_proc = proc { |controller| 258 - controller.current_user&.admin_access? 259 - } 260 - 261 - # Ignore admin and performance paths 262 - config.ignored_paths = ['/admin', '/rails/performance'] 263 - 264 - config.home_link = '/' 265 - config.skipable_rake_tasks = ['webpacker:compile'] 266 - end if defined?(RailsPerformance) 267 - RUBY 268 - 269 - initializer 'session_store.rb', <<~RUBY 270 - # Use Redis for session storage 271 - Rails.application.config.session_store :redis_store, 272 - servers: ENV.fetch("REDIS_URL") { "redis://localhost:6379/2/session" }, 273 - expire_after: 90.minutes, 274 - key: "_#{Rails.application.class.module_parent_name.underscore}_session", 275 - threadsafe: true, 276 - signed: true 277 - RUBY 278 - 279 - initializer 'rack_attack.rb', <<~RUBY 280 - # Configure Rack Attack for rate limiting 281 - class Rack::Attack 282 - # Use Redis for Rack Attack storage 283 - Rack::Attack.cache.store = ActiveSupport::Cache::RedisCacheStore.new( 284 - url: ENV.fetch("REDIS_URL") { "redis://localhost:6379/5" } 285 - ) 286 - 287 - # Throttle all requests by IP (300 req/5 minutes) 288 - throttle('req/ip', limit: 300, period: 5.minutes) do |req| 289 - req.ip unless req.path.start_with?('/assets') 290 - end 291 - 292 - # Throttle login attempts by email 293 - throttle('logins/email', limit: 5, period: 20.seconds) do |req| 294 - if req.path == '/users/sign_in' && req.post? 295 - req.params['user']&.dig('email')&.to_s&.downcase&.gsub(/\\s+/, "") 296 - end 297 - end 298 - 299 - # Throttle password reset attempts 300 - throttle('password_resets/email', limit: 3, period: 20.minutes) do |req| 301 - if req.path == '/users/password' && req.post? 302 - req.params['user']&.dig('email')&.to_s&.downcase&.gsub(/\\s+/, "") 303 - end 304 - end 305 - 306 - # Block suspicious requests 307 - blocklist('block suspicious requests') do |req| 308 - # Block requests with suspicious patterns 309 - Rack::Attack::Fail2Ban.filter("pentesters-\#{req.ip}", maxretry: 5, findtime: 10.minutes, bantime: 1.hour) do 310 - # Return true if this is a suspicious request 311 - CGI.unescape(req.query_string) =~ %r{/etc/passwd} || 312 - req.path.include?('/etc/passwd') || 313 - req.path.include?('wp-admin') || 314 - req.path.include?('wp-login') 315 - end 316 - end 317 - 318 - # Always allow requests from localhost in development 319 - safelist('allow from localhost') do |req| 320 - req.ip == '127.0.0.1' || req.ip == '::1' if Rails.env.development? 38 + if defined?(Bullet) && Rails.env.development? 39 + Rails.application.configure do 40 + config.after_initialize do 41 + Bullet.enable = true 42 + Bullet.alert = false 43 + Bullet.bullet_logger = true 44 + Bullet.console = true 45 + Bullet.rails_logger = true 46 + Bullet.add_footer = true 321 47 end 322 48 end 323 - RUBY 324 - 325 - # Configure cache store to use Redis in production 326 - inject_into_file 'config/environments/production.rb', after: "Rails.application.configure do\n" do 327 - <<~RUBY 328 - # Use Redis for caching 329 - config.cache_store = :redis_cache_store, { url: ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } } 330 - 331 - # Enable Rack Attack middleware 332 - config.middleware.use Rack::Attack 333 - 334 - RUBY 335 49 end 336 - 337 - # Configure cache store to use Redis in development 338 - inject_into_file 'config/environments/development.rb', after: "Rails.application.configure do\n" do 339 - <<~RUBY 340 - # Use Redis for caching (shared across processes) 341 - config.cache_store = :redis_cache_store, { url: ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } } 342 - 343 - # Enable Rack Attack middleware (useful for testing rate limits) 344 - config.middleware.use Rack::Attack 345 - 346 - RUBY 347 - end 348 - 349 - inject_into_file 'app/models/application_record.rb', 350 - " include PgSearch::Model\n", 351 - after: "primary_abstract_class\n" 352 - 353 - inject_into_file 'app/models/application_record.rb', 354 - " has_paper_trail\n", 355 - after: "include PgSearch::Model\n" 356 - 357 - generate "devise:views" 358 - generate "pg_search:migration:multisearch" 359 - generate "lockbox:audits" 360 - generate "pundit:install" 361 - generate "ahoy:install" 362 - generate "ahoy:messages --encryption=lockbox" 363 - generate "ahoy:clicks" 364 - generate "annotate_rb:install" 365 - generate "blazer:install" 366 - generate "flipper:setup" 367 - generate "bullet:install" 368 - generate "strong_migrations:install" 369 - generate "solid_queue:install" 370 - generate "paper_trail:install" 371 - generate "mailkick:install" 372 - generate "mailkick:views" 50 + RUBY 373 51 374 - # Set up RSpec 375 - generate "rspec:install" 376 - 377 - # Create and run migrations 378 - rails_command "db:create" 379 - rails_command "db:migrate" 380 - 381 - # Create a custom controller 382 - # generate :controller, "pages", "home" 383 - # route "root to: 'pages#home'" 384 - 385 - # Add enum and has_subscriptions to User model 386 - inject_into_file 'app/models/user.rb', after: "class User < ApplicationRecord\n" do 387 - <<~RUBY 388 - enum :access_level, { 389 - user: 0, 390 - admin: 1, 391 - super_admin: 2, 392 - owner: 3 393 - }, default: :user, null: false 394 - 395 - has_subscriptions 396 - 397 - # Helper method to check if user has any admin access 398 - def admin_access? 399 - admin? || super_admin? || owner? 52 + # Letter Opener Web for email preview in development 53 + route <<~RUBY 54 + if Rails.env.development? 55 + mount LetterOpenerWeb::Engine, at: '/letter_opener' 400 56 end 57 + RUBY 401 58 402 - # Return full name 403 - def full_name 404 - "\#{first_name} \#{last_name}".strip 405 - end 59 + inject_into_file 'config/environments/development.rb', before: /^end$/ do 60 + <<~RUBY 406 61 407 - RUBY 408 - end 409 - 410 - file 'app/controllers/admin/application_controller.rb', <<~RUBY 411 - module Admin 412 - class ApplicationController < ::ApplicationController 413 - include Pundit 414 - rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized 415 - before_action :require_admin 416 - 417 - # Shared admin logic here 418 - def index 419 - @current_user = current_user 420 - end 421 - 422 - private 62 + # Use letter_opener for email delivery 63 + config.action_mailer.delivery_method = :letter_opener_web 64 + config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } 65 + RUBY 66 + end 423 67 424 - def require_admin 425 - unless current_user&.admin? || current_user&.super_admin? || current_user&.owner? 426 - redirect_to root_path, alert: "You are not authorized to access this area." 427 - end 428 - end 68 + gsub_file 'app/controllers/application_controller.rb', 69 + /^\s*# Only allow modern browsers.*\n\s*allow_browser versions: :modern\n?/m, 70 + '' 429 71 430 - def user_not_authorized 431 - flash[:alert] = "You are not authorized to perform this action." 432 - redirect_to(request.referrer || root_path) 433 - end 434 - end 72 + # Initialize credentials for each environment 73 + say 'Setting up credentials for all environments...', :green 74 + %w[development staging production].each do |env| 75 + say " Creating #{env} credentials...", :cyan 76 + run "EDITOR='echo' bin/rails credentials:edit --environment #{env}", abort_on_failure: false 435 77 end 436 - RUBY 437 78 438 - # Create Admin index view 439 - file 'app/views/admin/application/index.html.erb', <<~HTML 440 - <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> 441 - <div class="mb-8"> 442 - <h1 class="text-3xl font-bold text-gray-900">Admin Dashboard</h1> 443 - <p class="mt-2 text-sm text-gray-600"> 444 - Welcome back, <%= @current_user.full_name %> 445 - </p> 446 - </div> 447 - 448 - <div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3"> 449 - <!-- Blazer Card --> 450 - <div class="bg-white overflow-hidden shadow rounded-lg hover:shadow-lg transition-shadow"> 451 - <div class="p-6"> 452 - <div class="flex items-center"> 453 - <div class="flex-shrink-0 bg-blue-500 rounded-md p-3"> 454 - <svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 455 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /> 456 - </svg> 457 - </div> 458 - <div class="ml-5 w-0 flex-1"> 459 - <dl> 460 - <dt class="text-sm font-medium text-gray-500 truncate">Blazer</dt> 461 - <dd class="mt-1 text-sm text-gray-900">Business Intelligence & Analytics</dd> 462 - </dl> 463 - </div> 464 - </div> 465 - <div class="mt-4"> 466 - <%= link_to "Open Blazer", "/admin/blazer", class: "text-blue-600 hover:text-blue-800 text-sm font-medium" %> 467 - </div> 468 - </div> 469 - </div> 470 - 471 - <% if @current_user.super_admin? || @current_user.owner? %> 472 - <!-- Flipper Card --> 473 - <div class="bg-white overflow-hidden shadow rounded-lg hover:shadow-lg transition-shadow"> 474 - <div class="p-6"> 475 - <div class="flex items-center"> 476 - <div class="flex-shrink-0 bg-purple-500 rounded-md p-3"> 477 - <svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 478 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" /> 479 - </svg> 480 - </div> 481 - <div class="ml-5 w-0 flex-1"> 482 - <dl> 483 - <dt class="text-sm font-medium text-gray-500 truncate">Flipper</dt> 484 - <dd class="mt-1 text-sm text-gray-900">Feature Flags Management</dd> 485 - </dl> 486 - </div> 487 - </div> 488 - <div class="mt-4"> 489 - <%= link_to "Manage Features", "/admin/flipper", class: "text-purple-600 hover:text-purple-800 text-sm font-medium" %> 490 - </div> 491 - </div> 492 - </div> 493 - <% end %> 494 - 495 - <!-- Performance Card --> 496 - <div class="bg-white overflow-hidden shadow rounded-lg hover:shadow-lg transition-shadow"> 497 - <div class="p-6"> 498 - <div class="flex items-center"> 499 - <div class="flex-shrink-0 bg-green-500 rounded-md p-3"> 500 - <svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 501 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /> 502 - </svg> 503 - </div> 504 - <div class="ml-5 w-0 flex-1"> 505 - <dl> 506 - <dt class="text-sm font-medium text-gray-500 truncate">Performance</dt> 507 - <dd class="mt-1 text-sm text-gray-900">Monitor Application Performance</dd> 508 - </dl> 509 - </div> 510 - </div> 511 - <div class="mt-4"> 512 - <%= link_to "View Performance", "/admin/performance", class: "text-green-600 hover:text-green-800 text-sm font-medium" %> 513 - </div> 514 - </div> 515 - </div> 516 - 517 - <!-- Users Card --> 518 - <div class="bg-white overflow-hidden shadow rounded-lg hover:shadow-lg transition-shadow"> 519 - <div class="p-6"> 520 - <div class="flex items-center"> 521 - <div class="flex-shrink-0 bg-yellow-500 rounded-md p-3"> 522 - <svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 523 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /> 524 - </svg> 525 - </div> 526 - <div class="ml-5 w-0 flex-1"> 527 - <dl> 528 - <dt class="text-sm font-medium text-gray-500 truncate">Users</dt> 529 - <dd class="mt-1 text-sm text-gray-900">Manage User Accounts</dd> 530 - </dl> 531 - </div> 532 - </div> 533 - <div class="mt-4"> 534 - <%= link_to "Manage Users", admin_users_path, class: "text-yellow-600 hover:text-yellow-800 text-sm font-medium" %> 535 - </div> 536 - </div> 537 - </div> 79 + # Create credentials example file 80 + file 'config/credentials.yml.example', <<~YAML 81 + # Credentials structure for all environments 82 + # Edit with: EDITOR=nano rails credentials:edit --environment <env> 83 + # 84 + # Generate keys in rails console: 85 + # Lockbox.generate_key 86 + # BlindIndex.generate_key 87 + # SecureRandom.hex(32) 538 88 539 - <!-- Health Checks Card --> 540 - <div class="bg-white overflow-hidden shadow rounded-lg hover:shadow-lg transition-shadow"> 541 - <div class="p-6"> 542 - <div class="flex items-center"> 543 - <div class="flex-shrink-0 bg-red-500 rounded-md p-3"> 544 - <svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 545 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" /> 546 - </svg> 547 - </div> 548 - <div class="ml-5 w-0 flex-1"> 549 - <dl> 550 - <dt class="text-sm font-medium text-gray-500 truncate">Health Checks</dt> 551 - <dd class="mt-1 text-sm text-gray-900">System Health Monitoring</dd> 552 - </dl> 553 - </div> 554 - </div> 555 - <div class="mt-4"> 556 - <%= link_to "View Health", "/healthchecks", class: "text-red-600 hover:text-red-800 text-sm font-medium" %> 557 - </div> 558 - </div> 559 - </div> 89 + secret_key_base: # auto-generated 560 90 561 - <!-- Mission Control Card --> 562 - <div class="bg-white overflow-hidden shadow rounded-lg hover:shadow-lg transition-shadow"> 563 - <div class="p-6"> 564 - <div class="flex items-center"> 565 - <div class="flex-shrink-0 bg-indigo-500 rounded-md p-3"> 566 - <svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 567 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /> 568 - </svg> 569 - </div> 570 - <div class="ml-5 w-0 flex-1"> 571 - <dl> 572 - <dt class="text-sm font-medium text-gray-500 truncate">Background Jobs</dt> 573 - <dd class="mt-1 text-sm text-gray-900">Monitor Background Jobs</dd> 574 - </dl> 575 - </div> 576 - </div> 577 - <div class="mt-4"> 578 - <%= link_to "View Jobs", "/jobs", class: "text-indigo-600 hover:text-indigo-800 text-sm font-medium" %> 579 - </div> 580 - </div> 581 - </div> 582 - </div> 583 - </div> 584 - HTML 91 + lockbox: 92 + master_key: # Lockbox.generate_key 585 93 586 - # Create Admin Policy 587 - file 'app/policies/admin_policy.rb', <<~RUBY 588 - class AdminPolicy < ApplicationPolicy 589 - def blazer? 590 - user&.admin? || user&.super_admin? || user&.owner? 591 - end 94 + blind_index: 95 + master_key: # BlindIndex.generate_key 592 96 593 - def flipper? 594 - user&.super_admin? || user&.owner? 595 - end 97 + hashid: 98 + salt: # SecureRandom.hex(32) 99 + YAML 596 100 597 - def access_admin_endpoints? 598 - user&.admin? || user&.super_admin? || user&.owner? 599 - end 600 - end 601 - RUBY 101 + # Database configuration (must run early) 102 + apply_template('database') 602 103 104 + # Core modules (always installed) 105 + apply_template('public_identifiable') 106 + apply_template('auth', ['run `rails db:migrate`']) 107 + gem 'tailwindcss-rails' 108 + apply_template('tailwind') 109 + apply_template('pundit') 110 + apply_template('redis') 111 + apply_template('security') 112 + apply_template('flipper') 113 + apply_template('solid_queue') 603 114 604 - # Configure admin routes 605 - route <<~RUBY 606 - namespace :admin do 607 - root to: "application#index" 115 + # Admin dashboards 116 + apply_template('blazer') 117 + apply_template('rails_performance') 608 118 609 - mount Blazer::Engine, at: "blazer", constraints: ->(request) { 610 - user = User.find_by(id: request.session[:user_id]) 611 - user && AdminPolicy.new(user, :admin).blazer? 612 - } 119 + # Infrastructure 120 + apply_template('health_checks') 121 + apply_template('analytics') 122 + apply_template('console1984') 613 123 614 - mount Flipper::UI.app(Flipper), at: "flipper", constraints: ->(request) { 615 - user = User.find_by(id: request.session[:user_id]) 616 - user && AdminPolicy.new(user, :admin).flipper? 617 - } 124 + # Common utilities 125 + apply_template('kaminari') 126 + apply_template('paper_trail') 127 + apply_template('soft_delete') 128 + apply_template('friendly_id') 129 + apply_template('pg_search') 130 + apply_template('aasm') 131 + apply_template('mailkick') 132 + apply_template('metrics') 618 133 619 - mount RailsPerformance::Engine, at: "performance", constraints: ->(request) { 620 - user = User.find_by(id: request.session[:user_id]) 621 - user && AdminPolicy.new(user, :admin).access_admin_endpoints? 622 - } 134 + # Apply admin routes after all admin-related modules are loaded 135 + apply_template('admin_routes') 623 136 624 - resources :users, shallow: true 625 - end 626 - RUBY 137 + # Create GitHub workflows 138 + say 'Creating GitHub workflows...', :green 139 + empty_directory '.github/workflows' 627 140 628 - # Mount OkComputer health checks 629 - route <<~RUBY 630 - mount OkComputer::Engine, at: "/healthchecks" 631 - RUBY 141 + file '.github/workflows/check-indexes.yml', <<~YAML 142 + name: Check Indexes 143 + on: 144 + pull_request: 145 + paths: 146 + - 'db/migrate/**.rb' 632 147 633 - inject_into_file 'app/mailers/application_mailer.rb', 634 - " has_history\nutm_params\n", 635 - after: "class ApplicationMailer < ActionMailer::Base\n" 148 + jobs: 149 + check-indexes: 150 + runs-on: ubuntu-latest 151 + steps: 152 + - uses: actions/checkout@v4 153 + with: 154 + fetch-depth: 0 636 155 637 - inject_into_file 'app/controllers/application_controller.rb', 638 - " include Pundit::Authorization\n before_action :set_paper_trail_whodunnit\n", 639 - after: "class ApplicationController < ActionController::Base\n" 156 + - name: Check Migration Indexes 157 + uses: speedshop/ids_must_be_indexed@v1.2.1 158 + YAML 640 159 641 - # Create GitHub workflows 642 - empty_directory '.github/workflows' 160 + # Configure ApplicationMailer for tracking 161 + inject_into_file 'app/mailers/application_mailer.rb', after: "class ApplicationMailer < ActionMailer::Base\n" do 162 + " has_history\n utm_params\n" 163 + end 643 164 644 - file '.github/workflows/check-indexes.yml', <<~YAML 645 - name: Check Indexes 646 - on: 647 - pull_request: 648 - paths: 649 - - 'db/migrate/**.rb' 165 + # Run generators after bundle 166 + after_bundle do 167 + say 'Running development tool generators...', :green 650 168 651 - jobs: 652 - check-indexes: 653 - runs-on: ubuntu-latest 654 - steps: 655 - - uses: actions/checkout@v4 656 - with: 657 - fetch-depth: 0 169 + say ' Running AnnotateRb installer...', :cyan 170 + rails_command 'generate annotate_rb:install' 658 171 659 - - name: Check Migration Indexes 660 - uses: speedshop/ids_must_be_indexed@v1.2.1 661 - YAML 172 + say ' Running Bullet installer...', :cyan 173 + rails_command 'generate bullet:install' 174 + end 662 175 663 - # Run migrations one final time to catch any remaining pending migrations 664 - rails_command "db:migrate" 176 + say '' 177 + say '' 178 + say '✅ boxcar setup complete!', :green 179 + say '' 665 180 666 - # Git initialization 667 - git :init 668 - git add: "." 669 - git commit: "-m 'Initial commit (from @jaspermayone/rails-template)'" 181 + if @post_install_tasks.any? 182 + say 'Next steps:', :yellow 183 + @post_install_tasks.uniq.each { |task| say " - #{task}", :yellow } 184 + say '' 670 185 end 186 + 187 + say 'Run `cd testapp && bin/dev` to start your app', :cyan 188 + say ''