From dd0681086b59f051ab152d1ad4cee14c0bc89d50 Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Tue, 23 Jul 2024 22:21:55 +0700 Subject: [PATCH 01/64] working version of postmark mailer --- Gemfile | 3 + Gemfile.lock | 6 + app/controllers/password_resets_controller.rb | 49 +++++ app/helpers/password_resets_helper.rb | 2 + app/jobs/send_reset_password_email_job.rb | 46 +++++ app/mailers/password_mailer.rb | 59 ++++++ app/models/setting.rb | 8 + app/views/authentications/new.html.erb | 115 ++++++----- app/views/layouts/public.html.erb | 10 +- app/views/password_resets/edit.html.erb | 29 +++ app/views/password_resets/new.html.erb | 23 +++ app/views/users/new.html.erb | 184 +++++++++--------- config/application.rb | 25 +++ config/environments/production.rb | 5 - config/options.yml | 10 + config/routes.rb | 9 + docker-compose.yml | 10 + .../password_resets_controller_test.rb | 23 +++ .../send_reset_password_email_job_test.rb | 7 + test/mailers/password_mailer_test.rb | 12 ++ .../previews/password_mailer_preview.rb | 9 + 21 files changed, 483 insertions(+), 161 deletions(-) create mode 100644 app/controllers/password_resets_controller.rb create mode 100644 app/helpers/password_resets_helper.rb create mode 100644 app/jobs/send_reset_password_email_job.rb create mode 100644 app/mailers/password_mailer.rb create mode 100644 app/views/password_resets/edit.html.erb create mode 100644 app/views/password_resets/new.html.erb create mode 100644 test/controllers/password_resets_controller_test.rb create mode 100644 test/jobs/send_reset_password_email_job_test.rb create mode 100644 test/mailers/password_mailer_test.rb create mode 100644 test/mailers/previews/password_mailer_preview.rb diff --git a/Gemfile b/Gemfile index 9c5a24198..a9d9e2ddd 100644 --- a/Gemfile +++ b/Gemfile @@ -50,6 +50,9 @@ gem "omniauth", "~> 2.1" gem "omniauth-google-oauth2", "~> 1.1" gem "omniauth-rails_csrf_protection", "~> 1.0.2" +# Action Mailer +gem 'postmark-rails' + group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem gem "debug", platforms: %i[mri windows] diff --git a/Gemfile.lock b/Gemfile.lock index af2ffa6e5..a699b2a2c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) @@ -448,6 +453,7 @@ 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 diff --git a/app/controllers/password_resets_controller.rb b/app/controllers/password_resets_controller.rb new file mode 100644 index 000000000..148eb37c2 --- /dev/null +++ b/app/controllers/password_resets_controller.rb @@ -0,0 +1,49 @@ +class PasswordResetsController < ApplicationController + require_unauthenticated_access + before_action :ensure_manual_login_allowed + + layout "public" + + def new + end + + def create + user_agent = request.env['HTTP_USER_AGENT'] + + SendResetPasswordEmailJob.perform_later(params[:email], user_agent) # queue as a job to avoid timing attacks + + redirect_to '/login', notice: 'If an account with that email was found, we have sent a link to reset the password' + end + + def edit + @person = find_signed_person(params[:token]) + end + + def update + @person = find_signed_person(params[:token]) + password_credential = @person&.personable&.password_credential + + if password_credential&.update(password_params) + redirect_to '/login', notice: 'Your password was reset succesfully. Please sign in.' + else + render 'edit', alert: 'There was an error resetting your password' + end + end + + private + + def find_signed_person(token) + Person.find_signed!(token, purpose: Rails.application.config.password_reset_token_purpose) + rescue ActiveSupport::MessageVerifier::InvalidSignature + redirect_to '/login', alert: 'Your token has expired. Please try again' + end + + def password_params + h = params.require(:person).permit(personable_attributes: [ + credentials_attributes: [ :type, :password ] + ]).to_h + person_params = format_and_strip_all_but_first_valid_credential(h) + { password: person_params[:personable_attributes][:credentials_attributes][0][:password] } + end + +end diff --git a/app/helpers/password_resets_helper.rb b/app/helpers/password_resets_helper.rb new file mode 100644 index 000000000..0c9d96ecf --- /dev/null +++ b/app/helpers/password_resets_helper.rb @@ -0,0 +1,2 @@ +module PasswordResetsHelper +end diff --git a/app/jobs/send_reset_password_email_job.rb b/app/jobs/send_reset_password_email_job.rb new file mode 100644 index 000000000..023f0d561 --- /dev/null +++ b/app/jobs/send_reset_password_email_job.rb @@ -0,0 +1,46 @@ +class SendResetPasswordEmailJob < ApplicationJob + queue_as :default + + def perform(email, user_agent) + os = get_os_from_user_agent(user_agent) + browser = get_browser_from_user_agent(user_agent) + + if @person = Person.find_by_email(email) + PasswordMailer.with(person: @person, os: os, browser: browser).reset.deliver_later + end + end + + private + + def get_os_from_user_agent(user_agent) + if user_agent.include?("Windows") + "Windows" + elsif user_agent.include?("Macintosh") + "Macintosh" + elsif user_agent.include?("Linux") + "Linux" + elsif user_agent.include?("Android") + "Android" + elsif user_agent.include?("iPhone") + "iPhone" + else + "unknown operating system" + end + end + + def get_browser_from_user_agent(user_agent) + if user_agent.include?("Chrome") + "Chrome" + elsif user_agent.include?("Safari") + "Safari" + elsif user_agent.include?("Firefox") + "Firefox" + elsif user_agent.include?("Edge") + "Edge" + elsif user_agent.include?("Opera") + "Opera" + else + "unknown browser" + end + end +end diff --git a/app/mailers/password_mailer.rb b/app/mailers/password_mailer.rb new file mode 100644 index 000000000..db4b2ae70 --- /dev/null +++ b/app/mailers/password_mailer.rb @@ -0,0 +1,59 @@ +require 'postmark-rails/templated_mailer' + +class PasswordMailer < PostmarkRails::TemplatedMailer + def reset + person = params[:person] + os = params[:os] + browser = params[:browser] + + @token = person.signed_id( + purpose: Rails.application.config.password_reset_token_purpose, + expires_in: Rails.application.config.password_reset_token_ttl_minutes.minutes + ) + token_url = password_reset_edit_url(token: @token) + + user = person.personable + + self.template_model = { + product_url: Setting.action_mailer_host, + product_name: Setting.product_name, + name: user.first_name, + token_ttl: ttl_minutes_as_human_readable, + action_url: token_url, + operating_system: os, + browser_name: browser, + support_url: Setting.support_url, + company_name: Setting.company_name, + company_address: Setting.company_address + } + + mail( + from: Setting.postmark_from_email, + to: person.email, + postmark_template_alias: Setting.postmark_password_reset_template_alias + ) + end + + private + + def ttl_minutes_as_human_readable + ttl_minutes = Rails.application.config.password_reset_token_ttl_minutes + duration = ActiveSupport::Duration.build(ttl_minutes * 60) + duration_as_sentence(duration) + end + + def duration_as_sentence(duration) + parts = duration.parts + units = [:days, :hours, :minutes] + map = { + :days => { :one => :d, :other => :days }, + :hours => { :one => :h, :other => :hours }, + :minutes => { :one => :m, :other => :minutes } + } + + parts. + sort_by { |unit, _| units.index(unit) }. + map { |unit, val| "#{val} #{val == 1 ? map[unit][:one].to_s : map[unit][:other].to_s}" }. + to_sentence + end +end diff --git a/app/models/setting.rb b/app/models/setting.rb index 1557820f6..f2b36ccc3 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -11,5 +11,13 @@ def method_missing(method_name, *arguments, &block) 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 diff --git a/app/views/authentications/new.html.erb b/app/views/authentications/new.html.erb index fcf042545..e51d17464 100644 --- a/app/views/authentications/new.html.erb +++ b/app/views/authentications/new.html.erb @@ -1,64 +1,59 @@ -
+<% content_for :heading, "Welcome back" %> +<% if Feature.registration? %> + + Don't have an account? <%= link_to "Sign up", register_path, class: "underline" %> + +<% end %> -
- <%= image_tag("logo.svg", width: "24", height: "24", class: "text-gray-950") %> -
+<% 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, + } + %> + + - Or - + +<% end %> -

Welcome back

- - <% if Feature.registration? %> - - Don't have an account? <%= link_to "Sign up", register_path, class: "underline" %> - - <% end %> - - <% 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, - } +<% 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..." } %> - - - Or - - - <% 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 %> <% end %> -
+ + <%= link_to "Forgot your password?", password_reset_path, class: "underline" %> + +<% end %> diff --git a/app/views/layouts/public.html.erb b/app/views/layouts/public.html.erb index 4396fd921..d82bf6718 100644 --- a/app/views/layouts/public.html.erb +++ b/app/views/layouts/public.html.erb @@ -3,7 +3,15 @@ <%= render "head" %>
- <%= content_for?(:main) ? yield(:main) : yield %> +
+
+ <%= image_tag("logo.svg", width: "24", height: "24", class: "text-gray-950") %> +
+ +

<%= content_for?(:heading) ? yield(:heading) : "Welcome back" %>

+ + <%= content_for?(:main) ? yield(:main) : yield %> +
<%= render "layouts/toast" %> diff --git a/app/views/password_resets/edit.html.erb b/app/views/password_resets/edit.html.erb new file mode 100644 index 000000000..2ab1afbf0 --- /dev/null +++ b/app/views/password_resets/edit.html.erb @@ -0,0 +1,29 @@ +<% content_for :heading, "Reset your password" %> + +<%= form_with model: @person, url: password_reset_edit_path(token: params[:token]), class: "flex flex-col space-y-4 w-80" do |f| %> + <%= f.fields_for :personable_attributes, @person.personable do |user_fields| %> + <%= user_fields.fields_for :credentials_attributes, @person.personable.credentials.build, index: "0" do |credential_fields| %> + <%= credential_fields.hidden_field :type, value: "PasswordCredential" %> + <%= credential_fields.label :password, class: "input input-bordered flex items-center justify-between" do %> + Password * + <%= credential_fields.password_field :password, + autofocus: true, + placeholder: "Password", + class: "w-44 border-0 dark:text-black", + data: { action: "focus->transition#toggleClassOn" } + %> + <% end %> + <% end %> + <% end %> + + <%= f.submit 'Reset Password', + 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 + | + %> +<%end%> diff --git a/app/views/password_resets/new.html.erb b/app/views/password_resets/new.html.erb new file mode 100644 index 000000000..89518c073 --- /dev/null +++ b/app/views/password_resets/new.html.erb @@ -0,0 +1,23 @@ +<% content_for :heading, "Forgot your password?" %> + +<%= form_with url: password_reset_path, class: "flex flex-col space-y-4 w-80" do |f| %> + <%= f.label :email, class: "input input-bordered flex items-center justify-between" do %> + Email * + <%= f.email_field :email, + autofocus: true, + placeholder: "Email", + class: "w-44 border-0 dark:text-black" + %> + <% end %> + + <%= f.submit 'Send reset email', + 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 + | + %> +<%end%> diff --git a/app/views/users/new.html.erb b/app/views/users/new.html.erb index a8ec0c5e9..f7daf40d4 100644 --- a/app/views/users/new.html.erb +++ b/app/views/users/new.html.erb @@ -1,22 +1,89 @@ -
-
- <%= image_tag("logo.svg", width: "24", height: "24", class: "text-gray-950") %> +<% content_for :heading, "Create your account" %> +<% if @person.errors.any? %> +
+
    + <% @person.errors.full_messages.reverse.each do |error| # We want the User error first %> +
  • <%= error.gsub(/Personable |credentials /, '').capitalize %><%# Cleanup "Personable password is ..." %>
  • + <% end %> +
-

Create your account

+<% end %> + +<% if Feature.google_authentication? %> + <%= button_to "Sign Up 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, + } + %> + + - Or - + +<% end %> + +<% if Feature.password_authentication? %> + <%= form_with method: :post, + model: @person, + url: users_path, + class: "flex flex-col space-y-4 w-80", + data: { + controller: "transition", + transition_toggle_class: "max-h-0 max-h-40" + } do + |f| %> + <%= f.hidden_field :personable_type, id: "personable_type", value: "User" %> + + <%= f.label :email, class: "input input-bordered flex items-center justify-between" do %> + Email * + <%= f.email_field :email, + autofocus: true, + placeholder: "Email", + class: "w-44 border-0 dark:text-black" + %> + <% end %> - <% if @person.errors.any? %> -
-
    - <% @person.errors.full_messages.reverse.each do |error| # We want the User error first %> -
  • <%= error.gsub(/Personable |credentials /, '').capitalize %><%# Cleanup "Personable password is ..." %>
  • + <%= f.fields_for :personable_attributes, @person.personable do |user_fields| %> + <%= user_fields.fields_for :credentials_attributes, @person.personable.credentials.build, index: "0" do |credential_fields| %> + <%= credential_fields.hidden_field :type, value: "PasswordCredential" %> + <%= credential_fields.label :password, class: "input input-bordered flex items-center justify-between" do %> + Password * + <%= credential_fields.password_field :password, + placeholder: "Password", + class: "w-44 border-0 dark:text-black", + data: { action: "focus->transition#toggleClassOn" } + %> <% end %> -
-
- <% end %> + <% end %> + +
+ flex flex-col + transition-all duration-100 ease-in + space-y-4 + " + data-transition-target="transitionable" + > - <% if Feature.google_authentication? %> - <%= button_to "Sign Up with Google", "/auth/google", - method: "post", + <%= user_fields.label :name, class: "input input-bordered flex items-center justify-between" do %> + Full Name * + <%= user_fields.text_field :name, + placeholder: "First & Last name", + class: "w-44 border-0 dark:text-black" + %> + <% end %> +
+ <% end %> + <%= f.submit "Sign Up", class: %| text-white font-medium bg-brand-blue dark:bg-gray-900 @@ -25,85 +92,12 @@ cursor-pointer hover:opacity-95 |, - form_class: "flex flex-col space-y-4 w-80", - data: { - turbo: false, - } + data: { turbo_submits_with: "Creating..." } %> - - - Or - - - <% end %> - - <% if Feature.password_authentication? %> - <%= form_with method: :post, - model: @person, - url: users_path, - class: "flex flex-col space-y-4 w-80", - data: { - controller: "transition", - transition_toggle_class: "max-h-0 max-h-40" - } do - |f| %> - <%= f.hidden_field :personable_type, id: "personable_type", value: "User" %> + <% end # password_authentication? %> +<% end %> - <%= f.label :email, class: "input input-bordered flex items-center justify-between" do %> - Email * - <%= f.email_field :email, - autofocus: true, - placeholder: "Email", - class: "w-44 border-0 dark:text-black" - %> - <% end %> - - <%= f.fields_for :personable_attributes, @person.personable do |user_fields| %> - <%= user_fields.fields_for :credentials_attributes, @person.personable.credentials.build, index: "0" do |credential_fields| %> - <%= credential_fields.hidden_field :type, value: "PasswordCredential" %> - <%= credential_fields.label :password, class: "input input-bordered flex items-center justify-between" do %> - Password * - <%= credential_fields.password_field :password, - placeholder: "Password", - class: "w-44 border-0 dark:text-black", - data: { action: "focus->transition#toggleClassOn" } - %> - <% end %> - <% end %> - -
- flex flex-col - transition-all duration-100 ease-in - space-y-4 - " - data-transition-target="transitionable" - > - - <%= user_fields.label :name, class: "input input-bordered flex items-center justify-between" do %> - Full Name * - <%= user_fields.text_field :name, - placeholder: "First & Last name", - class: "w-44 border-0 dark:text-black" - %> - <% end %> -
- <% end %> - <%= f.submit "Sign Up", - 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: "Creating..." } - %> - <% end # password_authentication? %> - <% end %> - - - Already have an account? - <%= link_to "Log in", login_path, class: "underline" %> - -
+ + Already have an account? + <%= link_to "Log in", login_path, class: "underline" %> + diff --git a/config/application.rb b/config/application.rb index 257eeb5cd..f528ca0b5 100644 --- a/config/application.rb +++ b/config/application.rb @@ -8,6 +8,7 @@ require_relative "../lib/true_class" require_relative "../lib/nil_class" require_relative "../app/models/feature" +require_relative "../app/models/setting" # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. @@ -28,6 +29,11 @@ class Application < Rails::Application # Common ones are `templates`, `generators`, or `middleware`, for example. config.autoload_lib(ignore: %w[assets tasks]) + # Info include generic and useful information about system operation, but avoids logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). If you + # want to log everything, set the level to "debug". + config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") + # Configuration for the application, engines, and railties goes here. # # These settings can be overridden in specific environments using the files @@ -42,5 +48,24 @@ class Application < Rails::Application else config.active_storage.service = :database end + + # Action Mailer + Setting.require_keys(:action_mailer_host) + config.action_mailer.default_url_options = { host: Setting.action_mailer_host } + + if Feature.postmark_mailer? + Setting.require_keys( + :postmark_server_api_token, + :postmark_from_email, + :postmark_password_reset_template_alias + ) + + config.action_mailer.delivery_method = :postmark + config.action_mailer.postmark_settings = { api_token: Setting.postmark_server_api_token } + end + + # Password Reset email + config.password_reset_token_ttl_minutes = (Setting.password_reset_token_ttl_minutes.presence || 30).to_i + config.password_reset_token_purpose = "password_reset" end end diff --git a/config/environments/production.rb b/config/environments/production.rb index 68544fc79..b08cd209e 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -58,11 +58,6 @@ # Prepend all log lines with the following tags. config.log_tags = [:request_id] - # Info include generic and useful information about system operation, but avoids logging too much - # information to avoid inadvertent exposure of personally identifiable information (PII). If you - # want to log everything, set the level to "debug". - config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") - # Use a different cache store in production. # config.cache_store = :mem_cache_store diff --git a/config/options.yml b/config/options.yml index c3b07436f..5be959050 100644 --- a/config/options.yml +++ b/config/options.yml @@ -9,6 +9,7 @@ shared: http_header_authentication: <%= ENV.fetch("HTTP_HEADER_AUTHENTICATION_FEATURE", false) %> voice: <%= ENV.fetch("VOICE_FEATURE", false) %> default_to_voice: <%= ENV.fetch("DEFAULT_TO_VOICE_FEATURE", false) %> + postmark_mailer: <%= ENV.fetch("POSTMARK_MAILER_FEATURE", false) %> settings: # Be sure to add these ENV to docker-compose.yml default_openai_key: <%= ENV["DEFAULT_OPENAI_KEY"] %> @@ -23,3 +24,12 @@ shared: http_header_auth_email: <%= ENV["HTTP_HEADER_AUTH_EMAIL"] || "X-WEBAUTH-EMAIL" %> http_header_auth_name: <%= ENV["HTTP_HEADER_AUTH_NAME"] || "X-WEBAUTH-NAME" %> http_header_auth_uid: <%= ENV["HTTP_HEADER_AUTH_UID"] || "X-WEBAUTH-USER" %> + postmark_server_api_token: <%= ENV["POSTMARK_SERVER_API_TOKEN"] || Rails.application.credentials[:postmark_server_api_token] %> + postmark_from_email: <%= ENV["POSTMARK_FROM_EMAIL"] || Rails.application.credentials[:postmark_from_email] %> + postmark_password_reset_template_alias: <%= ENV["POSTMARK_PASSWORD_RESET_TEMPLATE_ALIAS"] || Rails.application.credentials[:postmark_password_reset_template_alias] %> + action_mailer_host: <%= ENV["ACTION_MAILER_HOST"] %> + product_name: <%= ENV["PRODUCT_NAME"] || "HostedGPT" %> + support_url: <%= ENV["SUPPORT_URL"] %> + company_name: <%= ENV["COMPANY_NAME"] %> + company_address: <%= ENV["COMPANY_ADDRESS"] %> + password_reset_token_ttl_minutes: <%= ENV["PASSWORD_RESET_TOKEN_TTL_MINUTES"] %> diff --git a/config/routes.rb b/config/routes.rb index 63f0117a7..56f6204fd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,8 @@ Rails.application.routes.draw do + get 'password_resets/new' + get 'password_resets/create' + get 'password_resets/edit' + get 'password_resets/update' root to: "assistants#index" resources :users, only: [:new, :create, :update] @@ -28,6 +32,11 @@ get "/register", to: "users#new" get "/logout", to: "authentications#destroy" + get '/password/reset', to: 'password_resets#new' + post '/password/reset', to: 'password_resets#create' + get '/password/reset/edit', to: 'password_resets#edit' + patch '/password/reset/edit', to: 'password_resets#update' + get "/auth/:provider/callback" => "authentications/google_oauth#create", as: :google_oauth get "/auth/failure" => "authentications/google_oauth#failure" # connected in omniauth.rb diff --git a/docker-compose.yml b/docker-compose.yml index d61c73742..5718a5248 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -47,6 +47,16 @@ services: - CLOUDFLARE_ACCESS_KEY_ID - CLOUDFLARE_SECRET_ACCESS_KEY - CLOUDFLARE_BUCKET + - POSTMARK_MAILER_FEATURE + - POSTMARK_SERVER_API_TOKEN + - POSTMARK_FROM_EMAIL + - POSTMARK_PASSWORD_RESET_TEMPLATE_ALIAS + - ACTION_MAILER_HOST + - PRODUCT_NAME + - SUPPORT_URL + - COMPANY_NAME + - COMPANY_ADDRESS + - PASSWORD_RESET_TOKEN_TTL_MINUTES ports: ["3000:3000"] volumes: - .:/rails diff --git a/test/controllers/password_resets_controller_test.rb b/test/controllers/password_resets_controller_test.rb new file mode 100644 index 000000000..07d81d12b --- /dev/null +++ b/test/controllers/password_resets_controller_test.rb @@ -0,0 +1,23 @@ +require "test_helper" + +class PasswordResetsControllerTest < ActionDispatch::IntegrationTest + test "should get new" do + get password_resets_new_url + assert_response :success + end + + test "should get create" do + get password_resets_create_url + assert_response :success + end + + test "should get edit" do + get password_resets_edit_url + assert_response :success + end + + test "should get update" do + get password_resets_update_url + assert_response :success + end +end diff --git a/test/jobs/send_reset_password_email_job_test.rb b/test/jobs/send_reset_password_email_job_test.rb new file mode 100644 index 000000000..9ee7f1b43 --- /dev/null +++ b/test/jobs/send_reset_password_email_job_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class SendResetPasswordEmailJobTest < ActiveJob::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/mailers/password_mailer_test.rb b/test/mailers/password_mailer_test.rb new file mode 100644 index 000000000..6f5cc7daf --- /dev/null +++ b/test/mailers/password_mailer_test.rb @@ -0,0 +1,12 @@ +require "test_helper" + +class PasswordMailerTest < ActionMailer::TestCase + test "reset" do + mail = PasswordMailer.reset + assert_equal "Reset", mail.subject + assert_equal ["to@example.org"], mail.to + assert_equal ["from@example.com"], mail.from + assert_match "Hi", mail.body.encoded + end + +end diff --git a/test/mailers/previews/password_mailer_preview.rb b/test/mailers/previews/password_mailer_preview.rb new file mode 100644 index 000000000..684b74290 --- /dev/null +++ b/test/mailers/previews/password_mailer_preview.rb @@ -0,0 +1,9 @@ +# Preview all emails at http://localhost:3000/rails/mailers/password_mailer +class PasswordMailerPreview < ActionMailer::Preview + + # Preview this email at http://localhost:3000/rails/mailers/password_mailer/reset + def reset + PasswordMailer.reset + end + +end From 6ac901f23cc577300b91d8a271023c68e0012501 Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Tue, 23 Jul 2024 22:50:13 +0700 Subject: [PATCH 02/64] adds browser and os lists as globals in init --- app/jobs/send_reset_password_email_job.rb | 38 +++++------------------ config/initializers/user_agent.rb | 2 ++ 2 files changed, 9 insertions(+), 31 deletions(-) create mode 100644 config/initializers/user_agent.rb diff --git a/app/jobs/send_reset_password_email_job.rb b/app/jobs/send_reset_password_email_job.rb index 023f0d561..8f5d83761 100644 --- a/app/jobs/send_reset_password_email_job.rb +++ b/app/jobs/send_reset_password_email_job.rb @@ -2,8 +2,8 @@ class SendResetPasswordEmailJob < ApplicationJob queue_as :default def perform(email, user_agent) - os = get_os_from_user_agent(user_agent) - browser = get_browser_from_user_agent(user_agent) + os = get_item_in_str(user_agent, KNOWN_OPERATING_SYSTEMS) || "unknown operating system" + browser = get_item_in_str(user_agent, KNOWN_BROWSERS) || "unknown browser" if @person = Person.find_by_email(email) PasswordMailer.with(person: @person, os: os, browser: browser).reset.deliver_later @@ -12,35 +12,11 @@ def perform(email, user_agent) private - def get_os_from_user_agent(user_agent) - if user_agent.include?("Windows") - "Windows" - elsif user_agent.include?("Macintosh") - "Macintosh" - elsif user_agent.include?("Linux") - "Linux" - elsif user_agent.include?("Android") - "Android" - elsif user_agent.include?("iPhone") - "iPhone" - else - "unknown operating system" - end - end - - def get_browser_from_user_agent(user_agent) - if user_agent.include?("Chrome") - "Chrome" - elsif user_agent.include?("Safari") - "Safari" - elsif user_agent.include?("Firefox") - "Firefox" - elsif user_agent.include?("Edge") - "Edge" - elsif user_agent.include?("Opera") - "Opera" - else - "unknown browser" + def get_item_in_str(str, items) + items.each do |item| + if str.include?(item) + return item + end end end end diff --git a/config/initializers/user_agent.rb b/config/initializers/user_agent.rb new file mode 100644 index 000000000..cb7d5a57f --- /dev/null +++ b/config/initializers/user_agent.rb @@ -0,0 +1,2 @@ +KNOWN_OPERATING_SYSTEMS = ["Windows", "Macintosh", "Linux", "Android", "iPhone"].freeze +KNOWN_BROWSERS = ["Chrome", "Safari", "Firefox", "Edge", "Opera"].freeze From d9a2311117afc94d3b4132e43ea1c9a594f9fb15 Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Tue, 23 Jul 2024 23:07:55 +0700 Subject: [PATCH 03/64] moves user agent handling code to controller --- app/controllers/password_resets_controller.rb | 11 ++++++++++- app/jobs/send_reset_password_email_job.rb | 15 +-------------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/app/controllers/password_resets_controller.rb b/app/controllers/password_resets_controller.rb index 148eb37c2..0c90e2cfd 100644 --- a/app/controllers/password_resets_controller.rb +++ b/app/controllers/password_resets_controller.rb @@ -9,8 +9,10 @@ def new def create user_agent = request.env['HTTP_USER_AGENT'] + os = get_item_in_str(user_agent, KNOWN_OPERATING_SYSTEMS) || "unknown operating system" + browser = get_item_in_str(user_agent, KNOWN_BROWSERS) || "unknown browser" - SendResetPasswordEmailJob.perform_later(params[:email], user_agent) # queue as a job to avoid timing attacks + SendResetPasswordEmailJob.perform_later(params[:email], os, browser) # queue as a job to avoid timing attacks redirect_to '/login', notice: 'If an account with that email was found, we have sent a link to reset the password' end @@ -46,4 +48,11 @@ def password_params { password: person_params[:personable_attributes][:credentials_attributes][0][:password] } end + def get_item_in_str(str, items) + items.each do |item| + if str.include?(item) + return item + end + end + end end diff --git a/app/jobs/send_reset_password_email_job.rb b/app/jobs/send_reset_password_email_job.rb index 8f5d83761..95e12fa9a 100644 --- a/app/jobs/send_reset_password_email_job.rb +++ b/app/jobs/send_reset_password_email_job.rb @@ -1,22 +1,9 @@ class SendResetPasswordEmailJob < ApplicationJob queue_as :default - def perform(email, user_agent) - os = get_item_in_str(user_agent, KNOWN_OPERATING_SYSTEMS) || "unknown operating system" - browser = get_item_in_str(user_agent, KNOWN_BROWSERS) || "unknown browser" - + def perform(email, os, browser) if @person = Person.find_by_email(email) PasswordMailer.with(person: @person, os: os, browser: browser).reset.deliver_later end end - - private - - def get_item_in_str(str, items) - items.each do |item| - if str.include?(item) - return item - end - end - end end From 0aa520efaabbf7ea9cb2ae12b2a0817dea889174 Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Wed, 24 Jul 2024 13:59:47 +0700 Subject: [PATCH 04/64] moves agent helper functions onto request class with extension --- app/controllers/password_resets_controller.rb | 6 ++--- config/initializers/rails_extensions.rb | 4 +++ config/initializers/user_agent.rb | 2 -- .../action_dispatch/request.rb | 26 +++++++++++++++++++ 4 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 config/initializers/rails_extensions.rb delete mode 100644 config/initializers/user_agent.rb create mode 100644 lib/rails_extensions/action_dispatch/request.rb diff --git a/app/controllers/password_resets_controller.rb b/app/controllers/password_resets_controller.rb index 0c90e2cfd..af31d3669 100644 --- a/app/controllers/password_resets_controller.rb +++ b/app/controllers/password_resets_controller.rb @@ -8,10 +8,8 @@ def new end def create - user_agent = request.env['HTTP_USER_AGENT'] - os = get_item_in_str(user_agent, KNOWN_OPERATING_SYSTEMS) || "unknown operating system" - browser = get_item_in_str(user_agent, KNOWN_BROWSERS) || "unknown browser" - + os = request.operating_system + browser = request.browser SendResetPasswordEmailJob.perform_later(params[:email], os, browser) # queue as a job to avoid timing attacks redirect_to '/login', notice: 'If an account with that email was found, we have sent a link to reset the password' diff --git a/config/initializers/rails_extensions.rb b/config/initializers/rails_extensions.rb new file mode 100644 index 000000000..07a08bbc1 --- /dev/null +++ b/config/initializers/rails_extensions.rb @@ -0,0 +1,4 @@ +# recursive require of all files in lib/rails_extensions +Dir[File.join(Rails.root, "lib", "rails_extensions", "**/*.rb")].each do |path| + require path +end diff --git a/config/initializers/user_agent.rb b/config/initializers/user_agent.rb deleted file mode 100644 index cb7d5a57f..000000000 --- a/config/initializers/user_agent.rb +++ /dev/null @@ -1,2 +0,0 @@ -KNOWN_OPERATING_SYSTEMS = ["Windows", "Macintosh", "Linux", "Android", "iPhone"].freeze -KNOWN_BROWSERS = ["Chrome", "Safari", "Firefox", "Edge", "Opera"].freeze diff --git a/lib/rails_extensions/action_dispatch/request.rb b/lib/rails_extensions/action_dispatch/request.rb new file mode 100644 index 000000000..4b6e009e4 --- /dev/null +++ b/lib/rails_extensions/action_dispatch/request.rb @@ -0,0 +1,26 @@ +module ActionDispatch + KNOWN_OPERATING_SYSTEMS = ["Windows", "Macintosh", "Linux", "Android", "iPhone"].freeze + KNOWN_BROWSERS = ["Chrome", "Safari", "Firefox", "Edge", "Opera"].freeze + + class Request + def browser + user_agent = env['HTTP_USER_AGENT'] + get_item_in_str(user_agent, KNOWN_BROWSERS) || "unknown browser" + end + + def operating_system + user_agent = env['HTTP_USER_AGENT'] + get_item_in_str(user_agent, KNOWN_OPERATING_SYSTEMS) || "unknown operating system" + end + + private + + def get_item_in_str(str, items) + items.each do |item| + if str.include?(item) + return item + end + end + end + end +end From 33b54987e690816812c8779ca7b3ee9e0b82c161 Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Wed, 24 Jul 2024 14:00:52 +0700 Subject: [PATCH 05/64] removes leftover function --- app/controllers/password_resets_controller.rb | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/controllers/password_resets_controller.rb b/app/controllers/password_resets_controller.rb index af31d3669..fbe427097 100644 --- a/app/controllers/password_resets_controller.rb +++ b/app/controllers/password_resets_controller.rb @@ -45,12 +45,4 @@ def password_params person_params = format_and_strip_all_but_first_valid_credential(h) { password: person_params[:personable_attributes][:credentials_attributes][0][:password] } end - - def get_item_in_str(str, items) - items.each do |item| - if str.include?(item) - return item - end - end - end end From 248197a93bef6910d6bc69f309805bbe9cf8d775 Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Wed, 24 Jul 2024 22:54:37 +0700 Subject: [PATCH 06/64] updates from PR --- .rubocop.yml | 26 ++++----- Gemfile | 5 +- Gemfile.lock | 5 ++ app/controllers/password_resets_controller.rb | 13 ++--- app/mailers/password_mailer.rb | 34 ++--------- config/application.rb | 5 -- config/environments/production.rb | 5 ++ config/routes.rb | 4 -- .../action_dispatch/request.rb | 8 +-- .../active_support/duration.rb | 17 ++++++ .../password_resets_controller_test.rb | 57 ++++++++++++++++--- test/mailers/password_mailer_test.rb | 1 - .../previews/password_mailer_preview.rb | 3 +- test/test_helper.rb | 4 ++ 14 files changed, 112 insertions(+), 75 deletions(-) create mode 100644 lib/rails_extensions/active_support/duration.rb diff --git a/.rubocop.yml b/.rubocop.yml index 79f83e659..f53d22dbb 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -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. @@ -39,7 +39,7 @@ Layout/IndentationWidth: Enabled: true Width: 2 Exclude: - - '**/test/**/*' + - "**/test/**/*" # No spaces at the end of a line Layout/TrailingWhitespace: @@ -54,8 +54,8 @@ Style/SymbolArray: Enabled: true EnforcedStyle: brackets Include: - - 'routes.rb' - - 'app/controllers/**/*' + - "routes.rb" + - "app/controllers/**/*" # Rules below this line are disabled @@ -63,13 +63,13 @@ Style/SymbolArray: 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 @@ -246,7 +246,7 @@ Lint/DeprecatedClassMethods: Style/EvalWithLocation: Enabled: false Exclude: - - '**/test/**/*' + - "**/test/**/*" Style/ParenthesesAroundCondition: Enabled: false @@ -346,7 +346,7 @@ Minitest/UnreachableAssertion: # Check quotes usage according to lint rule below. Style/StringLiterals: - Enabled: false + EnforcedStyle: double_quotes # Files should always have a new line at the end Layout/TrailingEmptyLines: diff --git a/Gemfile b/Gemfile index a9d9e2ddd..cbbd53653 100644 --- a/Gemfile +++ b/Gemfile @@ -45,14 +45,12 @@ 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" gem "omniauth-rails_csrf_protection", "~> 1.0.2" -# Action Mailer -gem 'postmark-rails' - group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem gem "debug", platforms: %i[mri windows] @@ -84,4 +82,5 @@ group :test do gem "capybara" gem "selenium-webdriver" gem "minitest-stub_any_instance" + gem "rails-controller-testing" end diff --git a/Gemfile.lock b/Gemfile.lock index a699b2a2c..64b759147 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -279,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 @@ -458,6 +462,7 @@ DEPENDENCIES 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) diff --git a/app/controllers/password_resets_controller.rb b/app/controllers/password_resets_controller.rb index fbe427097..39708de30 100644 --- a/app/controllers/password_resets_controller.rb +++ b/app/controllers/password_resets_controller.rb @@ -12,7 +12,7 @@ def create browser = request.browser SendResetPasswordEmailJob.perform_later(params[:email], os, browser) # queue as a job to avoid timing attacks - redirect_to '/login', notice: 'If an account with that email was found, we have sent a link to reset the password' + redirect_to login_path, notice: "If an account with that email was found, we have sent a link to reset the password" end def edit @@ -20,13 +20,12 @@ def edit end def update - @person = find_signed_person(params[:token]) - password_credential = @person&.personable&.password_credential + person = find_signed_person(params[:token]) - if password_credential&.update(password_params) - redirect_to '/login', notice: 'Your password was reset succesfully. Please sign in.' + if person&.personable&.password_credential&.update(password_params) + redirect_to login_path, notice: "Your password was reset succesfully. Please sign in." else - render 'edit', alert: 'There was an error resetting your password' + render "edit", alert: "There was an error resetting your password" end end @@ -35,7 +34,7 @@ def update def find_signed_person(token) Person.find_signed!(token, purpose: Rails.application.config.password_reset_token_purpose) rescue ActiveSupport::MessageVerifier::InvalidSignature - redirect_to '/login', alert: 'Your token has expired. Please try again' + redirect_to login_path, alert: "Your token has expired. Please try again" end def password_params diff --git a/app/mailers/password_mailer.rb b/app/mailers/password_mailer.rb index db4b2ae70..816ffe9be 100644 --- a/app/mailers/password_mailer.rb +++ b/app/mailers/password_mailer.rb @@ -3,8 +3,6 @@ class PasswordMailer < PostmarkRails::TemplatedMailer def reset person = params[:person] - os = params[:os] - browser = params[:browser] @token = person.signed_id( purpose: Rails.application.config.password_reset_token_purpose, @@ -14,14 +12,17 @@ def reset user = person.personable + ttl_minutes = Rails.application.config.password_reset_token_ttl_minutes + ttl_sentence = ActiveSupport::Duration.build(ttl_minutes * 60).as_sentence + self.template_model = { product_url: Setting.action_mailer_host, product_name: Setting.product_name, name: user.first_name, - token_ttl: ttl_minutes_as_human_readable, + token_ttl: ttl_sentence, action_url: token_url, - operating_system: os, - browser_name: browser, + operating_system: params[:os], + browser_name: params[:browser], support_url: Setting.support_url, company_name: Setting.company_name, company_address: Setting.company_address @@ -33,27 +34,4 @@ def reset postmark_template_alias: Setting.postmark_password_reset_template_alias ) end - - private - - def ttl_minutes_as_human_readable - ttl_minutes = Rails.application.config.password_reset_token_ttl_minutes - duration = ActiveSupport::Duration.build(ttl_minutes * 60) - duration_as_sentence(duration) - end - - def duration_as_sentence(duration) - parts = duration.parts - units = [:days, :hours, :minutes] - map = { - :days => { :one => :d, :other => :days }, - :hours => { :one => :h, :other => :hours }, - :minutes => { :one => :m, :other => :minutes } - } - - parts. - sort_by { |unit, _| units.index(unit) }. - map { |unit, val| "#{val} #{val == 1 ? map[unit][:one].to_s : map[unit][:other].to_s}" }. - to_sentence - end end diff --git a/config/application.rb b/config/application.rb index f528ca0b5..7a10b6152 100644 --- a/config/application.rb +++ b/config/application.rb @@ -29,11 +29,6 @@ class Application < Rails::Application # Common ones are `templates`, `generators`, or `middleware`, for example. config.autoload_lib(ignore: %w[assets tasks]) - # Info include generic and useful information about system operation, but avoids logging too much - # information to avoid inadvertent exposure of personally identifiable information (PII). If you - # want to log everything, set the level to "debug". - config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") - # Configuration for the application, engines, and railties goes here. # # These settings can be overridden in specific environments using the files diff --git a/config/environments/production.rb b/config/environments/production.rb index b08cd209e..68544fc79 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -58,6 +58,11 @@ # Prepend all log lines with the following tags. config.log_tags = [:request_id] + # Info include generic and useful information about system operation, but avoids logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). If you + # want to log everything, set the level to "debug". + config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") + # Use a different cache store in production. # config.cache_store = :mem_cache_store diff --git a/config/routes.rb b/config/routes.rb index 56f6204fd..ded5448ad 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,8 +1,4 @@ Rails.application.routes.draw do - get 'password_resets/new' - get 'password_resets/create' - get 'password_resets/edit' - get 'password_resets/update' root to: "assistants#index" resources :users, only: [:new, :create, :update] diff --git a/lib/rails_extensions/action_dispatch/request.rb b/lib/rails_extensions/action_dispatch/request.rb index 4b6e009e4..e4ae1ee72 100644 --- a/lib/rails_extensions/action_dispatch/request.rb +++ b/lib/rails_extensions/action_dispatch/request.rb @@ -1,15 +1,13 @@ module ActionDispatch - KNOWN_OPERATING_SYSTEMS = ["Windows", "Macintosh", "Linux", "Android", "iPhone"].freeze - KNOWN_BROWSERS = ["Chrome", "Safari", "Firefox", "Edge", "Opera"].freeze - class Request + KNOWN_OPERATING_SYSTEMS = ["Windows", "Macintosh", "Linux", "Android", "iPhone"].freeze + KNOWN_BROWSERS = ["Chrome", "Safari", "Firefox", "Edge", "Opera"].freeze + def browser - user_agent = env['HTTP_USER_AGENT'] get_item_in_str(user_agent, KNOWN_BROWSERS) || "unknown browser" end def operating_system - user_agent = env['HTTP_USER_AGENT'] get_item_in_str(user_agent, KNOWN_OPERATING_SYSTEMS) || "unknown operating system" end diff --git a/lib/rails_extensions/active_support/duration.rb b/lib/rails_extensions/active_support/duration.rb new file mode 100644 index 000000000..dc80474c9 --- /dev/null +++ b/lib/rails_extensions/active_support/duration.rb @@ -0,0 +1,17 @@ +module ActiveSupport + class Duration + def as_sentence + units = [:days, :hours, :minutes] + map = { + :days => { :one => :d, :other => :days }, + :hours => { :one => :h, :other => :hours }, + :minutes => { :one => :m, :other => :minutes } + } + + parts. + sort_by { |unit, _| units.index(unit) }. + map { |unit, val| "#{val} #{val == 1 ? map[unit][:one].to_s : map[unit][:other].to_s}" }. + to_sentence + end + end +end diff --git a/test/controllers/password_resets_controller_test.rb b/test/controllers/password_resets_controller_test.rb index 07d81d12b..fb6aedcbc 100644 --- a/test/controllers/password_resets_controller_test.rb +++ b/test/controllers/password_resets_controller_test.rb @@ -1,23 +1,64 @@ require "test_helper" class PasswordResetsControllerTest < ActionDispatch::IntegrationTest + include ActionDispatch::TestProcess::FixtureFile + + setup do + @person = people(:keith_registered) + users(:keith) + credentials(:keith_password) + end + test "should get new" do - get password_resets_new_url + get password_reset_url + assert_response :success end - test "should get create" do - get password_resets_create_url - assert_response :success + test "should post create" do + # set user agent to simulate a browser + browser = "Chrome" + operating_system = "Windows" + + email = @person.email + + # set the user agent in the request headers + ActionDispatch::Request.stub_any_instance(:user_agent, "#{browser} on #{operating_system}") do + post password_reset_url, params: { email: email } + end + + assert_enqueued_jobs 1 + assert_enqueued_with(job: SendResetPasswordEmailJob, args: [email, operating_system, browser]) + assert_response :redirect + assert_redirected_to login_path end test "should get edit" do - get password_resets_edit_url + token = get_test_person_token(@person) + + get password_reset_edit_url, params: { token: token } + assert_response :success + assert assigns(:person).is_a?(Person) + assert_equal @person, assigns(:person) end - test "should get update" do - get password_resets_update_url - assert_response :success + test "should patch update" do + token = get_test_person_token(@person) + + patch password_reset_edit_url, params: { token: token, person: { personable_attributes: { credentials_attributes: { "0" => { type: "PasswordCredential", password: "new_password" } } } } } + + assert_response :redirect + assert_redirected_to login_path end + + private + + def get_test_person_token(person) + person.signed_id( + purpose: Rails.application.config.password_reset_token_purpose, + expires_in: Rails.application.config.password_reset_token_ttl_minutes.minutes + ) + end + end diff --git a/test/mailers/password_mailer_test.rb b/test/mailers/password_mailer_test.rb index 6f5cc7daf..23b766ab8 100644 --- a/test/mailers/password_mailer_test.rb +++ b/test/mailers/password_mailer_test.rb @@ -8,5 +8,4 @@ class PasswordMailerTest < ActionMailer::TestCase assert_equal ["from@example.com"], mail.from assert_match "Hi", mail.body.encoded end - end diff --git a/test/mailers/previews/password_mailer_preview.rb b/test/mailers/previews/password_mailer_preview.rb index 684b74290..75e363ee0 100644 --- a/test/mailers/previews/password_mailer_preview.rb +++ b/test/mailers/previews/password_mailer_preview.rb @@ -3,7 +3,8 @@ class PasswordMailerPreview < ActionMailer::Preview # Preview this email at http://localhost:3000/rails/mailers/password_mailer/reset def reset - PasswordMailer.reset + # PasswordMailer.reset + "no preview available" end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 92f58eada..23dff1732 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -5,6 +5,10 @@ require "pry" Dir[Rails.root.join('test/support/**/*.rb')].sort.each { |file| require file } +Dir[File.join(Rails.root, "lib", "rails_extensions", "**/*.rb")].each do |path| + require path +end + class Capybara::Node::Element def obsolete? inspect.include?('Obsolete') From f525615475eb3244fdc303bb2c1ba1626a819aa3 Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Thu, 25 Jul 2024 13:30:12 +0700 Subject: [PATCH 07/64] adds use of "user" instead of "personable" --- app/controllers/password_resets_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/password_resets_controller.rb b/app/controllers/password_resets_controller.rb index 39708de30..55e96ff6f 100644 --- a/app/controllers/password_resets_controller.rb +++ b/app/controllers/password_resets_controller.rb @@ -22,7 +22,7 @@ def edit def update person = find_signed_person(params[:token]) - if person&.personable&.password_credential&.update(password_params) + if person.user&.password_credential&.update(password_params) redirect_to login_path, notice: "Your password was reset succesfully. Please sign in." else render "edit", alert: "There was an error resetting your password" From ca220b2b55faa7ac38b152046004f3e8f7bb418b Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Thu, 25 Jul 2024 13:49:35 +0700 Subject: [PATCH 08/64] adds ! to method name that throws an error --- app/models/setting.rb | 2 +- config/application.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/setting.rb b/app/models/setting.rb index f2b36ccc3..14def757e 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -12,7 +12,7 @@ def method_missing(method_name, *arguments, &block) ) end - def require_keys(*keys) + 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 diff --git a/config/application.rb b/config/application.rb index 7a10b6152..2ea3e00cd 100644 --- a/config/application.rb +++ b/config/application.rb @@ -45,11 +45,11 @@ class Application < Rails::Application end # Action Mailer - Setting.require_keys(:action_mailer_host) + Setting.require_keys!(:action_mailer_host) config.action_mailer.default_url_options = { host: Setting.action_mailer_host } if Feature.postmark_mailer? - Setting.require_keys( + Setting.require_keys!( :postmark_server_api_token, :postmark_from_email, :postmark_password_reset_template_alias From 20ddbd8d92cd6d94126675b988edb9be9373420d Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Thu, 25 Jul 2024 14:00:58 +0700 Subject: [PATCH 09/64] removes extra settings --- app/mailers/password_mailer.rb | 10 ++++------ config/application.rb | 2 +- config/options.yml | 4 ---- docker-compose.yml | 4 ---- 4 files changed, 5 insertions(+), 15 deletions(-) diff --git a/app/mailers/password_mailer.rb b/app/mailers/password_mailer.rb index 816ffe9be..356ec2248 100644 --- a/app/mailers/password_mailer.rb +++ b/app/mailers/password_mailer.rb @@ -1,18 +1,19 @@ -require 'postmark-rails/templated_mailer' +require "postmark-rails/templated_mailer" class PasswordMailer < PostmarkRails::TemplatedMailer def reset person = params[:person] + ttl_minutes = Rails.application.config.password_reset_token_ttl_minutes + @token = person.signed_id( purpose: Rails.application.config.password_reset_token_purpose, - expires_in: Rails.application.config.password_reset_token_ttl_minutes.minutes + expires_in: ttl_minutes.minutes ) token_url = password_reset_edit_url(token: @token) user = person.personable - ttl_minutes = Rails.application.config.password_reset_token_ttl_minutes ttl_sentence = ActiveSupport::Duration.build(ttl_minutes * 60).as_sentence self.template_model = { @@ -23,9 +24,6 @@ def reset action_url: token_url, operating_system: params[:os], browser_name: params[:browser], - support_url: Setting.support_url, - company_name: Setting.company_name, - company_address: Setting.company_address } mail( diff --git a/config/application.rb b/config/application.rb index 2ea3e00cd..fbbe38d7e 100644 --- a/config/application.rb +++ b/config/application.rb @@ -60,7 +60,7 @@ class Application < Rails::Application end # Password Reset email - config.password_reset_token_ttl_minutes = (Setting.password_reset_token_ttl_minutes.presence || 30).to_i + config.password_reset_token_ttl_minutes = 30 config.password_reset_token_purpose = "password_reset" end end diff --git a/config/options.yml b/config/options.yml index 5be959050..57f5ea4fb 100644 --- a/config/options.yml +++ b/config/options.yml @@ -29,7 +29,3 @@ shared: postmark_password_reset_template_alias: <%= ENV["POSTMARK_PASSWORD_RESET_TEMPLATE_ALIAS"] || Rails.application.credentials[:postmark_password_reset_template_alias] %> action_mailer_host: <%= ENV["ACTION_MAILER_HOST"] %> product_name: <%= ENV["PRODUCT_NAME"] || "HostedGPT" %> - support_url: <%= ENV["SUPPORT_URL"] %> - company_name: <%= ENV["COMPANY_NAME"] %> - company_address: <%= ENV["COMPANY_ADDRESS"] %> - password_reset_token_ttl_minutes: <%= ENV["PASSWORD_RESET_TOKEN_TTL_MINUTES"] %> diff --git a/docker-compose.yml b/docker-compose.yml index 5718a5248..0ea36e683 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -53,10 +53,6 @@ services: - POSTMARK_PASSWORD_RESET_TEMPLATE_ALIAS - ACTION_MAILER_HOST - PRODUCT_NAME - - SUPPORT_URL - - COMPANY_NAME - - COMPANY_ADDRESS - - PASSWORD_RESET_TOKEN_TTL_MINUTES ports: ["3000:3000"] volumes: - .:/rails From b09ff5f19707d01b61a8fb8079253844d987a512 Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Thu, 25 Jul 2024 15:58:40 +0700 Subject: [PATCH 10/64] refactor for new routes --- app/controllers/password_resets_controller.rb | 30 -------------- app/controllers/passwords_controller.rb | 32 +++++++++++++++ app/helpers/passwords_helper.rb | 2 + app/jobs/send_reset_password_email_job.rb | 6 ++- app/mailers/password_mailer.rb | 9 ++--- app/views/authentications/new.html.erb | 20 +++++----- app/views/password_resets/edit.html.erb | 29 -------------- app/views/password_resets/new.html.erb | 18 ++++----- app/views/passwords/edit.html.erb | 24 ++++++++++++ config/routes.rb | 6 +-- .../password_resets_controller_test.rb | 33 +--------------- test/controllers/passwords_controller_test.rb | 39 +++++++++++++++++++ 12 files changed, 128 insertions(+), 120 deletions(-) create mode 100644 app/controllers/passwords_controller.rb create mode 100644 app/helpers/passwords_helper.rb delete mode 100644 app/views/password_resets/edit.html.erb create mode 100644 app/views/passwords/edit.html.erb create mode 100644 test/controllers/passwords_controller_test.rb diff --git a/app/controllers/password_resets_controller.rb b/app/controllers/password_resets_controller.rb index 55e96ff6f..b4624b417 100644 --- a/app/controllers/password_resets_controller.rb +++ b/app/controllers/password_resets_controller.rb @@ -14,34 +14,4 @@ def create redirect_to login_path, notice: "If an account with that email was found, we have sent a link to reset the password" end - - def edit - @person = find_signed_person(params[:token]) - end - - def update - person = find_signed_person(params[:token]) - - if person.user&.password_credential&.update(password_params) - redirect_to login_path, notice: "Your password was reset succesfully. Please sign in." - else - render "edit", alert: "There was an error resetting your password" - end - end - - private - - def find_signed_person(token) - Person.find_signed!(token, purpose: Rails.application.config.password_reset_token_purpose) - rescue ActiveSupport::MessageVerifier::InvalidSignature - redirect_to login_path, alert: "Your token has expired. Please try again" - end - - def password_params - h = params.require(:person).permit(personable_attributes: [ - credentials_attributes: [ :type, :password ] - ]).to_h - person_params = format_and_strip_all_but_first_valid_credential(h) - { password: person_params[:personable_attributes][:credentials_attributes][0][:password] } - end end diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb new file mode 100644 index 000000000..3d265a68c --- /dev/null +++ b/app/controllers/passwords_controller.rb @@ -0,0 +1,32 @@ +class PasswordsController < ApplicationController + require_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]) + + if user.password_credential&.update(password_params) + redirect_to login_path, notice: "Your password was reset succesfully. Please sign in." + else + render "edit", alert: "There was an error resetting your password" + end + end + + private + + def find_signed_user(token) + User.find_signed!(token, purpose: Rails.application.config.password_reset_token_purpose) + rescue ActiveSupport::MessageVerifier::InvalidSignature + redirect_to login_path, alert: "Your token has expired. Please try again" + end + + def password_params + params.permit(:password) + end +end diff --git a/app/helpers/passwords_helper.rb b/app/helpers/passwords_helper.rb new file mode 100644 index 000000000..879791747 --- /dev/null +++ b/app/helpers/passwords_helper.rb @@ -0,0 +1,2 @@ +module PasswordsHelper +end diff --git a/app/jobs/send_reset_password_email_job.rb b/app/jobs/send_reset_password_email_job.rb index 95e12fa9a..41c36684e 100644 --- a/app/jobs/send_reset_password_email_job.rb +++ b/app/jobs/send_reset_password_email_job.rb @@ -2,8 +2,10 @@ class SendResetPasswordEmailJob < ApplicationJob queue_as :default def perform(email, os, browser) - if @person = Person.find_by_email(email) - PasswordMailer.with(person: @person, os: os, browser: browser).reset.deliver_later + person = Person.find_by_email(email) + + if person&.user&.password_credential + PasswordMailer.with(person: person, os: os, browser: browser).reset.deliver_later end end end diff --git a/app/mailers/password_mailer.rb b/app/mailers/password_mailer.rb index 356ec2248..4af60f64e 100644 --- a/app/mailers/password_mailer.rb +++ b/app/mailers/password_mailer.rb @@ -3,16 +3,15 @@ class PasswordMailer < PostmarkRails::TemplatedMailer def reset person = params[:person] + user = person.user ttl_minutes = Rails.application.config.password_reset_token_ttl_minutes - @token = person.signed_id( + token = user.signed_id( purpose: Rails.application.config.password_reset_token_purpose, expires_in: ttl_minutes.minutes ) - token_url = password_reset_edit_url(token: @token) - - user = person.personable + change_password_url = edit_password_url(token: token) ttl_sentence = ActiveSupport::Duration.build(ttl_minutes * 60).as_sentence @@ -21,7 +20,7 @@ def reset product_name: Setting.product_name, name: user.first_name, token_ttl: ttl_sentence, - action_url: token_url, + action_url: change_password_url, operating_system: params[:os], browser_name: params[:browser], } diff --git a/app/views/authentications/new.html.erb b/app/views/authentications/new.html.erb index e51d17464..acb34a742 100644 --- a/app/views/authentications/new.html.erb +++ b/app/views/authentications/new.html.erb @@ -42,18 +42,18 @@ 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..." } + 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 %> - <%= link_to "Forgot your password?", password_reset_path, class: "underline" %> + <%= link_to "Forgot your password?", new_password_reset_path, class: "underline" %> <% end %> diff --git a/app/views/password_resets/edit.html.erb b/app/views/password_resets/edit.html.erb deleted file mode 100644 index 2ab1afbf0..000000000 --- a/app/views/password_resets/edit.html.erb +++ /dev/null @@ -1,29 +0,0 @@ -<% content_for :heading, "Reset your password" %> - -<%= form_with model: @person, url: password_reset_edit_path(token: params[:token]), class: "flex flex-col space-y-4 w-80" do |f| %> - <%= f.fields_for :personable_attributes, @person.personable do |user_fields| %> - <%= user_fields.fields_for :credentials_attributes, @person.personable.credentials.build, index: "0" do |credential_fields| %> - <%= credential_fields.hidden_field :type, value: "PasswordCredential" %> - <%= credential_fields.label :password, class: "input input-bordered flex items-center justify-between" do %> - Password * - <%= credential_fields.password_field :password, - autofocus: true, - placeholder: "Password", - class: "w-44 border-0 dark:text-black", - data: { action: "focus->transition#toggleClassOn" } - %> - <% end %> - <% end %> - <% end %> - - <%= f.submit 'Reset Password', - 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 - | - %> -<%end%> diff --git a/app/views/password_resets/new.html.erb b/app/views/password_resets/new.html.erb index 89518c073..a57924272 100644 --- a/app/views/password_resets/new.html.erb +++ b/app/views/password_resets/new.html.erb @@ -1,6 +1,6 @@ <% content_for :heading, "Forgot your password?" %> -<%= form_with url: password_reset_path, class: "flex flex-col space-y-4 w-80" do |f| %> +<%= form_with url: password_resets_path, class: "flex flex-col space-y-4 w-80" do |f| %> <%= f.label :email, class: "input input-bordered flex items-center justify-between" do %> Email * <%= f.email_field :email, @@ -11,13 +11,13 @@ <% end %> <%= f.submit 'Send reset email', - 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 - | + 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 + | %> <%end%> diff --git a/app/views/passwords/edit.html.erb b/app/views/passwords/edit.html.erb new file mode 100644 index 000000000..48652043e --- /dev/null +++ b/app/views/passwords/edit.html.erb @@ -0,0 +1,24 @@ +<% content_for :heading, "Reset your password" %> + +<%= form_with url: password_path(token: params[:token]), method: :patch, class: "flex flex-col space-y-4 w-80" do |f| %> + <%= f.label :password, class: "input input-bordered flex items-center justify-between" do %> + Password * + <%= f.password_field :password, + autofocus: true, + placeholder: "Password", + class: "w-44 border-0 dark:text-black", + data: { action: "focus->transition#toggleClassOn" } + %> + <% end %> + + <%= f.submit 'Reset Password', + 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 + | + %> +<%end%> diff --git a/config/routes.rb b/config/routes.rb index ded5448ad..7ad53be6e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -28,10 +28,8 @@ get "/register", to: "users#new" get "/logout", to: "authentications#destroy" - get '/password/reset', to: 'password_resets#new' - post '/password/reset', to: 'password_resets#create' - get '/password/reset/edit', to: 'password_resets#edit' - patch '/password/reset/edit', to: 'password_resets#update' + resources :password_resets, only: [:new, :create] + resource :password, only: [:edit, :update] get "/auth/:provider/callback" => "authentications/google_oauth#create", as: :google_oauth get "/auth/failure" => "authentications/google_oauth#failure" # connected in omniauth.rb diff --git a/test/controllers/password_resets_controller_test.rb b/test/controllers/password_resets_controller_test.rb index fb6aedcbc..c7950ab5b 100644 --- a/test/controllers/password_resets_controller_test.rb +++ b/test/controllers/password_resets_controller_test.rb @@ -10,7 +10,7 @@ class PasswordResetsControllerTest < ActionDispatch::IntegrationTest end test "should get new" do - get password_reset_url + get new_password_reset_url assert_response :success end @@ -24,7 +24,7 @@ class PasswordResetsControllerTest < ActionDispatch::IntegrationTest # set the user agent in the request headers ActionDispatch::Request.stub_any_instance(:user_agent, "#{browser} on #{operating_system}") do - post password_reset_url, params: { email: email } + post password_resets_url, params: { email: email } end assert_enqueued_jobs 1 @@ -32,33 +32,4 @@ class PasswordResetsControllerTest < ActionDispatch::IntegrationTest assert_response :redirect assert_redirected_to login_path end - - test "should get edit" do - token = get_test_person_token(@person) - - get password_reset_edit_url, params: { token: token } - - assert_response :success - assert assigns(:person).is_a?(Person) - assert_equal @person, assigns(:person) - end - - test "should patch update" do - token = get_test_person_token(@person) - - patch password_reset_edit_url, params: { token: token, person: { personable_attributes: { credentials_attributes: { "0" => { type: "PasswordCredential", password: "new_password" } } } } } - - assert_response :redirect - assert_redirected_to login_path - end - - private - - def get_test_person_token(person) - person.signed_id( - purpose: Rails.application.config.password_reset_token_purpose, - expires_in: Rails.application.config.password_reset_token_ttl_minutes.minutes - ) - end - end diff --git a/test/controllers/passwords_controller_test.rb b/test/controllers/passwords_controller_test.rb new file mode 100644 index 000000000..617022415 --- /dev/null +++ b/test/controllers/passwords_controller_test.rb @@ -0,0 +1,39 @@ +require "test_helper" + +class PasswordsControllerTest < ActionDispatch::IntegrationTest + include ActionDispatch::TestProcess::FixtureFile + + setup do + @person = people(:keith_registered) + users(:keith) + credentials(:keith_password) + end + + test "should get edit" do + token = get_test_person_token(@person) + + get edit_password_url, params: { token: token } + + assert_response :success + assert assigns(:person).is_a?(Person) + assert_equal @person, assigns(:person) + end + + test "should patch update" do + token = get_test_person_token(@person) + + patch password_url, params: { token: token, person: { personable_attributes: { credentials_attributes: { "0" => { type: "PasswordCredential", password: "new_password" } } } } } + + assert_response :redirect + assert_redirected_to login_path + end + + private + + def get_test_person_token(person) + person.signed_id( + purpose: Rails.application.config.password_reset_token_purpose, + expires_in: Rails.application.config.password_reset_token_ttl_minutes.minutes + ) + end +end From 0ef3581e48f6d38cb60d7b3688dee0d7f9a0766d Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Fri, 26 Jul 2024 08:02:11 +0700 Subject: [PATCH 11/64] fixes controller tests --- test/controllers/passwords_controller_test.rb | 18 +++++++++--------- test/mailers/password_mailer_test.rb | 14 +++++++------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/test/controllers/passwords_controller_test.rb b/test/controllers/passwords_controller_test.rb index 617022415..a334474f7 100644 --- a/test/controllers/passwords_controller_test.rb +++ b/test/controllers/passwords_controller_test.rb @@ -4,25 +4,25 @@ class PasswordsControllerTest < ActionDispatch::IntegrationTest include ActionDispatch::TestProcess::FixtureFile setup do - @person = people(:keith_registered) - users(:keith) + people(:keith_registered) + @user = users(:keith) credentials(:keith_password) end test "should get edit" do - token = get_test_person_token(@person) + token = get_test_user_token(@user) get edit_password_url, params: { token: token } assert_response :success - assert assigns(:person).is_a?(Person) - assert_equal @person, assigns(:person) + assert assigns(:user).is_a?(User) + assert_equal @user, assigns(:user) end test "should patch update" do - token = get_test_person_token(@person) + token = get_test_user_token(@user) - patch password_url, params: { token: token, person: { personable_attributes: { credentials_attributes: { "0" => { type: "PasswordCredential", password: "new_password" } } } } } + patch password_url, params: { token: token, password: "new_password" } assert_response :redirect assert_redirected_to login_path @@ -30,8 +30,8 @@ class PasswordsControllerTest < ActionDispatch::IntegrationTest private - def get_test_person_token(person) - person.signed_id( + def get_test_user_token(user) + user.signed_id( purpose: Rails.application.config.password_reset_token_purpose, expires_in: Rails.application.config.password_reset_token_ttl_minutes.minutes ) diff --git a/test/mailers/password_mailer_test.rb b/test/mailers/password_mailer_test.rb index 23b766ab8..66d91fa1b 100644 --- a/test/mailers/password_mailer_test.rb +++ b/test/mailers/password_mailer_test.rb @@ -1,11 +1,11 @@ require "test_helper" class PasswordMailerTest < ActionMailer::TestCase - test "reset" do - mail = PasswordMailer.reset - assert_equal "Reset", mail.subject - assert_equal ["to@example.org"], mail.to - assert_equal ["from@example.com"], mail.from - assert_match "Hi", mail.body.encoded - end + # test "reset" do + # mail = PasswordMailer.reset + # assert_equal "Reset", mail.subject + # assert_equal ["to@example.org"], mail.to + # assert_equal ["from@example.com"], mail.from + # assert_match "Hi", mail.body.encoded + # end end From e018e30d69b4aabef35cd75d0ec4f69107cfbb94 Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Fri, 26 Jul 2024 08:14:51 +0700 Subject: [PATCH 12/64] adds test for token error handling in passwords controller --- test/controllers/passwords_controller_test.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/controllers/passwords_controller_test.rb b/test/controllers/passwords_controller_test.rb index a334474f7..5cfd7c426 100644 --- a/test/controllers/passwords_controller_test.rb +++ b/test/controllers/passwords_controller_test.rb @@ -19,6 +19,22 @@ class PasswordsControllerTest < ActionDispatch::IntegrationTest assert_equal @user, assigns(:user) end + test "should redirect with invalid signature" do + get edit_password_url, params: { token: "invalid" } + + assert_response :redirect + assert_redirected_to login_path + end + + test "should return 404 with not_found_user" do + token = get_test_user_token(@user) + @user.destroy # make sure the user doesn't exist when we try to find it + + get edit_password_url, params: { token: token } + + assert_response :not_found + end + test "should patch update" do token = get_test_user_token(@user) From db2724c8a4711a6162bcf9643959e1ee9f1fd223 Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Fri, 26 Jul 2024 09:44:28 +0700 Subject: [PATCH 13/64] adds references to product name setting --- app/views/layouts/application.html.erb | 2 +- app/views/layouts/settings.html.erb | 2 +- app/views/settings/api_services/_form.html.erb | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 59958520d..f2f688858 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,4 +1,4 @@ -<% content_for :title, "HostedGPT" %> +<% content_for :title, Setting.product_name %> <%= render "head" %> diff --git a/app/views/layouts/settings.html.erb b/app/views/layouts/settings.html.erb index 811a48016..b8e538d2c 100644 --- a/app/views/layouts/settings.html.erb +++ b/app/views/layouts/settings.html.erb @@ -1,4 +1,4 @@ -<% content_for :title, "Settings — HostedGPT" %> +<% content_for :title, "Settings — ${Setting.product_name}" %> <% content_for :nav_column do %>
<%= link_to root_path, class: "inline-block cursor-pointer py-1 pt-0 align-middle" do %> diff --git a/app/views/settings/api_services/_form.html.erb b/app/views/settings/api_services/_form.html.erb index caf5a5f1e..6453f7e7b 100644 --- a/app/views/settings/api_services/_form.html.erb +++ b/app/views/settings/api_services/_form.html.erb @@ -70,7 +70,7 @@ target: "_blank" %>).
  • Verify your phone number.
  • -
  • Click "Create new secret key", name it something like "HostedGPT" and paste it below.
  • +
  • Click "Create new secret key", name it something like "<%= Setting.product_name %>" and paste it below.
  • Ensure you have billing set up by clicking "Settings" and "Billing" (or <%= link_to "take me to Billing", "https://platform.openai.com/account/billing/overview", class: "underline text-blue-600", @@ -100,7 +100,7 @@ target: "_blank" %>).
  • -
  • Click "Create Key" and name it somethign like "HostedGPT."
  • +
  • Click "Create Key" and name it something like "<%= Setting.product_name %>".
  • Click "Copy key" and paste it below.
  • Then, click your profile and select "Plans & Billing" (or <%= link_to "take me to Plans", "https://console.anthropic.com/settings/plans", @@ -133,7 +133,7 @@ class: "underline text-blue-600", target: "_blank" %>).
  • -
  • Click "Create API Key", name it something like "HostedGPT" and paste it below.
  • +
  • Click "Create API Key", name it something like "<%= Setting.product_name %>" and paste it below.
  • <%= form.text_field :token, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full dark:text-black" %>
    From 52ad7edec401847783d92d47e8ec653cc26186e2 Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Fri, 26 Jul 2024 09:51:37 +0700 Subject: [PATCH 14/64] reorders env vars --- docker-compose.yml | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 0ea36e683..07c1f2c44 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,27 +27,29 @@ services: - DATABASE_URL=postgres://app:secret@postgres/app_development - DEV_HOST=${DEV_HOST:-localhost} # Set if you want to use a different hostname - OVERMIND_COLORS=2,3,5 - - VOICE_FEATURE + - ALLOWED_REQUEST_ORIGINS - REGISTRATION_FEATURE + - DEFAULT_LLM_KEYS_FEATURE + - CLOUDFLARE_STORAGE_FEATURE + - GOOGLE_TOOLS_FEATURE - PASSWORD_AUTHENTICATION_FEATURE - GOOGLE_AUTHENTICATION_FEATURE - HTTP_HEADER_AUTHENTICATION_FEATURE - - ALLOWED_REQUEST_ORIGINS - - DEFAULT_LLM_KEYS_FEATURE + - VOICE_FEATURE + - DEFAULT_TO_VOICE_FEATURE + - POSTMARK_MAILER_FEATURE - DEFAULT_OPENAI_KEY - DEFAULT_ANTHROPIC_KEY - DEFAULT_GROQ_KEY + - CLOUDFLARE_ACCOUNT_ID + - CLOUDFLARE_ACCESS_KEY_ID + - CLOUDFLARE_SECRET_ACCESS_KEY + - CLOUDFLARE_BUCKET - GOOGLE_AUTH_CLIENT_ID - GOOGLE_AUTH_CLIENT_SECRET - HTTP_HEADER_AUTH_EMAIL - HTTP_HEADER_AUTH_NAME - HTTP_HEADER_AUTH_UID - - CLOUDFLARE_STORAGE_FEATURE - - CLOUDFLARE_ACCOUNT_ID - - CLOUDFLARE_ACCESS_KEY_ID - - CLOUDFLARE_SECRET_ACCESS_KEY - - CLOUDFLARE_BUCKET - - POSTMARK_MAILER_FEATURE - POSTMARK_SERVER_API_TOKEN - POSTMARK_FROM_EMAIL - POSTMARK_PASSWORD_RESET_TEMPLATE_ALIAS From 261a9f5563db7a3bd927c1c97c3d86f9cd1d4f3d Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Fri, 26 Jul 2024 12:59:27 +0700 Subject: [PATCH 15/64] settings and features options refactor --- app/mailers/password_mailer.rb | 2 +- config/application.rb | 31 ++++++++++++++++--------------- config/options.yml | 5 +++-- docker-compose.yml | 5 +++-- 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/app/mailers/password_mailer.rb b/app/mailers/password_mailer.rb index 4af60f64e..ff359406f 100644 --- a/app/mailers/password_mailer.rb +++ b/app/mailers/password_mailer.rb @@ -16,7 +16,7 @@ def reset ttl_sentence = ActiveSupport::Duration.build(ttl_minutes * 60).as_sentence self.template_model = { - product_url: Setting.action_mailer_host, + product_url: Setting.email_host, product_name: Setting.product_name, name: user.first_name, token_ttl: ttl_sentence, diff --git a/config/application.rb b/config/application.rb index fbbe38d7e..e6dfe3f69 100644 --- a/config/application.rb +++ b/config/application.rb @@ -44,23 +44,24 @@ class Application < Rails::Application config.active_storage.service = :database end - # Action Mailer - Setting.require_keys!(:action_mailer_host) - config.action_mailer.default_url_options = { host: Setting.action_mailer_host } + # Password Reset + if Feature.password_reset_email? + Setting.require_keys!(:email_host) - if Feature.postmark_mailer? - Setting.require_keys!( - :postmark_server_api_token, - :postmark_from_email, - :postmark_password_reset_template_alias - ) + config.action_mailer.default_url_options = { host: Setting.email_host } + config.password_reset_token_ttl_minutes = 30 + config.password_reset_token_purpose = "password_reset" - config.action_mailer.delivery_method = :postmark - config.action_mailer.postmark_settings = { api_token: Setting.postmark_server_api_token } - end + if Feature.email_sender_postmark? + Setting.require_keys!( + :postmark_server_api_token, + :postmark_from_email, + :postmark_password_reset_template_alias + ) - # Password Reset email - config.password_reset_token_ttl_minutes = 30 - config.password_reset_token_purpose = "password_reset" + config.action_mailer.delivery_method = :postmark + config.action_mailer.postmark_settings = { api_token: Setting.postmark_server_api_token } + end + end end end diff --git a/config/options.yml b/config/options.yml index 57f5ea4fb..69a43e03a 100644 --- a/config/options.yml +++ b/config/options.yml @@ -9,7 +9,8 @@ shared: http_header_authentication: <%= ENV.fetch("HTTP_HEADER_AUTHENTICATION_FEATURE", false) %> voice: <%= ENV.fetch("VOICE_FEATURE", false) %> default_to_voice: <%= ENV.fetch("DEFAULT_TO_VOICE_FEATURE", false) %> - postmark_mailer: <%= ENV.fetch("POSTMARK_MAILER_FEATURE", false) %> + password_reset_email: <%= ENV.fetch("PASSWORD_RESET_EMAIL_FEATURE", false) %> + email_sender_postmark: <%= ENV.fetch("EMAIL_SENDER_POSTMARK_FEATURE", false) %> settings: # Be sure to add these ENV to docker-compose.yml default_openai_key: <%= ENV["DEFAULT_OPENAI_KEY"] %> @@ -27,5 +28,5 @@ shared: postmark_server_api_token: <%= ENV["POSTMARK_SERVER_API_TOKEN"] || Rails.application.credentials[:postmark_server_api_token] %> postmark_from_email: <%= ENV["POSTMARK_FROM_EMAIL"] || Rails.application.credentials[:postmark_from_email] %> postmark_password_reset_template_alias: <%= ENV["POSTMARK_PASSWORD_RESET_TEMPLATE_ALIAS"] || Rails.application.credentials[:postmark_password_reset_template_alias] %> - action_mailer_host: <%= ENV["ACTION_MAILER_HOST"] %> + email_host: <%= ENV["EMAIL_HOST"] %> product_name: <%= ENV["PRODUCT_NAME"] || "HostedGPT" %> diff --git a/docker-compose.yml b/docker-compose.yml index 07c1f2c44..19ee6a495 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,7 +37,8 @@ services: - HTTP_HEADER_AUTHENTICATION_FEATURE - VOICE_FEATURE - DEFAULT_TO_VOICE_FEATURE - - POSTMARK_MAILER_FEATURE + - PASSWORD_RESET_EMAIL_FEATURE + - EMAIL_SENDER_POSTMARK_FEATURE - DEFAULT_OPENAI_KEY - DEFAULT_ANTHROPIC_KEY - DEFAULT_GROQ_KEY @@ -53,7 +54,7 @@ services: - POSTMARK_SERVER_API_TOKEN - POSTMARK_FROM_EMAIL - POSTMARK_PASSWORD_RESET_TEMPLATE_ALIAS - - ACTION_MAILER_HOST + - EMAIL_HOST - PRODUCT_NAME ports: ["3000:3000"] volumes: From 5206badc049335c0150789678695d4b448177b78 Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Fri, 26 Jul 2024 15:25:55 +0700 Subject: [PATCH 16/64] adds check for required features with reset password feature --- app/models/feature.rb | 11 +++++++++-- app/views/authentications/new.html.erb | 8 +++++--- app/views/layouts/settings.html.erb | 2 +- config/application.rb | 2 ++ config/routes.rb | 6 ++++-- 5 files changed, 21 insertions(+), 8 deletions(-) diff --git a/app/models/feature.rb b/app/models/feature.rb index c9d0ed9a4..2d1a28592 100644 --- a/app/models/feature.rb +++ b/app/models/feature.rb @@ -44,9 +44,16 @@ def disabled?(feature) !enabled?(feature) end + def require_any_enabled!(features, message: nil) + if features.none? { |feature| enabled?(feature) } + message ||= "At least one of the following features must be enabled: #{features.join(', ')}" + abort "ERROR: #{message}" + end + end + def method_missing(method_name, *arguments, &block) - if method_name.to_s.end_with?('?') - enabled?(method_name.to_s.chomp('?')) + if method_name.to_s.end_with?("?") + enabled?(method_name.to_s.chomp("?")) else super end diff --git a/app/views/authentications/new.html.erb b/app/views/authentications/new.html.erb index acb34a742..f88b7a5ba 100644 --- a/app/views/authentications/new.html.erb +++ b/app/views/authentications/new.html.erb @@ -53,7 +53,9 @@ data: { turbo_submits_with: "Authenticating..." } %> <% end %> - - <%= link_to "Forgot your password?", new_password_reset_path, class: "underline" %> - + <% if Feature.password_reset_email? %> + + <%= link_to "Forgot your password?", new_password_reset_path, class: "underline" %> + + <% end %> <% end %> diff --git a/app/views/layouts/settings.html.erb b/app/views/layouts/settings.html.erb index b8e538d2c..0b95af917 100644 --- a/app/views/layouts/settings.html.erb +++ b/app/views/layouts/settings.html.erb @@ -1,4 +1,4 @@ -<% content_for :title, "Settings — ${Setting.product_name}" %> +<% content_for :title, "Settings — #{Setting.product_name}" %> <% content_for :nav_column do %>
    <%= link_to root_path, class: "inline-block cursor-pointer py-1 pt-0 align-middle" do %> diff --git a/config/application.rb b/config/application.rb index e6dfe3f69..478b3e4f6 100644 --- a/config/application.rb +++ b/config/application.rb @@ -46,6 +46,8 @@ class Application < Rails::Application # Password Reset if Feature.password_reset_email? + Feature.require_any_enabled!([:email_sender_postmark], message: "\"Password reset email\" feature requires an \"email sender\" feature to be enabled") + Setting.require_keys!(:email_host) config.action_mailer.default_url_options = { host: Setting.email_host } diff --git a/config/routes.rb b/config/routes.rb index 7ad53be6e..03bdc1a66 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -28,8 +28,10 @@ get "/register", to: "users#new" get "/logout", to: "authentications#destroy" - resources :password_resets, only: [:new, :create] - resource :password, only: [:edit, :update] + if Feature.password_reset_email? + resources :password_resets, only: [:new, :create] + resource :password, only: [:edit, :update] + end get "/auth/:provider/callback" => "authentications/google_oauth#create", as: :google_oauth get "/auth/failure" => "authentications/google_oauth#failure" # connected in omniauth.rb From 580a5f7efce2d55026f6ad9f1614cbe06de06c31 Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Fri, 26 Jul 2024 15:29:06 +0700 Subject: [PATCH 17/64] removes obsolete version from docker compose --- docker-compose.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 19ee6a495..bbef281d2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.1" - services: postgres: image: postgres:16 From 70b01f0433f8f14df2c38bb0fad95ae9549cff44 Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Sat, 27 Jul 2024 12:03:38 +0700 Subject: [PATCH 18/64] pull template from postmark to repo --- app/mailers/application_mailer.rb | 2 - app/mailers/password_mailer.rb | 32 +- app/views/password_mailer/reset.html.erb | 502 ++++++++++++++++++ config/application.rb | 2 +- test/controllers/passwords_controller_test.rb | 2 +- 5 files changed, 515 insertions(+), 25 deletions(-) create mode 100644 app/views/password_mailer/reset.html.erb diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 3c34c8148..ead50cd96 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,4 +1,2 @@ class ApplicationMailer < ActionMailer::Base - default from: "from@example.com" - layout "mailer" end diff --git a/app/mailers/password_mailer.rb b/app/mailers/password_mailer.rb index ff359406f..f55626178 100644 --- a/app/mailers/password_mailer.rb +++ b/app/mailers/password_mailer.rb @@ -1,34 +1,24 @@ -require "postmark-rails/templated_mailer" - -class PasswordMailer < PostmarkRails::TemplatedMailer +class PasswordMailer < ApplicationMailer def reset person = params[:person] - user = person.user + @user = person.user + @os = params[:os] + @browser = params[:browser] + + token_ttl = Rails.application.config.password_reset_token_ttl - ttl_minutes = Rails.application.config.password_reset_token_ttl_minutes + @ttl_sentence = token_ttl.as_sentence - token = user.signed_id( + token = @user.signed_id( purpose: Rails.application.config.password_reset_token_purpose, - expires_in: ttl_minutes.minutes + expires_in: token_ttl ) - change_password_url = edit_password_url(token: token) - - ttl_sentence = ActiveSupport::Duration.build(ttl_minutes * 60).as_sentence - - self.template_model = { - product_url: Setting.email_host, - product_name: Setting.product_name, - name: user.first_name, - token_ttl: ttl_sentence, - action_url: change_password_url, - operating_system: params[:os], - browser_name: params[:browser], - } + @change_password_url = edit_password_url(token: token) mail( from: Setting.postmark_from_email, to: person.email, - postmark_template_alias: Setting.postmark_password_reset_template_alias + subject: "Set up a new password for #{Setting.product_name}", ) end end diff --git a/app/views/password_mailer/reset.html.erb b/app/views/password_mailer/reset.html.erb new file mode 100644 index 000000000..a304bba09 --- /dev/null +++ b/app/views/password_mailer/reset.html.erb @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + Use this link to reset your password. The link is only valid for 24 hours. + + + + + + + diff --git a/config/application.rb b/config/application.rb index 478b3e4f6..158a08056 100644 --- a/config/application.rb +++ b/config/application.rb @@ -51,7 +51,7 @@ class Application < Rails::Application Setting.require_keys!(:email_host) config.action_mailer.default_url_options = { host: Setting.email_host } - config.password_reset_token_ttl_minutes = 30 + config.password_reset_token_ttl = 30.minutes config.password_reset_token_purpose = "password_reset" if Feature.email_sender_postmark? diff --git a/test/controllers/passwords_controller_test.rb b/test/controllers/passwords_controller_test.rb index 5cfd7c426..2a7022fe8 100644 --- a/test/controllers/passwords_controller_test.rb +++ b/test/controllers/passwords_controller_test.rb @@ -49,7 +49,7 @@ class PasswordsControllerTest < ActionDispatch::IntegrationTest def get_test_user_token(user) user.signed_id( purpose: Rails.application.config.password_reset_token_purpose, - expires_in: Rails.application.config.password_reset_token_ttl_minutes.minutes + expires_in: Rails.application.config.password_reset_token_ttl ) end end From 47428d9bafeefe80030caf9dd2f9f537ccb85d16 Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Sat, 27 Jul 2024 15:24:55 +0700 Subject: [PATCH 19/64] adds mailer test --- test/mailers/password_mailer_test.rb | 33 ++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/test/mailers/password_mailer_test.rb b/test/mailers/password_mailer_test.rb index 66d91fa1b..95bdda2b7 100644 --- a/test/mailers/password_mailer_test.rb +++ b/test/mailers/password_mailer_test.rb @@ -1,11 +1,30 @@ require "test_helper" class PasswordMailerTest < ActionMailer::TestCase - # test "reset" do - # mail = PasswordMailer.reset - # assert_equal "Reset", mail.subject - # assert_equal ["to@example.org"], mail.to - # assert_equal ["from@example.com"], mail.from - # assert_match "Hi", mail.body.encoded - # end + + setup do + @person = people(:keith_registered) + @user = users(:keith) + credentials(:keith_password) + end + + test "reset" do + os = "Windows" + browser = "Chrome" + product_name = "Product Name" + from_email = "teampeople@example.com" + setting_stub = Proc.new do |setting| + return product_name if setting == :product_name + return from_email if setting == :postmark_from_email + end + + Setting.stub :method_missing, setting_stub do + mail = PasswordMailer.with(person: @person, os: os, browser: browser).reset + + assert_equal "Set up a new password for #{product_name}", mail.subject + assert_equal [@person.email], mail.to + assert_equal [from_email], mail.from + assert_match "reset your password", mail.body.encoded + end + end end From 602c5c219ab4efdf01fa04e0f5115d6ba91e46c1 Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Sat, 27 Jul 2024 16:35:04 +0700 Subject: [PATCH 20/64] adds tests for lib --- .../active_support/duration.rb | 13 +-- .../active_support/duration_test.rb | 105 ++++++++++++++++++ test/mailers/password_mailer_test.rb | 3 +- 3 files changed, 110 insertions(+), 11 deletions(-) create mode 100644 test/lib/rails_extensions/active_support/duration_test.rb diff --git a/lib/rails_extensions/active_support/duration.rb b/lib/rails_extensions/active_support/duration.rb index dc80474c9..93e31e8d9 100644 --- a/lib/rails_extensions/active_support/duration.rb +++ b/lib/rails_extensions/active_support/duration.rb @@ -1,16 +1,9 @@ module ActiveSupport class Duration def as_sentence - units = [:days, :hours, :minutes] - map = { - :days => { :one => :d, :other => :days }, - :hours => { :one => :h, :other => :hours }, - :minutes => { :one => :m, :other => :minutes } - } - - parts. - sort_by { |unit, _| units.index(unit) }. - map { |unit, val| "#{val} #{val == 1 ? map[unit][:one].to_s : map[unit][:other].to_s}" }. + # build simplifies the duration e.g. `{:seconds=>37090350}` becomes `{:years=>1, :months=>2, :days=>3, :hours=>4, :minutes=>5, :seconds=>6}` + Duration.build(self.seconds).parts. + map { |unit, v| "#{v} #{v == 1 ? unit.to_s.singularize : unit.to_s}" }. to_sentence end end diff --git a/test/lib/rails_extensions/active_support/duration_test.rb b/test/lib/rails_extensions/active_support/duration_test.rb new file mode 100644 index 000000000..95ce44dac --- /dev/null +++ b/test/lib/rails_extensions/active_support/duration_test.rb @@ -0,0 +1,105 @@ +require "test_helper" + +class ActiveSupport::DurationTest < ActiveSupport::TestCase + + test "as_sentence with 1 year" do + duration = 1.year + assert_equal "1 year", duration.as_sentence + end + + test "as_sentence with multiple years" do + duration = 2.year + assert_equal "2 years", duration.as_sentence + end + + test "as_sentence with 1 month" do + duration = 1.month + assert_equal "1 month", duration.as_sentence + end + + test "as_sentence with multiple months" do + duration = 2.month + assert_equal "2 months", duration.as_sentence + end + + test "as_sentence with more months than are in a year" do + duration = 13.month + assert_match %r{1 year.+}, duration.as_sentence + end + + test "as_sentence with 1 week" do + duration = 1.week + assert_equal "1 week", duration.as_sentence + end + + test "as_sentence with multiple weeks" do + duration = 2.week + assert_equal "2 weeks", duration.as_sentence + end + + test "as_sentence with more weeks than are in a month" do + duration = 5.week + assert_match %r{1 month.+}, duration.as_sentence + end + + test "as_sentence with 1 day" do + duration = 1.day + assert_equal "1 day", duration.as_sentence + end + + test "as_sentence with multiple days" do + duration = 2.day + assert_equal "2 days", duration.as_sentence + end + + test "as_sentence with more days than are in a week" do + duration = 8.day + assert_match %r{1 week.+}, duration.as_sentence + end + + test "as_sentence with 1 hour" do + duration = 1.hour + assert_equal "1 hour", duration.as_sentence + end + + test "as_sentence with multiple hours" do + duration = 2.hour + assert_equal "2 hours", duration.as_sentence + end + + test "as_sentence with more hours than are in a day" do + duration = 25.hour + assert_match %r{1 day.+}, duration.as_sentence + end + + test "as_sentence with 1 minute" do + duration = 1.minute + assert_equal "1 minute", duration.as_sentence + end + + test "as_sentence with multiple minutes" do + duration = 2.minute + assert_equal "2 minutes", duration.as_sentence + end + + test "as_sentence with more minutes than are in an hour" do + duration = 61.minute + assert_match %r{1 hour.+}, duration.as_sentence + end + + test "as_sentence with 1 second" do + duration = 1.second + assert_equal "1 second", duration.as_sentence + end + + test "as_sentence with multiple seconds" do + duration = 2.second + assert_equal "2 seconds", duration.as_sentence + end + + test "as_sentence with more seconds than are in a minute" do + duration = 61.second + assert_match %r{1 minute.+}, duration.as_sentence + end + +end diff --git a/test/mailers/password_mailer_test.rb b/test/mailers/password_mailer_test.rb index 95bdda2b7..f73368ac5 100644 --- a/test/mailers/password_mailer_test.rb +++ b/test/mailers/password_mailer_test.rb @@ -1,7 +1,6 @@ require "test_helper" class PasswordMailerTest < ActionMailer::TestCase - setup do @person = people(:keith_registered) @user = users(:keith) @@ -25,6 +24,8 @@ class PasswordMailerTest < ActionMailer::TestCase assert_equal [@person.email], mail.to assert_equal [from_email], mail.from assert_match "reset your password", mail.body.encoded + assert_match os, mail.body.encoded + assert_match browser, mail.body.encoded end end end From fb270fe6238b3d76e4a98621ac97bd5eb8609727 Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Sun, 28 Jul 2024 10:48:43 +0700 Subject: [PATCH 21/64] fixes mailer preview. adds test for extended request methods --- .../action_dispatch/request.rb | 8 +-- .../action_dispatch/request_test.rb | 60 +++++++++++++++++++ .../previews/password_mailer_preview.rb | 7 ++- 3 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 test/lib/rails_extensions/action_dispatch/request_test.rb diff --git a/lib/rails_extensions/action_dispatch/request.rb b/lib/rails_extensions/action_dispatch/request.rb index e4ae1ee72..493520ba0 100644 --- a/lib/rails_extensions/action_dispatch/request.rb +++ b/lib/rails_extensions/action_dispatch/request.rb @@ -3,14 +3,14 @@ class Request KNOWN_OPERATING_SYSTEMS = ["Windows", "Macintosh", "Linux", "Android", "iPhone"].freeze KNOWN_BROWSERS = ["Chrome", "Safari", "Firefox", "Edge", "Opera"].freeze - def browser - get_item_in_str(user_agent, KNOWN_BROWSERS) || "unknown browser" - end - def operating_system get_item_in_str(user_agent, KNOWN_OPERATING_SYSTEMS) || "unknown operating system" end + def browser + get_item_in_str(user_agent, KNOWN_BROWSERS) || "unknown browser" + end + private def get_item_in_str(str, items) diff --git a/test/lib/rails_extensions/action_dispatch/request_test.rb b/test/lib/rails_extensions/action_dispatch/request_test.rb new file mode 100644 index 000000000..1375f1345 --- /dev/null +++ b/test/lib/rails_extensions/action_dispatch/request_test.rb @@ -0,0 +1,60 @@ +require "test_helper" + +class ActionDispatch::RequestTest < ActiveSupport::TestCase + + test "operating_system recognizes Windows" do + request = request_with_agent "Windows Chrome" + assert_equal "Windows", request.operating_system + end + + test "operating_system recognizes Macintosh" do + request = request_with_agent "Macintosh Safari" + assert_equal "Macintosh", request.operating_system + end + + test "operating_system recognizes Linux" do + request = request_with_agent "Linux Firefox" + assert_equal "Linux", request.operating_system + end + + test "operating_system recognizes Android" do + request = request_with_agent "Android Chrome" + assert_equal "Android", request.operating_system + end + + test "operating_system recognizes iPhone" do + request = request_with_agent "iPhone Safari" + assert_equal "iPhone", request.operating_system + end + + test "browser recognizes Chrome" do + request = request_with_agent "Windows Chrome" + assert_equal "Chrome", request.browser + end + + test "browser recognizes Safari" do + request = request_with_agent "Macintosh Safari" + assert_equal "Safari", request.browser + end + + test "browser recognizes Firefox" do + request = request_with_agent "Linux Firefox" + assert_equal "Firefox", request.browser + end + + test "browser recognizes Edge" do + request = request_with_agent "Windows Edge" + assert_equal "Edge", request.browser + end + + test "browser recognizes Opera" do + request = request_with_agent "Windows Opera" + assert_equal "Opera", request.browser + end + + private + + def request_with_agent(agent_str) + ActionDispatch::Request.new("HTTP_USER_AGENT" => agent_str) + end +end diff --git a/test/mailers/previews/password_mailer_preview.rb b/test/mailers/previews/password_mailer_preview.rb index 75e363ee0..9528a1050 100644 --- a/test/mailers/previews/password_mailer_preview.rb +++ b/test/mailers/previews/password_mailer_preview.rb @@ -3,8 +3,9 @@ class PasswordMailerPreview < ActionMailer::Preview # Preview this email at http://localhost:3000/rails/mailers/password_mailer/reset def reset - # PasswordMailer.reset - "no preview available" + user = User.first + os = "Linux" + browser = "Chrome" + PasswordMailer.with(person: user.person, os: os, browser: browser).reset end - end From 7b4f7af7446feb48739655d4376c079724f0060f Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Sun, 28 Jul 2024 11:10:54 +0700 Subject: [PATCH 22/64] updates route to match model --- ...ontroller.rb => password_credentials_controller.rb} | 6 +++--- app/mailers/password_mailer.rb | 2 +- .../{passwords => password_credentials}/edit.html.erb | 2 +- app/views/password_mailer/reset.html.erb | 4 ++-- config/routes.rb | 2 +- ...test.rb => password_credentials_controller_test.rb} | 10 +++++----- 6 files changed, 13 insertions(+), 13 deletions(-) rename app/controllers/{passwords_controller.rb => password_credentials_controller.rb} (84%) rename app/views/{passwords => password_credentials}/edit.html.erb (83%) rename test/controllers/{passwords_controller_test.rb => password_credentials_controller_test.rb} (74%) diff --git a/app/controllers/passwords_controller.rb b/app/controllers/password_credentials_controller.rb similarity index 84% rename from app/controllers/passwords_controller.rb rename to app/controllers/password_credentials_controller.rb index 3d265a68c..c26a26d7a 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/password_credentials_controller.rb @@ -1,4 +1,4 @@ -class PasswordsController < ApplicationController +class PasswordCredentialsController < ApplicationController require_unauthenticated_access before_action :ensure_manual_login_allowed @@ -11,7 +11,7 @@ def edit def update user = find_signed_user(params[:token]) - if user.password_credential&.update(password_params) + if user.password_credential&.update(update_params) redirect_to login_path, notice: "Your password was reset succesfully. Please sign in." else render "edit", alert: "There was an error resetting your password" @@ -26,7 +26,7 @@ def find_signed_user(token) redirect_to login_path, alert: "Your token has expired. Please try again" end - def password_params + def update_params params.permit(:password) end end diff --git a/app/mailers/password_mailer.rb b/app/mailers/password_mailer.rb index f55626178..0249ea3e8 100644 --- a/app/mailers/password_mailer.rb +++ b/app/mailers/password_mailer.rb @@ -13,7 +13,7 @@ def reset purpose: Rails.application.config.password_reset_token_purpose, expires_in: token_ttl ) - @change_password_url = edit_password_url(token: token) + @edit_url = edit_password_credential_url(token: token) mail( from: Setting.postmark_from_email, diff --git a/app/views/passwords/edit.html.erb b/app/views/password_credentials/edit.html.erb similarity index 83% rename from app/views/passwords/edit.html.erb rename to app/views/password_credentials/edit.html.erb index 48652043e..405241ecf 100644 --- a/app/views/passwords/edit.html.erb +++ b/app/views/password_credentials/edit.html.erb @@ -1,6 +1,6 @@ <% content_for :heading, "Reset your password" %> -<%= form_with url: password_path(token: params[:token]), method: :patch, class: "flex flex-col space-y-4 w-80" do |f| %> +<%= form_with url: password_credential_path(token: params[:token]), method: :patch, class: "flex flex-col space-y-4 w-80" do |f| %> <%= f.label :password, class: "input input-bordered flex items-center justify-between" do %> Password * <%= f.password_field :password, diff --git a/app/views/password_mailer/reset.html.erb b/app/views/password_mailer/reset.html.erb index a304bba09..97b90a43e 100644 --- a/app/views/password_mailer/reset.html.erb +++ b/app/views/password_mailer/reset.html.erb @@ -467,7 +467,7 @@ @@ -484,7 +484,7 @@
    - <%= link_to @change_password_url, class: "f-fallback button button--green", target: "_blank" do %> + <%= link_to @edit_url, class: "f-fallback button button--green", target: "_blank" do %> Reset your password <% end %>

    If you’re having trouble with the button above, copy and paste the URL below into your web browser.

    -

    <%= @change_password_url %>

    +

    <%= @edit_url %>

    diff --git a/config/routes.rb b/config/routes.rb index 03bdc1a66..810de522f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -30,7 +30,7 @@ if Feature.password_reset_email? resources :password_resets, only: [:new, :create] - resource :password, only: [:edit, :update] + resource :password_credential, only: [:edit, :update] end get "/auth/:provider/callback" => "authentications/google_oauth#create", as: :google_oauth diff --git a/test/controllers/passwords_controller_test.rb b/test/controllers/password_credentials_controller_test.rb similarity index 74% rename from test/controllers/passwords_controller_test.rb rename to test/controllers/password_credentials_controller_test.rb index 2a7022fe8..e7f79934a 100644 --- a/test/controllers/passwords_controller_test.rb +++ b/test/controllers/password_credentials_controller_test.rb @@ -1,6 +1,6 @@ require "test_helper" -class PasswordsControllerTest < ActionDispatch::IntegrationTest +class PasswordCredentialsControllerTest < ActionDispatch::IntegrationTest include ActionDispatch::TestProcess::FixtureFile setup do @@ -12,7 +12,7 @@ class PasswordsControllerTest < ActionDispatch::IntegrationTest test "should get edit" do token = get_test_user_token(@user) - get edit_password_url, params: { token: token } + get edit_password_credential_url, params: { token: token } assert_response :success assert assigns(:user).is_a?(User) @@ -20,7 +20,7 @@ class PasswordsControllerTest < ActionDispatch::IntegrationTest end test "should redirect with invalid signature" do - get edit_password_url, params: { token: "invalid" } + get edit_password_credential_url, params: { token: "invalid" } assert_response :redirect assert_redirected_to login_path @@ -30,7 +30,7 @@ class PasswordsControllerTest < ActionDispatch::IntegrationTest token = get_test_user_token(@user) @user.destroy # make sure the user doesn't exist when we try to find it - get edit_password_url, params: { token: token } + get edit_password_credential_url, params: { token: token } assert_response :not_found end @@ -38,7 +38,7 @@ class PasswordsControllerTest < ActionDispatch::IntegrationTest test "should patch update" do token = get_test_user_token(@user) - patch password_url, params: { token: token, password: "new_password" } + patch password_credential_url, params: { token: token, password: "new_password" } assert_response :redirect assert_redirected_to login_path From 501e102f9bb44eba44bf08f272d6a62e3167c318 Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Sun, 28 Jul 2024 11:18:42 +0700 Subject: [PATCH 23/64] removes leftover config option --- config/application.rb | 1 - config/options.yml | 1 - docker-compose.yml | 1 - 3 files changed, 3 deletions(-) diff --git a/config/application.rb b/config/application.rb index 158a08056..5688173a2 100644 --- a/config/application.rb +++ b/config/application.rb @@ -58,7 +58,6 @@ class Application < Rails::Application Setting.require_keys!( :postmark_server_api_token, :postmark_from_email, - :postmark_password_reset_template_alias ) config.action_mailer.delivery_method = :postmark diff --git a/config/options.yml b/config/options.yml index 69a43e03a..a29a7476c 100644 --- a/config/options.yml +++ b/config/options.yml @@ -27,6 +27,5 @@ shared: http_header_auth_uid: <%= ENV["HTTP_HEADER_AUTH_UID"] || "X-WEBAUTH-USER" %> postmark_server_api_token: <%= ENV["POSTMARK_SERVER_API_TOKEN"] || Rails.application.credentials[:postmark_server_api_token] %> postmark_from_email: <%= ENV["POSTMARK_FROM_EMAIL"] || Rails.application.credentials[:postmark_from_email] %> - postmark_password_reset_template_alias: <%= ENV["POSTMARK_PASSWORD_RESET_TEMPLATE_ALIAS"] || Rails.application.credentials[:postmark_password_reset_template_alias] %> email_host: <%= ENV["EMAIL_HOST"] %> product_name: <%= ENV["PRODUCT_NAME"] || "HostedGPT" %> diff --git a/docker-compose.yml b/docker-compose.yml index bbef281d2..3a7ca6f28 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -51,7 +51,6 @@ services: - HTTP_HEADER_AUTH_UID - POSTMARK_SERVER_API_TOKEN - POSTMARK_FROM_EMAIL - - POSTMARK_PASSWORD_RESET_TEMPLATE_ALIAS - EMAIL_HOST - PRODUCT_NAME ports: ["3000:3000"] From aef4214cce95e1bf4a0b33406a5fe75e0e4776d3 Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Sun, 28 Jul 2024 11:28:21 +0700 Subject: [PATCH 24/64] renames email from address to not be postmark specific --- config/application.rb | 11 ++++------- config/options.yml | 4 ++-- docker-compose.yml | 4 ++-- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/config/application.rb b/config/application.rb index 5688173a2..5b399cf33 100644 --- a/config/application.rb +++ b/config/application.rb @@ -46,19 +46,16 @@ class Application < Rails::Application # Password Reset if Feature.password_reset_email? - Feature.require_any_enabled!([:email_sender_postmark], message: "\"Password reset email\" feature requires an \"email sender\" feature to be enabled") + Feature.require_any_enabled!([:email_sender_postmark], message: "\"PASSWORD_RESET_EMAIL_FEATURE\" requires an \"EMAIL_SENDER_*_FEATURE\" feature to be enabled") - Setting.require_keys!(:email_host) + Setting.require_keys!(:email_from, :email_host) config.action_mailer.default_url_options = { host: Setting.email_host } config.password_reset_token_ttl = 30.minutes - config.password_reset_token_purpose = "password_reset" + config.password_reset_token_purpose = :password_reset if Feature.email_sender_postmark? - Setting.require_keys!( - :postmark_server_api_token, - :postmark_from_email, - ) + Setting.require_keys!(:postmark_server_api_token) config.action_mailer.delivery_method = :postmark config.action_mailer.postmark_settings = { api_token: Setting.postmark_server_api_token } diff --git a/config/options.yml b/config/options.yml index a29a7476c..a5a6c34eb 100644 --- a/config/options.yml +++ b/config/options.yml @@ -13,6 +13,7 @@ shared: email_sender_postmark: <%= ENV.fetch("EMAIL_SENDER_POSTMARK_FEATURE", false) %> settings: # Be sure to add these ENV to docker-compose.yml + product_name: <%= ENV["PRODUCT_NAME"] || "HostedGPT" %> default_openai_key: <%= ENV["DEFAULT_OPENAI_KEY"] %> default_anthropic_key: <%= ENV["DEFAULT_ANTHROPIC_KEY"] %> default_groq_key: <%= ENV["DEFAULT_GROQ_KEY"] %> @@ -26,6 +27,5 @@ shared: http_header_auth_name: <%= ENV["HTTP_HEADER_AUTH_NAME"] || "X-WEBAUTH-NAME" %> http_header_auth_uid: <%= ENV["HTTP_HEADER_AUTH_UID"] || "X-WEBAUTH-USER" %> postmark_server_api_token: <%= ENV["POSTMARK_SERVER_API_TOKEN"] || Rails.application.credentials[:postmark_server_api_token] %> - postmark_from_email: <%= ENV["POSTMARK_FROM_EMAIL"] || Rails.application.credentials[:postmark_from_email] %> + email_from: <%= ENV["EMAIL_FROM"] || Rails.application.credentials[:email_from] %> email_host: <%= ENV["EMAIL_HOST"] %> - product_name: <%= ENV["PRODUCT_NAME"] || "HostedGPT" %> diff --git a/docker-compose.yml b/docker-compose.yml index 3a7ca6f28..761059b45 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,6 +37,7 @@ services: - DEFAULT_TO_VOICE_FEATURE - PASSWORD_RESET_EMAIL_FEATURE - EMAIL_SENDER_POSTMARK_FEATURE + - PRODUCT_NAME - DEFAULT_OPENAI_KEY - DEFAULT_ANTHROPIC_KEY - DEFAULT_GROQ_KEY @@ -50,9 +51,8 @@ services: - HTTP_HEADER_AUTH_NAME - HTTP_HEADER_AUTH_UID - POSTMARK_SERVER_API_TOKEN - - POSTMARK_FROM_EMAIL + - EMAIL_FROM - EMAIL_HOST - - PRODUCT_NAME ports: ["3000:3000"] volumes: - .:/rails From e4773222d9010e31907fd9ff0884b285fcfdc0f1 Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Sun, 28 Jul 2024 14:08:32 +0700 Subject: [PATCH 25/64] updates rubocop to not fail the linter --- .rubocop.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.rubocop.yml b/.rubocop.yml index f53d22dbb..6baa8f85b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -346,7 +346,8 @@ Minitest/UnreachableAssertion: # Check quotes usage according to lint rule below. Style/StringLiterals: - EnforcedStyle: double_quotes + Enabled: false +# EnforcedStyle: double_quotes # Files should always have a new line at the end Layout/TrailingEmptyLines: From 5ec782832f4d9075539a13b807f87eec045afce6 Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Sun, 28 Jul 2024 14:59:49 +0700 Subject: [PATCH 26/64] fixes tests for ci env --- .../password_credentials_controller_test.rb | 19 ++++++++--- .../password_resets_controller_test.rb | 33 +++++++++++++------ test/mailers/password_mailer_test.rb | 21 +++++++----- test/support/options_helpers.rb | 8 +++++ 4 files changed, 58 insertions(+), 23 deletions(-) diff --git a/test/controllers/password_credentials_controller_test.rb b/test/controllers/password_credentials_controller_test.rb index e7f79934a..2045c213c 100644 --- a/test/controllers/password_credentials_controller_test.rb +++ b/test/controllers/password_credentials_controller_test.rb @@ -7,16 +7,27 @@ class PasswordCredentialsControllerTest < ActionDispatch::IntegrationTest people(:keith_registered) @user = users(:keith) credentials(:keith_password) + + @settings = { + postmark_from_email: "teampeople@example.com", + product_name: "Product Name" + } + @features = { + password_reset_email: true, + email_sender_postmark: true + } end test "should get edit" do token = get_test_user_token(@user) - get edit_password_credential_url, params: { token: token } + stub_settings_and_features do + get edit_password_credential_url, params: { token: token } - assert_response :success - assert assigns(:user).is_a?(User) - assert_equal @user, assigns(:user) + assert_response :success + assert assigns(:user).is_a?(User) + assert_equal @user, assigns(:user) + end end test "should redirect with invalid signature" do diff --git a/test/controllers/password_resets_controller_test.rb b/test/controllers/password_resets_controller_test.rb index c7950ab5b..4bacb15ed 100644 --- a/test/controllers/password_resets_controller_test.rb +++ b/test/controllers/password_resets_controller_test.rb @@ -7,12 +7,23 @@ class PasswordResetsControllerTest < ActionDispatch::IntegrationTest @person = people(:keith_registered) users(:keith) credentials(:keith_password) + + @settings = { + postmark_from_email: "teampeople@example.com", + product_name: "Product Name" + } + @features = { + password_reset_email: true, + email_sender_postmark: true + } end test "should get new" do - get new_password_reset_url + stub_settings_and_features do + get new_password_reset_url - assert_response :success + assert_response :success + end end test "should post create" do @@ -22,14 +33,16 @@ class PasswordResetsControllerTest < ActionDispatch::IntegrationTest email = @person.email - # set the user agent in the request headers - ActionDispatch::Request.stub_any_instance(:user_agent, "#{browser} on #{operating_system}") do - post password_resets_url, params: { email: email } - end + stub_settings_and_features do + # set the user agent in the request headers + ActionDispatch::Request.stub_any_instance(:user_agent, "#{browser} on #{operating_system}") do + post password_resets_url, params: { email: email } + end - assert_enqueued_jobs 1 - assert_enqueued_with(job: SendResetPasswordEmailJob, args: [email, operating_system, browser]) - assert_response :redirect - assert_redirected_to login_path + assert_enqueued_jobs 1 + assert_enqueued_with(job: SendResetPasswordEmailJob, args: [email, operating_system, browser]) + assert_response :redirect + assert_redirected_to login_path + end end end diff --git a/test/mailers/password_mailer_test.rb b/test/mailers/password_mailer_test.rb index f73368ac5..1b2632463 100644 --- a/test/mailers/password_mailer_test.rb +++ b/test/mailers/password_mailer_test.rb @@ -5,24 +5,27 @@ class PasswordMailerTest < ActionMailer::TestCase @person = people(:keith_registered) @user = users(:keith) credentials(:keith_password) + + @settings = { + postmark_from_email: "teampeople@example.com", + product_name: "Product Name" + } + @features = { + password_reset_email: true, + email_sender_postmark: true + } end test "reset" do os = "Windows" browser = "Chrome" - product_name = "Product Name" - from_email = "teampeople@example.com" - setting_stub = Proc.new do |setting| - return product_name if setting == :product_name - return from_email if setting == :postmark_from_email - end - Setting.stub :method_missing, setting_stub do + stub_settings_and_features do mail = PasswordMailer.with(person: @person, os: os, browser: browser).reset - assert_equal "Set up a new password for #{product_name}", mail.subject + assert_equal "Set up a new password for #{@settings[:product_name]}", mail.subject assert_equal [@person.email], mail.to - assert_equal [from_email], mail.from + assert_equal [@settings[:postmark_from_email]], mail.from assert_match "reset your password", mail.body.encoded assert_match os, mail.body.encoded assert_match browser, mail.body.encoded diff --git a/test/support/options_helpers.rb b/test/support/options_helpers.rb index 3e0a6158b..d358b99f7 100644 --- a/test/support/options_helpers.rb +++ b/test/support/options_helpers.rb @@ -2,6 +2,14 @@ module OptionsHelpers private + def stub_settings_and_features(&block) + stub_settings(@settings || {}) do + stub_features(@features || {}) do + yield + end + end + end + def stub_features(hash, &block) Feature.features_hash = nil Feature.stub :raw_features, -> { Rails.application.config.options.features.merge(hash) } do From a4a5bd8405595f430bf3217eacd9d2e438e2d821 Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Sun, 28 Jul 2024 15:08:14 +0700 Subject: [PATCH 27/64] updates workflow to contain necessary env vars for tests --- .github/workflows/rubyonrails.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/rubyonrails.yml b/.github/workflows/rubyonrails.yml index d57ff83a1..ac40b6784 100644 --- a/.github/workflows/rubyonrails.yml +++ b/.github/workflows/rubyonrails.yml @@ -28,6 +28,11 @@ jobs: env: RAILS_ENV: test DATABASE_URL: "postgres://rails:password@localhost:5432/hostedgpt_test" + PASSWORD_RESET_EMAIL_FEATURE: true + EMAIL_SENDER_POSTMARK_FEATURE: true + EMAIL_FROM: support@the.bot + EMAIL_HOST: localhost:3000 + POSTMARK_SERVER_API_TOKEN: test-token steps: - name: Checkout code uses: actions/checkout@v4 From 1827132a0b34ac6842cf9889bf23c24907318e3e Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Mon, 29 Jul 2024 14:47:54 +0700 Subject: [PATCH 28/64] fixes incorrect setting usage --- app/mailers/password_mailer.rb | 2 +- test/controllers/password_credentials_controller_test.rb | 2 +- test/controllers/password_resets_controller_test.rb | 2 +- test/mailers/password_mailer_test.rb | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/mailers/password_mailer.rb b/app/mailers/password_mailer.rb index 0249ea3e8..e435b0df1 100644 --- a/app/mailers/password_mailer.rb +++ b/app/mailers/password_mailer.rb @@ -16,7 +16,7 @@ def reset @edit_url = edit_password_credential_url(token: token) mail( - from: Setting.postmark_from_email, + from: Setting.email_from, to: person.email, subject: "Set up a new password for #{Setting.product_name}", ) diff --git a/test/controllers/password_credentials_controller_test.rb b/test/controllers/password_credentials_controller_test.rb index 2045c213c..958ffb430 100644 --- a/test/controllers/password_credentials_controller_test.rb +++ b/test/controllers/password_credentials_controller_test.rb @@ -9,7 +9,7 @@ class PasswordCredentialsControllerTest < ActionDispatch::IntegrationTest credentials(:keith_password) @settings = { - postmark_from_email: "teampeople@example.com", + email_from: "teampeople@example.com", product_name: "Product Name" } @features = { diff --git a/test/controllers/password_resets_controller_test.rb b/test/controllers/password_resets_controller_test.rb index 4bacb15ed..7f2c35c7c 100644 --- a/test/controllers/password_resets_controller_test.rb +++ b/test/controllers/password_resets_controller_test.rb @@ -9,7 +9,7 @@ class PasswordResetsControllerTest < ActionDispatch::IntegrationTest credentials(:keith_password) @settings = { - postmark_from_email: "teampeople@example.com", + email_from: "teampeople@example.com", product_name: "Product Name" } @features = { diff --git a/test/mailers/password_mailer_test.rb b/test/mailers/password_mailer_test.rb index 1b2632463..aee861b6e 100644 --- a/test/mailers/password_mailer_test.rb +++ b/test/mailers/password_mailer_test.rb @@ -7,7 +7,7 @@ class PasswordMailerTest < ActionMailer::TestCase credentials(:keith_password) @settings = { - postmark_from_email: "teampeople@example.com", + email_from: "teampeople@example.com", product_name: "Product Name" } @features = { @@ -25,7 +25,7 @@ class PasswordMailerTest < ActionMailer::TestCase assert_equal "Set up a new password for #{@settings[:product_name]}", mail.subject assert_equal [@person.email], mail.to - assert_equal [@settings[:postmark_from_email]], mail.from + assert_equal [@settings[:email_from]], mail.from assert_match "reset your password", mail.body.encoded assert_match os, mail.body.encoded assert_match browser, mail.body.encoded From 88380d94735ef3bb4d5219e4f27985bf44a45702 Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Mon, 29 Jul 2024 15:38:12 +0700 Subject: [PATCH 29/64] updates rubocop setting severity. adds default from --- .rubocop.yml | 4 ++-- app/mailers/application_mailer.rb | 1 + app/mailers/password_mailer.rb | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 6baa8f85b..da30fc371 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -346,8 +346,8 @@ Minitest/UnreachableAssertion: # Check quotes usage according to lint rule below. Style/StringLiterals: - Enabled: false -# EnforcedStyle: double_quotes + EnforcedStyle: double_quotes + Severity: info # don't fail CI for this, but auto-correct # Files should always have a new line at the end Layout/TrailingEmptyLines: diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index ead50cd96..6e76ad21b 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,2 +1,3 @@ class ApplicationMailer < ActionMailer::Base + default from: Setting.email_from end diff --git a/app/mailers/password_mailer.rb b/app/mailers/password_mailer.rb index e435b0df1..cc116f80d 100644 --- a/app/mailers/password_mailer.rb +++ b/app/mailers/password_mailer.rb @@ -16,7 +16,6 @@ def reset @edit_url = edit_password_credential_url(token: token) mail( - from: Setting.email_from, to: person.email, subject: "Set up a new password for #{Setting.product_name}", ) From 2bf1e7724516aa2c7d1de8b0689f9d26d8283c53 Mon Sep 17 00:00:00 2001 From: Justin Vallelonga Date: Mon, 29 Jul 2024 23:00:45 +0700 Subject: [PATCH 30/64] use deliver_now. indentation. tests for job --- app/jobs/send_reset_password_email_job.rb | 2 +- .../settings/api_services/_form.html.erb | 42 ++++++------- app/views/users/new.html.erb | 16 ++--- .../send_reset_password_email_job_test.rb | 61 ++++++++++++++++++- 4 files changed, 88 insertions(+), 33 deletions(-) diff --git a/app/jobs/send_reset_password_email_job.rb b/app/jobs/send_reset_password_email_job.rb index 41c36684e..2c47a1bf4 100644 --- a/app/jobs/send_reset_password_email_job.rb +++ b/app/jobs/send_reset_password_email_job.rb @@ -5,7 +5,7 @@ def perform(email, os, browser) person = Person.find_by_email(email) if person&.user&.password_credential - PasswordMailer.with(person: person, os: os, browser: browser).reset.deliver_later + PasswordMailer.with(person: person, os: os, browser: browser).reset.deliver_now end end end diff --git a/app/views/settings/api_services/_form.html.erb b/app/views/settings/api_services/_form.html.erb index 6453f7e7b..3ead171ca 100644 --- a/app/views/settings/api_services/_form.html.erb +++ b/app/views/settings/api_services/_form.html.erb @@ -19,17 +19,17 @@
    <%= form.label :driver %> <%= form.select :driver, - APIService.drivers.keys, - {}, - class: %| - block - border border-gray-200 outline-none - shadow rounded-md - px-3 py-2 mt-2 - w-full - dark:text-black - | - %> + APIService.drivers.keys, + {}, + class: %| + block + border border-gray-200 outline-none + shadow rounded-md + px-3 py-2 mt-2 + w-full + dark:text-black + | + %>
    @@ -78,7 +78,7 @@ %>).
  • Add a "Payment method."
  • -