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(
|
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.
|
||||||
|
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 }
|
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>
|
||||||
|
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
|
# 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
|
||||||
|
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