diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..59511e1 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +ruby 2.7.8 diff --git a/test/integration/email_test.rb b/test/integration/email_test.rb index 85d40bb..c583cf1 100644 --- a/test/integration/email_test.rb +++ b/test/integration/email_test.rb @@ -15,6 +15,20 @@ class EmailTest < IntegrationTest assert_includes email.body.to_s, "Someone has created an account with this email address" end + test "mailer delivery - multi-tenant" do + register(login: "user@example.com", prefix: "/multi/tenant/animal-farm") + + assert_equal 1, ActionMailer::Base.deliveries.count + + email = ActionMailer::Base.deliveries[0] + + assert_equal "user@example.com", email[:to].to_s + assert_equal "noreply@rodauth.test", email[:from].to_s + assert_equal "[RodauthTest] Verify Account", email[:subject].to_s + + assert_includes email.body.to_s, "Someone has created an account with this email address" + end + test "verify login change email" do register(login: "user@example.com", password: "secret", verify: true) @@ -26,4 +40,16 @@ class EmailTest < IntegrationTest assert_equal 2, ActionMailer::Base.deliveries.count end + + test "verify login change email - multi-tenant" do + register(login: "user@example.com", password: "secret", prefix: "/multi/tenant/kiwi", verify: true) + + visit "/change-email" + + fill_in "Login", with: "new@example.com" + fill_in "Password", with: "secret" + click_on "Change Login" + + assert_equal 2, ActionMailer::Base.deliveries.count + end end diff --git a/test/rails_app/app/mailers/rodauth_multi_tenant_mailer.rb b/test/rails_app/app/mailers/rodauth_multi_tenant_mailer.rb new file mode 100644 index 0000000..2313143 --- /dev/null +++ b/test/rails_app/app/mailers/rodauth_multi_tenant_mailer.rb @@ -0,0 +1,42 @@ +class RodauthMultiTenantMailer < ActionMailer::Base + default to: -> { @rodauth.email_to }, from: -> { @rodauth.email_from } + + def verify_account(name, account_id, path_key, key) + @rodauth = rodauth(name, account_id, path_key) { @verify_account_key_value = key } + @account = @rodauth.rails_account + + mail(subject: @rodauth.email_subject_prefix + @rodauth.verify_account_email_subject) + end + + def reset_password(name, account_id, path_key, key) + @rodauth = rodauth(name, account_id, path_key) { @reset_password_key_value = key } + @account = @rodauth.rails_account + + mail(subject: @rodauth.email_subject_prefix + @rodauth.reset_password_email_subject) + end + + def verify_login_change(name, account_id, path_key, key) + @rodauth = rodauth(name, account_id, path_key) { @verify_login_change_key_value = key } + @account = @rodauth.rails_account + @new_email = @account.login_change_key.login + + mail(to: @new_email, subject: @rodauth.email_subject_prefix + @rodauth.verify_login_change_email_subject) + end + + def password_changed(name, account_id, path_key) + @rodauth = rodauth(name, account_id, path_key) + @account = @rodauth.rails_account + + mail(subject: @rodauth.email_subject_prefix + @rodauth.password_changed_email_subject) + end + + # ... + private + def rodauth(name, account_id, path_key, &block) + instance = RodauthApp.new({ path_key: path_key }).rodauth(name) + instance.path_key = path_key + instance.instance_eval { @account = account_ds(account_id).first! } + instance.instance_eval(&block) if block + instance + end +end diff --git a/test/rails_app/app/misc/rodauth_app.rb b/test/rails_app/app/misc/rodauth_app.rb index 0e2285a..58e0ef3 100644 --- a/test/rails_app/app/misc/rodauth_app.rb +++ b/test/rails_app/app/misc/rodauth_app.rb @@ -1,6 +1,12 @@ class RodauthApp < Rodauth::Rails::App configure RodauthMain configure RodauthAdmin, :admin + configure RodauthMultiTenant, :multi_tenant + + plugin :symbol_matchers + + # Allow UUID characters in path_key + symbol_matcher :path_key, /([A-Z0-9_-]+)/xi configure(:jwt) do enable :jwt, :create_account, :verify_account @@ -24,6 +30,21 @@ class RodauthApp < Rodauth::Rails::App r.rodauth r.rodauth(:admin) + r.on("multi/tenant") do + r.on(:path_key) do |path_key| + # In a real life application you'd only proceed to rodauth routing if a tenant was found. + # We'll mimic that possibility by skipping a /banana path-key + next if path_key == "banana" + + r.env[:path_key] = path_key + rodauth(:multi_tenant).path_key = path_key + + r.rodauth(:multi_tenant) + next + end + next + end + r.on("jwt") { r.rodauth(:jwt) } r.on("json") { r.rodauth(:json) } diff --git a/test/rails_app/app/misc/rodauth_multi_tenant.rb b/test/rails_app/app/misc/rodauth_multi_tenant.rb new file mode 100644 index 0000000..8399d0f --- /dev/null +++ b/test/rails_app/app/misc/rodauth_multi_tenant.rb @@ -0,0 +1,68 @@ +class RodauthMultiTenant < Rodauth::Rails::Auth + configure do + enable :create_account, :verify_account, :verify_account_grace_period, + :login, :remember, :logout, :active_sessions, + :reset_password, :change_password, :change_password_notify, + :change_login, :verify_login_change, + :close_account, :lockout, :recovery_codes, :internal_request, + :path_class_methods, :jwt + + prefix { "/multi/tenant/#{path_key}" } + + rails_controller { RodauthController } + + before_rodauth do + if param_or_nil("raise") + raise NotImplementedError + elsif param_or_nil("fail") + fail "failed" + end + end + + account_status_column :status + + email_subject_prefix "[RodauthTest] " + email_from "noreply@rodauth.test" + create_reset_password_email do + RodauthMultiTenantMailer.reset_password(:multi_tenant, account_id, request.env[:path_key], reset_password_key_value) + end + create_verify_account_email { RodauthMultiTenantMailer.verify_account(:multi_tenant, account_id, request.env[:path_key], verify_account_key_value) } + create_verify_login_change_email { |_login| RodauthMultiTenantMailer.verify_login_change(:multi_tenant, account_id, request.env[:path_key], verify_login_change_key_value) } + create_password_changed_email { RodauthMultiTenantMailer.password_changed(:multi_tenant, account_id, request.env[:path_key]) } + + require_login_confirmation? false + verify_account_set_password? false + extend_remember_deadline? true + max_invalid_logins 3 + + if defined?(::Turbo) + after_login_failure do + if rails_request.format.turbo_stream? + return_response rails_render(turbo_stream: [turbo_stream.append("login-form", %(
login failed
))]) + end + end + check_csrf? { rails_request.format.turbo_stream? ? false : super() } + end + + after_login { remember_login } + + logout_redirect { rails_routes.root_path } + login_redirect do + segs = login_path.split('/') + segs.insert(-2, request.env[:path_key]) + segs.join('/') + end + verify_account_redirect { login_redirect } + reset_password_redirect do + segs = login_path.split('/') + segs.insert(-2, request.env[:path_key]) + segs.join('/') + end + title_instance_variable :@page_title + + verify_login_change_route nil + change_login_route "change-email" + end + + attr_accessor :path_key +end diff --git a/test/rails_app/app/views/rodauth_multi_tenant_mailer/password_changed.text.erb b/test/rails_app/app/views/rodauth_multi_tenant_mailer/password_changed.text.erb new file mode 100644 index 0000000..2e48708 --- /dev/null +++ b/test/rails_app/app/views/rodauth_multi_tenant_mailer/password_changed.text.erb @@ -0,0 +1,2 @@ +Someone (hopefully you) has changed the password for the account +associated to this email address. diff --git a/test/rails_app/app/views/rodauth_multi_tenant_mailer/reset_password.text.erb b/test/rails_app/app/views/rodauth_multi_tenant_mailer/reset_password.text.erb new file mode 100644 index 0000000..fcfb698 --- /dev/null +++ b/test/rails_app/app/views/rodauth_multi_tenant_mailer/reset_password.text.erb @@ -0,0 +1,5 @@ +Someone has requested a password reset for the account with this email +address. If you did not request a password reset, please ignore this +message. If you requested a password reset, please go to +<%= @rodauth.reset_password_email_link %> +to reset the password for the account. diff --git a/test/rails_app/app/views/rodauth_multi_tenant_mailer/verify_account.text.erb b/test/rails_app/app/views/rodauth_multi_tenant_mailer/verify_account.text.erb new file mode 100644 index 0000000..78ff6ad --- /dev/null +++ b/test/rails_app/app/views/rodauth_multi_tenant_mailer/verify_account.text.erb @@ -0,0 +1,4 @@ +Someone has created an account with this email address. If you did not create +this account, please ignore this message. If you created this account, please go to +<%= @rodauth.verify_account_email_link %> +to verify the account. diff --git a/test/rails_app/app/views/rodauth_multi_tenant_mailer/verify_login_change.text.erb b/test/rails_app/app/views/rodauth_multi_tenant_mailer/verify_login_change.text.erb new file mode 100644 index 0000000..693680b --- /dev/null +++ b/test/rails_app/app/views/rodauth_multi_tenant_mailer/verify_login_change.text.erb @@ -0,0 +1,10 @@ +Someone with an account has requested their login be changed to this email address: + +Old email: <%= @account.email %> + +New email: <%= @new_email %> + +If you did not request this login change, please ignore this message. If you +requested this login change, please go to +<%= @rodauth.verify_login_change_email_link %> +to verify the login change. diff --git a/test/test_helper.rb b/test/test_helper.rb index d749205..0f85469 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -49,8 +49,8 @@ class IntegrationTest < ActionDispatch::IntegrationTest include Capybara::DSL include TestSetupTeardown - def register(login: "user@example.com", password: "secret", verify: false) - visit "/create-account" + def register(login: "user@example.com", password: "secret", verify: false, prefix: "") + visit "#{prefix}/create-account" fill_in "Login", with: login fill_in "Password", with: password fill_in "Confirm Password", with: password @@ -58,22 +58,22 @@ def register(login: "user@example.com", password: "secret", verify: false) if verify email = ActionMailer::Base.deliveries.last - verify_account_link = email.body.to_s[%r{/verify-account\S+}] + verify_account_link = email.body.to_s[%r{#{prefix}/verify-account\S+}] visit verify_account_link click_on "Verify Account" end end - def login(login: "user@example.com", password: "secret") - visit "/login" + def login(login: "user@example.com", password: "secret", prefix: "") + visit "#{prefix}/login" fill_in "Login", with: login fill_in "Password", with: password click_on "Login" end - def logout - visit "/logout" + def logout(prefix: "") + visit "#{prefix}/logout" click_on "Logout" end