Skip to content
This repository was archived by the owner on Jul 27, 2025. It is now read-only.
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
10 changes: 10 additions & 0 deletions app/controllers/settings/hostings_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ class Settings::HostingsController < ApplicationController
layout "settings"

before_action :raise_if_not_self_hosted
before_action :ensure_admin, only: :clear_cache

def show
@synth_usage = Current.family.synth_usage
Expand Down Expand Up @@ -38,6 +39,11 @@ def update
render :show, status: :unprocessable_entity
end

def clear_cache
DataCacheClearJob.perform_later(Current.family)
redirect_to settings_hosting_path, notice: t(".cache_cleared")
end

private
def hosting_params
params.require(:setting).permit(:render_deploy_hook, :upgrades_setting, :require_invite_for_signup, :require_email_confirmation, :synth_api_key)
Expand All @@ -46,4 +52,8 @@ def hosting_params
def raise_if_not_self_hosted
raise "Settings not available on non-self-hosted instance" unless self_hosted?
end

def ensure_admin
redirect_to settings_hosting_path, alert: t(".not_authorized") unless Current.user.admin?
end
end
10 changes: 10 additions & 0 deletions app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
class UsersController < ApplicationController
before_action :set_user
before_action :ensure_admin, only: :reset

def update
@user = Current.user
Expand All @@ -26,6 +27,11 @@ def update
end
end

def reset
FamilyResetJob.perform_later(Current.family)
redirect_to settings_profile_path, notice: t(".success")
end

def destroy
if @user.deactivate
Current.session.destroy
Expand Down Expand Up @@ -68,4 +74,8 @@ def user_params
def set_user
@user = Current.user
end

def ensure_admin
redirect_to settings_profile_path, alert: I18n.t("users.reset.unauthorized") unless Current.user.admin?
end
end
16 changes: 16 additions & 0 deletions app/jobs/data_cache_clear_job.rb
Original file line number Diff line number Diff line change
@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though we'll be deleting records across families and a self hosted instance will typically just be one family, I still think we should pass in perform(family) here and call family.sync_later at the end of this:

ExchangeRate.delete_all
Security::Price.delete_all

family.accounts.each do |account|
  account.balances.delete_all
  account.holdings.delete_all
end

# We can replace the Family.update_all and Family.find_each(&:broadcast_refresh) with this:
family.sync_later

end
19 changes: 19 additions & 0 deletions app/jobs/family_reset_job.rb
Original file line number Diff line number Diff line change
@@ -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 changes: 20 additions & 0 deletions app/views/settings/hostings/_danger_zone_settings.html.erb
Original file line number Diff line number Diff line change
@@ -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 %>
4 changes: 4 additions & 0 deletions app/views/settings/hostings/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@
<%= settings_section title: t(".invites") do %>
<%= render "settings/hostings/invite_code_settings" %>
<% end %>

<%= settings_section title: t(".danger_zone") do %>
<%= render "settings/hostings/danger_zone_settings" %>
<% end %>
48 changes: 34 additions & 14 deletions app/views/settings/profiles/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -127,20 +127,40 @@
<% end %>

<%= settings_section title: t(".danger_zone_title") do %>
<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 class="space-y-4">
<% if Current.user.admin? %>
<div class="flex items-center justify-between">
<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>
<%=
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>
<% end %>
5 changes: 5 additions & 0 deletions config/locales/views/settings/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ en:
body: Are you sure you want to permanently delete your account? This action
is irreversible.
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:
body: Are you sure you want to remove the invitation for %{email}?
title: Remove Invitation
Expand All @@ -49,6 +52,8 @@ en:
delete_account: Delete account
delete_account_warning: Deleting your account will permanently remove all
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
first_name: First Name
household_form_input_placeholder: Enter household name
Expand Down
9 changes: 9 additions & 0 deletions config/locales/views/settings/hostings/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ en:
general: General Settings
invites: Invite Codes
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:
api_calls_used: "%{used} / %{limit} API calls used (%{percentage})"
description: Input the API key provided by Synth
Expand All @@ -30,6 +36,8 @@ en:
update:
failure: Invalid setting value
success: Settings updated
clear_cache:
cache_cleared: Data cache has been cleared. This may take a few moments to complete.
upgrade_settings:
description: Configure how your application receives updates
latest_commit_description: Automatically update to the latest commit (unstable)
Expand All @@ -40,3 +48,4 @@ en:
manual_description: You control when to download and install updates
manual_title: Manual
title: Auto Upgrade
not_authorized: You are not authorized to perform this action
3 changes: 3 additions & 0 deletions config/locales/views/users/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ en:
email_change_initiated: Please check your new email address for confirmation
instructions.
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
8 changes: 6 additions & 2 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
resource :password, only: %i[edit update]
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
collection do
Expand All @@ -30,7 +32,9 @@
namespace :settings do
resource :profile, only: [ :show, :destroy ]
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 :security, only: :show
end
Expand Down
35 changes: 35 additions & 0 deletions test/controllers/settings/hostings_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,39 @@ class Settings::HostingsControllerTest < ActionDispatch::IntegrationTest
assert_equal NEW_RENDER_DEPLOY_HOOK, Setting.render_deploy_hook
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
35 changes: 35 additions & 0 deletions test/controllers/users_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,41 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_equal "Your profile has been updated.", flash[:notice]
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
sign_in @member = users(:family_member)
delete user_url(@member)
Expand Down