mirror of
https://github.com/we-promise/sure.git
synced 2026-07-12 12:55:20 +00:00
* feat(api): allow multiple active API keys per user Previously a user could hold only one active API key; creating a new one silently revoked the existing key, breaking any app using it. Allow multiple named active keys instead. - Drop the one-active-key-per-source validation; add name uniqueness among the user's active visible keys (revoked names are reusable, same name allowed across users). - Rewrite Settings::ApiKeysController as a RESTful collection (index/show/new/create/destroy); create no longer revokes existing keys, and key lookup is scoped to the current user's active keys. - resource :api_key -> resources :api_keys. - Add an API keys list view with per-key revoke and an empty state; rewrite the show page for a single key. - Update i18n (remove single-key copy, pluralise nav label) and tests. * refactor(api): address review on multiple API keys - Remove unreachable destroy branches (cannot_revoke is guarded by the .visible 404; revoke! raises rather than returning false) and document that .visible is the demo-key revocation guard. - Delete the orphaned created.html.erb / created.turbo_stream.erb templates (no action renders them) and their unused locale keys. - Extract shared partials (_scope_badges, _status_indicator, _key_meta, _key_reveal, _usage) to de-duplicate the index and show views; unify the active-status indicator on the standard dot. - Carry forward the @container / @lg:flex-row / min-w-0 responsive fixes from #2079 into the shared key-reveal partial. * test(api): cover newly-created API key confirmation render * refactor(api): harden demo-key guard and address review nits - Document the demo-key revocation guard on ApiKey's `visible` scope (the authoritative spot) and add a model test locking the invariant that `.visible` excludes the demo monitoring key. - _scope_badges: use an i18n lookup with a humanize fallback instead of bypassing translation for unknown scopes. - _key_reveal: drop the hard-coded `id` from the shared partial; the system test now locates the key via its data-clipboard-target. * refactor(api-keys): migrate hand-rolled badges to DS::Pill Replace raw span elements in scope badges and status indicator with DS::Pill to align with the design system migration convention. * fix(loans): opening anchor now uses current balance, not original principal When creating a loan manually, the opening anchor valuation was being set to `initial_balance` (the original loan principal) instead of `account.balance` (the current outstanding balance). After the sync job ran, `account.balance` was overwritten to match the anchor, making every manually-created loan show its original principal as the current balance. Fix by always using `account.balance` for the opening anchor in `create_and_sync`, and reading `Loan#original_balance` from the `loans.initial_balance` column directly (with a fallback to `first_valuation_amount` for provider-synced loans that may not have the column populated). * fix(api-keys): strip accidental loan changes; rescue revoke! failures The fix(loans) commit was accidentally committed into this branch. Remove the loan-related changes from account.rb, loan.rb, and both test files, restoring them to their pre-loan-commit state. Also fix the destroy action: revoke! uses update! internally which raises ActiveRecord::RecordInvalid on failure rather than returning false. Add rescue for RecordInvalid and RecordNotDestroyed so failures produce a flash alert instead of a 500. Re-adds the revoke_failed locale key that was dropped from settings.api_keys.destroy. --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
117 lines
5.3 KiB
Plaintext
117 lines
5.3 KiB
Plaintext
<%
|
|
nav_sections = [
|
|
{
|
|
header: t(".general_section_title"),
|
|
items: [
|
|
{ label: t(".accounts_label"), path: accounts_path, icon: "layers" },
|
|
{ label: t(".bank_sync_label"), path: settings_providers_path, icon: "banknote", if: Current.user&.admin? },
|
|
{ label: t(".preferences_label"), path: settings_preferences_path, icon: "bolt" },
|
|
{ label: t(".appearance_label"), path: settings_appearance_path, icon: "palette" },
|
|
{ label: t(".profile_label"), path: settings_profile_path, icon: "circle-user" },
|
|
{ label: t(".security_label"), path: settings_security_path, icon: "shield-check" },
|
|
{ label: t(".payment_label"), path: settings_payment_path, icon: "circle-dollar-sign", if: !self_hosted? && Current.family&.can_manage_subscription? }
|
|
]
|
|
},
|
|
{
|
|
header: t(".transactions_section_title"),
|
|
items: [
|
|
{ label: t(".categories_label"), path: categories_path, icon: "shapes" },
|
|
{ label: t(".tags_label"), path: tags_path, icon: "tags" },
|
|
{ label: t(".rules_label"), path: rules_path, icon: "git-branch" },
|
|
{ label: t(".merchants_label"), path: family_merchants_path, icon: "store" },
|
|
{ label: t(".recurring_transactions_label"), path: recurring_transactions_path, icon: "repeat" },
|
|
{ label: t(".statement_vault_label"), path: account_statements_path, icon: "archive", if: Current.user&.admin? }
|
|
]
|
|
},
|
|
(
|
|
Current.user&.admin? ? {
|
|
header: t(".advanced_section_title"),
|
|
items: [
|
|
{ label: t(".ai_prompts_label"), path: settings_ai_prompts_path, icon: "bot" },
|
|
{ label: t(".llm_usage_label"), path: settings_llm_usage_path, icon: "activity" },
|
|
{ label: t(".api_keys_label"), path: settings_api_keys_path, icon: "key" },
|
|
{ label: t(".mcp_label"), path: settings_mcp_path, icon: "plug" },
|
|
{ label: t(".debug_label", default: "Debug"), path: settings_debug_path, icon: "bug", if: Current.user&.super_admin? },
|
|
{ label: t(".self_hosting_label"), path: settings_hosting_path, icon: "database", if: self_hosted? },
|
|
{ label: t(".imports_label"), path: imports_path, icon: "download" },
|
|
{ label: t(".exports_label"), path: family_exports_path, icon: "upload" },
|
|
{ label: t(".sso_providers_label"), path: admin_sso_providers_path, icon: "key-round", if: Current.user&.super_admin? },
|
|
{ label: t(".users_label"), path: admin_users_path, icon: "users", if: Current.user&.super_admin? }
|
|
]
|
|
} : nil
|
|
),
|
|
{
|
|
header: t(".other_section_title"),
|
|
items: [
|
|
{ label: t(".guides_label"), path: settings_guides_path, icon: "book-open" },
|
|
{ label: t(".whats_new_label"), path: changelog_path, icon: "box" },
|
|
{ label: t(".feedback_label"), path: feedback_path, icon: "megaphone" }
|
|
]
|
|
}
|
|
]
|
|
%>
|
|
|
|
<div class="space-y-4">
|
|
<div class="hidden lg:flex items-center gap-2 p-1.5">
|
|
<%= render DS::Link.new(
|
|
text: t("settings.settings_nav_link_large.previous"),
|
|
icon: "chevron-left",
|
|
href: previous_path,
|
|
variant: "ghost",
|
|
) %>
|
|
<%= link_to previous_path, class: "hidden md:block uppercase bg-surface-inset-hover rounded-sm px-1 py-0.5 text-xs text-secondary shadow-sm ml-1 pointer-events-none", data: { controller: "hotkey", hotkey: "Escape" } do %>
|
|
<kbd>esc</kbd>
|
|
<% end %>
|
|
</div>
|
|
<nav class="space-y-4 hidden md:block">
|
|
<% nav_sections.compact.each do |section| %>
|
|
<section class="space-y-2">
|
|
<div class="flex items-center gap-2 px-3">
|
|
<h3 class="uppercase text-secondary font-medium text-xs"><%= section[:header] %></h3>
|
|
<%= render "shared/ruler", classes: "w-full" %>
|
|
</div>
|
|
<ul class="space-y-1">
|
|
<% section[:items].each do |item| %>
|
|
<% next if item[:if] == false %>
|
|
<li>
|
|
<%= render "settings/settings_nav_item", name: item[:label], path: item[:path], icon: item[:icon] %>
|
|
</li>
|
|
<% end %>
|
|
</ul>
|
|
</section>
|
|
<% end %>
|
|
<section>
|
|
<%= button_to session_path(Current.session), method: :delete, class: "flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-destructive hover:bg-surface-hover w-full" do %>
|
|
<%= icon("log-out", color: "current") %>
|
|
<span><%= t(".logout") %></span>
|
|
<% end %>
|
|
</section>
|
|
</nav>
|
|
<nav class="space-y-4 overflow-x-auto md:hidden no-scrollbar overscroll-none" id="mobile-settings-nav" data-controller="preserve-scroll scroll-on-connect">
|
|
<ul class="flex flex-nowrap space-x-1">
|
|
<li>
|
|
<%= render DS::Link.new(
|
|
text: t("settings.settings_nav_link_large.previous"),
|
|
icon: "chevron-left",
|
|
href: previous_path,
|
|
variant: "ghost",
|
|
) %>
|
|
</li>
|
|
<% nav_sections.compact.each do |section| %>
|
|
<% section[:items].each do |item| %>
|
|
<% next if item[:if] == false %>
|
|
<li>
|
|
<%= render "settings/settings_nav_item", name: item[:label], path: item[:path], icon: item[:icon] %>
|
|
</li>
|
|
<% end %>
|
|
<% end %>
|
|
<li>
|
|
<%= button_to session_path(Current.session), method: :delete, class: "flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-destructive hover:bg-surface-hover w-full" do %>
|
|
<%= icon("log-out", color: "current") %>
|
|
<span><%= t(".logout") %></span>
|
|
<% end %>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
</div>
|