Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "Forgot Password" reset feature using Postmark #470

Merged
merged 69 commits into from
Aug 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
dd06810
working version of postmark mailer
jlvallelonga Jul 23, 2024
6ac901f
adds browser and os lists as globals in init
jlvallelonga Jul 23, 2024
d9a2311
moves user agent handling code to controller
jlvallelonga Jul 23, 2024
0aa520e
moves agent helper functions onto request class with extension
jlvallelonga Jul 24, 2024
33b5498
removes leftover function
jlvallelonga Jul 24, 2024
248197a
updates from PR
jlvallelonga Jul 24, 2024
f525615
adds use of "user" instead of "personable"
jlvallelonga Jul 25, 2024
ca220b2
adds ! to method name that throws an error
jlvallelonga Jul 25, 2024
20ddbd8
removes extra settings
jlvallelonga Jul 25, 2024
b09ff5f
refactor for new routes
jlvallelonga Jul 25, 2024
0ef3581
fixes controller tests
jlvallelonga Jul 26, 2024
e018e30
adds test for token error handling in passwords controller
jlvallelonga Jul 26, 2024
db2724c
adds references to product name setting
jlvallelonga Jul 26, 2024
52ad7ed
reorders env vars
jlvallelonga Jul 26, 2024
261a9f5
settings and features options refactor
jlvallelonga Jul 26, 2024
5206bad
adds check for required features with reset password feature
jlvallelonga Jul 26, 2024
580a5f7
removes obsolete version from docker compose
jlvallelonga Jul 26, 2024
70b01f0
pull template from postmark to repo
jlvallelonga Jul 27, 2024
47428d9
adds mailer test
jlvallelonga Jul 27, 2024
602c5c2
adds tests for lib
jlvallelonga Jul 27, 2024
fb270fe
fixes mailer preview. adds test for extended request methods
jlvallelonga Jul 28, 2024
7b4f7af
updates route to match model
jlvallelonga Jul 28, 2024
501e102
removes leftover config option
jlvallelonga Jul 28, 2024
aef4214
renames email from address to not be postmark specific
jlvallelonga Jul 28, 2024
2d5853c
Merge remote-tracking branch 'origin/main' into feature/password-reset
jlvallelonga Jul 28, 2024
e477322
updates rubocop to not fail the linter
jlvallelonga Jul 28, 2024
5ec7828
fixes tests for ci env
jlvallelonga Jul 28, 2024
a4a5bd8
updates workflow to contain necessary env vars for tests
jlvallelonga Jul 28, 2024
1827132
fixes incorrect setting usage
jlvallelonga Jul 29, 2024
88380d9
updates rubocop setting severity. adds default from
jlvallelonga Jul 29, 2024
2bf1e77
use deliver_now. indentation. tests for job
jlvallelonga Jul 29, 2024
9b0e26f
renames missed file
jlvallelonga Jul 29, 2024
0626390
adds credentials option for email_host
jlvallelonga Jul 30, 2024
b133f43
Update app/controllers/password_credentials_controller.rb
jlvallelonga Jul 30, 2024
748b289
Update lib/rails_extensions/action_dispatch/request.rb
jlvallelonga Jul 30, 2024
9fd1223
Update app/views/password_resets/new.html.erb
jlvallelonga Jul 30, 2024
319b3f8
Update app/views/password_resets/new.html.erb
jlvallelonga Jul 30, 2024
c1e709a
Update app/views/password_credentials/edit.html.erb
jlvallelonga Jul 30, 2024
9df5da4
Update app/helpers/password_credentials_helper.rb
jlvallelonga Jul 30, 2024
f46d206
Update app/views/password_mailer/reset.html.erb
jlvallelonga Jul 30, 2024
df75526
Update app/views/password_mailer/reset.html.erb
jlvallelonga Jul 30, 2024
9411f47
Update app/views/password_mailer/reset.html.erb
jlvallelonga Jul 30, 2024
6721366
Update app/controllers/password_resets_controller.rb
jlvallelonga Jul 30, 2024
c6942c2
updates feature env var
jlvallelonga Jul 30, 2024
fb6aa35
allow unauthenticated access for credentials controller
jlvallelonga Jul 30, 2024
49cb154
allows users to update password if they sign up initially with google
jlvallelonga Jul 30, 2024
9f205f8
adds note to readme about troubleshooting google auth
jlvallelonga Jul 30, 2024
46c53e2
login user after reset password
jlvallelonga Jul 30, 2024
4b312f9
updates test helpers
jlvallelonga Jul 31, 2024
a84502c
Merge branch 'main' into feature/password-reset
jlvallelonga Jul 31, 2024
9183ae7
removes env vars in workflow in favor of stubbing
jlvallelonga Jul 31, 2024
d921ad6
adds env vars back to workflow temporarily
jlvallelonga Jul 31, 2024
6c7f94b
Rearrange email settings and add helpers (#476)
jlvallelonga Jul 31, 2024
d82aa18
updates github workflows with new env vars
jlvallelonga Jul 31, 2024
31bcafb
removes unused function. remove unecessary function
jlvallelonga Jul 31, 2024
db819ab
Merge branch 'main' into feature/password-reset
jlvallelonga Aug 2, 2024
4f052ac
Update app/controllers/password_resets_controller.rb
jlvallelonga Aug 2, 2024
c7ce9ee
updates tests to work with different env
jlvallelonga Aug 2, 2024
ca91c72
removes unnecessary fixtures calls. adds logout call
jlvallelonga Aug 2, 2024
82b73f1
Merge branch 'feature/password-reset' of github.com:AllYourBot/hosted…
jlvallelonga Aug 2, 2024
ee4f4a9
Update test/models/setting_test.rb
jlvallelonga Aug 2, 2024
300fbdb
adds test default
jlvallelonga Aug 2, 2024
3a53df2
Merge branch 'feature/password-reset' of github.com:AllYourBot/hosted…
jlvallelonga Aug 2, 2024
9a41a99
adds test defaults
jlvallelonga Aug 2, 2024
8bbe8ed
adds test default
jlvallelonga Aug 2, 2024
8691d5f
removes extra env vars
jlvallelonga Aug 2, 2024
2d2ab7e
adds env vars back to fix broken system tests
jlvallelonga Aug 2, 2024
f2718bd
removes env vars. removes section from readme
jlvallelonga Aug 9, 2024
ec9e10d
Discard changes to .github/workflows/rubyonrails.yml
krschacht Aug 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ AllCops:
DisabledByDefault: true
SuggestExtensions: false
Exclude:
- '**/tmp/**/*'
- '**/vendor/**/*'
- '**/node_modules/**/*'
- 'bin/*'
- 'native/**/*'
- "**/tmp/**/*"
- "**/vendor/**/*"
- "**/node_modules/**/*"
- "bin/*"
- "native/**/*"

Performance:
Exclude:
- '**/test/**/*'
- "**/test/**/*"

# Method definitions after `private` or `protected` isolated calls do NOT
# need extra level of indentation.
Expand All @@ -39,7 +39,7 @@ Layout/IndentationWidth:
Enabled: true
Width: 2
Exclude:
- '**/test/**/*'
- "**/test/**/*"

# No spaces at the end of a line
Layout/TrailingWhitespace:
Expand All @@ -54,22 +54,22 @@ Style/SymbolArray:
Enabled: true
EnforcedStyle: brackets
Include:
- 'routes.rb'
- 'app/controllers/**/*'
- "routes.rb"
- "app/controllers/**/*"

# Rules below this line are disabled

# Prefer assert_not over assert !
Rails/AssertNot:
Enabled: false
Include:
- '**/test/**/*'
- "**/test/**/*"

# Prefer assert_not_x over refute_x
Rails/RefuteMethods:
Enabled: false
Include:
- '**/test/**/*'
- "**/test/**/*"

Rails/IndexBy:
Enabled: false
Expand Down Expand Up @@ -246,7 +246,7 @@ Lint/DeprecatedClassMethods:
Style/EvalWithLocation:
Enabled: false
Exclude:
- '**/test/**/*'
- "**/test/**/*"

Style/ParenthesesAroundCondition:
Enabled: false
Expand Down
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ gem "solid_queue", "~> 0.2.1"
gem "name_of_person"
gem "actioncable-enhanced-postgresql-adapter" # longer paylaods w/ postgresql actioncable
gem "aws-sdk-s3", require: false
gem "postmark-rails"

gem "omniauth", "~> 2.1"
gem "omniauth-google-oauth2", "~> 1.1"
Expand Down Expand Up @@ -81,4 +82,5 @@ group :test do
gem "capybara"
gem "selenium-webdriver"
gem "minitest-stub_any_instance"
gem "rails-controller-testing"
end
11 changes: 11 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,11 @@ GEM
ast (~> 2.4.1)
racc
pg (1.5.6)
postmark (1.25.1)
json
postmark-rails (0.22.1)
actionmailer (>= 3.0.0)
postmark (>= 1.21.3, < 2.0)
prism (0.19.0)
protocol (2.0.0)
ruby_parser (~> 3.0)
Expand Down Expand Up @@ -274,6 +279,10 @@ GEM
activesupport (= 7.1.3.2)
bundler (>= 1.15.0)
railties (= 7.1.3.2)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
activesupport (>= 5.0.1.rc1)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
Expand Down Expand Up @@ -448,10 +457,12 @@ DEPENDENCIES
omniauth-google-oauth2 (~> 1.1)
omniauth-rails_csrf_protection (~> 1.0.2)
pg (~> 1.1)
postmark-rails
pry-rails
puma (>= 5.0)
rack-cors
rails (~> 7.1.3)
rails-controller-testing
rails_heroicon (~> 2.2.0)
redcarpet (~> 3.6.0)
redis (>= 4.0.1)
Expand Down
37 changes: 37 additions & 0 deletions app/controllers/password_credentials_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
class PasswordCredentialsController < ApplicationController
allow_unauthenticated_access
before_action :ensure_manual_login_allowed

layout "public"

def edit
@user = find_signed_user(params[:token])
end

def update
user = find_signed_user(params[:token])

credential = user.credentials.find_or_initialize_by(type: "PasswordCredential")
credential.password = update_params[:password]

if credential.save
logout_current if Current.client
login_as user.person, credential: user.password_credential
redirect_to root_path, notice: "Your password was reset successfully."
else
render "edit", alert: "There was an error resetting your password"
end
end

private

def find_signed_user(token)
User.find_signed!(token, purpose: Email::PasswordReset::TOKEN_PURPOSE)
rescue ActiveSupport::MessageVerifier::InvalidSignature
redirect_to login_path, alert: "Your token has expired. Please try again."
end

def update_params
params.permit(:password)
end
end
17 changes: 17 additions & 0 deletions app/controllers/password_resets_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class PasswordResetsController < ApplicationController
allow_unauthenticated_access
before_action :ensure_manual_login_allowed

layout "public"

def new
end

def create
os = request.operating_system
browser = request.browser
SendResetPasswordEmailJob.perform_later(params[:email], os, browser) # queue as a job to avoid timing attacks
krschacht marked this conversation as resolved.
Show resolved Hide resolved

redirect_to login_path, notice: "If that email is valid, we just sent password reset email."
end
end
2 changes: 2 additions & 0 deletions app/helpers/password_credentials_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module PasswordCredentialsHelper
end
2 changes: 2 additions & 0 deletions app/helpers/password_resets_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module PasswordResetsHelper
end
11 changes: 11 additions & 0 deletions app/jobs/send_reset_password_email_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class SendResetPasswordEmailJob < ApplicationJob
queue_as :default

def perform(email, os, browser)
person = Person.find_by_email(email)

if person&.user # make sure the user exists (i.e. user has not become a tombstone)
PasswordMailer.with(person: person, os: os, browser: browser).reset.deliver_now
end
end
end
3 changes: 1 addition & 2 deletions app/mailers/application_mailer.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
class ApplicationMailer < ActionMailer::Base
default from: "[email protected]"
layout "mailer"
default from: Setting.email_from
end
21 changes: 21 additions & 0 deletions app/mailers/password_mailer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class PasswordMailer < ApplicationMailer
def reset
person = params[:person]
@user = person.user
@os = params[:os]
@browser = params[:browser]

@token_ttl = Email::PasswordReset::TOKEN_TTL

token = @user.signed_id(
purpose: Email::PasswordReset::TOKEN_PURPOSE,
expires_in: @token_ttl
)
@edit_url = edit_password_credential_url(token: token)

mail(
to: person.email,
subject: "Set up a new password for #{Setting.product_name}",
)
end
end
12 changes: 12 additions & 0 deletions app/models/setting.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,21 @@ def settings
end

def method_missing(method_name, *arguments, &block)
if settings.keys.exclude?(method_name.to_sym)
abort "ERROR: no setting found for #{method_name}. Please check settings in options.yml"
end

ActiveRecord::Type::ImmutableString.new.cast(
settings.fetch(method_name.to_sym, nil)
)
end

def require_keys!(*keys)
keys.each do |key|
if send(key).blank?
abort "ERROR: Please set the #{key.upcase} environment variable or secret" # if we're missing a required setting then fail fast and don't start the app
end
end
end
end
end
99 changes: 48 additions & 51 deletions app/views/authentications/new.html.erb
Original file line number Diff line number Diff line change
@@ -1,20 +1,47 @@
<div class="flex flex-col items-center justify-center w-full h-full space-y-4">
<% content_for :heading, "Welcome back" %>
<% if Feature.registration? %>
<span class="text-sm text-center">
Don't have an account? <%= link_to "Sign up", register_path, class: "underline" %>
</span>
<% end %>

<div class="p-3 bg-white border border-gray-100 rounded-full">
<%= image_tag("logo.svg", width: "24", height: "24", class: "text-gray-950") %>
</div>
<% if Feature.google_authentication? %>
<%= button_to "Log In with Google", "/auth/google",
method: "post",
class: %|
text-white font-medium
bg-brand-blue dark:bg-gray-900
border border-white dark:border-gray-400
rounded-lg p-4 text-center
cursor-pointer
hover:opacity-95
|,
form_class: "flex flex-col space-y-4 w-80",
data: {
turbo: false,
}
%>
<span class="text-sm text-center <%= Feature.disabled?(:password_authentication) && 'hidden' %>">
- Or -
</span>
<% end %>

<h2 class="text-3xl font-semibold">Welcome back</h2>

<% if Feature.registration? %>
<span class="text-sm text-center">
Don't have an account? <%= link_to "Sign up", register_path, class: "underline" %>
</span>
<% end %>

<% if Feature.google_authentication? %>
<%= button_to "Log In with Google", "/auth/google",
method: "post",
<% if Feature.password_authentication? %>
<%= form_with(url: login_path, method: :post, class: "flex flex-col space-y-4 w-80") do |f| %>
<%= f.email_field :email,
value: params[:email],
id: "email",
autofocus: params[:email].present? ? false : true,
placeholder: "Email address",
class: "border border-gray-400 rounded p-3 dark:text-black"
%>
<%= f.password_field :password,
id: "password",
autofocus: params[:email].present? ? true : false,
placeholder: "Password",
class: "border border-gray-400 rounded p-3 dark:text-black"
%>
<%= f.submit "Log In",
class: %|
text-white font-medium
bg-brand-blue dark:bg-gray-900
Expand All @@ -23,42 +50,12 @@
cursor-pointer
hover:opacity-95
|,
form_class: "flex flex-col space-y-4 w-80",
data: {
turbo: false,
}
data: { turbo_submits_with: "Authenticating..." }
%>
<span class="text-sm text-center <%= Feature.disabled?(:password_authentication) && 'hidden' %>">
- Or -
</span>
<% end %>

<% if Feature.password_authentication? %>
<%= form_with(url: login_path, method: :post, class: "flex flex-col space-y-4 w-80") do |f| %>
<%= f.email_field :email,
value: params[:email],
id: "email",
autofocus: params[:email].present? ? false : true,
placeholder: "Email address",
class: "border border-gray-400 rounded p-3 dark:text-black"
%>
<%= f.password_field :password,
id: "password",
autofocus: params[:email].present? ? true : false,
placeholder: "Password",
class: "border border-gray-400 rounded p-3 dark:text-black"
%>
<%= f.submit "Log In",
class: %|
text-white font-medium
bg-brand-blue dark:bg-gray-900
border border-white dark:border-gray-400
rounded-lg p-4 text-center
cursor-pointer
hover:opacity-95
|,
data: { turbo_submits_with: "Authenticating..." }
%>
<% end %>
<% if Feature.password_reset_email? %>
<span class="text-sm text-center">
<%= link_to "Forgot your password?", new_password_reset_path, class: "underline" %>
</span>
<% end %>
</div>
<% end %>
2 changes: 1 addition & 1 deletion app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<% content_for :title, "HostedGPT" %>
<% content_for :title, Setting.product_name %>
<!DOCTYPE html>
<html data-role="<%= Rails.env %>">
<%= render "head" %>
Expand Down
10 changes: 9 additions & 1 deletion app/views/layouts/public.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@
<%= render "head" %>
<body class="font-sans text-gray-950 dark:text-gray-100 system">
<main class="flex bg-white dark:bg-gray-800 w-full dark:dark:text-gray-100">
<%= content_for?(:main) ? yield(:main) : yield %>
<div class="flex flex-col items-center justify-center w-full h-full space-y-4">
<div class="p-3 bg-white border border-gray-100 rounded-full">
<%= image_tag("logo.svg", width: "24", height: "24", class: "text-gray-950") %>
</div>

<h2 class='text-3xl font-semibold'><%= content_for?(:heading) ? yield(:heading) : "Welcome back" %></h2>
krschacht marked this conversation as resolved.
Show resolved Hide resolved

<%= content_for?(:main) ? yield(:main) : yield %>
</div>
</main>
<%= render "layouts/toast" %>
</body>
Expand Down
2 changes: 1 addition & 1 deletion app/views/layouts/settings.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<% content_for :title, "Settings — HostedGPT" %>
<% content_for :title, "Settings — #{Setting.product_name}" %>
<% content_for :nav_column do %>
<header class="pl-2 py-7 text-3xl relative">
<%= link_to root_path, class: "inline-block cursor-pointer py-1 pt-0 align-middle" do %>
Expand Down
Loading
Loading