mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 13:19:39 +02:00
Feat: Data "reset" button (#1913)
* feat: Allow admins to delete family data * feat: Allow self-hosting users to delete cached data * Remove system tests
This commit is contained in:
parent
f7064fd4dd
commit
8208722247
13 changed files with 206 additions and 16 deletions
|
@ -2,6 +2,7 @@ class Settings::HostingsController < ApplicationController
|
||||||
layout "settings"
|
layout "settings"
|
||||||
|
|
||||||
before_action :raise_if_not_self_hosted
|
before_action :raise_if_not_self_hosted
|
||||||
|
before_action :ensure_admin, only: :clear_cache
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@synth_usage = Current.family.synth_usage
|
@synth_usage = Current.family.synth_usage
|
||||||
|
@ -38,6 +39,11 @@ class Settings::HostingsController < ApplicationController
|
||||||
render :show, status: :unprocessable_entity
|
render :show, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def clear_cache
|
||||||
|
DataCacheClearJob.perform_later(Current.family)
|
||||||
|
redirect_to settings_hosting_path, notice: t(".cache_cleared")
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def hosting_params
|
def hosting_params
|
||||||
params.require(:setting).permit(:render_deploy_hook, :upgrades_setting, :require_invite_for_signup, :require_email_confirmation, :synth_api_key)
|
params.require(:setting).permit(:render_deploy_hook, :upgrades_setting, :require_invite_for_signup, :require_email_confirmation, :synth_api_key)
|
||||||
|
@ -46,4 +52,8 @@ class Settings::HostingsController < ApplicationController
|
||||||
def raise_if_not_self_hosted
|
def raise_if_not_self_hosted
|
||||||
raise "Settings not available on non-self-hosted instance" unless self_hosted?
|
raise "Settings not available on non-self-hosted instance" unless self_hosted?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ensure_admin
|
||||||
|
redirect_to settings_hosting_path, alert: t(".not_authorized") unless Current.user.admin?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
class UsersController < ApplicationController
|
class UsersController < ApplicationController
|
||||||
before_action :set_user
|
before_action :set_user
|
||||||
|
before_action :ensure_admin, only: :reset
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@user = Current.user
|
@user = Current.user
|
||||||
|
@ -26,6 +27,11 @@ class UsersController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def reset
|
||||||
|
FamilyResetJob.perform_later(Current.family)
|
||||||
|
redirect_to settings_profile_path, notice: t(".success")
|
||||||
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
if @user.deactivate
|
if @user.deactivate
|
||||||
Current.session.destroy
|
Current.session.destroy
|
||||||
|
@ -68,4 +74,8 @@ class UsersController < ApplicationController
|
||||||
def set_user
|
def set_user
|
||||||
@user = Current.user
|
@user = Current.user
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ensure_admin
|
||||||
|
redirect_to settings_profile_path, alert: I18n.t("users.reset.unauthorized") unless Current.user.admin?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
16
app/jobs/data_cache_clear_job.rb
Normal file
16
app/jobs/data_cache_clear_job.rb
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
class DataCacheClearJob < ApplicationJob
|
||||||
|
queue_as :default
|
||||||
|
|
||||||
|
def perform(family)
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
ExchangeRate.delete_all
|
||||||
|
Security::Price.delete_all
|
||||||
|
family.accounts.each do |account|
|
||||||
|
account.balances.delete_all
|
||||||
|
account.holdings.delete_all
|
||||||
|
end
|
||||||
|
|
||||||
|
family.sync_later
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
19
app/jobs/family_reset_job.rb
Normal file
19
app/jobs/family_reset_job.rb
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
class FamilyResetJob < ApplicationJob
|
||||||
|
queue_as :default
|
||||||
|
|
||||||
|
def perform(family)
|
||||||
|
# Delete all family data except users
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
# Delete accounts and related data
|
||||||
|
family.accounts.destroy_all
|
||||||
|
family.categories.destroy_all
|
||||||
|
family.tags.destroy_all
|
||||||
|
family.merchants.destroy_all
|
||||||
|
family.plaid_items.destroy_all
|
||||||
|
family.imports.destroy_all
|
||||||
|
family.budgets.destroy_all
|
||||||
|
|
||||||
|
family.sync_later
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
20
app/views/settings/hostings/_danger_zone_settings.html.erb
Normal file
20
app/views/settings/hostings/_danger_zone_settings.html.erb
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<% if Current.user.admin? %>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="w-2/3">
|
||||||
|
<h3 class="font-medium text-primary"><%= t("settings.hostings.show.clear_cache") %></h3>
|
||||||
|
<p class="text-secondary text-sm"><%= t("settings.hostings.show.clear_cache_warning") %></p>
|
||||||
|
</div>
|
||||||
|
<%=
|
||||||
|
button_to t("settings.hostings.show.clear_cache"), clear_cache_settings_hosting_path, method: :delete,
|
||||||
|
class: "bg-orange-500 text-white text-sm font-medium rounded-lg px-4 py-2",
|
||||||
|
data: { turbo_confirm: {
|
||||||
|
title: t("settings.hostings.show.confirm_clear_cache.title"),
|
||||||
|
body: t("settings.hostings.show.confirm_clear_cache.body"),
|
||||||
|
accept: t("settings.hostings.show.clear_cache"),
|
||||||
|
acceptClass: "w-full bg-orange-500 text-white rounded-xl text-center p-[10px] border mb-2"
|
||||||
|
}}
|
||||||
|
%>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
|
@ -11,3 +11,7 @@
|
||||||
<%= settings_section title: t(".invites") do %>
|
<%= settings_section title: t(".invites") do %>
|
||||||
<%= render "settings/hostings/invite_code_settings" %>
|
<%= render "settings/hostings/invite_code_settings" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<%= settings_section title: t(".danger_zone") do %>
|
||||||
|
<%= render "settings/hostings/danger_zone_settings" %>
|
||||||
|
<% end %>
|
||||||
|
|
|
@ -127,20 +127,40 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= settings_section title: t(".danger_zone_title") do %>
|
<%= settings_section title: t(".danger_zone_title") do %>
|
||||||
<div class="flex items-center justify-between">
|
<div class="space-y-4">
|
||||||
<div>
|
<% if Current.user.admin? %>
|
||||||
<h3 class="font-medium text-primary"><%= t(".delete_account") %></h3>
|
<div class="flex items-center justify-between">
|
||||||
<p class="text-secondary text-sm"><%= t(".delete_account_warning") %></p>
|
<div class="w-2/3">
|
||||||
|
<h3 class="font-medium text-primary"><%= t(".reset_account") %></h3>
|
||||||
|
<p class="text-secondary text-sm"><%= t(".reset_account_warning") %></p>
|
||||||
|
</div>
|
||||||
|
<%=
|
||||||
|
button_to t(".reset_account"), reset_user_path(@user), method: :delete,
|
||||||
|
class: "bg-orange-500 text-white text-sm font-medium rounded-lg px-4 py-2",
|
||||||
|
data: { turbo_confirm: {
|
||||||
|
title: t(".confirm_reset.title"),
|
||||||
|
body: t(".confirm_reset.body"),
|
||||||
|
accept: t(".reset_account"),
|
||||||
|
acceptClass: "w-full bg-orange-500 text-white rounded-xl text-center p-[10px] border mb-2"
|
||||||
|
}}
|
||||||
|
%>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium text-primary"><%= t(".delete_account") %></h3>
|
||||||
|
<p class="text-secondary text-sm"><%= t(".delete_account_warning") %></p>
|
||||||
|
</div>
|
||||||
|
<%=
|
||||||
|
button_to t(".delete_account"), user_path(@user), method: :delete,
|
||||||
|
class: "bg-red-500 text-white text-sm font-medium rounded-lg px-3 py-2",
|
||||||
|
data: { turbo_confirm: {
|
||||||
|
title: t(".confirm_delete.title"),
|
||||||
|
body: t(".confirm_delete.body"),
|
||||||
|
accept: t(".delete_account"),
|
||||||
|
acceptClass: "w-full bg-red-500 text-white rounded-xl text-center p-[10px] border mb-2"
|
||||||
|
}}
|
||||||
|
%>
|
||||||
</div>
|
</div>
|
||||||
<%=
|
|
||||||
button_to t(".delete_account"), user_path(@user), method: :delete,
|
|
||||||
class: "bg-red-500 text-white text-sm font-medium rounded-lg px-3 py-2",
|
|
||||||
data: { turbo_confirm: {
|
|
||||||
title: t(".confirm_delete.title"),
|
|
||||||
body: t(".confirm_delete.body"),
|
|
||||||
accept: t(".delete_account"),
|
|
||||||
acceptClass: "w-full bg-red-500 text-white rounded-xl text-center p-[10px] border mb-2"
|
|
||||||
}}
|
|
||||||
%>
|
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -39,6 +39,9 @@ en:
|
||||||
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_reset:
|
||||||
|
body: Are you sure you want to reset your account? This will delete all your accounts, categories, merchants, tags, and other data. This action cannot be undone.
|
||||||
|
title: Reset account?
|
||||||
confirm_remove_invitation:
|
confirm_remove_invitation:
|
||||||
body: Are you sure you want to remove the invitation for %{email}?
|
body: Are you sure you want to remove the invitation for %{email}?
|
||||||
title: Remove Invitation
|
title: Remove Invitation
|
||||||
|
@ -49,6 +52,8 @@ en:
|
||||||
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
|
||||||
your data and cannot be undone.
|
your data and cannot be undone.
|
||||||
|
reset_account: Reset account
|
||||||
|
reset_account_warning: Resetting your account will delete all your accounts, categories, merchants, tags, and other data, but keep your user account intact.
|
||||||
email: Email
|
email: Email
|
||||||
first_name: First Name
|
first_name: First Name
|
||||||
household_form_input_placeholder: Enter household name
|
household_form_input_placeholder: Enter household name
|
||||||
|
|
|
@ -20,6 +20,12 @@ en:
|
||||||
general: General Settings
|
general: General Settings
|
||||||
invites: Invite Codes
|
invites: Invite Codes
|
||||||
title: Self-Hosting
|
title: Self-Hosting
|
||||||
|
danger_zone: Danger Zone
|
||||||
|
clear_cache: Clear data cache
|
||||||
|
clear_cache_warning: Clearing the data cache will remove all exchange rates, security prices, account balances, and other data. This will not delete accounts, transactions, categories, or other user-owned data.
|
||||||
|
confirm_clear_cache:
|
||||||
|
title: Clear data cache?
|
||||||
|
body: Are you sure you want to clear the data cache? This will remove all exchange rates, security prices, account balances, and other data. This action cannot be undone.
|
||||||
synth_settings:
|
synth_settings:
|
||||||
api_calls_used: "%{used} / %{limit} API calls used (%{percentage})"
|
api_calls_used: "%{used} / %{limit} API calls used (%{percentage})"
|
||||||
description: Input the API key provided by Synth
|
description: Input the API key provided by Synth
|
||||||
|
@ -30,6 +36,8 @@ en:
|
||||||
update:
|
update:
|
||||||
failure: Invalid setting value
|
failure: Invalid setting value
|
||||||
success: Settings updated
|
success: Settings updated
|
||||||
|
clear_cache:
|
||||||
|
cache_cleared: Data cache has been cleared. This may take a few moments to complete.
|
||||||
upgrade_settings:
|
upgrade_settings:
|
||||||
description: Configure how your application receives updates
|
description: Configure how your application receives updates
|
||||||
latest_commit_description: Automatically update to the latest commit (unstable)
|
latest_commit_description: Automatically update to the latest commit (unstable)
|
||||||
|
@ -40,3 +48,4 @@ en:
|
||||||
manual_description: You control when to download and install updates
|
manual_description: You control when to download and install updates
|
||||||
manual_title: Manual
|
manual_title: Manual
|
||||||
title: Auto Upgrade
|
title: Auto Upgrade
|
||||||
|
not_authorized: You are not authorized to perform this action
|
||||||
|
|
|
@ -8,3 +8,6 @@ en:
|
||||||
email_change_initiated: Please check your new email address for confirmation
|
email_change_initiated: Please check your new email address for confirmation
|
||||||
instructions.
|
instructions.
|
||||||
success: Your profile has been updated.
|
success: Your profile has been updated.
|
||||||
|
reset:
|
||||||
|
success: Your account has been reset. Data will be deleted in the background in some time.
|
||||||
|
unauthorized: You are not authorized to perform this action
|
||||||
|
|
|
@ -18,7 +18,9 @@ Rails.application.routes.draw do
|
||||||
resource :password, only: %i[edit update]
|
resource :password, only: %i[edit update]
|
||||||
resource :email_confirmation, only: :new
|
resource :email_confirmation, only: :new
|
||||||
|
|
||||||
resources :users, only: %i[update destroy]
|
resources :users, only: %i[update destroy] do
|
||||||
|
delete :reset, on: :member
|
||||||
|
end
|
||||||
|
|
||||||
resource :onboarding, only: :show do
|
resource :onboarding, only: :show do
|
||||||
collection do
|
collection do
|
||||||
|
@ -30,7 +32,9 @@ Rails.application.routes.draw do
|
||||||
namespace :settings do
|
namespace :settings do
|
||||||
resource :profile, only: [ :show, :destroy ]
|
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] do
|
||||||
|
delete :clear_cache, on: :collection
|
||||||
|
end
|
||||||
resource :billing, only: :show
|
resource :billing, only: :show
|
||||||
resource :security, only: :show
|
resource :security, only: :show
|
||||||
end
|
end
|
||||||
|
|
|
@ -45,4 +45,39 @@ class Settings::HostingsControllerTest < ActionDispatch::IntegrationTest
|
||||||
assert_equal NEW_RENDER_DEPLOY_HOOK, Setting.render_deploy_hook
|
assert_equal NEW_RENDER_DEPLOY_HOOK, Setting.render_deploy_hook
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "can clear data cache when self hosting is enabled" do
|
||||||
|
account = accounts(:investment)
|
||||||
|
holding = account.holdings.first
|
||||||
|
exchange_rate = exchange_rates(:one)
|
||||||
|
security_price = holding.security.prices.first
|
||||||
|
account_balance = account.balances.create!(date: Date.current, balance: 1000, currency: "USD")
|
||||||
|
|
||||||
|
with_self_hosting do
|
||||||
|
perform_enqueued_jobs(only: DataCacheClearJob) do
|
||||||
|
delete clear_cache_settings_hosting_url
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_redirected_to settings_hosting_url
|
||||||
|
assert_equal I18n.t("settings.hostings.clear_cache.cache_cleared"), flash[:notice]
|
||||||
|
|
||||||
|
assert_not ExchangeRate.exists?(exchange_rate.id)
|
||||||
|
assert_not Security::Price.exists?(security_price.id)
|
||||||
|
assert_not Account::Holding.exists?(holding.id)
|
||||||
|
assert_not Account::Balance.exists?(account_balance.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "can clear data only when admin" do
|
||||||
|
with_self_hosting do
|
||||||
|
sign_in users(:family_member)
|
||||||
|
|
||||||
|
assert_no_enqueued_jobs do
|
||||||
|
delete clear_cache_settings_hosting_url
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_redirected_to settings_hosting_url
|
||||||
|
assert_equal I18n.t("settings.hostings.not_authorized"), flash[:alert]
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -31,6 +31,41 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
|
||||||
assert_equal "Your profile has been updated.", flash[:notice]
|
assert_equal "Your profile has been updated.", flash[:notice]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "admin can reset family data" do
|
||||||
|
account = accounts(:investment)
|
||||||
|
category = categories(:income)
|
||||||
|
tag = tags(:one)
|
||||||
|
merchant = merchants(:netflix)
|
||||||
|
import = imports(:transaction)
|
||||||
|
budget = budgets(:one)
|
||||||
|
plaid_item = plaid_items(:one)
|
||||||
|
|
||||||
|
perform_enqueued_jobs(only: FamilyResetJob) do
|
||||||
|
delete reset_user_url(@user)
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_redirected_to settings_profile_url
|
||||||
|
assert_equal I18n.t("users.reset.success"), flash[:notice]
|
||||||
|
|
||||||
|
assert_not Account.exists?(account.id)
|
||||||
|
assert_not Category.exists?(category.id)
|
||||||
|
assert_not Tag.exists?(tag.id)
|
||||||
|
assert_not Merchant.exists?(merchant.id)
|
||||||
|
assert_not Import.exists?(import.id)
|
||||||
|
assert_not Budget.exists?(budget.id)
|
||||||
|
assert_not PlaidItem.exists?(plaid_item.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "non-admin cannot reset family data" do
|
||||||
|
sign_in @member = users(:family_member)
|
||||||
|
|
||||||
|
delete reset_user_url(@member)
|
||||||
|
|
||||||
|
assert_redirected_to settings_profile_url
|
||||||
|
assert_equal I18n.t("users.reset.unauthorized"), flash[:alert]
|
||||||
|
assert_no_enqueued_jobs only: FamilyResetJob
|
||||||
|
end
|
||||||
|
|
||||||
test "member can deactivate their account" do
|
test "member can deactivate their account" do
|
||||||
sign_in @member = users(:family_member)
|
sign_in @member = users(:family_member)
|
||||||
delete user_url(@member)
|
delete user_url(@member)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue