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

Simplify self host settings controller (#1230)

This commit is contained in:
Zach Gollwitzer 2024-10-02 12:07:56 -04:00 committed by GitHub
parent cb75c537fe
commit 7fabca4679
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 252 additions and 399 deletions

View file

@ -1,71 +1,43 @@
class Settings::HostingsController < SettingsController
before_action :verify_hosting_mode
before_action :raise_if_not_self_hosted
def show
@synth_usage = Current.family.synth_usage
end
def update
if all_updates_valid?
hosting_params.keys.each do |key|
Setting.send("#{key}=", hosting_params[key].strip)
end
if hosting_params[:upgrades_setting].present?
mode = hosting_params[:upgrades_setting] == "manual" ? "manual" : "auto"
target = hosting_params[:upgrades_setting] == "commit" ? "commit" : "release"
redirect_to settings_hosting_path, notice: t(".success")
else
flash.now[:error] = @errors.first.message
render :show, status: :unprocessable_entity
end
end
def send_test_email
unless Setting.smtp_settings_populated?
flash[:alert] = t(".missing_smtp_setting_error")
render(:show, status: :unprocessable_entity)
return
Setting.upgrades_mode = mode
Setting.upgrades_target = target
end
begin
NotificationMailer.with(user: Current.user).test_email.deliver_now
rescue => _e
flash[:alert] = t(".error")
render :show, status: :unprocessable_entity
return
if hosting_params.key?(:render_deploy_hook)
Setting.render_deploy_hook = hosting_params[:render_deploy_hook]
end
if hosting_params.key?(:require_invite_for_signup)
Setting.require_invite_for_signup = hosting_params[:require_invite_for_signup]
end
if hosting_params.key?(:synth_api_key)
Setting.synth_api_key = hosting_params[:synth_api_key]
end
redirect_to settings_hosting_path, notice: t(".success")
rescue ActiveRecord::RecordInvalid => error
flash.now[:alert] = t(".failure")
render :show, status: :unprocessable_entity
end
private
def all_updates_valid?
@errors = ActiveModel::Errors.new(Setting)
hosting_params.keys.each do |key|
setting = Setting.new(var: key)
setting.value = hosting_params[key].strip
unless setting.valid?
@errors.merge!(setting.errors)
end
end
if hosting_params[:upgrades_mode] == "auto" && hosting_params[:render_deploy_hook].blank?
@errors.add(:render_deploy_hook, t("settings.hostings.update.render_deploy_hook_error"))
end
@errors.empty?
end
def hosting_params
permitted_params = params.require(:setting).permit(:render_deploy_hook, :upgrades_mode, :email_sender, :app_domain, :smtp_host, :smtp_port, :smtp_username, :smtp_password, :require_invite_for_signup)
result = {}
result[:upgrades_mode] = permitted_params[:upgrades_mode] == "manual" ? "manual" : "auto" if permitted_params.key?(:upgrades_mode)
result[:render_deploy_hook] = permitted_params[:render_deploy_hook] if permitted_params.key?(:render_deploy_hook)
result[:upgrades_target] = permitted_params[:upgrades_mode] unless permitted_params[:upgrades_mode] == "manual" if permitted_params.key?(:upgrades_mode)
result.merge!(permitted_params.slice(:email_sender, :app_domain, :smtp_host, :smtp_port, :smtp_username, :smtp_password, :require_invite_for_signup))
result
params.require(:setting).permit(:render_deploy_hook, :upgrades_setting, :require_invite_for_signup, :synth_api_key)
end
def verify_hosting_mode
head :not_found unless self_hosted?
def raise_if_not_self_hosted
raise "Settings not available on non-self-hosted instance" unless self_hosted?
end
end

View file

@ -1,16 +1,3 @@
class ApplicationMailer < ActionMailer::Base
layout "mailer"
after_action :set_self_host_settings, if: -> { Rails.configuration.app_mode.self_hosted? }
private
def set_self_host_settings
mail.from = Setting.email_sender
mail.delivery_method.settings.merge!({ address: Setting.smtp_host,
port: Setting.smtp_port,
user_name: Setting.smtp_username,
password: Setting.smtp_password,
tls: ENV.fetch("SMTP_TLS_ENABLED", "true") == "true" })
end
end

View file

@ -1,5 +0,0 @@
class NotificationMailer < ApplicationMailer
def test_email
mail(to: params[:user].email, subject: t(".test_email_subject"), body: t(".test_email_body"))
end
end

View file

@ -18,13 +18,12 @@ module Providable
Provider::Github.new
end
def synth_provider
api_key = self_hosted? ? Setting.synth_api_key : ENV["SYNTH_API_KEY"]
api_key.present? ? Provider::Synth.new(api_key) : nil
end
private
def synth_provider
api_key = self_hosted? ? Setting.synth_api_key : ENV["SYNTH_API_KEY"]
api_key.present? ? Provider::Synth.new(api_key) : nil
end
def self_hosted?
Rails.application.config.app_mode.self_hosted?
end

View file

@ -1,4 +1,6 @@
class Family < ApplicationRecord
include Providable
has_many :users, dependent: :destroy
has_many :tags, dependent: :destroy
has_many :accounts, dependent: :destroy
@ -119,4 +121,8 @@ class Family < ApplicationRecord
def needs_sync?
last_synced_at.nil? || last_synced_at.to_date < Date.current
end
def synth_usage
self.class.synth_provider&.usage
end
end

View file

@ -9,6 +9,38 @@ class Provider::Synth
response = client.get("#{base_url}/user")
JSON.parse(response.body).dig("id").present?
end
def usage
response = client.get("#{base_url}/user")
if response.status == 401
return UsageResponse.new(
success?: false,
error: "Unauthorized: Invalid API key",
raw_response: response
)
end
parsed = JSON.parse(response.body)
remaining = parsed.dig("api_calls_remaining")
limit = parsed.dig("api_limit")
used = limit - remaining
UsageResponse.new(
used: used,
limit: limit,
utilization: used.to_f / limit * 100,
plan: parsed.dig("plan"),
success?: true,
raw_response: response
)
rescue StandardError => error
UsageResponse.new(
success?: false,
error: error,
raw_response: error
)
end
def fetch_security_prices(ticker:, start_date:, end_date:)
prices = paginate(
@ -96,6 +128,7 @@ class Provider::Synth
ExchangeRateResponse = Struct.new :rate, :success?, :error, :raw_response, keyword_init: true
SecurityPriceResponse = Struct.new :prices, :success?, :error, :raw_response, keyword_init: true
ExchangeRatesResponse = Struct.new :rates, :success?, :error, :raw_response, keyword_init: true
UsageResponse = Struct.new :used, :limit, :utilization, :plan, :success?, :error, :raw_response, keyword_init: true
def base_url
"https://api.synthfinance.com"

View file

@ -17,21 +17,7 @@ class Setting < RailsSettings::Base
default: ENV.fetch("UPGRADES_TARGET", "release"),
validates: { inclusion: { in: %w[release commit] } }
field :app_domain, type: :string, default: ENV["APP_DOMAIN"]
field :email_sender, type: :string, default: ENV["EMAIL_SENDER"]
field :synth_api_key, type: :string, default: ENV["SYNTH_API_KEY"]
field :require_invite_for_signup, type: :boolean, default: false
scope :smtp_settings do
field :smtp_host, type: :string, read_only: true, default: ENV["SMTP_ADDRESS"]
field :smtp_port, type: :string, read_only: true, default: ENV["SMTP_PORT"]
field :smtp_username, type: :string, read_only: true, default: ENV["SMTP_USERNAME"]
field :smtp_password, type: :string, read_only: true, default: ENV["SMTP_PASSWORD"]
end
def self.smtp_settings_populated?
Setting.defined_fields.select { |f| f.scope == :smtp_settings }.map(&:read).all?(&:present?)
end
end

View file

@ -0,0 +1,34 @@
<div class="space-y-4">
<div class="flex items-center justify-between">
<div class="space-y-1">
<p class="text-sm"><%= t(".title") %></p>
<p class="text-gray-500 text-sm"><%= t(".description") %></p>
</div>
<%= styled_form_with model: Setting.new, url: settings_hosting_path, method: :patch, data: { controller: "auto-submit-form", "auto-submit-form-trigger-event-value" => "blur" } do |form| %>
<div class="relative inline-block select-none">
<%= form.check_box :require_invite_for_signup, class: "sr-only peer", "data-auto-submit-form-target": "auto", "data-autosubmit-trigger-event": "input" %>
<%= form.label :require_invite_for_signup, "&nbsp;".html_safe, class: "maybe-switch" %>
</div>
<% end %>
</div>
<% if Setting.require_invite_for_signup %>
<div class="flex items-center justify-between mb-4">
<div>
<span class="text-gray-900 text-base font-medium"><%= t(".generated_tokens") %></span>
</div>
<div>
<%= button_to invite_codes_path,
method: :post,
class: "flex gap-1 bg-gray-50 text-gray-900 text-sm rounded-lg px-3 py-2" do %>
<span><%= t(".generate_tokens") %></span>
<% end %>
</div>
</div>
<div>
<%= turbo_frame_tag :invite_codes, src: invite_codes_path %>
</div>
<% end %>
</div>

View file

@ -0,0 +1,9 @@
<% if ENV["HOSTING_PLATFORM"] == "render" %>
<%= styled_form_with model: Setting.new, url: settings_hosting_path, method: :patch, data: { controller: "auto-submit-form", "auto-submit-form-trigger-event-value" => "blur" } do |form| %>
<div class="space-y-4">
<h2 class="font-medium mb-1"><%= t(".title") %></h2>
<p class="text-gray-500 text-sm mb-4"><%= t(".description") %></p>
<%= form.url_field :render_deploy_hook, label: t(".render_deploy_hook_label"), placeholder: t(".render_deploy_hook_placeholder"), value: Setting.render_deploy_hook, data: { "auto-submit-form-target" => "auto" } %>
</div>
<% end %>
<% end %>

View file

@ -0,0 +1,44 @@
<div class="space-y-4">
<div>
<h2 class="font-medium mb-1"><%= t(".title") %></h2>
<p class="text-gray-500 text-sm mb-4"><%= t(".description") %></p>
</div>
<%= styled_form_with model: Setting.new,
url: settings_hosting_path,
method: :patch,
data: {
controller: "auto-submit-form",
"auto-submit-form-trigger-event-value": "blur"
} do |form| %>
<%= form.text_field :synth_api_key,
label: t(".label"),
type: "password",
placeholder: t(".placeholder"),
value: Setting.synth_api_key,
container_class: @synth_usage.present? && !@synth_usage.success? ? "border-red-500" : "",
data: { "auto-submit-form-target": "auto" } %>
<% end %>
<% if @synth_usage.present? && @synth_usage.success? %>
<div class="space-y-4">
<div class="space-y-2">
<p class="text-sm text-gray-500">
<%= t(".api_calls_used",
used: number_with_delimiter(@synth_usage.used),
limit: number_with_delimiter(@synth_usage.limit),
percentage: number_to_percentage(@synth_usage.utilization, precision: 1)) %>
</p>
<div class="w-52 h-1.5 bg-gray-100 rounded-2xl">
<div class="h-full bg-green-500 rounded-2xl"
style="width: <%= [@synth_usage.utilization, 2].max %>%;"></div>
</div>
</div>
<div class="bg-gray-100 rounded-md px-1.5 py-0.5 w-fit">
<p class="text-xs font-medium text-gray-500 uppercase">
<%= t(".plan", plan: @synth_usage.plan) %>
</p>
</div>
</div>
<% end %>
</div>

View file

@ -0,0 +1,41 @@
<% if ENV["HOSTING_PLATFORM"] == "render" %>
<div>
<h2 class="font-medium mb-1"><%= t(".title") %></h2>
<p class="text-gray-500 text-sm mb-4"><%= t(".description") %></p>
<%= styled_form_with model: Setting.new, url: settings_hosting_path, method: :patch, data: { controller: "auto-submit-form", "auto-submit-form-trigger-event-value" => "blur" } do |form| %>
<div class="space-y-4">
<div class="flex items-center gap-4">
<%= form.radio_button :upgrades_setting, "manual", checked: Setting.upgrades_mode == "manual", data: { "auto-submit-form-target" => "auto", "autosubmit-trigger-event": "input" } %>
<%= form.label :upgrades_mode_manual, t(".manual_title"), class: "text-gray-900 text-sm" do %>
<span class="font-medium"><%= t(".manual_title") %></span>
<br>
<span class="text-gray-500">
<%= t(".manual_description") %>
</span>
<% end %>
</div>
<div class="flex items-center gap-4">
<%= form.radio_button :upgrades_setting, "release", checked: Setting.upgrades_mode == "auto" && Setting.upgrades_target == "release", data: { "auto-submit-form-target" => "auto", "autosubmit-trigger-event": "input" } %>
<%= form.label :upgrades_mode_release, t(".latest_release_title"), class: "text-gray-900 text-sm" do %>
<span class="font-medium"><%= t(".latest_release_title") %></span>
<br>
<span class="text-gray-500">
<%= t(".latest_release_description") %>
</span>
<% end %>
</div>
<div class="flex items-center gap-4">
<%= form.radio_button :upgrades_setting, "commit", checked: Setting.upgrades_mode == "auto" && Setting.upgrades_target == "commit", data: { "auto-submit-form-target" => "auto", "autosubmit-trigger-event": "input" } %>
<%= form.label :upgrades_mode_commit, t(".latest_commit_title"), class: "text-gray-900 text-sm" do %>
<span class="font-medium"><%= t(".latest_commit_title") %></span>
<br>
<span class="text-gray-500">
<%= t(".latest_commit_description") %>
</span>
<% end %>
</div>
</div>
<% end %>
</div>
<% end %>

View file

@ -3,122 +3,19 @@
<% end %>
<div class="space-y-4 pb-32">
<h1 class="text-gray-900 text-xl font-medium mb-4"><%= t(".page_title") %></h1>
<h1 class="text-gray-900 text-xl font-medium mb-4"><%= t(".title") %></h1>
<% if ENV["HOSTING_PLATFORM"] == "render" %>
<%= settings_section title: t(".general_settings_title") do %>
<%= styled_form_with model: Setting.new, url: settings_hosting_path, method: :patch, data: { controller: "auto-submit-form", "auto-submit-form-trigger-event-value" => "blur" } do |form| %>
<h2 class="font-medium mb-1"><%= t(".upgrades.title") %></h2>
<p class="text-gray-500 text-sm mb-4"><%= t(".upgrades.description") %></p>
<div class="space-y-4">
<div class="flex items-center gap-4">
<%= form.radio_button :upgrades_mode, "manual", checked: Setting.upgrades_mode == "manual", data: { "auto-submit-form-target" => "auto", "autosubmit-trigger-event": "input" } %>
<%= form.label :upgrades_mode_manual, t(".upgrades.manual.title"), class: "text-gray-900 text-sm" do %>
<span class="font-medium"><%= t(".upgrades.manual.title") %></span>
<br>
<span class="text-gray-500">
<%= t(".upgrades.manual.description") %>
</span>
<% end %>
</div>
<div class="flex items-center gap-4">
<%= form.radio_button :upgrades_mode, "release", checked: Setting.upgrades_mode == "auto" && Setting.upgrades_target == "release", data: { "auto-submit-form-target" => "auto", "autosubmit-trigger-event": "input" } %>
<%= form.label :upgrades_mode_release, t(".upgrades.latest_release.title"), class: "text-gray-900 text-sm" do %>
<span class="font-medium"><%= t(".upgrades.latest_release.title") %></span>
<br>
<span class="text-gray-500">
<%= t(".upgrades.latest_release.description") %>
</span>
<% end %>
</div>
<div class="flex items-center gap-4">
<%= form.radio_button :upgrades_mode, "commit", checked: Setting.upgrades_mode == "auto" && Setting.upgrades_target == "commit", data: { "auto-submit-form-target" => "auto", "autosubmit-trigger-event": "input" } %>
<%= form.label :upgrades_mode_commit, t(".upgrades.latest_commit.title"), class: "text-gray-900 text-sm" do %>
<span class="font-medium"><%= t(".upgrades.latest_commit.title") %></span>
<br>
<span class="text-gray-500">
<%= t(".upgrades.latest_commit.description") %>
</span>
<% end %>
</div>
</div>
<div>
<h2 class="font-medium mb-1"><%= t(".provider_settings.title") %></h2>
<p class="text-gray-500 text-sm mb-4"><%= t(".render_deploy_hook_description") %></p>
<%= form.url_field :render_deploy_hook, label: t(".render_deploy_hook_label"), placeholder: t(".render_deploy_hook_placeholder"), value: Setting.render_deploy_hook, data: { "auto-submit-form-target" => "auto" } %>
</div>
<% end %>
<% end %>
<% end %>
<%= settings_section title: t(".smtp_settings.title") do %>
<%= styled_form_with model: Setting.new, url: settings_hosting_path, method: :patch, data: { controller: "auto-submit-form", "auto-submit-form-trigger-event-value" => "blur" } do |form| %>
<p class="text-gray-500 text-sm mb-4"><%= t(".smtp_settings.description") %></p>
<div class="space-y-4">
<div class="space-y-3">
<%= form.text_field :email_sender, label: t(".email_sender"), placeholder: t(".email_sender_placeholder"), value: Setting.email_sender, data: { "auto-submit-form-target" => "auto" } %>
<%= form.text_field :app_domain, label: t(".domain"), placeholder: t(".domain_placeholder"), value: Setting.app_domain, data: { "auto-submit-form-target" => "auto" } %>
<%= form.text_field :smtp_host, label: t(".smtp_settings.host"), placeholder: t(".smtp_settings.host_placeholder"), value: Setting.smtp_host, data: { "auto-submit-form-target" => "auto" } %>
<%= form.number_field :smtp_port, label: t(".smtp_settings.port"), placeholder: t(".smtp_settings.port_placeholder"), value: Setting.smtp_port, data: { "auto-submit-form-target" => "auto" } %>
<%= form.text_field :smtp_username, label: t(".smtp_settings.username"), placeholder: t(".smtp_settings.username_placeholder"), value: Setting.smtp_username, data: { "auto-submit-form-target" => "auto" } %>
<%= form.password_field :smtp_password, label: t(".smtp_settings.password"), placeholder: t(".smtp_settings.password_placeholder"), value: Setting.smtp_password, data: { "auto-submit-form-target" => "auto" } %>
</div>
<div class="flex items-center justify-between bg-white border border-alpha-black-100 p-4 rounded-lg">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-gray-25 flex items-center justify-center">
<%= lucide_icon "mails", class: "w-6 h-6 text-gray-500" %>
</div>
<div>
<p class="text-gray-900 font-medium text-sm"><%= t(".smtp_settings.send_test_email") %></p>
<p class="text-gray-500 text-sm"><%= t(".smtp_settings.send_test_email_description") %></p>
</div>
</div>
<div>
<%= link_to t(".smtp_settings.send_test_email_button"), send_test_email_settings_hosting_path, data: { turbo_method: :post }, class: "bg-gray-50 text-gray-900 text-sm font-medium rounded-lg px-3 py-2" %>
</div>
</div>
</div>
<% end %>
<% end %>
<%= settings_section title: t(".invite_settings.title") do %>
<div class="space-y-4">
<div class="flex items-center justify-between">
<div class="space-y-1">
<p class="text-sm"><%= t(".invite_settings.require_invite_for_signup") %></p>
<p class="text-gray-500 text-sm"><%= t(".invite_settings.invite_code_description") %></p>
</div>
<%= styled_form_with model: Setting.new, url: settings_hosting_path, method: :patch, data: { controller: "auto-submit-form", "auto-submit-form-trigger-event-value" => "blur" } do |form| %>
<div class="relative inline-block select-none">
<%= form.check_box :require_invite_for_signup, class: "sr-only peer", "data-auto-submit-form-target": "auto", "data-autosubmit-trigger-event": "input" %>
<%= form.label :require_invite_for_signup, "&nbsp;".html_safe, class: "maybe-switch" %>
</div>
<% end %>
</div>
<% if Setting.require_invite_for_signup %>
<div class="flex items-center justify-between mb-4">
<div>
<span class="text-gray-900 text-base font-medium"><%= t(".invite_settings.generated_tokens") %></span>
</div>
<div>
<%= button_to invite_codes_path,
method: :post,
class: "flex gap-1 bg-gray-50 text-gray-900 text-sm rounded-lg px-3 py-2" do %>
<span><%= t(".invite_settings.generate_tokens") %></span>
<% end %>
</div>
</div>
<div>
<%= turbo_frame_tag :invite_codes, src: invite_codes_path %>
</div>
<% end %>
<%= settings_section title: t(".general") do %>
<div class="space-y-6">
<%= render "settings/hostings/upgrade_settings" %>
<%= render "settings/hostings/provider_settings" %>
<%= render "settings/hostings/synth_settings" %>
</div>
<% end %>
<%= settings_section title: t(".invites") do %>
<%= render "settings/hostings/invite_code_settings" %>
<% end %>
<%= settings_nav_footer %>
</div>