From 5da306d14c484a94bc12e4840f85f0fd9ce19600 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 9 Jun 2021 11:14:02 +0700 Subject: [PATCH 01/39] [#5] Edit default from email --- config/initializers/devise.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 56030eba..22622073 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 = 'noreply@nimblehq.co' # Configure the class responsible to send e-mails. # config.mailer = 'Devise::Mailer' From 7175a1d61a41dc979512f8bd56ca5740972d3ee9 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 9 Jun 2021 11:15:26 +0700 Subject: [PATCH 02/39] [#5] Add flash notice-alert in view layout --- app/views/layouts/application.html.erb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 9522d94e..70a9880c 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -11,6 +11,8 @@ +

<%= notice %>

+

<%= alert %>

<%= yield %> From 50076a3d069fc39977ea509b1e8d36d48dc25224 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 9 Jun 2021 11:17:43 +0700 Subject: [PATCH 03/39] [#5] Generate basic User model from Devise --- app/models/user.rb | 6 +++ config/routes.rb | 1 + .../20210609041601_devise_create_users.rb | 44 +++++++++++++++++++ db/schema.rb | 18 ++++++++ spec/fabricators/user_fabricator.rb | 2 + spec/models/user_spec.rb | 5 +++ 6 files changed, 76 insertions(+) create mode 100644 app/models/user.rb create mode 100644 db/migrate/20210609041601_devise_create_users.rb create mode 100644 db/schema.rb create mode 100644 spec/fabricators/user_fabricator.rb create mode 100644 spec/models/user_spec.rb diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 00000000..47567994 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,6 @@ +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/config/routes.rb b/config/routes.rb index c06383a1..54b04d77 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,3 +1,4 @@ Rails.application.routes.draw do + devise_for :users # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html end diff --git a/db/migrate/20210609041601_devise_create_users.rb b/db/migrate/20210609041601_devise_create_users.rb new file mode 100644 index 00000000..cc0991d9 --- /dev/null +++ b/db/migrate/20210609041601_devise_create_users.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +class DeviseCreateUsers < ActiveRecord::Migration[6.1] + 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.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 new file mode 100644 index 00000000..4603022a --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,18 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 0) do + + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" + +end diff --git a/spec/fabricators/user_fabricator.rb b/spec/fabricators/user_fabricator.rb new file mode 100644 index 00000000..52374f86 --- /dev/null +++ b/spec/fabricators/user_fabricator.rb @@ -0,0 +1,2 @@ +Fabricator(:user) do +end \ No newline at end of file diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb new file mode 100644 index 00000000..47a31bb4 --- /dev/null +++ b/spec/models/user_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe User, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end From 10cbeea465e771410cf7328725691d5bca82a364 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 9 Jun 2021 11:19:02 +0700 Subject: [PATCH 04/39] [#5] Add firstname lastname to User --- db/migrate/20210609041826_add_name_to_users.rb | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 db/migrate/20210609041826_add_name_to_users.rb diff --git a/db/migrate/20210609041826_add_name_to_users.rb b/db/migrate/20210609041826_add_name_to_users.rb new file mode 100644 index 00000000..5224fc27 --- /dev/null +++ b/db/migrate/20210609041826_add_name_to_users.rb @@ -0,0 +1,6 @@ +class AddNameToUsers < ActiveRecord::Migration[6.1] + def change + add_column :users, :firstname, :string + add_column :users, :lastname, :string + end +end From b6ae3541a8c5f668e3ce1421ee0d7bf593b3b266 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 9 Jun 2021 11:22:42 +0700 Subject: [PATCH 05/39] [#5] Generate Devise views --- app/views/devise/confirmations/new.html.erb | 16 +++++++ .../mailer/confirmation_instructions.html.erb | 5 +++ .../devise/mailer/email_changed.html.erb | 7 +++ .../devise/mailer/password_change.html.erb | 3 ++ .../reset_password_instructions.html.erb | 8 ++++ .../mailer/unlock_instructions.html.erb | 7 +++ app/views/devise/passwords/edit.html.erb | 25 +++++++++++ app/views/devise/passwords/new.html.erb | 16 +++++++ app/views/devise/registrations/edit.html.erb | 43 +++++++++++++++++++ app/views/devise/registrations/new.html.erb | 29 +++++++++++++ app/views/devise/sessions/new.html.erb | 26 +++++++++++ .../devise/shared/_error_messages.html.erb | 15 +++++++ app/views/devise/shared/_links.html.erb | 25 +++++++++++ app/views/devise/unlocks/new.html.erb | 16 +++++++ db/schema.rb | 16 ++++++- 15 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 app/views/devise/confirmations/new.html.erb create mode 100644 app/views/devise/mailer/confirmation_instructions.html.erb create mode 100644 app/views/devise/mailer/email_changed.html.erb create mode 100644 app/views/devise/mailer/password_change.html.erb create mode 100644 app/views/devise/mailer/reset_password_instructions.html.erb create mode 100644 app/views/devise/mailer/unlock_instructions.html.erb create mode 100644 app/views/devise/passwords/edit.html.erb create mode 100644 app/views/devise/passwords/new.html.erb create mode 100644 app/views/devise/registrations/edit.html.erb create mode 100644 app/views/devise/registrations/new.html.erb create mode 100644 app/views/devise/sessions/new.html.erb create mode 100644 app/views/devise/shared/_error_messages.html.erb create mode 100644 app/views/devise/shared/_links.html.erb create mode 100644 app/views/devise/unlocks/new.html.erb diff --git a/app/views/devise/confirmations/new.html.erb b/app/views/devise/confirmations/new.html.erb new file mode 100644 index 00000000..b12dd0cb --- /dev/null +++ b/app/views/devise/confirmations/new.html.erb @@ -0,0 +1,16 @@ +

Resend confirmation instructions

+ +<%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email", value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %> +
+ +
+ <%= f.submit "Resend confirmation instructions" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb new file mode 100644 index 00000000..dc55f64f --- /dev/null +++ b/app/views/devise/mailer/confirmation_instructions.html.erb @@ -0,0 +1,5 @@ +

Welcome <%= @email %>!

+ +

You can confirm your account email through the link below:

+ +

<%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>

diff --git a/app/views/devise/mailer/email_changed.html.erb b/app/views/devise/mailer/email_changed.html.erb new file mode 100644 index 00000000..32f4ba80 --- /dev/null +++ b/app/views/devise/mailer/email_changed.html.erb @@ -0,0 +1,7 @@ +

Hello <%= @email %>!

+ +<% if @resource.try(:unconfirmed_email?) %> +

We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.

+<% else %> +

We're contacting you to notify you that your email has been changed to <%= @resource.email %>.

+<% end %> diff --git a/app/views/devise/mailer/password_change.html.erb b/app/views/devise/mailer/password_change.html.erb new file mode 100644 index 00000000..b41daf47 --- /dev/null +++ b/app/views/devise/mailer/password_change.html.erb @@ -0,0 +1,3 @@ +

Hello <%= @resource.email %>!

+ +

We're contacting you to notify you that your password has been changed.

diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb new file mode 100644 index 00000000..f667dc12 --- /dev/null +++ b/app/views/devise/mailer/reset_password_instructions.html.erb @@ -0,0 +1,8 @@ +

Hello <%= @resource.email %>!

+ +

Someone has requested a link to change your password. You can do this through the link below.

+ +

<%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %>

+ +

If you didn't request this, please ignore this email.

+

Your password won't change until you access the link above and create a new one.

diff --git a/app/views/devise/mailer/unlock_instructions.html.erb b/app/views/devise/mailer/unlock_instructions.html.erb new file mode 100644 index 00000000..41e148bf --- /dev/null +++ b/app/views/devise/mailer/unlock_instructions.html.erb @@ -0,0 +1,7 @@ +

Hello <%= @resource.email %>!

+ +

Your account has been locked due to an excessive number of unsuccessful sign in attempts.

+ +

Click the link below to unlock your account:

+ +

<%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %>

diff --git a/app/views/devise/passwords/edit.html.erb b/app/views/devise/passwords/edit.html.erb new file mode 100644 index 00000000..5fbb9ff0 --- /dev/null +++ b/app/views/devise/passwords/edit.html.erb @@ -0,0 +1,25 @@ +

Change your password

+ +<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + <%= f.hidden_field :reset_password_token %> + +
+ <%= f.label :password, "New password" %>
+ <% if @minimum_password_length %> + (<%= @minimum_password_length %> characters minimum)
+ <% end %> + <%= f.password_field :password, autofocus: true, autocomplete: "new-password" %> +
+ +
+ <%= f.label :password_confirmation, "Confirm new password" %>
+ <%= f.password_field :password_confirmation, autocomplete: "new-password" %> +
+ +
+ <%= f.submit "Change my password" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/passwords/new.html.erb b/app/views/devise/passwords/new.html.erb new file mode 100644 index 00000000..9b486b81 --- /dev/null +++ b/app/views/devise/passwords/new.html.erb @@ -0,0 +1,16 @@ +

Forgot your password?

+ +<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ +
+ <%= f.submit "Send me reset password instructions" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb new file mode 100644 index 00000000..38d95b85 --- /dev/null +++ b/app/views/devise/registrations/edit.html.erb @@ -0,0 +1,43 @@ +

Edit <%= resource_name.to_s.humanize %>

+ +<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ + <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %> +
Currently waiting confirmation for: <%= resource.unconfirmed_email %>
+ <% end %> + +
+ <%= f.label :password %> (leave blank if you don't want to change it)
+ <%= f.password_field :password, autocomplete: "new-password" %> + <% if @minimum_password_length %> +
+ <%= @minimum_password_length %> characters minimum + <% end %> +
+ +
+ <%= f.label :password_confirmation %>
+ <%= f.password_field :password_confirmation, autocomplete: "new-password" %> +
+ +
+ <%= f.label :current_password %> (we need your current password to confirm your changes)
+ <%= f.password_field :current_password, autocomplete: "current-password" %> +
+ +
+ <%= f.submit "Update" %> +
+<% end %> + +

Cancel my account

+ +

Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>

+ +<%= link_to "Back", :back %> diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb new file mode 100644 index 00000000..d655b66f --- /dev/null +++ b/app/views/devise/registrations/new.html.erb @@ -0,0 +1,29 @@ +

Sign up

+ +<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ +
+ <%= f.label :password %> + <% if @minimum_password_length %> + (<%= @minimum_password_length %> characters minimum) + <% end %>
+ <%= f.password_field :password, autocomplete: "new-password" %> +
+ +
+ <%= f.label :password_confirmation %>
+ <%= f.password_field :password_confirmation, autocomplete: "new-password" %> +
+ +
+ <%= f.submit "Sign up" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb new file mode 100644 index 00000000..5ede9648 --- /dev/null +++ b/app/views/devise/sessions/new.html.erb @@ -0,0 +1,26 @@ +

Log in

+ +<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ +
+ <%= f.label :password %>
+ <%= f.password_field :password, autocomplete: "current-password" %> +
+ + <% if devise_mapping.rememberable? %> +
+ <%= f.check_box :remember_me %> + <%= f.label :remember_me %> +
+ <% end %> + +
+ <%= f.submit "Log in" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/shared/_error_messages.html.erb b/app/views/devise/shared/_error_messages.html.erb new file mode 100644 index 00000000..ba7ab887 --- /dev/null +++ b/app/views/devise/shared/_error_messages.html.erb @@ -0,0 +1,15 @@ +<% if resource.errors.any? %> +
+

+ <%= I18n.t("errors.messages.not_saved", + count: resource.errors.count, + resource: resource.class.model_name.human.downcase) + %> +

+
    + <% resource.errors.full_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
+
+<% end %> diff --git a/app/views/devise/shared/_links.html.erb b/app/views/devise/shared/_links.html.erb new file mode 100644 index 00000000..96a94124 --- /dev/null +++ b/app/views/devise/shared/_links.html.erb @@ -0,0 +1,25 @@ +<%- if controller_name != 'sessions' %> + <%= link_to "Log in", new_session_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.registerable? && controller_name != 'registrations' %> + <%= link_to "Sign up", new_registration_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> + <%= link_to "Forgot your password?", new_password_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> + <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> + <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.omniauthable? %> + <%- resource_class.omniauth_providers.each do |provider| %> + <%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), method: :post %>
+ <% end %> +<% end %> diff --git a/app/views/devise/unlocks/new.html.erb b/app/views/devise/unlocks/new.html.erb new file mode 100644 index 00000000..ffc34de8 --- /dev/null +++ b/app/views/devise/unlocks/new.html.erb @@ -0,0 +1,16 @@ +

Resend unlock instructions

+ +<%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ +
+ <%= f.submit "Resend unlock instructions" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/db/schema.rb b/db/schema.rb index 4603022a..c5221d09 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,9 +10,23 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 0) do +ActiveRecord::Schema.define(version: 2021_06_09_041826) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + 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.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.string "firstname" + t.string "lastname" + 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 From 3b359ae6d9ff8679c4ed603d9cf24ae3a2eb678f Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 9 Jun 2021 11:32:52 +0700 Subject: [PATCH 06/39] [#5] Add lastname and firstname to registration views --- app/controllers/application_controller.rb | 13 +++++++++++++ app/models/user.rb | 2 ++ app/views/devise/registrations/edit.html.erb | 12 +++++++++++- app/views/devise/registrations/new.html.erb | 12 +++++++++++- spec/fabricators/user_fabricator.rb | 4 +++- spec/models/user_spec.rb | 2 ++ 6 files changed, 42 insertions(+), 3 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 41855895..de7acdb2 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -2,4 +2,17 @@ class ApplicationController < ActionController::Base include Localization + + protect_from_forgery with: :exception + + before_action :update_allowed_parameters, if: :devise_controller? + + protected + + def update_allowed_parameters + devise_parameter_sanitizer.permit(:sign_up) { |u| u.permit(:firstname, :lastname, :email, :password) } + devise_parameter_sanitizer.permit(:account_update) do |u| + u.permit(:firstname, :lastname, :email, :password, :current_password) + end + end end diff --git a/app/models/user.rb b/app/models/user.rb index 47567994..66651f6c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class User < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb index 38d95b85..a27eb554 100644 --- a/app/views/devise/registrations/edit.html.erb +++ b/app/views/devise/registrations/edit.html.erb @@ -3,9 +3,19 @@ <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %> <%= render "devise/shared/error_messages", resource: resource %> +
+ <%= f.label :firstname %>
+ <%= f.text_field :firstname, autofocus: true %> +
+ +
+ <%= f.label :lastname %>
+ <%= f.text_field :lastname %> +
+
<%= f.label :email %>
- <%= f.email_field :email, autofocus: true, autocomplete: "email" %> + <%= f.email_field :email, autocomplete: "email" %>
<% if devise_mapping.confirmable? && resource.pending_reconfirmation? %> diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb index d655b66f..9bf55d7f 100644 --- a/app/views/devise/registrations/new.html.erb +++ b/app/views/devise/registrations/new.html.erb @@ -3,9 +3,19 @@ <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> <%= render "devise/shared/error_messages", resource: resource %> +
+ <%= f.label :firstname %>
+ <%= f.text_field :firstname, autofocus: true %> +
+ +
+ <%= f.label :lastname %>
+ <%= f.text_field :lastname %> +
+
<%= f.label :email %>
- <%= f.email_field :email, autofocus: true, autocomplete: "email" %> + <%= f.email_field :email, autocomplete: "email" %>
diff --git a/spec/fabricators/user_fabricator.rb b/spec/fabricators/user_fabricator.rb index 52374f86..bead9b14 100644 --- a/spec/fabricators/user_fabricator.rb +++ b/spec/fabricators/user_fabricator.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + Fabricator(:user) do -end \ No newline at end of file +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 47a31bb4..7da47d13 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_helper' RSpec.describe User, type: :model do From 7147cf953f6773735417bbbbc09c407751311f31 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 9 Jun 2021 16:21:21 +0700 Subject: [PATCH 07/39] [#293] Add logout button --- app/views/layouts/application.html.erb | 1 + config/locales/en.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 70a9880c..d314f54f 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -11,6 +11,7 @@ + <%= button_to t('logout'), destroy_user_session_path, method: :delete %>

<%= notice %>

<%= alert %>

<%= yield %> diff --git a/config/locales/en.yml b/config/locales/en.yml index cf9b342d..14ffadfb 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -30,4 +30,4 @@ # available at https://guides.rubyonrails.org/i18n.html. en: - hello: "Hello world" + logout: "Sign out" From c2c642ba7a60f51fcd218a0dc1cdb1df9701df6b Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 9 Jun 2021 16:25:27 +0700 Subject: [PATCH 08/39] [#293] Add before_action authenticate_user --- app/controllers/application_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index de7acdb2..545dcecc 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -5,6 +5,7 @@ class ApplicationController < ActionController::Base protect_from_forgery with: :exception + before_action :authenticate_user! before_action :update_allowed_parameters, if: :devise_controller? protected From d656a8d802788850382f46d9199ea9080a2ac11e Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 9 Jun 2021 18:17:25 +0700 Subject: [PATCH 09/39] [#293] Setup DoorKeeper to embbed OAuth2 --- db/schema.rb | 32 -------------------------------- 1 file changed, 32 deletions(-) delete mode 100644 db/schema.rb diff --git a/db/schema.rb b/db/schema.rb deleted file mode 100644 index c5221d09..00000000 --- a/db/schema.rb +++ /dev/null @@ -1,32 +0,0 @@ -# This file is auto-generated from the current state of the database. Instead -# of editing this file, please use the migrations feature of Active Record to -# incrementally modify your database, and then regenerate this schema definition. -# -# This file is the source Rails uses to define your schema when running `bin/rails -# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to -# be faster and is potentially less error prone than running all of your -# migrations from scratch. Old migrations may fail to apply correctly if those -# migrations use external dependencies or application code. -# -# It's strongly recommended that you check this file into your version control system. - -ActiveRecord::Schema.define(version: 2021_06_09_041826) do - - # These are extensions that must be enabled in order to support this database - enable_extension "plpgsql" - - 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.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false - t.string "firstname" - t.string "lastname" - 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 From 38e6db8c9a7774057048f2a2bf996c5d165f4ffe Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 9 Jun 2021 18:21:41 +0700 Subject: [PATCH 10/39] [#293] Setup DoorKeeper to embbed OAuth2 --- Gemfile | 1 + Gemfile.lock | 3 + app/models/user.rb | 8 + config/initializers/doorkeeper.rb | 453 ++++++++++++++++++ config/locales/doorkeeper.en.yml | 148 ++++++ config/routes.rb | 7 +- ...20210609093354_create_doorkeeper_tables.rb | 58 +++ db/schema.rb | 61 +++ db/seeds.rb | 7 + 9 files changed, 744 insertions(+), 2 deletions(-) create mode 100644 config/initializers/doorkeeper.rb create mode 100644 config/locales/doorkeeper.en.yml create mode 100644 db/migrate/20210609093354_create_doorkeeper_tables.rb create mode 100644 db/schema.rb diff --git a/Gemfile b/Gemfile index c169cc1b..2a9fa924 100644 --- a/Gemfile +++ b/Gemfile @@ -17,6 +17,7 @@ gem 'i18n-js', '3.5.1' # A library to provide the I18n translations on the Javas # Authentications & Authorizations gem 'devise' # Authentication solution for Rails with Warden gem 'pundit' # Minimal authorization through OO design and pure Ruby classes +gem 'doorkeeper' # Awesome OAuth 2 provider for your Rails / Grape app # Assets gem 'webpacker', '~>5.2.0' # Transpile app-like JavaScript diff --git a/Gemfile.lock b/Gemfile.lock index 2069ede6..adc5c2a1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -157,6 +157,8 @@ GEM warden (~> 1.2.3) diff-lcs (1.4.4) docile (1.4.0) + doorkeeper (5.5.1) + railties (>= 5) erubi (1.10.0) erubis (2.7.0) fabrication (2.22.0) @@ -480,6 +482,7 @@ DEPENDENCIES danger-undercover database_cleaner devise + doorkeeper fabrication ffaker figaro diff --git a/app/models/user.rb b/app/models/user.rb index 66651f6c..d08d38e7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -5,4 +5,12 @@ class User < ApplicationRecord # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable + + validates :email, format: URI::MailTo::EMAIL_REGEXP + + # the authenticate method from devise documentation + def self.authenticate(email, password) + user = User.find_for_authentication(email: email) + user&.valid_password?(password) ? user : nil + end end diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb new file mode 100644 index 00000000..16dc55c5 --- /dev/null +++ b/config/initializers/doorkeeper.rb @@ -0,0 +1,453 @@ +# frozen_string_literal: true + +Doorkeeper.configure do + # Change the ORM that doorkeeper will use (requires ORM extensions installed). + # Check the list of supported ORMs here: https://github.com/doorkeeper-gem/doorkeeper#orms + orm :active_record + + # This block will be called to check whether the resource owner is authenticated or not. + # resource_owner_authenticator do + # User.find_by(id: session[:user_id]) || redirect_to(new_user_session_url) + # end + + resource_owner_from_credentials do |_routes| + User.authenticate(params[:email], params[:password]) + end + + # enable password grant + grant_flows %w[password] + + # Allow to create an application without redirection + allow_blank_redirect_uri true + + # Under some circumstances you might want to have applications auto-approved, + # so that the user skips the authorization step. + # For example if dealing with a trusted application. + # + # skip_authorization do |resource_owner, client| + # client.superapp? or resource_owner.admin? + # end + skip_authorization do + true + end + + # Issue access tokens with refresh token (disabled by default), you may also + # pass a block which accepts `context` to customize when to give a refresh + # token or not. Similar to +custom_access_token_expires_in+, `context` has + # the following properties: + # + # `client` - the OAuth client application (see Doorkeeper::OAuth::Client) + # `grant_type` - the grant type of the request (see Doorkeeper::OAuth) + # `scopes` - the requested scopes (see Doorkeeper::OAuth::Scopes) + # + use_refresh_token + + + # If you didn't skip applications controller from Doorkeeper routes in your application routes.rb + # file then you need to declare this block in order to restrict access to the web interface for + # adding oauth authorized applications. In other case it will return 403 Forbidden response + # every time somebody will try to access the admin web interface. + # + # admin_authenticator do + # # Put your admin authentication logic here. + # # Example implementation: + # + # if current_user + # head :forbidden unless current_user.admin? + # else + # redirect_to sign_in_url + # end + # end + + # You can use your own model classes if you need to extend (or even override) default + # Doorkeeper models such as `Application`, `AccessToken` and `AccessGrant. + # + # Be default Doorkeeper ActiveRecord ORM uses it's own classes: + # + # access_token_class "Doorkeeper::AccessToken" + # access_grant_class "Doorkeeper::AccessGrant" + # application_class "Doorkeeper::Application" + # + # Don't forget to include Doorkeeper ORM mixins into your custom models: + # + # * ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessToken - for access token + # * ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessGrant - for access grant + # * ::Doorkeeper::Orm::ActiveRecord::Mixins::Application - for application (OAuth2 clients) + # + # For example: + # + # access_token_class "MyAccessToken" + # + # class MyAccessToken < ApplicationRecord + # include ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessToken + # + # self.table_name = "hey_i_wanna_my_name" + # + # def destroy_me! + # destroy + # end + # end + + # Enables polymorphic Resource Owner association for Access Tokens and Access Grants. + # By default this option is disabled. + # + # Make sure you properly setup you database and have all the required columns (run + # `bundle exec rails generate doorkeeper:enable_polymorphic_resource_owner` and execute Rails + # migrations). + # + # If this option enabled, Doorkeeper will store not only Resource Owner primary key + # value, but also it's type (class name). See "Polymorphic Associations" section of + # Rails guides: https://guides.rubyonrails.org/association_basics.html#polymorphic-associations + # + # [NOTE] If you apply this option on already existing project don't forget to manually + # update `resource_owner_type` column in the database and fix migration template as it will + # set NOT NULL constraint for Access Grants table. + # + # use_polymorphic_resource_owner + + # If you are planning to use Doorkeeper in Rails 5 API-only application, then you might + # want to use API mode that will skip all the views management and change the way how + # Doorkeeper responds to a requests. + # + # api_only + + # Enforce token request content type to application/x-www-form-urlencoded. + # It is not enabled by default to not break prior versions of the gem. + # + # enforce_content_type + + # Authorization Code expiration time (default: 10 minutes). + # + # authorization_code_expires_in 10.minutes + + # Access token expiration time (default: 2 hours). + # If you want to disable expiration, set this to `nil`. + # + # access_token_expires_in 2.hours + + # Assign custom TTL for access tokens. Will be used instead of access_token_expires_in + # option if defined. In case the block returns `nil` value Doorkeeper fallbacks to + # +access_token_expires_in+ configuration option value. If you really need to issue a + # non-expiring access token (which is not recommended) then you need to return + # Float::INFINITY from this block. + # + # `context` has the following properties available: + # + # * `client` - the OAuth client application (see Doorkeeper::OAuth::Client) + # * `grant_type` - the grant type of the request (see Doorkeeper::OAuth) + # * `scopes` - the requested scopes (see Doorkeeper::OAuth::Scopes) + # * `resource_owner` - authorized resource owner instance (if present) + # + # custom_access_token_expires_in do |context| + # context.client.additional_settings.implicit_oauth_expiration + # end + + # Use a custom class for generating the access token. + # See https://doorkeeper.gitbook.io/guides/configuration/other-configurations#custom-access-token-generator + # + # access_token_generator '::Doorkeeper::JWT' + + # The controller +Doorkeeper::ApplicationController+ inherits from. + # Defaults to +ActionController::Base+ unless +api_only+ is set, which changes the default to + # +ActionController::API+. The return value of this option must be a stringified class name. + # See https://doorkeeper.gitbook.io/guides/configuration/other-configurations#custom-base-controller + # + # base_controller 'ApplicationController' + + # Reuse access token for the same resource owner within an application (disabled by default). + # + # This option protects your application from creating new tokens before old valid one becomes + # expired so your database doesn't bloat. Keep in mind that when this option is `on` Doorkeeper + # doesn't updates existing token expiration time, it will create a new token instead. + # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/383 + # + # You can not enable this option together with +hash_token_secrets+. + # + # reuse_access_token + + # In case you enabled `reuse_access_token` option Doorkeeper will try to find matching + # token using `matching_token_for` Access Token API that searches for valid records + # in batches in order not to pollute the memory with all the database records. By default + # Doorkeeper uses batch size of 10 000 records. You can increase or decrease this value + # depending on your needs and server capabilities. + # + # token_lookup_batch_size 10_000 + + # Set a limit for token_reuse if using reuse_access_token option + # + # This option limits token_reusability to some extent. + # If not set then access_token will be reused unless it expires. + # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/1189 + # + # This option should be a percentage(i.e. (0,100]) + # + # token_reuse_limit 100 + + # Only allow one valid access token obtained via client credentials + # per client. If a new access token is obtained before the old one + # expired, the old one gets revoked (disabled by default) + # + # When enabling this option, make sure that you do not expect multiple processes + # using the same credentials at the same time (e.g. web servers spanning + # multiple machines and/or processes). + # + # revoke_previous_client_credentials_token + + # Hash access and refresh tokens before persisting them. + # This will disable the possibility to use +reuse_access_token+ + # since plain values can no longer be retrieved. + # + # Note: If you are already a user of doorkeeper and have existing tokens + # in your installation, they will be invalid without adding 'fallback: :plain'. + # + # hash_token_secrets + # By default, token secrets will be hashed using the + # +Doorkeeper::Hashing::SHA256+ strategy. + # + # If you wish to use another hashing implementation, you can override + # this strategy as follows: + # + # hash_token_secrets using: '::Doorkeeper::Hashing::MyCustomHashImpl' + # + # Keep in mind that changing the hashing function will invalidate all existing + # secrets, if there are any. + + # Hash application secrets before persisting them. + # + # hash_application_secrets + # + # By default, applications will be hashed + # with the +Doorkeeper::SecretStoring::SHA256+ strategy. + # + # If you wish to use bcrypt for application secret hashing, uncomment + # this line instead: + # + # hash_application_secrets using: '::Doorkeeper::SecretStoring::BCrypt' + + # When the above option is enabled, and a hashed token or secret is not found, + # you can allow to fall back to another strategy. For users upgrading + # doorkeeper and wishing to enable hashing, you will probably want to enable + # the fallback to plain tokens. + # + # This will ensure that old access tokens and secrets + # will remain valid even if the hashing above is enabled. + # + # This can be done by adding 'fallback: plain', e.g. : + # + # hash_application_secrets using: '::Doorkeeper::SecretStoring::BCrypt', fallback: :plain + + # Provide support for an owner to be assigned to each registered application (disabled by default) + # Optional parameter confirmation: true (default: false) if you want to enforce ownership of + # a registered application + # NOTE: you must also run the rails g doorkeeper:application_owner generator + # to provide the necessary support + # + # enable_application_owner confirmation: false + + # Define access token scopes for your provider + # For more information go to + # https://doorkeeper.gitbook.io/guides/ruby-on-rails/scopes + # + # default_scopes :public + # optional_scopes :write, :update + + # Allows to restrict only certain scopes for grant_type. + # By default, all the scopes will be available for all the grant types. + # + # Keys to this hash should be the name of grant_type and + # values should be the array of scopes for that grant type. + # Note: scopes should be from configured_scopes (i.e. default or optional) + # + # scopes_by_grant_type password: [:write], client_credentials: [:update] + + # Forbids creating/updating applications with arbitrary scopes that are + # not in configuration, i.e. +default_scopes+ or +optional_scopes+. + # (disabled by default) + # + # enforce_configured_scopes + + # Change the way client credentials are retrieved from the request object. + # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then + # falls back to the `:client_id` and `:client_secret` params from the `params` object. + # Check out https://github.com/doorkeeper-gem/doorkeeper/wiki/Changing-how-clients-are-authenticated + # for more information on customization + # + # client_credentials :from_basic, :from_params + + # Change the way access token is authenticated from the request object. + # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then + # falls back to the `:access_token` or `:bearer_token` params from the `params` object. + # Check out https://github.com/doorkeeper-gem/doorkeeper/wiki/Changing-how-clients-are-authenticated + # for more information on customization + # + # access_token_methods :from_bearer_authorization, :from_access_token_param, :from_bearer_param + + # Forces the usage of the HTTPS protocol in non-native redirect uris (enabled + # by default in non-development environments). OAuth2 delegates security in + # communication to the HTTPS protocol so it is wise to keep this enabled. + # + # Callable objects such as proc, lambda, block or any object that responds to + # #call can be used in order to allow conditional checks (to allow non-SSL + # redirects to localhost for example). + # + # force_ssl_in_redirect_uri !Rails.env.development? + # + # force_ssl_in_redirect_uri { |uri| uri.host != 'localhost' } + + # Specify what redirect URI's you want to block during Application creation. + # Any redirect URI is whitelisted by default. + # + # You can use this option in order to forbid URI's with 'javascript' scheme + # for example. + # + # forbid_redirect_uri { |uri| uri.scheme.to_s.downcase == 'javascript' } + + # Specify how authorization errors should be handled. + # By default, doorkeeper renders json errors when access token + # is invalid, expired, revoked or has invalid scopes. + # + # If you want to render error response yourself (i.e. rescue exceptions), + # set +handle_auth_errors+ to `:raise` and rescue Doorkeeper::Errors::InvalidToken + # or following specific errors: + # + # Doorkeeper::Errors::TokenForbidden, Doorkeeper::Errors::TokenExpired, + # Doorkeeper::Errors::TokenRevoked, Doorkeeper::Errors::TokenUnknown + # + # handle_auth_errors :raise + + + # Allows to customize OAuth grant flows that +each+ application support. + # You can configure a custom block (or use a class respond to `#call`) that must + # return `true` in case Application instance supports requested OAuth grant flow + # during the authorization request to the server. This configuration +doesn't+ + # set flows per application, it only allows to check if application supports + # specific grant flow. + # + # For example you can add an additional database column to `oauth_applications` table, + # say `t.array :grant_flows, default: []`, and store allowed grant flows that can + # be used with this application there. Then when authorization requested Doorkeeper + # will call this block to check if specific Application (passed with client_id and/or + # client_secret) is allowed to perform the request for the specific grant type + # (authorization, password, client_credentials, etc). + # + # Example of the block: + # + # ->(flow, client) { client.grant_flows.include?(flow) } + # + # In case this option invocation result is `false`, Doorkeeper server returns + # :unauthorized_client error and stops the request. + # + # @param allow_grant_flow_for_client [Proc] Block or any object respond to #call + # @return [Boolean] `true` if allow or `false` if forbid the request + # + # allow_grant_flow_for_client do |grant_flow, client| + # # `grant_flows` is an Array column with grant + # # flows that application supports + # + # client.grant_flows.include?(grant_flow) + # end + + # If you need arbitrary Resource Owner-Client authorization you can enable this option + # and implement the check your need. Config option must respond to #call and return + # true in case resource owner authorized for the specific application or false in other + # cases. + # + # Be default all Resource Owners are authorized to any Client (application). + # + # authorize_resource_owner_for_client do |client, resource_owner| + # resource_owner.admin? || client.owners_whitelist.include?(resource_owner) + # end + + # Hook into the strategies' request & response life-cycle in case your + # application needs advanced customization or logging: + # + # before_successful_strategy_response do |request| + # puts "BEFORE HOOK FIRED! #{request}" + # end + # + # after_successful_strategy_response do |request, response| + # puts "AFTER HOOK FIRED! #{request}, #{response}" + # end + + # Hook into Authorization flow in order to implement Single Sign Out + # or add any other functionality. Inside the block you have an access + # to `controller` (authorizations controller instance) and `context` + # (Doorkeeper::OAuth::Hooks::Context instance) which provides pre auth + # or auth objects with issued token based on hook type (before or after). + # + # before_successful_authorization do |controller, context| + # Rails.logger.info(controller.request.params.inspect) + # + # Rails.logger.info(context.pre_auth.inspect) + # end + # + # after_successful_authorization do |controller, context| + # controller.session[:logout_urls] << + # Doorkeeper::Application + # .find_by(controller.request.params.slice(:redirect_uri)) + # .logout_uri + # + # Rails.logger.info(context.auth.inspect) + # Rails.logger.info(context.issued_token) + # end + + + + # Configure custom constraints for the Token Introspection request. + # By default this configuration option allows to introspect a token by another + # token of the same application, OR to introspect the token that belongs to + # authorized client (from authenticated client) OR when token doesn't + # belong to any client (public token). Otherwise requester has no access to the + # introspection and it will return response as stated in the RFC. + # + # Block arguments: + # + # @param token [Doorkeeper::AccessToken] + # token to be introspected + # + # @param authorized_client [Doorkeeper::Application] + # authorized client (if request is authorized using Basic auth with + # Client Credentials for example) + # + # @param authorized_token [Doorkeeper::AccessToken] + # Bearer token used to authorize the request + # + # In case the block returns `nil` or `false` introspection responses with 401 status code + # when using authorized token to introspect, or you'll get 200 with { "active": false } body + # when using authorized client to introspect as stated in the + # RFC 7662 section 2.2. Introspection Response. + # + # Using with caution: + # Keep in mind that these three parameters pass to block can be nil as following case: + # `authorized_client` is nil if and only if `authorized_token` is present, and vice versa. + # `token` will be nil if and only if `authorized_token` is present. + # So remember to use `&` or check if it is present before calling method on + # them to make sure you doesn't get NoMethodError exception. + # + # You can define your custom check: + # + # allow_token_introspection do |token, authorized_client, authorized_token| + # if authorized_token + # # customize: require `introspection` scope + # authorized_token.application == token&.application || + # authorized_token.scopes.include?("introspection") + # elsif token.application + # # `protected_resource` is a new database boolean column, for example + # authorized_client == token.application || authorized_client.protected_resource? + # else + # # public token (when token.application is nil, token doesn't belong to any application) + # true + # end + # end + # + # Or you can completely disable any token introspection: + # + # allow_token_introspection false + # + # If you need to block the request at all, then configure your routes.rb or web-server + # like nginx to forbid the request. + + # WWW-Authenticate Realm (default: "Doorkeeper"). + # + # realm "Doorkeeper" +end diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml new file mode 100644 index 00000000..e3cf04ca --- /dev/null +++ b/config/locales/doorkeeper.en.yml @@ -0,0 +1,148 @@ +en: + activerecord: + attributes: + doorkeeper/application: + name: 'Name' + redirect_uri: 'Redirect URI' + errors: + models: + doorkeeper/application: + attributes: + redirect_uri: + fragment_present: 'cannot contain a fragment.' + invalid_uri: 'must be a valid URI.' + unspecified_scheme: 'must specify a scheme.' + relative_uri: 'must be an absolute URI.' + secured_uri: 'must be an HTTPS/SSL URI.' + forbidden_uri: 'is forbidden by the server.' + scopes: + not_match_configured: "doesn't match configured on the server." + + doorkeeper: + applications: + confirmations: + destroy: 'Are you sure?' + buttons: + edit: 'Edit' + destroy: 'Destroy' + submit: 'Submit' + cancel: 'Cancel' + authorize: 'Authorize' + form: + error: 'Whoops! Check your form for possible errors' + help: + confidential: 'Application will be used where the client secret can be kept confidential. Native mobile apps and Single Page Apps are considered non-confidential.' + redirect_uri: 'Use one line per URI' + blank_redirect_uri: "Leave it blank if you configured your provider to use Client Credentials, Resource Owner Password Credentials or any other grant type that doesn't require redirect URI." + scopes: 'Separate scopes with spaces. Leave blank to use the default scopes.' + edit: + title: 'Edit application' + index: + title: 'Your applications' + new: 'New Application' + name: 'Name' + callback_url: 'Callback URL' + confidential: 'Confidential?' + actions: 'Actions' + confidentiality: + 'yes': 'Yes' + 'no': 'No' + new: + title: 'New Application' + show: + title: 'Application: %{name}' + application_id: 'UID' + secret: 'Secret' + secret_hashed: 'Secret hashed' + scopes: 'Scopes' + confidential: 'Confidential' + callback_urls: 'Callback urls' + actions: 'Actions' + not_defined: 'Not defined' + + authorizations: + buttons: + authorize: 'Authorize' + deny: 'Deny' + error: + title: 'An error has occurred' + new: + title: 'Authorization required' + prompt: 'Authorize %{client_name} to use your account?' + able_to: 'This application will be able to' + show: + title: 'Authorization code' + form_post: + title: 'Submit this form' + + authorized_applications: + confirmations: + revoke: 'Are you sure?' + buttons: + revoke: 'Revoke' + index: + title: 'Your authorized applications' + application: 'Application' + created_at: 'Created At' + date_format: '%Y-%m-%d %H:%M:%S' + + pre_authorization: + status: 'Pre-authorization' + + errors: + messages: + # Common error messages + invalid_request: + unknown: 'The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.' + missing_param: 'Missing required parameter: %{value}.' + request_not_authorized: 'Request need to be authorized. Required parameter for authorizing request is missing or invalid.' + invalid_redirect_uri: "The requested redirect uri is malformed or doesn't match client redirect URI." + unauthorized_client: 'The client is not authorized to perform this request using this method.' + access_denied: 'The resource owner or authorization server denied the request.' + invalid_scope: 'The requested scope is invalid, unknown, or malformed.' + invalid_code_challenge_method: 'The code challenge method must be plain or S256.' + server_error: 'The authorization server encountered an unexpected condition which prevented it from fulfilling the request.' + temporarily_unavailable: 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.' + + # Configuration error messages + credential_flow_not_configured: 'Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured.' + resource_owner_authenticator_not_configured: 'Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfigured.' + admin_authenticator_not_configured: 'Access to admin panel is forbidden due to Doorkeeper.configure.admin_authenticator being unconfigured.' + + # Access grant errors + unsupported_response_type: 'The authorization server does not support this response type.' + unsupported_response_mode: 'The authorization server does not support this response mode.' + + # Access token errors + invalid_client: 'Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.' + invalid_grant: 'The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.' + unsupported_grant_type: 'The authorization grant type is not supported by the authorization server.' + + invalid_token: + revoked: "The access token was revoked" + expired: "The access token expired" + unknown: "The access token is invalid" + revoke: + unauthorized: "You are not authorized to revoke this token" + + flash: + applications: + create: + notice: 'Application created.' + destroy: + notice: 'Application deleted.' + update: + notice: 'Application updated.' + authorized_applications: + destroy: + notice: 'Application revoked.' + + layouts: + admin: + title: 'Doorkeeper' + nav: + oauth2_provider: 'OAuth2 Provider' + applications: 'Applications' + home: 'Home' + application: + title: 'OAuth authorization required' diff --git a/config/routes.rb b/config/routes.rb index 78209024..f6d58566 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,8 +1,11 @@ Rails.application.routes.draw do - root to: 'keywords#index' + use_doorkeeper do + skip_controllers :authorizations, :applications, :authorized_applications + end devise_for :users + root to: 'keywords#index' + resources :keywords, only: :index - # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html end diff --git a/db/migrate/20210609093354_create_doorkeeper_tables.rb b/db/migrate/20210609093354_create_doorkeeper_tables.rb new file mode 100644 index 00000000..13f806d8 --- /dev/null +++ b/db/migrate/20210609093354_create_doorkeeper_tables.rb @@ -0,0 +1,58 @@ +class CreateDoorkeeperTables < ActiveRecord::Migration[6.1] + def change + create_table :oauth_applications do |t| + t.string :name, null: false + t.string :uid, null: false + t.string :secret, null: false + + # Remove `null: false` if you are planning to use grant flows + # that doesn't require redirect URI to be used during authorization + # like Client Credentials flow or Resource Owner Password. + t.text :redirect_uri + t.string :scopes, null: false, default: '' + t.boolean :confidential, null: false, default: true + t.timestamps null: false + end + + add_index :oauth_applications, :uid, unique: true + + create_table :oauth_access_tokens do |t| + t.references :resource_owner, index: true + + # Remove `null: false` if you are planning to use Password + # Credentials Grant flow that doesn't require an application. + t.references :application, null: false + + t.string :token, null: false + + t.string :refresh_token + t.integer :expires_in + t.datetime :revoked_at + t.datetime :created_at, null: false + t.string :scopes + + # The authorization server MAY issue a new refresh token, in which case + # *the client MUST discard the old refresh token* and replace it with the + # new refresh token. The authorization server MAY revoke the old + # refresh token after issuing a new refresh token to the client. + # @see https://tools.ietf.org/html/rfc6749#section-6 + # + # Doorkeeper implementation: if there is a `previous_refresh_token` column, + # refresh tokens will be revoked after a related access token is used. + # If there is no `previous_refresh_token` column, previous tokens are + # revoked as soon as a new access token is created. + # + # Comment out this line if you want refresh tokens to be instantly + # revoked after use. + t.string :previous_refresh_token, null: false, default: "" + end + + add_index :oauth_access_tokens, :token, unique: true + add_index :oauth_access_tokens, :refresh_token, unique: true + add_foreign_key( + :oauth_access_tokens, + :oauth_applications, + column: :application_id + ) + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 00000000..2da16ccc --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,61 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 2021_06_09_093354) do + + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" + + create_table "oauth_access_tokens", force: :cascade do |t| + t.bigint "resource_owner_id" + t.bigint "application_id", null: false + t.string "token", null: false + t.string "refresh_token" + t.integer "expires_in" + t.datetime "revoked_at" + t.datetime "created_at", null: false + t.string "scopes" + t.string "previous_refresh_token", default: "", null: false + t.index ["application_id"], name: "index_oauth_access_tokens_on_application_id" + t.index ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true + t.index ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id" + t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true + end + + create_table "oauth_applications", force: :cascade do |t| + t.string "name", null: false + t.string "uid", null: false + t.string "secret", null: false + t.text "redirect_uri" + t.string "scopes", default: "", null: false + t.boolean "confidential", default: true, null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true + 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.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.string "firstname" + t.string "lastname" + 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 + + add_foreign_key "oauth_access_tokens", "oauth_applications", column: "application_id" +end diff --git a/db/seeds.rb b/db/seeds.rb index f3a0480d..34f20929 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -5,3 +5,10 @@ # # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) # Character.create(name: 'Luke', movie: movies.first) +# + +# if there is no OAuth application created, create them +if Doorkeeper::Application.count.zero? + Doorkeeper::Application.create(name: "iOS client", redirect_uri: "", scopes: "") + Doorkeeper::Application.create(name: "Android client", redirect_uri: "", scopes: "") +end From 145411ba12a40fbdc978b03d580f8e6e12f375b7 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Thu, 10 Jun 2021 15:50:19 +0700 Subject: [PATCH 11/39] [#5] Hide sign out button when not logged --- app/views/layouts/application.html.erb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index d314f54f..c9e413bb 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -11,7 +11,9 @@ - <%= button_to t('logout'), destroy_user_session_path, method: :delete %> + <% if user_signed_in? %> + <%= button_to t('logout'), destroy_user_session_path, method: :delete %> + <% end %>

<%= notice %>

<%= alert %>

<%= yield %> From b49d33419206ffe81f4e5ebb0d8556e4bb3f1525 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 9 Jun 2021 11:14:02 +0700 Subject: [PATCH 12/39] [#5] Edit default from email --- config/initializers/devise.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 56030eba..22622073 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 = 'noreply@nimblehq.co' # Configure the class responsible to send e-mails. # config.mailer = 'Devise::Mailer' From 271ef92d6932d90f06b4731878706cf8f05fbba4 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 9 Jun 2021 11:15:26 +0700 Subject: [PATCH 13/39] [#5] Add flash notice-alert in view layout --- app/views/layouts/application.html.erb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 9522d94e..70a9880c 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -11,6 +11,8 @@ +

<%= notice %>

+

<%= alert %>

<%= yield %> From 111bad210d0468287a0e50b5bd7fe1f7fd3e3d7f Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Thu, 10 Jun 2021 15:57:34 +0700 Subject: [PATCH 14/39] [#5] Rebase from Config Deployment --- app/models/user.rb | 6 +++ config/routes.rb | 2 +- .../20210609041601_devise_create_users.rb | 44 +++++++++++++++++++ db/schema.rb | 18 ++++++++ spec/fabricators/user_fabricator.rb | 2 + spec/models/user_spec.rb | 5 +++ 6 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 app/models/user.rb create mode 100644 db/migrate/20210609041601_devise_create_users.rb create mode 100644 db/schema.rb create mode 100644 spec/fabricators/user_fabricator.rb create mode 100644 spec/models/user_spec.rb diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 00000000..47567994 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,6 @@ +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/config/routes.rb b/config/routes.rb index fdbc5d0a..033c048c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,6 @@ Rails.application.routes.draw do root to: 'keywords#index' + devise_for :users #authentication resources :keywords, only: :index - # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html end diff --git a/db/migrate/20210609041601_devise_create_users.rb b/db/migrate/20210609041601_devise_create_users.rb new file mode 100644 index 00000000..cc0991d9 --- /dev/null +++ b/db/migrate/20210609041601_devise_create_users.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +class DeviseCreateUsers < ActiveRecord::Migration[6.1] + 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.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 new file mode 100644 index 00000000..4603022a --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,18 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 0) do + + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" + +end diff --git a/spec/fabricators/user_fabricator.rb b/spec/fabricators/user_fabricator.rb new file mode 100644 index 00000000..52374f86 --- /dev/null +++ b/spec/fabricators/user_fabricator.rb @@ -0,0 +1,2 @@ +Fabricator(:user) do +end \ No newline at end of file diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb new file mode 100644 index 00000000..47a31bb4 --- /dev/null +++ b/spec/models/user_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe User, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end From 574b46716244205614c5d9327597cfe3e75d28e0 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 9 Jun 2021 11:19:02 +0700 Subject: [PATCH 15/39] [#5] Add firstname lastname to User --- db/migrate/20210609041826_add_name_to_users.rb | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 db/migrate/20210609041826_add_name_to_users.rb diff --git a/db/migrate/20210609041826_add_name_to_users.rb b/db/migrate/20210609041826_add_name_to_users.rb new file mode 100644 index 00000000..5224fc27 --- /dev/null +++ b/db/migrate/20210609041826_add_name_to_users.rb @@ -0,0 +1,6 @@ +class AddNameToUsers < ActiveRecord::Migration[6.1] + def change + add_column :users, :firstname, :string + add_column :users, :lastname, :string + end +end From d150da13eed041fc5d2892a1b93bbd777446e778 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 9 Jun 2021 11:22:42 +0700 Subject: [PATCH 16/39] [#5] Generate Devise views --- app/views/devise/confirmations/new.html.erb | 16 +++++++ .../mailer/confirmation_instructions.html.erb | 5 +++ .../devise/mailer/email_changed.html.erb | 7 +++ .../devise/mailer/password_change.html.erb | 3 ++ .../reset_password_instructions.html.erb | 8 ++++ .../mailer/unlock_instructions.html.erb | 7 +++ app/views/devise/passwords/edit.html.erb | 25 +++++++++++ app/views/devise/passwords/new.html.erb | 16 +++++++ app/views/devise/registrations/edit.html.erb | 43 +++++++++++++++++++ app/views/devise/registrations/new.html.erb | 29 +++++++++++++ app/views/devise/sessions/new.html.erb | 26 +++++++++++ .../devise/shared/_error_messages.html.erb | 15 +++++++ app/views/devise/shared/_links.html.erb | 25 +++++++++++ app/views/devise/unlocks/new.html.erb | 16 +++++++ db/schema.rb | 16 ++++++- 15 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 app/views/devise/confirmations/new.html.erb create mode 100644 app/views/devise/mailer/confirmation_instructions.html.erb create mode 100644 app/views/devise/mailer/email_changed.html.erb create mode 100644 app/views/devise/mailer/password_change.html.erb create mode 100644 app/views/devise/mailer/reset_password_instructions.html.erb create mode 100644 app/views/devise/mailer/unlock_instructions.html.erb create mode 100644 app/views/devise/passwords/edit.html.erb create mode 100644 app/views/devise/passwords/new.html.erb create mode 100644 app/views/devise/registrations/edit.html.erb create mode 100644 app/views/devise/registrations/new.html.erb create mode 100644 app/views/devise/sessions/new.html.erb create mode 100644 app/views/devise/shared/_error_messages.html.erb create mode 100644 app/views/devise/shared/_links.html.erb create mode 100644 app/views/devise/unlocks/new.html.erb diff --git a/app/views/devise/confirmations/new.html.erb b/app/views/devise/confirmations/new.html.erb new file mode 100644 index 00000000..b12dd0cb --- /dev/null +++ b/app/views/devise/confirmations/new.html.erb @@ -0,0 +1,16 @@ +

Resend confirmation instructions

+ +<%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email", value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %> +
+ +
+ <%= f.submit "Resend confirmation instructions" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb new file mode 100644 index 00000000..dc55f64f --- /dev/null +++ b/app/views/devise/mailer/confirmation_instructions.html.erb @@ -0,0 +1,5 @@ +

Welcome <%= @email %>!

+ +

You can confirm your account email through the link below:

+ +

<%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>

diff --git a/app/views/devise/mailer/email_changed.html.erb b/app/views/devise/mailer/email_changed.html.erb new file mode 100644 index 00000000..32f4ba80 --- /dev/null +++ b/app/views/devise/mailer/email_changed.html.erb @@ -0,0 +1,7 @@ +

Hello <%= @email %>!

+ +<% if @resource.try(:unconfirmed_email?) %> +

We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.

+<% else %> +

We're contacting you to notify you that your email has been changed to <%= @resource.email %>.

+<% end %> diff --git a/app/views/devise/mailer/password_change.html.erb b/app/views/devise/mailer/password_change.html.erb new file mode 100644 index 00000000..b41daf47 --- /dev/null +++ b/app/views/devise/mailer/password_change.html.erb @@ -0,0 +1,3 @@ +

Hello <%= @resource.email %>!

+ +

We're contacting you to notify you that your password has been changed.

diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb new file mode 100644 index 00000000..f667dc12 --- /dev/null +++ b/app/views/devise/mailer/reset_password_instructions.html.erb @@ -0,0 +1,8 @@ +

Hello <%= @resource.email %>!

+ +

Someone has requested a link to change your password. You can do this through the link below.

+ +

<%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %>

+ +

If you didn't request this, please ignore this email.

+

Your password won't change until you access the link above and create a new one.

diff --git a/app/views/devise/mailer/unlock_instructions.html.erb b/app/views/devise/mailer/unlock_instructions.html.erb new file mode 100644 index 00000000..41e148bf --- /dev/null +++ b/app/views/devise/mailer/unlock_instructions.html.erb @@ -0,0 +1,7 @@ +

Hello <%= @resource.email %>!

+ +

Your account has been locked due to an excessive number of unsuccessful sign in attempts.

+ +

Click the link below to unlock your account:

+ +

<%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %>

diff --git a/app/views/devise/passwords/edit.html.erb b/app/views/devise/passwords/edit.html.erb new file mode 100644 index 00000000..5fbb9ff0 --- /dev/null +++ b/app/views/devise/passwords/edit.html.erb @@ -0,0 +1,25 @@ +

Change your password

+ +<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + <%= f.hidden_field :reset_password_token %> + +
+ <%= f.label :password, "New password" %>
+ <% if @minimum_password_length %> + (<%= @minimum_password_length %> characters minimum)
+ <% end %> + <%= f.password_field :password, autofocus: true, autocomplete: "new-password" %> +
+ +
+ <%= f.label :password_confirmation, "Confirm new password" %>
+ <%= f.password_field :password_confirmation, autocomplete: "new-password" %> +
+ +
+ <%= f.submit "Change my password" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/passwords/new.html.erb b/app/views/devise/passwords/new.html.erb new file mode 100644 index 00000000..9b486b81 --- /dev/null +++ b/app/views/devise/passwords/new.html.erb @@ -0,0 +1,16 @@ +

Forgot your password?

+ +<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ +
+ <%= f.submit "Send me reset password instructions" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb new file mode 100644 index 00000000..38d95b85 --- /dev/null +++ b/app/views/devise/registrations/edit.html.erb @@ -0,0 +1,43 @@ +

Edit <%= resource_name.to_s.humanize %>

+ +<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ + <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %> +
Currently waiting confirmation for: <%= resource.unconfirmed_email %>
+ <% end %> + +
+ <%= f.label :password %> (leave blank if you don't want to change it)
+ <%= f.password_field :password, autocomplete: "new-password" %> + <% if @minimum_password_length %> +
+ <%= @minimum_password_length %> characters minimum + <% end %> +
+ +
+ <%= f.label :password_confirmation %>
+ <%= f.password_field :password_confirmation, autocomplete: "new-password" %> +
+ +
+ <%= f.label :current_password %> (we need your current password to confirm your changes)
+ <%= f.password_field :current_password, autocomplete: "current-password" %> +
+ +
+ <%= f.submit "Update" %> +
+<% end %> + +

Cancel my account

+ +

Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>

+ +<%= link_to "Back", :back %> diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb new file mode 100644 index 00000000..d655b66f --- /dev/null +++ b/app/views/devise/registrations/new.html.erb @@ -0,0 +1,29 @@ +

Sign up

+ +<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ +
+ <%= f.label :password %> + <% if @minimum_password_length %> + (<%= @minimum_password_length %> characters minimum) + <% end %>
+ <%= f.password_field :password, autocomplete: "new-password" %> +
+ +
+ <%= f.label :password_confirmation %>
+ <%= f.password_field :password_confirmation, autocomplete: "new-password" %> +
+ +
+ <%= f.submit "Sign up" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb new file mode 100644 index 00000000..5ede9648 --- /dev/null +++ b/app/views/devise/sessions/new.html.erb @@ -0,0 +1,26 @@ +

Log in

+ +<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ +
+ <%= f.label :password %>
+ <%= f.password_field :password, autocomplete: "current-password" %> +
+ + <% if devise_mapping.rememberable? %> +
+ <%= f.check_box :remember_me %> + <%= f.label :remember_me %> +
+ <% end %> + +
+ <%= f.submit "Log in" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/shared/_error_messages.html.erb b/app/views/devise/shared/_error_messages.html.erb new file mode 100644 index 00000000..ba7ab887 --- /dev/null +++ b/app/views/devise/shared/_error_messages.html.erb @@ -0,0 +1,15 @@ +<% if resource.errors.any? %> +
+

+ <%= I18n.t("errors.messages.not_saved", + count: resource.errors.count, + resource: resource.class.model_name.human.downcase) + %> +

+
    + <% resource.errors.full_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
+
+<% end %> diff --git a/app/views/devise/shared/_links.html.erb b/app/views/devise/shared/_links.html.erb new file mode 100644 index 00000000..96a94124 --- /dev/null +++ b/app/views/devise/shared/_links.html.erb @@ -0,0 +1,25 @@ +<%- if controller_name != 'sessions' %> + <%= link_to "Log in", new_session_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.registerable? && controller_name != 'registrations' %> + <%= link_to "Sign up", new_registration_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> + <%= link_to "Forgot your password?", new_password_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> + <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> + <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.omniauthable? %> + <%- resource_class.omniauth_providers.each do |provider| %> + <%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), method: :post %>
+ <% end %> +<% end %> diff --git a/app/views/devise/unlocks/new.html.erb b/app/views/devise/unlocks/new.html.erb new file mode 100644 index 00000000..ffc34de8 --- /dev/null +++ b/app/views/devise/unlocks/new.html.erb @@ -0,0 +1,16 @@ +

Resend unlock instructions

+ +<%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ +
+ <%= f.submit "Resend unlock instructions" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/db/schema.rb b/db/schema.rb index 4603022a..c5221d09 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,9 +10,23 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 0) do +ActiveRecord::Schema.define(version: 2021_06_09_041826) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + 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.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.string "firstname" + t.string "lastname" + 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 From a57d6dae08e68608269a75a3dd853dc3a7a541cb Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 9 Jun 2021 11:32:52 +0700 Subject: [PATCH 17/39] [#5] Add lastname and firstname to registration views --- app/controllers/application_controller.rb | 13 +++++++++++++ app/models/user.rb | 2 ++ app/views/devise/registrations/edit.html.erb | 12 +++++++++++- app/views/devise/registrations/new.html.erb | 12 +++++++++++- spec/fabricators/user_fabricator.rb | 4 +++- spec/models/user_spec.rb | 2 ++ 6 files changed, 42 insertions(+), 3 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 41855895..de7acdb2 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -2,4 +2,17 @@ class ApplicationController < ActionController::Base include Localization + + protect_from_forgery with: :exception + + before_action :update_allowed_parameters, if: :devise_controller? + + protected + + def update_allowed_parameters + devise_parameter_sanitizer.permit(:sign_up) { |u| u.permit(:firstname, :lastname, :email, :password) } + devise_parameter_sanitizer.permit(:account_update) do |u| + u.permit(:firstname, :lastname, :email, :password, :current_password) + end + end end diff --git a/app/models/user.rb b/app/models/user.rb index 47567994..66651f6c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class User < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb index 38d95b85..a27eb554 100644 --- a/app/views/devise/registrations/edit.html.erb +++ b/app/views/devise/registrations/edit.html.erb @@ -3,9 +3,19 @@ <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %> <%= render "devise/shared/error_messages", resource: resource %> +
+ <%= f.label :firstname %>
+ <%= f.text_field :firstname, autofocus: true %> +
+ +
+ <%= f.label :lastname %>
+ <%= f.text_field :lastname %> +
+
<%= f.label :email %>
- <%= f.email_field :email, autofocus: true, autocomplete: "email" %> + <%= f.email_field :email, autocomplete: "email" %>
<% if devise_mapping.confirmable? && resource.pending_reconfirmation? %> diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb index d655b66f..9bf55d7f 100644 --- a/app/views/devise/registrations/new.html.erb +++ b/app/views/devise/registrations/new.html.erb @@ -3,9 +3,19 @@ <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> <%= render "devise/shared/error_messages", resource: resource %> +
+ <%= f.label :firstname %>
+ <%= f.text_field :firstname, autofocus: true %> +
+ +
+ <%= f.label :lastname %>
+ <%= f.text_field :lastname %> +
+
<%= f.label :email %>
- <%= f.email_field :email, autofocus: true, autocomplete: "email" %> + <%= f.email_field :email, autocomplete: "email" %>
diff --git a/spec/fabricators/user_fabricator.rb b/spec/fabricators/user_fabricator.rb index 52374f86..bead9b14 100644 --- a/spec/fabricators/user_fabricator.rb +++ b/spec/fabricators/user_fabricator.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + Fabricator(:user) do -end \ No newline at end of file +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 47a31bb4..7da47d13 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_helper' RSpec.describe User, type: :model do From 3dbcb86195776cbb496ee46b8b7090bb8d363b5d Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 9 Jun 2021 16:21:21 +0700 Subject: [PATCH 18/39] [#293] Add logout button --- app/views/layouts/application.html.erb | 1 + config/locales/en.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 70a9880c..d314f54f 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -11,6 +11,7 @@ + <%= button_to t('logout'), destroy_user_session_path, method: :delete %>

<%= notice %>

<%= alert %>

<%= yield %> diff --git a/config/locales/en.yml b/config/locales/en.yml index cf9b342d..14ffadfb 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -30,4 +30,4 @@ # available at https://guides.rubyonrails.org/i18n.html. en: - hello: "Hello world" + logout: "Sign out" From bfe7091f25e1604ecbdeee74d1563828f4ff3b2c Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 9 Jun 2021 16:25:27 +0700 Subject: [PATCH 19/39] [#293] Add before_action authenticate_user --- app/controllers/application_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index de7acdb2..545dcecc 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -5,6 +5,7 @@ class ApplicationController < ActionController::Base protect_from_forgery with: :exception + before_action :authenticate_user! before_action :update_allowed_parameters, if: :devise_controller? protected From 3131f0334023f01c5f510901d7ef6dc7fe5d2d13 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 9 Jun 2021 18:17:25 +0700 Subject: [PATCH 20/39] [#293] Setup DoorKeeper to embbed OAuth2 --- db/schema.rb | 32 -------------------------------- 1 file changed, 32 deletions(-) delete mode 100644 db/schema.rb diff --git a/db/schema.rb b/db/schema.rb deleted file mode 100644 index c5221d09..00000000 --- a/db/schema.rb +++ /dev/null @@ -1,32 +0,0 @@ -# This file is auto-generated from the current state of the database. Instead -# of editing this file, please use the migrations feature of Active Record to -# incrementally modify your database, and then regenerate this schema definition. -# -# This file is the source Rails uses to define your schema when running `bin/rails -# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to -# be faster and is potentially less error prone than running all of your -# migrations from scratch. Old migrations may fail to apply correctly if those -# migrations use external dependencies or application code. -# -# It's strongly recommended that you check this file into your version control system. - -ActiveRecord::Schema.define(version: 2021_06_09_041826) do - - # These are extensions that must be enabled in order to support this database - enable_extension "plpgsql" - - 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.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false - t.string "firstname" - t.string "lastname" - 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 From 5fc126d8fc4e43652879a33ad86f8dc823abec90 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 9 Jun 2021 18:21:41 +0700 Subject: [PATCH 21/39] [#293] Setup DoorKeeper to embbed OAuth2 Rebase from setup deployment workflows --- Gemfile | 1 + Gemfile.lock | 3 + app/models/user.rb | 8 + config/initializers/doorkeeper.rb | 453 ++++++++++++++++++ config/locales/doorkeeper.en.yml | 148 ++++++ config/routes.rb | 8 +- ...20210609093354_create_doorkeeper_tables.rb | 58 +++ db/schema.rb | 61 +++ db/seeds.rb | 7 + 9 files changed, 745 insertions(+), 2 deletions(-) create mode 100644 config/initializers/doorkeeper.rb create mode 100644 config/locales/doorkeeper.en.yml create mode 100644 db/migrate/20210609093354_create_doorkeeper_tables.rb create mode 100644 db/schema.rb diff --git a/Gemfile b/Gemfile index c169cc1b..2a9fa924 100644 --- a/Gemfile +++ b/Gemfile @@ -17,6 +17,7 @@ gem 'i18n-js', '3.5.1' # A library to provide the I18n translations on the Javas # Authentications & Authorizations gem 'devise' # Authentication solution for Rails with Warden gem 'pundit' # Minimal authorization through OO design and pure Ruby classes +gem 'doorkeeper' # Awesome OAuth 2 provider for your Rails / Grape app # Assets gem 'webpacker', '~>5.2.0' # Transpile app-like JavaScript diff --git a/Gemfile.lock b/Gemfile.lock index 2069ede6..adc5c2a1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -157,6 +157,8 @@ GEM warden (~> 1.2.3) diff-lcs (1.4.4) docile (1.4.0) + doorkeeper (5.5.1) + railties (>= 5) erubi (1.10.0) erubis (2.7.0) fabrication (2.22.0) @@ -480,6 +482,7 @@ DEPENDENCIES danger-undercover database_cleaner devise + doorkeeper fabrication ffaker figaro diff --git a/app/models/user.rb b/app/models/user.rb index 66651f6c..d08d38e7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -5,4 +5,12 @@ class User < ApplicationRecord # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable + + validates :email, format: URI::MailTo::EMAIL_REGEXP + + # the authenticate method from devise documentation + def self.authenticate(email, password) + user = User.find_for_authentication(email: email) + user&.valid_password?(password) ? user : nil + end end diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb new file mode 100644 index 00000000..16dc55c5 --- /dev/null +++ b/config/initializers/doorkeeper.rb @@ -0,0 +1,453 @@ +# frozen_string_literal: true + +Doorkeeper.configure do + # Change the ORM that doorkeeper will use (requires ORM extensions installed). + # Check the list of supported ORMs here: https://github.com/doorkeeper-gem/doorkeeper#orms + orm :active_record + + # This block will be called to check whether the resource owner is authenticated or not. + # resource_owner_authenticator do + # User.find_by(id: session[:user_id]) || redirect_to(new_user_session_url) + # end + + resource_owner_from_credentials do |_routes| + User.authenticate(params[:email], params[:password]) + end + + # enable password grant + grant_flows %w[password] + + # Allow to create an application without redirection + allow_blank_redirect_uri true + + # Under some circumstances you might want to have applications auto-approved, + # so that the user skips the authorization step. + # For example if dealing with a trusted application. + # + # skip_authorization do |resource_owner, client| + # client.superapp? or resource_owner.admin? + # end + skip_authorization do + true + end + + # Issue access tokens with refresh token (disabled by default), you may also + # pass a block which accepts `context` to customize when to give a refresh + # token or not. Similar to +custom_access_token_expires_in+, `context` has + # the following properties: + # + # `client` - the OAuth client application (see Doorkeeper::OAuth::Client) + # `grant_type` - the grant type of the request (see Doorkeeper::OAuth) + # `scopes` - the requested scopes (see Doorkeeper::OAuth::Scopes) + # + use_refresh_token + + + # If you didn't skip applications controller from Doorkeeper routes in your application routes.rb + # file then you need to declare this block in order to restrict access to the web interface for + # adding oauth authorized applications. In other case it will return 403 Forbidden response + # every time somebody will try to access the admin web interface. + # + # admin_authenticator do + # # Put your admin authentication logic here. + # # Example implementation: + # + # if current_user + # head :forbidden unless current_user.admin? + # else + # redirect_to sign_in_url + # end + # end + + # You can use your own model classes if you need to extend (or even override) default + # Doorkeeper models such as `Application`, `AccessToken` and `AccessGrant. + # + # Be default Doorkeeper ActiveRecord ORM uses it's own classes: + # + # access_token_class "Doorkeeper::AccessToken" + # access_grant_class "Doorkeeper::AccessGrant" + # application_class "Doorkeeper::Application" + # + # Don't forget to include Doorkeeper ORM mixins into your custom models: + # + # * ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessToken - for access token + # * ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessGrant - for access grant + # * ::Doorkeeper::Orm::ActiveRecord::Mixins::Application - for application (OAuth2 clients) + # + # For example: + # + # access_token_class "MyAccessToken" + # + # class MyAccessToken < ApplicationRecord + # include ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessToken + # + # self.table_name = "hey_i_wanna_my_name" + # + # def destroy_me! + # destroy + # end + # end + + # Enables polymorphic Resource Owner association for Access Tokens and Access Grants. + # By default this option is disabled. + # + # Make sure you properly setup you database and have all the required columns (run + # `bundle exec rails generate doorkeeper:enable_polymorphic_resource_owner` and execute Rails + # migrations). + # + # If this option enabled, Doorkeeper will store not only Resource Owner primary key + # value, but also it's type (class name). See "Polymorphic Associations" section of + # Rails guides: https://guides.rubyonrails.org/association_basics.html#polymorphic-associations + # + # [NOTE] If you apply this option on already existing project don't forget to manually + # update `resource_owner_type` column in the database and fix migration template as it will + # set NOT NULL constraint for Access Grants table. + # + # use_polymorphic_resource_owner + + # If you are planning to use Doorkeeper in Rails 5 API-only application, then you might + # want to use API mode that will skip all the views management and change the way how + # Doorkeeper responds to a requests. + # + # api_only + + # Enforce token request content type to application/x-www-form-urlencoded. + # It is not enabled by default to not break prior versions of the gem. + # + # enforce_content_type + + # Authorization Code expiration time (default: 10 minutes). + # + # authorization_code_expires_in 10.minutes + + # Access token expiration time (default: 2 hours). + # If you want to disable expiration, set this to `nil`. + # + # access_token_expires_in 2.hours + + # Assign custom TTL for access tokens. Will be used instead of access_token_expires_in + # option if defined. In case the block returns `nil` value Doorkeeper fallbacks to + # +access_token_expires_in+ configuration option value. If you really need to issue a + # non-expiring access token (which is not recommended) then you need to return + # Float::INFINITY from this block. + # + # `context` has the following properties available: + # + # * `client` - the OAuth client application (see Doorkeeper::OAuth::Client) + # * `grant_type` - the grant type of the request (see Doorkeeper::OAuth) + # * `scopes` - the requested scopes (see Doorkeeper::OAuth::Scopes) + # * `resource_owner` - authorized resource owner instance (if present) + # + # custom_access_token_expires_in do |context| + # context.client.additional_settings.implicit_oauth_expiration + # end + + # Use a custom class for generating the access token. + # See https://doorkeeper.gitbook.io/guides/configuration/other-configurations#custom-access-token-generator + # + # access_token_generator '::Doorkeeper::JWT' + + # The controller +Doorkeeper::ApplicationController+ inherits from. + # Defaults to +ActionController::Base+ unless +api_only+ is set, which changes the default to + # +ActionController::API+. The return value of this option must be a stringified class name. + # See https://doorkeeper.gitbook.io/guides/configuration/other-configurations#custom-base-controller + # + # base_controller 'ApplicationController' + + # Reuse access token for the same resource owner within an application (disabled by default). + # + # This option protects your application from creating new tokens before old valid one becomes + # expired so your database doesn't bloat. Keep in mind that when this option is `on` Doorkeeper + # doesn't updates existing token expiration time, it will create a new token instead. + # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/383 + # + # You can not enable this option together with +hash_token_secrets+. + # + # reuse_access_token + + # In case you enabled `reuse_access_token` option Doorkeeper will try to find matching + # token using `matching_token_for` Access Token API that searches for valid records + # in batches in order not to pollute the memory with all the database records. By default + # Doorkeeper uses batch size of 10 000 records. You can increase or decrease this value + # depending on your needs and server capabilities. + # + # token_lookup_batch_size 10_000 + + # Set a limit for token_reuse if using reuse_access_token option + # + # This option limits token_reusability to some extent. + # If not set then access_token will be reused unless it expires. + # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/1189 + # + # This option should be a percentage(i.e. (0,100]) + # + # token_reuse_limit 100 + + # Only allow one valid access token obtained via client credentials + # per client. If a new access token is obtained before the old one + # expired, the old one gets revoked (disabled by default) + # + # When enabling this option, make sure that you do not expect multiple processes + # using the same credentials at the same time (e.g. web servers spanning + # multiple machines and/or processes). + # + # revoke_previous_client_credentials_token + + # Hash access and refresh tokens before persisting them. + # This will disable the possibility to use +reuse_access_token+ + # since plain values can no longer be retrieved. + # + # Note: If you are already a user of doorkeeper and have existing tokens + # in your installation, they will be invalid without adding 'fallback: :plain'. + # + # hash_token_secrets + # By default, token secrets will be hashed using the + # +Doorkeeper::Hashing::SHA256+ strategy. + # + # If you wish to use another hashing implementation, you can override + # this strategy as follows: + # + # hash_token_secrets using: '::Doorkeeper::Hashing::MyCustomHashImpl' + # + # Keep in mind that changing the hashing function will invalidate all existing + # secrets, if there are any. + + # Hash application secrets before persisting them. + # + # hash_application_secrets + # + # By default, applications will be hashed + # with the +Doorkeeper::SecretStoring::SHA256+ strategy. + # + # If you wish to use bcrypt for application secret hashing, uncomment + # this line instead: + # + # hash_application_secrets using: '::Doorkeeper::SecretStoring::BCrypt' + + # When the above option is enabled, and a hashed token or secret is not found, + # you can allow to fall back to another strategy. For users upgrading + # doorkeeper and wishing to enable hashing, you will probably want to enable + # the fallback to plain tokens. + # + # This will ensure that old access tokens and secrets + # will remain valid even if the hashing above is enabled. + # + # This can be done by adding 'fallback: plain', e.g. : + # + # hash_application_secrets using: '::Doorkeeper::SecretStoring::BCrypt', fallback: :plain + + # Provide support for an owner to be assigned to each registered application (disabled by default) + # Optional parameter confirmation: true (default: false) if you want to enforce ownership of + # a registered application + # NOTE: you must also run the rails g doorkeeper:application_owner generator + # to provide the necessary support + # + # enable_application_owner confirmation: false + + # Define access token scopes for your provider + # For more information go to + # https://doorkeeper.gitbook.io/guides/ruby-on-rails/scopes + # + # default_scopes :public + # optional_scopes :write, :update + + # Allows to restrict only certain scopes for grant_type. + # By default, all the scopes will be available for all the grant types. + # + # Keys to this hash should be the name of grant_type and + # values should be the array of scopes for that grant type. + # Note: scopes should be from configured_scopes (i.e. default or optional) + # + # scopes_by_grant_type password: [:write], client_credentials: [:update] + + # Forbids creating/updating applications with arbitrary scopes that are + # not in configuration, i.e. +default_scopes+ or +optional_scopes+. + # (disabled by default) + # + # enforce_configured_scopes + + # Change the way client credentials are retrieved from the request object. + # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then + # falls back to the `:client_id` and `:client_secret` params from the `params` object. + # Check out https://github.com/doorkeeper-gem/doorkeeper/wiki/Changing-how-clients-are-authenticated + # for more information on customization + # + # client_credentials :from_basic, :from_params + + # Change the way access token is authenticated from the request object. + # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then + # falls back to the `:access_token` or `:bearer_token` params from the `params` object. + # Check out https://github.com/doorkeeper-gem/doorkeeper/wiki/Changing-how-clients-are-authenticated + # for more information on customization + # + # access_token_methods :from_bearer_authorization, :from_access_token_param, :from_bearer_param + + # Forces the usage of the HTTPS protocol in non-native redirect uris (enabled + # by default in non-development environments). OAuth2 delegates security in + # communication to the HTTPS protocol so it is wise to keep this enabled. + # + # Callable objects such as proc, lambda, block or any object that responds to + # #call can be used in order to allow conditional checks (to allow non-SSL + # redirects to localhost for example). + # + # force_ssl_in_redirect_uri !Rails.env.development? + # + # force_ssl_in_redirect_uri { |uri| uri.host != 'localhost' } + + # Specify what redirect URI's you want to block during Application creation. + # Any redirect URI is whitelisted by default. + # + # You can use this option in order to forbid URI's with 'javascript' scheme + # for example. + # + # forbid_redirect_uri { |uri| uri.scheme.to_s.downcase == 'javascript' } + + # Specify how authorization errors should be handled. + # By default, doorkeeper renders json errors when access token + # is invalid, expired, revoked or has invalid scopes. + # + # If you want to render error response yourself (i.e. rescue exceptions), + # set +handle_auth_errors+ to `:raise` and rescue Doorkeeper::Errors::InvalidToken + # or following specific errors: + # + # Doorkeeper::Errors::TokenForbidden, Doorkeeper::Errors::TokenExpired, + # Doorkeeper::Errors::TokenRevoked, Doorkeeper::Errors::TokenUnknown + # + # handle_auth_errors :raise + + + # Allows to customize OAuth grant flows that +each+ application support. + # You can configure a custom block (or use a class respond to `#call`) that must + # return `true` in case Application instance supports requested OAuth grant flow + # during the authorization request to the server. This configuration +doesn't+ + # set flows per application, it only allows to check if application supports + # specific grant flow. + # + # For example you can add an additional database column to `oauth_applications` table, + # say `t.array :grant_flows, default: []`, and store allowed grant flows that can + # be used with this application there. Then when authorization requested Doorkeeper + # will call this block to check if specific Application (passed with client_id and/or + # client_secret) is allowed to perform the request for the specific grant type + # (authorization, password, client_credentials, etc). + # + # Example of the block: + # + # ->(flow, client) { client.grant_flows.include?(flow) } + # + # In case this option invocation result is `false`, Doorkeeper server returns + # :unauthorized_client error and stops the request. + # + # @param allow_grant_flow_for_client [Proc] Block or any object respond to #call + # @return [Boolean] `true` if allow or `false` if forbid the request + # + # allow_grant_flow_for_client do |grant_flow, client| + # # `grant_flows` is an Array column with grant + # # flows that application supports + # + # client.grant_flows.include?(grant_flow) + # end + + # If you need arbitrary Resource Owner-Client authorization you can enable this option + # and implement the check your need. Config option must respond to #call and return + # true in case resource owner authorized for the specific application or false in other + # cases. + # + # Be default all Resource Owners are authorized to any Client (application). + # + # authorize_resource_owner_for_client do |client, resource_owner| + # resource_owner.admin? || client.owners_whitelist.include?(resource_owner) + # end + + # Hook into the strategies' request & response life-cycle in case your + # application needs advanced customization or logging: + # + # before_successful_strategy_response do |request| + # puts "BEFORE HOOK FIRED! #{request}" + # end + # + # after_successful_strategy_response do |request, response| + # puts "AFTER HOOK FIRED! #{request}, #{response}" + # end + + # Hook into Authorization flow in order to implement Single Sign Out + # or add any other functionality. Inside the block you have an access + # to `controller` (authorizations controller instance) and `context` + # (Doorkeeper::OAuth::Hooks::Context instance) which provides pre auth + # or auth objects with issued token based on hook type (before or after). + # + # before_successful_authorization do |controller, context| + # Rails.logger.info(controller.request.params.inspect) + # + # Rails.logger.info(context.pre_auth.inspect) + # end + # + # after_successful_authorization do |controller, context| + # controller.session[:logout_urls] << + # Doorkeeper::Application + # .find_by(controller.request.params.slice(:redirect_uri)) + # .logout_uri + # + # Rails.logger.info(context.auth.inspect) + # Rails.logger.info(context.issued_token) + # end + + + + # Configure custom constraints for the Token Introspection request. + # By default this configuration option allows to introspect a token by another + # token of the same application, OR to introspect the token that belongs to + # authorized client (from authenticated client) OR when token doesn't + # belong to any client (public token). Otherwise requester has no access to the + # introspection and it will return response as stated in the RFC. + # + # Block arguments: + # + # @param token [Doorkeeper::AccessToken] + # token to be introspected + # + # @param authorized_client [Doorkeeper::Application] + # authorized client (if request is authorized using Basic auth with + # Client Credentials for example) + # + # @param authorized_token [Doorkeeper::AccessToken] + # Bearer token used to authorize the request + # + # In case the block returns `nil` or `false` introspection responses with 401 status code + # when using authorized token to introspect, or you'll get 200 with { "active": false } body + # when using authorized client to introspect as stated in the + # RFC 7662 section 2.2. Introspection Response. + # + # Using with caution: + # Keep in mind that these three parameters pass to block can be nil as following case: + # `authorized_client` is nil if and only if `authorized_token` is present, and vice versa. + # `token` will be nil if and only if `authorized_token` is present. + # So remember to use `&` or check if it is present before calling method on + # them to make sure you doesn't get NoMethodError exception. + # + # You can define your custom check: + # + # allow_token_introspection do |token, authorized_client, authorized_token| + # if authorized_token + # # customize: require `introspection` scope + # authorized_token.application == token&.application || + # authorized_token.scopes.include?("introspection") + # elsif token.application + # # `protected_resource` is a new database boolean column, for example + # authorized_client == token.application || authorized_client.protected_resource? + # else + # # public token (when token.application is nil, token doesn't belong to any application) + # true + # end + # end + # + # Or you can completely disable any token introspection: + # + # allow_token_introspection false + # + # If you need to block the request at all, then configure your routes.rb or web-server + # like nginx to forbid the request. + + # WWW-Authenticate Realm (default: "Doorkeeper"). + # + # realm "Doorkeeper" +end diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml new file mode 100644 index 00000000..e3cf04ca --- /dev/null +++ b/config/locales/doorkeeper.en.yml @@ -0,0 +1,148 @@ +en: + activerecord: + attributes: + doorkeeper/application: + name: 'Name' + redirect_uri: 'Redirect URI' + errors: + models: + doorkeeper/application: + attributes: + redirect_uri: + fragment_present: 'cannot contain a fragment.' + invalid_uri: 'must be a valid URI.' + unspecified_scheme: 'must specify a scheme.' + relative_uri: 'must be an absolute URI.' + secured_uri: 'must be an HTTPS/SSL URI.' + forbidden_uri: 'is forbidden by the server.' + scopes: + not_match_configured: "doesn't match configured on the server." + + doorkeeper: + applications: + confirmations: + destroy: 'Are you sure?' + buttons: + edit: 'Edit' + destroy: 'Destroy' + submit: 'Submit' + cancel: 'Cancel' + authorize: 'Authorize' + form: + error: 'Whoops! Check your form for possible errors' + help: + confidential: 'Application will be used where the client secret can be kept confidential. Native mobile apps and Single Page Apps are considered non-confidential.' + redirect_uri: 'Use one line per URI' + blank_redirect_uri: "Leave it blank if you configured your provider to use Client Credentials, Resource Owner Password Credentials or any other grant type that doesn't require redirect URI." + scopes: 'Separate scopes with spaces. Leave blank to use the default scopes.' + edit: + title: 'Edit application' + index: + title: 'Your applications' + new: 'New Application' + name: 'Name' + callback_url: 'Callback URL' + confidential: 'Confidential?' + actions: 'Actions' + confidentiality: + 'yes': 'Yes' + 'no': 'No' + new: + title: 'New Application' + show: + title: 'Application: %{name}' + application_id: 'UID' + secret: 'Secret' + secret_hashed: 'Secret hashed' + scopes: 'Scopes' + confidential: 'Confidential' + callback_urls: 'Callback urls' + actions: 'Actions' + not_defined: 'Not defined' + + authorizations: + buttons: + authorize: 'Authorize' + deny: 'Deny' + error: + title: 'An error has occurred' + new: + title: 'Authorization required' + prompt: 'Authorize %{client_name} to use your account?' + able_to: 'This application will be able to' + show: + title: 'Authorization code' + form_post: + title: 'Submit this form' + + authorized_applications: + confirmations: + revoke: 'Are you sure?' + buttons: + revoke: 'Revoke' + index: + title: 'Your authorized applications' + application: 'Application' + created_at: 'Created At' + date_format: '%Y-%m-%d %H:%M:%S' + + pre_authorization: + status: 'Pre-authorization' + + errors: + messages: + # Common error messages + invalid_request: + unknown: 'The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.' + missing_param: 'Missing required parameter: %{value}.' + request_not_authorized: 'Request need to be authorized. Required parameter for authorizing request is missing or invalid.' + invalid_redirect_uri: "The requested redirect uri is malformed or doesn't match client redirect URI." + unauthorized_client: 'The client is not authorized to perform this request using this method.' + access_denied: 'The resource owner or authorization server denied the request.' + invalid_scope: 'The requested scope is invalid, unknown, or malformed.' + invalid_code_challenge_method: 'The code challenge method must be plain or S256.' + server_error: 'The authorization server encountered an unexpected condition which prevented it from fulfilling the request.' + temporarily_unavailable: 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.' + + # Configuration error messages + credential_flow_not_configured: 'Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured.' + resource_owner_authenticator_not_configured: 'Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfigured.' + admin_authenticator_not_configured: 'Access to admin panel is forbidden due to Doorkeeper.configure.admin_authenticator being unconfigured.' + + # Access grant errors + unsupported_response_type: 'The authorization server does not support this response type.' + unsupported_response_mode: 'The authorization server does not support this response mode.' + + # Access token errors + invalid_client: 'Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.' + invalid_grant: 'The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.' + unsupported_grant_type: 'The authorization grant type is not supported by the authorization server.' + + invalid_token: + revoked: "The access token was revoked" + expired: "The access token expired" + unknown: "The access token is invalid" + revoke: + unauthorized: "You are not authorized to revoke this token" + + flash: + applications: + create: + notice: 'Application created.' + destroy: + notice: 'Application deleted.' + update: + notice: 'Application updated.' + authorized_applications: + destroy: + notice: 'Application revoked.' + + layouts: + admin: + title: 'Doorkeeper' + nav: + oauth2_provider: 'OAuth2 Provider' + applications: 'Applications' + home: 'Home' + application: + title: 'OAuth authorization required' diff --git a/config/routes.rb b/config/routes.rb index 033c048c..9df071b5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,10 @@ Rails.application.routes.draw do - root to: 'keywords#index' + use_doorkeeper do + skip_controllers :authorizations, :applications, :authorized_applications + end + + devise_for :users - devise_for :users #authentication + root to: 'keywords#index' resources :keywords, only: :index end diff --git a/db/migrate/20210609093354_create_doorkeeper_tables.rb b/db/migrate/20210609093354_create_doorkeeper_tables.rb new file mode 100644 index 00000000..13f806d8 --- /dev/null +++ b/db/migrate/20210609093354_create_doorkeeper_tables.rb @@ -0,0 +1,58 @@ +class CreateDoorkeeperTables < ActiveRecord::Migration[6.1] + def change + create_table :oauth_applications do |t| + t.string :name, null: false + t.string :uid, null: false + t.string :secret, null: false + + # Remove `null: false` if you are planning to use grant flows + # that doesn't require redirect URI to be used during authorization + # like Client Credentials flow or Resource Owner Password. + t.text :redirect_uri + t.string :scopes, null: false, default: '' + t.boolean :confidential, null: false, default: true + t.timestamps null: false + end + + add_index :oauth_applications, :uid, unique: true + + create_table :oauth_access_tokens do |t| + t.references :resource_owner, index: true + + # Remove `null: false` if you are planning to use Password + # Credentials Grant flow that doesn't require an application. + t.references :application, null: false + + t.string :token, null: false + + t.string :refresh_token + t.integer :expires_in + t.datetime :revoked_at + t.datetime :created_at, null: false + t.string :scopes + + # The authorization server MAY issue a new refresh token, in which case + # *the client MUST discard the old refresh token* and replace it with the + # new refresh token. The authorization server MAY revoke the old + # refresh token after issuing a new refresh token to the client. + # @see https://tools.ietf.org/html/rfc6749#section-6 + # + # Doorkeeper implementation: if there is a `previous_refresh_token` column, + # refresh tokens will be revoked after a related access token is used. + # If there is no `previous_refresh_token` column, previous tokens are + # revoked as soon as a new access token is created. + # + # Comment out this line if you want refresh tokens to be instantly + # revoked after use. + t.string :previous_refresh_token, null: false, default: "" + end + + add_index :oauth_access_tokens, :token, unique: true + add_index :oauth_access_tokens, :refresh_token, unique: true + add_foreign_key( + :oauth_access_tokens, + :oauth_applications, + column: :application_id + ) + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 00000000..2da16ccc --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,61 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 2021_06_09_093354) do + + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" + + create_table "oauth_access_tokens", force: :cascade do |t| + t.bigint "resource_owner_id" + t.bigint "application_id", null: false + t.string "token", null: false + t.string "refresh_token" + t.integer "expires_in" + t.datetime "revoked_at" + t.datetime "created_at", null: false + t.string "scopes" + t.string "previous_refresh_token", default: "", null: false + t.index ["application_id"], name: "index_oauth_access_tokens_on_application_id" + t.index ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true + t.index ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id" + t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true + end + + create_table "oauth_applications", force: :cascade do |t| + t.string "name", null: false + t.string "uid", null: false + t.string "secret", null: false + t.text "redirect_uri" + t.string "scopes", default: "", null: false + t.boolean "confidential", default: true, null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true + 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.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.string "firstname" + t.string "lastname" + 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 + + add_foreign_key "oauth_access_tokens", "oauth_applications", column: "application_id" +end diff --git a/db/seeds.rb b/db/seeds.rb index f3a0480d..34f20929 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -5,3 +5,10 @@ # # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) # Character.create(name: 'Luke', movie: movies.first) +# + +# if there is no OAuth application created, create them +if Doorkeeper::Application.count.zero? + Doorkeeper::Application.create(name: "iOS client", redirect_uri: "", scopes: "") + Doorkeeper::Application.create(name: "Android client", redirect_uri: "", scopes: "") +end From f0a6257350b822fc2f8737dd86b9433aaf6a5745 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Thu, 10 Jun 2021 15:50:19 +0700 Subject: [PATCH 22/39] [#5] Hide sign out button when not logged --- app/views/layouts/application.html.erb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index d314f54f..c9e413bb 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -11,7 +11,9 @@ - <%= button_to t('logout'), destroy_user_session_path, method: :delete %> + <% if user_signed_in? %> + <%= button_to t('logout'), destroy_user_session_path, method: :delete %> + <% end %>

<%= notice %>

<%= alert %>

<%= yield %> From 53ea9b77b81fb019d4ca3f58e59d07885207a120 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Thu, 10 Jun 2021 18:17:03 +0700 Subject: [PATCH 23/39] [#5] Add user db seed for system tests --- db/seeds.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/db/seeds.rb b/db/seeds.rb index 34f20929..f68db517 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -12,3 +12,6 @@ Doorkeeper::Application.create(name: "iOS client", redirect_uri: "", scopes: "") Doorkeeper::Application.create(name: "Android client", redirect_uri: "", scopes: "") end + + +Fabricate(:user) From e7389a9f95252ada028f5a426d41396a343c2275 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Thu, 10 Jun 2021 18:17:29 +0700 Subject: [PATCH 24/39] [#5] Add login system tests --- spec/fabricators/user_fabricator.rb | 5 +++++ spec/rails_helper.rb | 4 ++++ spec/support/authentication_helper.rb | 11 ++++++++++ spec/systems/login_spec.rb | 31 +++++++++++++++++++++++++++ 4 files changed, 51 insertions(+) create mode 100644 spec/support/authentication_helper.rb create mode 100644 spec/systems/login_spec.rb diff --git a/spec/fabricators/user_fabricator.rb b/spec/fabricators/user_fabricator.rb index bead9b14..78d99be0 100644 --- a/spec/fabricators/user_fabricator.rb +++ b/spec/fabricators/user_fabricator.rb @@ -1,4 +1,9 @@ # frozen_string_literal: true Fabricator(:user) do + lastname 'DENT' + firstname 'Arthur' + email 'arthur@dent.com' + password 'password' + password_confirmation 'password' end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index a45e2e53..0f212dc7 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -21,4 +21,8 @@ config.infer_spec_type_from_file_location! config.include Rails.application.routes.url_helpers + + config.include Devise::Test::IntegrationHelpers, type: :system + + config.include AuthenticationHelper end diff --git a/spec/support/authentication_helper.rb b/spec/support/authentication_helper.rb new file mode 100644 index 00000000..a857d899 --- /dev/null +++ b/spec/support/authentication_helper.rb @@ -0,0 +1,11 @@ +module AuthenticationHelper + def sign_in(user = nil) + user ||= Fabricate(:user) + + visit root_path + + fill_in 'user_email', with: user.email + fill_in 'user_password', with: user.password + click_button 'Log in' + end +end diff --git a/spec/systems/login_spec.rb b/spec/systems/login_spec.rb new file mode 100644 index 00000000..a9c3df77 --- /dev/null +++ b/spec/systems/login_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'login', type: :system do + context 'when unlogged user reach the app' do + it 'should redirect him to the login page' do + visit root_path + expect(find('h2')).to have_content('Log in') + expect(find('form')).to have_field('user_email') + expect(find('form')).to have_field('user_password') + end + end + + context 'when valid credentials' do + it 'should log the user in and display a flash message' do + sign_in + expect(page).to have_content(I18n.t('devise.sessions.signed_in')) + end + end + + context 'when invalid credentials' do + it 'should refuse login and display an error' do + bad_user = Fabricate(:user) + bad_user.password = 'bad' + sign_in bad_user + error_msg = I18n.t('devise.failure.invalid').to_s.gsub('%{authentication_keys}', 'Email') + expect(page).to have_content(error_msg) + end + end +end From ad3fab58895df1535e6b8614feed76682d1ac958 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Fri, 11 Jun 2021 13:11:49 +0700 Subject: [PATCH 25/39] [#5] Fix missing password_confirmation param in sign_up request --- app/controllers/application_controller.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 545dcecc..0261cc2a 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -11,7 +11,9 @@ class ApplicationController < ActionController::Base protected def update_allowed_parameters - devise_parameter_sanitizer.permit(:sign_up) { |u| u.permit(:firstname, :lastname, :email, :password) } + devise_parameter_sanitizer.permit(:sign_up) do |u| + u.permit(:firstname, :lastname, :email, :password, :password_confirmation) + end devise_parameter_sanitizer.permit(:account_update) do |u| u.permit(:firstname, :lastname, :email, :password, :current_password) end From 49e917b6d8ae1e6e54145fe3b939e8cf81e80783 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Fri, 11 Jun 2021 13:18:16 +0700 Subject: [PATCH 26/39] [#5] Add login and signup system tests --- spec/support/authentication_helper.rb | 13 ++++++ spec/systems/login_spec.rb | 18 ++++---- spec/systems/signup_spec.rb | 63 +++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 spec/systems/signup_spec.rb diff --git a/spec/support/authentication_helper.rb b/spec/support/authentication_helper.rb index a857d899..ca53b90a 100644 --- a/spec/support/authentication_helper.rb +++ b/spec/support/authentication_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module AuthenticationHelper def sign_in(user = nil) user ||= Fabricate(:user) @@ -8,4 +10,15 @@ def sign_in(user = nil) fill_in 'user_password', with: user.password click_button 'Log in' end + + def sign_up(email, password, password_confirm = nil) + user = Fabricate(:user) + visit new_user_registration_path + fill_in 'user_firstname', with: user.firstname + fill_in 'user_lastname', with: user.lastname + fill_in 'user_email', with: email + fill_in 'user_password', with: password + fill_in 'user_password_confirmation', with: password_confirm || password + click_button 'Sign up' + end end diff --git a/spec/systems/login_spec.rb b/spec/systems/login_spec.rb index a9c3df77..91942c06 100644 --- a/spec/systems/login_spec.rb +++ b/spec/systems/login_spec.rb @@ -3,28 +3,30 @@ require 'rails_helper' describe 'login', type: :system do - context 'when unlogged user reach the app' do - it 'should redirect him to the login page' do + context 'when non logged user reach the app' do + it 'redirect the user to the login page' do visit root_path - expect(find('h2')).to have_content('Log in') - expect(find('form')).to have_field('user_email') - expect(find('form')).to have_field('user_password') + + expect(page).to have_current_path(new_user_session_path) end end context 'when valid credentials' do - it 'should log the user in and display a flash message' do + it 'log the user in and display a flash message' do sign_in + expect(page).to have_content(I18n.t('devise.sessions.signed_in')) end end context 'when invalid credentials' do - it 'should refuse login and display an error' do + it 'refuse login and display an error' do bad_user = Fabricate(:user) bad_user.password = 'bad' - sign_in bad_user error_msg = I18n.t('devise.failure.invalid').to_s.gsub('%{authentication_keys}', 'Email') + + sign_in bad_user + expect(page).to have_content(error_msg) end end diff --git a/spec/systems/signup_spec.rb b/spec/systems/signup_spec.rb new file mode 100644 index 00000000..68812247 --- /dev/null +++ b/spec/systems/signup_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'signup', type: :system do + context 'when non logged user reach the sign up page' do + it 'displays email field' do + visit new_user_registration_path + expect(find('form')).to have_field('user_email') + end + + it 'displays password field ' do + visit new_user_registration_path + expect(find('form')).to have_field('user_password') + end + + it 'displays password confirmation field ' do + visit new_user_registration_path + expect(find('form')).to have_field('user_password_confirmation') + end + end + + context 'when non logged user sign up with good data' do + it 'create an account and log the user in' do + sign_up 'good@email.com', 'password123' + + expect(page).to have_content(I18n.t('devise.registrations.signed_up')) + end + end + + context 'when an non logged user sign up with bad email' do + it 'stays on the same page' do + sign_up 'bad_email', 'password123' + + expect(page).to have_current_path new_user_registration_path + end + end + + context 'when an non logged user sign up with too short password' do + it 'display an error message' do + sign_up 'good@email.com', '123' + + expect(page).to have_selector('#error_explanation li') + end + end + + context 'when an non logged user sign up with bad password confirmation' do + it 'display an error message' do + sign_up 'good@email.com', 'complex123password', 'differentPassword123' + + expect(page).to have_selector('#error_explanation li') + end + end + + context 'when a logged user reach the sign up page' do + it 'redirect him to the root_page' do + sign_in + visit new_user_registration_path + + expect(page).to have_current_path(root_path) + end + end +end From 557c93610cfc3dd6a45010a4b88f00c2dbc39b27 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Mon, 14 Jun 2021 11:57:42 +0700 Subject: [PATCH 27/39] [#5] Update system tests to use faster sign_in method helper --- spec/rails_helper.rb | 2 +- spec/support/authentication_helper.rb | 47 +++++++++++++++++---------- spec/systems/login_spec.rb | 13 ++++++-- spec/systems/signup_spec.rb | 10 +++--- 4 files changed, 47 insertions(+), 25 deletions(-) diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 0f212dc7..64d3b366 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -24,5 +24,5 @@ config.include Devise::Test::IntegrationHelpers, type: :system - config.include AuthenticationHelper + config.include DeviseHelpers::SystemHelpers end diff --git a/spec/support/authentication_helper.rb b/spec/support/authentication_helper.rb index ca53b90a..b0f775c2 100644 --- a/spec/support/authentication_helper.rb +++ b/spec/support/authentication_helper.rb @@ -1,24 +1,37 @@ # frozen_string_literal: true -module AuthenticationHelper - def sign_in(user = nil) - user ||= Fabricate(:user) +module DeviseHelpers + module RequestHelpers + # def sign_in_user(user) + # request.env['devise.mapping'] = Devise.mappings[:user] + # sign_in(user) + # end + end - visit root_path + module SystemHelpers + def sign_in_ui(user = nil) + user ||= Fabricate(:user) - fill_in 'user_email', with: user.email - fill_in 'user_password', with: user.password - click_button 'Log in' - end + visit root_path + + fill_in 'user_email', with: user.email + fill_in 'user_password', with: user.password + click_button 'Log in' + end + + def sign_in(user) + login_as user, scope: :user + end - def sign_up(email, password, password_confirm = nil) - user = Fabricate(:user) - visit new_user_registration_path - fill_in 'user_firstname', with: user.firstname - fill_in 'user_lastname', with: user.lastname - fill_in 'user_email', with: email - fill_in 'user_password', with: password - fill_in 'user_password_confirmation', with: password_confirm || password - click_button 'Sign up' + def sign_up_ui(email, password, password_confirm = nil) + user = Fabricate(:user) + visit new_user_registration_path + fill_in 'user_firstname', with: user.firstname + fill_in 'user_lastname', with: user.lastname + fill_in 'user_email', with: email + fill_in 'user_password', with: password + fill_in 'user_password_confirmation', with: password_confirm || password + click_button 'Sign up' + end end end diff --git a/spec/systems/login_spec.rb b/spec/systems/login_spec.rb index 91942c06..a7749c80 100644 --- a/spec/systems/login_spec.rb +++ b/spec/systems/login_spec.rb @@ -13,7 +13,7 @@ context 'when valid credentials' do it 'log the user in and display a flash message' do - sign_in + sign_in_ui expect(page).to have_content(I18n.t('devise.sessions.signed_in')) end @@ -25,9 +25,18 @@ bad_user.password = 'bad' error_msg = I18n.t('devise.failure.invalid').to_s.gsub('%{authentication_keys}', 'Email') - sign_in bad_user + sign_in_ui bad_user expect(page).to have_content(error_msg) end end + + context 'when signed in and reach the home page' do + it 'does not redirect to the sign in page' do + sign_in Fabricate(:user) + visit root_path + + expect(page).to have_current_path(root_path) + end + end end diff --git a/spec/systems/signup_spec.rb b/spec/systems/signup_spec.rb index 68812247..ec6a2e02 100644 --- a/spec/systems/signup_spec.rb +++ b/spec/systems/signup_spec.rb @@ -22,7 +22,7 @@ context 'when non logged user sign up with good data' do it 'create an account and log the user in' do - sign_up 'good@email.com', 'password123' + sign_up_ui 'good@email.com', 'password123' expect(page).to have_content(I18n.t('devise.registrations.signed_up')) end @@ -30,7 +30,7 @@ context 'when an non logged user sign up with bad email' do it 'stays on the same page' do - sign_up 'bad_email', 'password123' + sign_up_ui 'bad_email', 'password123' expect(page).to have_current_path new_user_registration_path end @@ -38,7 +38,7 @@ context 'when an non logged user sign up with too short password' do it 'display an error message' do - sign_up 'good@email.com', '123' + sign_up_ui 'good@email.com', '123' expect(page).to have_selector('#error_explanation li') end @@ -46,7 +46,7 @@ context 'when an non logged user sign up with bad password confirmation' do it 'display an error message' do - sign_up 'good@email.com', 'complex123password', 'differentPassword123' + sign_up_ui 'good@email.com', 'complex123password', 'differentPassword123' expect(page).to have_selector('#error_explanation li') end @@ -54,7 +54,7 @@ context 'when a logged user reach the sign up page' do it 'redirect him to the root_page' do - sign_in + sign_in_ui visit new_user_registration_path expect(page).to have_current_path(root_path) From d9a84e0527a65d40c455660fa13a972ec6f5d68d Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Mon, 14 Jun 2021 12:09:22 +0700 Subject: [PATCH 28/39] [#5] Add FFaker to fabricate users --- Gemfile | 1 + spec/fabricators/user_fabricator.rb | 12 +++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 2a9fa924..c81352a4 100644 --- a/Gemfile +++ b/Gemfile @@ -53,6 +53,7 @@ group :development, :test do gem 'rubocop-rails', require: false # A RuboCop extension focused on enforcing Rails best practices and coding conventions. gem 'rubocop-rspec', require: false # Code style checking for RSpec files gem 'rubocop-performance', require: false # An extension of RuboCop focused on code performance checks. + gem 'ffaker' # used to easily generate fake data: names, addresses, phone numbers, etc. gem 'undercover' # Report missing test coverage in new changes gem 'danger' # Automated code review. diff --git a/spec/fabricators/user_fabricator.rb b/spec/fabricators/user_fabricator.rb index 78d99be0..2ed8fd45 100644 --- a/spec/fabricators/user_fabricator.rb +++ b/spec/fabricators/user_fabricator.rb @@ -1,9 +1,11 @@ # frozen_string_literal: true +require 'ffaker' + Fabricator(:user) do - lastname 'DENT' - firstname 'Arthur' - email 'arthur@dent.com' - password 'password' - password_confirmation 'password' + lastname FFaker::Name.last_name + firstname FFaker::Name.first_name + email FFaker::Internet.email + password 'password123' + password_confirmation 'password123' end From a52174d16b7ec69c612212edc463d261c7451042 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Mon, 14 Jun 2021 13:15:27 +0700 Subject: [PATCH 29/39] [#5] Fix english mistakes in tests --- spec/systems/login_spec.rb | 14 +++++++------- spec/systems/signup_spec.rb | 26 +++++++++++++------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/spec/systems/login_spec.rb b/spec/systems/login_spec.rb index a7749c80..3e13e701 100644 --- a/spec/systems/login_spec.rb +++ b/spec/systems/login_spec.rb @@ -3,24 +3,24 @@ require 'rails_helper' describe 'login', type: :system do - context 'when non logged user reach the app' do - it 'redirect the user to the login page' do + context 'when an unauthenticated user reaches the app' do + it 'redirects the user to the login page' do visit root_path expect(page).to have_current_path(new_user_session_path) end end - context 'when valid credentials' do - it 'log the user in and display a flash message' do + context 'when the user signed in with valid credentials' do + it 'logs the user in and displays a flash message' do sign_in_ui expect(page).to have_content(I18n.t('devise.sessions.signed_in')) end end - context 'when invalid credentials' do - it 'refuse login and display an error' do + context 'when the user tried to sign in with invalid credentials' do + it 'refuses login and displays an error' do bad_user = Fabricate(:user) bad_user.password = 'bad' error_msg = I18n.t('devise.failure.invalid').to_s.gsub('%{authentication_keys}', 'Email') @@ -31,7 +31,7 @@ end end - context 'when signed in and reach the home page' do + context 'when the user signed in and reached the home page' do it 'does not redirect to the sign in page' do sign_in Fabricate(:user) visit root_path diff --git a/spec/systems/signup_spec.rb b/spec/systems/signup_spec.rb index ec6a2e02..2363f621 100644 --- a/spec/systems/signup_spec.rb +++ b/spec/systems/signup_spec.rb @@ -3,32 +3,32 @@ require 'rails_helper' describe 'signup', type: :system do - context 'when non logged user reach the sign up page' do - it 'displays email field' do + context 'when an unauthenticated user reaches the sign up page' do + it 'displays the email field' do visit new_user_registration_path expect(find('form')).to have_field('user_email') end - it 'displays password field ' do + it 'displays the password field ' do visit new_user_registration_path expect(find('form')).to have_field('user_password') end - it 'displays password confirmation field ' do + it 'displays the password confirmation field ' do visit new_user_registration_path expect(find('form')).to have_field('user_password_confirmation') end end - context 'when non logged user sign up with good data' do - it 'create an account and log the user in' do + context 'when an unauthenticated user signs up with good data' do + it 'creates an account and logs the user in' do sign_up_ui 'good@email.com', 'password123' expect(page).to have_content(I18n.t('devise.registrations.signed_up')) end end - context 'when an non logged user sign up with bad email' do + context 'when an unauthenticated user signs up with bad email' do it 'stays on the same page' do sign_up_ui 'bad_email', 'password123' @@ -36,24 +36,24 @@ end end - context 'when an non logged user sign up with too short password' do - it 'display an error message' do + context 'when an unauthenticated user signs up with a too short password' do + it 'displays an error message' do sign_up_ui 'good@email.com', '123' expect(page).to have_selector('#error_explanation li') end end - context 'when an non logged user sign up with bad password confirmation' do - it 'display an error message' do + context 'when an unauthenticated user signs up with bad password confirmation' do + it 'displays an error message' do sign_up_ui 'good@email.com', 'complex123password', 'differentPassword123' expect(page).to have_selector('#error_explanation li') end end - context 'when a logged user reach the sign up page' do - it 'redirect him to the root_page' do + context 'when an authenticated user reaches the sign up page' do + it 'redirects him to the root_page' do sign_in_ui visit new_user_registration_path From 7f93b227fc438d99ae2c55948eb92e70f0bee872 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Mon, 14 Jun 2021 13:17:49 +0700 Subject: [PATCH 30/39] [#5] Remove duplicated email validation rule --- app/models/user.rb | 2 -- config/initializers/devise.rb | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index d08d38e7..c731ed78 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -6,8 +6,6 @@ class User < ApplicationRecord devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable - validates :email, format: URI::MailTo::EMAIL_REGEXP - # the authenticate method from devise documentation def self.authenticate(email, password) user = User.find_for_authentication(email: email) diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 22622073..f65176bb 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -183,7 +183,7 @@ # 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/ + config.email_regexp = URI::MailTo::EMAIL_REGEXP # ==> Configuration for :timeoutable # The time you want to timeout the user session without activity. After this From 3408c3091b72af4c4e3e809df0d6add9cbf00f73 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Mon, 14 Jun 2021 16:12:23 +0700 Subject: [PATCH 31/39] [#5] Add missing line break in tests --- spec/systems/signup_spec.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/systems/signup_spec.rb b/spec/systems/signup_spec.rb index 2363f621..8cb5e2f9 100644 --- a/spec/systems/signup_spec.rb +++ b/spec/systems/signup_spec.rb @@ -6,16 +6,19 @@ context 'when an unauthenticated user reaches the sign up page' do it 'displays the email field' do visit new_user_registration_path + expect(find('form')).to have_field('user_email') end it 'displays the password field ' do visit new_user_registration_path + expect(find('form')).to have_field('user_password') end it 'displays the password confirmation field ' do visit new_user_registration_path + expect(find('form')).to have_field('user_password_confirmation') end end From 220792fd91d7efea24cbaccc21e262449181bc1f Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Fri, 11 Jun 2021 09:30:31 +0700 Subject: [PATCH 32/39] root commit From 4d46c2da34c9a51594a5214e16b9d8c830560341 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Mon, 14 Jun 2021 16:45:28 +0700 Subject: [PATCH 33/39] [#5] Remove module prefix I18n in views --- app/views/devise/shared/_error_messages.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/devise/shared/_error_messages.html.erb b/app/views/devise/shared/_error_messages.html.erb index ba7ab887..bb5b70b0 100644 --- a/app/views/devise/shared/_error_messages.html.erb +++ b/app/views/devise/shared/_error_messages.html.erb @@ -1,7 +1,7 @@ <% if resource.errors.any? %>

- <%= I18n.t("errors.messages.not_saved", + <%= t("errors.messages.not_saved", count: resource.errors.count, resource: resource.class.model_name.human.downcase) %> From 99a488b6ca946e89b03358f794eaf074a7909721 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Mon, 14 Jun 2021 17:00:55 +0700 Subject: [PATCH 34/39] [#5] Update email db type to citext --- .../20210614164900_change_email_to_citext_for_users.rb | 6 ++++++ db/schema.rb | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20210614164900_change_email_to_citext_for_users.rb diff --git a/db/migrate/20210614164900_change_email_to_citext_for_users.rb b/db/migrate/20210614164900_change_email_to_citext_for_users.rb new file mode 100644 index 00000000..1ff27b4f --- /dev/null +++ b/db/migrate/20210614164900_change_email_to_citext_for_users.rb @@ -0,0 +1,6 @@ +class ChangeEmailToCitextForUsers < ActiveRecord::Migration[6.1] + def change + enable_extension 'citext' + change_column :users, :email, :citext + end +end diff --git a/db/schema.rb b/db/schema.rb index 2da16ccc..1a46f624 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,9 +10,10 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_06_09_093354) do +ActiveRecord::Schema.define(version: 2021_06_14_164900) do # These are extensions that must be enabled in order to support this database + enable_extension "citext" enable_extension "plpgsql" create_table "oauth_access_tokens", force: :cascade do |t| @@ -44,7 +45,7 @@ end create_table "users", force: :cascade do |t| - t.string "email", default: "", null: false + t.citext "email", default: "", null: false t.string "encrypted_password", default: "", null: false t.string "reset_password_token" t.datetime "reset_password_sent_at" From 3471a43a54d8bbfa45fa881d898db07625c63a65 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Mon, 14 Jun 2021 17:04:33 +0700 Subject: [PATCH 35/39] [#5] Rename lastname/firstname into last_name first_name --- app/controllers/application_controller.rb | 4 ++-- app/views/devise/registrations/edit.html.erb | 8 ++++---- app/views/devise/registrations/new.html.erb | 8 ++++---- db/migrate/20210614165400_rename_names_for_users.rb | 6 ++++++ 4 files changed, 16 insertions(+), 10 deletions(-) create mode 100644 db/migrate/20210614165400_rename_names_for_users.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0261cc2a..5aeb0078 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -12,10 +12,10 @@ class ApplicationController < ActionController::Base def update_allowed_parameters devise_parameter_sanitizer.permit(:sign_up) do |u| - u.permit(:firstname, :lastname, :email, :password, :password_confirmation) + u.permit(:first_name, :last_name, :email, :password, :password_confirmation) end devise_parameter_sanitizer.permit(:account_update) do |u| - u.permit(:firstname, :lastname, :email, :password, :current_password) + u.permit(:first_name, :last_name, :email, :password, :current_password) end end end diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb index a27eb554..3bfea996 100644 --- a/app/views/devise/registrations/edit.html.erb +++ b/app/views/devise/registrations/edit.html.erb @@ -4,13 +4,13 @@ <%= render "devise/shared/error_messages", resource: resource %>
- <%= f.label :firstname %>
- <%= f.text_field :firstname, autofocus: true %> + <%= f.label :first_name %>
+ <%= f.text_field :first_name, autofocus: true %>
- <%= f.label :lastname %>
- <%= f.text_field :lastname %> + <%= f.label :last_name %>
+ <%= f.text_field :last_name %>
diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb index 9bf55d7f..2a4fe044 100644 --- a/app/views/devise/registrations/new.html.erb +++ b/app/views/devise/registrations/new.html.erb @@ -4,13 +4,13 @@ <%= render "devise/shared/error_messages", resource: resource %>
- <%= f.label :firstname %>
- <%= f.text_field :firstname, autofocus: true %> + <%= f.label :first_name %>
+ <%= f.text_field :first_name, autofocus: true %>
- <%= f.label :lastname %>
- <%= f.text_field :lastname %> + <%= f.label :last_name %>
+ <%= f.text_field :last_name %>
diff --git a/db/migrate/20210614165400_rename_names_for_users.rb b/db/migrate/20210614165400_rename_names_for_users.rb new file mode 100644 index 00000000..88fb6530 --- /dev/null +++ b/db/migrate/20210614165400_rename_names_for_users.rb @@ -0,0 +1,6 @@ +class RenameNamesForUsers < ActiveRecord::Migration[6.1] + def change + rename_column :users, :lastname, :last_name + rename_column :users, :firstname, :first_name + end +end From 76dd4eecd13c98fdf0522326db1667d3d00d9eeb Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Mon, 14 Jun 2021 17:10:07 +0700 Subject: [PATCH 36/39] [#5] Fix missing renaming for lastname firstname --- db/schema.rb | 6 +++--- db/seeds.rb | 1 - spec/fabricators/user_fabricator.rb | 6 ++---- spec/support/authentication_helper.rb | 4 ++-- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index 1a46f624..8dc8d22a 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.define(version: 2021_06_14_164900) do +ActiveRecord::Schema.define(version: 2021_06_14_165400) do # These are extensions that must be enabled in order to support this database enable_extension "citext" @@ -52,8 +52,8 @@ t.datetime "remember_created_at" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false - t.string "firstname" - t.string "lastname" + t.string "first_name" + t.string "last_name" 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 diff --git a/db/seeds.rb b/db/seeds.rb index f68db517..88e00606 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -13,5 +13,4 @@ Doorkeeper::Application.create(name: "Android client", redirect_uri: "", scopes: "") end - Fabricate(:user) diff --git a/spec/fabricators/user_fabricator.rb b/spec/fabricators/user_fabricator.rb index 2ed8fd45..b449c33b 100644 --- a/spec/fabricators/user_fabricator.rb +++ b/spec/fabricators/user_fabricator.rb @@ -1,10 +1,8 @@ # frozen_string_literal: true -require 'ffaker' - Fabricator(:user) do - lastname FFaker::Name.last_name - firstname FFaker::Name.first_name + last_name FFaker::Name.last_name + first_name FFaker::Name.first_name email FFaker::Internet.email password 'password123' password_confirmation 'password123' diff --git a/spec/support/authentication_helper.rb b/spec/support/authentication_helper.rb index b0f775c2..9e50da4d 100644 --- a/spec/support/authentication_helper.rb +++ b/spec/support/authentication_helper.rb @@ -26,8 +26,8 @@ def sign_in(user) def sign_up_ui(email, password, password_confirm = nil) user = Fabricate(:user) visit new_user_registration_path - fill_in 'user_firstname', with: user.firstname - fill_in 'user_lastname', with: user.lastname + fill_in 'user_first_name', with: user.first_name + fill_in 'user_last_name', with: user.last_name fill_in 'user_email', with: email fill_in 'user_password', with: password fill_in 'user_password_confirmation', with: password_confirm || password From 2c9230608368b5767e28e88b0576fc196ef20f88 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Mon, 14 Jun 2021 17:11:41 +0700 Subject: [PATCH 37/39] [#5] Fix spacing for clarity in authentication helper --- spec/support/authentication_helper.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/support/authentication_helper.rb b/spec/support/authentication_helper.rb index 9e50da4d..7a9ffded 100644 --- a/spec/support/authentication_helper.rb +++ b/spec/support/authentication_helper.rb @@ -25,12 +25,15 @@ def sign_in(user) def sign_up_ui(email, password, password_confirm = nil) user = Fabricate(:user) + visit new_user_registration_path + fill_in 'user_first_name', with: user.first_name fill_in 'user_last_name', with: user.last_name fill_in 'user_email', with: email fill_in 'user_password', with: password fill_in 'user_password_confirmation', with: password_confirm || password + click_button 'Sign up' end end From 39f17ff94b9dcd2c2a121d43ea9dd20ac47238b8 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Mon, 14 Jun 2021 17:17:37 +0700 Subject: [PATCH 38/39] [#5] Merge email (citext) and renaming names into previous db migrations --- ...164900_change_email_to_citext_for_users.rb | 6 -- .../20210614165400_rename_names_for_users.rb | 6 -- db/schema.rb | 62 ------------------- 3 files changed, 74 deletions(-) delete mode 100644 db/migrate/20210614164900_change_email_to_citext_for_users.rb delete mode 100644 db/migrate/20210614165400_rename_names_for_users.rb delete mode 100644 db/schema.rb diff --git a/db/migrate/20210614164900_change_email_to_citext_for_users.rb b/db/migrate/20210614164900_change_email_to_citext_for_users.rb deleted file mode 100644 index 1ff27b4f..00000000 --- a/db/migrate/20210614164900_change_email_to_citext_for_users.rb +++ /dev/null @@ -1,6 +0,0 @@ -class ChangeEmailToCitextForUsers < ActiveRecord::Migration[6.1] - def change - enable_extension 'citext' - change_column :users, :email, :citext - end -end diff --git a/db/migrate/20210614165400_rename_names_for_users.rb b/db/migrate/20210614165400_rename_names_for_users.rb deleted file mode 100644 index 88fb6530..00000000 --- a/db/migrate/20210614165400_rename_names_for_users.rb +++ /dev/null @@ -1,6 +0,0 @@ -class RenameNamesForUsers < ActiveRecord::Migration[6.1] - def change - rename_column :users, :lastname, :last_name - rename_column :users, :firstname, :first_name - end -end diff --git a/db/schema.rb b/db/schema.rb deleted file mode 100644 index 8dc8d22a..00000000 --- a/db/schema.rb +++ /dev/null @@ -1,62 +0,0 @@ -# This file is auto-generated from the current state of the database. Instead -# of editing this file, please use the migrations feature of Active Record to -# incrementally modify your database, and then regenerate this schema definition. -# -# This file is the source Rails uses to define your schema when running `bin/rails -# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to -# be faster and is potentially less error prone than running all of your -# migrations from scratch. Old migrations may fail to apply correctly if those -# migrations use external dependencies or application code. -# -# It's strongly recommended that you check this file into your version control system. - -ActiveRecord::Schema.define(version: 2021_06_14_165400) do - - # These are extensions that must be enabled in order to support this database - enable_extension "citext" - enable_extension "plpgsql" - - create_table "oauth_access_tokens", force: :cascade do |t| - t.bigint "resource_owner_id" - t.bigint "application_id", null: false - t.string "token", null: false - t.string "refresh_token" - t.integer "expires_in" - t.datetime "revoked_at" - t.datetime "created_at", null: false - t.string "scopes" - t.string "previous_refresh_token", default: "", null: false - t.index ["application_id"], name: "index_oauth_access_tokens_on_application_id" - t.index ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true - t.index ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id" - t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true - end - - create_table "oauth_applications", force: :cascade do |t| - t.string "name", null: false - t.string "uid", null: false - t.string "secret", null: false - t.text "redirect_uri" - t.string "scopes", default: "", null: false - t.boolean "confidential", default: true, null: false - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false - t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true - end - - create_table "users", force: :cascade do |t| - t.citext "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.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false - t.string "first_name" - t.string "last_name" - 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 - - add_foreign_key "oauth_access_tokens", "oauth_applications", column: "application_id" -end From ff83bd7b7b49d0e7e2d4bc2b76dcd2e2f42ea7a2 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Mon, 14 Jun 2021 17:43:12 +0700 Subject: [PATCH 39/39] [#5] Merge email (citext) and renaming names into previous db migrations --- .../20210609041601_devise_create_users.rb | 4 +- .../20210609041826_add_name_to_users.rb | 4 +- db/schema.rb | 62 +++++++++++++++++++ 3 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 db/schema.rb diff --git a/db/migrate/20210609041601_devise_create_users.rb b/db/migrate/20210609041601_devise_create_users.rb index cc0991d9..f805680a 100644 --- a/db/migrate/20210609041601_devise_create_users.rb +++ b/db/migrate/20210609041601_devise_create_users.rb @@ -2,9 +2,11 @@ class DeviseCreateUsers < ActiveRecord::Migration[6.1] def change + enable_extension 'citext' + create_table :users do |t| ## Database authenticatable - t.string :email, null: false, default: "" + t.citext :email, null: false, default: "" t.string :encrypted_password, null: false, default: "" ## Recoverable diff --git a/db/migrate/20210609041826_add_name_to_users.rb b/db/migrate/20210609041826_add_name_to_users.rb index 5224fc27..ffe21801 100644 --- a/db/migrate/20210609041826_add_name_to_users.rb +++ b/db/migrate/20210609041826_add_name_to_users.rb @@ -1,6 +1,6 @@ class AddNameToUsers < ActiveRecord::Migration[6.1] def change - add_column :users, :firstname, :string - add_column :users, :lastname, :string + add_column :users, :first_name, :string + add_column :users, :last_name, :string end end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 00000000..3c69c4f6 --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,62 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 2021_06_09_093354) do + + # These are extensions that must be enabled in order to support this database + enable_extension "citext" + enable_extension "plpgsql" + + create_table "oauth_access_tokens", force: :cascade do |t| + t.bigint "resource_owner_id" + t.bigint "application_id", null: false + t.string "token", null: false + t.string "refresh_token" + t.integer "expires_in" + t.datetime "revoked_at" + t.datetime "created_at", null: false + t.string "scopes" + t.string "previous_refresh_token", default: "", null: false + t.index ["application_id"], name: "index_oauth_access_tokens_on_application_id" + t.index ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true + t.index ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id" + t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true + end + + create_table "oauth_applications", force: :cascade do |t| + t.string "name", null: false + t.string "uid", null: false + t.string "secret", null: false + t.text "redirect_uri" + t.string "scopes", default: "", null: false + t.boolean "confidential", default: true, null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true + end + + create_table "users", force: :cascade do |t| + t.citext "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.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.string "first_name" + t.string "last_name" + 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 + + add_foreign_key "oauth_access_tokens", "oauth_applications", column: "application_id" +end