21
1
Fork 0
mirror of https://github.com/Evolix/chexpire.git synced 2024-04-29 23:40:49 +02:00

User with devise, basic integration in navbar with simple form & tests

This commit is contained in:
Colin Darie 2018-05-23 17:59:28 +02:00
parent b9b8024233
commit 138b554772
No known key found for this signature in database
GPG key ID: 4FB865FDBCA4BCC4
32 changed files with 530 additions and 42 deletions

View file

@ -24,3 +24,4 @@ install:
script:
- bundle exec rubocop
- bundle exec rails test
- bundle exec rails test:system

View file

@ -69,6 +69,7 @@ group :test do
gem 'selenium-webdriver'
# Easy installation and use of chromedriver to run system tests with Chrome
gem 'chromedriver-helper'
gem 'launchy'
end
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem

View file

@ -0,0 +1,12 @@
module UsersHelper
# Inject a devise template inside a same container
# while translation form keys are still valid
# (original partial scope is preserved)
def devise_form_container
content_for(:devise_form_content) do
yield
end
render "shared/devise_form_container"
end
end

View file

@ -1 +1,2 @@
@import '~bootstrap/scss/bootstrap';
@import 'components/users';

View file

@ -0,0 +1,5 @@
.new_user {
.form-check-label.boolean {
color: inherit;
}
}

35
app/models/user.rb Normal file
View file

@ -0,0 +1,35 @@
# == Schema Information
#
# Table name: users
#
# id :bigint(8) not null, primary key
# confirmation_sent_at :datetime
# confirmation_token :string(255)
# confirmed_at :datetime
# current_sign_in_at :datetime
# current_sign_in_ip :string(255)
# email :string(255) default(""), not null
# encrypted_password :string(255) default(""), not null
# last_sign_in_at :datetime
# last_sign_in_ip :string(255)
# remember_created_at :datetime
# reset_password_sent_at :datetime
# reset_password_token :string(255)
# sign_in_count :integer default(0), not null
# unconfirmed_email :string(255)
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_users_on_confirmation_token (confirmation_token) UNIQUE
# index_users_on_email (email) UNIQUE
# index_users_on_reset_password_token (reset_password_token) UNIQUE
#
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable, :confirmable
end

View file

@ -0,0 +1,12 @@
<%= devise_form_container do %>
<h2><%= t('.resend_confirmation_instructions') %></h2>
<%= simple_form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %>
<%= f.input :email, autofocus: true, autocomplete: "email",
label: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %>
<%= f.button :submit, t('.resend_confirmation_instructions'), class: "btn-primary" %>
<% end %>
<%= render "devise/shared/links" %>
<% end %>

View file

@ -0,0 +1,4 @@
<p><%= t('.greeting', recipient: @email) %></p>
<p><%= t('.instruction') %></p>
<p><%= link_to t('.action'), confirmation_url(@resource, confirmation_token: @token) %></p>

View file

@ -0,0 +1,7 @@
<p><%= t('.greeting', recipient: @email) %></p>
<% if @resource.try(:unconfirmed_email?) %>
<p><%= t('.message', email: @resource.unconfirmed_email) %></p>
<% else %>
<p><%= t('.message', email: @resource.email) %></p>
<% end %>

View file

@ -0,0 +1,3 @@
<p><%= t('.greeting', recipient: @resource.email) %></p>
<p><%= t('.message') %></p>

View file

@ -0,0 +1,8 @@
<p><%= t('.greeting', recipient: @resource.email) %></p>
<p><%= t('.instruction') %></p>
<p><%= link_to t('.action'), edit_password_url(@resource, reset_password_token: @token) %></p>
<p><%= t('.instruction_2') %></p>
<p><%= t('.instruction_3') %></p>

View file

@ -0,0 +1,7 @@
<p><%= t('.greeting', recipient: @resource.email) %></p>
<p><%= t('.message') %></p>
<p><%= t('.instruction') %></p>
<p><%= link_to t('.action'), unlock_url(@resource, unlock_token: @token) %></p>

View file

@ -0,0 +1,16 @@
<%= devise_form_container do %>
<h2><%= t('.change_your_password') %></h2>
<%= simple_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %>
<%= f.hidden_field :reset_password_token %>
<%= f.input :password, label: t('.new_password'), autocomplete: "off",
hint: t('devise.shared.minimum_password_length', count: @minimum_password_length) %>
<%= f.input :password_confirmation, label: t('.confirm_new_password'), autocomplete: "off" %>
<%= f.button :submit, t('.change_my_password'), class: "btn-primary" %>
<% end %>
<%= render "devise/shared/links" %>
<% end %>

View file

@ -0,0 +1,11 @@
<%= devise_form_container do %>
<h2><%= t('.forgot_your_password') %></h2>
<%= simple_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %>
<%= f.input :email, autofocus: true, autocomplete: "email" %>
<%= f.button :submit, t('.send_me_reset_password_instructions'), class: "btn-primary" %>
<% end %>
<%= render "devise/shared/links" %>
<% end %>

View file

@ -0,0 +1,40 @@
<%= devise_form_container do %>
<h2><%= t('.title', resource: resource_name.to_s.humanize) %></h2>
<%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
<%= f.input :email, autofocus: true, autocomplete: "email" %>
<% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
<div class="alert alert-info">
<%= t('.currently_waiting_confirmation_for_email', email: resource.unconfirmed_email) %>
</div>
<% end %>
<%= f.input :current_password,
autocomplete: "off",
hint: t('.we_need_your_current_password_to_confirm_your_changes') %>
<div class="alert border">
<h3>Want to change your password ?</h3>
<%= f.input :password,
autocomplete: "off",
hint: (t('devise.shared.minimum_password_length', count: @minimum_password_length)
+ " " + t('.leave_blank_if_you_don_t_want_to_change_it'))%>
<%= f.input :password_confirmation, autocomplete: "off" %>
</div>
<%= f.button :submit, t('.update'), class: "btn-primary" %>
<% end %>
<h3 class="mt-5"><%= t('.cancel_my_account') %></h3>
<p><%= t('.unhappy') %> <%=
button_to t('.cancel_my_account'), registration_path(resource_name),
class: "btn btn-danger",
data: { confirm: t('.are_you_sure') }, method: :delete %></p>
<%= link_to t('devise.shared.links.back'), :back %>
<% end %>

View file

@ -0,0 +1,16 @@
<%= devise_form_container do %>
<h2><%= t('.sign_up') %></h2>
<%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<%= f.input :email, autofocus: true, autocomplete: "email" %>
<%= f.input :password, autocomplete: "off",
hint: t('devise.shared.minimum_password_length', count: @minimum_password_length) %>
<%= f.input :password_confirmation, autocomplete: "off" %>
<%= f.button :submit, t('.sign_up'), class: "btn-primary" %>
<% end %>
<%= render "devise/shared/links" %>
<% end %>

View file

@ -0,0 +1,16 @@
<%= devise_form_container do %>
<h2><%= t('.sign_in') %></h2>
<%= simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
<%= f.input :email, autofocus: true, autocomplete: "email" %>
<%= f.input :password, autocomplete: "off" %>
<% if devise_mapping.rememberable? -%>
<%= f.input :remember_me, as: :boolean %>
<% end -%>
<%= f.button :submit, t('.sign_in'), class: "btn-primary" %>
<% end %>
<%= render "devise/shared/links" %>
<% end %>

View file

@ -0,0 +1,25 @@
<%- if controller_name != 'sessions' %>
<%= link_to t(".sign_in"), new_session_path(resource_name) %><br />
<% end -%>
<%- if devise_mapping.registerable? && controller_name != 'registrations' %>
<%= link_to t(".sign_up"), new_registration_path(resource_name) %><br />
<% end -%>
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
<%= link_to t(".forgot_your_password"), new_password_path(resource_name) %><br />
<% end -%>
<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
<%= link_to t('.didn_t_receive_confirmation_instructions'), new_confirmation_path(resource_name) %><br />
<% end -%>
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
<%= link_to t('.didn_t_receive_unlock_instructions'), new_unlock_path(resource_name) %><br />
<% end -%>
<%- if devise_mapping.omniauthable? %>
<%- resource_class.omniauth_providers.each do |provider| %>
<%= link_to t('.sign_in_with_provider', provider: OmniAuth::Utils.camelize(provider)), omniauth_authorize_path(resource_name, provider) %><br />
<% end -%>
<% end -%>

View file

@ -0,0 +1,11 @@
<%= devise_form_container do %>
<h2><%= t('.resend_unlock_instructions') %></h2>
<%= simple_form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %>
<%= f.input :email, autofocus: true, autocomplete: "email" %>
<%= f.button :submit, t('.resend_unlock_instructions'), class: "btn-primary" %>
<% end %>
<%= render "devise/shared/links" %>
<% end %>

View file

@ -10,7 +10,9 @@
</head>
<body>
<%= render "shared/navbar" %>
<%= render "shared/notices" %>
<%= yield %>
</body>
</html>

View file

@ -0,0 +1,7 @@
<div class="container">
<div class="row justify-content-center">
<div class="col-12 col-md-6">
<%= yield :devise_form_content %>
</div>
</div>
</div>

View file

@ -0,0 +1,31 @@
<nav class="navbar navbar-expand-lg navbar-light bg-light justify-content-between">
<%= link_to "Chexpire", root_path, class: "navbar-brand" %>
<div class="my-2 my-lg-0">
<% if user_signed_in? %>
<div class="navbar-item">
<div class="dropdown">
<a class="nav-link dropdown-toggle" href="#"
id="navbarDropdown" role="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<%= current_user.email %>
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<%= link_to edit_user_registration_path, class: "dropdown-item" do %>
<i class="fa fa-user"></i> <%= t(".profile", default: "Profile") %>
<% end %>
<%= link_to destroy_user_session_path, method: :delete, class: "dropdown-item" do %>
<i class="fa fa-sign-out"></i> <%= t(".sign_out", default: "Log out") %>
<% end %>
</div>
</div>
</div>
<% else %>
<!-- Login link (when logged out) -->
<%= link_to t(".sign_in"), new_user_session_path, class: "navbar-item navbar-link" %>
<%= link_to t(".sign_up"), new_user_registration_path, class: "navbar-item navbar-link" %>
<% end %>
</div>
</nav>

View file

@ -136,7 +136,7 @@ Devise.setup do |config|
# their account can't be confirmed with the token any more.
# Default is nil, meaning there is no restriction on how long a user can take
# before confirming their account.
config.confirm_within = 3.days
config.confirm_within = 7.days
# If true, requires any email changes to be confirmed (exactly the same way as
# initial account confirmation) to be applied. Requires additional unconfirmed_email

View file

@ -1,33 +1,7 @@
# Files in the config/locales directory are used for internationalization
# and are automatically loaded by Rails. If you want to use locales other
# than English, add the necessary files in this directory.
#
# To use the locales, use `I18n.t`:
#
# I18n.t 'hello'
#
# In views, this is aliased to just `t`:
#
# <%= t('hello') %>
#
# To use a different locale, set it with `I18n.locale`:
#
# I18n.locale = :es
#
# This would use the information in config/locales/es.yml.
#
# The following keys must be escaped otherwise they will not be retrieved by
# the default I18n backend:
#
# true, false, on, off, yes, no
#
# Instead, surround them with single quotes.
#
# en:
# 'true': 'foo'
#
# To learn more, please read the Rails Internationalization guide
# available at http://guides.rubyonrails.org/i18n.html.
en:
hello: "Hello world"
shared:
navbar:
sign_up: "Sign up"
sign_in: "Sign in"
sign_out: "Sign out"
profile: "Profile"

View file

@ -1,16 +1,45 @@
# == Route Map
#
# Prefix Verb URI Pattern Controller#Action
# root GET / pages#home
# rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show
# rails_blob_representation GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show
# rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show
# update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update
# rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create
# Prefix Verb URI Pattern Controller#Action
# new_user_session GET /users/sign_in(.:format) devise/sessions#new
# user_session POST /users/sign_in(.:format) devise/sessions#create
# destroy_user_session DELETE /users/sign_out(.:format) devise/sessions#destroy
# new_user_password GET /users/password/new(.:format) devise/passwords#new
# edit_user_password GET /users/password/edit(.:format) devise/passwords#edit
# user_password PATCH /users/password(.:format) devise/passwords#update
# PUT /users/password(.:format) devise/passwords#update
# POST /users/password(.:format) devise/passwords#create
# cancel_user_registration GET /users/cancel(.:format) devise/registrations#cancel
# new_user_registration GET /users/sign_up(.:format) devise/registrations#new
# edit_user_registration GET /users/edit(.:format) devise/registrations#edit
# user_registration PATCH /users(.:format) devise/registrations#update
# PUT /users(.:format) devise/registrations#update
# DELETE /users(.:format) devise/registrations#destroy
# POST /users(.:format) devise/registrations#create
# new_user_confirmation GET /users/confirmation/new(.:format) devise/confirmations#new
# user_confirmation GET /users/confirmation(.:format) devise/confirmations#show
# POST /users/confirmation(.:format) devise/confirmations#create
# root GET / pages#home
# letter_opener_web /letter_opener LetterOpenerWeb::Engine
# rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show
# rails_blob_representation GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show
# rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show
# update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update
# rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create
#
# Routes for LetterOpenerWeb::Engine:
# clear_letters DELETE /clear(.:format) letter_opener_web/letters#clear
# delete_letter DELETE /:id(.:format) letter_opener_web/letters#destroy
# letters GET / letter_opener_web/letters#index
# letter GET /:id(/:style)(.:format) letter_opener_web/letters#show
# GET /:id/attachments/:file(.:format) letter_opener_web/letters#attachment
# In order to update the route map above,
# run `bundle exec annotate -r` after modifying this file
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
devise_for :users
root to: "pages#home"
mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development?
end

View file

@ -0,0 +1,44 @@
# frozen_string_literal: true
class DeviseCreateUsers < ActiveRecord::Migration[5.2]
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

View file

@ -10,6 +10,28 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 0) do
ActiveRecord::Schema.define(version: 2018_05_23_145630) do
create_table "users", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", 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.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"
t.string "confirmation_token"
t.datetime "confirmed_at"
t.datetime "confirmation_sent_at"
t.string "unconfirmed_email"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
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

View file

@ -1,5 +1,5 @@
require "test_helper"
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
driven_by :headless_chrome
end

39
test/fixtures/users.yml vendored Normal file
View file

@ -0,0 +1,39 @@
# == Schema Information
#
# Table name: users
#
# id :bigint(8) not null, primary key
# confirmation_sent_at :datetime
# confirmation_token :string(255)
# confirmed_at :datetime
# current_sign_in_at :datetime
# current_sign_in_ip :string(255)
# email :string(255) default(""), not null
# encrypted_password :string(255) default(""), not null
# last_sign_in_at :datetime
# last_sign_in_ip :string(255)
# remember_created_at :datetime
# reset_password_sent_at :datetime
# reset_password_token :string(255)
# sign_in_count :integer default(0), not null
# unconfirmed_email :string(255)
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_users_on_confirmation_token (confirmation_token) UNIQUE
# index_users_on_email (email) UNIQUE
# index_users_on_reset_password_token (reset_password_token) UNIQUE
#
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
# This model initially had no columns defined. If you add columns to the
# model remove the '{}' from the fixture names and add the columns immediately
# below each fixture, per the syntax in the comments below
#
user1:
email: user@chexpire.org
encrypted_password: <%= User.new.send(:password_digest, 'password') %>
confirmed_at: <%= 1.minute.ago %>

36
test/models/user_test.rb Normal file
View file

@ -0,0 +1,36 @@
# == Schema Information
#
# Table name: users
#
# id :bigint(8) not null, primary key
# confirmation_sent_at :datetime
# confirmation_token :string(255)
# confirmed_at :datetime
# current_sign_in_at :datetime
# current_sign_in_ip :string(255)
# email :string(255) default(""), not null
# encrypted_password :string(255) default(""), not null
# last_sign_in_at :datetime
# last_sign_in_ip :string(255)
# remember_created_at :datetime
# reset_password_sent_at :datetime
# reset_password_token :string(255)
# sign_in_count :integer default(0), not null
# unconfirmed_email :string(255)
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_users_on_confirmation_token (confirmation_token) UNIQUE
# index_users_on_email (email) UNIQUE
# index_users_on_reset_password_token (reset_password_token) UNIQUE
#
require "test_helper"
class UserTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

61
test/system/users_test.rb Normal file
View file

@ -0,0 +1,61 @@
require "application_system_test_case"
class UsersTest < ApplicationSystemTestCase
test "an user can signup from the homepage and confirm its account" do
visit root_path
click_on I18n.t("shared.navbar.sign_up")
email = "new@chexpire.org"
password = "password"
fill_in("user[email]", with: email)
fill_in("user[password]", with: password)
fill_in("user[password_confirmation]", with: password)
click_button I18n.t("devise.registrations.new.sign_up")
assert_equal root_path, page.current_path
user = User.find_by!(email: email, confirmed_at: nil)
assert_not_nil user
confirmation_path = user_confirmation_path(confirmation_token: user.confirmation_token)
confirmation_email = ActionMailer::Base.deliveries.last
assert confirmation_email.body.include?(confirmation_path)
visit confirmation_path
assert_equal new_user_session_path, page.current_path
assert page.has_css?(".alert-success")
end
test "an user can signin from the homepage" do
user = users(:user1)
visit root_path
click_on I18n.t("shared.navbar.sign_in")
fill_in "user[email]", with: user.email
fill_in "user[password]", with: "password"
click_button I18n.t("devise.sessions.new.sign_in")
assert_equal root_path, page.current_path
assert page.has_content?(user.email)
end
test "an user can signout from the homepage" do
user = users(:user1)
login_as user
visit root_path
find ".navbar" do
click_on user.email
click_on I18n.t("shared.navbar.sign_out")
end
assert_equal root_path, page.current_path
assert page.has_content?(I18n.t("shared.navbar.sign_in"))
end
end

View file

@ -6,5 +6,17 @@ class ActiveSupport::TestCase
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all
include Warden::Test::Helpers
Warden.test_mode!
# Add more helper methods to be used by all tests here...
end
Capybara.register_driver :headless_chrome do |app|
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
"chromeOptions" => { args: %w[headless disable-gpu] + ["window-size=1280,800"] },
)
Capybara::Selenium::Driver.new app, browser: :chrome, desired_capabilities: capabilities
end
Capybara.save_path = Rails.root.join("tmp/capybara")
Capybara.javascript_driver = :headless_chrome