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( devise(
:invitable, :database_authenticatable, :registerable, :invitable, :database_authenticatable, :registerable,
:rememberable, :trackable, :validatable :rememberable, :trackable, :validatable, :recoverable
) )
# alias the Devise Invitable method to check if a user is pending creation. # 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 } html: { method: :post }
) do |f| %> ) do |f| %>
<%= f.hidden_field(:email, value: f.object.email) %> <%= 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 %> type: "submit") do %>
<i data-feather="mail"></i> <i data-feather="mail"></i>
<% end %> <% end %>
<% end %> <% end %>
<% end %> <% end %>
</td> </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> </tr>

@ -26,6 +26,7 @@
<th><%= t('.created_on') %></th> <th><%= t('.created_on') %></th>
<th><%= t('.status') %></th> <th><%= t('.status') %></th>
<th></th> <th></th>
<th></th>
</tr> </tr>
</thead> </thead>
<tbody> <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 # The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array. # ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test config.action_mailer.delivery_method = :test
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
# Print deprecation notices to the stderr. # Print deprecation notices to the stderr.
config.active_support.deprecation = :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." 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: "Your password has been changed successfully. You are now signed in."
updated_not_active: "Your password has been changed successfully." updated_not_active: "Your password has been changed successfully."
edit:
action: Reset password
header: Reset your password
registrations: registrations:
destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon."
signed_up: "Welcome! You have signed up successfully." signed_up: "Welcome! You have signed up successfully."

@ -133,6 +133,18 @@ en:
invite: Invite team member invite: Invite team member
status: Status status: Status
title: Here's your team 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: invitations:
instructions: instructions:
index: index:
@ -147,3 +159,6 @@ en:
disable_registration: > disable_registration: >
If you already have an account, please sign in. Otherwise, you If you already have an account, please sign in. Otherwise, you
must be invited to join Storm. 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: { devise_for :users, controllers: {
sessions: 'users/sessions', sessions: 'users/sessions',
registrations: 'users/registrations', registrations: 'users/registrations',
invitations: 'users/invitations' invitations: 'users/invitations',
passwords: 'users/passwords'
}, path_names: { }, path_names: {
sign_in: 'sign-in', sign_in: 'sign-in',
sign_out: 'sign-out', sign_out: 'sign-out',
@ -31,5 +32,9 @@ Rails.application.routes.draw do
namespace(:invitations) do namespace(:invitations) do
resources(:instructions, only: %w(index)) resources(:instructions, only: %w(index))
end end
namespace(:passwords) do
resources(:instructions, only: %w(index))
end
end 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