Add functionality to reset passwords (#23)

Add easy "lock" icon to regenerate the users reset password URL. This is
important because the app does not send emails. If the user wants to
re-set their password, it has to be done in app.
This commit is contained in:
Justin Licata 2017-10-27 09:25:06 -05:00 committed by GitHub
parent 2b6dc3c296
commit 50e98da43d
12 changed files with 241 additions and 3 deletions

@ -0,0 +1,10 @@
module Users
module Passwords
# InstructionsController
class InstructionsController < ApplicationController
def index
@user = User.find(params[:id])
end
end
end
end

@ -0,0 +1,44 @@
module Users
# PasswordsController
class PasswordsController < Devise::PasswordsController
skip_before_action(:require_no_authentication, only: %w(create))
before_action(:authenticate_user!, only: %w(create))
before_action(:set_user, only: %w(create))
layout('unauthenticated', only: %w(edit update))
def create
if @token = @user.send_reset_password_instructions
redirect_to(after_sending_reset_password_instructions_path_for(@user))
else
flash[:danger] = t('.create.fail')
redirect_back(fallback_location: users_path)
end
end
def edit
super
end
private
def authenticate_user!
warden.authenticate!
end
def set_user
@user = User.find_by!(email: params[:user][:email])
end
def after_sending_reset_password_instructions_path_for(user)
users_passwords_instructions_path(
id: user.id,
token: @token
)
end
def after_resetting_password_path_for(_resource)
root_path
end
end
end

@ -3,7 +3,7 @@ class User < ApplicationRecord
devise(
:invitable, :database_authenticatable, :registerable,
:rememberable, :trackable, :validatable
:rememberable, :trackable, :validatable, :recoverable
)
# alias the Devise Invitable method to check if a user is pending creation.

@ -0,0 +1,45 @@
<div class="container">
<div class="auth">
<%= link_to(root_path) do %>
<%= image_tag("bolt.png", class: "auth-logo mx-auto d-block mb-3") %>
<% end %>
<h3 class="auth-title text-center mb-3">
<%= t('.header') %>
</h3>
<div class="card">
<div class="card-block">
<%= form_for(resource, as: resource_name,
url: password_path(resource_name), html: { method: :put }) do |f| %>
<%= render("shared/flash") %>
<%= f.hidden_field :reset_password_token %>
<div class="form-group">
<%= f.label :password %>
<%= f.password_field :password,
autofocus: true,
autocomplete: "off",
class: "form-control" %>
<%= error_message_on(f.object, :password) %>
</div>
<div class="form-group">
<%= f.label :password_confirmation %>
<%= f.password_field :password_confirmation,
autocomplete: "off",
class: "form-control" %>
<%= error_message_on(f.object, :password_confirmation) %>
</div>
<div class="actions">
<%= f.submit t('.action'), class: "btn btn-primary btn-block" %>
</div>
<% end %>
</div>
</div>
<%= render "devise/shared/links" %>
</div>
</div>

@ -18,11 +18,27 @@
html: { method: :post }
) do |f| %>
<%= f.hidden_field(:email, value: f.object.email) %>
<%= button_tag(class: "btn btn-link btn-sm icon-15", title: "Invitation Link",
<%= button_tag(class: "btn btn-link btn-sm icon-15",
title: t('.invitation_link'),
type: "submit") do %>
<i data-feather="mail"></i>
<% end %>
<% end %>
<% end %>
</td>
<td>
<%= form_for(
user,
as: :user,
url: password_path(:user),
html: { method: :post }) do |f| %>
<%= f.hidden_field(:email, value: f.object.email) %>
<%= button_tag(class: "btn btn-link btn-sm icon-15",
title: t('.reset_password'),
type: "submit") do %>
<i data-feather="lock"></i>
<% end %>
<% end %>
</td>
</tr>

@ -26,6 +26,7 @@
<th><%= t('.created_on') %></th>
<th><%= t('.status') %></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>

@ -0,0 +1,45 @@
<div class="page-heading">
<h4 class="page-title"><%= t('.title') %></h4>
</div>
<div class="row">
<div class="col-sm-4 offset-sm-4">
<%= link_to(users_path,
class: "card card-link") do %>
<div class="card-block text-muted text-center d-flex align-items-center justify-content-center">
<i class="mr-3" data-feather="arrow-left"></i>
<h6 class="mb-0"><%= t('.back') %></h6>
</div>
<% end %>
</div>
</div>
<div class="row">
<div class="col-md-6 offset-md-3">
<div class="card">
<div class="card-block">
<p><%= sanitize(t('.instructions', email: @user.email)) %></p>
<div class="input-group">
<input type="text"
class="form-control"
value="<%= edit_password_url(
@user,
reset_password_token: params[:token]
) %>" />
<span class="input-group-btn">
<button class="btn btn-primary" type="button"
data-clipboard-text="<%= edit_password_url(
@user,
reset_password_token: params[:token]
) %>"
data-behavior="copy-to-clipboard">
<%= t('.action') %>
</button>
</span>
</div>
</div>
</div>
</div>
</div>

@ -33,6 +33,7 @@ Rails.application.configure do
# The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr

@ -36,6 +36,9 @@ en:
send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes."
updated: "Your password has been changed successfully. You are now signed in."
updated_not_active: "Your password has been changed successfully."
edit:
action: Reset password
header: Reset your password
registrations:
destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon."
signed_up: "Welcome! You have signed up successfully."

@ -133,6 +133,18 @@ en:
invite: Invite team member
status: Status
title: Here's your team
passwords:
create:
fail: You have unsuccessfully generated the Reset Password URL.
instructions:
index:
action: Copy link to clipboard
title: Instructions
back: Back to users
instructions: >
You have successfully generated the Reset Password URL for
<code>%{email}</code>. Since we do not deliver emails,
here is a fancy link for them.
invitations:
instructions:
index:
@ -147,3 +159,6 @@ en:
disable_registration: >
If you already have an account, please sign in. Otherwise, you
must be invited to join Storm.
user:
invitation_link: Invitation Link
reset_password: Reset password

@ -10,7 +10,8 @@ Rails.application.routes.draw do
devise_for :users, controllers: {
sessions: 'users/sessions',
registrations: 'users/registrations',
invitations: 'users/invitations'
invitations: 'users/invitations',
passwords: 'users/passwords'
}, path_names: {
sign_in: 'sign-in',
sign_out: 'sign-out',
@ -31,5 +32,9 @@ Rails.application.routes.draw do
namespace(:invitations) do
resources(:instructions, only: %w(index))
end
namespace(:passwords) do
resources(:instructions, only: %w(index))
end
end
end

@ -0,0 +1,53 @@
require('test_helper')
module Users
class PasswordsControllerTest < ActionDispatch::IntegrationTest
setup do
@user = create(:user)
@params = params
end
feature 'as an authenticated user' do
before(:each) do
sign_in(@user)
end
test 'should create password reset token successfully' do
token = @user.reset_password_token
post('/users/password', params: params)
assert_not_equal(token, @user.reload.reset_password_token)
end
test 'should raise RecordNotFound' do
assert_raise do
post('/users/password', params: bad_params)
end
end
end
feature 'as an unauthenticated user' do
test 'should not create password reset token' do
post('/users/password', params: params)
assert_nil(@user.reload.reset_password_token)
end
end
private
def params
{
user: {
email: @user.email
}
}
end
def bad_params
{
user: {
email: 'bad'
}
}
end
end
end