Repository navigation
feat(api,riexchange): symmetric dual-auth + Approve button for approveRIExchange (mirror of #286) #300
Description
Background
PR for #286 added in-dashboard purchase approval via the approve-{any,own}:purchases RBAC matrix and a session-first dispatch in internal/api/handler_purchases.go::approvePurchase. The companion approveRIExchange at internal/api/handler_ri_exchange.go:580 has the same shape gap (token-only auth, no session-authed branch) but mutates a state-machine and triggers downstream cloud-side execution — substantially larger surface than fits in #286's PR.
This issue tracks the symmetric work.
What needs to happen
Apply the same dual-auth shape approvePurchase now uses, adapted to the RI-exchange flow:
-
Reuse the existing
approve-{any,own}:purchasesverbs — the RI-exchange approval is conceptually still "approving a purchase action", just on a different resource type. Don't add new RI-specific verbs unless an operational reason emerges. (Alternative if you want stricter isolation: addapprove-{any,own}:ri-exchangesand gate separately. Default to reuse to keep the matrix small.) -
Refactor
approveRIExchangeinto a three-mode dispatch likeapprovePurchase:- Session present + RBAC matches → session-authed approve.
- token != "" → legacy email-link, unchanged.
- token == "" → session-authed branch.
Permission-denied (403) on the session check falls through to the token branch (legacy operators retain their email-link path).
-
approveRIExchangeViaSessionthat:- Calls
validateExchangeApprovalminus the token-equality check (session is the auth) but keeping the existing-record + state-machine state guards. - Calls
TransitionRIExchangeStatus(pending → processing). The atomic transition there already handles concurrent cancel/expire — that protection remains. - Calls the existing
executeApprovedExchangewith the session-supplied actor. - Stamps approver attribution somewhere on the RI-exchange row (mirror of
purchase_executions.approved_byTEXT). If the schema doesn't have a column today, add one in a small migration.
- Calls
-
Frontend Approve button on the RI-exchange "pending" row UI, mirroring the History page's button. Find where pending RI-exchanges render today (
riexchange.tsor similar) and add the same predicate + click + confirmDialog →api.approveRIExchange(id)→ reload + toast pattern. Thecancel/approvepredicates can sharecreated_by_user_id-based own-ness logic. -
Tests:
- Backend: copy the cancel/approve precedent test pattern. Cover admin-session, approve-own-with-creator-match, approve-own-without-match (rejected), legacy email-token path still works (backwards-compat).
- Frontend: button render + click handler tests mirroring
history-approve-button.test.ts.
-
Cross-cutting check: ensure the RI-exchange
executeApprovedExchangeis safe to call from a logged-in session context (it might assume token-bearer-only — verify it doesn't read state that's only present on the email-link path).
Acceptance
- A logged-in admin (or
approve-anyholder) can approve a pending RI-exchange directly from the dashboard. - Token-based email-link approval continues to work for non-session approvers.
- A test seeds an admin session + a pending RI-exchange, hits the route with no token, asserts the row transitions and the cloud-side execution kicks off (mockable).
- Pre-existing RI-exchange tests on the token-only path continue to pass — backwards-compat mandatory.
References
- feat(api,history): add Approve button + approve-{any,own} RBAC for in-dashboard purchase approval #286 (parent feature)
- PR opened for feat(api,history): add Approve button + approve-{any,own} RBAC for in-dashboard purchase approval #286 (in-dashboard purchase approval — the template this issue mirrors)
internal/api/handler_ri_exchange.go:579-624(approveRIExchange+validateExchangeApproval)internal/api/handler_ri_exchange.go:634+(executeApprovedExchange)internal/api/handler_purchases.go::approvePurchase/approvePurchaseViaSession/authorizeSessionApprove(the precedent to mirror)
Activity
feat(ri-exchange): add symmetric session-auth approve path (closes #300)
feat(ri-exchange): symmetric session-auth approve path (closes #300) (#…
cristim commentedon May 28, 2026
Verified: PR #590 (commit 8f3e320) merged into origin/feat/multicloud-web-frontend on 2026-05-27. The commit message explicitly states 'closes #300'. PR adds approveRIExchangeViaSession, the frontend Approve button in riexchange.ts, and backend tests covering admin-session and approve-own paths.