mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 21:29:38 +02:00
Add/remove members and invitations (#1744)
* Add/remove members and invitations * Lint
This commit is contained in:
parent
282c05345d
commit
0696e1f2f7
10 changed files with 188 additions and 29 deletions
|
@ -34,6 +34,24 @@ class InvitationsController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
unless Current.user.admin?
|
||||||
|
flash[:alert] = t("invitations.destroy.not_authorized")
|
||||||
|
redirect_to settings_profile_path
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
@invitation = Current.family.invitations.find(params[:id])
|
||||||
|
|
||||||
|
if @invitation.destroy
|
||||||
|
flash[:notice] = t("invitations.destroy.success")
|
||||||
|
else
|
||||||
|
flash[:alert] = t("invitations.destroy.failure")
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect_to settings_profile_path
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def invitation_params
|
def invitation_params
|
||||||
|
|
|
@ -4,4 +4,28 @@ class Settings::ProfilesController < SettingsController
|
||||||
@users = Current.family.users.order(:created_at)
|
@users = Current.family.users.order(:created_at)
|
||||||
@pending_invitations = Current.family.invitations.pending
|
@pending_invitations = Current.family.invitations.pending
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
unless Current.user.admin?
|
||||||
|
flash[:alert] = t("settings.profiles.destroy.not_authorized")
|
||||||
|
redirect_to settings_profile_path
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
@user = Current.family.users.find(params[:user_id])
|
||||||
|
|
||||||
|
if @user == Current.user
|
||||||
|
flash[:alert] = t("settings.profiles.destroy.cannot_remove_self")
|
||||||
|
redirect_to settings_profile_path
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if @user.destroy
|
||||||
|
flash[:notice] = t("settings.profiles.destroy.member_removed")
|
||||||
|
else
|
||||||
|
flash[:alert] = t("settings.profiles.destroy.member_removal_failed")
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect_to settings_profile_path
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -43,6 +43,21 @@
|
||||||
<div class="rounded-md bg-gray-100 px-1.5 py-0.5">
|
<div class="rounded-md bg-gray-100 px-1.5 py-0.5">
|
||||||
<p class="uppercase text-gray-500 font-medium text-xs"><%= user.role %></p>
|
<p class="uppercase text-gray-500 font-medium text-xs"><%= user.role %></p>
|
||||||
</div>
|
</div>
|
||||||
|
<% if Current.user.admin? && user != Current.user %>
|
||||||
|
<div class="ml-auto">
|
||||||
|
<%= button_to settings_profile_path(user_id: user),
|
||||||
|
method: :delete,
|
||||||
|
class: "text-red-500 hover:text-red-700",
|
||||||
|
data: { turbo_confirm: {
|
||||||
|
title: t(".confirm_remove_member.title"),
|
||||||
|
body: t(".confirm_remove_member.body", name: user.display_name),
|
||||||
|
accept: t(".remove_member"),
|
||||||
|
acceptClass: "w-full bg-red-500 text-white rounded-xl text-center p-[10px] border mb-2"
|
||||||
|
}} do %>
|
||||||
|
<%= lucide_icon "x", class: "w-5 h-5" %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if @pending_invitations.any? %>
|
<% if @pending_invitations.any? %>
|
||||||
|
@ -59,6 +74,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
<% if self_hosted? %>
|
<% if self_hosted? %>
|
||||||
<div class="flex items-center gap-2" data-controller="clipboard">
|
<div class="flex items-center gap-2" data-controller="clipboard">
|
||||||
<p class="text-gray-500 text-sm"><%= t(".invitation_link") %></p>
|
<p class="text-gray-500 text-sm"><%= t(".invitation_link") %></p>
|
||||||
|
@ -78,6 +94,20 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% if Current.user.admin? %>
|
||||||
|
<%= button_to invitation_path(invitation),
|
||||||
|
method: :delete,
|
||||||
|
class: "text-red-500 hover:text-red-700",
|
||||||
|
data: { turbo_confirm: {
|
||||||
|
title: t(".confirm_remove_invitation.title"),
|
||||||
|
body: t(".confirm_remove_invitation.body", email: invitation.email),
|
||||||
|
accept: t(".remove_invitation"),
|
||||||
|
acceptClass: "w-full bg-red-500 text-white rounded-xl text-center p-[10px] border mb-2"
|
||||||
|
}} do %>
|
||||||
|
<%= lucide_icon "x", class: "w-5 h-5" %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -3,7 +3,7 @@ if ENV["SENTRY_DSN"].present?
|
||||||
config.dsn = ENV["SENTRY_DSN"]
|
config.dsn = ENV["SENTRY_DSN"]
|
||||||
config.environment = ENV["RAILS_ENV"]
|
config.environment = ENV["RAILS_ENV"]
|
||||||
config.breadcrumbs_logger = [ :active_support_logger, :http_logger ]
|
config.breadcrumbs_logger = [ :active_support_logger, :http_logger ]
|
||||||
config.enabled_environments = %w[development production]
|
config.enabled_environments = %w[production]
|
||||||
|
|
||||||
# Set traces_sample_rate to 1.0 to capture 100%
|
# Set traces_sample_rate to 1.0 to capture 100%
|
||||||
# of transactions for performance monitoring.
|
# of transactions for performance monitoring.
|
||||||
|
|
|
@ -4,6 +4,10 @@ en:
|
||||||
create:
|
create:
|
||||||
failure: Could not send invitation
|
failure: Could not send invitation
|
||||||
success: Invitation sent successfully
|
success: Invitation sent successfully
|
||||||
|
destroy:
|
||||||
|
not_authorized: You are not authorized to manage invitations.
|
||||||
|
success: Invitation was successfully removed.
|
||||||
|
failure: There was a problem removing the invitation.
|
||||||
new:
|
new:
|
||||||
email_label: Email Address
|
email_label: Email Address
|
||||||
email_placeholder: Enter email address
|
email_placeholder: Enter email address
|
||||||
|
|
|
@ -48,11 +48,24 @@ en:
|
||||||
theme_title: Theme
|
theme_title: Theme
|
||||||
timezone: Timezone
|
timezone: Timezone
|
||||||
profiles:
|
profiles:
|
||||||
|
destroy:
|
||||||
|
not_authorized: You are not authorized to remove members.
|
||||||
|
cannot_remove_self: You cannot remove yourself from the account.
|
||||||
|
member_removed: Member was successfully removed.
|
||||||
|
member_removal_failed: There was a problem removing the member.
|
||||||
show:
|
show:
|
||||||
confirm_delete:
|
confirm_delete:
|
||||||
body: Are you sure you want to permanently delete your account? This action
|
body: Are you sure you want to permanently delete your account? This action
|
||||||
is irreversible.
|
is irreversible.
|
||||||
title: Delete account?
|
title: Delete account?
|
||||||
|
confirm_remove_member:
|
||||||
|
title: Remove Member
|
||||||
|
body: Are you sure you want to remove %{name} from your account?
|
||||||
|
remove_member: Remove Member
|
||||||
|
confirm_remove_invitation:
|
||||||
|
title: Remove Invitation
|
||||||
|
body: Are you sure you want to remove the invitation for %{email}?
|
||||||
|
remove_invitation: Remove Invitation
|
||||||
danger_zone_title: Danger Zone
|
danger_zone_title: Danger Zone
|
||||||
delete_account: Delete account
|
delete_account: Delete account
|
||||||
delete_account_warning: Deleting your account will permanently remove all
|
delete_account_warning: Deleting your account will permanently remove all
|
||||||
|
|
|
@ -20,7 +20,7 @@ Rails.application.routes.draw do
|
||||||
end
|
end
|
||||||
|
|
||||||
namespace :settings do
|
namespace :settings do
|
||||||
resource :profile, only: :show
|
resource :profile, only: [ :show, :destroy ]
|
||||||
resource :preferences, only: :show
|
resource :preferences, only: :show
|
||||||
resource :hosting, only: %i[show update]
|
resource :hosting, only: %i[show update]
|
||||||
resource :billing, only: :show
|
resource :billing, only: :show
|
||||||
|
@ -142,7 +142,7 @@ Rails.application.routes.draw do
|
||||||
resources :exchange_rate_provider_missings, only: :update
|
resources :exchange_rate_provider_missings, only: :update
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :invitations, only: [ :new, :create ] do
|
resources :invitations, only: [ :new, :create, :destroy ] do
|
||||||
get :accept, on: :member
|
get :accept, on: :member
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ require "test_helper"
|
||||||
|
|
||||||
class InvitationsControllerTest < ActionDispatch::IntegrationTest
|
class InvitationsControllerTest < ActionDispatch::IntegrationTest
|
||||||
setup do
|
setup do
|
||||||
sign_in @user = users(:family_admin)
|
sign_in @admin = users(:family_admin)
|
||||||
@invitation = invitations(:one)
|
@invitation = invitations(:one)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ class InvitationsControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
|
||||||
invitation = Invitation.order(created_at: :desc).first
|
invitation = Invitation.order(created_at: :desc).first
|
||||||
assert_equal "member", invitation.role
|
assert_equal "member", invitation.role
|
||||||
assert_equal @user, invitation.inviter
|
assert_equal @admin, invitation.inviter
|
||||||
assert_equal "new@example.com", invitation.email
|
assert_equal "new@example.com", invitation.email
|
||||||
assert_redirected_to settings_profile_path
|
assert_redirected_to settings_profile_path
|
||||||
assert_equal I18n.t("invitations.create.success"), flash[:notice]
|
assert_equal I18n.t("invitations.create.success"), flash[:notice]
|
||||||
|
@ -59,8 +59,8 @@ class InvitationsControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
|
||||||
invitation = Invitation.order(created_at: :desc).first
|
invitation = Invitation.order(created_at: :desc).first
|
||||||
assert_equal "admin", invitation.role
|
assert_equal "admin", invitation.role
|
||||||
assert_equal @user.family, invitation.family
|
assert_equal @admin.family, invitation.family
|
||||||
assert_equal @user, invitation.inviter
|
assert_equal @admin, invitation.inviter
|
||||||
end
|
end
|
||||||
|
|
||||||
test "should handle invalid invitation creation" do
|
test "should handle invalid invitation creation" do
|
||||||
|
@ -86,4 +86,29 @@ class InvitationsControllerTest < ActionDispatch::IntegrationTest
|
||||||
get accept_invitation_url("invalid-token")
|
get accept_invitation_url("invalid-token")
|
||||||
assert_response :not_found
|
assert_response :not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "admin can remove pending invitation" do
|
||||||
|
assert_difference("Invitation.count", -1) do
|
||||||
|
delete invitation_url(@invitation)
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_redirected_to settings_profile_path
|
||||||
|
assert_equal I18n.t("invitations.destroy.success"), flash[:notice]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "non-admin cannot remove invitations" do
|
||||||
|
sign_in users(:family_member)
|
||||||
|
|
||||||
|
assert_no_difference("Invitation.count") do
|
||||||
|
delete invitation_url(@invitation)
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_redirected_to settings_profile_path
|
||||||
|
assert_equal I18n.t("invitations.destroy.not_authorized"), flash[:alert]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should handle invalid invitation removal" do
|
||||||
|
delete invitation_url(id: "invalid-id")
|
||||||
|
assert_response :not_found
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,11 +2,46 @@ require "test_helper"
|
||||||
|
|
||||||
class Settings::ProfilesControllerTest < ActionDispatch::IntegrationTest
|
class Settings::ProfilesControllerTest < ActionDispatch::IntegrationTest
|
||||||
setup do
|
setup do
|
||||||
sign_in @user = users(:family_admin)
|
@admin = users(:family_admin)
|
||||||
|
@member = users(:family_member)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "get" do
|
test "should get show" do
|
||||||
get settings_profile_url
|
sign_in @admin
|
||||||
|
get settings_profile_path
|
||||||
assert_response :success
|
assert_response :success
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "admin can remove a family member" do
|
||||||
|
sign_in @admin
|
||||||
|
assert_difference("User.count", -1) do
|
||||||
|
delete settings_profile_path(user_id: @member)
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_redirected_to settings_profile_path
|
||||||
|
assert_equal I18n.t("settings.profiles.destroy.member_removed"), flash[:notice]
|
||||||
|
assert_raises(ActiveRecord::RecordNotFound) { User.find(@member.id) }
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin cannot remove themselves" do
|
||||||
|
sign_in @admin
|
||||||
|
assert_no_difference("User.count") do
|
||||||
|
delete settings_profile_path(user_id: @admin)
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_redirected_to settings_profile_path
|
||||||
|
assert_equal I18n.t("settings.profiles.destroy.cannot_remove_self"), flash[:alert]
|
||||||
|
assert User.find(@admin.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "non-admin cannot remove members" do
|
||||||
|
sign_in @member
|
||||||
|
assert_no_difference("User.count") do
|
||||||
|
delete settings_profile_path(user_id: @admin)
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_redirected_to settings_profile_path
|
||||||
|
assert_equal I18n.t("settings.profiles.destroy.not_authorized"), flash[:alert]
|
||||||
|
assert User.find(@admin.id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
10
test/fixtures/invitations.yml
vendored
10
test/fixtures/invitations.yml
vendored
|
@ -17,3 +17,13 @@ two:
|
||||||
created_at: <%= Time.current %>
|
created_at: <%= Time.current %>
|
||||||
updated_at: <%= Time.current %>
|
updated_at: <%= Time.current %>
|
||||||
expires_at: <%= 3.days.from_now %>
|
expires_at: <%= 3.days.from_now %>
|
||||||
|
|
||||||
|
other_family:
|
||||||
|
email: "other@example.com"
|
||||||
|
token: "valid-token-789"
|
||||||
|
role: "member"
|
||||||
|
inviter: empty
|
||||||
|
family: empty
|
||||||
|
created_at: <%= Time.current %>
|
||||||
|
updated_at: <%= Time.current %>
|
||||||
|
expires_at: <%= 3.days.from_now %>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue