1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-19 05:09:38 +02:00

Add/remove members and invitations (#1744)

* Add/remove members and invitations

* Lint
This commit is contained in:
Josh Pigford 2025-01-30 13:13:37 -06:00 committed by GitHub
parent 282c05345d
commit 0696e1f2f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 188 additions and 29 deletions

View file

@ -34,6 +34,24 @@ class InvitationsController < ApplicationController
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
def invitation_params

View file

@ -4,4 +4,28 @@ class Settings::ProfilesController < SettingsController
@users = Current.family.users.order(:created_at)
@pending_invitations = Current.family.invitations.pending
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

View file

@ -43,6 +43,21 @@
<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>
</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>
<% end %>
<% if @pending_invitations.any? %>
@ -59,6 +74,7 @@
</div>
</div>
</div>
<div class="flex items-center gap-4">
<% if self_hosted? %>
<div class="flex items-center gap-2" data-controller="clipboard">
<p class="text-gray-500 text-sm"><%= t(".invitation_link") %></p>
@ -78,6 +94,20 @@
</button>
</div>
<% 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>
<% end %>
<% end %>

View file

@ -3,7 +3,7 @@ if ENV["SENTRY_DSN"].present?
config.dsn = ENV["SENTRY_DSN"]
config.environment = ENV["RAILS_ENV"]
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%
# of transactions for performance monitoring.

View file

@ -4,6 +4,10 @@ en:
create:
failure: Could not send invitation
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:
email_label: Email Address
email_placeholder: Enter email address

View file

@ -48,11 +48,24 @@ en:
theme_title: Theme
timezone: Timezone
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:
confirm_delete:
body: Are you sure you want to permanently delete your account? This action
is irreversible.
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
delete_account: Delete account
delete_account_warning: Deleting your account will permanently remove all

View file

@ -20,7 +20,7 @@ Rails.application.routes.draw do
end
namespace :settings do
resource :profile, only: :show
resource :profile, only: [ :show, :destroy ]
resource :preferences, only: :show
resource :hosting, only: %i[show update]
resource :billing, only: :show
@ -142,7 +142,7 @@ Rails.application.routes.draw do
resources :exchange_rate_provider_missings, only: :update
end
resources :invitations, only: [ :new, :create ] do
resources :invitations, only: [ :new, :create, :destroy ] do
get :accept, on: :member
end

View file

@ -2,7 +2,7 @@ require "test_helper"
class InvitationsControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in @user = users(:family_admin)
sign_in @admin = users(:family_admin)
@invitation = invitations(:one)
end
@ -25,7 +25,7 @@ class InvitationsControllerTest < ActionDispatch::IntegrationTest
invitation = Invitation.order(created_at: :desc).first
assert_equal "member", invitation.role
assert_equal @user, invitation.inviter
assert_equal @admin, invitation.inviter
assert_equal "new@example.com", invitation.email
assert_redirected_to settings_profile_path
assert_equal I18n.t("invitations.create.success"), flash[:notice]
@ -59,8 +59,8 @@ class InvitationsControllerTest < ActionDispatch::IntegrationTest
invitation = Invitation.order(created_at: :desc).first
assert_equal "admin", invitation.role
assert_equal @user.family, invitation.family
assert_equal @user, invitation.inviter
assert_equal @admin.family, invitation.family
assert_equal @admin, invitation.inviter
end
test "should handle invalid invitation creation" do
@ -86,4 +86,29 @@ class InvitationsControllerTest < ActionDispatch::IntegrationTest
get accept_invitation_url("invalid-token")
assert_response :not_found
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

View file

@ -2,11 +2,46 @@ require "test_helper"
class Settings::ProfilesControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in @user = users(:family_admin)
@admin = users(:family_admin)
@member = users(:family_member)
end
test "get" do
get settings_profile_url
test "should get show" do
sign_in @admin
get settings_profile_path
assert_response :success
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

View file

@ -17,3 +17,13 @@ two:
created_at: <%= Time.current %>
updated_at: <%= Time.current %>
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 %>