From b3a792c47db917d98fa6c4ccf038a66b98f43e5e Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Fri, 2 Feb 2024 17:49:28 -0600 Subject: [PATCH] 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