Wire admin add-account endpoint into the UI #46

Merged
padreug merged 12 commits from feat/add-account-ui into main 2026-06-18 10:03:10 +00:00
Owner

Surfaces the existing POST /api/v1/admin/accounts endpoint in the Libra UI, plus a backend hardening fix in the same code path.

What

  • UI (feat): super-user-only Add Account button on the Chart of Accounts card → dialog for the hierarchical account name + optional description → posts with the wallet admin key (require_super_user) → reloads accounts. Client-side prefix validation mirrors the server's _VALID_ACCOUNT_PREFIXES.
  • No currency input: an open directive's currency constraint is optional (verified against Beancount's grammar), and the account-creation flow doesn't need it — so add_account's currencies arg is now optional and the directive is written unconstrained when omitted.
  • Fix (fava): add_account wrote free-text metadata values straight into the ledger source via /api/source with no escaping. An unescaped quote or newline in an admin-supplied description would corrupt the Beancount file or forge extra metadata lines. Now escapes backslash/quote/newline per the tokenizer's cunescape rules.

Verification

  • fava_client.py / views_api.py compile; index.js lints.
  • Escape logic round-tripped through Beancount's actual parser across plain / quoted / backslash / newline / injection-attempt inputs — all recovered the original string intact (the evil"\n account: Assets:Hack case is contained as a string value rather than breaking out into a forged metadata line).
  • Not yet click-tested in a running LNbits instance.

Commits

  • fix(fava): escape string metadata + make Open currencies optional
  • feat(ui): wire admin add-account endpoint into Chart of Accounts

Follow-ups

  • Booking method for cost-basis accounts — #45.
  • Account search-keywords/aliases (filed separately).

🤖 Generated with Claude Code

Surfaces the existing `POST /api/v1/admin/accounts` endpoint in the Libra UI, plus a backend hardening fix in the same code path. ## What - **UI (feat):** super-user-only **Add Account** button on the Chart of Accounts card → dialog for the hierarchical account name + optional description → posts with the wallet admin key (`require_super_user`) → reloads accounts. Client-side prefix validation mirrors the server's `_VALID_ACCOUNT_PREFIXES`. - **No currency input:** an `open` directive's currency constraint is optional (verified against Beancount's grammar), and the account-creation flow doesn't need it — so `add_account`'s `currencies` arg is now optional and the directive is written unconstrained when omitted. - **Fix (fava):** `add_account` wrote free-text metadata values straight into the ledger source via `/api/source` with **no escaping**. An unescaped quote or newline in an admin-supplied `description` would corrupt the Beancount file or forge extra metadata lines. Now escapes backslash/quote/newline per the tokenizer's `cunescape` rules. ## Verification - `fava_client.py` / `views_api.py` compile; `index.js` lints. - Escape logic round-tripped through Beancount's **actual parser** across plain / quoted / backslash / newline / injection-attempt inputs — all recovered the original string intact (the `evil"\n account: Assets:Hack` case is contained as a string value rather than breaking out into a forged metadata line). - Not yet click-tested in a running LNbits instance. ## Commits - `fix(fava): escape string metadata + make Open currencies optional` - `feat(ui): wire admin add-account endpoint into Chart of Accounts` ## Follow-ups - Booking method for cost-basis accounts — #45. - Account search-keywords/aliases (filed separately). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
add_account wrote free-text metadata values straight into the ledger
source via /api/source with no escaping — an unescaped quote or newline
in an admin-supplied description would corrupt the Beancount file (or
forge extra metadata lines). Escape backslash/quote/newline per the
tokenizer's cunescape rules (verified round-trip through beancount's
parser). Also make the currency constraint list optional so an Open
directive can be written unconstrained (currencies are an optional
part of the directive, not required).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Surface the existing POST /api/v1/admin/accounts endpoint in the UI: a
super-user-only 'Add Account' button on the Chart of Accounts card opens
a dialog for the hierarchical account name + optional description, posts
with the wallet admin key (require_super_user), then reloads accounts.
Client-side prefix validation mirrors the server's _VALID_ACCOUNT_PREFIXES.
No currency input — an Open directive does not require currency constraints.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The UI omits currencies so the Open directive is written unconstrained,
but the model defaulted currencies to ["EUR","SATS","USD"], so Pydantic
refilled them and the endpoint passed the constraint through — every
admin-created account got a currency-constrained Open (which would
reject postings in other currencies, the same CAD/GBP/JPY bean-check
class we hit on user accounts). Default to None so omission reaches
add_account and the directive is unconstrained; an explicit list still
works for API callers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
add_account no-ops if the Open directive is already present but returned
a normal-looking dict, so the admin endpoint reported success ('created
(sync pending)') for a duplicate. Return an already_existed flag and
raise 409 from the endpoint. Also anchor the existence check on the Open
directive with a trailing-boundary match so a prefix (Expenses:Gas)
doesn't match a longer sibling (Expenses:GasStation). The flag is
additive, so the idempotent user-account path keeps no-opping silently.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Free-typing the full hierarchical name let admins fat-finger the parent
(wrong/invalid root). Replace the single name field with a required
Account Type select (the 5 valid roots, mirroring _VALID_ACCOUNT_PREFIXES)
plus a sub-account input, a live 'Will create: ...' preview, and
per-segment validation (each part must be a capitalized Beancount
account component). The root prefix is now structurally guaranteed valid.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The endpoint only checked the root prefix, so a direct API call (bypassing
the UI) could write a malformed Open directive into the ledger source.
Add _validate_account_name mirroring Beancount's core/account.py grammar
(root [\p{Lu}][\p{L}\p{Nd}-]*, sub [\p{Lu}\p{Nd}][\p{L}\p{Nd}-]*, >=1
sub-account) — verified to match beancount.core.account.is_valid across
20 cases incl. Unicode, digit-start subs, hyphens. Align the client
segment regex to the same rule (was ASCII-only, rejected valid names).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The test harness was never updated to the post-server-deploy#4 split ledger
layout, so libra's per-user account opens (routed to accounts/users.beancount
by fava_client._infer_target_file) 500'd as a 'non-source file' and fell back
to DB-only — breaking the balance test and contributing to settlement errors.
Make the harness ledger a faithful split (root includes accounts/chart.beancount
+ accounts/users.beancount; title stays in root so the slug still matches).

Also raise lnbits_rate_limit_no for the session: the full suite fires >200
req/min and the default limiter 429'd fixture setup intermittently (10-11
errors). The limiter is built once at app creation, so setting it in the
session settings fixture (before the app fixture) disables it suite-wide.

Net: full suite goes from 1 failed / ~10 errors to fully green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
10 integration tests for POST /api/v1/admin/accounts: unconstrained Open
write + escaped description metadata, explicit-currency path, duplicate->409,
invalid-prefix->400, invalid-characters->400 (parametrized), super-user-only
->403. Adds the add_chart_account helper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The existence check matched 'open <name>' anywhere in the chart source,
so a prior account's description metadata or a comment mentioning the
name produced a false 409, while a real directive with an inline comment
and no space ('open X;legacy') was missed → a duplicate Open was appended
and bean-check then rejected the file, breaking every later /api/source
write. Extract the check into a pure _open_directive_exists() anchored to
'^YYYY-MM-DD open <name>' with an account-boundary negative-lookahead, and
unit-test both failure directions plus prefix/child non-matches.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
get_or_create_user_account opened per-user receivable/payable accounts
constrained to EUR/SATS/USD, so a posting in any other currency tripped
'Invalid currency CAD/GBP/JPY for account Assets:Receivable:User-…' at
bean-check — the exact errors the optional-currencies work set out to fix,
which had only reached the admin chart-account path. Open user accounts
unconstrained (currencies=None) so they hold arbitrary fiat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When add_account reported the Open already existed, the endpoint raised
409 before the DB-mirror step — so an account present in the ledger but
missing from libra's DB (a prior sync failure with no cross-DB atomicity,
or an out-of-band open) was stranded: invisible to permissions with no
recovery path. Now 409 only when the account is already in the DB too;
otherwise sync it and return success. Adds a recovery test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_open_directive_exists hardcoded '^YYYY-MM-DD open ' (dash-only, 2-digit,
single-space), but Beancount's DATE token (parser/lexer.l) is
(17|18|19|20)[0-9]{2}[-/][0-9]+[-/][0-9]+ and inter-token whitespace is any
[ \t\r] run. So a validly-formatted existing Open written as '2024/3/5 open X'
or '2020-01-01  open  X' escaped detection → duplicate Open appended →
bean-check rejects the file. Anchor on Beancount's actual date pattern and
[ \t]+ separators. Adds parametrized coverage for slash/single-digit/multi-
space/tab variants.

Found in a coherence pass over the Beancount source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
padreug deleted branch feat/add-account-ui 2026-06-18 10:03:11 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/libra!46
No description provided.