From 1678b052370eddadb8c305b3cf8a8a064b6377a0 Mon Sep 17 00:00:00 2001 From: "Claude (Chronicler #57)" Date: Fri, 3 Apr 2026 11:04:20 +0000 Subject: [PATCH] feat: add database transaction safety to Trinity Console critical operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WHAT WAS DONE: Added BEGIN/COMMIT/ROLLBACK transaction wrappers to all multi-step database operations in Trinity Console to prevent data corruption from partial failures. WHY: Gemini's architectural guidance: 'Database transactions are CRITICAL. Do not launch without this. Partial failures corrupting subscription data is an absolute nightmare. At 10 subscribers, manually fixing a corrupted tier change in Postgres while cross-referencing Discord roles and Stripe logs will burn hours of your time and destroy your structured workflow.' RV Reality: When managing operations from a campground with spotty cellular internet, data corruption is the biggest enemy. Transaction safety is the ultimate safety net for remote management. WHAT WAS FIXED: All 4 critical multi-step operations now use proper transactions: 1. Tier Changes (players.js) - UPDATE subscriptions + INSERT audit log - Now wrapped in BEGIN/COMMIT with ROLLBACK on error 2. Staff Toggle (players.js) - UPDATE users + INSERT audit log - Now wrapped in BEGIN/COMMIT with ROLLBACK on error 3. Extend Grace Period (grace.js) - UPDATE subscriptions + INSERT audit log - Now wrapped in BEGIN/COMMIT with ROLLBACK on error 4. Manual Payment Override (grace.js) - UPDATE subscriptions + INSERT audit log - Now wrapped in BEGIN/COMMIT with ROLLBACK on error TECHNICAL IMPLEMENTATION: - Use db.pool.connect() to get dedicated client - Wrap operations in try/catch/finally - BEGIN transaction before operations - COMMIT on success - ROLLBACK on any error - client.release() in finally block (prevents connection leaks) FILES MODIFIED (2 files): - services/arbiter-3.0/src/routes/admin/players.js (2 operations) - services/arbiter-3.0/src/routes/admin/grace.js (2 operations) GEMINI'S SECURITY ASSESSMENT COMPLETE: ✅ Database Transactions - DONE (this commit) ✅ CSRF Protection - Already implemented (csurf middleware) ✅ Database Indexes - Already implemented (Chronicler #51) ⏳ Ban Management UI - Deferred (manual Postgres for first 10 subscribers) ⏳ Email Integration - Deferred (manual emails for first 10 subscribers) REMAINING SOFT LAUNCH WORK: - Unsubscribe Flow UI (2-3 hours) - End-to-End Testing (2-3 hours) - Launch April 15! This eliminates the data corruption risk that would be catastrophic for remote RV management. Trinity Console is now transactionally safe. Signed-off-by: Claude (Chronicler #57) --- .../arbiter-3.0/src/routes/admin/grace.js | 34 ++++++++++++++++--- .../arbiter-3.0/src/routes/admin/players.js | 34 ++++++++++++++++--- 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/services/arbiter-3.0/src/routes/admin/grace.js b/services/arbiter-3.0/src/routes/admin/grace.js index a97be19..fb0ae9b 100644 --- a/services/arbiter-3.0/src/routes/admin/grace.js +++ b/services/arbiter-3.0/src/routes/admin/grace.js @@ -51,21 +51,34 @@ router.post('/:discord_id/extend', async (req, res) => { const adminId = req.user.id; const adminUsername = req.user.username; + const client = await db.pool.connect(); + try { - await db.query(` + // BEGIN TRANSACTION + await client.query('BEGIN'); + + await client.query(` UPDATE subscriptions SET grace_period_ends_at = grace_period_ends_at + INTERVAL '24 hours' WHERE discord_id = $1 AND status = 'grace_period' `, [discord_id]); - await db.query(` + await client.query(` INSERT INTO admin_audit_log (admin_discord_id, admin_username, action_type, target_identifier, details) VALUES ($1, $2, 'extend_grace_period', $3, '{"extension": "24h"}') `, [adminId, adminUsername, discord_id]); + // COMMIT TRANSACTION + await client.query('COMMIT'); + res.send(`✅ Extended 24h`); } catch (error) { + // ROLLBACK on error + await client.query('ROLLBACK'); + console.error('Grace period extension error:', error); res.status(500).send(`❌ Error`); + } finally { + client.release(); } }); @@ -75,21 +88,34 @@ router.post('/:discord_id/manual', async (req, res) => { const adminId = req.user.id; const adminUsername = req.user.username; + const client = await db.pool.connect(); + try { - await db.query(` + // BEGIN TRANSACTION + await client.query('BEGIN'); + + await client.query(` UPDATE subscriptions SET status = 'active', grace_period_started_at = NULL, grace_period_ends_at = NULL WHERE discord_id = $1 `, [discord_id]); - await db.query(` + await client.query(` INSERT INTO admin_audit_log (admin_discord_id, admin_username, action_type, target_identifier, details) VALUES ($1, $2, 'manual_payment_override', $3, '{"reason": "admin_override"}') `, [adminId, adminUsername, discord_id]); + // COMMIT TRANSACTION + await client.query('COMMIT'); + res.send(`✅ Activated`); } catch (error) { + // ROLLBACK on error + await client.query('ROLLBACK'); + console.error('Manual payment override error:', error); res.status(500).send(`❌ Error`); + } finally { + client.release(); } }); diff --git a/services/arbiter-3.0/src/routes/admin/players.js b/services/arbiter-3.0/src/routes/admin/players.js index 378bf1b..6528f7c 100644 --- a/services/arbiter-3.0/src/routes/admin/players.js +++ b/services/arbiter-3.0/src/routes/admin/players.js @@ -37,15 +37,20 @@ router.post('/:discord_id/tier', async (req, res) => { const { discord_id } = req.params; const { tier_level } = req.body; + const client = await db.pool.connect(); + try { // Validate tier exists if (!TIER_INFO[tier_level]) { return res.status(400).send('Invalid tier'); } + // BEGIN TRANSACTION + await client.query('BEGIN'); + // Update tier in database const tierInfo = TIER_INFO[tier_level]; - await db.query(` + await client.query(` UPDATE subscriptions SET tier_level = $1, mrr_value = $2, updated_at = NOW() WHERE discord_id = $3 @@ -55,17 +60,24 @@ router.post('/:discord_id/tier', async (req, res) => { // This will be implemented when Discord bot integration is ready // Create audit log entry - await db.query(` + await client.query(` INSERT INTO admin_audit_log (admin_discord_id, action, target_discord_id, details) VALUES ($1, 'tier_change', $2, $3) `, [req.user.id, discord_id, `Changed tier to ${tierInfo.name}`]); + // COMMIT TRANSACTION + await client.query('COMMIT'); + // Return success (htmx will handle UI update) res.send('OK'); } catch (error) { + // ROLLBACK on error + await client.query('ROLLBACK'); console.error('Tier change error:', error); res.status(500).send('Error updating tier'); + } finally { + client.release(); } }); @@ -73,29 +85,41 @@ router.post('/:discord_id/tier', async (req, res) => { router.post('/:discord_id/staff', async (req, res) => { const { discord_id } = req.params; + const client = await db.pool.connect(); + try { + // BEGIN TRANSACTION + await client.query('BEGIN'); + // Toggle staff status - await db.query(` + await client.query(` UPDATE users SET is_staff = NOT COALESCE(is_staff, FALSE) WHERE discord_id = $1 `, [discord_id]); // Get new status for audit log - const { rows } = await db.query(`SELECT is_staff FROM users WHERE discord_id = $1`, [discord_id]); + const { rows } = await client.query(`SELECT is_staff FROM users WHERE discord_id = $1`, [discord_id]); const newStatus = rows[0]?.is_staff ? 'Staff' : 'Non-Staff'; // Create audit log entry - await db.query(` + await client.query(` INSERT INTO admin_audit_log (admin_discord_id, action, target_discord_id, details) VALUES ($1, 'staff_toggle', $2, $3) `, [req.user.id, discord_id, `Changed to ${newStatus}`]); + // COMMIT TRANSACTION + await client.query('COMMIT'); + res.send('OK'); } catch (error) { + // ROLLBACK on error + await client.query('ROLLBACK'); console.error('Staff toggle error:', error); res.status(500).send('Error updating staff status'); + } finally { + client.release(); } });