Files
LightningLotto/back_end/src/controllers/admin.ts
Michilis 404fdf2610 Maintenance mode activates after current draw completes
- When admin enables maintenance, it's set to 'pending' state
- Maintenance activates automatically after the current draw completes
- Admin can use immediate=true to force immediate activation
- Frontend shows 'Maintenance Scheduled' banner when pending
- Telegram bot warns users but still allows purchases when pending
- Both mode and pending status tracked in system_settings table
2025-12-09 00:46:55 +00:00

311 lines
9.1 KiB
TypeScript

import { Request, Response } from 'express';
import { db } from '../database';
import { executeDraw } from '../services/draw';
import { retryPayoutService } from '../services/payout';
import { JackpotCycle, Payout } from '../types';
/**
* GET /admin/cycles
* List all cycles with optional filters
*/
export async function listCycles(req: Request, res: Response) {
try {
const { status, cycle_type, limit = 50, offset = 0 } = req.query;
let query = `SELECT * FROM jackpot_cycles WHERE 1=1`;
const params: any[] = [];
let paramCount = 0;
if (status) {
paramCount++;
query += ` AND status = $${paramCount}`;
params.push(status);
}
if (cycle_type) {
paramCount++;
query += ` AND cycle_type = $${paramCount}`;
params.push(cycle_type);
}
query += ` ORDER BY scheduled_at DESC LIMIT $${paramCount + 1} OFFSET $${paramCount + 2}`;
params.push(limit, offset);
const result = await db.query<JackpotCycle>(query, params);
const cycles = result.rows.map(c => ({
id: c.id,
lottery_id: c.lottery_id,
cycle_type: c.cycle_type,
sequence_number: c.sequence_number,
scheduled_at: c.scheduled_at.toISOString(),
status: c.status,
pot_total_sats: parseInt(c.pot_total_sats.toString()),
pot_after_fee_sats: c.pot_after_fee_sats ? parseInt(c.pot_after_fee_sats.toString()) : null,
winning_ticket_id: c.winning_ticket_id,
winning_lightning_address: c.winning_lightning_address,
}));
return res.json({
version: '1.0',
data: {
cycles,
total: cycles.length,
},
});
} catch (error: any) {
console.error('List cycles error:', error);
return res.status(500).json({
version: '1.0',
error: 'INTERNAL_ERROR',
message: 'Failed to list cycles',
});
}
}
/**
* POST /admin/cycles/:id/run-draw
* Manually trigger draw execution
*/
export async function runDrawManually(req: Request, res: Response) {
try {
const { id } = req.params;
const result = await executeDraw(id);
if (!result.success) {
return res.status(400).json({
version: '1.0',
error: result.error || 'DRAW_FAILED',
message: result.message || 'Failed to execute draw',
});
}
return res.json({
version: '1.0',
data: {
cycle_id: id,
winning_ticket_id: result.winningTicketId,
pot_after_fee_sats: result.potAfterFeeSats,
payout_status: result.payoutStatus,
},
});
} catch (error: any) {
console.error('Manual draw error:', error);
return res.status(500).json({
version: '1.0',
error: 'INTERNAL_ERROR',
message: 'Failed to run draw',
});
}
}
/**
* GET /admin/payouts
* List payouts with optional filters
*/
export async function listPayouts(req: Request, res: Response) {
try {
const { status, limit = 50, offset = 0 } = req.query;
let query = `SELECT * FROM payouts WHERE 1=1`;
const params: any[] = [];
let paramCount = 0;
if (status) {
paramCount++;
query += ` AND status = $${paramCount}`;
params.push(status);
}
query += ` ORDER BY created_at DESC LIMIT $${paramCount + 1} OFFSET $${paramCount + 2}`;
params.push(limit, offset);
const result = await db.query<Payout>(query, params);
const payouts = result.rows.map(p => ({
id: p.id,
lottery_id: p.lottery_id,
cycle_id: p.cycle_id,
ticket_id: p.ticket_id,
lightning_address: p.lightning_address,
amount_sats: parseInt(p.amount_sats.toString()),
status: p.status,
error_message: p.error_message,
retry_count: p.retry_count,
created_at: p.created_at.toISOString(),
}));
return res.json({
version: '1.0',
data: {
payouts,
total: payouts.length,
},
});
} catch (error: any) {
console.error('List payouts error:', error);
return res.status(500).json({
version: '1.0',
error: 'INTERNAL_ERROR',
message: 'Failed to list payouts',
});
}
}
/**
* POST /admin/payouts/:id/retry
* Retry a failed payout
*/
export async function retryPayout(req: Request, res: Response) {
try {
const { id } = req.params;
const result = await retryPayoutService(id);
if (!result.success) {
return res.status(400).json({
version: '1.0',
error: result.error || 'RETRY_FAILED',
message: result.message || 'Failed to retry payout',
});
}
return res.json({
version: '1.0',
data: {
payout_id: id,
status: result.status,
retry_count: result.retryCount,
},
});
} catch (error: any) {
console.error('Retry payout error:', error);
return res.status(500).json({
version: '1.0',
error: 'INTERNAL_ERROR',
message: 'Failed to retry payout',
});
}
}
/**
* GET /admin/maintenance
* Get current maintenance mode status
*/
export async function getMaintenanceStatus(req: Request, res: Response) {
try {
const result = await db.query(
`SELECT value FROM system_settings WHERE key = 'maintenance_mode'`
);
const pendingResult = await db.query(
`SELECT value FROM system_settings WHERE key = 'maintenance_pending'`
);
const messageResult = await db.query(
`SELECT value FROM system_settings WHERE key = 'maintenance_message'`
);
const enabled = result.rows[0]?.value === 'true';
const pending = pendingResult.rows[0]?.value === 'true';
const message = messageResult.rows[0]?.value || 'System is under maintenance. Please try again later.';
return res.json({
version: '1.0',
data: {
maintenance_mode: enabled,
maintenance_pending: pending,
message: message,
},
});
} catch (error: any) {
console.error('Get maintenance status error:', error);
return res.status(500).json({
version: '1.0',
error: 'INTERNAL_ERROR',
message: 'Failed to get maintenance status',
});
}
}
/**
* POST /admin/maintenance
* Enable or disable maintenance mode
* When enabling, maintenance is set to "pending" and activates after current draw completes
*/
export async function setMaintenanceMode(req: Request, res: Response) {
try {
const { enabled, message, immediate } = req.body;
if (typeof enabled !== 'boolean') {
return res.status(400).json({
version: '1.0',
error: 'INVALID_INPUT',
message: 'enabled must be a boolean',
});
}
if (enabled) {
if (immediate === true) {
// Immediate activation (admin override)
await db.query(
`INSERT INTO system_settings (key, value, updated_at) VALUES ('maintenance_mode', 'true', datetime('now'))
ON CONFLICT(key) DO UPDATE SET value = 'true', updated_at = datetime('now')`
);
await db.query(
`INSERT INTO system_settings (key, value, updated_at) VALUES ('maintenance_pending', 'false', datetime('now'))
ON CONFLICT(key) DO UPDATE SET value = 'false', updated_at = datetime('now')`
);
console.log('Maintenance mode ENABLED IMMEDIATELY');
} else {
// Set pending - will activate after current draw completes
await db.query(
`INSERT INTO system_settings (key, value, updated_at) VALUES ('maintenance_pending', 'true', datetime('now'))
ON CONFLICT(key) DO UPDATE SET value = 'true', updated_at = datetime('now')`
);
console.log('Maintenance mode PENDING (will activate after current draw)');
}
} else {
// Disable both active and pending maintenance
await db.query(
`INSERT INTO system_settings (key, value, updated_at) VALUES ('maintenance_mode', 'false', datetime('now'))
ON CONFLICT(key) DO UPDATE SET value = 'false', updated_at = datetime('now')`
);
await db.query(
`INSERT INTO system_settings (key, value, updated_at) VALUES ('maintenance_pending', 'false', datetime('now'))
ON CONFLICT(key) DO UPDATE SET value = 'false', updated_at = datetime('now')`
);
console.log('Maintenance mode DISABLED');
}
// Update message if provided
if (message && typeof message === 'string') {
await db.query(
`INSERT INTO system_settings (key, value, updated_at) VALUES ('maintenance_message', $1, datetime('now'))
ON CONFLICT(key) DO UPDATE SET value = $1, updated_at = datetime('now')`,
[message]
);
}
// Get current state
const modeResult = await db.query(`SELECT value FROM system_settings WHERE key = 'maintenance_mode'`);
const pendingResult = await db.query(`SELECT value FROM system_settings WHERE key = 'maintenance_pending'`);
return res.json({
version: '1.0',
data: {
maintenance_mode: modeResult.rows[0]?.value === 'true',
maintenance_pending: pendingResult.rows[0]?.value === 'true',
message: message || 'System is under maintenance. Please try again later.',
},
});
} catch (error: any) {
console.error('Set maintenance mode error:', error);
return res.status(500).json({
version: '1.0',
error: 'INTERNAL_ERROR',
message: 'Failed to set maintenance mode',
});
}
}