Add password reset functionality

This commit is contained in:
Trevor Vallender 2024-06-03 11:54:38 +01:00
parent f17b5046c4
commit 2f940fab13
17 changed files with 171 additions and 7 deletions

View File

@ -27,6 +27,7 @@ group :development, :test do
end end
group :development do group :development do
gem "letter_opener"
gem "rubocop-rails-omakase" gem "rubocop-rails-omakase"
gem "web-console" gem "web-console"
end end

View File

@ -99,6 +99,7 @@ GEM
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0) regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2) xpath (~> 3.2)
childprocess (5.0.0)
concurrent-ruby (1.2.3) concurrent-ruby (1.2.3)
connection_pool (2.4.1) connection_pool (2.4.1)
crass (1.0.6) crass (1.0.6)
@ -134,6 +135,11 @@ GEM
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
json (2.7.2) json (2.7.2)
language_server-protocol (3.17.0.3) language_server-protocol (3.17.0.3)
launchy (3.0.1)
addressable (~> 2.8)
childprocess (~> 5.0)
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
loofah (2.22.0) loofah (2.22.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
@ -327,6 +333,7 @@ DEPENDENCIES
image_processing (~> 1.2) image_processing (~> 1.2)
importmap-rails importmap-rails
jbuilder jbuilder
letter_opener
mission_control-jobs mission_control-jobs
pg pg
propshaft propshaft

View File

@ -1,9 +1,9 @@
:root { :root {
--background-color: #111; --background-color: #333;
--main-background-color: --main-background-color:
--header-color: #15345b; --header-color: #15345b;
--header-text-color: #fff; --header-text-color: #fff;
--header-height: 150px; --header-height: 200px;
--inset-bg-color: #eee; --inset-bg-color: #eee;

View File

@ -63,7 +63,7 @@ header:before {
} }
header h1 { header h1 {
padding: .5em 0; padding: 1em 0;
} }
header h1 a:link, header h1 a:visited { header h1 a:link, header h1 a:visited {

View File

@ -0,0 +1,42 @@
# frozen_string_literal: true
class PasswordResetsController < ApplicationController
skip_before_action :authenticate
def new
reset_session
end
def create
user = User.find_by(username: params[:username])
if user
token = user.generate_token_for(:password_reset)
UserMailer.with(user: user, token: token).password_reset.deliver_later
redirect_to new_session_path, notice: t(".success") and return
end
redirect_to :root, notice: t(".error")
end
def edit
reset_session
@user = User.find_by(username: params[:id])
@token = params[:token]
unless @user == User.find_by_token_for(:password_reset, params[:token])
redirect_to :root, notice: t(".invalid_token") and return
end
end
def update
user = User.find_by(username: params[:id])
unless user == User.find_by_token_for(:password_reset, params[:token])
redirect_to :root, notice: t(".invalid_token") and return
end
if user.update(password: params[:password], password_confirmation: params[:password_confirmation])
redirect_to new_session_path, notice: t(".success")
else
redirect_to :root, notice: t(".error")
end
end
end

View File

@ -6,12 +6,12 @@ class TableInviteMailer < ApplicationMailer
def invite_user def invite_user
@table_invite = params[:table_invite] @table_invite = params[:table_invite]
mail(to: @table_invite.email, subject: "[Tabletop Companion] Invite to join a table") mail(to: @table_invite.email, subject: t(".invite_user.subject"))
end end
def invite_new_user def invite_new_user
@table_invite = params[:table_invite] @table_invite = params[:table_invite]
mail(to: @table_invite.email, subject: "[Tabletop Companion] Youve been invited to a game!") mail(to: @table_invite.email, subject: t(".invite_new_user.subject"))
end end
end end

View File

@ -7,12 +7,19 @@ class UserMailer < ApplicationMailer
@user = params[:user] @user = params[:user]
@token = params[:token] @token = params[:token]
mail(to: @user.email, subject: "[Tabletop Companion] Verify your email") mail(to: @user.email, subject: t(".email_verification.subject"))
end end
def email_verified def email_verified
@user = params[:user] @user = params[:user]
mail(to: @user.email, subject: "[Tabletop Companion] Your email has been verified") mail(to: @user.email, subject: t(".email_verified.subject"))
end
def password_reset
@user = params[:user]
@token = params[:token]
mail(to: @user.email, subject: t(".password_reset.subject"))
end end
end end

View File

@ -0,0 +1,17 @@
<%= content_for :title, t(".reset_password") %>
<h2><%= t(".reset_password") %></h2>
<section class="inset">
<%= form_with url: password_reset_path(id: @user.username), method: :patch do |f| %>
<%= f.hidden_field :token, value: @token %>
<%= f.label :password %>
<%= f.password_field :password %>
<%= f.label :password_confirmation %>
<%= f.password_field :password_confirmation %>
<%= f.submit t(".reset_password_button") %>
<% end %>
</section>

View File

@ -0,0 +1,15 @@
<%= content_for :title, t(".reset_password") %>
<h2><%= t(".reset_password") %></h2>
<section class="inset">
<p><%= t(".forgotten_password_intro") %></p>
<%= form_with url: password_resets_path do |f| %>
<%= f.label :username %>
<%= f.text_field :username %>
<%= f.submit t(".reset_password_button") %>
<% end %>
</section>

View File

@ -14,3 +14,4 @@
<% end %> <% end %>
</section> </section>
<%= link_to(t(".forgot_password"), new_password_reset_path) %>

View File

@ -0,0 +1,3 @@
<%= htmlify_email(t(".content")) %>
<%= link_to t(".reset_password"), edit_password_reset_url(id: @user, token: @token) %>

View File

@ -0,0 +1,3 @@
<%= t(".content") %>
<%= edit_password_reset_url(id: @user, token: @token) %>

View File

@ -40,6 +40,7 @@ Rails.application.configure do
# Don't care if the mailer can't send. # Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false config.action_mailer.raise_delivery_errors = false
config.action_mailer.delivery_method = :letter_opener
config.action_mailer.perform_caching = false config.action_mailer.perform_caching = false

View File

@ -54,22 +54,42 @@ en:
destroy: destroy:
success: Successfully deleted “%{name}”. success: Successfully deleted “%{name}”.
error: “%{name}” could not be deleted. error: “%{name}” could not be deleted.
password_resets:
new:
reset_password: Reset your password
forgotten_password_intro: |-
If you have forgotten your password, enter your information below to reset it.
reset_password_button: Reset password
create:
success: Please check your email to reset your password.
error: Could not reset your password.
edit:
reset_password: Reset your password
invalid_token: That token seems to have expired, please try resettting your password again.
reset_password_button: Update password
update:
invalid_token: That token seems to have expired, please try resettting your password again.
success: Your password has been reset, you may now log in.
error: Failed to reset password. Please try again or contact us for help.
sessions: sessions:
create: create:
success: "Hello, %{name}!" success: "Hello, %{name}!"
error: "Could not sign in. Please check your username and password." error: "Could not sign in. Please check your username and password."
new: new:
log_in: Log in log_in: Log in
forgot_password: Forgotten your password?
destroy: destroy:
log_out: Log out log_out: Log out
success: "You have signed out." success: "You have signed out."
table_invite_mailer: table_invite_mailer:
invite_new_user: invite_new_user:
subject: Youve been invited to join a game on Tabletop Companion!
content: |- content: |-
Youve been invited to join a game on Tabletop Companion. To start playing, sign up at the Youve been invited to join a game on Tabletop Companion. To start playing, sign up at the
link below and accept your invitation! link below and accept your invitation!
sign_up: Sign up now sign_up: Sign up now
invite_user: invite_user:
subject: Youve been invited to a new game!
content: |- content: |-
You have been invited to join the table “%{table_name}” on Tabletop Companion. To You have been invited to join the table “%{table_name}” on Tabletop Companion. To
respond, visit the link below. respond, visit the link below.
@ -145,13 +165,22 @@ en:
error: Failed to update profile error: Failed to update profile
user_mailer: user_mailer:
email_verified: email_verified:
subject: Email verified on Tabletop Companion
content: |- content: |-
Thanks for verifying your email address, and welcome to Tabletop Companion! Thanks for verifying your email address, and welcome to Tabletop Companion!
You can now go ahead and log in. Enjoy! You can now go ahead and log in. Enjoy!
email_verification: email_verification:
subject: Verify your email on Tabletop Companion
content: |- content: |-
If you did not sign up for Tabletop Companion, please ignore this email. If you did not sign up for Tabletop Companion, please ignore this email.
Otherwise, please visit the link below to verify your email address. Otherwise, please visit the link below to verify your email address.
password_reset:
subject: Reset your password on Tabletop Companion
reset_password: Reset your password
content: |-
If you did not request a password reset, please ignore this email.
Otherwise, please visit the link below to reset your password.

View File

@ -10,6 +10,7 @@ Rails.application.routes.draw do
resources :users, only: [ :new, :create, :show, :edit, :update ] resources :users, only: [ :new, :create, :show, :edit, :update ]
resources :account_verifications, only: [ :show ] resources :account_verifications, only: [ :show ]
resources :password_resets, only: [ :new, :create, :edit, :update ]
resources :sessions, only: [ :new, :create, :destroy ] resources :sessions, only: [ :new, :create, :destroy ]
resources :table_invites, only: [ :index, :edit, :update ] resources :table_invites, only: [ :index, :edit, :update ]

View File

@ -0,0 +1,34 @@
# frozen_string_literal: true
require "test_helper"
class PasswordResetsControllerTest < ActionDispatch::IntegrationTest
test "should get new" do
get new_password_reset_path
assert_response :success
end
test "should send a password reset email" do
user = users(:trevor)
assert_emails(+1) do
post password_resets_path, params: { username: user.username }
assert_redirected_to new_session_path
end
end
test "should get edit" do
user = users(:trevor)
token = user.generate_token_for(:password_reset)
get edit_password_reset_path(token: token, id: user.username)
assert_response :success
end
test "should update password" do
user = users(:trevor)
token = user.generate_token_for(:password_reset)
put password_reset_path(id: user.username, token: token),
params: { password: "password", password_confirmation: "password" }
assert_redirected_to new_session_path
assert user.reload.authenticate("password")
end
end

View File

@ -2,6 +2,9 @@
- delete avatar - delete avatar
- default avatars - default avatars
- discrete password page - discrete password page
- shared/private notes
- notifications - notifications
- Add characters to users/tables - Add characters to users/tables
- Character sheets/prototypes - Character sheets/prototypes
- chat
- maps