Skip to content

Commit

Permalink
feat: views for accepting or declining team invitations
Browse files Browse the repository at this point in the history
  • Loading branch information
kjellberg committed Apr 8, 2024
1 parent ed3289c commit a6579cc
Show file tree
Hide file tree
Showing 17 changed files with 173 additions and 15 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ group :development, :test do
gem "letter_opener_web", "~> 2.0"
gem "factory_bot_rails", "~> 6.4.3"
gem "rails-controller-testing"
gem "faker"
end

group :development do
Expand Down
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ GEM
factory_bot_rails (6.4.3)
factory_bot (~> 6.4)
railties (>= 5.0.0)
faker (3.3.1)
i18n (>= 1.8.11, < 2)
globalid (1.2.1)
activesupport (>= 6.1)
i18n (1.14.4)
Expand Down Expand Up @@ -400,6 +402,7 @@ DEPENDENCIES
dry-initializer (~> 3.1)
erb_lint
factory_bot_rails (~> 6.4.3)
faker
importmap-rails
jbuilder
letter_opener_web (~> 2.0)
Expand Down
2 changes: 1 addition & 1 deletion app/assets/stylesheets/application.tailwind.css
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
}

.button {
@apply border-border bg-button inline-flex items-center gap-x-2 cursor-pointer rounded px-5 py-2 font-bold text-white no-underline;
@apply border-border bg-button inline-flex justify-center items-center gap-x-2 cursor-pointer rounded px-5 py-2 font-bold text-white no-underline;
}

.button.xs {
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/accounts/invitations_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class Accounts::InvitationsController < ApplicationController
def index
@invitations = current_account.account_invitations
@invitations = current_account.account_invitations.pending
@account = current_account
end

Expand Down
5 changes: 0 additions & 5 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,6 @@ def ensure_onboarded
redirect_to onboarding_path if user_signed_in? && !current_user.onboarded?
end

# Override the method to change the sign-in redirect path
def after_sign_in_path_for(resource)
current_user.accounts.any? ? select_account_path : dashboard_path
end

# Override the method to change the sign-out redirect path
def after_sign_out_path_for(resource_or_scope)
# Generate the root path without default URL options
Expand Down
43 changes: 43 additions & 0 deletions app/controllers/users/invitations_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
class Users::InvitationsController < ApplicationController
skip_before_action :authenticate_user!, only: [:show]

def show
@invitation = AccountInvitation.find_puid!(params[:id])

if @invitation.accepted_at?
flash[:alert] = "Invitation has already been accepted by you or someone else."
return redirect_to dashboard_path
elsif current_user&.accounts&.include? @invitation.account
flash[:notice] = "You are already a member of this team."
return redirect_to dashboard_path(account_id: @invitation.account)
end

@team = @invitation.account
session[:after_sign_in_path] = user_invitation_path(@invitation)
end

def update
return redirect_back(fallback_location: dashboard_path) unless params[:user_invitation][:status] == "accepted"
return head :forbidden if AccountInvitation.find_puid!(params[:id]).accepted_at?

@invitation = AccountInvitation.find_puid!(params[:id])
@account = @invitation.account
@account.account_users.new(user: current_user)

if @account.save
@invitation.update(accepted_at: Time.now)
flash[:notice] = "You have successfully joined the team"
redirect_to dashboard_path(account_id: @account)
else
redirect_to user_invitation_path(@invitation), alert: "Failed to accept invitation. Please try again later"
end
end

def destroy
@invitation = AccountInvitation.find_puid!(params[:id])
@invitation.destroy

flash[:notice] = "You have declined the invitation"
redirect_to dashboard_path
end
end
13 changes: 13 additions & 0 deletions app/controllers/users/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,17 @@ def validate_otp_code
render :otp, status: :unprocessable_entity
end
end

protected

# Override the method to change the sign-in redirect path
def after_sign_in_path_for(resource)
if session[:after_sign_in_path].present?
session.delete(:after_sign_in_path)
elsif current_user.accounts.any?
select_account_path
else
dashboard_path
end
end
end
5 changes: 5 additions & 0 deletions app/models/account_invitation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ class AccountInvitation < ApplicationRecord
# Learn more: https://github.com/equivalent/public_uid
include PublicUid::ModelConcern

# Generate a more secure public_uid for invitations using SecureRandom.hex(32)
generate_public_uid generator: PublicUid::Generators::HexStringSecureRandom.new(32)

belongs_to :account, inverse_of: :account_invitations, counter_cache: true

validates :email, presence: true, format: {with: URI::MailTo::EMAIL_REGEXP}
validates :email, uniqueness: {scope: :account_id, message: I18n.t("accounts.invitations.new.form.errors.email.taken")}

scope :pending, -> { where(accepted_at: nil) }
end
4 changes: 2 additions & 2 deletions app/views/accounts/invitations/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@
<thead>
<tr>
<th><%= t(".table.email") %></th>
<th><%= t(".table.sent_at") %></th>
<th><%= t(".table.url") %></th>
<th class="text-right"><%= t(".table.actions") %></th>
</tr>
</thead>
<tbody>
<% @invitations.each do |invitation| %>
<tr>
<td><%= invitation.email %></td>
<td><%= invitation.created_at %></td>
<td><%= user_invitation_url(invitation, account_id: nil) %></td>
<td class="text-right">
<%= button_to t(".table.remove"), invitation_path(id: invitation), class: "button xs danger", method: :delete, data: { confirm: t(".confirm_delete", email: invitation.email), turbo_confirm: t(".confirm_delete", email: invitation.email) } %>
</td>
Expand Down
30 changes: 30 additions & 0 deletions app/views/users/invitations/show.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<% title "You've been invited to join #{@team.name}" %>

<%= fullscreen do |screen| %>
<%= screen.with_form(
title: t(".title"),
description: t(".description", team_name: @team.name)
) do %>
<main class="flex flex-col gap-8 w-full max-w-xl">

<% if user_signed_in? && onboarded? %>
<p><%= t(".instructions", team_name: @team.name) %></p>
<div class="flex justify-center gap-x-4">
<%= button_to t(".buttons.accept"), "#", class: "button", method: :patch, params: {
user_invitation: { status: "accepted" }
} %>
<%= button_to t(".buttons.decline"), "#", class: "button danger", method: :patch, params: {
user_invitation: { status: "declined" }
} %>
</div>
<% else %>
<p><%= t(".guest_instructions", team_name: @team.name) %></p>
<div class="flex justify-center items-center gap-x-4">
<%= link_to "Sign in", new_user_session_path, class: "button" %>
or
<%= link_to "Create account", new_user_session_path, class: "button alt" %>
</div>
<% end %>
</main>
<% end %>
<% end %>
13 changes: 9 additions & 4 deletions config/locales/kiqr.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,7 @@ en:
confirm_delete: "Are you sure you want to cancel the invitation to %{email}?"
table:
email: "Email"
status: "Status"
actions: "Actions"
resend: "Resend"
cancel: "Cancel"
sent_at: "Sent at"
buttons:
invite: "Invite a new user"
back: "Back to team members"
Expand Down Expand Up @@ -179,6 +176,14 @@ en:
owned_team_accounts: "Team accounts you are the owner of:"
submit: "Delete my account"
confirmation_message: "Are you sure? This will DELETE all of your data."
invitations:
show:
title: "Accept invitation"
description: "You've been invited to join team %{team_name}."
guest_instructions: "You've been invited to join team %{team_name}. To accept the invitation, please create a new account or sign in with an existing one."
instructions: "You've been invited to join team %{team_name}. Choose whether you want to accept or decline the invitation."
sign_up: "Create a new account"
sign_in: "Sign in"
two_factor:
show:
title: "Two-factor authentication"
Expand Down
4 changes: 4 additions & 0 deletions config/routes/authentication.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
get "delete" => "cancellations#show", :as => :delete_user_registration
end

scope module: :users, path: nil do
resources :invitations, only: %i[show update destroy], as: :user_invitation
end

get "select-account", to: "accounts#select", as: :select_account

resources :accounts, only: [:new, :create]
Expand Down
1 change: 1 addition & 0 deletions db/migrate/20240407174725_create_account_invitations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ def change
t.string :public_uid, index: {unique: true}
t.references :account, null: false, foreign_key: true
t.string :email, null: false
t.datetime :accepted_at
t.timestamps
end

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddUniqueIndexToAccountUsers < ActiveRecord::Migration[7.1]
def change
add_index :account_users, [:account_id, :user_id], unique: true
end
end
4 changes: 3 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 50 additions & 0 deletions test/controllers/users/invitations_controller_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
require "test_helper"

class Users::InvitationsControllerTest < ActionDispatch::IntegrationTest
test "can view page without beeing signed in" do
invitation = create(:account_invitation)
get user_invitation_path(invitation)
assert_response :success
end

test "can accept invitation" do
user = create(:user)
invitation = create(:account_invitation)
sign_in user

assert_difference -> { user.accounts.count } do
patch user_invitation_path(invitation), params: {user_invitation: {status: "accepted"}}
end

assert_redirected_to dashboard_path(account_id: invitation.account)
assert_not_nil invitation.reload.accepted_at
end

test "cant be accepted twice" do
user = create(:user)
invitation = create(:account_invitation, accepted_at: Time.now)
sign_in user

get user_invitation_path(invitation)
assert_redirected_to dashboard_path

assert_no_difference -> { user.accounts.count } do
patch user_invitation_path(invitation), params: {user_invitation: {status: "accepted"}}
end

assert_response :forbidden
end

test "can decline invitation" do
user = create(:user)
invitation = create(:account_invitation)
sign_in user

assert_no_difference -> { user.accounts.count } do
delete user_invitation_path(invitation)
end

assert_redirected_to dashboard_path
assert_nil AccountInvitation.find_by(id: invitation.id)
end
end
3 changes: 2 additions & 1 deletion test/factories/account_invitations.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
FactoryBot.define do
factory :account_invitation do
account { nil }
account { create(:account) }
email { Faker::Internet.email }
end
end

0 comments on commit a6579cc

Please sign in to comment.