mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 05:09:38 +02:00
Implement invitation codes
This commit is contained in:
parent
533e6690c2
commit
b3a792c47d
11 changed files with 149 additions and 9 deletions
|
@ -37,4 +37,9 @@ class ApplicationController < ActionController::Base
|
||||||
Current.user = nil
|
Current.user = nil
|
||||||
reset_session
|
reset_session
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def hosted_app?
|
||||||
|
ENV["HOSTED"] == "true"
|
||||||
|
end
|
||||||
|
helper_method :hosted_app?
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
class RegistrationsController < ApplicationController
|
class RegistrationsController < ApplicationController
|
||||||
layout "auth"
|
layout "auth"
|
||||||
|
|
||||||
|
before_action :set_user, only: :create
|
||||||
|
before_action :claim_invite_code, only: :create, if: :hosted_app?
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@user = User.new
|
@user = User.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@user = User.new(user_params)
|
|
||||||
|
|
||||||
family = Family.new
|
family = Family.new
|
||||||
@user.family = family
|
@user.family = family
|
||||||
|
|
||||||
|
@ -23,7 +24,17 @@ class RegistrationsController < ApplicationController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def set_user
|
||||||
|
@user = User.new user_params.except(:invite_code)
|
||||||
|
end
|
||||||
|
|
||||||
def user_params
|
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
|
||||||
end
|
end
|
||||||
|
|
25
app/models/invite_code.rb
Normal file
25
app/models/invite_code.rb
Normal file
|
@ -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
|
|
@ -66,7 +66,7 @@
|
||||||
<% current_family.accounts.depository.each do |account| %>
|
<% current_family.accounts.depository.each do |account| %>
|
||||||
<div class="flex items-center justify-between py-2">
|
<div class="flex items-center justify-between py-2">
|
||||||
<div class="flex items-center text-sm">
|
<div class="flex items-center text-sm">
|
||||||
<%= account.name %>
|
<%= account.name %>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-right">
|
<p class="text-sm text-right">
|
||||||
<span class="block mb-1"><%= number_to_currency account.balance %></span>
|
<span class="block mb-1"><%= number_to_currency account.balance %></span>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
<meta name="apple-mobile-web-app-title" content="Maybe">
|
<meta name="apple-mobile-web-app-title" content="Maybe">
|
||||||
|
|
||||||
<%= csrf_meta_tags %>
|
<%= csrf_meta_tags %>
|
||||||
<%= csp_meta_tag %>
|
<%= csp_meta_tag %>
|
||||||
<%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>
|
<%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>
|
||||||
|
@ -18,15 +18,21 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="h-full">
|
<body class="h-full">
|
||||||
|
<% flash.each do |type, msg| %>
|
||||||
|
<div>
|
||||||
|
<%= msg %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<div class="flex flex-col justify-center min-h-full px-6 py-12">
|
<div class="flex flex-col justify-center min-h-full px-6 py-12">
|
||||||
|
|
||||||
<div class="sm:mx-auto sm:w-full sm:max-w-md">
|
<div class="sm:mx-auto sm:w-full sm:max-w-md">
|
||||||
<%= render "shared/logo" %>
|
<%= render "shared/logo" %>
|
||||||
|
|
||||||
<h2 class="mt-6 text-3xl font-semibold tracking-tight text-center font-display">
|
<h2 class="mt-6 text-3xl font-semibold tracking-tight text-center font-display">
|
||||||
<%= content_for?(:header_title) ? yield(:header_title).html_safe : "Your account" %>
|
<%= content_for?(:header_title) ? yield(:header_title).html_safe : "Your account" %>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<% if params[:controller] == "devise/sessions" && params[:action] == "new" %>
|
<% if params[:controller] == "devise/sessions" && params[:action] == "new" %>
|
||||||
<p class="mt-2 text-sm text-center text-gray-600">
|
<p class="mt-2 text-sm text-center text-gray-600">
|
||||||
or <%= link_to "create an account", new_user_registration_path, class: 'font-medium text-candlelight-600 hover:text-candlelight-500' %>
|
or <%= link_to "create an account", new_user_registration_path, class: 'font-medium text-candlelight-600 hover:text-candlelight-500' %>
|
||||||
|
|
|
@ -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' %>
|
<%= 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' %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<% if hosted_app? %>
|
||||||
|
<div class="bg-offwhite rounded-xl focus-within:bg-white focus-within:shadow focus-within:opacity-100 relative p-4 border border-gray-100">
|
||||||
|
<%= 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' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<%= 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' %>
|
<%= 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' %>
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,4 +38,4 @@
|
||||||
|
|
||||||
<div class="mt-6 text-center">
|
<div class="mt-6 text-center">
|
||||||
<p class="text-sm text-gray-600">Already have an account? <%= link_to "Sign in", new_session_path, class: 'font-medium text-candlelight-600 hover:text-candlelight-500' %></p>
|
<p class="text-sm text-gray-600">Already have an account? <%= link_to "Sign in", new_session_path, class: 'font-medium text-candlelight-600 hover:text-candlelight-500' %></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
9
db/migrate/20240202230325_create_invite_codes.rb
Normal file
9
db/migrate/20240202230325_create_invite_codes.rb
Normal file
|
@ -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
|
8
db/schema.rb
generated
8
db/schema.rb
generated
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# 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
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pgcrypto"
|
enable_extension "pgcrypto"
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -34,6 +34,12 @@ ActiveRecord::Schema[7.2].define(version: 2024_02_02_015428) do
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
end
|
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|
|
create_table "users", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||||
t.uuid "family_id", null: false
|
t.uuid "family_id", null: false
|
||||||
t.string "first_name"
|
t.string "first_name"
|
||||||
|
|
46
test/controllers/registrations_controller_test.rb
Normal file
46
test/controllers/registrations_controller_test.rb
Normal file
|
@ -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
|
25
test/models/invite_code_test.rb
Normal file
25
test/models/invite_code_test.rb
Normal file
|
@ -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
|
Loading…
Add table
Add a link
Reference in a new issue