mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-21 16:18:33 +02:00
fix(payment): kill double invoice + restore Smirk one-click (status 403)
1) Double invoice: the "Buy Selected Package" button carries BOTH id=buy-credits-btn AND class=mode-option, so a click fired the generic .mode-option handler (no data attrs -> defaulted to single_game -> a spurious 1-credit invoice) AND its own buy-credits handler (the real pack). Guard the generic handler to ignore #buy-credits-btn and intentless clicks. 2) Smirk one-click payment never opened: /api/auth/smirk/status 403d, leaving _isLinked=false so onPaymentCreated skipped the Smirk path. The BOLA-hardened query matched socket_id AND anon_token, but socket_id is VOLATILE (changes every refresh) so it returned 0 rows after a reload. Match on the anon_token alone — the actual ownership secret — same protection, no refresh fragility.
This commit is contained in:
@@ -18,9 +18,14 @@ const PaymentUI = {
|
||||
|
||||
// Bind mode options (use delegation for dynamic content)
|
||||
$('#payment-ui').on('click', '.mode-option', function() {
|
||||
// The "Buy Selected Package" button ALSO carries .mode-option (for styling) but has its
|
||||
// own handler below; without this guard it double-fired here with NO data attrs, defaulting
|
||||
// to single_game — so buying a 10-credit pack ALSO created a spurious 1-credit invoice.
|
||||
if (this.id === 'buy-credits-btn') return;
|
||||
const mode = $(this).data('mode');
|
||||
const action = $(this).data('action');
|
||||
const packageId = $(this).data('package-id');
|
||||
if (!mode && !action) return; // stray click with no intent
|
||||
PaymentUI.handleModeSelection(mode, action, packageId);
|
||||
});
|
||||
|
||||
|
||||
+9
-3
@@ -251,14 +251,20 @@ router.get('/api/auth/smirk/status', asyncHandler(async (req, res) => {
|
||||
return res.status(401).json({ error: 'Session token required' });
|
||||
}
|
||||
|
||||
// Identify the caller by anon_token ALONE — it is the ownership secret. The previous query also
|
||||
// required socket_id = current socket.id, but socket_id is VOLATILE (changes on every refresh /
|
||||
// reconnect), so after a refresh the row's stored socket_id no longer matched and this 403'd —
|
||||
// which left the client's _isLinked=false and silently disabled Smirk one-click payment. Matching
|
||||
// on the unguessable token is the same guarantee (you can only read your OWN state) without the
|
||||
// socket_id fragility. (socketId is still required in the query string for API shape.)
|
||||
const result = await db.query(`
|
||||
SELECT smirk_public_key, payout_address
|
||||
FROM users
|
||||
WHERE socket_id = $1 AND anon_token = $2
|
||||
`, [socketId, token]);
|
||||
WHERE anon_token = $1
|
||||
`, [token]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
// Do not disclose whether the socketId exists — ownership check failed.
|
||||
// Unknown/again-rotated token — ownership check failed; disclose nothing.
|
||||
return res.status(403).json({ error: 'Session ownership verification failed' });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user