Files
cannaiq/backend/src/routes/proxies.ts
Kelly 74981fd399 feat: Auto-migrations on startup, worker exit location, proxy improvements
- Add auto-migration system that runs SQL files from migrations/ on server startup
- Track applied migrations in schema_migrations table
- Show proxy exit location in Workers dashboard
- Add "Cleanup Stale" button to remove old workers
- Add remove button for individual workers
- Include proxy location (city, state, country) in worker heartbeats
- Update Proxy interface with location fields
- Re-enable bulk proxy import without ON CONFLICT

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-10 01:42:00 -07:00

292 lines
9.1 KiB
TypeScript
Executable File

import { Router } from 'express';
import { authMiddleware, requireRole } from '../auth/middleware';
import { pool } from '../db/pool';
import { testProxy, addProxy, addProxiesFromList } from '../services/proxy';
import { createProxyTestJob, getProxyTestJob, getActiveProxyTestJob, cancelProxyTestJob, ProxyTestMode } from '../services/proxyTestQueue';
const router = Router();
router.use(authMiddleware);
// Get all proxies
router.get('/', async (req, res) => {
try {
const result = await pool.query(`
SELECT id, host, port, protocol, username, password, active, is_anonymous,
last_tested_at, test_result, response_time_ms, created_at,
city, state, country, country_code, location_updated_at,
COALESCE(max_connections, 1) as max_connections
FROM proxies
ORDER BY created_at DESC
`);
res.json({ proxies: result.rows });
} catch (error) {
console.error('Error fetching proxies:', error);
res.status(500).json({ error: 'Failed to fetch proxies' });
}
});
// Get active proxy test job (must be before /:id route)
router.get('/test-job', async (req, res) => {
try {
const job = await getActiveProxyTestJob();
res.json({ job });
} catch (error) {
console.error('Error fetching active job:', error);
res.status(500).json({ error: 'Failed to fetch active job' });
}
});
// Get proxy test job status (must be before /:id route)
router.get('/test-job/:jobId', async (req, res) => {
try {
const { jobId } = req.params;
const job = await getProxyTestJob(parseInt(jobId));
if (!job) {
return res.status(404).json({ error: 'Job not found' });
}
res.json({ job });
} catch (error) {
console.error('Error fetching job status:', error);
res.status(500).json({ error: 'Failed to fetch job status' });
}
});
// Get single proxy
router.get('/:id', async (req, res) => {
try {
const { id } = req.params;
const result = await pool.query(`
SELECT id, host, port, protocol, username, active, is_anonymous,
last_tested_at, test_result, response_time_ms, created_at
FROM proxies
WHERE id = $1
`, [id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Proxy not found' });
}
res.json({ proxy: result.rows[0] });
} catch (error) {
console.error('Error fetching proxy:', error);
res.status(500).json({ error: 'Failed to fetch proxy' });
}
});
// Add single proxy
router.post('/', requireRole('superadmin', 'admin'), async (req, res) => {
try {
const { host, port, protocol, username, password } = req.body;
if (!host || !port || !protocol) {
return res.status(400).json({ error: 'Host, port, and protocol required' });
}
// Test and add proxy
const proxyId = await addProxy(host, port, protocol, username, password);
const result = await pool.query(`
SELECT * FROM proxies WHERE id = $1
`, [proxyId]);
res.status(201).json({ proxy: result.rows[0] });
} catch (error: any) {
console.error('Error adding proxy:', error);
res.status(400).json({ error: error.message || 'Failed to add proxy' });
}
});
// Add multiple proxies
router.post('/bulk', requireRole('superadmin', 'admin'), async (req, res) => {
try {
const { proxies } = req.body;
if (!proxies || !Array.isArray(proxies)) {
return res.status(400).json({ error: 'Proxies array required' });
}
const result = await addProxiesFromList(proxies);
res.status(201).json(result);
} catch (error) {
console.error('Error adding proxies:', error);
res.status(500).json({ error: 'Failed to add proxies' });
}
});
// Test single proxy
router.post('/:id/test', requireRole('superadmin', 'admin'), async (req, res) => {
try {
const { id } = req.params;
const proxyResult = await pool.query(`
SELECT host, port, protocol, username, password
FROM proxies
WHERE id = $1
`, [id]);
if (proxyResult.rows.length === 0) {
return res.status(404).json({ error: 'Proxy not found' });
}
const proxy = proxyResult.rows[0];
const testResult = await testProxy(
proxy.host,
proxy.port,
proxy.protocol,
proxy.username,
proxy.password
);
// Update proxy with test results
await pool.query(`
UPDATE proxies
SET last_tested_at = CURRENT_TIMESTAMP,
test_result = $1,
response_time_ms = $2,
is_anonymous = $3,
active = $4
WHERE id = $5
`, [
testResult.success ? 'success' : 'failed',
testResult.responseTimeMs,
testResult.isAnonymous,
testResult.success,
id
]);
res.json({ test_result: testResult });
} catch (error) {
console.error('Error testing proxy:', error);
res.status(500).json({ error: 'Failed to test proxy' });
}
});
// Start proxy test job
// Query params: mode=all|failed|inactive, concurrency=10
router.post('/test-all', requireRole('superadmin', 'admin'), async (req, res) => {
try {
const mode = (req.query.mode as ProxyTestMode) || 'all';
const concurrency = parseInt(req.query.concurrency as string) || 10;
// Validate mode
if (!['all', 'failed', 'inactive'].includes(mode)) {
return res.status(400).json({ error: 'Invalid mode. Use: all, failed, or inactive' });
}
// Validate concurrency (1-50)
if (concurrency < 1 || concurrency > 50) {
return res.status(400).json({ error: 'Concurrency must be between 1 and 50' });
}
const jobId = await createProxyTestJob(mode, concurrency);
res.json({ jobId, mode, concurrency, message: `Proxy test job started (mode: ${mode}, concurrency: ${concurrency})` });
} catch (error: any) {
console.error('Error starting proxy test job:', error);
res.status(500).json({ error: error.message || 'Failed to start proxy test job' });
}
});
// Convenience endpoint: Test only failed proxies
router.post('/test-failed', requireRole('superadmin', 'admin'), async (req, res) => {
try {
const concurrency = parseInt(req.query.concurrency as string) || 10;
const jobId = await createProxyTestJob('failed', concurrency);
res.json({ jobId, mode: 'failed', concurrency, message: 'Retesting failed proxies...' });
} catch (error: any) {
console.error('Error starting failed proxy test:', error);
res.status(500).json({ error: error.message || 'Failed to start proxy test job' });
}
});
// Cancel proxy test job
router.post('/test-job/:jobId/cancel', requireRole('superadmin', 'admin'), async (req, res) => {
try {
const { jobId } = req.params;
const cancelled = await cancelProxyTestJob(parseInt(jobId));
if (!cancelled) {
return res.status(404).json({ error: 'Job not found or already completed' });
}
res.json({ message: 'Job cancelled successfully' });
} catch (error) {
console.error('Error cancelling job:', error);
res.status(500).json({ error: 'Failed to cancel job' });
}
});
// Update proxy
router.put('/:id', requireRole('superadmin', 'admin'), async (req, res) => {
try {
const { id } = req.params;
const { host, port, protocol, username, password, active, max_connections } = req.body;
const result = await pool.query(`
UPDATE proxies
SET host = COALESCE($1, host),
port = COALESCE($2, port),
protocol = COALESCE($3, protocol),
username = COALESCE($4, username),
password = COALESCE($5, password),
active = COALESCE($6, active),
max_connections = COALESCE($7, max_connections),
updated_at = CURRENT_TIMESTAMP
WHERE id = $8
RETURNING *
`, [host, port, protocol, username, password, active, max_connections, id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Proxy not found' });
}
res.json({ proxy: result.rows[0] });
} catch (error) {
console.error('Error updating proxy:', error);
res.status(500).json({ error: 'Failed to update proxy' });
}
});
// Delete proxy
router.delete('/:id', requireRole('superadmin'), async (req, res) => {
try {
const { id } = req.params;
const result = await pool.query(`
DELETE FROM proxies WHERE id = $1 RETURNING id
`, [id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Proxy not found' });
}
res.json({ message: 'Proxy deleted successfully' });
} catch (error) {
console.error('Error deleting proxy:', error);
res.status(500).json({ error: 'Failed to delete proxy' });
}
});
// Update all proxy locations
router.post('/update-locations', requireRole('superadmin', 'admin'), async (req, res) => {
try {
const { updateAllProxyLocations } = await import('../services/geolocation');
// Run in background
updateAllProxyLocations().catch(err => {
console.error('❌ Location update failed:', err);
});
res.json({ message: 'Location update job started' });
} catch (error) {
console.error('Error starting location update:', error);
res.status(500).json({ error: 'Failed to start location update' });
}
});
export default router;