invoices: payout addresses in the database
All checks were successful
Build and Deploy to Production / test (push) Successful in 26s
Build and Deploy to Production / build (push) Successful in 59s
Build and Deploy to Production / deploy (push) Successful in 38s

Default crypto payout addresses lived in the CRYPTO_ADDRESSES and
LTC_ADDRESS Forgejo secrets, so changing one meant editing a secret and
redeploying. Keep them in a payout_addresses table edited at
/admin/invoices/payout-addresses (read to view, write to change, every
change audited), and prefill the new-invoice form from it.

The old env values are imported once on the first boot with an empty
table, so the current addresses carry over; after that the secrets can
be removed.

Assisted-by: Claude:opus-5
This commit is contained in:
Sergei Poljanski 2026-10-10 08:36:32 +04:00
commit 1dd8f9081f
Signed by: asxpi
GPG key ID: 4F8851660FA4121B
10 changed files with 179 additions and 14 deletions

View file

@ -1,7 +1,7 @@
<%== erb :'admin/_nav', layout: false %>
<div class="header">
<h1>Invoices</h1>
<% if can?('invoices', 'write') %><p class="tagline"><a href="/admin/invoices/new">+ New invoice</a></p><% end %>
<p class="tagline"><% if can?('invoices', 'write') %><a href="/admin/invoices/new">+ New invoice</a> · <% end %><a href="/admin/invoices/payout-addresses">payout addresses</a></p>
</div>
<% if @invoices.empty? %>

View file

@ -97,7 +97,7 @@
<div class="field">
<label for="crypto_address">Address</label>
<input type="text" id="crypto_address" name="crypto_address"
value="<%= @form_values[:crypto_address] || CryptoAsset.default_addresses[@form_values[:crypto_coin] || 'LTC'] %>"
value="<%= @form_values[:crypto_address] || PayoutAddress.map[@form_values[:crypto_coin] || 'LTC'] %>"
placeholder="payout address" maxlength="128" />
</div>
</div>
@ -115,7 +115,7 @@
placeholder="auto" inputmode="decimal" />
</div>
</div>
<script type="application/json" id="crypto-defaults"><%== CryptoAsset.default_addresses.to_json %></script>
<script type="application/json" id="crypto-defaults"><%== PayoutAddress.map.to_json %></script>
<button type="submit" class="download-button">Create invoice</button>
</form>

View file

@ -0,0 +1,30 @@
<%== erb :'admin/_nav', layout: false %>
<div class="header">
<h1>Payout addresses</h1>
<p class="tagline"><a href="/admin/invoices">← invoices</a> · default address per coin, prefilled into new invoices</p>
</div>
<% if @flash %><div class="form-error-base"><div><%= @flash %></div></div><% end %>
<section class="section">
<form class="invoice-form" method="post" action="/admin/invoices/payout-addresses" autocomplete="off">
<input type="hidden" name="authenticity_token" value="<%= csrf_token %>" />
<fieldset class="plain" <%= 'disabled' unless can?('invoices', 'write') %>>
<table class="invoice-list">
<thead><tr><th>Coin</th><th>Address</th><th>Updated</th></tr></thead>
<tbody>
<% CryptoAsset::CODES.each do |coin| %>
<% row = @addresses[coin] %>
<tr>
<td><b class="mono"><%= coin %></b><br /><span class="optional"><%= CryptoAsset.name(coin) %></span></td>
<td><input type="text" name="address[<%= coin %>]" value="<%= row&.address %>" class="mono" placeholder="not set" maxlength="128" spellcheck="false" /></td>
<td class="optional"><%= row ? "#{row.updated_at.strftime('%Y-%m-%d')} · #{row.updated_by}" : '—' %></td>
</tr>
<% end %>
</tbody>
</table>
<p class="optional">Leave a field empty to remove that default. Existing invoices keep the address captured when they were issued.</p>
<% if can?('invoices', 'write') %><button type="submit" class="download-button">Save addresses</button><% end %>
</fieldset>
</form>
</section>