My opinionated ruby on rails template
at main 369 lines 12 kB view raw
1# frozen_string_literal: true 2 3say 'Setting up custom authentication...', :green 4say ' Adding bcrypt gem...', :cyan 5 6gem 'bcrypt', '~> 3.1' 7 8# Generate models after bundle install 9after_bundle do 10 say ' Generating User model...', :cyan 11 generate :model, 'User email:string:uniq password_digest:string role:integer' 12 13 say ' Generating Session model...', :cyan 14 generate :model, 'Session user:references token:string:uniq ip_address:string user_agent:string' 15end 16 17# User model with has_secure_password and roles 18file 'app/models/user.rb', <<~RUBY, force: true 19 class User < ApplicationRecord 20 include PublicIdentifiable 21 set_public_id_prefix :usr 22 23 has_secure_password 24 25 enum :role, { user: 0, admin: 1, super_admin: 2, owner: 3 }, default: :user 26 27 normalizes :email, with: ->(email) { email.strip.downcase } 28 29 validates :email, presence: true, 30 uniqueness: { case_sensitive: false }, 31 format: { with: URI::MailTo::EMAIL_REGEXP } 32 validates :password, length: { minimum: 8 }, if: -> { password.present? } 33 validates :role, presence: true 34 35 # Helper method to check if user has any admin access 36 def admin_or_above? 37 admin? || super_admin? || owner? 38 end 39 40 # Helper method to check if user has super admin or owner access 41 def super_admin_or_above? 42 super_admin? || owner? 43 end 44 end 45RUBY 46 47file 'app/models/session.rb', <<~RUBY, force: true 48 class Session < ApplicationRecord 49 belongs_to :user 50 51 before_create :generate_token 52 53 validates :token, presence: true, uniqueness: true 54 55 private 56 57 def generate_token 58 self.token = SecureRandom.urlsafe_base64(32) 59 end 60 end 61RUBY 62 63say ' Creating Current model...', :cyan 64file 'app/models/current.rb', <<~RUBY 65 class Current < ActiveSupport::CurrentAttributes 66 attribute :session, :user 67 68 delegate :user, to: :session, allow_nil: true 69 end 70RUBY 71 72say ' Creating Authentication concern...', :cyan 73file 'app/controllers/concerns/authentication.rb', <<~RUBY 74 module Authentication 75 extend ActiveSupport::Concern 76 77 included do 78 before_action :authenticate 79 helper_method :signed_in?, :current_user 80 end 81 82 private 83 84 def authenticate 85 if (session_record = find_session_by_cookie) 86 Current.session = session_record 87 end 88 end 89 90 def require_authentication 91 redirect_to sign_in_path, alert: 'Please sign in to continue.' unless signed_in? 92 end 93 94 def require_admin 95 require_authentication 96 return if current_user&.admin_or_above? 97 98 redirect_to root_path, alert: 'You are not authorized to access this page.' 99 end 100 101 def require_super_admin 102 require_authentication 103 return if current_user&.super_admin? 104 105 redirect_to root_path, alert: 'You are not authorized to access this page.' 106 end 107 108 def signed_in? 109 Current.session.present? 110 end 111 112 def current_user 113 Current.user 114 end 115 116 def find_session_by_cookie 117 Session.find_by(token: cookies.signed[:session_token]) 118 end 119 120 def start_session(user) 121 session_record = user.sessions.create!( 122 ip_address: request.remote_ip, 123 user_agent: request.user_agent 124 ) 125 cookies.signed.permanent[:session_token] = { 126 value: session_record.token, 127 httponly: true, 128 same_site: :lax 129 } 130 Current.session = session_record 131 end 132 133 def end_session 134 Current.session&.destroy 135 cookies.delete(:session_token) 136 end 137 end 138RUBY 139 140say ' Adding to ApplicationController...', :cyan 141inject_into_class 'app/controllers/application_controller.rb', 'ApplicationController', <<~RUBY 142 include Authentication 143RUBY 144 145say ' Creating SessionsController...', :cyan 146file 'app/controllers/sessions_controller.rb', <<~RUBY 147 class SessionsController < ApplicationController 148 skip_before_action :authenticate, only: %i[new create] 149 150 def new 151 redirect_to root_path if signed_in? 152 end 153 154 def create 155 if (user = User.find_by(email: params[:email])&.authenticate(params[:password])) 156 start_session(user) 157 redirect_to root_path, notice: 'Signed in successfully.' 158 else 159 flash.now[:alert] = 'Invalid email or password.' 160 render :new, status: :unprocessable_entity 161 end 162 end 163 164 def destroy 165 end_session 166 redirect_to root_path, notice: 'Signed out successfully.' 167 end 168 end 169RUBY 170 171say ' Creating RegistrationsController...', :cyan 172file 'app/controllers/registrations_controller.rb', <<~RUBY 173 class RegistrationsController < ApplicationController 174 skip_before_action :authenticate, only: %i[new create] 175 176 def new 177 redirect_to root_path if signed_in? 178 @user = User.new 179 end 180 181 def create 182 @user = User.new(user_params) 183 if @user.save 184 start_session(@user) 185 redirect_to root_path, notice: 'Account created successfully.' 186 else 187 render :new, status: :unprocessable_entity 188 end 189 end 190 191 private 192 193 def user_params 194 params.require(:user).permit(:email, :password, :password_confirmation) 195 end 196 end 197RUBY 198 199say ' Creating sign in view...', :cyan 200file 'app/views/sessions/new.html.erb', <<~ERB 201 <div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"> 202 <div class="max-w-md w-full space-y-8"> 203 <div> 204 <h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900"> 205 Sign in to your account 206 </h2> 207 <p class="mt-2 text-center text-sm text-gray-600"> 208 Or 209 <%= link_to 'create a new account', sign_up_path, class: 'font-medium text-canopy-green hover:text-fresh-leaf' %> 210 </p> 211 </div> 212 213 <%= form_with url: sign_in_path, class: 'mt-8 space-y-6' do |f| %> 214 <div class="rounded-md shadow-sm -space-y-px"> 215 <div> 216 <%= f.label :email, class: 'sr-only' %> 217 <%= f.email_field :email, required: true, autofocus: true, autocomplete: 'email', 218 placeholder: 'Email address', 219 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' %> 220 </div> 221 <div> 222 <%= f.label :password, class: 'sr-only' %> 223 <%= f.password_field :password, required: true, autocomplete: 'current-password', 224 placeholder: 'Password', 225 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' %> 226 </div> 227 </div> 228 229 <div> 230 <%= f.submit 'Sign in', 231 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' %> 232 </div> 233 <% end %> 234 </div> 235 </div> 236ERB 237 238say ' Creating sign up view...', :cyan 239file 'app/views/registrations/new.html.erb', <<~ERB 240 <div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"> 241 <div class="max-w-md w-full space-y-8"> 242 <div> 243 <h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900"> 244 Create your account 245 </h2> 246 <p class="mt-2 text-center text-sm text-gray-600"> 247 Already have an account? 248 <%= link_to 'Sign in', sign_in_path, class: 'font-medium text-canopy-green hover:text-fresh-leaf' %> 249 </p> 250 </div> 251 252 <%= form_with model: @user, url: sign_up_path, class: 'mt-8 space-y-6' do |f| %> 253 <% if @user.errors.any? %> 254 <div class="rounded-md bg-red-50 p-4"> 255 <div class="text-sm text-red-700"> 256 <ul class="list-disc pl-5 space-y-1"> 257 <% @user.errors.full_messages.each do |message| %> 258 <li><%= message %></li> 259 <% end %> 260 </ul> 261 </div> 262 </div> 263 <% end %> 264 265 <div class="rounded-md shadow-sm -space-y-px"> 266 <div> 267 <%= f.label :email, class: 'sr-only' %> 268 <%= f.email_field :email, required: true, autofocus: true, autocomplete: 'email', 269 placeholder: 'Email address', 270 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' %> 271 </div> 272 <div> 273 <%= f.label :password, class: 'sr-only' %> 274 <%= f.password_field :password, required: true, autocomplete: 'new-password', 275 placeholder: 'Password (min 8 characters)', 276 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' %> 277 </div> 278 <div> 279 <%= f.label :password_confirmation, class: 'sr-only' %> 280 <%= f.password_field :password_confirmation, required: true, autocomplete: 'new-password', 281 placeholder: 'Confirm password', 282 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' %> 283 </div> 284 </div> 285 286 <div> 287 <%= f.submit 'Create account', 288 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' %> 289 </div> 290 <% end %> 291 </div> 292 </div> 293ERB 294 295say ' Adding routes...', :cyan 296route <<~RUBY 297 # Authentication 298 get 'sign_in', to: 'sessions#new' 299 post 'sign_in', to: 'sessions#create' 300 delete 'sign_out', to: 'sessions#destroy' 301 get 'sign_up', to: 'registrations#new' 302 post 'sign_up', to: 'registrations#create' 303RUBY 304 305say ' Creating Admin namespace...', :cyan 306file 'app/controllers/admin/application_controller.rb', <<~RUBY 307 # frozen_string_literal: true 308 309 module Admin 310 class ApplicationController < ::ApplicationController 311 before_action :require_admin 312 313 def index 314 @current_user = current_user 315 end 316 317 private 318 319 def require_admin 320 unless current_user&.admin_or_above? 321 redirect_to root_path, alert: 'You are not authorized to access this area.' 322 end 323 end 324 end 325 end 326RUBY 327 328file 'app/controllers/admin/users_controller.rb', <<~RUBY 329 # frozen_string_literal: true 330 331 module Admin 332 class UsersController < Admin::ApplicationController 333 before_action :set_user, only: %i[show edit update destroy] 334 335 def index 336 @users = User.all 337 end 338 339 def show; end 340 341 def edit; end 342 343 def update 344 if @user.update(user_params) 345 redirect_to admin_user_path(@user), notice: 'User updated successfully.' 346 else 347 render :edit, status: :unprocessable_entity 348 end 349 end 350 351 def destroy 352 @user.destroy 353 redirect_to admin_users_path, notice: 'User deleted successfully.' 354 end 355 356 private 357 358 def set_user 359 @user = User.find(params[:id]) 360 end 361 362 def user_params 363 params.require(:user).permit(:email, :role) 364 end 365 end 366 end 367RUBY 368 369say 'Custom authentication setup complete!', :green