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:
parent
2b6dc3c296
commit
50e98da43d
10
app/controllers/users/passwords/instructions_controller.rb
Normal file
10
app/controllers/users/passwords/instructions_controller.rb
Normal file
@ -0,0 +1,10 @@
|
||||
module Users
|
||||
module Passwords
|
||||
# InstructionsController
|
||||
class InstructionsController < ApplicationController
|
||||
def index
|
||||
@user = User.find(params[:id])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
44
app/controllers/users/passwords_controller.rb
Normal file
44
app/controllers/users/passwords_controller.rb
Normal file
@ -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.
|
||||
|
45
app/views/devise/passwords/edit.html.erb
Normal file
45
app/views/devise/passwords/edit.html.erb
Normal file
@ -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>
|
||||
|
45
app/views/users/passwords/instructions/index.html.erb
Normal file
45
app/views/users/passwords/instructions/index.html.erb
Normal file
@ -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
|
||||
|
53
test/controllers/users/passwords_controller_test.rb
Normal file
53
test/controllers/users/passwords_controller_test.rb
Normal file
@ -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
|
Loading…
Reference in New Issue
Block a user