├── .cursorrules ├── .gitattributes ├── .gitignore ├── .rspec ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── Procfile.dev ├── README.md ├── Rakefile ├── app ├── assets │ ├── builds │ │ └── .keep │ ├── config │ │ └── manifest.js │ ├── images │ │ ├── .keep │ │ ├── rapid-ruby-stamp-dark.svg │ │ └── rapid-ruby-stamp.svg │ └── stylesheets │ │ ├── application.css │ │ └── application.tailwind.css ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb ├── components │ ├── admin │ │ ├── masquerade_component.html.erb │ │ └── masquerade_component.rb │ ├── application_component.rb │ ├── avatar_component.html.erb │ ├── avatar_component.rb │ ├── fieldset_header_component.html.erb │ ├── fieldset_header_component.rb │ ├── user_dropdown_component.html.erb │ └── user_dropdown_component.rb ├── controllers │ ├── admin │ │ ├── application_controller.rb │ │ ├── base_controller.rb │ │ ├── sessions_controller.rb │ │ ├── team_users_controller.rb │ │ ├── teams_controller.rb │ │ └── users_controller.rb │ ├── application_controller.rb │ ├── concerns │ │ ├── .keep │ │ └── authenticatable.rb │ ├── identity │ │ ├── account_controller.rb │ │ ├── email_verifications_controller.rb │ │ ├── emails_controller.rb │ │ └── password_resets_controller.rb │ ├── lessons_controller.rb │ ├── pages_controller.rb │ ├── passwords_controller.rb │ ├── registrations_controller.rb │ └── sessions_controller.rb ├── dashboards │ ├── session_dashboard.rb │ ├── team_dashboard.rb │ ├── team_user_dashboard.rb │ └── user_dashboard.rb ├── helpers │ ├── application_helper.rb │ └── styles_helper.rb ├── javascript │ ├── application.js │ └── controllers │ │ ├── application.js │ │ ├── flash_controller.js │ │ ├── index.js │ │ └── toggle_controller.js ├── jobs │ └── application_job.rb ├── mailers │ ├── application_mailer.rb │ ├── session_mailer.rb │ └── user_mailer.rb ├── models │ ├── application_record.rb │ ├── concerns │ │ ├── .keep │ │ └── avatarable.rb │ ├── current.rb │ ├── email_verification_token.rb │ ├── password_reset_token.rb │ ├── session.rb │ ├── team.rb │ ├── team_user.rb │ └── user.rb ├── services │ ├── disposable_email_service.rb │ └── markdown_service.rb ├── validators │ └── disposable_email_validator.rb └── views │ ├── admin │ ├── application │ │ └── _navigation.html.erb │ └── users │ │ └── show.html.erb │ ├── identity │ ├── account │ │ └── show.html.erb │ ├── emails │ │ └── _form.html.erb │ └── password_resets │ │ ├── edit.html.erb │ │ └── new.html.erb │ ├── layouts │ ├── application.html.erb │ ├── mailer.html.erb │ ├── mailer.text.erb │ └── partials │ │ └── _head.html.erb │ ├── lessons │ └── index.html.erb │ ├── pages │ └── home.html.erb │ ├── passwords │ └── _form.html.erb │ ├── registrations │ └── new.html.erb │ ├── session_mailer │ └── signed_in_notification.html.erb │ ├── sessions │ ├── index.html.erb │ └── new.html.erb │ ├── shared │ ├── _flash_alerts.html.erb │ └── _form_errors.html.erb │ └── user_mailer │ ├── email_verification.html.erb │ └── password_reset.html.erb ├── bin ├── brakeman ├── bundle ├── dev ├── importmap ├── jobs ├── rails ├── rake ├── rspec ├── rubocop ├── setup └── thrust ├── config.ru ├── config ├── application.rb ├── boot.rb ├── cable.yml ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── importmap.rb ├── initializers │ ├── assets.rb │ ├── content_security_policy.rb │ ├── exception_notification.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── new_framework_defaults_8_0.rb │ └── permissions_policy.rb ├── locales │ └── en.yml ├── puma.rb ├── queue.yml ├── recurring.yml ├── routes.rb ├── storage.yml └── tailwind.config.js ├── data └── disposable_email_domains.txt ├── db ├── migrate │ ├── 20230205103754_create_users.rb │ ├── 20230205103755_create_sessions.rb │ ├── 20230205103756_create_email_verification_tokens.rb │ ├── 20230205103757_create_password_reset_tokens.rb │ ├── 20240820163037_add_service_name_to_active_storage_blobs.active_storage.rb │ ├── 20240820163038_create_active_storage_variant_records.active_storage.rb │ ├── 20240820163039_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb │ ├── 20241026134459_create_teams.rb │ ├── 20241120200338_install_solid_cable.rb │ ├── 20241124153040_install_solid_queue.rb │ ├── 20241124153608_add_admin_to_users.rb │ ├── 20241124161131_create_active_storage_tables.active_storage.rb │ └── 20241124165139_add_masquerade_to_sessions.rb ├── schema.rb └── seeds.rb ├── lib ├── assets │ └── .keep └── tasks │ ├── .keep │ └── admin.rake ├── log └── .keep ├── public ├── 400.html ├── 404.html ├── 406-unsupported-browser.html ├── 422.html ├── 500.html ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png ├── favicon.ico ├── icon.png ├── icon.svg └── robots.txt ├── spec ├── components │ ├── admin │ │ └── masquerade_component_spec.rb │ ├── avatar_component_spec.rb │ ├── fieldset_header_component_spec.rb │ └── user_dropdown_component_spec.rb ├── features │ ├── identity │ │ ├── emails_spec.rb │ │ └── password_resets_spec.rb │ ├── passwords_spec.rb │ ├── registrations_spec.rb │ └── sessions_spec.rb ├── fixtures │ ├── team_users.yml │ ├── teams.yml │ └── users.yml ├── mailers │ ├── session_mailer_spec.rb │ └── user_mailer_spec.rb ├── models │ ├── concerns │ │ └── avatarable_spec.rb │ ├── session_spec.rb │ ├── team_spec.rb │ ├── team_user_spec.rb │ └── user_spec.rb ├── rails_helper.rb ├── requests │ ├── admin │ │ └── masquerade_spec.rb │ ├── identity │ │ ├── account_spec.rb │ │ ├── email_verifications_spec.rb │ │ ├── emails_spec.rb │ │ └── password_resets_spec.rb │ ├── pages_requests_spec.rb │ ├── passwords_spec.rb │ ├── registrations_spec.rb │ └── sessions_spec.rb ├── services │ ├── disposable_email_service_spec.rb │ └── markdown_service_spec.rb ├── spec_helper.rb └── support │ ├── capybara_config.rb │ ├── mail_helper.rb │ ├── request_helper.rb │ └── system_test_helper.rb ├── storage └── .keep ├── tmp ├── .keep ├── pids │ └── .keep └── storage │ └── .keep └── vendor ├── .keep └── javascript ├── .keep └── stimulus-use.js /.cursorrules: -------------------------------------------------------------------------------- 1 | 2 | You are an expert in Ruby on Rails, PostgreSQL, Hotwire (Turbo and Stimulus), and Tailwind CSS. 3 | 4 | Code Style and Structure 5 | - Write concise, idiomatic Ruby code with accurate examples. 6 | - Follow Rails conventions and best practices. 7 | - Use object-oriented and functional programming patterns as appropriate. 8 | - Prefer iteration and modularization over code duplication. 9 | - Use descriptive variable and method names (e.g., user_signed_in?, calculate_total). 10 | - Structure files according to Rails conventions (MVC, concerns, helpers, etc.). 11 | 12 | Naming Conventions 13 | - Use snake_case for file names, method names, and variables. 14 | - Use CamelCase for class and module names. 15 | - Follow Rails naming conventions for models, controllers, and views. 16 | 17 | Ruby and Rails Usage 18 | - Use Ruby 3.x features when appropriate (e.g., pattern matching, endless methods). 19 | - Leverage Rails' built-in helpers and methods. 20 | - Use ActiveRecord effectively for database operations. 21 | 22 | Syntax and Formatting 23 | - Follow the Ruby Style Guide (https://rubystyle.guide/) 24 | - Use Ruby's expressive syntax (e.g., unless, ||=, &.) 25 | - Prefer double quotes for strings. 26 | 27 | Error Handling and Validation 28 | - Use exceptions for exceptional cases, not for control flow. 29 | - Implement proper error logging and user-friendly messages. 30 | - Use ActiveModel validations in models. 31 | - Handle errors gracefully in controllers and display appropriate flash messages. 32 | 33 | UI and Styling 34 | - Use Hotwire (Turbo and Stimulus) for dynamic, SPA-like interactions. 35 | - Implement responsive design with Tailwind CSS. 36 | - Use Rails view helpers and partials to keep views DRY. 37 | 38 | Performance Optimization 39 | - Use database indexing effectively. 40 | - Implement caching strategies (fragment caching, Russian Doll caching). 41 | - Use eager loading to avoid N+1 queries. 42 | - Optimize database queries using includes, joins, or select. 43 | 44 | Key Conventions 45 | - Follow RESTful routing conventions. 46 | - Use concerns for shared behavior across models or controllers. 47 | - Implement service objects for complex business logic. 48 | - Use background jobs (e.g., ActiveJob) for time-consuming tasks. 49 | 50 | Testing 51 | - Write comprehensive tests using RSpec. 52 | - Follow TDD/BDD practices. 53 | - Use fixtures or create Active Record objects for test data generation. 54 | 55 | Security 56 | - Implement proper authentication and authorization (e.g., Devise, Pundit). 57 | - Use strong parameters in controllers. 58 | - Protect against common web vulnerabilities (XSS, CSRF, SQL injection). 59 | 60 | Follow the official Ruby on Rails guides for best practices in routing, controllers, models, views, and other Rails components. 61 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # See https://git-scm.com/docs/gitattributes for more about git attribute files. 2 | 3 | # Mark the database schema as having been generated. 4 | db/schema.rb linguist-generated 5 | 6 | # Mark any vendored files as having been vendored. 7 | vendor/* linguist-vendored 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-* 13 | 14 | # Ignore all logfiles and tempfiles. 15 | /log/* 16 | /tmp/* 17 | !/log/.keep 18 | !/tmp/.keep 19 | 20 | # Ignore pidfiles, but keep the directory. 21 | /tmp/pids/* 22 | !/tmp/pids/ 23 | !/tmp/pids/.keep 24 | 25 | # Ignore uploaded files in development. 26 | /storage/* 27 | !/storage/.keep 28 | /tmp/storage/* 29 | !/tmp/storage/ 30 | !/tmp/storage/.keep 31 | 32 | /public/assets 33 | 34 | # Ignore master key for decrypting credentials and more. 35 | /config/master.key 36 | .env 37 | 38 | /app/assets/builds/* 39 | !/app/assets/builds/.keep 40 | 41 | /node_modules 42 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.3.4 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | ruby "3.3.4" 5 | 6 | # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" 7 | gem "rails", "~> 8.0.0" 8 | 9 | # The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] 10 | gem "sprockets-rails" 11 | 12 | # Use sqlite3 as the database for Active Record 13 | gem "sqlite3" 14 | 15 | # Use the Puma web server [https://github.com/puma/puma] 16 | gem "puma", "~> 6.0" 17 | 18 | gem "importmap-rails" 19 | 20 | # Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] 21 | gem "turbo-rails" 22 | 23 | # Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] 24 | gem "stimulus-rails" 25 | 26 | # Use Tailwind CSS [https://github.com/rails/tailwindcss-rails] 27 | gem "tailwindcss-rails" 28 | 29 | # Use solid_cable adapter to run Action Cable in production 30 | gem "solid_cable" 31 | 32 | # Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] 33 | # gem "kredis" 34 | 35 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 36 | gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ] 37 | 38 | # Reduces boot times through caching; required in config/boot.rb 39 | gem "bootsnap", require: false 40 | 41 | gem "authentication-zero" 42 | # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] 43 | gem "bcrypt", "~> 3.1.7" 44 | 45 | # Use Sass to process CSS 46 | # gem "sassc-rails" 47 | 48 | # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] 49 | gem "image_processing", "~> 1.2" 50 | 51 | group :development, :test do 52 | # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem 53 | gem "debug", platforms: %i[ mri mingw x64_mingw ] 54 | gem "rspec-rails" 55 | gem "dotenv-rails" 56 | end 57 | 58 | group :development do 59 | gem "letter_opener_web" 60 | 61 | # Use console on exceptions pages [https://github.com/rails/web-console] 62 | gem "web-console" 63 | 64 | # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler] 65 | # gem "rack-mini-profiler" 66 | 67 | gem "hotwire-spark" 68 | end 69 | 70 | group :test do 71 | gem "shoulda-matchers", require: false 72 | gem "capybara" 73 | gem "cuprite" 74 | # gem "vcr", require: false 75 | # gem "webmock" 76 | end 77 | 78 | gem "meta-tags" 79 | gem "class_variants" 80 | gem "http" 81 | gem "inline_svg" 82 | 83 | gem "view_component", "~> 3.13" 84 | gem "administrate" 85 | gem "redcarpet", "~> 3.6" # Markdown renderer 86 | 87 | gem "solid_queue" 88 | gem "mission_control-jobs" 89 | 90 | gem "dry-initializer", "~> 3.1" 91 | gem "dry-operation" 92 | 93 | # Exception tracking 94 | gem "exception_notification" 95 | gem "slack-notifier" 96 | 97 | # Use Redis for Action Cable 98 | gem "redis", "~> 4.0" 99 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Pete Hawkins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | web: bin/rails server -p 3000 2 | css: bin/rails tailwindcss:watch 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RapidRuby.com Starter template 2 | 3 | This app is a starter template that I use for most of my example apps that I build for my Youtube and Rapid Ruby episodes. 4 | 5 | ### Core features 6 | 7 | - Rails 8 8 | - Hotwire using importmaps and spark for development 9 | - Tailwind CSS 10 | - User authentication with authentication-zero 11 | - ViewComponent 12 | - RSpec 13 | 14 | ### Creating a fresh app 15 | 16 | The below is customised to my workflow, you may want to skip/tweak a few of the steps. Requires `brew install gh` to create the new repository. 17 | 18 | ```sh 19 | # Setup commands 20 | cd ~/Sites/youtube 21 | git clone git@github.com:phawk/rapid-ruby-starter.git new_project_name 22 | cd new_project_name 23 | code . 24 | bundle install 25 | yarn install 26 | git remote rm origin 27 | gh repo create new_project_name --public --source=. 28 | git push origin main -u 29 | bin/rails db:system:change --to=postgresql # optional 30 | bin/rails db:create 31 | bin/rails db:migrate && bin/rails db:migrate RAILS_ENV=test 32 | # Create your user account interactively 33 | bin/rails admin:create_user 34 | # Run the server 35 | bin/dev 36 | ``` 37 | 38 | ### License 39 | 40 | [The MIT License (MIT)](LICENSE.txt) 41 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative "config/application" 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/assets/builds/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapidruby/rapid-ruby-starter/8b6700c7f9e974da31c728d7da167fffd2939faa/app/assets/builds/.keep -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../stylesheets .css 3 | //= link_tree ../builds 4 | //= link_tree ../../javascript .js 5 | //= link_tree ../../../vendor/javascript .js 6 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapidruby/rapid-ruby-starter/8b6700c7f9e974da31c728d7da167fffd2939faa/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/images/rapid-ruby-stamp-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/assets/images/rapid-ruby-stamp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's 6 | * vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* 6 | 7 | @layer components { 8 | .btn-primary { 9 | @apply py-2 px-4 bg-blue-200; 10 | } 11 | } 12 | 13 | */ 14 | -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/components/admin/masquerade_component.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

You are masquerading as <%= session.user.email %>

3 | <%= button_to "Return to admin", reverse_masquerade_admin_user_path(session.admin_user), method: :post, class: helpers.button_classes.render(style: :secondary) %> 4 |
5 | -------------------------------------------------------------------------------- /app/components/admin/masquerade_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Admin::MasqueradeComponent < ApplicationComponent 4 | option :session 5 | 6 | def render? 7 | return false if session.blank? 8 | 9 | session.masquerading? 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/components/application_component.rb: -------------------------------------------------------------------------------- 1 | require "dry-initializer" 2 | 3 | class ApplicationComponent < ViewComponent::Base 4 | extend Dry::Initializer 5 | end 6 | -------------------------------------------------------------------------------- /app/components/avatar_component.html.erb: -------------------------------------------------------------------------------- 1 | <% if has_custom_avatar? %> 2 | <%= image_tag user.avatar.variant(:medium), class: combined_css_classes, alt: user.obfuscated_name %> 3 | <% else %> 4 | <%= user.letters_svg(size: svg_dimensions, font_size: svg_font_size, extra_classes: combined_css_classes) %> 5 | <% end %> 6 | -------------------------------------------------------------------------------- /app/components/avatar_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AvatarComponent < ApplicationComponent 4 | option :user 5 | option :size, default: -> { :medium } # :thumb / :small / :medium 6 | option :css_classes, default: -> { "rounded-full" } 7 | 8 | def has_custom_avatar? 9 | user.avatar.attached? 10 | end 11 | 12 | def svg_dimensions 13 | return 96 if size == :medium 14 | return 48 if size == :small 15 | 32 16 | end 17 | 18 | def svg_font_size 19 | return 36 if size == :medium 20 | return 18 if size == :small 21 | 14 22 | end 23 | 24 | def combined_css_classes 25 | classes = [css_classes] 26 | classes << "flex-none bg-gray-800 object-cover" 27 | classes << "h-24 w-24" if size == :medium 28 | classes << "h-12 w-12" if size == :small 29 | classes << "h-8 w-8" if size == :thumb 30 | classes.join(" ") 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/components/fieldset_header_component.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

<%= title %>

3 | <% if description.present? %> 4 |

<%= description %>

5 | <% elsif content.present? %> 6 |
7 | <%= content %> 8 |
9 | <% end %> 10 |
11 | -------------------------------------------------------------------------------- /app/components/fieldset_header_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class FieldsetHeaderComponent < ApplicationComponent 4 | option :title, required: true 5 | option :description, optional: true, default: nil 6 | 7 | def render? 8 | title.present? || description.present? 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/components/user_dropdown_component.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | 8 |
9 | 10 | 31 |
32 | -------------------------------------------------------------------------------- /app/components/user_dropdown_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UserDropdownComponent < ApplicationComponent 4 | option :current_user 5 | option :current_session 6 | 7 | delegate :admin?, to: :current_user 8 | 9 | def render? 10 | current_user.present? && current_session.present? 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/controllers/admin/application_controller.rb: -------------------------------------------------------------------------------- 1 | # All Administrate controllers inherit from this 2 | # `Administrate::ApplicationController`, making it the ideal place to put 3 | # authentication logic or other before_actions. 4 | # 5 | # If you want to add pagination or other controller-level concerns, 6 | # you're free to overwrite the RESTful controller actions. 7 | module Admin 8 | class ApplicationController < Administrate::ApplicationController 9 | include Authenticatable 10 | 11 | before_action :authenticate_admin 12 | 13 | def authenticate_admin 14 | return redirect_to(root_path) if Current.user.blank? 15 | 16 | unless Current.user.admin? 17 | redirect_to root_path, alert: "You are not an admin" 18 | end 19 | end 20 | 21 | # Override this value to specify the number of elements to display at a time 22 | # on index pages. Defaults to 20. 23 | # def records_per_page 24 | # params[:per_page] || 20 25 | # end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/controllers/admin/base_controller.rb: -------------------------------------------------------------------------------- 1 | module Admin 2 | class BaseController < ApplicationController 3 | before_action :ensure_admin! 4 | 5 | private 6 | 7 | def ensure_admin! 8 | return redirect_to root_path if Current.user.blank? 9 | 10 | unless Current.user.admin? 11 | redirect_to root_path, alert: "You are not an admin" 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/controllers/admin/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | module Admin 2 | class SessionsController < Admin::ApplicationController 3 | # Overwrite any of the RESTful controller actions to implement custom behavior 4 | # For example, you may want to send an email after a foo is updated. 5 | # 6 | # def update 7 | # super 8 | # send_foo_updated_email(requested_resource) 9 | # end 10 | 11 | # Override this method to specify custom lookup behavior. 12 | # This will be used to set the resource for the `show`, `edit`, and `update` 13 | # actions. 14 | # 15 | # def find_resource(param) 16 | # Foo.find_by!(slug: param) 17 | # end 18 | 19 | # The result of this lookup will be available as `requested_resource` 20 | 21 | # Override this if you have certain roles that require a subset 22 | # this will be used to set the records shown on the `index` action. 23 | # 24 | # def scoped_resource 25 | # if current_user.super_admin? 26 | # resource_class 27 | # else 28 | # resource_class.with_less_stuff 29 | # end 30 | # end 31 | 32 | # Override `resource_params` if you want to transform the submitted 33 | # data before it's persisted. For example, the following would turn all 34 | # empty values into nil values. It uses other APIs such as `resource_class` 35 | # and `dashboard`: 36 | # 37 | # def resource_params 38 | # params.require(resource_class.model_name.param_key). 39 | # permit(dashboard.permitted_attributes(action_name)). 40 | # transform_values { |value| value == "" ? nil : value } 41 | # end 42 | 43 | # See https://administrate-demo.herokuapp.com/customizing_controller_actions 44 | # for more information 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /app/controllers/admin/team_users_controller.rb: -------------------------------------------------------------------------------- 1 | module Admin 2 | class TeamUsersController < Admin::ApplicationController 3 | # Overwrite any of the RESTful controller actions to implement custom behavior 4 | # For example, you may want to send an email after a foo is updated. 5 | # 6 | # def update 7 | # super 8 | # send_foo_updated_email(requested_resource) 9 | # end 10 | 11 | # Override this method to specify custom lookup behavior. 12 | # This will be used to set the resource for the `show`, `edit`, and `update` 13 | # actions. 14 | # 15 | # def find_resource(param) 16 | # Foo.find_by!(slug: param) 17 | # end 18 | 19 | # The result of this lookup will be available as `requested_resource` 20 | 21 | # Override this if you have certain roles that require a subset 22 | # this will be used to set the records shown on the `index` action. 23 | # 24 | # def scoped_resource 25 | # if current_user.super_admin? 26 | # resource_class 27 | # else 28 | # resource_class.with_less_stuff 29 | # end 30 | # end 31 | 32 | # Override `resource_params` if you want to transform the submitted 33 | # data before it's persisted. For example, the following would turn all 34 | # empty values into nil values. It uses other APIs such as `resource_class` 35 | # and `dashboard`: 36 | # 37 | # def resource_params 38 | # params.require(resource_class.model_name.param_key). 39 | # permit(dashboard.permitted_attributes(action_name)). 40 | # transform_values { |value| value == "" ? nil : value } 41 | # end 42 | 43 | # See https://administrate-demo.herokuapp.com/customizing_controller_actions 44 | # for more information 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /app/controllers/admin/teams_controller.rb: -------------------------------------------------------------------------------- 1 | module Admin 2 | class TeamsController < Admin::ApplicationController 3 | # Overwrite any of the RESTful controller actions to implement custom behavior 4 | # For example, you may want to send an email after a foo is updated. 5 | # 6 | # def update 7 | # super 8 | # send_foo_updated_email(requested_resource) 9 | # end 10 | 11 | # Override this method to specify custom lookup behavior. 12 | # This will be used to set the resource for the `show`, `edit`, and `update` 13 | # actions. 14 | # 15 | # def find_resource(param) 16 | # Foo.find_by!(slug: param) 17 | # end 18 | 19 | # The result of this lookup will be available as `requested_resource` 20 | 21 | # Override this if you have certain roles that require a subset 22 | # this will be used to set the records shown on the `index` action. 23 | # 24 | # def scoped_resource 25 | # if current_user.super_admin? 26 | # resource_class 27 | # else 28 | # resource_class.with_less_stuff 29 | # end 30 | # end 31 | 32 | # Override `resource_params` if you want to transform the submitted 33 | # data before it's persisted. For example, the following would turn all 34 | # empty values into nil values. It uses other APIs such as `resource_class` 35 | # and `dashboard`: 36 | # 37 | # def resource_params 38 | # params.require(resource_class.model_name.param_key). 39 | # permit(dashboard.permitted_attributes(action_name)). 40 | # transform_values { |value| value == "" ? nil : value } 41 | # end 42 | 43 | # See https://administrate-demo.herokuapp.com/customizing_controller_actions 44 | # for more information 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /app/controllers/admin/users_controller.rb: -------------------------------------------------------------------------------- 1 | module Admin 2 | class UsersController < Admin::ApplicationController 3 | skip_before_action :authenticate_admin, only: :reverse_masquerade 4 | 5 | def masquerade 6 | Current.session.masquerade_as!(requested_resource) 7 | redirect_to root_path 8 | end 9 | 10 | def reverse_masquerade 11 | Current.session.reverse_masquerade! 12 | redirect_to admin_users_path 13 | end 14 | 15 | # Overwrite any of the RESTful controller actions to implement custom behavior 16 | # For example, you may want to send an email after a foo is updated. 17 | # 18 | # def update 19 | # super 20 | # send_foo_updated_email(requested_resource) 21 | # end 22 | 23 | # Override this method to specify custom lookup behavior. 24 | # This will be used to set the resource for the `show`, `edit`, and `update` 25 | # actions. 26 | # 27 | # def find_resource(param) 28 | # Foo.find_by!(slug: param) 29 | # end 30 | 31 | # The result of this lookup will be available as `requested_resource` 32 | 33 | # Override this if you have certain roles that require a subset 34 | # this will be used to set the records shown on the `index` action. 35 | # 36 | # def scoped_resource 37 | # if current_user.super_admin? 38 | # resource_class 39 | # else 40 | # resource_class.with_less_stuff 41 | # end 42 | # end 43 | 44 | # Override `resource_params` if you want to transform the submitted 45 | # data before it's persisted. For example, the following would turn all 46 | # empty values into nil values. It uses other APIs such as `resource_class` 47 | # and `dashboard`: 48 | # 49 | # def resource_params 50 | # params.require(resource_class.model_name.param_key). 51 | # permit(dashboard.permitted_attributes(action_name)). 52 | # transform_values { |value| value == "" ? nil : value } 53 | # end 54 | 55 | # See https://administrate-demo.herokuapp.com/customizing_controller_actions 56 | # for more information 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | include Authenticatable 3 | 4 | before_action :prepare_exception_notifier 5 | 6 | private 7 | 8 | def prepare_exception_notifier 9 | return unless user_signed_in? 10 | 11 | request.env["exception_notifier.exception_data"] = { 12 | current_user: Current.user 13 | } 14 | end 15 | 16 | def markdown(text) 17 | MarkdownService.to_html(text) 18 | end 19 | helper_method :markdown 20 | end 21 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapidruby/rapid-ruby-starter/8b6700c7f9e974da31c728d7da167fffd2939faa/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/concerns/authenticatable.rb: -------------------------------------------------------------------------------- 1 | module Authenticatable 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | before_action :set_current_request_details 6 | before_action :load_session 7 | before_action :authenticate 8 | 9 | helper_method :user_signed_in? 10 | end 11 | 12 | def user_signed_in? 13 | Current.user.present? 14 | end 15 | 16 | def ensure_signed_out! 17 | redirect_to(root_path) if user_signed_in? 18 | end 19 | 20 | def load_session 21 | if session = Session.find_by_id(cookies.signed[:session_token]) 22 | Current.session = session 23 | end 24 | end 25 | 26 | def authenticate 27 | redirect_to(sign_in_path) unless user_signed_in? 28 | end 29 | 30 | def set_current_request_details 31 | Current.user_agent = request.user_agent 32 | Current.ip_address = request.ip 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/controllers/identity/account_controller.rb: -------------------------------------------------------------------------------- 1 | class Identity::AccountController < ApplicationController 2 | def show 3 | @user = Current.user 4 | end 5 | 6 | def update 7 | @user = Current.user 8 | 9 | if @user.update(update_params) 10 | redirect_to identity_account_path, notice: "Personal details updated!" 11 | else 12 | render :show 13 | end 14 | end 15 | 16 | private 17 | 18 | def update_params 19 | params.require(:user).permit(:first_name, :last_name, :avatar) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/controllers/identity/email_verifications_controller.rb: -------------------------------------------------------------------------------- 1 | class Identity::EmailVerificationsController < ApplicationController 2 | skip_before_action :authenticate, only: :edit 3 | 4 | before_action :set_user, only: :edit 5 | 6 | def edit 7 | @user.update! verified: true 8 | redirect_to identity_account_path, notice: "Thank you for verifying your email address" 9 | end 10 | 11 | def create 12 | UserMailer.with(user: Current.user).email_verification.deliver_later 13 | redirect_to identity_account_path, notice: "We sent a verification email to your email address" 14 | end 15 | 16 | private 17 | def set_user 18 | @token = EmailVerificationToken.find_signed!(params[:sid]); @user = @token.user 19 | rescue 20 | redirect_to identity_account_path, alert: "That email verification link is invalid" 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/controllers/identity/emails_controller.rb: -------------------------------------------------------------------------------- 1 | class Identity::EmailsController < ApplicationController 2 | before_action :set_user 3 | 4 | def update 5 | if @user.update(user_params) 6 | redirect_to_root 7 | else 8 | render turbo_stream: turbo_stream.replace( 9 | "change_email_form", 10 | partial: "identity/emails/form" 11 | ) 12 | end 13 | end 14 | 15 | private 16 | def set_user 17 | @user = Current.user 18 | end 19 | 20 | def user_params 21 | params.permit(:email) 22 | end 23 | 24 | def redirect_to_root 25 | if @user.email_previously_changed? 26 | resend_email_verification 27 | redirect_to root_path, notice: "Your email has been changed" 28 | else 29 | redirect_to root_path 30 | end 31 | end 32 | 33 | def resend_email_verification 34 | UserMailer.with(user: @user).email_verification.deliver_later 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /app/controllers/identity/password_resets_controller.rb: -------------------------------------------------------------------------------- 1 | class Identity::PasswordResetsController < ApplicationController 2 | skip_before_action :authenticate 3 | 4 | before_action :set_user, only: %i[ edit update ] 5 | 6 | def new 7 | end 8 | 9 | def edit 10 | end 11 | 12 | def create 13 | if @user = User.find_by(email: params[:email], verified: true) 14 | UserMailer.with(user: @user).password_reset.deliver_later 15 | redirect_to sign_in_path, notice: "Check your email for reset instructions" 16 | else 17 | redirect_to new_identity_password_reset_path, alert: "You can't reset your password until you verify your email" 18 | end 19 | end 20 | 21 | def update 22 | if @user.update(user_params) 23 | @token.destroy; redirect_to(sign_in_path, notice: "Your password was reset successfully. Please sign in") 24 | else 25 | render :edit, status: :unprocessable_entity 26 | end 27 | end 28 | 29 | private 30 | def set_user 31 | @token = PasswordResetToken.find_signed!(params[:sid]); @user = @token.user 32 | rescue 33 | redirect_to new_identity_password_reset_path, alert: "That password reset link is invalid" 34 | end 35 | 36 | def user_params 37 | params.permit(:password, :password_confirmation) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /app/controllers/lessons_controller.rb: -------------------------------------------------------------------------------- 1 | class LessonsController < ApplicationController 2 | skip_before_action :authenticate, only: %i[index] 3 | 4 | def index 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/controllers/pages_controller.rb: -------------------------------------------------------------------------------- 1 | class PagesController < ApplicationController 2 | skip_before_action :authenticate 3 | 4 | def home 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/controllers/passwords_controller.rb: -------------------------------------------------------------------------------- 1 | class PasswordsController < ApplicationController 2 | before_action :set_user 3 | 4 | def update 5 | if !@user.authenticate(params[:current_password]) 6 | redirect_to identity_account_path, alert: "The current password you entered is incorrect" 7 | elsif @user.update(user_params) 8 | redirect_to identity_account_path, notice: "Your password has been changed" 9 | else 10 | # render :edit, status: :unprocessable_entity 11 | render turbo_stream: turbo_stream.replace( 12 | "change_password_form", 13 | partial: "passwords/form" 14 | ) 15 | end 16 | end 17 | 18 | private 19 | def set_user 20 | @user = Current.user 21 | end 22 | 23 | def user_params 24 | params.permit(:password, :password_confirmation) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/controllers/registrations_controller.rb: -------------------------------------------------------------------------------- 1 | class RegistrationsController < ApplicationController 2 | skip_before_action :authenticate, only: %i[ new create ] 3 | before_action :ensure_signed_out!, only: %i[ new ] 4 | 5 | def new 6 | @user = User.new 7 | end 8 | 9 | def create 10 | @user = User.new(user_params) 11 | 12 | @user.team = Team.new(name: "#{@user.first_name}’s Team") 13 | 14 | if @user.save 15 | @user.team.users << @user 16 | session = @user.sessions.create! 17 | cookies.signed.permanent[:session_token] = { value: session.id, httponly: true } 18 | 19 | send_email_verification 20 | redirect_to root_path, notice: "Welcome! You have signed up successfully" 21 | else 22 | render :new, status: :unprocessable_entity 23 | end 24 | end 25 | 26 | private 27 | def user_params 28 | params.permit(:first_name, :last_name, :email, :password, :password_confirmation) 29 | end 30 | 31 | def send_email_verification 32 | UserMailer.with(user: @user).email_verification.deliver_later 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/controllers/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class SessionsController < ApplicationController 2 | skip_before_action :authenticate, only: %i[ new create ] 3 | before_action :ensure_signed_out!, only: %i[ new create ] 4 | 5 | before_action :set_session, only: :destroy 6 | 7 | def index 8 | @sessions = Current.user.sessions.order(created_at: :desc) 9 | end 10 | 11 | def new 12 | @user = User.new 13 | end 14 | 15 | def create 16 | user = User.find_by(email: params[:email]) 17 | 18 | if user && user.authenticate(params[:password]) 19 | @session = user.sessions.create! 20 | cookies.signed.permanent[:session_token] = { value: @session.id, httponly: true } 21 | 22 | redirect_to root_path, notice: "Signed in successfully" 23 | else 24 | redirect_to sign_in_path(email_hint: params[:email]), alert: "That email or password is incorrect" 25 | end 26 | end 27 | 28 | def destroy 29 | @session.destroy; redirect_to(sessions_path, notice: "That session has been logged out") 30 | end 31 | 32 | private 33 | def set_session 34 | @session = Current.user.sessions.find(params[:id]) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /app/dashboards/session_dashboard.rb: -------------------------------------------------------------------------------- 1 | require "administrate/base_dashboard" 2 | 3 | class SessionDashboard < Administrate::BaseDashboard 4 | # ATTRIBUTE_TYPES 5 | # a hash that describes the type of each of the model's fields. 6 | # 7 | # Each different type represents an Administrate::Field object, 8 | # which determines how the attribute is displayed 9 | # on pages throughout the dashboard. 10 | ATTRIBUTE_TYPES = { 11 | id: Field::Number, 12 | ip_address: Field::String, 13 | user: Field::BelongsTo, 14 | user_agent: Field::String, 15 | created_at: Field::DateTime, 16 | updated_at: Field::DateTime, 17 | }.freeze 18 | 19 | # COLLECTION_ATTRIBUTES 20 | # an array of attributes that will be displayed on the model's index page. 21 | # 22 | # By default, it's limited to four items to reduce clutter on index pages. 23 | # Feel free to add, remove, or rearrange items. 24 | COLLECTION_ATTRIBUTES = %i[ 25 | id 26 | ip_address 27 | user 28 | user_agent 29 | ].freeze 30 | 31 | # SHOW_PAGE_ATTRIBUTES 32 | # an array of attributes that will be displayed on the model's show page. 33 | SHOW_PAGE_ATTRIBUTES = %i[ 34 | id 35 | ip_address 36 | user 37 | user_agent 38 | created_at 39 | updated_at 40 | ].freeze 41 | 42 | # FORM_ATTRIBUTES 43 | # an array of attributes that will be displayed 44 | # on the model's form (`new` and `edit`) pages. 45 | FORM_ATTRIBUTES = %i[ 46 | ip_address 47 | user 48 | user_agent 49 | ].freeze 50 | 51 | # COLLECTION_FILTERS 52 | # a hash that defines filters that can be used while searching via the search 53 | # field of the dashboard. 54 | # 55 | # For example to add an option to search for open resources by typing "open:" 56 | # in the search field: 57 | # 58 | # COLLECTION_FILTERS = { 59 | # open: ->(resources) { resources.where(open: true) } 60 | # }.freeze 61 | COLLECTION_FILTERS = {}.freeze 62 | 63 | # Overwrite this method to customize how sessions are displayed 64 | # across all pages of the admin dashboard. 65 | # 66 | # def display_resource(session) 67 | # "Session ##{session.id}" 68 | # end 69 | end 70 | -------------------------------------------------------------------------------- /app/dashboards/team_dashboard.rb: -------------------------------------------------------------------------------- 1 | require "administrate/base_dashboard" 2 | 3 | class TeamDashboard < Administrate::BaseDashboard 4 | # ATTRIBUTE_TYPES 5 | # a hash that describes the type of each of the model's fields. 6 | # 7 | # Each different type represents an Administrate::Field object, 8 | # which determines how the attribute is displayed 9 | # on pages throughout the dashboard. 10 | ATTRIBUTE_TYPES = { 11 | id: Field::Number, 12 | name: Field::String, 13 | team_users: Field::HasMany, 14 | users: Field::HasMany, 15 | created_at: Field::DateTime, 16 | updated_at: Field::DateTime, 17 | }.freeze 18 | 19 | # COLLECTION_ATTRIBUTES 20 | # an array of attributes that will be displayed on the model's index page. 21 | # 22 | # By default, it's limited to four items to reduce clutter on index pages. 23 | # Feel free to add, remove, or rearrange items. 24 | COLLECTION_ATTRIBUTES = %i[ 25 | id 26 | name 27 | team_users 28 | users 29 | ].freeze 30 | 31 | # SHOW_PAGE_ATTRIBUTES 32 | # an array of attributes that will be displayed on the model's show page. 33 | SHOW_PAGE_ATTRIBUTES = %i[ 34 | id 35 | name 36 | team_users 37 | users 38 | created_at 39 | updated_at 40 | ].freeze 41 | 42 | # FORM_ATTRIBUTES 43 | # an array of attributes that will be displayed 44 | # on the model's form (`new` and `edit`) pages. 45 | FORM_ATTRIBUTES = %i[ 46 | name 47 | team_users 48 | users 49 | ].freeze 50 | 51 | # COLLECTION_FILTERS 52 | # a hash that defines filters that can be used while searching via the search 53 | # field of the dashboard. 54 | # 55 | # For example to add an option to search for open resources by typing "open:" 56 | # in the search field: 57 | # 58 | # COLLECTION_FILTERS = { 59 | # open: ->(resources) { resources.where(open: true) } 60 | # }.freeze 61 | COLLECTION_FILTERS = {}.freeze 62 | 63 | # Overwrite this method to customize how teams are displayed 64 | # across all pages of the admin dashboard. 65 | # 66 | # def display_resource(team) 67 | # "Team ##{team.id}" 68 | # end 69 | end 70 | -------------------------------------------------------------------------------- /app/dashboards/team_user_dashboard.rb: -------------------------------------------------------------------------------- 1 | require "administrate/base_dashboard" 2 | 3 | class TeamUserDashboard < Administrate::BaseDashboard 4 | # ATTRIBUTE_TYPES 5 | # a hash that describes the type of each of the model's fields. 6 | # 7 | # Each different type represents an Administrate::Field object, 8 | # which determines how the attribute is displayed 9 | # on pages throughout the dashboard. 10 | ATTRIBUTE_TYPES = { 11 | id: Field::Number, 12 | role: Field::String, 13 | team: Field::BelongsTo, 14 | user: Field::BelongsTo, 15 | created_at: Field::DateTime, 16 | updated_at: Field::DateTime, 17 | }.freeze 18 | 19 | # COLLECTION_ATTRIBUTES 20 | # an array of attributes that will be displayed on the model's index page. 21 | # 22 | # By default, it's limited to four items to reduce clutter on index pages. 23 | # Feel free to add, remove, or rearrange items. 24 | COLLECTION_ATTRIBUTES = %i[ 25 | id 26 | role 27 | team 28 | user 29 | ].freeze 30 | 31 | # SHOW_PAGE_ATTRIBUTES 32 | # an array of attributes that will be displayed on the model's show page. 33 | SHOW_PAGE_ATTRIBUTES = %i[ 34 | id 35 | role 36 | team 37 | user 38 | created_at 39 | updated_at 40 | ].freeze 41 | 42 | # FORM_ATTRIBUTES 43 | # an array of attributes that will be displayed 44 | # on the model's form (`new` and `edit`) pages. 45 | FORM_ATTRIBUTES = %i[ 46 | role 47 | team 48 | user 49 | ].freeze 50 | 51 | # COLLECTION_FILTERS 52 | # a hash that defines filters that can be used while searching via the search 53 | # field of the dashboard. 54 | # 55 | # For example to add an option to search for open resources by typing "open:" 56 | # in the search field: 57 | # 58 | # COLLECTION_FILTERS = { 59 | # open: ->(resources) { resources.where(open: true) } 60 | # }.freeze 61 | COLLECTION_FILTERS = {}.freeze 62 | 63 | # Overwrite this method to customize how team users are displayed 64 | # across all pages of the admin dashboard. 65 | # 66 | # def display_resource(team_user) 67 | # "TeamUser ##{team_user.id}" 68 | # end 69 | end 70 | -------------------------------------------------------------------------------- /app/dashboards/user_dashboard.rb: -------------------------------------------------------------------------------- 1 | require "administrate/base_dashboard" 2 | 3 | class UserDashboard < Administrate::BaseDashboard 4 | # ATTRIBUTE_TYPES 5 | # a hash that describes the type of each of the model's fields. 6 | # 7 | # Each different type represents an Administrate::Field object, 8 | # which determines how the attribute is displayed 9 | # on pages throughout the dashboard. 10 | ATTRIBUTE_TYPES = { 11 | id: Field::Number, 12 | email: Field::String, 13 | first_name: Field::String, 14 | last_name: Field::String, 15 | sessions: Field::HasMany, 16 | team: Field::BelongsTo, 17 | team_users: Field::HasMany, 18 | teams: Field::HasMany, 19 | verified: Field::Boolean, 20 | admin: Field::Boolean, 21 | created_at: Field::DateTime, 22 | updated_at: Field::DateTime, 23 | }.freeze 24 | 25 | # COLLECTION_ATTRIBUTES 26 | # an array of attributes that will be displayed on the model's index page. 27 | # 28 | # By default, it's limited to four items to reduce clutter on index pages. 29 | # Feel free to add, remove, or rearrange items. 30 | COLLECTION_ATTRIBUTES = %i[ 31 | id 32 | email 33 | first_name 34 | last_name 35 | admin 36 | ].freeze 37 | 38 | # SHOW_PAGE_ATTRIBUTES 39 | # an array of attributes that will be displayed on the model's show page. 40 | SHOW_PAGE_ATTRIBUTES = %i[ 41 | id 42 | email 43 | first_name 44 | last_name 45 | sessions 46 | team 47 | team_users 48 | teams 49 | verified 50 | admin 51 | created_at 52 | updated_at 53 | ].freeze 54 | 55 | # FORM_ATTRIBUTES 56 | # an array of attributes that will be displayed 57 | # on the model's form (`new` and `edit`) pages. 58 | FORM_ATTRIBUTES = %i[ 59 | email 60 | first_name 61 | last_name 62 | team 63 | teams 64 | verified 65 | admin 66 | ].freeze 67 | 68 | # COLLECTION_FILTERS 69 | # a hash that defines filters that can be used while searching via the search 70 | # field of the dashboard. 71 | # 72 | # For example to add an option to search for open resources by typing "open:" 73 | # in the search field: 74 | # 75 | # COLLECTION_FILTERS = { 76 | # open: ->(resources) { resources.where(open: true) } 77 | # }.freeze 78 | COLLECTION_FILTERS = {}.freeze 79 | 80 | # Overwrite this method to customize how users are displayed 81 | # across all pages of the admin dashboard. 82 | # 83 | def display_resource(user) 84 | user.email 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/styles_helper.rb: -------------------------------------------------------------------------------- 1 | module StylesHelper 2 | def page_title(title) 3 | tag.h1 title, class: page_title_classes.render 4 | end 5 | 6 | def page_title_classes 7 | class_variants("font-bold tracking-tight text-gray-800", 8 | variants: { 9 | style: { 10 | md: "text-3xl", 11 | }, 12 | align: { 13 | left: "text-left", 14 | center: "text-center", 15 | } 16 | }, 17 | defaults: { 18 | style: :md, 19 | align: :left 20 | } 21 | ) 22 | end 23 | 24 | def button_classes 25 | class_variants("py-2 px-4 rounded-md md:text-sm font-semibold inline-flex items-center cursor-pointer", 26 | variants: { 27 | style: { 28 | secondary: "bg-white text-gray-600 border border-gray-200 hover:border-gray-500", 29 | primary: "bg-slate-800 hover:bg-opacity-90 text-white" 30 | }, 31 | fullwidth: "w-full justify-center", 32 | nowrap: "whitespace-nowrap" 33 | }, 34 | defaults: { 35 | style: :primary 36 | } 37 | ) 38 | end 39 | 40 | def label_classes 41 | class_variants("block text-sm font-medium text-gray-700") 42 | end 43 | 44 | def input_classes 45 | class_variants("appearance-none block w-full px-3 py-2 border-2 border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:ring-blue-200 focus:border-blue-200 sm:text-sm") 46 | end 47 | 48 | def hint_classes 49 | class_variants("mt-2 text-sm text-gray-500") 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /app/javascript/application.js: -------------------------------------------------------------------------------- 1 | // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails 2 | import "@hotwired/turbo-rails" 3 | import "controllers" 4 | -------------------------------------------------------------------------------- /app/javascript/controllers/application.js: -------------------------------------------------------------------------------- 1 | import { Application } from "@hotwired/stimulus" 2 | 3 | const application = Application.start() 4 | 5 | // Configure Stimulus development experience 6 | application.debug = false 7 | window.Stimulus = application 8 | 9 | export { application } 10 | -------------------------------------------------------------------------------- /app/javascript/controllers/flash_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | // Connects to data-controller="flash" 4 | export default class extends Controller { 5 | connect() { 6 | setTimeout(() => { 7 | this.hideAlert(); 8 | }, 5000); 9 | } 10 | 11 | dismiss(event) { 12 | event.preventDefault(); 13 | event.stopPropagation(); 14 | 15 | this.hideAlert(); 16 | } 17 | 18 | hideAlert() { 19 | this.element.style.display = "none"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/javascript/controllers/index.js: -------------------------------------------------------------------------------- 1 | // Import and register all your controllers from the importmap via controllers/**/*_controller 2 | import { application } from "controllers/application" 3 | import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" 4 | eagerLoadControllersFrom("controllers", application) 5 | -------------------------------------------------------------------------------- /app/javascript/controllers/toggle_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | import { useClickOutside } from "stimulus-use"; 3 | 4 | /* 5 | * Usage 6 | * ===== 7 | * 8 | * add data-controller="toggle" to common ancestor 9 | * 10 | * Action: 11 | * data-action="toggle#toggle" 12 | * 13 | * Targets: 14 | * data-toggle-target="toggleable" data-css-class="class-to-toggle" 15 | * 16 | */ 17 | export default class extends Controller { 18 | static targets = ["toggleable"]; 19 | 20 | connect() { 21 | useClickOutside(this); 22 | } 23 | 24 | toggle(e) { 25 | e.preventDefault(); 26 | 27 | this.toggleableTargets.forEach((target) => { 28 | target.classList.toggle(target.dataset.cssClass); 29 | }); 30 | } 31 | 32 | clickOutside(event) { 33 | if (this.data.get("clickOutside") === "add") { 34 | this.toggleableTargets.forEach((target) => { 35 | target.classList.add(target.dataset.cssClass); 36 | }); 37 | } else if (this.data.get("clickOutside") === "remove") { 38 | this.toggleableTargets.forEach((target) => { 39 | target.classList.remove(target.dataset.cssClass); 40 | }); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | # Automatically retry jobs that encountered a deadlock 3 | # retry_on ActiveRecord::Deadlocked 4 | 5 | # Most jobs are safe to ignore if the underlying records are no longer available 6 | # discard_on ActiveJob::DeserializationError 7 | end 8 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: "from@example.com" 3 | layout "mailer" 4 | end 5 | -------------------------------------------------------------------------------- /app/mailers/session_mailer.rb: -------------------------------------------------------------------------------- 1 | class SessionMailer < ApplicationMailer 2 | def signed_in_notification 3 | @session = params[:session] 4 | mail to: @session.user.email, subject: "New sign-in to your account" 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/mailers/user_mailer.rb: -------------------------------------------------------------------------------- 1 | class UserMailer < ApplicationMailer 2 | def password_reset 3 | @user = params[:user] 4 | @signed_id = @user.password_reset_tokens.create.signed_id(expires_in: 20.minutes) 5 | 6 | mail to: @user.email, subject: "Reset your password" 7 | end 8 | 9 | def email_verification 10 | @user = params[:user] 11 | @signed_id = @user.email_verification_tokens.create.signed_id(expires_in: 2.days) 12 | 13 | mail to: @user.email, subject: "Verify your email" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | primary_abstract_class 3 | end 4 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapidruby/rapid-ruby-starter/8b6700c7f9e974da31c728d7da167fffd2939faa/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/concerns/avatarable.rb: -------------------------------------------------------------------------------- 1 | module Avatarable 2 | extend ActiveSupport::Concern 3 | 4 | def letters_svg(size: 32, font_size: 14, extra_classes: "rounded-full h-8 w-8 text-gray-600") 5 | letters = name.split.map(&:first).join.upcase[0..1] if name.is_a?(String) 6 | [ 7 | '', 8 | "", 9 | " ", 10 | " ", 11 | " #{letters}", 12 | " ", 13 | "" 14 | ].join("\n").html_safe 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/models/current.rb: -------------------------------------------------------------------------------- 1 | class Current < ActiveSupport::CurrentAttributes 2 | attribute :session, :user 3 | attribute :user_agent, :ip_address 4 | 5 | def session=(session) 6 | super; self.user = session.user 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/models/email_verification_token.rb: -------------------------------------------------------------------------------- 1 | class EmailVerificationToken < ApplicationRecord 2 | belongs_to :user 3 | end 4 | -------------------------------------------------------------------------------- /app/models/password_reset_token.rb: -------------------------------------------------------------------------------- 1 | class PasswordResetToken < ApplicationRecord 2 | belongs_to :user 3 | end 4 | -------------------------------------------------------------------------------- /app/models/session.rb: -------------------------------------------------------------------------------- 1 | class Session < ApplicationRecord 2 | belongs_to :user 3 | belongs_to :admin_user, class_name: "User", optional: true 4 | 5 | before_create do 6 | self.user_agent = Current.user_agent 7 | self.ip_address = Current.ip_address 8 | end 9 | 10 | after_create_commit do 11 | SessionMailer.with(session: self).signed_in_notification.deliver_later 12 | end 13 | 14 | def masquerade_as!(other_user) 15 | self.masquerade_at = Time.current 16 | self.admin_user = user 17 | self.user = other_user 18 | save! 19 | end 20 | 21 | def masquerading? 22 | admin_user.present? 23 | end 24 | 25 | def reverse_masquerade! 26 | return unless masquerading? 27 | 28 | self.masquerade_at = nil 29 | self.user = admin_user 30 | self.admin_user = nil 31 | save! 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/models/team.rb: -------------------------------------------------------------------------------- 1 | class Team < ApplicationRecord 2 | has_many :team_users, dependent: :destroy 3 | has_many :users, through: :team_users 4 | end 5 | -------------------------------------------------------------------------------- /app/models/team_user.rb: -------------------------------------------------------------------------------- 1 | class TeamUser < ApplicationRecord 2 | belongs_to :team 3 | belongs_to :user 4 | end 5 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ApplicationRecord 2 | include Avatarable 3 | 4 | attribute :terms_and_conditions 5 | 6 | has_secure_password 7 | 8 | belongs_to :team 9 | has_many :team_users, dependent: :destroy 10 | has_many :teams, through: :team_users 11 | 12 | has_many :email_verification_tokens, dependent: :destroy 13 | has_many :password_reset_tokens, dependent: :destroy 14 | has_many :sessions, dependent: :destroy 15 | 16 | has_one_attached :avatar do |attachable| 17 | attachable.variant :thumb, resize_to_limit: [64, 64] 18 | attachable.variant :medium, resize_to_limit: [192, 192] 19 | end 20 | 21 | validates :first_name, :last_name, presence: true 22 | validates :email, disposable_email: true, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP } 23 | validates :password, allow_nil: true, length: { minimum: 8 }, format: { with: /(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])/ } 24 | validates :terms_and_conditions, acceptance: true, on: :create 25 | 26 | before_validation if: -> { email.present? } do 27 | self.email = email.downcase.strip 28 | end 29 | 30 | before_validation if: :email_changed?, unless: :new_record? do 31 | self.verified = false 32 | end 33 | 34 | after_update if: :password_digest_previously_changed? do 35 | sessions.where.not(id: Current.session).destroy_all 36 | end 37 | 38 | def name 39 | [first_name, last_name].compact.join(" ") 40 | end 41 | 42 | def obfuscated_name 43 | if last_name.present? 44 | "#{first_name} #{last_name.chars.first}." 45 | else 46 | first_name 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /app/services/disposable_email_service.rb: -------------------------------------------------------------------------------- 1 | module DisposableEmailService 2 | module_function 3 | 4 | def disposable?(email_address) 5 | email = Mail::Address.new(email_address.downcase) rescue nil 6 | 7 | if email 8 | disposable_email_domains.include?(email.domain) 9 | else 10 | false 11 | end 12 | end 13 | 14 | def disposable_email_domains 15 | @_disposable_email_domains ||= File.readlines(Rails.root.join("data", "disposable_email_domains.txt")).map(&:strip) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/services/markdown_service.rb: -------------------------------------------------------------------------------- 1 | require "redcarpet" 2 | 3 | module MarkdownService 4 | module_function 5 | 6 | class HTML < Redcarpet::Render::HTML; end 7 | 8 | def to_html(text) 9 | return "" if text.nil? 10 | 11 | raise ArgumentError, "Input must be a string or nil" unless text.is_a?(String) 12 | 13 | renderer = HTML.new({ 14 | # filer_html: true, 15 | hard_wrap: true, 16 | link_attributes: { rel: "nofollow noreferer noopener", target: "_blank" }, 17 | prettify: true 18 | }) 19 | 20 | extras = { 21 | autolink: true, 22 | no_intra_emphasis: true, 23 | disable_indented_code_blocks: true, 24 | fenced_code_blocks: true, 25 | strikethrough: true, 26 | superscript: true, 27 | lax_spacing: true 28 | } 29 | 30 | markdown = Redcarpet::Markdown.new(renderer, extras) 31 | markdown.render(text).html_safe 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/validators/disposable_email_validator.rb: -------------------------------------------------------------------------------- 1 | class DisposableEmailValidator < ActiveModel::EachValidator 2 | def validate_each(record, attribute, value) 3 | if DisposableEmailService.disposable?(value) 4 | record.errors.add(attribute, (options[:message] || "is a disposable email address and is banned")) 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/views/admin/application/_navigation.html.erb: -------------------------------------------------------------------------------- 1 | <%# 2 | # Navigation 3 | 4 | This partial is used to display the navigation in Administrate. 5 | By default, the navigation contains navigation links 6 | for all resources in the admin dashboard, 7 | as defined by the routes in the `admin/` namespace 8 | %> 9 | 10 | 26 | -------------------------------------------------------------------------------- /app/views/admin/users/show.html.erb: -------------------------------------------------------------------------------- 1 | <%# 2 | # Show 3 | 4 | This view is the template for the show page. 5 | It renders the attributes of a resource, 6 | as well as a link to its edit page. 7 | 8 | ## Local variables: 9 | 10 | - `page`: 11 | An instance of [Administrate::Page::Show][1]. 12 | Contains methods for accessing the resource to be displayed on the page, 13 | as well as helpers for describing how each attribute of the resource 14 | should be displayed. 15 | 16 | [1]: http://www.rubydoc.info/gems/administrate/Administrate/Page/Show 17 | %> 18 | 19 | <% content_for(:title) { t("administrate.actions.show_resource", name: page.page_title) } %> 20 | 21 |
22 |

23 | <%= content_for(:title) %> 24 |

25 | 26 |
27 | <%= button_to "Login as #{page.resource.first_name}", masquerade_admin_user_path(page.resource), method: :post, class: "button" %> 28 | 29 | <%= link_to( 30 | t("administrate.actions.edit_resource", name: page.page_title), 31 | [:edit, namespace, page.resource], 32 | class: "button", 33 | ) if accessible_action?(page.resource, :edit) %> 34 | 35 | <%= link_to( 36 | t("administrate.actions.destroy"), 37 | [namespace, page.resource], 38 | class: "button button--danger", 39 | method: :delete, 40 | data: { confirm: t("administrate.actions.confirm") } 41 | ) if accessible_action?(page.resource, :destroy) %> 42 |
43 |
44 | 45 |
46 |
47 | <% page.attributes.each do |attribute| %> 48 |
49 | <%= t( 50 | "helpers.label.#{resource_name}.#{attribute.name}", 51 | default: page.resource.class.human_attribute_name(attribute.name), 52 | ) %> 53 |
54 | 55 |
<%= render_field attribute, page: page %>
57 | <% end %> 58 |
59 |
60 | -------------------------------------------------------------------------------- /app/views/identity/account/show.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%= tag.h1 "Manage your account", class: page_title_classes.render %> 4 | 5 |
6 |
7 | <%= render(FieldsetHeaderComponent.new( 8 | title: "Personal Information", 9 | description: "Manage your personal info and change the avatar associated with your account." 10 | )) %> 11 | 12 | <%= form_with(url: identity_account_path, model: @user, method: :patch, html: { class: "md:col-span-2" }) do |f| %> 13 |
14 |
15 | <%= render(AvatarComponent.new(user: Current.user, css_classes: "rounded-lg")) %> 16 |
17 | <%= f.label :avatar, class: label_classes.render %> 18 |
19 | <%= f.file_field :avatar, accept: "image/jpg,image/jpeg,image/png,image/gif" %> 20 |

JPG, GIF or PNG. 1MB max.

21 |
22 |
23 |
24 | 25 |
26 | <%= f.label :first_name, class: label_classes.render %> 27 |
28 | <%= f.text_field :first_name, autocomplete: "given-name", class: input_classes.render %> 29 |
30 |
31 | 32 |
33 | <%= f.label :last_name, class: label_classes.render %> 34 |
35 | <%= f.text_field :last_name, autocomplete: "family-name", class: input_classes.render %> 36 |
37 |
38 | 39 |
40 | <%= f.label :email, class: label_classes.render %> 41 |
42 | <%= f.text_field :email, disabled: true, class: input_classes.render %> 43 | You can change your email address using the form below… 44 |
45 |
46 |
47 | 48 |
49 | <%= f.submit "Save changes", class: button_classes.render %> 50 |
51 | <% end %> 52 |
53 | 54 |
55 | <%= render(FieldsetHeaderComponent.new( 56 | title: Current.user.verified? ? "Change email" : "Verify your email" 57 | )) do %> 58 | <% if Current.user.verified? %> 59 |

Update the email address associated with your account.

60 | <% else %> 61 |

We've sent you an email to <%= Current.user.email %> to verify your account. Click the link in the email to verify your account.

62 |

<%= button_to "Re-send verification email", identity_email_verification_path, class: button_classes.render(style: :secondary) %>

63 | <% end %> 64 | <% end %> 65 | 66 |
67 | <%= render partial: "identity/emails/form" %> 68 |
69 |
70 | 71 |
72 | <%= render(FieldsetHeaderComponent.new( 73 | title: "Change password", 74 | description: "Update your password associated with your account." 75 | )) %> 76 | 77 |
78 | <%= render partial: "passwords/form" %> 79 |
80 |
81 | 82 |
83 | <%= render(FieldsetHeaderComponent.new( 84 | title: "Log out other sessions", 85 | description: "You can manage your sessions to log out across any of your other devices." 86 | )) %> 87 | 88 |
89 | <%= link_to "Manage devices and sessions", sessions_path, class: button_classes.render(style: :secondary) %> 90 |
91 |
92 | 93 | <% if false %> 94 |
95 | <%= render(FieldsetHeaderComponent.new( 96 | title: "Delete account", 97 | description: "No longer want to use our service? You can delete your account here. This action is not reversible. All information related to this account will be deleted permanently." 98 | )) %> 99 | 100 |
101 | 102 |
103 |
104 | <% end %> 105 |
106 |
107 |
108 | -------------------------------------------------------------------------------- /app/views/identity/emails/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with(url: identity_email_path, method: :patch, id: "change_email_form") do |form| %> 2 | <%= render "shared/form_errors", resource: @user %> 3 | 4 |
5 | <%= form.label :email, "New email", class: label_classes.render %> 6 |
<%= form.email_field :email, class: input_classes.render %>
7 |
8 | 9 |
10 | <%= form.submit "Save changes", class: button_classes.render %> 11 |
12 | <% end %> 13 | -------------------------------------------------------------------------------- /app/views/identity/password_resets/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Reset your password" %> 2 | 3 |
4 |
5 | <%= tag.h1 "Reset your password", class: page_title_classes.render %> 6 |
7 | 8 | <%= form_with(url: identity_password_reset_path, method: :patch) do |form| %> 9 | <%= render "shared/form_errors", resource: @user %> 10 | 11 | <%= form.hidden_field :sid, value: params[:sid] %> 12 | 13 |
14 | <%= form.label :password, "New password", style: "display: block" %> 15 |
<%= form.password_field :password, required: true, autofocus: true, autocomplete: "new-password", class: input_classes.render %>
16 | <%= tag.span "8 characters minimum.", class: hint_classes.render %> 17 |
18 | 19 |
20 | <%= form.label :password_confirmation, "Confirm new password", style: "display: block" %> 21 |
<%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", class: input_classes.render %>
22 |
23 | 24 |
25 | <%= form.submit "Save changes", class: button_classes.render(fullwidth: true) %> 26 |
27 | <% end %> 28 |
29 | -------------------------------------------------------------------------------- /app/views/identity/password_resets/new.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Forgot your password?" %> 2 | 3 |
4 |
5 | <%= tag.h1 "Forgot your password?", class: page_title_classes.render %> 6 |
7 | 8 | <%= form_with(url: identity_password_reset_path) do |form| %> 9 |
10 | <%= form.label :email, class: label_classes.render %> 11 |
<%= form.email_field :email, autofocus: true, required: true, class: input_classes.render %>
12 |
13 | 14 |
15 | <%= form.submit "Send password reset email", class: button_classes.render(fullwidth: true) %> 16 |
17 | <% end %> 18 |
19 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= render partial: "layouts/partials/head" %> 5 | 6 | 7 | 8 | <%= render "shared/flash_alerts" %> 9 | 10 |
11 | 37 |
38 | 39 |
40 | <%= render Admin::MasqueradeComponent.new(session: Current.session) %> 41 | 42 | <%= yield %> 43 |
44 | 45 | 46 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /app/views/layouts/partials/_head.html.erb: -------------------------------------------------------------------------------- 1 | <%= display_meta_tags( 2 | reverse: true, 3 | site: "Rapid Ruby Starter" 4 | ) %> 5 | 6 | <%= csrf_meta_tags %> 7 | <%= csp_meta_tag %> 8 | <%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %> 9 | 10 | <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> 11 | <%= javascript_importmap_tags %> 12 | -------------------------------------------------------------------------------- /app/views/lessons/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= page_title("Lessons") %> 3 | 4 |

Find me in app/views/lessons/index.html.erb

5 |
6 | -------------------------------------------------------------------------------- /app/views/pages/home.html.erb: -------------------------------------------------------------------------------- 1 | <% set_meta_tags title: "Home" %> 2 | 3 |
4 | <%= page_title("Rapid Ruby Starter") %> 5 |

6 | A starter rails template setup with some defaults and a few features to get you going. 7 |

8 | 9 | 14 |
15 | -------------------------------------------------------------------------------- /app/views/passwords/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with(url: password_path, method: :patch, id: "change_password_form") do |form| %> 2 | <%= render "shared/form_errors", resource: @user %> 3 | 4 |
5 | <%= form.label :current_password, class: label_classes.render %> 6 |
<%= form.password_field :current_password, required: true, autocomplete: "current-password", class: input_classes.render %>
7 |
8 | 9 |
10 | <%= form.label :password, "New password", class: label_classes.render %> 11 |
<%= form.password_field :password, required: true, autocomplete: "new-password", class: input_classes.render %>
12 | <%= tag.span "8 characters minimum.", class: hint_classes.render %> 13 |
14 | 15 |
16 | <%= form.label :password_confirmation, "Confirm new password", class: label_classes.render %> 17 |
<%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", class: input_classes.render %>
18 |
19 | 20 |
21 | <%= form.submit "Save changes", class: button_classes.render %> 22 |
23 | <% end %> 24 | -------------------------------------------------------------------------------- /app/views/registrations/new.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%= tag.h1 "Sign up", class: page_title_classes.render %> 4 |

5 | Already have an account? 6 | <%= link_to "Sign in", sign_in_path, class: "font-medium text-red-600 hover:text-red-700" %> 7 |

8 |
9 | 10 | <%= form_with(url: sign_up_path, model: @user, scope: "") do |form| %> 11 | <%= render "shared/form_errors", resource: @user %> 12 | 13 |
14 |
15 | <%= form.label :first_name, class: label_classes.render %> 16 |
17 | <%= form.text_field :first_name, autofocus: true, autocomplete: "given-name", class: input_classes.render %> 18 |
19 |
20 | 21 | 22 |
23 | <%= form.label :last_name, class: label_classes.render %> 24 |
25 | <%= form.text_field :last_name, autocomplete: "family-name", class: input_classes.render %> 26 |
27 |
28 |
29 | 30 | 31 |
32 | <%= form.label :email, class: label_classes.render %> 33 |
34 | <%= form.email_field :email, autocomplete: "email", class: input_classes.render %> 35 |
36 |
37 | 38 |
39 | <%= form.label :password, class: label_classes.render %> 40 |
41 | <%= form.password_field :password, autocomplete: "new-password", class: input_classes.render %> 42 |
43 | <%= tag.span "8 characters minimum.", class: hint_classes.render %> 44 |
45 | 46 |
47 | <%= form.label :password_confirmation, class: label_classes.render %> 48 |
49 | <%= form.password_field :password_confirmation, autocomplete: "new-password", class: input_classes.render %> 50 |
51 |
52 | 53 |
54 | <%= form.label :terms_and_conditions do %> 55 |
56 | <%= form.check_box :terms_and_conditions, class: "mt-0.5 mr-3 focus:ring-indigo-500 h-4 w-4 text-red-600 border-gray-300 rounded" %> 57 | 58 | I have read and agreee to the <%= link_to "Terms and Conditions", "#", target: "_blank", class: "underline text-gray-900" %> and <%= link_to "Privacy Policy", "#", target: "_blank", class: "underline text-gray-900" %> 59 | 60 |
61 | <% end %> 62 |
63 | 64 |
65 | <%= form.submit "Sign up", class: button_classes.render(fullwidth: true) %> 66 |
67 | <% end %> 68 |
69 | -------------------------------------------------------------------------------- /app/views/session_mailer/signed_in_notification.html.erb: -------------------------------------------------------------------------------- 1 |

Hey there,

2 | 3 |

A new device just signed in to your account (<%= @session.user.email %>).

4 | 5 |

6 | <%= @session.user_agent %> 7 |
8 | <%= @session.created_at %> 9 |
10 | IP address: <%= @session.ip_address %> 11 |

12 | 13 |

If this was you, carry on. We could notify you about sign-ins from this device again.

14 | 15 |

If you don't recognize this device, someone else may have accessed your account. You should immediately <%= link_to "change your password", new_identity_password_reset_url %>.

16 | 17 |

Tip: It's a good idea to periodically review all of the <%= link_to "devices and sessions", sessions_url %> in your account for suspicious activity.

18 | 19 |
20 | 21 |

Have questions or need help? Just reply to this email and our support team will help you sort it out.

22 | -------------------------------------------------------------------------------- /app/views/sessions/index.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Devices & Sessions" %> 2 | 3 | <%= tag.h1 "Devices & Sessions", class: page_title_classes.render %> 4 | 5 |
6 | <% @sessions.each do |session| %> 7 |
8 |
9 |

10 | User Agent: 11 | <%= session.user_agent %> 12 |

13 | 14 |

15 | Ip Address: 16 | <%= session.ip_address %> 17 |

18 | 19 |

20 | Created at: 21 | <%= session.created_at %> 22 |

23 |
24 | <%= button_to "Log out", session, method: :delete, class: button_classes.render(style: :secondary) %> 25 |
26 | <% end %> 27 |
28 | 29 |
30 | 31 |
32 | <%= link_to "Back", root_path %> 33 |
34 | -------------------------------------------------------------------------------- /app/views/sessions/new.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%= tag.h1 "Login", class: page_title_classes.render %> 4 |

5 | Need have an account? 6 | <%= link_to "Get started", sign_in_path, class: "font-medium text-red-600 hover:text-red-700" %> 7 |

8 |
9 | 10 | <%= form_with(url: sign_in_path) do |form| %> 11 |
12 | <%= form.label :email, class: label_classes.render %> 13 |
<%= form.email_field :email, value: params[:email_hint], autofocus: true, required: true, autocomplete: "email", class: input_classes.render %>
14 |
15 | 16 |
17 | <%= form.label :password, class: label_classes.render %> 18 |
<%= form.password_field :password, required: true, autocomplete: "current-password", class: input_classes.render %>
19 |
20 | 21 |
22 | <%= form.submit "Sign in", class: button_classes.render(fullwidth: true) %> 23 |
24 | <% end %> 25 | 26 |
27 |
28 |
29 |
30 |
31 | Or 32 |
33 |
34 | 35 |
36 | <%= link_to "Forgot your password?", new_identity_password_reset_path %> 37 |
38 |
39 | -------------------------------------------------------------------------------- /app/views/shared/_flash_alerts.html.erb: -------------------------------------------------------------------------------- 1 | <% flash.each do |name, msg| %> 2 | <% if msg.is_a?(String) %> 3 |
4 |
5 | <%= content_tag :div, msg, :id => "flash_#{name}", class: "flex-grow" %> 6 | 7 | × 8 | 9 |
10 |
11 | <% end %> 12 | <% end %> 13 | -------------------------------------------------------------------------------- /app/views/shared/_form_errors.html.erb: -------------------------------------------------------------------------------- 1 | <% if resource.errors.any? %> 2 |
3 |

4 | There are <%= pluralize(resource.errors.count, "error") %> with this form: 5 |

6 | 11 |
12 | <% end %> 13 | -------------------------------------------------------------------------------- /app/views/user_mailer/email_verification.html.erb: -------------------------------------------------------------------------------- 1 |

Hey there,

2 | 3 |

This is to confirm that <%= @user.email %> is the email you want to use on your account. If you ever lose your password, that's where we'll email a reset link.

4 | 5 |

You must hit the link below to confirm that you received this email.

6 | 7 | <%= link_to "Yes, use this email for my account", edit_identity_email_verification_url(sid: @signed_id) %> 8 | 9 |
10 | 11 |

Have questions or need help? Just reply to this email and our support team will help you sort it out.

12 | -------------------------------------------------------------------------------- /app/views/user_mailer/password_reset.html.erb: -------------------------------------------------------------------------------- 1 |

Hey there,

2 | 3 |

Can't remember your password for <%= @user.email %>? That's OK, it happens. Just hit the link below to set a new one.

4 | 5 |

<%= link_to "Reset my password", edit_identity_password_reset_url(sid: @signed_id) %>

6 | 7 |

If you did not request a password reset you can safely ignore this email, it expires in 20 minutes. Only someone with access to this email account can reset your password.

8 | 9 |
10 | 11 |

Have questions or need help? Just reply to this email and our support team will help you sort it out.

12 | -------------------------------------------------------------------------------- /bin/brakeman: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | ARGV.unshift("--ensure-latest") 6 | 7 | load Gem.bin_path("brakeman", "brakeman") 8 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'bundle' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "rubygems" 12 | 13 | m = Module.new do 14 | module_function 15 | 16 | def invoked_as_script? 17 | File.expand_path($0) == File.expand_path(__FILE__) 18 | end 19 | 20 | def env_var_version 21 | ENV["BUNDLER_VERSION"] 22 | end 23 | 24 | def cli_arg_version 25 | return unless invoked_as_script? # don't want to hijack other binstubs 26 | return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` 27 | bundler_version = nil 28 | update_index = nil 29 | ARGV.each_with_index do |a, i| 30 | if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN 31 | bundler_version = a 32 | end 33 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ 34 | bundler_version = $1 35 | update_index = i 36 | end 37 | bundler_version 38 | end 39 | 40 | def gemfile 41 | gemfile = ENV["BUNDLE_GEMFILE"] 42 | return gemfile if gemfile && !gemfile.empty? 43 | 44 | File.expand_path("../../Gemfile", __FILE__) 45 | end 46 | 47 | def lockfile 48 | lockfile = 49 | case File.basename(gemfile) 50 | when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) 51 | else "#{gemfile}.lock" 52 | end 53 | File.expand_path(lockfile) 54 | end 55 | 56 | def lockfile_version 57 | return unless File.file?(lockfile) 58 | lockfile_contents = File.read(lockfile) 59 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ 60 | Regexp.last_match(1) 61 | end 62 | 63 | def bundler_requirement 64 | @bundler_requirement ||= 65 | env_var_version || cli_arg_version || 66 | bundler_requirement_for(lockfile_version) 67 | end 68 | 69 | def bundler_requirement_for(version) 70 | return "#{Gem::Requirement.default}.a" unless version 71 | 72 | bundler_gem_version = Gem::Version.new(version) 73 | 74 | requirement = bundler_gem_version.approximate_recommendation 75 | 76 | return requirement unless Gem.rubygems_version < Gem::Version.new("2.7.0") 77 | 78 | requirement += ".a" if bundler_gem_version.prerelease? 79 | 80 | requirement 81 | end 82 | 83 | def load_bundler! 84 | ENV["BUNDLE_GEMFILE"] ||= gemfile 85 | 86 | activate_bundler 87 | end 88 | 89 | def activate_bundler 90 | gem_error = activation_error_handling do 91 | gem "bundler", bundler_requirement 92 | end 93 | return if gem_error.nil? 94 | require_error = activation_error_handling do 95 | require "bundler/version" 96 | end 97 | return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) 98 | warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" 99 | exit 42 100 | end 101 | 102 | def activation_error_handling 103 | yield 104 | nil 105 | rescue StandardError, LoadError => e 106 | e 107 | end 108 | end 109 | 110 | m.load_bundler! 111 | 112 | if m.invoked_as_script? 113 | load Gem.bin_path("bundler", "bundle") 114 | end 115 | -------------------------------------------------------------------------------- /bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | if ! gem list foreman -i --silent; then 4 | echo "Installing foreman..." 5 | gem install foreman 6 | fi 7 | 8 | exec foreman start -f Procfile.dev "$@" 9 | -------------------------------------------------------------------------------- /bin/importmap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative "../config/application" 4 | require "importmap/commands" 5 | -------------------------------------------------------------------------------- /bin/jobs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative "../config/environment" 4 | require "solid_queue/cli" 5 | 6 | SolidQueue::Cli.start(ARGV) 7 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rspec' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("rspec-core", "rspec") 30 | -------------------------------------------------------------------------------- /bin/rubocop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | # explicit rubocop config increases performance slightly while avoiding config confusion. 6 | ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) 7 | 8 | load Gem.bin_path("rubocop", "rubocop") 9 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | APP_ROOT = File.expand_path("..", __dir__) 5 | 6 | def system!(*args) 7 | system(*args, exception: true) 8 | end 9 | 10 | FileUtils.chdir APP_ROOT do 11 | # This script is a way to set up or update your development environment automatically. 12 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 13 | # Add necessary setup steps to this file. 14 | 15 | puts "== Installing dependencies ==" 16 | system("bundle check") || system!("bundle install") 17 | 18 | # puts "\n== Copying sample files ==" 19 | # unless File.exist?("config/database.yml") 20 | # FileUtils.cp "config/database.yml.sample", "config/database.yml" 21 | # end 22 | 23 | puts "\n== Preparing database ==" 24 | system! "bin/rails db:prepare" 25 | 26 | puts "\n== Removing old logs and tempfiles ==" 27 | system! "bin/rails log:clear tmp:clear" 28 | 29 | unless ARGV.include?("--skip-server") 30 | puts "\n== Starting development server ==" 31 | STDOUT.flush # flush the output before exec(2) so that it displays 32 | exec "bin/dev" 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /bin/thrust: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | load Gem.bin_path("thruster", "thrust") 6 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "rails" 4 | # Pick the frameworks you want: 5 | require "active_model/railtie" 6 | require "active_job/railtie" 7 | require "active_record/railtie" 8 | require "active_storage/engine" 9 | require "action_controller/railtie" 10 | require "action_mailer/railtie" 11 | require "action_mailbox/engine" 12 | require "action_text/engine" 13 | require "action_view/railtie" 14 | require "action_cable/engine" 15 | # require "rails/test_unit/railtie" 16 | 17 | # Require the gems listed in Gemfile, including any gems 18 | # you've limited to :test, :development, or :production. 19 | Bundler.require(*Rails.groups) 20 | 21 | module RapidRubyStarter 22 | class Application < Rails::Application 23 | # Initialize configuration defaults for originally generated Rails version. 24 | config.load_defaults 7.2 25 | 26 | # Please, add to the `ignore` list any other `lib` subdirectories that do 27 | # not contain `.rb` files, or that should not be reloaded or eager loaded. 28 | # Common ones are `templates`, `generators`, or `middleware`, for example. 29 | config.autoload_lib(ignore: %w[assets tasks]) 30 | 31 | # Configuration for the application, engines, and railties goes here. 32 | # 33 | # These settings can be overridden in specific environments using the files 34 | # in config/environments, which are processed later. 35 | # 36 | # config.time_zone = "Central Time (US & Canada)" 37 | # config.eager_load_paths << Rails.root.join("extras") 38 | 39 | # Set Active::Job backend 40 | config.active_job.queue_adapter = :solid_queue 41 | config.mission_control.jobs.base_controller_class = "Admin::BaseController" 42 | config.active_storage.queues.analysis = :default 43 | config.active_storage.queues.purge = :default 44 | config.active_storage.queues.mirror = :default 45 | 46 | config.generators do |g| 47 | g.skip_routes true 48 | g.helper false 49 | g.assets false 50 | g.test_framework :rspec, fixture: false 51 | g.helper_specs false 52 | g.controller_specs false 53 | g.system_tests false 54 | g.view_specs false 55 | end 56 | 57 | # GZip all responses 58 | config.middleware.use Rack::Deflater 59 | 60 | # Fix tailwind/administrate issue 61 | config.assets.css_compressor = nil 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | require "bootsnap/setup" # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | # Async adapter only works within the same process, so for manually triggering cable updates from a console, 2 | # and seeing results in the browser, you must do so from the web console (running inside the dev process), 3 | # not a terminal started via bin/rails console! Add "console" to any action or any ERB template view 4 | # to make the web console appear. 5 | development: 6 | adapter: solid_cable 7 | polling_interval: 0.1.seconds 8 | message_retention: 1.day 9 | 10 | test: 11 | adapter: test 12 | 13 | production: 14 | adapter: solid_cable 15 | polling_interval: 0.1.seconds 16 | message_retention: 1.day 17 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite. Versions 3.8.0 and up are supported. 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem "sqlite3" 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Make code changes take effect immediately without server restart. 7 | config.enable_reloading = true 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable server timing. 16 | config.server_timing = true 17 | 18 | # Enable/disable Action Controller caching. By default Action Controller caching is disabled. 19 | # Run rails dev:cache to toggle Action Controller caching. 20 | if Rails.root.join("tmp/caching-dev.txt").exist? 21 | config.action_controller.perform_caching = true 22 | config.action_controller.enable_fragment_cache_logging = true 23 | config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" } 24 | else 25 | config.action_controller.perform_caching = false 26 | end 27 | 28 | # Change to :null_store to avoid any caching. 29 | config.cache_store = :memory_store 30 | 31 | # Store uploaded files on the local file system (see config/storage.yml for options). 32 | config.active_storage.service = :local 33 | 34 | # Don't care if the mailer can't send. 35 | config.action_mailer.raise_delivery_errors = false 36 | 37 | # Make template changes take effect immediately. 38 | config.action_mailer.perform_caching = false 39 | 40 | # Set localhost to be used by links generated in mailer templates. 41 | config.action_mailer.default_url_options = { host: "localhost", port: 3000 } 42 | 43 | # Print deprecation notices to the Rails logger. 44 | config.active_support.deprecation = :log 45 | 46 | # Raise an error on page load if there are pending migrations. 47 | config.active_record.migration_error = :page_load 48 | 49 | # Highlight code that triggered database queries in logs. 50 | config.active_record.verbose_query_logs = true 51 | 52 | # Append comments with runtime information tags to SQL queries in logs. 53 | config.active_record.query_log_tags_enabled = true 54 | 55 | # Highlight code that enqueued background job in logs. 56 | config.active_job.verbose_enqueue_logs = true 57 | 58 | # Raises error for missing translations. 59 | # config.i18n.raise_on_missing_translations = true 60 | 61 | # Annotate rendered view with file names. 62 | config.action_view.annotate_rendered_view_with_filenames = true 63 | 64 | # Uncomment if you wish to allow Action Cable access from any origin. 65 | # config.action_cable.disable_request_forgery_protection = true 66 | 67 | # Raise error when a before_action's only/except options reference missing actions. 68 | config.action_controller.raise_on_missing_callback_actions = true 69 | 70 | config.action_mailer.delivery_method = :letter_opener 71 | 72 | # Hotwire Spark 73 | config.hotwire.spark.enabled = true 74 | config.hotwire.spark.logging = true 75 | config.hotwire.spark.html_reload_method = :morph 76 | config.hotwire.spark.html_paths << "app/components" 77 | end 78 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Code is not reloaded between requests. 7 | config.enable_reloading = false 8 | 9 | # Eager load code on boot for better performance and memory savings (ignored by Rake tasks). 10 | config.eager_load = true 11 | 12 | # Full error reports are disabled. 13 | config.consider_all_requests_local = false 14 | 15 | # Turn on fragment caching in view templates. 16 | config.action_controller.perform_caching = true 17 | 18 | # Cache assets for far-future expiry since they are all digest stamped. 19 | config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" } 20 | 21 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 22 | # config.asset_host = "http://assets.example.com" 23 | 24 | # Store uploaded files on the local file system (see config/storage.yml for options). 25 | config.active_storage.service = :local 26 | 27 | # Assume all access to the app is happening through a SSL-terminating reverse proxy. 28 | config.assume_ssl = true 29 | 30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 31 | config.force_ssl = true 32 | 33 | # Skip http-to-https redirect for the default health check endpoint. 34 | # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } 35 | 36 | # Log to STDOUT with the current request id as a default log tag. 37 | config.log_tags = [ :request_id ] 38 | config.logger = ActiveSupport::TaggedLogging.logger(STDOUT) 39 | 40 | # Change to "debug" to log everything (including potentially personally-identifiable information!) 41 | config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") 42 | 43 | # Prevent health checks from clogging up the logs. 44 | config.silence_healthcheck_path = "/up" 45 | 46 | # Don't log any deprecations. 47 | config.active_support.report_deprecations = false 48 | 49 | # Replace the default in-process memory cache store with a durable alternative. 50 | # config.cache_store = :mem_cache_store 51 | 52 | # Replace the default in-process and non-durable queuing backend for Active Job. 53 | # config.active_job.queue_adapter = :resque 54 | 55 | # Ignore bad email addresses and do not raise email delivery errors. 56 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 57 | # config.action_mailer.raise_delivery_errors = false 58 | 59 | # Set host to be used by links generated in mailer templates. 60 | config.action_mailer.default_url_options = { host: "example.com" } 61 | 62 | # Specify outgoing SMTP server. Remember to add smtp/* credentials via rails credentials:edit. 63 | # config.action_mailer.smtp_settings = { 64 | # user_name: Rails.application.credentials.dig(:smtp, :user_name), 65 | # password: Rails.application.credentials.dig(:smtp, :password), 66 | # address: "smtp.example.com", 67 | # port: 587, 68 | # authentication: :plain 69 | # } 70 | 71 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 72 | # the I18n.default_locale when a translation cannot be found). 73 | config.i18n.fallbacks = true 74 | 75 | # Do not dump schema after migrations. 76 | config.active_record.dump_schema_after_migration = false 77 | 78 | # Only use :id for inspections in production. 79 | config.active_record.attributes_for_inspect = [ :id ] 80 | 81 | # Enable DNS rebinding protection and other `Host` header attacks. 82 | # config.hosts = [ 83 | # "example.com", # Allow requests from example.com 84 | # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` 85 | # ] 86 | # 87 | # Skip DNS rebinding protection for the default health check endpoint. 88 | # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } 89 | end 90 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # The test environment is used exclusively to run your application's 2 | # test suite. You never need to work with it otherwise. Remember that 3 | # your test database is "scratch space" for the test suite and is wiped 4 | # and recreated between test runs. Don't rely on the data there! 5 | 6 | Rails.application.configure do 7 | # Settings specified here will take precedence over those in config/application.rb. 8 | 9 | # While tests run files are not watched, reloading is not necessary. 10 | config.enable_reloading = false 11 | 12 | # Eager loading loads your entire application. When running a single test locally, 13 | # this is usually not necessary, and can slow down your test suite. However, it's 14 | # recommended that you enable it in continuous integration systems to ensure eager 15 | # loading is working properly before deploying your code. 16 | config.eager_load = ENV["CI"].present? 17 | 18 | # Configure public file server for tests with cache-control for performance. 19 | config.public_file_server.headers = { "cache-control" => "public, max-age=3600" } 20 | 21 | # Show full error reports. 22 | config.consider_all_requests_local = true 23 | config.cache_store = :null_store 24 | 25 | # Render exception templates for rescuable exceptions and raise for other exceptions. 26 | config.action_dispatch.show_exceptions = :rescuable 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | 31 | # Store uploaded files on the local file system in a temporary directory. 32 | config.active_storage.service = :test 33 | 34 | # Tell Action Mailer not to deliver emails to the real world. 35 | # The :test delivery method accumulates sent emails in the 36 | # ActionMailer::Base.deliveries array. 37 | config.action_mailer.delivery_method = :test 38 | 39 | # Set host to be used by links generated in mailer templates. 40 | config.action_mailer.default_url_options = { host: "example.com" } 41 | 42 | # Print deprecation notices to the stderr. 43 | config.active_support.deprecation = :stderr 44 | 45 | # Raises error for missing translations. 46 | # config.i18n.raise_on_missing_translations = true 47 | 48 | # Annotate rendered view with file names. 49 | # config.action_view.annotate_rendered_view_with_filenames = true 50 | 51 | # Raise error when a before_action's only/except options reference missing actions. 52 | config.action_controller.raise_on_missing_callback_actions = true 53 | 54 | # URLs 55 | # routes.default_url_options = { host: "rapidrubystarter.test" } 56 | # config.action_controller.default_url_options = { host: "rapidrubystarter.test" } 57 | 58 | config.action_mailer.default_url_options = { host: "rapidrubystarter.test" } 59 | 60 | config.active_job.queue_adapter = :test 61 | end 62 | -------------------------------------------------------------------------------- /config/importmap.rb: -------------------------------------------------------------------------------- 1 | # Pin npm packages by running ./bin/importmap 2 | 3 | pin "application" 4 | pin "@hotwired/turbo-rails", to: "turbo.min.js" 5 | pin "@hotwired/stimulus", to: "stimulus.min.js" 6 | pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" 7 | pin "stimulus-use" # @0.52.3 8 | pin_all_from "app/javascript/controllers", under: "controllers" 9 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = "1.0" 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | -------------------------------------------------------------------------------- /config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy. 4 | # See the Securing Rails Applications Guide for more information: 5 | # https://guides.rubyonrails.org/security.html#content-security-policy-header 6 | 7 | # Rails.application.configure do 8 | # config.content_security_policy do |policy| 9 | # policy.default_src :self, :https 10 | # policy.font_src :self, :https, :data 11 | # policy.img_src :self, :https, :data 12 | # policy.object_src :none 13 | # policy.script_src :self, :https 14 | # policy.style_src :self, :https 15 | # # Specify URI for violation reports 16 | # # policy.report_uri "/csp-violation-report-endpoint" 17 | # end 18 | # 19 | # # Generate session nonces for permitted importmap, inline scripts, and inline styles. 20 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } 21 | # config.content_security_policy_nonce_directives = %w(script-src style-src) 22 | # 23 | # # Report violations without enforcing the policy. 24 | # # config.content_security_policy_report_only = true 25 | # end 26 | -------------------------------------------------------------------------------- /config/initializers/exception_notification.rb: -------------------------------------------------------------------------------- 1 | if Rails.env.production? 2 | require "exception_notification/rails" 3 | 4 | ExceptionNotification.configure do |config| 5 | # Ignore additional exception types. 6 | # ActiveRecord::RecordNotFound, Mongoid::Errors::DocumentNotFound, AbstractController::ActionNotFound and ActionController::RoutingError are already added. 7 | config.ignored_exceptions += %w[ 8 | ActionDispatch::RemoteIp::IpSpoofAttackError 9 | ActionController::InvalidAuthenticityToken 10 | ActionController::RoutingError 11 | ActiveRecord::RecordNotFound 12 | ActionView::MissingTemplate 13 | Stripe::CardError 14 | URI::InvalidURIError 15 | OpenSSL::SSL::SSLError 16 | OutgoingWebhookResponseError 17 | ] 18 | 19 | # Adds a condition to decide when an exception must be ignored or not. 20 | # The ignore_if method can be invoked multiple times to add extra conditions. 21 | config.ignore_if do |exception, options| 22 | !Rails.env.production? && !Rails.env.staging? 23 | end 24 | 25 | # Ignore exceptions generated by crawlers 26 | # config.ignore_crawlers %w{Googlebot bingbot} 27 | 28 | # Notifiers ================================================================= 29 | 30 | if (slack_exceptions_webhook_url = Rails.application.credentials.slack_exceptions_webhook_url) 31 | # Email notifier sends notifications by email. 32 | config.add_notifier :slack, { 33 | webhook_url: slack_exceptions_webhook_url, 34 | username: "Rapid Ruby error (#{Rails.env})", 35 | channel: "#rr-errors", 36 | additional_parameters: { 37 | mrkdwn: true 38 | } 39 | } 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. 4 | # Use this to limit dissemination of sensitive information. 5 | # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. 6 | Rails.application.config.filter_parameters += [ 7 | :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc 8 | ] 9 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, "\\1en" 8 | # inflect.singular /^(ox)en/i, "\\1" 9 | # inflect.irregular "person", "people" 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym "RESTful" 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/new_framework_defaults_8_0.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file eases your Rails 8.0 framework defaults upgrade. 4 | # 5 | # Uncomment each configuration one by one to switch to the new default. 6 | # Once your application is ready to run with all new defaults, you can remove 7 | # this file and set the `config.load_defaults` to `8.0`. 8 | # 9 | # Read the Guide for Upgrading Ruby on Rails for more info on each option. 10 | # https://guides.rubyonrails.org/upgrading_ruby_on_rails.html 11 | 12 | ### 13 | # Specifies whether `to_time` methods preserve the UTC offset of their receivers or preserves the timezone. 14 | # If set to `:zone`, `to_time` methods will use the timezone of their receivers. 15 | # If set to `:offset`, `to_time` methods will use the UTC offset. 16 | # If `false`, `to_time` methods will convert to the local system UTC offset instead. 17 | #++ 18 | # Rails.application.config.active_support.to_time_preserves_timezone = :zone 19 | 20 | ### 21 | # When both `If-Modified-Since` and `If-None-Match` are provided by the client 22 | # only consider `If-None-Match` as specified by RFC 7232 Section 6. 23 | # If set to `false` both conditions need to be satisfied. 24 | #++ 25 | # Rails.application.config.action_dispatch.strict_freshness = true 26 | 27 | ### 28 | # Set `Regexp.timeout` to `1`s by default to improve security over Regexp Denial-of-Service attacks. 29 | #++ 30 | # Regexp.timeout = 1 31 | -------------------------------------------------------------------------------- /config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide HTTP permissions policy. For further 4 | # information see: https://developers.google.com/web/updates/2018/06/feature-policy 5 | 6 | # Rails.application.config.permissions_policy do |policy| 7 | # policy.camera :none 8 | # policy.gyroscope :none 9 | # policy.microphone :none 10 | # policy.usb :none 11 | # policy.fullscreen :self 12 | # policy.payment :self, "https://secure.example.com" 13 | # end 14 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t "hello" 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t("hello") %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # "true": "foo" 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # This configuration file will be evaluated by Puma. The top-level methods that 2 | # are invoked here are part of Puma's configuration DSL. For more information 3 | # about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. 4 | # 5 | # Puma starts a configurable number of processes (workers) and each process 6 | # serves each request in a thread from an internal thread pool. 7 | # 8 | # You can control the number of workers using ENV["WEB_CONCURRENCY"]. You 9 | # should only set this value when you want to run 2 or more workers. The 10 | # default is already 1. 11 | # 12 | # The ideal number of threads per worker depends both on how much time the 13 | # application spends waiting for IO operations and on how much you wish to 14 | # prioritize throughput over latency. 15 | # 16 | # As a rule of thumb, increasing the number of threads will increase how much 17 | # traffic a given process can handle (throughput), but due to CRuby's 18 | # Global VM Lock (GVL) it has diminishing returns and will degrade the 19 | # response time (latency) of the application. 20 | # 21 | # The default is set to 3 threads as it's deemed a decent compromise between 22 | # throughput and latency for the average Rails application. 23 | # 24 | # Any libraries that use a connection pool or another resource pool should 25 | # be configured to provide at least as many connections as the number of 26 | # threads. This includes Active Record's `pool` parameter in `database.yml`. 27 | threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) 28 | threads threads_count, threads_count 29 | 30 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 31 | port ENV.fetch("PORT", 3000) 32 | 33 | # Allow puma to be restarted by `bin/rails restart` command. 34 | plugin :tmp_restart 35 | 36 | # Run the Solid Queue supervisor inside of Puma for single-server deployments 37 | plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] 38 | 39 | # Specify the PID file. Defaults to tmp/pids/server.pid in development. 40 | # In other environments, only set the PID file if requested. 41 | pidfile ENV["PIDFILE"] if ENV["PIDFILE"] 42 | -------------------------------------------------------------------------------- /config/queue.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | dispatchers: 3 | - polling_interval: 1 4 | batch_size: 500 5 | workers: 6 | - queues: "*" 7 | threads: 3 8 | processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %> 9 | polling_interval: 0.1 10 | 11 | development: 12 | <<: *default 13 | 14 | test: 15 | <<: *default 16 | 17 | production: 18 | <<: *default 19 | -------------------------------------------------------------------------------- /config/recurring.yml: -------------------------------------------------------------------------------- 1 | # production: 2 | # periodic_cleanup: 3 | # class: CleanSoftDeletedRecordsJob 4 | # queue: background 5 | # args: [ 1000, { batch_size: 500 } ] 6 | # schedule: every hour 7 | # periodic_command: 8 | # command: "SoftDeletedRecord.due.delete_all" 9 | # priority: 2 10 | # schedule: at 5am every day 11 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | namespace :admin do 3 | resources :sessions 4 | resources :teams 5 | resources :team_users 6 | resources :users do 7 | member do 8 | post :masquerade 9 | post :reverse_masquerade 10 | end 11 | end 12 | 13 | root to: "users#index" 14 | end 15 | # Authentication routes 16 | get "sign_in", to: "sessions#new" 17 | post "sign_in", to: "sessions#create" 18 | get "sign_up", to: "registrations#new" 19 | post "sign_up", to: "registrations#create" 20 | resources :sessions, only: [:index, :show, :destroy] 21 | resource :password, only: [:update] 22 | namespace :identity do 23 | resource :account, only: [:show, :update], controller: :account 24 | resource :email, only: [:update] 25 | resource :email_verification, only: [:edit, :create] 26 | resource :password_reset, only: [:new, :edit, :create, :update] 27 | end 28 | 29 | # Solid Queue UI 30 | mount MissionControl::Jobs::Engine, at: "/admin/jobs" 31 | 32 | # App routes 33 | resources :lessons, only: :index 34 | 35 | root "pages#home" 36 | end 37 | -------------------------------------------------------------------------------- /config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket-<%= Rails.env %> 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket-<%= Rails.env %> 23 | 24 | # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name-<%= Rails.env %> 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /config/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require('tailwindcss/defaultTheme') 2 | 3 | module.exports = { 4 | content: [ 5 | './public/*.html', 6 | './app/helpers/**/*.rb', 7 | './app/javascript/**/*.js', 8 | './app/views/**/*.{erb,haml,html,slim}', 9 | 'app/components/**/*.{erb,rb}', 10 | ], 11 | theme: { 12 | extend: { 13 | fontFamily: { 14 | sans: ['Inter var', ...defaultTheme.fontFamily.sans], 15 | }, 16 | }, 17 | }, 18 | plugins: [ 19 | require('@tailwindcss/forms'), 20 | require('@tailwindcss/aspect-ratio'), 21 | require('@tailwindcss/typography'), 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /db/migrate/20230205103754_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :users do |t| 4 | t.string :first_name, null: false 5 | t.string :last_name, null: false 6 | t.string :email, null: false 7 | t.string :password_digest, null: false 8 | 9 | t.boolean :verified, null: false, default: false 10 | 11 | t.timestamps 12 | end 13 | 14 | add_index :users, :email, unique: true 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/20230205103755_create_sessions.rb: -------------------------------------------------------------------------------- 1 | class CreateSessions < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :sessions do |t| 4 | t.references :user, null: false, foreign_key: true 5 | t.string :user_agent 6 | t.string :ip_address 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20230205103756_create_email_verification_tokens.rb: -------------------------------------------------------------------------------- 1 | class CreateEmailVerificationTokens < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :email_verification_tokens do |t| 4 | t.references :user, null: false, foreign_key: true 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20230205103757_create_password_reset_tokens.rb: -------------------------------------------------------------------------------- 1 | class CreatePasswordResetTokens < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :password_reset_tokens do |t| 4 | t.references :user, null: false, foreign_key: true 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20240820163037_add_service_name_to_active_storage_blobs.active_storage.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from active_storage (originally 20190112182829) 2 | class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0] 3 | def up 4 | return unless table_exists?(:active_storage_blobs) 5 | 6 | unless column_exists?(:active_storage_blobs, :service_name) 7 | add_column :active_storage_blobs, :service_name, :string 8 | 9 | if configured_service = ActiveStorage::Blob.service.name 10 | ActiveStorage::Blob.unscoped.update_all(service_name: configured_service) 11 | end 12 | 13 | change_column :active_storage_blobs, :service_name, :string, null: false 14 | end 15 | end 16 | 17 | def down 18 | return unless table_exists?(:active_storage_blobs) 19 | 20 | remove_column :active_storage_blobs, :service_name 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /db/migrate/20240820163038_create_active_storage_variant_records.active_storage.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from active_storage (originally 20191206030411) 2 | class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0] 3 | def change 4 | return unless table_exists?(:active_storage_blobs) 5 | 6 | # Use Active Record's configured type for primary key 7 | create_table :active_storage_variant_records, id: primary_key_type, if_not_exists: true do |t| 8 | t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type 9 | t.string :variation_digest, null: false 10 | 11 | t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true 12 | t.foreign_key :active_storage_blobs, column: :blob_id 13 | end 14 | end 15 | 16 | private 17 | def primary_key_type 18 | config = Rails.configuration.generators 19 | config.options[config.orm][:primary_key_type] || :primary_key 20 | end 21 | 22 | def blobs_primary_key_type 23 | pkey_name = connection.primary_key(:active_storage_blobs) 24 | pkey_column = connection.columns(:active_storage_blobs).find { |c| c.name == pkey_name } 25 | pkey_column.bigint? ? :bigint : pkey_column.type 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /db/migrate/20240820163039_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from active_storage (originally 20211119233751) 2 | class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0] 3 | def change 4 | return unless table_exists?(:active_storage_blobs) 5 | 6 | change_column_null(:active_storage_blobs, :checksum, true) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20241026134459_create_teams.rb: -------------------------------------------------------------------------------- 1 | class CreateTeams < ActiveRecord::Migration[7.2] 2 | def change 3 | create_table :teams do |t| 4 | t.string :name, null: false 5 | 6 | t.timestamps 7 | end 8 | 9 | create_table :team_users do |t| 10 | t.references :team, null: false, foreign_key: true 11 | t.references :user, null: false, foreign_key: true 12 | t.string :role, null: false, default: "owner" 13 | 14 | t.timestamps 15 | end 16 | 17 | add_reference :users, :team, null: false, foreign_key: true 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /db/migrate/20241120200338_install_solid_cable.rb: -------------------------------------------------------------------------------- 1 | class InstallSolidCable < ActiveRecord::Migration[7.2] 2 | def change 3 | create_table "solid_cable_messages", force: :cascade do |t| 4 | t.binary "channel", limit: 1024, null: false 5 | t.binary "payload", limit: 536870912, null: false 6 | t.datetime "created_at", null: false 7 | t.integer "channel_hash", limit: 8, null: false 8 | t.index ["channel"], name: "index_solid_cable_messages_on_channel" 9 | t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash" 10 | t.index ["created_at"], name: "index_solid_cable_messages_on_created_at" 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20241124153040_install_solid_queue.rb: -------------------------------------------------------------------------------- 1 | class InstallSolidQueue < ActiveRecord::Migration[7.2] 2 | def change 3 | create_table "solid_queue_blocked_executions", force: :cascade do |t| 4 | t.bigint "job_id", null: false 5 | t.string "queue_name", null: false 6 | t.integer "priority", default: 0, null: false 7 | t.string "concurrency_key", null: false 8 | t.datetime "expires_at", null: false 9 | t.datetime "created_at", null: false 10 | t.index [ "concurrency_key", "priority", "job_id" ], name: "index_solid_queue_blocked_executions_for_release" 11 | t.index [ "expires_at", "concurrency_key" ], name: "index_solid_queue_blocked_executions_for_maintenance" 12 | t.index [ "job_id" ], name: "index_solid_queue_blocked_executions_on_job_id", unique: true 13 | end 14 | 15 | create_table "solid_queue_claimed_executions", force: :cascade do |t| 16 | t.bigint "job_id", null: false 17 | t.bigint "process_id" 18 | t.datetime "created_at", null: false 19 | t.index [ "job_id" ], name: "index_solid_queue_claimed_executions_on_job_id", unique: true 20 | t.index [ "process_id", "job_id" ], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id" 21 | end 22 | 23 | create_table "solid_queue_failed_executions", force: :cascade do |t| 24 | t.bigint "job_id", null: false 25 | t.text "error" 26 | t.datetime "created_at", null: false 27 | t.index [ "job_id" ], name: "index_solid_queue_failed_executions_on_job_id", unique: true 28 | end 29 | 30 | create_table "solid_queue_jobs", force: :cascade do |t| 31 | t.string "queue_name", null: false 32 | t.string "class_name", null: false 33 | t.text "arguments" 34 | t.integer "priority", default: 0, null: false 35 | t.string "active_job_id" 36 | t.datetime "scheduled_at" 37 | t.datetime "finished_at" 38 | t.string "concurrency_key" 39 | t.datetime "created_at", null: false 40 | t.datetime "updated_at", null: false 41 | t.index [ "active_job_id" ], name: "index_solid_queue_jobs_on_active_job_id" 42 | t.index [ "class_name" ], name: "index_solid_queue_jobs_on_class_name" 43 | t.index [ "finished_at" ], name: "index_solid_queue_jobs_on_finished_at" 44 | t.index [ "queue_name", "finished_at" ], name: "index_solid_queue_jobs_for_filtering" 45 | t.index [ "scheduled_at", "finished_at" ], name: "index_solid_queue_jobs_for_alerting" 46 | end 47 | 48 | create_table "solid_queue_pauses", force: :cascade do |t| 49 | t.string "queue_name", null: false 50 | t.datetime "created_at", null: false 51 | t.index [ "queue_name" ], name: "index_solid_queue_pauses_on_queue_name", unique: true 52 | end 53 | 54 | create_table "solid_queue_processes", force: :cascade do |t| 55 | t.string "kind", null: false 56 | t.datetime "last_heartbeat_at", null: false 57 | t.bigint "supervisor_id" 58 | t.integer "pid", null: false 59 | t.string "hostname" 60 | t.text "metadata" 61 | t.datetime "created_at", null: false 62 | t.string "name", null: false 63 | t.index [ "last_heartbeat_at" ], name: "index_solid_queue_processes_on_last_heartbeat_at" 64 | t.index [ "name", "supervisor_id" ], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true 65 | t.index [ "supervisor_id" ], name: "index_solid_queue_processes_on_supervisor_id" 66 | end 67 | 68 | create_table "solid_queue_ready_executions", force: :cascade do |t| 69 | t.bigint "job_id", null: false 70 | t.string "queue_name", null: false 71 | t.integer "priority", default: 0, null: false 72 | t.datetime "created_at", null: false 73 | t.index [ "job_id" ], name: "index_solid_queue_ready_executions_on_job_id", unique: true 74 | t.index [ "priority", "job_id" ], name: "index_solid_queue_poll_all" 75 | t.index [ "queue_name", "priority", "job_id" ], name: "index_solid_queue_poll_by_queue" 76 | end 77 | 78 | create_table "solid_queue_recurring_executions", force: :cascade do |t| 79 | t.bigint "job_id", null: false 80 | t.string "task_key", null: false 81 | t.datetime "run_at", null: false 82 | t.datetime "created_at", null: false 83 | t.index [ "job_id" ], name: "index_solid_queue_recurring_executions_on_job_id", unique: true 84 | t.index [ "task_key", "run_at" ], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true 85 | end 86 | 87 | create_table "solid_queue_recurring_tasks", force: :cascade do |t| 88 | t.string "key", null: false 89 | t.string "schedule", null: false 90 | t.string "command", limit: 2048 91 | t.string "class_name" 92 | t.text "arguments" 93 | t.string "queue_name" 94 | t.integer "priority", default: 0 95 | t.boolean "static", default: true, null: false 96 | t.text "description" 97 | t.datetime "created_at", null: false 98 | t.datetime "updated_at", null: false 99 | t.index [ "key" ], name: "index_solid_queue_recurring_tasks_on_key", unique: true 100 | t.index [ "static" ], name: "index_solid_queue_recurring_tasks_on_static" 101 | end 102 | 103 | create_table "solid_queue_scheduled_executions", force: :cascade do |t| 104 | t.bigint "job_id", null: false 105 | t.string "queue_name", null: false 106 | t.integer "priority", default: 0, null: false 107 | t.datetime "scheduled_at", null: false 108 | t.datetime "created_at", null: false 109 | t.index [ "job_id" ], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true 110 | t.index [ "scheduled_at", "priority", "job_id" ], name: "index_solid_queue_dispatch_all" 111 | end 112 | 113 | create_table "solid_queue_semaphores", force: :cascade do |t| 114 | t.string "key", null: false 115 | t.integer "value", default: 1, null: false 116 | t.datetime "expires_at", null: false 117 | t.datetime "created_at", null: false 118 | t.datetime "updated_at", null: false 119 | t.index [ "expires_at" ], name: "index_solid_queue_semaphores_on_expires_at" 120 | t.index [ "key", "value" ], name: "index_solid_queue_semaphores_on_key_and_value" 121 | t.index [ "key" ], name: "index_solid_queue_semaphores_on_key", unique: true 122 | end 123 | 124 | add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade 125 | add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade 126 | add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade 127 | add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade 128 | add_foreign_key "solid_queue_recurring_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade 129 | add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /db/migrate/20241124153608_add_admin_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddAdminToUsers < ActiveRecord::Migration[7.2] 2 | def change 3 | add_column :users, :admin, :boolean, default: false, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20241124161131_create_active_storage_tables.active_storage.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from active_storage (originally 20170806125915) 2 | class CreateActiveStorageTables < ActiveRecord::Migration[7.0] 3 | def change 4 | # Use Active Record's configured type for primary and foreign keys 5 | primary_key_type, foreign_key_type = primary_and_foreign_key_types 6 | 7 | create_table :active_storage_blobs, id: primary_key_type do |t| 8 | t.string :key, null: false 9 | t.string :filename, null: false 10 | t.string :content_type 11 | t.text :metadata 12 | t.string :service_name, null: false 13 | t.bigint :byte_size, null: false 14 | t.string :checksum 15 | 16 | if connection.supports_datetime_with_precision? 17 | t.datetime :created_at, precision: 6, null: false 18 | else 19 | t.datetime :created_at, null: false 20 | end 21 | 22 | t.index [ :key ], unique: true 23 | end 24 | 25 | create_table :active_storage_attachments, id: primary_key_type do |t| 26 | t.string :name, null: false 27 | t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type 28 | t.references :blob, null: false, type: foreign_key_type 29 | 30 | if connection.supports_datetime_with_precision? 31 | t.datetime :created_at, precision: 6, null: false 32 | else 33 | t.datetime :created_at, null: false 34 | end 35 | 36 | t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true 37 | t.foreign_key :active_storage_blobs, column: :blob_id 38 | end 39 | 40 | create_table :active_storage_variant_records, id: primary_key_type do |t| 41 | t.belongs_to :blob, null: false, index: false, type: foreign_key_type 42 | t.string :variation_digest, null: false 43 | 44 | t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true 45 | t.foreign_key :active_storage_blobs, column: :blob_id 46 | end 47 | end 48 | 49 | private 50 | def primary_and_foreign_key_types 51 | config = Rails.configuration.generators 52 | setting = config.options[config.orm][:primary_key_type] 53 | primary_key_type = setting || :primary_key 54 | foreign_key_type = setting || :bigint 55 | [ primary_key_type, foreign_key_type ] 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /db/migrate/20241124165139_add_masquerade_to_sessions.rb: -------------------------------------------------------------------------------- 1 | class AddMasqueradeToSessions < ActiveRecord::Migration[8.0] 2 | def change 3 | add_reference :sessions, :admin_user, null: true, foreign_key: { to_table: :users } 4 | add_column :sessions, :masquerade_at, :datetime 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # movies = Movie.create([{ name: "Star Wars" }, { name: "Lord of the Rings" }]) 7 | # Character.create(name: "Luke", movie: movies.first) 8 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapidruby/rapid-ruby-starter/8b6700c7f9e974da31c728d7da167fffd2939faa/lib/assets/.keep -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapidruby/rapid-ruby-starter/8b6700c7f9e974da31c728d7da167fffd2939faa/lib/tasks/.keep -------------------------------------------------------------------------------- /lib/tasks/admin.rake: -------------------------------------------------------------------------------- 1 | namespace :admin do 2 | desc "Creates a user account" 3 | task create_user: :environment do 4 | puts "First name:" 5 | first_name = STDIN.gets.strip 6 | puts "Last name:" 7 | last_name = STDIN.gets.strip 8 | puts "Email:" 9 | email = STDIN.gets.strip 10 | puts "Password (characters will be hidden):" 11 | password = STDIN.noecho(&:gets).strip 12 | 13 | user = User.new( 14 | first_name: first_name, 15 | last_name: last_name, 16 | email: email, 17 | password: password, 18 | verified: true, 19 | admin: true 20 | ) 21 | 22 | user.team = Team.new(name: "#{user.first_name}’s Team") 23 | 24 | if user.save(validate: false) 25 | user.team.users << user 26 | puts "User created with email: #{email}" 27 | else 28 | puts "Creation failed, please try again" 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapidruby/rapid-ruby-starter/8b6700c7f9e974da31c728d7da167fffd2939faa/log/.keep -------------------------------------------------------------------------------- /public/400.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | The server cannot process the request due to a client error (400 Bad Request) 8 | 9 | 10 | 11 | 12 | 13 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 |
104 |
105 | 106 |
107 |
108 |

The server cannot process the request due to a client error. Please check the request and try again. If you’re the application owner check the logs for more information.

109 |
110 |
111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | The page you were looking for doesn’t exist (404 Not found) 8 | 9 | 10 | 11 | 12 | 13 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 |
104 |
105 | 106 |
107 |
108 |

The page you were looking for doesn’t exist. You may have mistyped the address or the page may have moved. If you’re the application owner check the logs for more information.

109 |
110 |
111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /public/406-unsupported-browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Your browser is not supported (406 Not Acceptable) 8 | 9 | 10 | 11 | 12 | 13 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 |
104 |
105 | 106 |
107 |
108 |

Your browser is not supported.
Please upgrade your browser to continue.

109 |
110 |
111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | The change you wanted was rejected (422 Unprocessable Entity) 8 | 9 | 10 | 11 | 12 | 13 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 |
104 |
105 | 106 |
107 |
108 |

The change you wanted was rejected. Maybe you tried to change something you didn’t have access to. If you’re the application owner check the logs for more information.

109 |
110 |
111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | We’re sorry, but something went wrong (500 Internal Server Error) 8 | 9 | 10 | 11 | 12 | 13 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 |
104 |
105 | 106 |
107 |
108 |

We’re sorry, but something went wrong.
If you’re the application owner check the logs for more information.

109 |
110 |
111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapidruby/rapid-ruby-starter/8b6700c7f9e974da31c728d7da167fffd2939faa/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapidruby/rapid-ruby-starter/8b6700c7f9e974da31c728d7da167fffd2939faa/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapidruby/rapid-ruby-starter/8b6700c7f9e974da31c728d7da167fffd2939faa/public/favicon.ico -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapidruby/rapid-ruby-starter/8b6700c7f9e974da31c728d7da167fffd2939faa/public/icon.png -------------------------------------------------------------------------------- /public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /spec/components/admin/masquerade_component_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | RSpec.describe Admin::MasqueradeComponent, type: :component do 6 | it "renders a masquerade banner if masquerading" do 7 | user = double(:user, email: "test@example.com") 8 | admin_user = double(:admin_user, email: "admin@example.com") 9 | session = double(:session, masquerading?: true, user: user, admin_user: admin_user) 10 | 11 | render_inline(described_class.new(session: session)) 12 | 13 | expect(page).to have_text("You are masquerading as #{session.user.email}") 14 | end 15 | 16 | it "doesn't render if not masquerading" do 17 | session = double(:session, masquerading?: false) 18 | 19 | render_inline(described_class.new(session: session)) 20 | 21 | expect(page).to have_no_text("You are masquerading as") 22 | end 23 | 24 | it "doesn't render if session is blank" do 25 | session = nil 26 | 27 | render_inline(described_class.new(session: session)) 28 | 29 | expect(page).to have_no_text("You are masquerading as") 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/components/avatar_component_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe AvatarComponent, type: :component do 4 | let(:user) { users(:pete) } 5 | 6 | it "renders with default settings" do 7 | render_inline(described_class.new(user: user)) 8 | end 9 | 10 | it "renders at :thumb size" do 11 | render_inline(described_class.new(user: user, size: :thumb)) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/components/fieldset_header_component_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe FieldsetHeaderComponent, type: :component do 4 | subject(:component) { described_class.new(title: "Title", description: "Description") } 5 | 6 | it "renders the component" do 7 | render_inline(component) 8 | 9 | expect(page).to have_text("Title") 10 | expect(page).to have_text("Description") 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/components/user_dropdown_component_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | RSpec.describe UserDropdownComponent, type: :component do 6 | let(:user) { users(:pete) } 7 | let(:session) { Session.create!(user: user) } 8 | 9 | it "renders something useful" do 10 | render_inline(described_class.new(current_user: user, current_session: session)) 11 | 12 | expect(page).to have_content(user.email) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/features/identity/emails_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe "Identity::EmailsTest", type: :feature do 4 | let!(:user) { sign_in_as(users(:lazaro_nixon)) } 5 | 6 | it "updates the email" do 7 | click_on "Manage account", match: :first 8 | 9 | within("#change_email_form") do 10 | fill_in "New email", with: "new_email@hey.com" 11 | click_on "Save changes" 12 | end 13 | 14 | assert_text "Your email has been changed" 15 | end 16 | 17 | it "sends a verification email" do 18 | user.update! verified: false 19 | 20 | click_on "Manage account", match: :first 21 | assert_current_path "/identity/account" 22 | click_on "Re-send verification email" 23 | 24 | assert_text "We sent a verification email to your email address" 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/features/identity/password_resets_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe "Identity::PasswordResetsTest", type: :feature do 4 | before do 5 | @user = users(:lazaro_nixon) 6 | @sid = @user.password_reset_tokens.create.signed_id(expires_in: 20.minutes) 7 | end 8 | 9 | it "sending a password reset email" do 10 | visit sign_in_url 11 | click_on "Forgot your password?" 12 | 13 | fill_in "Email", with: @user.email 14 | click_on "Send password reset email" 15 | 16 | assert_text "Check your email for reset instructions" 17 | end 18 | 19 | it "updating password" do 20 | visit edit_identity_password_reset_url(sid: @sid) 21 | 22 | fill_in "New password", with: "Secret6*4*2*" 23 | fill_in "Confirm new password", with: "Secret6*4*2*" 24 | click_on "Save changes" 25 | 26 | assert_text "Your password was reset successfully. Please sign in" 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/features/passwords_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe "PasswordsTest", type: :feature do 4 | before { sign_in_as(users(:lazaro_nixon)) } 5 | 6 | it "updating the password" do 7 | click_on "Manage account", match: :first 8 | 9 | within("#change_password_form") do 10 | fill_in "Current password", with: "Secret1*3*5*" 11 | fill_in "New password", with: "Secret6*4*2*" 12 | fill_in "Confirm new password", with: "Secret6*4*2*" 13 | click_on "Save changes" 14 | end 15 | 16 | assert_text "Your password has been changed" 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/features/registrations_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe "RegistrationsTest", type: :feature do 4 | it "allows signing up" do 5 | visit sign_up_url 6 | 7 | fill_in "First name", with: "Jane" 8 | fill_in "Last name", with: "Nixon" 9 | fill_in "Email", with: "janenixon@hey.com" 10 | fill_in "Password", with: "Secret6*4*2*" 11 | fill_in "Password confirmation", with: "Secret6*4*2*" 12 | check "terms_and_conditions" 13 | within ".form-actions" do 14 | click_on "Sign up" 15 | end 16 | 17 | assert_text "Welcome! You have signed up successfully" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/features/sessions_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe "SessionsTest", type: :feature do 4 | before do 5 | @user = users(:lazaro_nixon) 6 | end 7 | 8 | it "visiting the index" do 9 | sign_in_as @user 10 | 11 | click_on "Manage account", match: :first 12 | click_on "Manage devices and sessions", match: :first 13 | assert_selector "h1", text: "Sessions" 14 | end 15 | 16 | it "signing in" do 17 | visit sign_in_url 18 | fill_in "Email", with: @user.email 19 | fill_in "Password", with: "Secret1*3*5*" 20 | click_on "Sign in" 21 | 22 | assert_text "Signed in successfully" 23 | end 24 | 25 | it "signing out" do 26 | sign_in_as @user 27 | 28 | click_on "Log out", match: :first 29 | assert_text "That session has been logged out" 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/fixtures/team_users.yml: -------------------------------------------------------------------------------- 1 | pete_team_user: 2 | user: pete 3 | team: petes_team 4 | role: owner 5 | lazaro_nixon_team_user: 6 | user: lazaro_nixon 7 | team: lazaro_nixon_team 8 | role: owner 9 | -------------------------------------------------------------------------------- /spec/fixtures/teams.yml: -------------------------------------------------------------------------------- 1 | petes_team: 2 | name: Pete's Team 3 | 4 | lazaro_nixon_team: 5 | name: Lazaro's Team 6 | -------------------------------------------------------------------------------- /spec/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | pete: 3 | first_name: Pete 4 | last_name: Hawkins 5 | email: pete@rapidruby.com 6 | password_digest: <%= BCrypt::Password.create("Secret1*3*5*") %> 7 | verified: true 8 | admin: true 9 | team: petes_team 10 | lazaro_nixon: 11 | first_name: "Lazaro" 12 | last_name: "Nixon" 13 | email: lazaronixon@hotmail.com 14 | password_digest: <%= BCrypt::Password.create("Secret1*3*5*") %> 15 | verified: true 16 | team: lazaro_nixon_team 17 | -------------------------------------------------------------------------------- /spec/mailers/session_mailer_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe SessionMailer, type: :mailer do 4 | let(:session) { users(:lazaro_nixon).sessions.create! } 5 | 6 | it "#signed_in_notification" do 7 | mail = SessionMailer.with(session: session).signed_in_notification 8 | expect(mail.subject).to eq("New sign-in to your account") 9 | expect(mail.to.first).to eq(session.user.email) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/mailers/user_mailer_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe UserMailer, type: :mailer do 4 | let(:user) { users(:lazaro_nixon) } 5 | 6 | it "#password_reset" do 7 | mail = UserMailer.with(user: user).password_reset 8 | expect(mail.subject).to eq("Reset your password") 9 | expect(mail.to.first).to eq(user.email) 10 | end 11 | 12 | it "#email_verification" do 13 | mail = UserMailer.with(user: user).email_verification 14 | expect(mail.subject).to eq("Verify your email") 15 | expect(mail.to.first).to eq(user.email) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/models/concerns/avatarable_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe Avatarable, type: :model do 4 | let(:user) { users(:pete) } 5 | 6 | describe "#letters_svg" do 7 | it "returns an svg of initials" do 8 | svg = user.letters_svg 9 | expect(svg).to include("PH") 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/models/session_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe Session, type: :model do 4 | describe "associations" do 5 | it { should belong_to(:user) } 6 | it { should belong_to(:admin_user).optional } 7 | end 8 | 9 | describe "#masquerade_as!" do 10 | let(:admin_user) { users(:pete) } 11 | let(:normal_user) { users(:lazaro_nixon) } 12 | 13 | before do 14 | admin_user.update!(admin: true) 15 | end 16 | 17 | it "masquerades as the given user" do 18 | session = Session.create!(user: admin_user) 19 | expect(session.masquerading?).to be(false) 20 | 21 | session.masquerade_as!(normal_user) 22 | 23 | expect(session.admin_user).to eq(admin_user) 24 | expect(session.user).to eq(normal_user) 25 | expect(session.masquerading?).to be(true) 26 | 27 | session.reverse_masquerade! 28 | 29 | expect(session.admin_user).to be_nil 30 | expect(session.user).to eq(admin_user) 31 | expect(session.masquerading?).to be(false) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/models/team_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Team, type: :model do 4 | it { is_expected.to have_many :team_users } 5 | it { is_expected.to have_many :users } 6 | end 7 | -------------------------------------------------------------------------------- /spec/models/team_user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe TeamUser, type: :model do 4 | it { is_expected.to belong_to :user } 5 | it { is_expected.to belong_to :team } 6 | end 7 | -------------------------------------------------------------------------------- /spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe User, type: :model do 4 | it { is_expected.to have_many :email_verification_tokens } 5 | it { is_expected.to have_many :password_reset_tokens } 6 | it { is_expected.to have_many :sessions } 7 | it { is_expected.to have_many :team_users } 8 | it { is_expected.to have_many :teams } 9 | it { is_expected.to belong_to :team } 10 | 11 | it { is_expected.to validate_presence_of :first_name } 12 | it { is_expected.to validate_presence_of :last_name } 13 | it { is_expected.to validate_presence_of :email } 14 | 15 | describe "#name" do 16 | it "concatenates first and last name" do 17 | user = User.new(first_name: "John", last_name: "Doe") 18 | expect(user.name).to eq("John Doe") 19 | end 20 | end 21 | 22 | describe "#obfuscated_name" do 23 | it "concatenates first name and the first letter of last name" do 24 | user = User.new(first_name: "John", last_name: "Doe") 25 | expect(user.obfuscated_name).to eq("John D.") 26 | end 27 | 28 | it "uses only the first name if last name is empty" do 29 | user = User.new(first_name: "John", last_name: nil) 30 | expect(user.obfuscated_name).to eq("John") 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | require 'spec_helper' 3 | ENV['RAILS_ENV'] ||= 'test' 4 | require_relative '../config/environment' 5 | # Prevent database truncation if the environment is production 6 | abort("The Rails environment is running in production mode!") if Rails.env.production? 7 | require 'rspec/rails' 8 | # Add additional requires below this line. Rails is not loaded until this point! 9 | require "shoulda/matchers" 10 | require "view_component/test_helpers" 11 | 12 | Shoulda::Matchers.configure do |config| 13 | config.integrate do |with| 14 | with.test_framework :rspec 15 | with.library :rails 16 | end 17 | end 18 | 19 | # Requires supporting ruby files with custom matchers and macros, etc, in 20 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are 21 | # run as spec files by default. This means that files in spec/support that end 22 | # in _spec.rb will both be required and run as specs, causing the specs to be 23 | # run twice. It is recommended that you do not name files matching this glob to 24 | # end with _spec.rb. You can configure this pattern with the --pattern 25 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. 26 | # 27 | # The following line is provided for convenience purposes. It has the downside 28 | # of increasing the boot-up time by auto-requiring all files in the support 29 | # directory. Alternatively, in the individual `*_spec.rb` files, manually 30 | # require only the support files necessary. 31 | # 32 | Dir[Rails.root.join("spec", "support", "**", "*.rb")].sort.each { |f| require f } 33 | 34 | # Checks for pending migrations and applies them before tests are run. 35 | # If you are not using ActiveRecord, you can remove these lines. 36 | begin 37 | ActiveRecord::Migration.maintain_test_schema! 38 | rescue ActiveRecord::PendingMigrationError => e 39 | abort e.to_s.strip 40 | end 41 | RSpec.configure do |config| 42 | config.include ViewComponent::TestHelpers, type: :component 43 | config.include Capybara::RSpecMatchers, type: :component 44 | 45 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 46 | config.fixture_paths = ["#{::Rails.root}/spec/fixtures"] 47 | config.global_fixtures = :all 48 | 49 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 50 | # examples within a transaction, remove the following line or assign false 51 | # instead of true. 52 | config.use_transactional_fixtures = true 53 | 54 | # You can uncomment this line to turn off ActiveRecord support entirely. 55 | # config.use_active_record = false 56 | 57 | # RSpec Rails can automatically mix in different behaviours to your tests 58 | # based on their file location, for example enabling you to call `get` and 59 | # `post` in specs under `spec/controllers`. 60 | # 61 | # You can disable this behaviour by removing the line below, and instead 62 | # explicitly tag your specs with their type, e.g.: 63 | # 64 | # RSpec.describe UsersController, type: :controller do 65 | # # ... 66 | # end 67 | # 68 | # The different available types are documented in the features, such as in 69 | # https://relishapp.com/rspec/rspec-rails/docs 70 | config.infer_spec_type_from_file_location! 71 | 72 | # Filter lines from Rails gems in backtraces. 73 | config.filter_rails_from_backtrace! 74 | # arbitrary gems may also be filtered via: 75 | # config.filter_gems_from_backtrace("gem name") 76 | end 77 | -------------------------------------------------------------------------------- /spec/requests/admin/masquerade_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe "Admin Masquerade as another user", type: :request do 4 | let(:admin_user) { users(:pete) } 5 | let(:normal_user) { users(:lazaro_nixon) } 6 | 7 | before do 8 | sign_in(admin_user) 9 | end 10 | 11 | describe "POST /admin/users/:id/masquerade" do 12 | it "masquerades as the given user and can reverse masquerade" do 13 | post masquerade_admin_user_path(normal_user) 14 | expect(response).to redirect_to(root_path) 15 | follow_redirect! 16 | expect(response.body).to include("You are masquerading as #{normal_user.email}") 17 | expect(response.body).to include("Return to admin") 18 | 19 | post reverse_masquerade_admin_user_path(admin_user) 20 | expect(response).to redirect_to(admin_users_path) 21 | follow_redirect! 22 | expect(response.body).not_to include("You are masquerading as #{normal_user.email}") 23 | expect(response.body).to include("Users") 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/requests/identity/account_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe Identity::AccountController, type: :request do 4 | let(:user) { users(:pete) } 5 | before { sign_in(user) } 6 | 7 | describe "#show" do 8 | it "should load without errors" do 9 | get identity_account_url 10 | expect(response).to have_http_status(:success) 11 | end 12 | end 13 | 14 | describe "#update" do 15 | it "should load without errors" do 16 | patch identity_account_url, params: { 17 | user: { 18 | first_name: "New first name", 19 | last_name: "New last name" 20 | } 21 | } 22 | expect(response).to redirect_to(identity_account_url) 23 | expect(user.reload.name).to eq("New first name New last name") 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/requests/identity/email_verifications_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe Identity::EmailVerificationsController, type: :request do 4 | let(:user) { users(:lazaro_nixon) } 5 | before do 6 | sign_in(user) 7 | user.update! verified: false 8 | end 9 | 10 | describe "#create" do 11 | it "should send a verification email" do 12 | perform_enqueued_jobs do 13 | post identity_email_verification_url 14 | 15 | expect(last_email.to.first).to eq(user.email) 16 | expect(last_email.subject).to eq("Verify your email") 17 | 18 | expect(response).to redirect_to(identity_account_path) 19 | end 20 | end 21 | end 22 | 23 | describe "#edit" do 24 | it "should verify email" do 25 | sid = user.email_verification_tokens.create.signed_id(expires_in: 2.days) 26 | 27 | get edit_identity_email_verification_url(sid: sid, email: user.email) 28 | expect(response).to redirect_to(identity_account_path) 29 | end 30 | 31 | it "should not verify email with expired token" do 32 | sid_exp = user.email_verification_tokens.create.signed_id(expires_in: 0.minutes) 33 | 34 | get edit_identity_email_verification_url(sid: sid_exp, email: user.email) 35 | 36 | expect(response).to redirect_to(identity_account_url) 37 | expect(flash[:alert]).to eq("That email verification link is invalid") 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/requests/identity/emails_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe Identity::EmailsController, type: :request do 4 | let(:user) { users(:lazaro_nixon) } 5 | before { sign_in(user) } 6 | 7 | describe "#update" do 8 | it "should update email" do 9 | patch identity_email_url, params: { email: "new_email@hey.com" } 10 | expect(response).to redirect_to(root_url) 11 | end 12 | 13 | it "responds with a turbo_stream when validation fails" do 14 | patch identity_email_url, params: { email: "new_email" } 15 | 16 | expect(response).to have_http_status(:success) 17 | assert_select("turbo-stream[action='replace'][target='change_email_form']", 1) 18 | expect(response.body).to include("Email is invalid") 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/requests/identity/password_resets_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe Identity::PasswordResetsController, type: :request do 4 | let(:user) { users(:lazaro_nixon) } 5 | 6 | describe "#new" do 7 | it "should get new" do 8 | get new_identity_password_reset_url 9 | expect(response).to have_http_status(:success) 10 | end 11 | end 12 | 13 | describe "#edit" do 14 | it "should get edit" do 15 | sid = user.password_reset_tokens.create.signed_id(expires_in: 20.minutes) 16 | 17 | get edit_identity_password_reset_url(sid: sid) 18 | expect(response).to have_http_status(:success) 19 | end 20 | end 21 | 22 | describe "#create" do 23 | it "should send a password reset email" do 24 | perform_enqueued_jobs do 25 | post identity_password_reset_url, params: { email: user.email } 26 | 27 | expect(last_email.to.first).to eq(user.email) 28 | expect(last_email.subject).to eq("Reset your password") 29 | 30 | expect(response).to redirect_to(sign_in_url) 31 | end 32 | end 33 | 34 | it "should not send a password reset email to a nonexistent email" do 35 | post identity_password_reset_url, params: { email: "invalid_email@hey.com" } 36 | 37 | expect(last_email).to be_nil 38 | 39 | expect(response).to redirect_to(new_identity_password_reset_url) 40 | expect(flash[:alert]).to eq("You can't reset your password until you verify your email") 41 | end 42 | 43 | it "should not send a password reset email to a unverified email" do 44 | user.update! verified: false 45 | 46 | post identity_password_reset_url, params: { email: user.email } 47 | 48 | expect(last_email).to be_nil 49 | 50 | expect(response).to redirect_to(new_identity_password_reset_url) 51 | expect(flash[:alert]).to eq("You can't reset your password until you verify your email") 52 | end 53 | end 54 | 55 | describe "#update" do 56 | it "should update password" do 57 | sid = user.password_reset_tokens.create.signed_id(expires_in: 20.minutes) 58 | 59 | patch identity_password_reset_url, params: { sid: sid, password: "Secret6*4*2*", password_confirmation: "Secret6*4*2*" } 60 | expect(response).to redirect_to(sign_in_url) 61 | end 62 | 63 | it "should not update password with expired token" do 64 | sid_exp = user.password_reset_tokens.create.signed_id(expires_in: 0.minutes) 65 | 66 | patch identity_password_reset_url, params: { sid: sid_exp, password: "Secret6*4*2*", password_confirmation: "Secret6*4*2*" } 67 | expect(response).to redirect_to(new_identity_password_reset_url) 68 | expect(flash[:alert]).to eq("That password reset link is invalid") 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/requests/pages_requests_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe PagesController, type: :request do 4 | describe "#home" do 5 | it "renders the home page" do 6 | get "/" 7 | expect(response).to have_http_status(:success) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/requests/passwords_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe PasswordsController, type: :request do 4 | let(:user) { users(:lazaro_nixon) } 5 | before { sign_in(user) } 6 | 7 | describe "#update" do 8 | it "should update password" do 9 | patch password_url, params: { current_password: "Secret1*3*5*", password: "Secret6*4*2*", password_confirmation: "Secret6*4*2*" } 10 | expect(response).to redirect_to(identity_account_url) 11 | end 12 | 13 | it "should not update password with wrong current password" do 14 | patch password_url, params: { current_password: "SecretWrong1*3", password: "Secret6*4*2*", password_confirmation: "Secret6*4*2*" } 15 | 16 | expect(response).to redirect_to(identity_account_url) 17 | expect(flash[:alert]).to eq("The current password you entered is incorrect") 18 | end 19 | 20 | it "should respond with a turbo stream when there are errors" do 21 | patch password_url, params: { format: :turbo_stream, current_password: "Secret1*3*5*", password: "dont", password_confirmation: "match" } 22 | 23 | expect(response).to have_http_status(:success) 24 | assert_select("turbo-stream[action='replace'][target='change_password_form']", 1) 25 | expect(response.body).to include("Password is invalid") 26 | end 27 | 28 | it "redirects to manage account when current_passowrd is invalid" do 29 | patch password_url, params: { format: :turbo_stream, current_password: "bad_password", password: "dont", password_confirmation: "match" } 30 | 31 | expect(response).to redirect_to(identity_account_url) 32 | expect(flash[:alert]).to eq("The current password you entered is incorrect") 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/requests/registrations_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe RegistrationsController, type: :request do 4 | describe "#new" do 5 | it "should get new" do 6 | get sign_up_url 7 | expect(response).to have_http_status(:success) 8 | end 9 | end 10 | 11 | describe "#create" do 12 | it "signs up the user and creates a team" do 13 | expect do 14 | post sign_up_url, params: { first_name: "New", last_name: "User", email: "newuser@hey.com", password: "Secret1*3*5*", password_confirmation: "Secret1*3*5*" } 15 | end.to change(User, :count).by(1) 16 | .and change(Team, :count).by(1) 17 | 18 | expect(response).to redirect_to(root_path) 19 | expect(User.last.team.name).to eq("New’s Team") 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/requests/sessions_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe SessionsController, type: :request do 4 | let(:user) { users(:lazaro_nixon) } 5 | 6 | describe "#index" do 7 | it "should get index" do 8 | sign_in(user) 9 | 10 | get sessions_url 11 | expect(response).to have_http_status(:success) 12 | end 13 | end 14 | 15 | describe "#new" do 16 | it "should get new" do 17 | get sign_in_url 18 | expect(response).to have_http_status(:success) 19 | end 20 | end 21 | 22 | describe "#create" do 23 | it "should sign in" do 24 | perform_enqueued_jobs do 25 | post sign_in_url, params: { email: user.email, password: "Secret1*3*5*" } 26 | 27 | expect(response).to redirect_to(root_path) 28 | 29 | get root_url 30 | expect(response).to have_http_status(:success) 31 | 32 | expect(last_email.to.first).to eq(user.email) 33 | expect(last_email.subject).to eq("New sign-in to your account") 34 | end 35 | end 36 | 37 | it "should not sign in with wrong credentials" do 38 | post sign_in_url, params: { email: user.email, password: "SecretWrong1*3" } 39 | expect(response).to redirect_to(sign_in_url(email_hint: user.email)) 40 | expect(flash[:alert]).to eq("That email or password is incorrect") 41 | end 42 | end 43 | 44 | describe "#destroy" do 45 | it "should sign out" do 46 | sign_in(user) 47 | 48 | delete session_url(user.sessions.last) 49 | expect(response).to redirect_to(sessions_url) 50 | 51 | follow_redirect! 52 | assert_redirected_to sign_in_url 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/services/disposable_email_service_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe DisposableEmailService do 4 | describe "#disposable?(email)" do 5 | it "returns false when the email domain is NOT disposable" do 6 | expect(described_class.disposable?("hi@rapidruby.com")).to be(false) 7 | end 8 | 9 | it "returns true when the email domain is disposable" do 10 | expect(described_class.disposable?("info@zzz-xxx.com")).to be(true) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/services/markdown_service_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe MarkdownService do 4 | describe ".to_html" do 5 | it "converts markdown to HTML" do 6 | html = described_class.to_html("# Hello") 7 | expect(html).to eq("

Hello

\n") 8 | end 9 | 10 | it "adds target: _blank and rel: noopener to links" do 11 | html = described_class.to_html("[Google](https://google.com)") 12 | expect(html).to eq('

Google

' + "\n") 13 | end 14 | 15 | it "handles nil" do 16 | html = described_class.to_html(nil) 17 | expect(html).to eq("") 18 | end 19 | 20 | it "handles invalid input like integers" do 21 | expect { described_class.to_html(42) }.to raise_error(ArgumentError, "Input must be a string or nil") 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rails generate rspec:install` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # The generated `.rspec` file contains `--require spec_helper` which will cause 4 | # this file to always be loaded, without a need to explicitly require it in any 5 | # files. 6 | # 7 | # Given that it is always loaded, you are encouraged to keep this file as 8 | # light-weight as possible. Requiring heavyweight dependencies from this file 9 | # will add to the boot time of your test suite on EVERY test run, even for an 10 | # individual file that may not need all of that loaded. Instead, consider making 11 | # a separate helper file that requires the additional dependencies and performs 12 | # the additional setup, and require it from the spec files that actually need 13 | # it. 14 | # 15 | # See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 16 | RSpec.configure do |config| 17 | # rspec-expectations config goes here. You can use an alternate 18 | # assertion/expectation library such as wrong or the stdlib/minitest 19 | # assertions if you prefer. 20 | config.expect_with :rspec do |expectations| 21 | # This option will default to `true` in RSpec 4. It makes the `description` 22 | # and `failure_message` of custom matchers include text for helper methods 23 | # defined using `chain`, e.g.: 24 | # be_bigger_than(2).and_smaller_than(4).description 25 | # # => "be bigger than 2 and smaller than 4" 26 | # ...rather than: 27 | # # => "be bigger than 2" 28 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 29 | end 30 | 31 | # rspec-mocks config goes here. You can use an alternate test double 32 | # library (such as bogus or mocha) by changing the `mock_with` option here. 33 | config.mock_with :rspec do |mocks| 34 | # Prevents you from mocking or stubbing a method that does not exist on 35 | # a real object. This is generally recommended, and will default to 36 | # `true` in RSpec 4. 37 | mocks.verify_partial_doubles = true 38 | end 39 | 40 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 41 | # have no way to turn it off -- the option exists only for backwards 42 | # compatibility in RSpec 3). It causes shared context metadata to be 43 | # inherited by the metadata hash of host groups and examples, rather than 44 | # triggering implicit auto-inclusion in groups with matching metadata. 45 | config.shared_context_metadata_behavior = :apply_to_host_groups 46 | 47 | # The settings below are suggested to provide a good initial experience 48 | # with RSpec, but feel free to customize to your heart's content. 49 | =begin 50 | # This allows you to limit a spec run to individual examples or groups 51 | # you care about by tagging them with `:focus` metadata. When nothing 52 | # is tagged with `:focus`, all examples get run. RSpec also provides 53 | # aliases for `it`, `describe`, and `context` that include `:focus` 54 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 55 | config.filter_run_when_matching :focus 56 | 57 | # Allows RSpec to persist some state between runs in order to support 58 | # the `--only-failures` and `--next-failure` CLI options. We recommend 59 | # you configure your source control system to ignore this file. 60 | config.example_status_persistence_file_path = "spec/examples.txt" 61 | 62 | # Limits the available syntax to the non-monkey patched syntax that is 63 | # recommended. For more details, see: 64 | # https://relishapp.com/rspec/rspec-core/docs/configuration/zero-monkey-patching-mode 65 | config.disable_monkey_patching! 66 | 67 | # Many RSpec users commonly either run the entire suite or an individual 68 | # file, and it's useful to allow more verbose output when running an 69 | # individual spec file. 70 | if config.files_to_run.one? 71 | # Use the documentation formatter for detailed output, 72 | # unless a formatter has already been configured 73 | # (e.g. via a command-line flag). 74 | config.default_formatter = "doc" 75 | end 76 | 77 | # Print the 10 slowest examples and example groups at the 78 | # end of the spec run, to help surface which specs are running 79 | # particularly slow. 80 | config.profile_examples = 10 81 | 82 | # Run specs in random order to surface order dependencies. If you find an 83 | # order dependency and want to debug it, you can fix the order by providing 84 | # the seed, which is printed after each run. 85 | # --seed 1234 86 | config.order = :random 87 | 88 | # Seed global randomization in this process using the `--seed` CLI option. 89 | # Setting this allows you to use `--seed` to deterministically reproduce 90 | # test failures related to randomization by passing the same `--seed` value 91 | # as the one that triggered the failure. 92 | Kernel.srand config.seed 93 | =end 94 | end 95 | -------------------------------------------------------------------------------- /spec/support/capybara_config.rb: -------------------------------------------------------------------------------- 1 | require "capybara/rspec" 2 | require "capybara/cuprite" 3 | 4 | Capybara.javascript_driver = :cuprite 5 | Capybara.register_driver(:cuprite) do |app| 6 | Capybara::Cuprite::Driver.new(app, window_size: [1200, 800], inspector: ENV["INSPECTOR"]) 7 | end 8 | -------------------------------------------------------------------------------- /spec/support/mail_helper.rb: -------------------------------------------------------------------------------- 1 | module MailHelper 2 | def reset_emails! 3 | ActionMailer::Base.deliveries.clear 4 | end 5 | 6 | def last_email 7 | ActionMailer::Base.deliveries.last 8 | end 9 | 10 | def delivered_emails 11 | ActionMailer::Base.deliveries 12 | end 13 | end 14 | 15 | RSpec.configure do |config| 16 | config.include MailHelper 17 | config.before(:each) { reset_emails! } 18 | end 19 | -------------------------------------------------------------------------------- /spec/support/request_helper.rb: -------------------------------------------------------------------------------- 1 | module RequestHelper 2 | def sign_in(user) 3 | post(sign_in_url, params: { email: user.email, password: "Secret1*3*5*" }); user 4 | end 5 | end 6 | 7 | RSpec.configure do |config| 8 | config.include RequestHelper, type: :request 9 | config.before(:each, type: :request) { host! "rapidrubystarter.test" } 10 | end 11 | -------------------------------------------------------------------------------- /spec/support/system_test_helper.rb: -------------------------------------------------------------------------------- 1 | module SystemTestHelper 2 | def sign_in_as(user) 3 | visit sign_in_url 4 | fill_in :email, with: user.email 5 | fill_in :password, with: "Secret1*3*5*" 6 | click_on "Sign in" 7 | 8 | assert_current_path root_path 9 | user 10 | end 11 | 12 | def open_debug! 13 | page.driver.debug(binding) 14 | end 15 | end 16 | 17 | RSpec.configure do |config| 18 | config.include SystemTestHelper, type: :feature 19 | end 20 | -------------------------------------------------------------------------------- /storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapidruby/rapid-ruby-starter/8b6700c7f9e974da31c728d7da167fffd2939faa/storage/.keep -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapidruby/rapid-ruby-starter/8b6700c7f9e974da31c728d7da167fffd2939faa/tmp/.keep -------------------------------------------------------------------------------- /tmp/pids/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapidruby/rapid-ruby-starter/8b6700c7f9e974da31c728d7da167fffd2939faa/tmp/pids/.keep -------------------------------------------------------------------------------- /tmp/storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapidruby/rapid-ruby-starter/8b6700c7f9e974da31c728d7da167fffd2939faa/tmp/storage/.keep -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapidruby/rapid-ruby-starter/8b6700c7f9e974da31c728d7da167fffd2939faa/vendor/.keep -------------------------------------------------------------------------------- /vendor/javascript/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapidruby/rapid-ruby-starter/8b6700c7f9e974da31c728d7da167fffd2939faa/vendor/javascript/.keep --------------------------------------------------------------------------------