CI/CD: - Fix build_args format in woodpecker CI (comma-separated, not YAML list) - This fixes "unknown" SHA/version showing on remote deployments Backend schema-tolerant fixes (graceful fallbacks when tables missing): - users.ts: Check which columns exist before querying - worker-registry.ts: Return empty result if table doesn't exist - task-service.ts: Add tableExists() helper, handle missing tables/views - proxies.ts: Return totalProxies in test-all response Frontend fixes: - Proxies: Use total from response for accurate progress display - SEO PagesTab: Dim Generate button when no AI provider active 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
292 lines
9.1 KiB
TypeScript
Executable File
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, totalProxies } = await createProxyTestJob(mode, concurrency);
|
|
res.json({ jobId, total: totalProxies, 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, totalProxies } = await createProxyTestJob('failed', concurrency);
|
|
res.json({ jobId, total: totalProxies, 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;
|