From b3a792c47db917d98fa6c4ccf038a66b98f43e5e Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Fri, 2 Feb 2024 17:49:28 -0600 Subject: [PATCH 1/6] Implement invitation codes --- app/controllers/application_controller.rb | 5 ++ app/controllers/registrations_controller.rb | 17 +++++-- app/models/invite_code.rb | 25 ++++++++++ app/views/layouts/application.html.erb | 2 +- app/views/layouts/auth.html.erb | 12 +++-- app/views/registrations/new.html.erb | 9 +++- .../20240202230325_create_invite_codes.rb | 9 ++++ db/schema.rb | 8 +++- test/controllers/.keep | 0 .../registrations_controller_test.rb | 46 +++++++++++++++++++ test/models/invite_code_test.rb | 25 ++++++++++ 11 files changed, 149 insertions(+), 9 deletions(-) create mode 100644 app/models/invite_code.rb create mode 100644 db/migrate/20240202230325_create_invite_codes.rb delete mode 100644 test/controllers/.keep create mode 100644 test/controllers/registrations_controller_test.rb create mode 100644 test/models/invite_code_test.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index c7c7cc88..4f7214fe 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -37,4 +37,9 @@ class ApplicationController < ActionController::Base Current.user = nil reset_session end + + def hosted_app? + ENV["HOSTED"] == "true" + end + helper_method :hosted_app? end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index cecad15a..3973ddbf 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -1,13 +1,14 @@ class RegistrationsController < ApplicationController layout "auth" + before_action :set_user, only: :create + before_action :claim_invite_code, only: :create, if: :hosted_app? + def new @user = User.new end def create - @user = User.new(user_params) - family = Family.new @user.family = family @@ -23,7 +24,17 @@ class RegistrationsController < ApplicationController private + def set_user + @user = User.new user_params.except(:invite_code) + end + def user_params - params.require(:user).permit(:name, :email, :password, :password_confirmation) + params.require(:user).permit(:name, :email, :password, :password_confirmation, :invite_code) + end + + def claim_invite_code + unless InviteCode.claim! params[:user][:invite_code] + redirect_to new_registration_path, alert: "Invalid invite code, please try again." + end end end diff --git a/app/models/invite_code.rb b/app/models/invite_code.rb new file mode 100644 index 00000000..59ebf855 --- /dev/null +++ b/app/models/invite_code.rb @@ -0,0 +1,25 @@ +class InviteCode < ApplicationRecord + before_validation :generate_token, on: :create + + class << self + def claim!(token) + if invite_code = find_by(token: token&.downcase) + invite_code.destroy! + true + end + end + + def generate! + create!.token + end + end + + private + + def generate_token + loop do + self.token = SecureRandom.hex(4) + break token unless self.class.exists?(token: token) + end + end +end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 893c7c40..ba49490e 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -66,7 +66,7 @@ <% current_family.accounts.depository.each do |account| %>
- <%= account.name %> + <%= account.name %>

<%= number_to_currency account.balance %> diff --git a/app/views/layouts/auth.html.erb b/app/views/layouts/auth.html.erb index 668dd47a..e6720d52 100644 --- a/app/views/layouts/auth.html.erb +++ b/app/views/layouts/auth.html.erb @@ -6,7 +6,7 @@ - + <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %> @@ -18,15 +18,21 @@ + <% flash.each do |type, msg| %> +

+ <%= msg %> +
+ <% end %> +
<%= render "shared/logo" %> - +

<%= content_for?(:header_title) ? yield(:header_title).html_safe : "Your account" %>

- + <% if params[:controller] == "devise/sessions" && params[:action] == "new" %>

or <%= link_to "create an account", new_user_registration_path, class: 'font-medium text-candlelight-600 hover:text-candlelight-500' %> diff --git a/app/views/registrations/new.html.erb b/app/views/registrations/new.html.erb index 0545b464..d762f0e0 100644 --- a/app/views/registrations/new.html.erb +++ b/app/views/registrations/new.html.erb @@ -24,6 +24,13 @@ <%= form.password_field :password_confirmation, autocomplete: "new-password", required: 'required', class: 'p-0 mt-1 bg-transparent border-none opacity-50 focus:outline-none focus:ring-0 focus-within:opacity-100 w-full' %>

+ <% if hosted_app? %> +
+ <%= form.label :invite_code, "Invite code", class: 'block text-sm font-medium text-gray-700' %> + <%= form.password_field :invite_code, required: 'required', class: 'p-0 mt-1 bg-transparent border-none opacity-50 focus:outline-none focus:ring-0 focus-within:opacity-100 w-full' %> +
+ <% end %> +
<%= form.submit "Continue", class: 'flex justify-center w-full px-4 py-3 text-sm font-medium text-white bg-black rounded-xl hover:bg-black focus:outline-none focus:ring-2 focus:ring-gray-200 shadow' %>
@@ -31,4 +38,4 @@

Already have an account? <%= link_to "Sign in", new_session_path, class: 'font-medium text-candlelight-600 hover:text-candlelight-500' %>

-
\ No newline at end of file +
diff --git a/db/migrate/20240202230325_create_invite_codes.rb b/db/migrate/20240202230325_create_invite_codes.rb new file mode 100644 index 00000000..0ed9fb45 --- /dev/null +++ b/db/migrate/20240202230325_create_invite_codes.rb @@ -0,0 +1,9 @@ +class CreateInviteCodes < ActiveRecord::Migration[7.2] + def change + create_table :invite_codes, id: :uuid do |t| + t.string :token, null: false + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 90da754d..1a132587 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2024_02_02_015428) do +ActiveRecord::Schema[7.2].define(version: 2024_02_02_230325) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -34,6 +34,12 @@ ActiveRecord::Schema[7.2].define(version: 2024_02_02_015428) do t.datetime "updated_at", null: false end + create_table "invite_codes", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.string "token", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "users", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.uuid "family_id", null: false t.string "first_name" diff --git a/test/controllers/.keep b/test/controllers/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/test/controllers/registrations_controller_test.rb b/test/controllers/registrations_controller_test.rb new file mode 100644 index 00000000..c32f2c79 --- /dev/null +++ b/test/controllers/registrations_controller_test.rb @@ -0,0 +1,46 @@ +require "test_helper" + +class RegistrationsControllerTest < ActionDispatch::IntegrationTest + test "new" do + get new_registration_url + assert_response :success + end + + test "create" do + post registration_url, params: { user: { + email: "john@example.com", + password: "password", + password_confirmation: "password" } } + + assert_redirected_to root_url + end + + test "create when hosted requires an invitation code" do + ENV["HOSTED"] = "true" + + assert_no_difference "User.count" do + post registration_url, params: { user: { + email: "john@example.com", + password: "password", + password_confirmation: "password" } } + assert_redirected_to new_registration_url + + post registration_url, params: { user: { + email: "john@example.com", + password: "password", + password_confirmation: "password", + invite_code: "foo" } } + assert_redirected_to new_registration_url + end + + assert_difference "User.count", +1 do + post registration_url, params: { user: { + email: "john@example.com", + password: "password", + password_confirmation: "password", + invite_code: InviteCode.generate! } } + end + ensure + ENV["HOSTED"] = nil + end +end diff --git a/test/models/invite_code_test.rb b/test/models/invite_code_test.rb new file mode 100644 index 00000000..58b3af2f --- /dev/null +++ b/test/models/invite_code_test.rb @@ -0,0 +1,25 @@ +require "test_helper" + +class InviteCodeTest < ActiveSupport::TestCase + test "claim! destroys the invitation token" do + code = InviteCode.generate! + + assert_difference "InviteCode.count", -1 do + InviteCode.claim! code + end + end + + test "claim! returns true if valid" do + assert InviteCode.claim!(InviteCode.generate!) + end + + test "claim! is falsy if invalid" do + assert_not InviteCode.claim!("invalid") + end + + test "generate! creates a new invitation and returns its token" do + assert_difference "InviteCode.count", +1 do + assert_instance_of String, InviteCode.generate! + end + end +end From 6a4ab5664d7f989e88c141b1882b7b86a3752bdc Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Fri, 2 Feb 2024 18:02:46 -0600 Subject: [PATCH 2/6] Add rake task for creating invite codes --- lib/tasks/.keep | 0 lib/tasks/invites.rake | 6 ++++++ 2 files changed, 6 insertions(+) delete mode 100644 lib/tasks/.keep create mode 100644 lib/tasks/invites.rake diff --git a/lib/tasks/.keep b/lib/tasks/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/tasks/invites.rake b/lib/tasks/invites.rake new file mode 100644 index 00000000..021da370 --- /dev/null +++ b/lib/tasks/invites.rake @@ -0,0 +1,6 @@ +namespace :invites do + desc "Create an invitation code" + task create: :environment do + puts InviteCode.generate! + end +end From 5eb6418f01f663881108755428a2215ab5a4c0bd Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Fri, 2 Feb 2024 18:24:22 -0600 Subject: [PATCH 3/6] Assert redirection in registration test --- test/controllers/registrations_controller_test.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/controllers/registrations_controller_test.rb b/test/controllers/registrations_controller_test.rb index c32f2c79..4c33cc6e 100644 --- a/test/controllers/registrations_controller_test.rb +++ b/test/controllers/registrations_controller_test.rb @@ -39,6 +39,7 @@ class RegistrationsControllerTest < ActionDispatch::IntegrationTest password: "password", password_confirmation: "password", invite_code: InviteCode.generate! } } + assert_redirected_to root_url end ensure ENV["HOSTED"] = nil From 8118927f9b6d301bb49fb3c2db346ef300736b58 Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Fri, 2 Feb 2024 19:34:58 -0600 Subject: [PATCH 4/6] Improve InviteCode generate! test --- test/models/invite_code_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/models/invite_code_test.rb b/test/models/invite_code_test.rb index 58b3af2f..02df2d7c 100644 --- a/test/models/invite_code_test.rb +++ b/test/models/invite_code_test.rb @@ -19,7 +19,7 @@ class InviteCodeTest < ActiveSupport::TestCase test "generate! creates a new invitation and returns its token" do assert_difference "InviteCode.count", +1 do - assert_instance_of String, InviteCode.generate! + assert_equal InviteCode.generate!, InviteCode.last.token end end end From 3852b79121d6dff0f8299cbf7a1ad0943439fce5 Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Fri, 2 Feb 2024 19:37:10 -0600 Subject: [PATCH 5/6] Use consistent language for invite codes --- lib/tasks/invites.rake | 2 +- test/controllers/registrations_controller_test.rb | 2 +- test/models/invite_code_test.rb | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/tasks/invites.rake b/lib/tasks/invites.rake index 021da370..61e385cf 100644 --- a/lib/tasks/invites.rake +++ b/lib/tasks/invites.rake @@ -1,5 +1,5 @@ namespace :invites do - desc "Create an invitation code" + desc "Create an invite code" task create: :environment do puts InviteCode.generate! end diff --git a/test/controllers/registrations_controller_test.rb b/test/controllers/registrations_controller_test.rb index 4c33cc6e..8d441b83 100644 --- a/test/controllers/registrations_controller_test.rb +++ b/test/controllers/registrations_controller_test.rb @@ -15,7 +15,7 @@ class RegistrationsControllerTest < ActionDispatch::IntegrationTest assert_redirected_to root_url end - test "create when hosted requires an invitation code" do + test "create when hosted requires an invite code" do ENV["HOSTED"] = "true" assert_no_difference "User.count" do diff --git a/test/models/invite_code_test.rb b/test/models/invite_code_test.rb index 02df2d7c..b95c0271 100644 --- a/test/models/invite_code_test.rb +++ b/test/models/invite_code_test.rb @@ -1,7 +1,7 @@ require "test_helper" class InviteCodeTest < ActiveSupport::TestCase - test "claim! destroys the invitation token" do + test "claim! destroys the invite token" do code = InviteCode.generate! assert_difference "InviteCode.count", -1 do @@ -17,7 +17,7 @@ class InviteCodeTest < ActiveSupport::TestCase assert_not InviteCode.claim!("invalid") end - test "generate! creates a new invitation and returns its token" do + test "generate! creates a new invite and returns its token" do assert_difference "InviteCode.count", +1 do assert_equal InviteCode.generate!, InviteCode.last.token end From 78c84c5028053caf524deb0b4a113d6dca73a282 Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Fri, 2 Feb 2024 19:48:45 -0600 Subject: [PATCH 6/6] Pull out in_hosted_app helper --- .../registrations_controller_test.rb | 55 +++++++++++-------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/test/controllers/registrations_controller_test.rb b/test/controllers/registrations_controller_test.rb index 8d441b83..cbf3b6c4 100644 --- a/test/controllers/registrations_controller_test.rb +++ b/test/controllers/registrations_controller_test.rb @@ -16,31 +16,38 @@ class RegistrationsControllerTest < ActionDispatch::IntegrationTest end test "create when hosted requires an invite code" do + in_hosted_app do + assert_no_difference "User.count" do + post registration_url, params: { user: { + email: "john@example.com", + password: "password", + password_confirmation: "password" } } + assert_redirected_to new_registration_url + + post registration_url, params: { user: { + email: "john@example.com", + password: "password", + password_confirmation: "password", + invite_code: "foo" } } + assert_redirected_to new_registration_url + end + + assert_difference "User.count", +1 do + post registration_url, params: { user: { + email: "john@example.com", + password: "password", + password_confirmation: "password", + invite_code: InviteCode.generate! } } + assert_redirected_to root_url + end + end + end + + private + + def in_hosted_app ENV["HOSTED"] = "true" - - assert_no_difference "User.count" do - post registration_url, params: { user: { - email: "john@example.com", - password: "password", - password_confirmation: "password" } } - assert_redirected_to new_registration_url - - post registration_url, params: { user: { - email: "john@example.com", - password: "password", - password_confirmation: "password", - invite_code: "foo" } } - assert_redirected_to new_registration_url - end - - assert_difference "User.count", +1 do - post registration_url, params: { user: { - email: "john@example.com", - password: "password", - password_confirmation: "password", - invite_code: InviteCode.generate! } } - assert_redirected_to root_url - end + yield ensure ENV["HOSTED"] = nil end