From b4eb76e1b3c113015e5fd8c0e84be9378fe9e68f Mon Sep 17 00:00:00 2001 From: Ben Purinton Date: Fri, 21 Mar 2025 09:46:53 -0700 Subject: [PATCH 1/5] Part 3 solutions --- app/controllers/application_controller.rb | 2 + app/controllers/movies_controller.rb | 23 +- app/models/user.rb | 26 ++ app/views/layouts/application.html.erb | 14 +- app/views/movies/_form.html.erb | 19 ++ app/views/movies/_movie_card.html.erb | 44 +++ app/views/movies/edit.html.erb | 20 +- app/views/movies/index.html.erb | 56 +--- app/views/movies/new.html.erb | 20 +- app/views/movies/show.html.erb | 52 +-- app/views/shared/_cdn_assets.html.erb | 8 + app/views/shared/_flash_messages.html.erb | 11 + app/views/shared/_navbar.html.erb | 30 ++ config/environments/development.rb | 3 + config/initializers/devise.rb | 313 ++++++++++++++++++ config/locales/devise.en.yml | 65 ++++ config/routes.rb | 1 + .../20250321162355_devise_create_users.rb | 46 +++ db/schema.rb | 16 +- erd.png | Bin 0 -> 32558 bytes 20 files changed, 613 insertions(+), 156 deletions(-) create mode 100644 app/models/user.rb create mode 100644 app/views/movies/_form.html.erb create mode 100644 app/views/movies/_movie_card.html.erb create mode 100644 app/views/shared/_cdn_assets.html.erb create mode 100644 app/views/shared/_flash_messages.html.erb create mode 100644 app/views/shared/_navbar.html.erb create mode 100644 config/initializers/devise.rb create mode 100644 config/locales/devise.en.yml create mode 100644 db/migrate/20250321162355_devise_create_users.rb create mode 100644 erd.png diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0d95db22..f4427639 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,4 +1,6 @@ class ApplicationController < ActionController::Base # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. allow_browser versions: :modern + + before_action :authenticate_user! end diff --git a/app/controllers/movies_controller.rb b/app/controllers/movies_controller.rb index 012b295d..de144649 100644 --- a/app/controllers/movies_controller.rb +++ b/app/controllers/movies_controller.rb @@ -1,4 +1,6 @@ class MoviesController < ApplicationController + before_action :set_movie, only: [:show, :edit, :update, :destroy] + def new @movie = Movie.new end @@ -14,12 +16,9 @@ def index end def show - @movie = Movie.find(params.fetch(:id)) end def create - movie_params = params.require(:movie).permit(:title, :description) - @movie = Movie.new(movie_params) if @movie.valid? @@ -32,14 +31,9 @@ def create end def edit - @movie = Movie.find(params.fetch(:id)) end def update - @movie = Movie.find(params.fetch(:id)) - - movie_params = params.require(:movie).permit(:title, :description) - if @movie.update(movie_params) redirect_to @movie, notice: "Movie was successfully updated." else @@ -48,10 +42,19 @@ def update end def destroy - @movie = Movie.find(params.fetch(:id)) - @movie.destroy redirect_to movies_url, notice: "Movie was successfully destroyed." end + + private + + def movie_params + movie_params = params.require(:movie).permit(:title, :description) + end + + def set_movie + @movie = Movie.find(params.fetch(:id)) + end + end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 00000000..1233c26e --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,26 @@ +# == Schema Information +# +# Table name: users +# +# id :bigint not null, primary key +# email :string default(""), not null +# encrypted_password :string default(""), not null +# first_name :string +# last_name :string +# remember_created_at :datetime +# reset_password_sent_at :datetime +# reset_password_token :string +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_users_on_email (email) UNIQUE +# index_users_on_reset_password_token (reset_password_token) UNIQUE +# +class User < ApplicationRecord + # Include default devise modules. Others available are: + # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable + devise :database_authenticatable, :registerable, + :recoverable, :rememberable, :validatable +end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 12367a3b..bf305fe2 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -8,6 +8,8 @@ <%= csrf_meta_tags %> <%= csp_meta_tag %> + <%= render "shared/cdn_assets" %> + <%= yield :head %> <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %> @@ -23,14 +25,12 @@ -
- <%= notice %> -
+ <%= render "shared/navbar" %> + +
+ <%= render "shared/flash_messages" %> -
- <%= alert %> + <%= yield %>
- - <%= yield %> diff --git a/app/views/movies/_form.html.erb b/app/views/movies/_form.html.erb new file mode 100644 index 00000000..bb594ee3 --- /dev/null +++ b/app/views/movies/_form.html.erb @@ -0,0 +1,19 @@ +<% zebra.errors.full_messages.each do |message| %> +

<%= message %>

+<% end %> + +<%= form_with(model: zebra, data: { turbo: false }) do |form| %> +
+ <%= form.label :title %> + <%= form.text_field :title %> +
+ +
+ <%= form.label :description %> + <%= form.text_area :description %> +
+ +
+ <%= form.submit %> +
+<% end %> diff --git a/app/views/movies/_movie_card.html.erb b/app/views/movies/_movie_card.html.erb new file mode 100644 index 00000000..6cb02152 --- /dev/null +++ b/app/views/movies/_movie_card.html.erb @@ -0,0 +1,44 @@ +
+
+ <%= link_to "Movie ##{giraffe.id}", giraffe %> +
+ +
+
+
+ Title +
+
+ <%= giraffe.title %> +
+ +
+ Description +
+
+ <%= giraffe.description %> +
+
+ +
+
+
+ <%= link_to edit_movie_path(giraffe), class: "btn btn-outline-secondary" do %> + + <% end %> +
+
+
+
+ <%= link_to giraffe, class: "btn btn-outline-secondary", data: { turbo_method: :delete } do %> + + <% end %> +
+
+
+
+ + +
diff --git a/app/views/movies/edit.html.erb b/app/views/movies/edit.html.erb index 0d3d2485..8cc65419 100644 --- a/app/views/movies/edit.html.erb +++ b/app/views/movies/edit.html.erb @@ -1,21 +1,3 @@

Edit movie

-<% @movie.errors.full_messages.each do |message| %> -

<%= message %>

-<% end %> - -<%= form_with(model: @movie, data: { turbo: :false }) do |form| %> -
- <%= form.label :title %> - <%= form.text_field :title %> -
- -
- <%= form.label :description %> - <%= form.text_area :description %> -
- -
- <%= form.submit %> -
-<% end %> +<%= render partial: "movies/form", locals: { zebra: @movie } %> diff --git a/app/views/movies/index.html.erb b/app/views/movies/index.html.erb index 1d868e73..d96c109b 100644 --- a/app/views/movies/index.html.erb +++ b/app/views/movies/index.html.erb @@ -10,56 +10,10 @@
- - - - - - - - - - - - - - - +
<% @movies.each do |movie| %> -
- - - - - - - - - - - +
+ <%= render partial: "movies/movie_card", locals: { giraffe: movie } %> +
<% end %> -
- ID - - Title - - Description - - Created at - - Updated at - -
- <%= movie.id %> - - <%= movie.title %> - - <%= movie.description %> - - <%= time_ago_in_words(movie.created_at) %> ago - - <%= time_ago_in_words(movie.updated_at) %> ago - - <%= link_to "Show details", movie %> -
+
diff --git a/app/views/movies/new.html.erb b/app/views/movies/new.html.erb index c044dcca..245b3484 100644 --- a/app/views/movies/new.html.erb +++ b/app/views/movies/new.html.erb @@ -1,21 +1,3 @@

New movie

-<% @movie.errors.full_messages.each do |message| %> -

<%= message %>

-<% end %> - -<%= form_with(model: @movie, data: { turbo: :false }) do |form| %> -
- <%= form.label :title %> - <%= form.text_field :title %> -
- -
- <%= form.label :description %> - <%= form.text_area :description %> -
- -
- <%= form.submit %> -
-<% end %> +<%= render partial: "movies/form", locals: { zebra: @movie } %> diff --git a/app/views/movies/show.html.erb b/app/views/movies/show.html.erb index c088c55e..5e5bb7c8 100644 --- a/app/views/movies/show.html.erb +++ b/app/views/movies/show.html.erb @@ -1,51 +1,5 @@ -
-
-

- Movie #<%= @movie.id %> details -

- -
-
- <%= link_to "Go back", movies_path %> -
- -
- <%= link_to "Edit Movie", edit_movie_path(@movie) %> -
- -
- <%= link_to "Delete Movie", @movie, data: { turbo_method: :delete } %> -
-
- -
-
- Title -
-
- <%= @movie.title %> -
- -
- Description -
-
- <%= @movie.description %> -
- -
- Created at -
-
- <%= time_ago_in_words(@movie.created_at) %> ago -
- -
- Updated at -
-
- <%= time_ago_in_words(@movie.updated_at) %> ago -
-
+
+
+ <%= render partial: "movies/movie_card", locals: { giraffe: @movie } %>
diff --git a/app/views/shared/_cdn_assets.html.erb b/app/views/shared/_cdn_assets.html.erb new file mode 100644 index 00000000..8c88f257 --- /dev/null +++ b/app/views/shared/_cdn_assets.html.erb @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/app/views/shared/_flash_messages.html.erb b/app/views/shared/_flash_messages.html.erb new file mode 100644 index 00000000..b8234d92 --- /dev/null +++ b/app/views/shared/_flash_messages.html.erb @@ -0,0 +1,11 @@ +<% if notice.present? %> + +<% end %> + +<% if alert.present? %> + +<% end %> diff --git a/app/views/shared/_navbar.html.erb b/app/views/shared/_navbar.html.erb new file mode 100644 index 00000000..52dc83e9 --- /dev/null +++ b/app/views/shared/_navbar.html.erb @@ -0,0 +1,30 @@ + diff --git a/config/environments/development.rb b/config/environments/development.rb index 355b1712..fcf075d1 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,6 +1,9 @@ require "active_support/core_ext/integer/time" Rails.application.configure do + # Devise action mailer setup + config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } + # Allow server to be hosted on any URL config.hosts.clear # Allow better_errors to work in online IDE diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb new file mode 100644 index 00000000..b20ba361 --- /dev/null +++ b/config/initializers/devise.rb @@ -0,0 +1,313 @@ +# frozen_string_literal: true + +# Assuming you have not yet modified this file, each configuration option below +# is set to its default value. Note that some are commented out while others +# are not: uncommented lines are intended to protect your configuration from +# breaking changes in upgrades (i.e., in the event that future versions of +# Devise change the default values for those options). +# +# Use this hook to configure devise mailer, warden hooks and so forth. +# Many of these configuration options can be set straight in your model. +Devise.setup do |config| + # The secret key used by Devise. Devise uses this key to generate + # random tokens. Changing this key will render invalid all existing + # confirmation, reset password and unlock tokens in the database. + # Devise will use the `secret_key_base` as its `secret_key` + # by default. You can change it below and use your own secret key. + # config.secret_key = '8b4052c2857c53d2ca396f41e8798b66f63b063c66e15c6fb1931843d6f449c59e13f961ec48814f49595702bb3145334abf31742545d0d2a5ba50624ef3bdad' + + # ==> Controller configuration + # Configure the parent class to the devise controllers. + # config.parent_controller = 'DeviseController' + + # ==> Mailer Configuration + # Configure the e-mail address which will be shown in Devise::Mailer, + # note that it will be overwritten if you use your own mailer class + # with default "from" parameter. + config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' + + # Configure the class responsible to send e-mails. + # config.mailer = 'Devise::Mailer' + + # Configure the parent class responsible to send e-mails. + # config.parent_mailer = 'ActionMailer::Base' + + # ==> ORM configuration + # Load and configure the ORM. Supports :active_record (default) and + # :mongoid (bson_ext recommended) by default. Other ORMs may be + # available as additional gems. + require 'devise/orm/active_record' + + # ==> Configuration for any authentication mechanism + # Configure which keys are used when authenticating a user. The default is + # just :email. You can configure it to use [:username, :subdomain], so for + # authenticating a user, both parameters are required. Remember that those + # parameters are used only when authenticating and not when retrieving from + # session. If you need permissions, you should implement that in a before filter. + # You can also supply a hash where the value is a boolean determining whether + # or not authentication should be aborted when the value is not present. + # config.authentication_keys = [:email] + + # Configure parameters from the request object used for authentication. Each entry + # given should be a request method and it will automatically be passed to the + # find_for_authentication method and considered in your model lookup. For instance, + # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. + # The same considerations mentioned for authentication_keys also apply to request_keys. + # config.request_keys = [] + + # Configure which authentication keys should be case-insensitive. + # These keys will be downcased upon creating or modifying a user and when used + # to authenticate or find a user. Default is :email. + config.case_insensitive_keys = [:email] + + # Configure which authentication keys should have whitespace stripped. + # These keys will have whitespace before and after removed upon creating or + # modifying a user and when used to authenticate or find a user. Default is :email. + config.strip_whitespace_keys = [:email] + + # Tell if authentication through request.params is enabled. True by default. + # It can be set to an array that will enable params authentication only for the + # given strategies, for example, `config.params_authenticatable = [:database]` will + # enable it only for database (email + password) authentication. + # config.params_authenticatable = true + + # Tell if authentication through HTTP Auth is enabled. False by default. + # It can be set to an array that will enable http authentication only for the + # given strategies, for example, `config.http_authenticatable = [:database]` will + # enable it only for database authentication. + # For API-only applications to support authentication "out-of-the-box", you will likely want to + # enable this with :database unless you are using a custom strategy. + # The supported strategies are: + # :database = Support basic authentication with authentication key + password + # config.http_authenticatable = false + + # If 401 status code should be returned for AJAX requests. True by default. + # config.http_authenticatable_on_xhr = true + + # The realm used in Http Basic Authentication. 'Application' by default. + # config.http_authentication_realm = 'Application' + + # It will change confirmation, password recovery and other workflows + # to behave the same regardless if the e-mail provided was right or wrong. + # Does not affect registerable. + # config.paranoid = true + + # By default Devise will store the user in session. You can skip storage for + # particular strategies by setting this option. + # Notice that if you are skipping storage for all authentication paths, you + # may want to disable generating routes to Devise's sessions controller by + # passing skip: :sessions to `devise_for` in your config/routes.rb + config.skip_session_storage = [:http_auth] + + # By default, Devise cleans up the CSRF token on authentication to + # avoid CSRF token fixation attacks. This means that, when using AJAX + # requests for sign in and sign up, you need to get a new CSRF token + # from the server. You can disable this option at your own risk. + # config.clean_up_csrf_token_on_authentication = true + + # When false, Devise will not attempt to reload routes on eager load. + # This can reduce the time taken to boot the app but if your application + # requires the Devise mappings to be loaded during boot time the application + # won't boot properly. + # config.reload_routes = true + + # ==> Configuration for :database_authenticatable + # For bcrypt, this is the cost for hashing the password and defaults to 12. If + # using other algorithms, it sets how many times you want the password to be hashed. + # The number of stretches used for generating the hashed password are stored + # with the hashed password. This allows you to change the stretches without + # invalidating existing passwords. + # + # Limiting the stretches to just one in testing will increase the performance of + # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use + # a value less than 10 in other environments. Note that, for bcrypt (the default + # algorithm), the cost increases exponentially with the number of stretches (e.g. + # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). + config.stretches = Rails.env.test? ? 1 : 12 + + # Set up a pepper to generate the hashed password. + # config.pepper = '5d710e2c925df3b9cac96d623eb478eb88e169a7f8a7b9bc7323268a63678f2c96ecfe46712c073e78453146b98be08114f6da16c3f0197c436dacb3ebdd69be' + + # Send a notification to the original email when the user's email is changed. + # config.send_email_changed_notification = false + + # Send a notification email when the user's password is changed. + # config.send_password_change_notification = false + + # ==> Configuration for :confirmable + # A period that the user is allowed to access the website even without + # confirming their account. For instance, if set to 2.days, the user will be + # able to access the website for two days without confirming their account, + # access will be blocked just in the third day. + # You can also set it to nil, which will allow the user to access the website + # without confirming their account. + # Default is 0.days, meaning the user cannot access the website without + # confirming their account. + # config.allow_unconfirmed_access_for = 2.days + + # A period that the user is allowed to confirm their account before their + # token becomes invalid. For example, if set to 3.days, the user can confirm + # their account within 3 days after the mail was sent, but on the fourth day + # their account can't be confirmed with the token any more. + # Default is nil, meaning there is no restriction on how long a user can take + # before confirming their account. + # config.confirm_within = 3.days + + # If true, requires any email changes to be confirmed (exactly the same way as + # initial account confirmation) to be applied. Requires additional unconfirmed_email + # db field (see migrations). Until confirmed, new email is stored in + # unconfirmed_email column, and copied to email column on successful confirmation. + config.reconfirmable = true + + # Defines which key will be used when confirming an account + # config.confirmation_keys = [:email] + + # ==> Configuration for :rememberable + # The time the user will be remembered without asking for credentials again. + # config.remember_for = 2.weeks + + # Invalidates all the remember me tokens when the user signs out. + config.expire_all_remember_me_on_sign_out = true + + # If true, extends the user's remember period when remembered via cookie. + # config.extend_remember_period = false + + # Options to be passed to the created cookie. For instance, you can set + # secure: true in order to force SSL only cookies. + # config.rememberable_options = {} + + # ==> Configuration for :validatable + # Range for password length. + config.password_length = 6..128 + + # Email regex used to validate email formats. It simply asserts that + # one (and only one) @ exists in the given string. This is mainly + # to give user feedback and not to assert the e-mail validity. + config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ + + # ==> Configuration for :timeoutable + # The time you want to timeout the user session without activity. After this + # time the user will be asked for credentials again. Default is 30 minutes. + # config.timeout_in = 30.minutes + + # ==> Configuration for :lockable + # Defines which strategy will be used to lock an account. + # :failed_attempts = Locks an account after a number of failed attempts to sign in. + # :none = No lock strategy. You should handle locking by yourself. + # config.lock_strategy = :failed_attempts + + # Defines which key will be used when locking and unlocking an account + # config.unlock_keys = [:email] + + # Defines which strategy will be used to unlock an account. + # :email = Sends an unlock link to the user email + # :time = Re-enables login after a certain amount of time (see :unlock_in below) + # :both = Enables both strategies + # :none = No unlock strategy. You should handle unlocking by yourself. + # config.unlock_strategy = :both + + # Number of authentication tries before locking an account if lock_strategy + # is failed attempts. + # config.maximum_attempts = 20 + + # Time interval to unlock the account if :time is enabled as unlock_strategy. + # config.unlock_in = 1.hour + + # Warn on the last attempt before the account is locked. + # config.last_attempt_warning = true + + # ==> Configuration for :recoverable + # + # Defines which key will be used when recovering the password for an account + # config.reset_password_keys = [:email] + + # Time interval you can reset your password with a reset password key. + # Don't put a too small interval or your users won't have the time to + # change their passwords. + config.reset_password_within = 6.hours + + # When set to false, does not sign a user in automatically after their password is + # reset. Defaults to true, so a user is signed in automatically after a reset. + # config.sign_in_after_reset_password = true + + # ==> Configuration for :encryptable + # Allow you to use another hashing or encryption algorithm besides bcrypt (default). + # You can use :sha1, :sha512 or algorithms from others authentication tools as + # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20 + # for default behavior) and :restful_authentication_sha1 (then you should set + # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper). + # + # Require the `devise-encryptable` gem when using anything other than bcrypt + # config.encryptor = :sha512 + + # ==> Scopes configuration + # Turn scoped views on. Before rendering "sessions/new", it will first check for + # "users/sessions/new". It's turned off by default because it's slower if you + # are using only default views. + # config.scoped_views = false + + # Configure the default scope given to Warden. By default it's the first + # devise role declared in your routes (usually :user). + # config.default_scope = :user + + # Set this configuration to false if you want /users/sign_out to sign out + # only the current scope. By default, Devise signs out all scopes. + # config.sign_out_all_scopes = true + + # ==> Navigation configuration + # Lists the formats that should be treated as navigational. Formats like + # :html should redirect to the sign in page when the user does not have + # access, but formats like :xml or :json, should return 401. + # + # If you have any extra navigational formats, like :iphone or :mobile, you + # should add them to the navigational formats lists. + # + # The "*/*" below is required to match Internet Explorer requests. + # config.navigational_formats = ['*/*', :html, :turbo_stream] + + # The default HTTP method used to sign out a resource. Default is :delete. + config.sign_out_via = :delete + + # ==> OmniAuth + # Add a new OmniAuth provider. Check the wiki for more information on setting + # up on your models and hooks. + # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' + + # ==> Warden configuration + # If you want to use other strategies, that are not supported by Devise, or + # change the failure app, you can configure them inside the config.warden block. + # + # config.warden do |manager| + # manager.intercept_401 = false + # manager.default_strategies(scope: :user).unshift :some_external_strategy + # end + + # ==> Mountable engine configurations + # When using Devise inside an engine, let's call it `MyEngine`, and this engine + # is mountable, there are some extra configurations to be taken into account. + # The following options are available, assuming the engine is mounted as: + # + # mount MyEngine, at: '/my_engine' + # + # The router that invoked `devise_for`, in the example above, would be: + # config.router_name = :my_engine + # + # When using OmniAuth, Devise cannot automatically set OmniAuth path, + # so you need to do it manually. For the users scope, it would be: + # config.omniauth_path_prefix = '/my_engine/users/auth' + + # ==> Hotwire/Turbo configuration + # When using Devise with Hotwire/Turbo, the http status for error responses + # and some redirects must match the following. The default in Devise for existing + # apps is `200 OK` and `302 Found` respectively, but new apps are generated with + # these new defaults that match Hotwire/Turbo behavior. + # Note: These might become the new default in future versions of Devise. + config.responder.error_status = :unprocessable_entity + config.responder.redirect_status = :see_other + + # ==> Configuration for :registerable + + # When set to false, does not sign a user in automatically after their password is + # changed. Defaults to true, so a user is signed in automatically after changing a password. + # config.sign_in_after_change_password = true +end diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml new file mode 100644 index 00000000..260e1c4b --- /dev/null +++ b/config/locales/devise.en.yml @@ -0,0 +1,65 @@ +# Additional translations at https://github.com/heartcombo/devise/wiki/I18n + +en: + devise: + confirmations: + confirmed: "Your email address has been successfully confirmed." + send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." + failure: + already_authenticated: "You are already signed in." + inactive: "Your account is not activated yet." + invalid: "Invalid %{authentication_keys} or password." + locked: "Your account is locked." + last_attempt: "You have one more attempt before your account is locked." + not_found_in_database: "Invalid %{authentication_keys} or password." + timeout: "Your session expired. Please sign in again to continue." + unauthenticated: "You need to sign in or sign up before continuing." + unconfirmed: "You have to confirm your email address before continuing." + mailer: + confirmation_instructions: + subject: "Confirmation instructions" + reset_password_instructions: + subject: "Reset password instructions" + unlock_instructions: + subject: "Unlock instructions" + email_changed: + subject: "Email Changed" + password_change: + subject: "Password Changed" + omniauth_callbacks: + failure: "Could not authenticate you from %{kind} because \"%{reason}\"." + success: "Successfully authenticated from %{kind} account." + passwords: + no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." + send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." + updated: "Your password has been changed successfully. You are now signed in." + updated_not_active: "Your password has been changed successfully." + registrations: + destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." + signed_up: "Welcome! You have signed up successfully." + signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." + signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." + signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." + update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirmation link to confirm your new email address." + updated: "Your account has been updated successfully." + updated_but_not_signed_in: "Your account has been updated successfully, but since your password was changed, you need to sign in again." + sessions: + signed_in: "Signed in successfully." + signed_out: "Signed out successfully." + already_signed_out: "Signed out successfully." + unlocks: + send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." + send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." + unlocked: "Your account has been unlocked successfully. Please sign in to continue." + errors: + messages: + already_confirmed: "was already confirmed, please try signing in" + confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" + expired: "has expired, please request a new one" + not_found: "not found" + not_locked: "was not locked" + not_saved: + one: "1 error prohibited this %{resource} from being saved:" + other: "%{count} errors prohibited this %{resource} from being saved:" diff --git a/config/routes.rb b/config/routes.rb index 56520c96..6a8a2f92 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,5 @@ Rails.application.routes.draw do + devise_for :users root "movies#index" resources :movies diff --git a/db/migrate/20250321162355_devise_create_users.rb b/db/migrate/20250321162355_devise_create_users.rb new file mode 100644 index 00000000..37eb81a3 --- /dev/null +++ b/db/migrate/20250321162355_devise_create_users.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +class DeviseCreateUsers < ActiveRecord::Migration[8.0] + def change + create_table :users do |t| + ## Database authenticatable + t.string :email, null: false, default: "" + t.string :encrypted_password, null: false, default: "" + + ## Recoverable + t.string :reset_password_token + t.datetime :reset_password_sent_at + + ## Rememberable + t.datetime :remember_created_at + + ## Trackable + # t.integer :sign_in_count, default: 0, null: false + # t.datetime :current_sign_in_at + # t.datetime :last_sign_in_at + # t.string :current_sign_in_ip + # t.string :last_sign_in_ip + + ## Confirmable + # t.string :confirmation_token + # t.datetime :confirmed_at + # t.datetime :confirmation_sent_at + # t.string :unconfirmed_email # Only if using reconfirmable + + ## Lockable + # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts + # t.string :unlock_token # Only if unlock strategy is :email or :both + # t.datetime :locked_at + + t.string :first_name + t.string :last_name + + t.timestamps null: false + end + + add_index :users, :email, unique: true + add_index :users, :reset_password_token, unique: true + # add_index :users, :confirmation_token, unique: true + # add_index :users, :unlock_token, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 212926d9..554553d8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2021_04_09_010404) do +ActiveRecord::Schema[8.0].define(version: 2025_03_21_162355) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -20,4 +20,18 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false end + + create_table "users", force: :cascade do |t| + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false + t.string "reset_password_token" + t.datetime "reset_password_sent_at" + t.datetime "remember_created_at" + t.string "first_name" + t.string "last_name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["email"], name: "index_users_on_email", unique: true + t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true + end end diff --git a/erd.png b/erd.png new file mode 100644 index 0000000000000000000000000000000000000000..9bcfbeec78a915ad517c7491b87c61c4b67b8f82 GIT binary patch literal 32558 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 literal 0 HcmV?d00001 From 2ddd2004262ba6f390fec6f9bbe1900dca3a006c Mon Sep 17 00:00:00 2001 From: Ben Purinton Date: Fri, 21 Mar 2025 09:49:52 -0700 Subject: [PATCH 2/5] Rubocop -A --- app/controllers/movies_controller.rb | 3 +-- config/environments/development.rb | 2 +- config/initializers/devise.rb | 10 +++++----- spec/features/basic_spec.rb | 2 +- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/controllers/movies_controller.rb b/app/controllers/movies_controller.rb index de144649..65437814 100644 --- a/app/controllers/movies_controller.rb +++ b/app/controllers/movies_controller.rb @@ -1,5 +1,5 @@ class MoviesController < ApplicationController - before_action :set_movie, only: [:show, :edit, :update, :destroy] + before_action :set_movie, only: [ :show, :edit, :update, :destroy ] def new @movie = Movie.new @@ -56,5 +56,4 @@ def movie_params def set_movie @movie = Movie.find(params.fetch(:id)) end - end diff --git a/config/environments/development.rb b/config/environments/development.rb index fcf075d1..a211b5e8 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -2,7 +2,7 @@ Rails.application.configure do # Devise action mailer setup - config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } + config.action_mailer.default_url_options = { host: "localhost", port: 3000 } # Allow server to be hosted on any URL config.hosts.clear diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index b20ba361..ecb149b1 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -24,7 +24,7 @@ # Configure the e-mail address which will be shown in Devise::Mailer, # note that it will be overwritten if you use your own mailer class # with default "from" parameter. - config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' + config.mailer_sender = "please-change-me-at-config-initializers-devise@example.com" # Configure the class responsible to send e-mails. # config.mailer = 'Devise::Mailer' @@ -36,7 +36,7 @@ # Load and configure the ORM. Supports :active_record (default) and # :mongoid (bson_ext recommended) by default. Other ORMs may be # available as additional gems. - require 'devise/orm/active_record' + require "devise/orm/active_record" # ==> Configuration for any authentication mechanism # Configure which keys are used when authenticating a user. The default is @@ -58,12 +58,12 @@ # Configure which authentication keys should be case-insensitive. # These keys will be downcased upon creating or modifying a user and when used # to authenticate or find a user. Default is :email. - config.case_insensitive_keys = [:email] + config.case_insensitive_keys = [ :email ] # Configure which authentication keys should have whitespace stripped. # These keys will have whitespace before and after removed upon creating or # modifying a user and when used to authenticate or find a user. Default is :email. - config.strip_whitespace_keys = [:email] + config.strip_whitespace_keys = [ :email ] # Tell if authentication through request.params is enabled. True by default. # It can be set to an array that will enable params authentication only for the @@ -97,7 +97,7 @@ # Notice that if you are skipping storage for all authentication paths, you # may want to disable generating routes to Devise's sessions controller by # passing skip: :sessions to `devise_for` in your config/routes.rb - config.skip_session_storage = [:http_auth] + config.skip_session_storage = [ :http_auth ] # By default, Devise cleans up the CSRF token on authentication to # avoid CSRF token fixation attacks. This means that, when using AJAX diff --git a/spec/features/basic_spec.rb b/spec/features/basic_spec.rb index 171642b1..ebb331db 100644 --- a/spec/features/basic_spec.rb +++ b/spec/features/basic_spec.rb @@ -172,7 +172,7 @@ describe "User authentication with the Devise gem" do it "requires sign in before visiting /movies/new with the Devise `before_action :authenticate_user!` method", points: 1 do visit "/movies/new" - + current_url = page.current_path expect(current_url).to eq("/users/sign_in") From e5dc0ebf86ffe94d5e20896b87b34c4db384d379 Mon Sep 17 00:00:00 2001 From: Ben Purinton Date: Fri, 21 Mar 2025 10:04:47 -0700 Subject: [PATCH 3/5] conventional partial local names --- app/views/movies/_form.html.erb | 4 ++-- app/views/movies/_movie_card.html.erb | 12 ++++++------ app/views/movies/edit.html.erb | 2 +- app/views/movies/index.html.erb | 2 +- app/views/movies/new.html.erb | 2 +- app/views/movies/show.html.erb | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/views/movies/_form.html.erb b/app/views/movies/_form.html.erb index bb594ee3..d1d990b9 100644 --- a/app/views/movies/_form.html.erb +++ b/app/views/movies/_form.html.erb @@ -1,8 +1,8 @@ -<% zebra.errors.full_messages.each do |message| %> +<% movie.errors.full_messages.each do |message| %>

<%= message %>

<% end %> -<%= form_with(model: zebra, data: { turbo: false }) do |form| %> +<%= form_with(model: movie, data: { turbo: false }) do |form| %>
<%= form.label :title %> <%= form.text_field :title %> diff --git a/app/views/movies/_movie_card.html.erb b/app/views/movies/_movie_card.html.erb index 6cb02152..f1f7052f 100644 --- a/app/views/movies/_movie_card.html.erb +++ b/app/views/movies/_movie_card.html.erb @@ -1,6 +1,6 @@
- <%= link_to "Movie ##{giraffe.id}", giraffe %> + <%= link_to "Movie ##{movie.id}", movie %>
@@ -9,28 +9,28 @@ Title
- <%= giraffe.title %> + <%= movie.title %>
Description
- <%= giraffe.description %> + <%= movie.description %>
- <%= link_to edit_movie_path(giraffe), class: "btn btn-outline-secondary" do %> + <%= link_to edit_movie_path(movie), class: "btn btn-outline-secondary" do %> <% end %>
- <%= link_to giraffe, class: "btn btn-outline-secondary", data: { turbo_method: :delete } do %> + <%= link_to movie, class: "btn btn-outline-secondary", data: { turbo_method: :delete } do %> <% end %>
@@ -39,6 +39,6 @@
diff --git a/app/views/movies/edit.html.erb b/app/views/movies/edit.html.erb index 8cc65419..18af35eb 100644 --- a/app/views/movies/edit.html.erb +++ b/app/views/movies/edit.html.erb @@ -1,3 +1,3 @@

Edit movie

-<%= render partial: "movies/form", locals: { zebra: @movie } %> +<%= render partial: "movies/form", locals: { movie: @movie } %> diff --git a/app/views/movies/index.html.erb b/app/views/movies/index.html.erb index d96c109b..2d7bb287 100644 --- a/app/views/movies/index.html.erb +++ b/app/views/movies/index.html.erb @@ -13,7 +13,7 @@
<% @movies.each do |movie| %>
- <%= render partial: "movies/movie_card", locals: { giraffe: movie } %> + <%= render partial: "movies/movie_card", locals: { movie: movie } %>
<% end %>
diff --git a/app/views/movies/new.html.erb b/app/views/movies/new.html.erb index 245b3484..bbb9616e 100644 --- a/app/views/movies/new.html.erb +++ b/app/views/movies/new.html.erb @@ -1,3 +1,3 @@

New movie

-<%= render partial: "movies/form", locals: { zebra: @movie } %> +<%= render partial: "movies/form", locals: { movie: @movie } %> diff --git a/app/views/movies/show.html.erb b/app/views/movies/show.html.erb index 5e5bb7c8..3f6c328e 100644 --- a/app/views/movies/show.html.erb +++ b/app/views/movies/show.html.erb @@ -1,5 +1,5 @@
- <%= render partial: "movies/movie_card", locals: { giraffe: @movie } %> + <%= render partial: "movies/movie_card", locals: { movie: @movie } %>
From 8f0cd06f956bdc68fddbd64203e019d7261cb879 Mon Sep 17 00:00:00 2001 From: Ben Purinton Date: Fri, 4 Apr 2025 10:32:20 -0700 Subject: [PATCH 4/5] Turbo off by default --- app/javascript/application.js | 4 ++++ app/views/movies/_form.html.erb | 2 +- app/views/movies/_movie_card.html.erb | 2 +- app/views/shared/_navbar.html.erb | 2 +- spec/features/basic_spec.rb | 2 +- 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/javascript/application.js b/app/javascript/application.js index 85130579..ef1c2c1e 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -2,6 +2,10 @@ import "@hotwired/turbo-rails" import "controllers" +// Change to true to allow Turbo +Turbo.session.drive = false + +// Allow UJS alongside Turbo import jquery from "jquery"; window.jQuery = jquery; window.$ = jquery; diff --git a/app/views/movies/_form.html.erb b/app/views/movies/_form.html.erb index d1d990b9..74261985 100644 --- a/app/views/movies/_form.html.erb +++ b/app/views/movies/_form.html.erb @@ -2,7 +2,7 @@

<%= message %>

<% end %> -<%= form_with(model: movie, data: { turbo: false }) do |form| %> +<%= form_with(model: movie) do |form| %>
<%= form.label :title %> <%= form.text_field :title %> diff --git a/app/views/movies/_movie_card.html.erb b/app/views/movies/_movie_card.html.erb index f1f7052f..be4b87ff 100644 --- a/app/views/movies/_movie_card.html.erb +++ b/app/views/movies/_movie_card.html.erb @@ -30,7 +30,7 @@
- <%= link_to movie, class: "btn btn-outline-secondary", data: { turbo_method: :delete } do %> + <%= link_to movie, class: "btn btn-outline-secondary", data: { method: :delete } do %> <% end %>
diff --git a/app/views/shared/_navbar.html.erb b/app/views/shared/_navbar.html.erb index 52dc83e9..8b6c28b9 100644 --- a/app/views/shared/_navbar.html.erb +++ b/app/views/shared/_navbar.html.erb @@ -14,7 +14,7 @@ <%= link_to "#{current_user.first_name} #{current_user.last_name}", edit_user_registration_path(current_user), class: "nav-link" %> <% else %> <% if user_signed_in? %>