fix: restart-loop-A findings — pending sweep refunds, tip carve on discounts, TOCTOU redemption, single-use 2FA code + mint endpoint, refresh-token family revocation, admin 2FA code UX
Restart of Loop A (fresh review -> fix -> verify) findings from commit 5e967fa: - B1: sweep auto-refund treats Square PENDING refunds as NON-terminal (row stays pending, no gift-card clawback, refunds row inserted for payments AND till_sales, re-polls the deterministic sweepdup- key); Square-less pre-pass exempts square_refund_id IS NOT NULL rows - M4: terminal tip carve accounts for pending campaign discounts (headroom = total - pending - paid) so explicit tips aren't absorbed as service revenue; no-tip case stays a single record - max_redemptions TOCTOU closed with atomic conditional UPDATE ... RETURNING; exhausted-at-apply surfaces campaign_fully_redeemed - 2FA: verification code is single-use on the saved-card gate (VerifyForUser consume=true, interactive flows unaffected); new POST /api/user/2fa/code mints a fresh code for enabled users (RequireAuth + RequireNonGuest + mint cooldown + per-user limiter) - Refresh tokens: family_id + used_at columns; reuse of an already-rotated token revokes the ENTIRE family and inserts a refresh_token_reuse admin alert; rotation mints descendants in the same family - Frontend: 2FA code input + Request-a-new-code on all saved-card surfaces; admin modal keys code input to customer 2FA + 403 self-heal; tip-display note for pending discounts; 76 frontend tests - Verified: all 26 backend packages pass, frontend build+tests green, env-docs 41/41
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
isOverflowTipConfirmationRequired,
|
||||
isSavedCardVerificationRequired,
|
||||
isTwoFactorVerificationGateFailure,
|
||||
requestNewTwoFactorCode,
|
||||
sanitizeDecimalInput,
|
||||
SAVED_CARD_VERIFICATION_MESSAGE,
|
||||
submitPaymentWithRetry
|
||||
@@ -79,6 +80,31 @@
|
||||
show2FACodeInput && twoFactorEnabled && twoFactorCode.trim() === ''
|
||||
);
|
||||
|
||||
// POST /api/user/2fa/code mint state for the "Request a new code" button
|
||||
// (session user = card owner, so a minted code authorizes their charge).
|
||||
let requesting2FACode = $state(false);
|
||||
async function handleRequestNew2FACode() {
|
||||
if (requesting2FACode) return;
|
||||
requesting2FACode = true;
|
||||
try {
|
||||
const result = await requestNewTwoFactorCode();
|
||||
if (result.ok) {
|
||||
twoFactorCode = '';
|
||||
toast.success(result.message);
|
||||
} else if (result.status === 429) {
|
||||
toast.error(result.message || 'Too many requests. Wait before requesting a new code.');
|
||||
} else if (result.status === 503) {
|
||||
toast.error(
|
||||
result.message || 'Verification codes are unavailable right now. Try again later.'
|
||||
);
|
||||
} else {
|
||||
toast.error(result.message);
|
||||
}
|
||||
} finally {
|
||||
requesting2FACode = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Cached idempotency key per payment attempt (amount + type + card): reused
|
||||
// on retry so a lost-response retry dedups instead of double-charging,
|
||||
// regenerated when any of those change. Matches the tip-flow pattern.
|
||||
@@ -733,7 +759,7 @@
|
||||
|
||||
{#if overflowConfirm}
|
||||
<div class="space-y-4">
|
||||
<!-- Overpayment confirmation: the backend rejected the payment because
|
||||
<!-- Overpayment confirmation: the backend rejected the payment because
|
||||
the booking's remaining balance has changed since it was loaded
|
||||
(stale data). The excess over the remaining balance will be
|
||||
recorded as a tip once confirmed. Applies both before and after
|
||||
@@ -1001,6 +1027,18 @@
|
||||
showInput={show2FACodeInput}
|
||||
enabled={twoFactorEnabled}
|
||||
/>
|
||||
{#if show2FACodeInput && twoFactorEnabled}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="w-full"
|
||||
loading={requesting2FACode}
|
||||
disabled={requesting2FACode}
|
||||
onclick={handleRequestNew2FACode}
|
||||
>
|
||||
Request a new code
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
{#if depositPolicyWarning}
|
||||
<div class="rounded-md border border-amber-200 bg-amber-50 p-3 text-xs text-amber-800">
|
||||
|
||||
Reference in New Issue
Block a user