Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def destroy
end

def resend
@accounts_invitation.send_invite
@accounts_invitation.resend_invite
render json: {status: 200}, status: :ok
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,22 @@ class Api::V1::Shopkeeper::AccountsInvitationsController < Api::V1::Shopkeeper::
skip_after_action :verify_authorized

def show
if @accounts_invitation.expired?
render json: {code: 410, error_message: I18n.t("api.shopkeeper.accounts_invitations.expired")}, status: :gone
return
end

options = {}
options[:include] = [:account, :invited_by]
render json: AccountsInvitationSerializer.new(@accounts_invitation, options).serializable_hash
end

def update
if @accounts_invitation.expired?
render json: {code: 410, error_message: I18n.t("api.shopkeeper.accounts_invitations.expired")}, status: :gone
return
end

if @accounts_invitation.accept!(current_shopkeeper)
render json: {status: 200}, status: :ok
else
Expand Down
13 changes: 13 additions & 0 deletions app/models/accounts_invitation.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
class AccountsInvitation < ApplicationRecord
ROLES = AccountsShopkeeper::ROLES
EXPIRES_IN = ConfigSettings.accounts_invitation.expires_in_hours.hours

include Rolified

Expand All @@ -11,6 +12,13 @@ class AccountsInvitation < ApplicationRecord

before_create :set_token

scope :active, -> { where(created_at: EXPIRES_IN.ago..) }
scope :expired, -> { where(created_at: ...EXPIRES_IN.ago) }

def expired?
created_at < EXPIRES_IN.ago
end

def save_and_send_invite
save && send_invite
end
Expand All @@ -19,6 +27,11 @@ def send_invite
Shopkeeper::NotificationMailer.with(accounts_invitation: self).invited.deliver_later
end

def resend_invite
touch(:created_at)
send_invite
end

def accept!(shopkeeper)
accounts_shopkeeper = account.accounts_shopkeepers.new(shopkeeper: shopkeeper, roles: roles)
if accounts_shopkeeper.valid?
Expand Down
2 changes: 2 additions & 0 deletions app/views/shopkeeper/notification_mailer/invited.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@

<p><%= link_to ConfigSettings.mobile_app.name_ios, ConfigSettings.mobile_app.app_store_url, target: :_blank %></p>
<p><%= link_to ConfigSettings.mobile_app.name_android, ConfigSettings.mobile_app.play_store_url, target: :_blank %></p>

<p><%= t(".expires_in", expires_in: ConfigSettings.accounts_invitation.expires_in_hours) %></p>
2 changes: 2 additions & 0 deletions app/views/shopkeeper/notification_mailer/invited.text.erb
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@

<%= ConfigSettings.mobile_app.name_ios %>: <%= ConfigSettings.mobile_app.app_store_url %>
<%= ConfigSettings.mobile_app.name_android %>: <%= ConfigSettings.mobile_app.play_store_url %>

<%= t(".expires_in", expires_in: ConfigSettings.accounts_invitation.expires_in_hours) %>
2 changes: 2 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ en:
cannot_delete: "You cannot delete your personal account."
accounts_invitations:
not_found: "Whoops, we weren't able to find this invitation. Check with your organization admin for a new invitation."
expired: "This invitation has expired. Please ask the organization admin to resend the invitation."
accounts_shopkeepers:
require_non_personal_account: "Require non personal organization."
item_tags:
Expand All @@ -70,6 +71,7 @@ en:
tap_join_button: "Tap Join button."
enter_your_invitation_code: "Enter your invitation code below."
if_you_accepted_invitation_switch_to_organization: "If you accepted the invitation, switch to the organization(Organizations > [Organization] > Switch)."
expires_in: "This invitation will expire in %{expires_in} hours."
reset_password_instructions:
subject: "Reset password instructions"
request_reset_link_msg: "Someone has requested a link to change your password. You can do this through the link below."
Expand Down
3 changes: 3 additions & 0 deletions config/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ account:
accounts_shopkeeper:
limit_count: 99

accounts_invitation:
expires_in_hours: 48

item_tag:
limit_count: 99
default_count: 10
Expand Down
2 changes: 2 additions & 0 deletions config/settings/development.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
accounts_invitation:
expires_in_hours: 0.083 # ~5 minutes
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,16 @@ class Api::V1::Shopkeeper::Accounts::AccountsInvitationsControllerTest < ActionD
assert_response :unauthorized
end

test "resend sends invitation email again" do
post resend_api_v1_shopkeeper_account_accounts_invitation_path(@account, @invitation.token),
headers: @shopkeeper.create_new_auth_token
test "resend sends invitation email again and touches created_at" do
original_created_at = @invitation.created_at

assert_response :success
travel_to(1.hour.from_now) do
post resend_api_v1_shopkeeper_account_accounts_invitation_path(@account, @invitation.token),
headers: @shopkeeper.create_new_auth_token

assert_response :success
assert @invitation.reload.created_at > original_created_at
end
end

test "resend requires admin role" do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,27 @@ class Api::V1::Shopkeeper::AccountsInvitationsControllerTest < ActionDispatch::I
assert_response :success
end

test "show returns 410 gone for expired invitation" do
@invitation.update_column(:created_at, (AccountsInvitation::EXPIRES_IN + 1.minute).ago)

get api_v1_shopkeeper_accounts_invitation_url(@invitation.token),
headers: @shopkeeper.create_new_auth_token

assert_response :gone
assert_equal I18n.t("api.shopkeeper.accounts_invitations.expired"), response.parsed_body["error_message"]
end

test "update returns 410 gone for expired invitation" do
@invitation.update_column(:created_at, (AccountsInvitation::EXPIRES_IN + 1.minute).ago)

other_shopkeeper = shopkeepers(:two)
patch api_v1_shopkeeper_accounts_invitation_url(@invitation.token),
headers: other_shopkeeper.create_new_auth_token

assert_response :gone
assert_equal I18n.t("api.shopkeeper.accounts_invitations.expired"), response.parsed_body["error_message"]
end

test "requires authentication" do
get api_v1_shopkeeper_accounts_invitation_url(@invitation.token)

Expand Down
65 changes: 65 additions & 0 deletions test/models/accounts_invitation_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -274,4 +274,69 @@ def setup
invitation.send_invite
end
end

test "expired? returns false for recent invitation" do
invitation = AccountsInvitation.create!(
account: @account,
name: "Invited User",
email: "recent@example.com",
junior_member: true
)

assert_not invitation.expired?
end

test "expired? returns true for old invitation" do
invitation = AccountsInvitation.create!(
account: @account,
name: "Invited User",
email: "old@example.com",
junior_member: true
)

travel_to(AccountsInvitation::EXPIRES_IN.from_now + 1.minute) do
assert invitation.expired?
end
end

test "active scope returns non-expired invitations" do
active_invitation = AccountsInvitation.create!(
account: @account,
name: "Active User",
email: "active_scope@example.com",
junior_member: true
)

expired_invitation = AccountsInvitation.create!(
account: @account,
name: "Expired User",
email: "expired_scope@example.com",
junior_member: true
)
expired_invitation.update_column(:created_at, (AccountsInvitation::EXPIRES_IN + 1.minute).ago)

active_invitations = AccountsInvitation.active
assert_includes active_invitations, active_invitation
assert_not_includes active_invitations, expired_invitation
end

test "resend_invite touches created_at and sends invite" do
invitation = AccountsInvitation.create!(
account: @account,
name: "Invited User",
email: "resend@example.com",
invited_by: @shopkeeper,
junior_member: true
)

original_created_at = invitation.created_at

travel_to(1.hour.from_now) do
assert_nothing_raised do
invitation.resend_invite
end

assert invitation.created_at > original_created_at
end
end
end
Loading