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:
Claude (Chronicler #57)
2026-04-03 11:04:20 +00:00
parent aeeaa14865
commit 1678b05237
2 changed files with 59 additions and 9 deletions

View File

@@ -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();
}
});

View File

@@ -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();
}
});