From 5da306d14c484a94bc12e4840f85f0fd9ce19600 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 9 Jun 2021 11:14:02 +0700 Subject: [PATCH 01/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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/99] 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/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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/99] [#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 From 16044eb92ee535822775fcec273ef76ac1f03150 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 9 Jun 2021 11:14:02 +0700 Subject: [PATCH 40/99] [#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 203b7292d6060cab42d7061c7bdf46f2d202dc81 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 9 Jun 2021 11:15:26 +0700 Subject: [PATCH 41/99] [#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 1d3142ae924e82072e3628b7e1874e843e050164 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 16 Jun 2021 16:14:49 +0700 Subject: [PATCH 42/99] Rebase from develop routes --- app/models/user.rb | 6 +++ config/routes.rb | 3 +- .../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, 77 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 fe7e4611..3ec01389 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,5 +2,6 @@ root to: 'keywords#index' resources :keywords, only: [:index, :create] - # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html + + devise_for :users #authentication 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 e858d6de87fa9e01d379bd7936cd2c5575410ed1 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 9 Jun 2021 11:19:02 +0700 Subject: [PATCH 43/99] [#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 4d2c82880634ca70ae5721528eafd99a4c7ce3cc Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 9 Jun 2021 11:22:42 +0700 Subject: [PATCH 44/99] [#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 6434a3380c1e4afdf1cc08a6d6a27a34a3c2c9a9 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 9 Jun 2021 11:32:52 +0700 Subject: [PATCH 45/99] [#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 62479724afff3125abb4bb4290c53612cd851ed1 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 9 Jun 2021 16:21:21 +0700 Subject: [PATCH 46/99] [#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 09a9a7d2664a016f745f23777e5825bd4023ddf2 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 9 Jun 2021 16:25:27 +0700 Subject: [PATCH 47/99] [#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 046bd91c355de23be68f8d97012afc96cc8a9b35 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 9 Jun 2021 18:17:25 +0700 Subject: [PATCH 48/99] [#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 f4c0c8ad5d992a1e0388df2dd863fb36141f08d9 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 16 Jun 2021 16:16:16 +0700 Subject: [PATCH 49/99] Rebase from develop routes --- 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 22820aa2..50318565 100644 --- a/Gemfile +++ b/Gemfile @@ -18,6 +18,7 @@ gem 'httparty' #Makes http fun again! Ain't no party like a httparty, because a # 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 768b5958..96918292 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) @@ -487,6 +489,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 3ec01389..6cce7cb6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,11 @@ Rails.application.routes.draw do root to: 'keywords#index' - resources :keywords, only: [:index, :create] + use_doorkeeper do + skip_controllers :authorizations, :applications, :authorized_applications + end + + devise_for :users - devise_for :users #authentication + resources :keywords, only: [:index, :create] 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 d01ad1fc111aa73fc72bfb2b3213ee3dfe780a0c Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Thu, 10 Jun 2021 15:50:19 +0700 Subject: [PATCH 50/99] [#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 c14ef694d39dc707184f8c56b4710586a45766cb Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 16 Jun 2021 16:19:45 +0700 Subject: [PATCH 51/99] Rebase from develop pull --- config/routes.rb | 1 + db/schema.rb | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index 6cce7cb6..8430f33a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,5 @@ Rails.application.routes.draw do +<<<<<<< HEAD root to: 'keywords#index' use_doorkeeper do diff --git a/db/schema.rb b/db/schema.rb index 2da16ccc..2e3bd7a9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -51,8 +51,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 From c848535a3f0f1e4cd93266d8f55c94b9d8d6a44e Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 16 Jun 2021 16:24:16 +0700 Subject: [PATCH 52/99] Rebase from develop pull --- config/routes.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 8430f33a..6cce7cb6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,4 @@ Rails.application.routes.draw do -<<<<<<< HEAD root to: 'keywords#index' use_doorkeeper do From 2422d5f9f1226cb5059bc4ccc6f815032489af4b Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Thu, 10 Jun 2021 18:17:03 +0700 Subject: [PATCH 53/99] [#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 89fbf4cf8740076a4b8f185228537aa3e90ec875 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Thu, 10 Jun 2021 18:17:29 +0700 Subject: [PATCH 54/99] [#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 8688bddbe241631c56627979528445a24a3f9353 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Fri, 11 Jun 2021 13:11:49 +0700 Subject: [PATCH 55/99] [#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 c967e47514239f690ac19c58c15671e86c96dbcf Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Fri, 11 Jun 2021 13:18:16 +0700 Subject: [PATCH 56/99] [#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 f353dad827c7f9929e5833cf7597b8aeeeafe1a4 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Mon, 14 Jun 2021 11:57:42 +0700 Subject: [PATCH 57/99] [#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 39b1629fe3e2cdbce8cd7654a447c24ab3dfc311 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Mon, 14 Jun 2021 12:09:22 +0700 Subject: [PATCH 58/99] [#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 50318565..8c112f4f 100644 --- a/Gemfile +++ b/Gemfile @@ -54,6 +54,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 550e4c1ae115984ebcbcaeb757cd87335018fc79 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Mon, 14 Jun 2021 13:15:27 +0700 Subject: [PATCH 59/99] [#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 963b41bb5374969217ed56f56957829026a4e23a Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Mon, 14 Jun 2021 13:17:49 +0700 Subject: [PATCH 60/99] [#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 3b4ab59c73dd414c7657063a2681ce9bf3360a7d Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Mon, 14 Jun 2021 16:12:23 +0700 Subject: [PATCH 61/99] [#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 4dc2e4584a637cd302680739ca71409672f00a03 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Fri, 11 Jun 2021 09:30:31 +0700 Subject: [PATCH 62/99] root commit From cd23962e62379399d8ce9d465fd2e73d67acc242 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Mon, 14 Jun 2021 16:45:28 +0700 Subject: [PATCH 63/99] [#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 a7cc9bb53a24c24afc738bc27b88ecacc4f08046 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Mon, 14 Jun 2021 17:00:55 +0700 Subject: [PATCH 64/99] [#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 2e3bd7a9..df1d0e82 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 0f0bd4715be9be0948f9f6ec0a6d8e9ee3bce0ff Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Mon, 14 Jun 2021 17:04:33 +0700 Subject: [PATCH 65/99] [#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 825cf313f358e67c67c01ac71d2a340c3c902490 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Mon, 14 Jun 2021 17:10:07 +0700 Subject: [PATCH 66/99] [#5] Fix missing renaming for lastname firstname --- db/schema.rb | 2 +- db/seeds.rb | 1 - spec/fabricators/user_fabricator.rb | 6 ++---- spec/support/authentication_helper.rb | 4 ++-- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index df1d0e82..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" 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 0802b35aa3f460be56350e867b3b95eb8634b118 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Mon, 14 Jun 2021 17:11:41 +0700 Subject: [PATCH 67/99] [#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 8350f994e502d184d5e8ee488f765badfbd26b8c Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Mon, 14 Jun 2021 17:17:37 +0700 Subject: [PATCH 68/99] [#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 10359cbdf4164d47417dd524ad6a63f0c303d33f Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Mon, 14 Jun 2021 17:43:12 +0700 Subject: [PATCH 69/99] [#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 From a92e863feeda47f6cfa2c483e073092da5026d19 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 16 Jun 2021 16:28:52 +0700 Subject: [PATCH 70/99] [#6] Declare vcr inline to improve code readability --- .../services/google_service/client_service_spec.rb | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/spec/services/google_service/client_service_spec.rb b/spec/services/google_service/client_service_spec.rb index 47e6294c..5054760e 100644 --- a/spec/services/google_service/client_service_spec.rb +++ b/spec/services/google_service/client_service_spec.rb @@ -4,20 +4,14 @@ RSpec.describe GoogleService::ClientService, type: :service do context 'when querying a simple keyword' do - it 'returns an HTTParty Response' do - result = nil - VCR.use_cassette('google_search') do - result = described_class.query(FFaker::Lorem.word) - end + it 'returns an HTTParty Response', vcr: 'google_search' do + result = described_class.query(FFaker::Lorem.word) expect(result).to be_an_instance_of(HTTParty::Response) end - it 'queries Google Search' do - path = nil - VCR.use_cassette('google_search') do - path = described_class.query(FFaker::Lorem.word).request.path - end + it 'queries Google Search', vcr: 'google_search' do + path = described_class.query(FFaker::Lorem.word).request.path expect(path.to_s).to start_with('https://www.google.com/search') end From 6a336f91b1700db6f1f874838c73f41be788f3c5 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 16 Jun 2021 16:42:14 +0700 Subject: [PATCH 71/99] [#6] Replace instance variables by local variables inside view --- app/controllers/keywords_controller.rb | 9 +++++++-- app/views/keywords/create.html.erb | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/controllers/keywords_controller.rb b/app/controllers/keywords_controller.rb index 0e1db84a..ae8dec5b 100644 --- a/app/controllers/keywords_controller.rb +++ b/app/controllers/keywords_controller.rb @@ -4,7 +4,12 @@ class KeywordsController < ApplicationController def index; end def create - @keyword = params['keyword'] - @raw_response = GoogleService::ClientService.query(@keyword) + keyword = params['keyword'] + raw_response = GoogleService::ClientService.query(keyword) + + render :create, locals: { + keyword: keyword, + raw_response: raw_response + } end end diff --git a/app/views/keywords/create.html.erb b/app/views/keywords/create.html.erb index e415f5a4..1585a806 100644 --- a/app/views/keywords/create.html.erb +++ b/app/views/keywords/create.html.erb @@ -1,4 +1,4 @@ -

Result for <%= @keyword %>

+

Result for <%= keyword %>

- <%= @raw_response.body.html_safe %> + <%= raw_response.body.html_safe %>
From 91aacc3dd729a4724fa5b26db5ea947cc77b7557 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 16 Jun 2021 17:42:07 +0700 Subject: [PATCH 72/99] [#6] Handle error while query google - not colorized --- app/controllers/keywords_controller.rb | 4 ++- app/services/google_service/client_service.rb | 32 ++++++++++++++++--- config/locales/en.yml | 2 ++ 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/app/controllers/keywords_controller.rb b/app/controllers/keywords_controller.rb index ae8dec5b..960e50da 100644 --- a/app/controllers/keywords_controller.rb +++ b/app/controllers/keywords_controller.rb @@ -5,7 +5,9 @@ def index; end def create keyword = params['keyword'] - raw_response = GoogleService::ClientService.query(keyword) + raw_response = GoogleService::ClientService.new(keyword).query_result + + return redirect_to keywords_path, alert: I18n.t('keywords.could_not_query') unless raw_response render :create, locals: { keyword: keyword, diff --git a/app/services/google_service/client_service.rb b/app/services/google_service/client_service.rb index 725d9017..5181a540 100644 --- a/app/services/google_service/client_service.rb +++ b/app/services/google_service/client_service.rb @@ -4,12 +4,34 @@ module GoogleService class ClientService require 'httparty' - def self.query(keyword, lang = 'en') - escaped_keyword = CGI.escape(keyword) - uri = URI("https://google.com/search?q=#{escaped_keyword}&hl=#{lang}&gl=#{lang}") - user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) '\ + USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) '\ 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36' - HTTParty.get(uri, { headers: { 'User-Agent' => user_agent } }) + + def initialize(keyword, lang = 'en') + escaped_keyword = CGI.escape(keyword) + @uri = URI("https://auibawsasxasxechjksdc.fr/search?q=#{escaped_keyword}&hl=#{lang}&gl=#{lang}") + end + + # Inspect Http response status code + # Any non 200 response code will be logged + # response is set to nil in order to notify the error + def validate_result + return if @result.response.code == '200' + + Rails.logger.warn "Warning: Query Google with keyword #{@keyword} return status code #{@result.response.code}" + @result = nil + end + + def query_result + begin + @result = HTTParty.get(@uri, { headers: { 'User-Agent' => USER_AGENT } }) + rescue HTTParty::Error, Timeout::Error, SocketError => e + Rails.logger.error "Error: Query Google with keyword #{@keyword} throw an error: #{e}" + @result = nil + else + validate_result + end + @result end end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 14ffadfb..3981694a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -30,4 +30,6 @@ # available at https://guides.rubyonrails.org/i18n.html. en: + keywords: + could_not_query: 'An error occurs when performing the Google Search, please try again.' logout: "Sign out" From a930f345779aab1486de39200a1ae685e0f52aa3 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 16 Jun 2021 17:44:08 +0700 Subject: [PATCH 73/99] [#6] Review methods order and exposure --- app/services/google_service/client_service.rb | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/app/services/google_service/client_service.rb b/app/services/google_service/client_service.rb index 5181a540..c6aecd0e 100644 --- a/app/services/google_service/client_service.rb +++ b/app/services/google_service/client_service.rb @@ -9,17 +9,7 @@ class ClientService def initialize(keyword, lang = 'en') escaped_keyword = CGI.escape(keyword) - @uri = URI("https://auibawsasxasxechjksdc.fr/search?q=#{escaped_keyword}&hl=#{lang}&gl=#{lang}") - end - - # Inspect Http response status code - # Any non 200 response code will be logged - # response is set to nil in order to notify the error - def validate_result - return if @result.response.code == '200' - - Rails.logger.warn "Warning: Query Google with keyword #{@keyword} return status code #{@result.response.code}" - @result = nil + @uri = URI("https://google.com/search?q=#{escaped_keyword}&hl=#{lang}&gl=#{lang}") end def query_result @@ -33,5 +23,16 @@ def query_result end @result end + + private + # Inspect Http response status code + # Any non 200 response code will be logged + # response is set to nil in order to notify the error + def validate_result + return if @result.response.code == '200' + + Rails.logger.warn "Warning: Query Google with keyword #{@keyword} return status code #{@result.response.code}" + @result = nil + end end end From b77e6facfb200c53790553c21534da4e805f4a28 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 16 Jun 2021 17:45:53 +0700 Subject: [PATCH 74/99] [#6] Update tests from class method to instance methods --- app/services/google_service/client_service.rb | 1 + spec/services/google_service/client_service_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/services/google_service/client_service.rb b/app/services/google_service/client_service.rb index c6aecd0e..da18325d 100644 --- a/app/services/google_service/client_service.rb +++ b/app/services/google_service/client_service.rb @@ -25,6 +25,7 @@ def query_result end private + # Inspect Http response status code # Any non 200 response code will be logged # response is set to nil in order to notify the error diff --git a/spec/services/google_service/client_service_spec.rb b/spec/services/google_service/client_service_spec.rb index 5054760e..be0e5685 100644 --- a/spec/services/google_service/client_service_spec.rb +++ b/spec/services/google_service/client_service_spec.rb @@ -5,13 +5,13 @@ RSpec.describe GoogleService::ClientService, type: :service do context 'when querying a simple keyword' do it 'returns an HTTParty Response', vcr: 'google_search' do - result = described_class.query(FFaker::Lorem.word) + result = described_class.new(FFaker::Lorem.word).query_result expect(result).to be_an_instance_of(HTTParty::Response) end it 'queries Google Search', vcr: 'google_search' do - path = described_class.query(FFaker::Lorem.word).request.path + path = described_class.new(FFaker::Lorem.word).query_result.request.path expect(path.to_s).to start_with('https://www.google.com/search') end From c13f432cf2e90f7dda0a0d605e10d7295f82967e Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Wed, 16 Jun 2021 18:23:51 +0700 Subject: [PATCH 75/99] [#6] Colorize logs from Rails.logger --- Gemfile | 3 +++ Gemfile.lock | 2 ++ app/services/google_service/client_service.rb | 4 +++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 8c112f4f..7bb4a7b6 100644 --- a/Gemfile +++ b/Gemfile @@ -24,6 +24,9 @@ gem 'doorkeeper' # Awesome OAuth 2 provider for your Rails / Grape app gem 'webpacker', '~>5.2.0' # Transpile app-like JavaScript gem 'sass-rails' # SASS +# Logging tools +gem 'colorize' # Ruby gem for colorizing text using ANSI escape sequences + # Translations # gem 'devise-i18n' # Translations for Devise # gem 'rails-i18n', '~> 6.0.0' # Translations for Rails diff --git a/Gemfile.lock b/Gemfile.lock index 96918292..218778fa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -97,6 +97,7 @@ GEM sexp_processor coderay (1.1.3) colored2 (3.1.2) + colorize (0.8.1) concurrent-ruby (1.1.9) connection_pool (2.2.5) cork (0.3.0) @@ -478,6 +479,7 @@ DEPENDENCIES brakeman bullet capybara (>= 2.15) + colorize danger danger-brakeman_scanner danger-eslint diff --git a/app/services/google_service/client_service.rb b/app/services/google_service/client_service.rb index da18325d..d59d40dd 100644 --- a/app/services/google_service/client_service.rb +++ b/app/services/google_service/client_service.rb @@ -3,6 +3,7 @@ module GoogleService class ClientService require 'httparty' + require 'colorize' USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) '\ 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36' @@ -16,7 +17,7 @@ def query_result begin @result = HTTParty.get(@uri, { headers: { 'User-Agent' => USER_AGENT } }) rescue HTTParty::Error, Timeout::Error, SocketError => e - Rails.logger.error "Error: Query Google with keyword #{@keyword} throw an error: #{e}" + Rails.logger.error "Error: Query Google with keyword #{@keyword} throw an error: #{e}".colorize(:red) @result = nil else validate_result @@ -33,6 +34,7 @@ def validate_result return if @result.response.code == '200' Rails.logger.warn "Warning: Query Google with keyword #{@keyword} return status code #{@result.response.code}" + .colorize(:yellow) @result = nil end end From ce2610529927dbd1fe9e8cf6b1b42522c39b4dd9 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Fri, 11 Jun 2021 09:30:31 +0700 Subject: [PATCH 76/99] root commit From 24e908be132f2e07e4448259336cc63129eed7f5 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Thu, 17 Jun 2021 09:34:58 +0700 Subject: [PATCH 77/99] Rebase to develop - pull --- db/schema.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index 3c69c4f6..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_09_093354) 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" From 4d2d11b6b0d74e3cde5020aa59c637b1ae11e026 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Thu, 17 Jun 2021 09:37:14 +0700 Subject: [PATCH 78/99] Rebase to develop - pull --- config/routes.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/routes.rb b/config/routes.rb index 6cce7cb6..710cf69a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -8,4 +8,5 @@ devise_for :users resources :keywords, only: [:index, :create] + end From 8af64860de184fac56a38e35e23ceee6138275cb Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Thu, 17 Jun 2021 09:37:55 +0700 Subject: [PATCH 79/99] Rebase to develop - pull --- config/routes.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 710cf69a..6cce7cb6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -8,5 +8,4 @@ devise_for :users resources :keywords, only: [:index, :create] - end From b5243f8cff704ae08946daebcf14f56a734ca40e Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Fri, 11 Jun 2021 09:30:31 +0700 Subject: [PATCH 80/99] root commit From f147f0c86a26de930f54b7d40461298dbb2c6c1f Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Thu, 17 Jun 2021 10:37:19 +0700 Subject: [PATCH 81/99] [#6] Improve create action readability --- app/controllers/keywords_controller.rb | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/controllers/keywords_controller.rb b/app/controllers/keywords_controller.rb index 960e50da..c4d72027 100644 --- a/app/controllers/keywords_controller.rb +++ b/app/controllers/keywords_controller.rb @@ -7,11 +7,14 @@ def create keyword = params['keyword'] raw_response = GoogleService::ClientService.new(keyword).query_result - return redirect_to keywords_path, alert: I18n.t('keywords.could_not_query') unless raw_response + if raw_response + render :create, locals: { + keyword: keyword, + raw_response: raw_response + } + return + end - render :create, locals: { - keyword: keyword, - raw_response: raw_response - } + redirect_to keywords_path, alert: I18n.t('keywords.could_not_query') end end From f4e257db21626cf6d64108b4252a8a53157e21c3 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Mon, 21 Jun 2021 11:29:14 +0700 Subject: [PATCH 82/99] [#6] Remove create action and index form --- app/controllers/keywords_controller.rb | 15 --------------- app/views/keywords/create.html.erb | 4 ---- app/views/keywords/index.html.erb | 6 ------ 3 files changed, 25 deletions(-) delete mode 100644 app/views/keywords/create.html.erb diff --git a/app/controllers/keywords_controller.rb b/app/controllers/keywords_controller.rb index c4d72027..22006d8a 100644 --- a/app/controllers/keywords_controller.rb +++ b/app/controllers/keywords_controller.rb @@ -2,19 +2,4 @@ class KeywordsController < ApplicationController def index; end - - def create - keyword = params['keyword'] - raw_response = GoogleService::ClientService.new(keyword).query_result - - if raw_response - render :create, locals: { - keyword: keyword, - raw_response: raw_response - } - return - end - - redirect_to keywords_path, alert: I18n.t('keywords.could_not_query') - end end diff --git a/app/views/keywords/create.html.erb b/app/views/keywords/create.html.erb deleted file mode 100644 index 1585a806..00000000 --- a/app/views/keywords/create.html.erb +++ /dev/null @@ -1,4 +0,0 @@ -

Result for <%= keyword %>

-
- <%= raw_response.body.html_safe %> -
diff --git a/app/views/keywords/index.html.erb b/app/views/keywords/index.html.erb index 64970f2b..6d0bb5a1 100644 --- a/app/views/keywords/index.html.erb +++ b/app/views/keywords/index.html.erb @@ -1,8 +1,2 @@

Keywords#index

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

- -<%= form_with url: keywords_path, method: :post do |form| %> - <%= form.label :keyword, "Search for:" %> - <%= form.text_field :keyword %> - <%= form.submit "Search" %> -<% end %> From c0808a6d6e64f50e28fdbb4a03d3f4ebb41828ee Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Mon, 21 Jun 2021 17:09:04 +0700 Subject: [PATCH 83/99] [#6] remove useless require --- app/services/google_service/client_service.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/services/google_service/client_service.rb b/app/services/google_service/client_service.rb index d59d40dd..b70d8bd8 100644 --- a/app/services/google_service/client_service.rb +++ b/app/services/google_service/client_service.rb @@ -2,8 +2,6 @@ module GoogleService class ClientService - require 'httparty' - require 'colorize' USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) '\ 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36' From a510af8295cf6c75e7d47bba42738f1ff183755a Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Mon, 21 Jun 2021 17:13:03 +0700 Subject: [PATCH 84/99] [#6] Set base url google as constant --- app/services/google_service/client_service.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/services/google_service/client_service.rb b/app/services/google_service/client_service.rb index b70d8bd8..7c86d109 100644 --- a/app/services/google_service/client_service.rb +++ b/app/services/google_service/client_service.rb @@ -6,9 +6,11 @@ class ClientService USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) '\ 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36' + BASE_SEARCH_URL2 = 'https://google.com/search' + def initialize(keyword, lang = 'en') escaped_keyword = CGI.escape(keyword) - @uri = URI("https://google.com/search?q=#{escaped_keyword}&hl=#{lang}&gl=#{lang}") + @uri = URI("#{BASE_SEARCH_URL2}?q=#{escaped_keyword}&hl=#{lang}&gl=#{lang}") end def query_result @@ -32,7 +34,7 @@ def validate_result return if @result.response.code == '200' Rails.logger.warn "Warning: Query Google with keyword #{@keyword} return status code #{@result.response.code}" - .colorize(:yellow) + .colorize(:yellow) @result = nil end end From 74b3c561c74e9867fd243091e017258379de9072 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Mon, 21 Jun 2021 17:14:25 +0700 Subject: [PATCH 85/99] [#6] Fix formatting warnings --- app/services/google_service/client_service.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/services/google_service/client_service.rb b/app/services/google_service/client_service.rb index 7c86d109..848578b9 100644 --- a/app/services/google_service/client_service.rb +++ b/app/services/google_service/client_service.rb @@ -2,7 +2,6 @@ module GoogleService class ClientService - USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) '\ 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36' @@ -34,7 +33,7 @@ def validate_result return if @result.response.code == '200' Rails.logger.warn "Warning: Query Google with keyword #{@keyword} return status code #{@result.response.code}" - .colorize(:yellow) + .colorize(:yellow) @result = nil end end From b5537820a05b83e12d08d2bed1831c66a76fd6e9 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Mon, 21 Jun 2021 17:30:22 +0700 Subject: [PATCH 86/99] [#6] Rename query_result in call --- app/services/google_service/client_service.rb | 2 +- spec/services/google_service/client_service_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/services/google_service/client_service.rb b/app/services/google_service/client_service.rb index 848578b9..7b9e5a93 100644 --- a/app/services/google_service/client_service.rb +++ b/app/services/google_service/client_service.rb @@ -12,7 +12,7 @@ def initialize(keyword, lang = 'en') @uri = URI("#{BASE_SEARCH_URL2}?q=#{escaped_keyword}&hl=#{lang}&gl=#{lang}") end - def query_result + def call begin @result = HTTParty.get(@uri, { headers: { 'User-Agent' => USER_AGENT } }) rescue HTTParty::Error, Timeout::Error, SocketError => e diff --git a/spec/services/google_service/client_service_spec.rb b/spec/services/google_service/client_service_spec.rb index be0e5685..cacdc586 100644 --- a/spec/services/google_service/client_service_spec.rb +++ b/spec/services/google_service/client_service_spec.rb @@ -5,13 +5,13 @@ RSpec.describe GoogleService::ClientService, type: :service do context 'when querying a simple keyword' do it 'returns an HTTParty Response', vcr: 'google_search' do - result = described_class.new(FFaker::Lorem.word).query_result + result = described_class.new(FFaker::Lorem.word).call expect(result).to be_an_instance_of(HTTParty::Response) end it 'queries Google Search', vcr: 'google_search' do - path = described_class.new(FFaker::Lorem.word).query_result.request.path + path = described_class.new(FFaker::Lorem.word).call.request.path expect(path.to_s).to start_with('https://www.google.com/search') end From 3b397582b8cf4a0ad63ff4b1270d3be42df16af6 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Tue, 22 Jun 2021 14:24:00 +0700 Subject: [PATCH 87/99] [#6] Remove keywords#create action from routes --- config/routes.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index 6e3ef4dc..9c495773 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,11 +1,11 @@ Rails.application.routes.draw do root to: 'keywords#index' - + use_doorkeeper do skip_controllers :authorizations, :applications, :authorized_applications end devise_for :users - resources :keywords, only: [:index, :create] + resources :keywords, only: :index end From 22dd1eedb1f7571debf3c95e82e7f88109958ec9 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Tue, 22 Jun 2021 14:30:07 +0700 Subject: [PATCH 88/99] [#6] Use keyword arguments to initialize service --- app/services/google_service/client_service.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/services/google_service/client_service.rb b/app/services/google_service/client_service.rb index 7b9e5a93..52ad1300 100644 --- a/app/services/google_service/client_service.rb +++ b/app/services/google_service/client_service.rb @@ -5,11 +5,11 @@ class ClientService USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) '\ 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36' - BASE_SEARCH_URL2 = 'https://google.com/search' + BASE_SEARCH_URL = 'https://google.com/search' - def initialize(keyword, lang = 'en') + def initialize(keyword:, lang: 'en') escaped_keyword = CGI.escape(keyword) - @uri = URI("#{BASE_SEARCH_URL2}?q=#{escaped_keyword}&hl=#{lang}&gl=#{lang}") + @uri = URI("#{BASE_SEARCH_URL}?q=#{escaped_keyword}&hl=#{lang}&gl=#{lang}") end def call From daaa4436d17c97f39f200e4dbb8b7e632d2cb62d Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Tue, 22 Jun 2021 14:37:14 +0700 Subject: [PATCH 89/99] [#6] Rename GoogleService namespace in Google --- app/services/{google_service => google}/client_service.rb | 2 +- spec/services/{google_service => google}/client_service_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename app/services/{google_service => google}/client_service.rb (98%) rename spec/services/{google_service => google}/client_service_spec.rb (89%) diff --git a/app/services/google_service/client_service.rb b/app/services/google/client_service.rb similarity index 98% rename from app/services/google_service/client_service.rb rename to app/services/google/client_service.rb index 52ad1300..fbc0d2bd 100644 --- a/app/services/google_service/client_service.rb +++ b/app/services/google/client_service.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module GoogleService +module Service class ClientService USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) '\ 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36' diff --git a/spec/services/google_service/client_service_spec.rb b/spec/services/google/client_service_spec.rb similarity index 89% rename from spec/services/google_service/client_service_spec.rb rename to spec/services/google/client_service_spec.rb index cacdc586..44416409 100644 --- a/spec/services/google_service/client_service_spec.rb +++ b/spec/services/google/client_service_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe GoogleService::ClientService, type: :service do +RSpec.describe Google::ClientService, type: :service do context 'when querying a simple keyword' do it 'returns an HTTParty Response', vcr: 'google_search' do result = described_class.new(FFaker::Lorem.word).call From bd0d2331d0a3680be774b46f4ff836dc50be48fc Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Tue, 22 Jun 2021 14:37:44 +0700 Subject: [PATCH 90/99] [#6] Rename GoogleService namespace in Google --- app/services/google/client_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/google/client_service.rb b/app/services/google/client_service.rb index fbc0d2bd..74ab194f 100644 --- a/app/services/google/client_service.rb +++ b/app/services/google/client_service.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Service +module Google class ClientService USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) '\ 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36' From d39603473adb6e9d652a3e452b2c93ca063127ee Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Tue, 22 Jun 2021 14:47:57 +0700 Subject: [PATCH 91/99] [#6] Fix test to map keyword arguments --- spec/services/google/client_service_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/services/google/client_service_spec.rb b/spec/services/google/client_service_spec.rb index 44416409..095a624f 100644 --- a/spec/services/google/client_service_spec.rb +++ b/spec/services/google/client_service_spec.rb @@ -5,13 +5,13 @@ RSpec.describe Google::ClientService, type: :service do context 'when querying a simple keyword' do it 'returns an HTTParty Response', vcr: 'google_search' do - result = described_class.new(FFaker::Lorem.word).call + result = described_class.new(keyword: FFaker::Lorem.word).call expect(result).to be_an_instance_of(HTTParty::Response) end it 'queries Google Search', vcr: 'google_search' do - path = described_class.new(FFaker::Lorem.word).call.request.path + path = described_class.new(keyword: FFaker::Lorem.word).call.request.path expect(path.to_s).to start_with('https://www.google.com/search') end From 8f202cc7afea0d1b38c6d27c4b758e7d7bbab4be Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Tue, 22 Jun 2021 14:49:56 +0700 Subject: [PATCH 92/99] [#6] Review implementation of ClientService.call to be less risky with result edition --- app/services/google/client_service.rb | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/app/services/google/client_service.rb b/app/services/google/client_service.rb index 74ab194f..9ba23c79 100644 --- a/app/services/google/client_service.rb +++ b/app/services/google/client_service.rb @@ -14,14 +14,15 @@ def initialize(keyword:, lang: 'en') def call begin - @result = HTTParty.get(@uri, { headers: { 'User-Agent' => USER_AGENT } }) + result = HTTParty.get(@uri, { headers: { 'User-Agent' => USER_AGENT } }) + rescue HTTParty::Error, Timeout::Error, SocketError => e Rails.logger.error "Error: Query Google with keyword #{@keyword} throw an error: #{e}".colorize(:red) - @result = nil - else - validate_result + + result = nil end - @result + + validate_result result end private @@ -29,12 +30,13 @@ def call # Inspect Http response status code # Any non 200 response code will be logged # response is set to nil in order to notify the error - def validate_result - return if @result.response.code == '200' + def validate_result(result) + return result if result.response.code == '200' Rails.logger.warn "Warning: Query Google with keyword #{@keyword} return status code #{@result.response.code}" .colorize(:yellow) - @result = nil + + nil end end end From 93254008c72b183e1d55a915fd7b69437f789c8c Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Tue, 22 Jun 2021 15:03:47 +0700 Subject: [PATCH 93/99] [#6] Fix rubocop empty line --- app/services/google/client_service.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/services/google/client_service.rb b/app/services/google/client_service.rb index 9ba23c79..e4d36100 100644 --- a/app/services/google/client_service.rb +++ b/app/services/google/client_service.rb @@ -15,7 +15,6 @@ def initialize(keyword:, lang: 'en') def call begin result = HTTParty.get(@uri, { headers: { 'User-Agent' => USER_AGENT } }) - rescue HTTParty::Error, Timeout::Error, SocketError => e Rails.logger.error "Error: Query Google with keyword #{@keyword} throw an error: #{e}".colorize(:red) From 0ec394cc8bce1c4b48de36971e91b0316029abdf Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Tue, 22 Jun 2021 15:09:21 +0700 Subject: [PATCH 94/99] [#6] Fix nil keyword in error messages --- app/services/google/client_service.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/services/google/client_service.rb b/app/services/google/client_service.rb index e4d36100..1ec00099 100644 --- a/app/services/google/client_service.rb +++ b/app/services/google/client_service.rb @@ -8,15 +8,15 @@ class ClientService BASE_SEARCH_URL = 'https://google.com/search' def initialize(keyword:, lang: 'en') - escaped_keyword = CGI.escape(keyword) - @uri = URI("#{BASE_SEARCH_URL}?q=#{escaped_keyword}&hl=#{lang}&gl=#{lang}") + @escaped_keyword = CGI.escape(keyword) + @uri = URI("#{BASE_SEARCH_URL}?q=#{@escaped_keyword}&hl=#{lang}&gl=#{lang}") end def call begin result = HTTParty.get(@uri, { headers: { 'User-Agent' => USER_AGENT } }) rescue HTTParty::Error, Timeout::Error, SocketError => e - Rails.logger.error "Error: Query Google with keyword #{@keyword} throw an error: #{e}".colorize(:red) + Rails.logger.error "Error: Query Google with keyword #{@escaped_keyword} throw an error: #{e}".colorize(:red) result = nil end @@ -32,7 +32,7 @@ def call def validate_result(result) return result if result.response.code == '200' - Rails.logger.warn "Warning: Query Google with keyword #{@keyword} return status code #{@result.response.code}" + Rails.logger.warn "Warning: Query Google with keyword #{@escaped_keyword} return status code #{result.response.code}" .colorize(:yellow) nil From 20e6c32dcdc12bcef028a06350ea88f1a866ccab Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Tue, 22 Jun 2021 15:11:37 +0700 Subject: [PATCH 95/99] [#6] Fix rubocop warnings --- app/services/google/client_service.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/services/google/client_service.rb b/app/services/google/client_service.rb index 1ec00099..6d80722f 100644 --- a/app/services/google/client_service.rb +++ b/app/services/google/client_service.rb @@ -16,7 +16,8 @@ def call begin result = HTTParty.get(@uri, { headers: { 'User-Agent' => USER_AGENT } }) rescue HTTParty::Error, Timeout::Error, SocketError => e - Rails.logger.error "Error: Query Google with keyword #{@escaped_keyword} throw an error: #{e}".colorize(:red) + Rails.logger.error "Error: Query Google with keyword #{@escaped_keyword} throw an error: #{e}" + .colorize(:red) result = nil end @@ -32,7 +33,8 @@ def call def validate_result(result) return result if result.response.code == '200' - Rails.logger.warn "Warning: Query Google with keyword #{@escaped_keyword} return status code #{result.response.code}" + Rails.logger.warn "Warning: Query Google with keyword #{@escaped_keyword} "\ + " return status code #{result.response.code}" .colorize(:yellow) nil From 78bb1258f324bb86c6859b23f03b96a22d627b97 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Tue, 22 Jun 2021 16:36:39 +0700 Subject: [PATCH 96/99] [#6] Improve tests to cover result validation --- app/services/google/client_service.rb | 2 +- spec/fixtures/vcr/google_warn.yml | 63 +++++++++++++++++++++ spec/services/google/client_service_spec.rb | 17 ++++++ 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 spec/fixtures/vcr/google_warn.yml diff --git a/app/services/google/client_service.rb b/app/services/google/client_service.rb index 6d80722f..d3887f9f 100644 --- a/app/services/google/client_service.rb +++ b/app/services/google/client_service.rb @@ -31,7 +31,7 @@ def call # Any non 200 response code will be logged # response is set to nil in order to notify the error def validate_result(result) - return result if result.response.code == '200' + return result if result&.response&.code == '200' Rails.logger.warn "Warning: Query Google with keyword #{@escaped_keyword} "\ " return status code #{result.response.code}" diff --git a/spec/fixtures/vcr/google_warn.yml b/spec/fixtures/vcr/google_warn.yml new file mode 100644 index 00000000..5804d9ed --- /dev/null +++ b/spec/fixtures/vcr/google_warn.yml @@ -0,0 +1,63 @@ +--- +http_interactions: + - request: + method: get + uri: https://www.google.com/search?gl=en&hl=en&q=vpn + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, + like Gecko) Chrome/91.0.4472.77 Safari/537.36 + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 429 + message: Too Many Requests + headers: + Content-Type: + - text/html; charset=UTF-8 + Date: + - Mon, 14 Jun 2021 08:39:12 GMT + Expires: + - "-1" + Cache-Control: + - private, max-age=0 + Strict-Transport-Security: + - max-age=31536000 + Bfcache-Opt-In: + - unload + P3p: + - CP="This is not a P3P policy! See g.co/p3phelp for more info." + Server: + - gws + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + Set-Cookie: + - 1P_JAR=2021-06-14-08; expires=Wed, 14-Jul-2021 08:39:12 GMT; path=/; domain=.google.com; + Secure; SameSite=none + - CGIC=IgMqLyo; expires=Sat, 11-Dec-2021 08:39:12 GMT; path=/complete/search; + domain=.google.com; HttpOnly + - CGIC=IgMqLyo; expires=Sat, 11-Dec-2021 08:39:12 GMT; path=/search; domain=.google.com; + HttpOnly + - NID=216=RIaFqvX4KKi9ZQ9qGAicOJwbAOtokQNW9gIxE67VedOJHU0vWABUDx3P_0KdnOfQgkFyh1X3aSZ_on3Q4G3HwNCevH3-dM-VdV-Kkz0jh4xpGZV0K8n1dm2BVDm341KMPj_luc32sxztW9pdoTU3YnXYADzv212zuQPwAfhoSFI; + expires=Tue, 14-Dec-2021 08:39:12 GMT; path=/; domain=.google.com; Secure; + HttpOnly; SameSite=none + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; + ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; + ma=2592000; v="46,43" + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: !binary |- + + + recorded_at: Mon, 14 Jun 2021 08:39:13 GMT +recorded_with: VCR 6.0.0 diff --git a/spec/services/google/client_service_spec.rb b/spec/services/google/client_service_spec.rb index 095a624f..68741547 100644 --- a/spec/services/google/client_service_spec.rb +++ b/spec/services/google/client_service_spec.rb @@ -16,4 +16,21 @@ expect(path.to_s).to start_with('https://www.google.com/search') end end + + context 'when google returns an HTTP error' do + it 'returns a nil result', vcr: 'google_warn' do + result = described_class.new(keyword: FFaker::Lorem.word).call + + expect(result).to be_nil + end + + it 'log a warning with the escaped keyword', vcr: 'google_warn' do + allow(Rails.logger).to receive(:warn) + + word = FFaker::Lorem.word + described_class.new(keyword: word).call + + expect(Rails.logger).to have_received(:warn).with(/#{CGI.escape(word)}/) + end + end end From 717b0898776ad1b3dc1b8621932317122f2e249a Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Tue, 22 Jun 2021 16:37:15 +0700 Subject: [PATCH 97/99] [#6] Improve tests to cover result validation - spelling fix --- spec/services/google/client_service_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/services/google/client_service_spec.rb b/spec/services/google/client_service_spec.rb index 68741547..844dc887 100644 --- a/spec/services/google/client_service_spec.rb +++ b/spec/services/google/client_service_spec.rb @@ -24,7 +24,7 @@ expect(result).to be_nil end - it 'log a warning with the escaped keyword', vcr: 'google_warn' do + it 'logs a warning with the escaped keyword', vcr: 'google_warn' do allow(Rails.logger).to receive(:warn) word = FFaker::Lorem.word From 7b608829d0add0192b7d38b3c36df6cc76428507 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Tue, 22 Jun 2021 16:46:53 +0700 Subject: [PATCH 98/99] [#6] Improve call method for worker retrial ease --- app/services/google/client_service.rb | 20 +++++++++----------- spec/services/google/client_service_spec.rb | 2 +- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/app/services/google/client_service.rb b/app/services/google/client_service.rb index d3887f9f..167e130c 100644 --- a/app/services/google/client_service.rb +++ b/app/services/google/client_service.rb @@ -13,16 +13,15 @@ def initialize(keyword:, lang: 'en') end def call - begin - result = HTTParty.get(@uri, { headers: { 'User-Agent' => USER_AGENT } }) - rescue HTTParty::Error, Timeout::Error, SocketError => e - Rails.logger.error "Error: Query Google with keyword #{@escaped_keyword} throw an error: #{e}" - .colorize(:red) + result = HTTParty.get(@uri, { headers: { 'User-Agent' => USER_AGENT } }) - result = nil - end + return false unless validate_result result - validate_result result + result + rescue HTTParty::Error, Timeout::Error, SocketError => e + Rails.logger.error "Error: Query Google with '#{@escaped_keyword}' thrown an error: #{e}".colorize(:red) + + false end private @@ -33,11 +32,10 @@ def call def validate_result(result) return result if result&.response&.code == '200' - Rails.logger.warn "Warning: Query Google with keyword #{@escaped_keyword} "\ - " return status code #{result.response.code}" + Rails.logger.warn "Warning: Query Google with '#{@escaped_keyword}' return status code #{result.response.code}" .colorize(:yellow) - nil + false end end end diff --git a/spec/services/google/client_service_spec.rb b/spec/services/google/client_service_spec.rb index 844dc887..d5909f0c 100644 --- a/spec/services/google/client_service_spec.rb +++ b/spec/services/google/client_service_spec.rb @@ -21,7 +21,7 @@ it 'returns a nil result', vcr: 'google_warn' do result = described_class.new(keyword: FFaker::Lorem.word).call - expect(result).to be_nil + expect(result).to be_falsey end it 'logs a warning with the escaped keyword', vcr: 'google_warn' do From e59f52037f6261d41f2d32e3583e135f91de5708 Mon Sep 17 00:00:00 2001 From: Xavier MALPARTY Date: Tue, 22 Jun 2021 16:48:26 +0700 Subject: [PATCH 99/99] [#6] Rename validate_result in valid_result? --- app/services/google/client_service.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/services/google/client_service.rb b/app/services/google/client_service.rb index 167e130c..fe807574 100644 --- a/app/services/google/client_service.rb +++ b/app/services/google/client_service.rb @@ -15,7 +15,7 @@ def initialize(keyword:, lang: 'en') def call result = HTTParty.get(@uri, { headers: { 'User-Agent' => USER_AGENT } }) - return false unless validate_result result + return false unless valid_result? result result rescue HTTParty::Error, Timeout::Error, SocketError => e @@ -29,7 +29,7 @@ def call # Inspect Http response status code # Any non 200 response code will be logged # response is set to nil in order to notify the error - def validate_result(result) + def valid_result?(result) return result if result&.response&.code == '200' Rails.logger.warn "Warning: Query Google with '#{@escaped_keyword}' return status code #{result.response.code}"