feat: add database transaction safety to Trinity Console critical operations
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) <claude@firefrostgaming.com>
This commit is contained in:
@@ -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(`<span class="text-green-500 font-bold text-sm">✅ Extended 24h</span>`);
|
||||
} catch (error) {
|
||||
// ROLLBACK on error
|
||||
await client.query('ROLLBACK');
|
||||
console.error('Grace period extension error:', error);
|
||||
res.status(500).send(`<span class="text-red-500 font-bold text-sm">❌ Error</span>`);
|
||||
} 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(`<span class="text-purple-500 font-bold text-sm">✅ Activated</span>`);
|
||||
} catch (error) {
|
||||
// ROLLBACK on error
|
||||
await client.query('ROLLBACK');
|
||||
console.error('Manual payment override error:', error);
|
||||
res.status(500).send(`<span class="text-red-500 font-bold text-sm">❌ Error</span>`);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user