feat: Add trusted origins management UI at /users
- Create trusted_origins table for DB-backed origin management - Add API routes for CRUD operations on trusted origins - Add tabbed interface on /users page with Users and Trusted Origins tabs - Seeds default trusted origins (cannaiq.co, findadispo.com, findagram.co, etc.) - Fix TypeScript error in WorkersDashboard fingerprint type 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -88,6 +88,7 @@ import versionRoutes from './routes/version';
|
||||
import deployStatusRoutes from './routes/deploy-status';
|
||||
import publicApiRoutes from './routes/public-api';
|
||||
import usersRoutes from './routes/users';
|
||||
import trustedOriginsRoutes from './routes/trusted-origins';
|
||||
import staleProcessesRoutes from './routes/stale-processes';
|
||||
import orchestratorAdminRoutes from './routes/orchestrator-admin';
|
||||
import proxyAdminRoutes from './routes/proxy-admin';
|
||||
@@ -122,7 +123,6 @@ import workerRegistryRoutes from './routes/worker-registry';
|
||||
// Per TASK_WORKFLOW_2024-12-10.md: Raw payload access API
|
||||
import payloadsRoutes from './routes/payloads';
|
||||
import k8sRoutes from './routes/k8s';
|
||||
import trustedOriginsRoutes from './routes/trusted-origins';
|
||||
|
||||
|
||||
// Mark requests from trusted domains (cannaiq.co, findagram.co, findadispo.com)
|
||||
|
||||
@@ -1,223 +1,185 @@
|
||||
/**
|
||||
* Trusted Origins Admin Routes
|
||||
*
|
||||
* Manage IPs and domains that bypass API key authentication.
|
||||
* Available at /api/admin/trusted-origins
|
||||
*/
|
||||
|
||||
import { Router, Response } from 'express';
|
||||
import { Router } from 'express';
|
||||
import { pool } from '../db/pool';
|
||||
import { AuthRequest, authMiddleware, requireRole, clearTrustedOriginsCache } from '../auth/middleware';
|
||||
import { authMiddleware, requireRole, AuthRequest, clearTrustedOriginsCache } from '../auth/middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// All routes require admin auth
|
||||
// All routes require authentication and admin/superadmin role
|
||||
router.use(authMiddleware);
|
||||
router.use(requireRole('admin', 'superadmin'));
|
||||
|
||||
/**
|
||||
* GET /api/admin/trusted-origins
|
||||
* List all trusted origins
|
||||
*/
|
||||
router.get('/', async (req: AuthRequest, res: Response) => {
|
||||
// Get all trusted origins
|
||||
router.get('/', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
id,
|
||||
origin_type,
|
||||
origin_value,
|
||||
description,
|
||||
active,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM trusted_origins
|
||||
ORDER BY origin_type, origin_value
|
||||
t.*,
|
||||
u.email as created_by_email
|
||||
FROM trusted_origins t
|
||||
LEFT JOIN users u ON t.created_by = u.id
|
||||
ORDER BY t.origin_type, t.name
|
||||
`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
origins: result.rows,
|
||||
counts: {
|
||||
total: result.rows.length,
|
||||
active: result.rows.filter(r => r.active).length,
|
||||
ips: result.rows.filter(r => r.origin_type === 'ip').length,
|
||||
domains: result.rows.filter(r => r.origin_type === 'domain').length,
|
||||
patterns: result.rows.filter(r => r.origin_type === 'pattern').length,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('[TrustedOrigins] List error:', error.message);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
res.json({ origins: result.rows });
|
||||
} catch (error) {
|
||||
console.error('Error fetching trusted origins:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch trusted origins' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/admin/trusted-origins
|
||||
* Add a new trusted origin
|
||||
*/
|
||||
router.post('/', async (req: AuthRequest, res: Response) => {
|
||||
// Get single trusted origin
|
||||
router.get('/:id', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const { origin_type, origin_value, description } = req.body;
|
||||
const { id } = req.params;
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
t.*,
|
||||
u.email as created_by_email
|
||||
FROM trusted_origins t
|
||||
LEFT JOIN users u ON t.created_by = u.id
|
||||
WHERE t.id = $1
|
||||
`, [id]);
|
||||
|
||||
if (!origin_type || !origin_value) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'origin_type and origin_value are required',
|
||||
});
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Trusted origin not found' });
|
||||
}
|
||||
|
||||
if (!['ip', 'domain', 'pattern'].includes(origin_type)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'origin_type must be: ip, domain, or pattern',
|
||||
});
|
||||
res.json({ origin: result.rows[0] });
|
||||
} catch (error) {
|
||||
console.error('Error fetching trusted origin:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch trusted origin' });
|
||||
}
|
||||
});
|
||||
|
||||
// Create trusted origin
|
||||
router.post('/', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const { name, origin_type, origin_value, description, active } = req.body;
|
||||
|
||||
if (!name || !origin_type || !origin_value) {
|
||||
return res.status(400).json({ error: 'Name, origin_type, and origin_value are required' });
|
||||
}
|
||||
|
||||
// Validate pattern if regex
|
||||
const validTypes = ['domain', 'ip', 'pattern'];
|
||||
if (!validTypes.includes(origin_type)) {
|
||||
return res.status(400).json({ error: 'origin_type must be domain, ip, or pattern' });
|
||||
}
|
||||
|
||||
// Validate pattern if provided
|
||||
if (origin_type === 'pattern') {
|
||||
try {
|
||||
new RegExp(origin_value);
|
||||
} catch {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid regex pattern',
|
||||
});
|
||||
return res.status(400).json({ error: 'Invalid regex pattern' });
|
||||
}
|
||||
}
|
||||
|
||||
const result = await pool.query(`
|
||||
INSERT INTO trusted_origins (origin_type, origin_value, description, created_by)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id, origin_type, origin_value, description, active, created_at
|
||||
`, [origin_type, origin_value, description || null, req.user?.id || null]);
|
||||
INSERT INTO trusted_origins (name, origin_type, origin_value, description, active, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING *
|
||||
`, [name, origin_type, origin_value, description || null, active !== false, req.user?.id || null]);
|
||||
|
||||
// Invalidate cache
|
||||
// Clear cache so middleware picks up new origin
|
||||
clearTrustedOriginsCache();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
origin: result.rows[0],
|
||||
});
|
||||
res.status(201).json({ origin: result.rows[0] });
|
||||
} catch (error: any) {
|
||||
if (error.code === '23505') {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
error: 'This origin already exists',
|
||||
});
|
||||
return res.status(400).json({ error: 'This origin already exists' });
|
||||
}
|
||||
console.error('[TrustedOrigins] Add error:', error.message);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
console.error('Error creating trusted origin:', error);
|
||||
res.status(500).json({ error: 'Failed to create trusted origin' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/admin/trusted-origins/:id
|
||||
* Update a trusted origin
|
||||
*/
|
||||
router.put('/:id', async (req: AuthRequest, res: Response) => {
|
||||
// Update trusted origin
|
||||
router.put('/:id', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const { origin_type, origin_value, description, active } = req.body;
|
||||
const { id } = req.params;
|
||||
const { name, origin_type, origin_value, description, active } = req.body;
|
||||
|
||||
// Validate pattern if regex
|
||||
// Validate pattern if provided
|
||||
if (origin_type === 'pattern' && origin_value) {
|
||||
try {
|
||||
new RegExp(origin_value);
|
||||
} catch {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid regex pattern',
|
||||
});
|
||||
return res.status(400).json({ error: 'Invalid regex pattern' });
|
||||
}
|
||||
}
|
||||
|
||||
const result = await pool.query(`
|
||||
UPDATE trusted_origins
|
||||
SET
|
||||
origin_type = COALESCE($1, origin_type),
|
||||
origin_value = COALESCE($2, origin_value),
|
||||
description = COALESCE($3, description),
|
||||
active = COALESCE($4, active),
|
||||
updated_at = NOW()
|
||||
WHERE id = $5
|
||||
RETURNING id, origin_type, origin_value, description, active, updated_at
|
||||
`, [origin_type, origin_value, description, active, id]);
|
||||
name = COALESCE($1, name),
|
||||
origin_type = COALESCE($2, origin_type),
|
||||
origin_value = COALESCE($3, origin_value),
|
||||
description = COALESCE($4, description),
|
||||
active = COALESCE($5, active)
|
||||
WHERE id = $6
|
||||
RETURNING *
|
||||
`, [name, origin_type, origin_value, description, active, id]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ success: false, error: 'Origin not found' });
|
||||
return res.status(404).json({ error: 'Trusted origin not found' });
|
||||
}
|
||||
|
||||
// Invalidate cache
|
||||
// Clear cache so middleware picks up changes
|
||||
clearTrustedOriginsCache();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
origin: result.rows[0],
|
||||
});
|
||||
res.json({ origin: result.rows[0] });
|
||||
} catch (error: any) {
|
||||
console.error('[TrustedOrigins] Update error:', error.message);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
if (error.code === '23505') {
|
||||
return res.status(400).json({ error: 'This origin already exists' });
|
||||
}
|
||||
console.error('Error updating trusted origin:', error);
|
||||
res.status(500).json({ error: 'Failed to update trusted origin' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/admin/trusted-origins/:id
|
||||
* Delete a trusted origin
|
||||
*/
|
||||
router.delete('/:id', async (req: AuthRequest, res: Response) => {
|
||||
// Delete trusted origin
|
||||
router.delete('/:id', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const { id } = req.params;
|
||||
|
||||
const result = await pool.query(`
|
||||
DELETE FROM trusted_origins WHERE id = $1 RETURNING id, origin_value
|
||||
`, [id]);
|
||||
const result = await pool.query('DELETE FROM trusted_origins WHERE id = $1 RETURNING *', [id]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ success: false, error: 'Origin not found' });
|
||||
return res.status(404).json({ error: 'Trusted origin not found' });
|
||||
}
|
||||
|
||||
// Invalidate cache
|
||||
// Clear cache so middleware picks up deletion
|
||||
clearTrustedOriginsCache();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
deleted: result.rows[0],
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('[TrustedOrigins] Delete error:', error.message);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
res.json({ success: true, message: 'Trusted origin deleted' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting trusted origin:', error);
|
||||
res.status(500).json({ error: 'Failed to delete trusted origin' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/admin/trusted-origins/:id/toggle
|
||||
* Toggle active status
|
||||
*/
|
||||
router.post('/:id/toggle', async (req: AuthRequest, res: Response) => {
|
||||
// Toggle active status
|
||||
router.patch('/:id/toggle', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const { id } = req.params;
|
||||
|
||||
const result = await pool.query(`
|
||||
UPDATE trusted_origins
|
||||
SET active = NOT active, updated_at = NOW()
|
||||
SET active = NOT active
|
||||
WHERE id = $1
|
||||
RETURNING id, origin_type, origin_value, active
|
||||
RETURNING *
|
||||
`, [id]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ success: false, error: 'Origin not found' });
|
||||
return res.status(404).json({ error: 'Trusted origin not found' });
|
||||
}
|
||||
|
||||
// Invalidate cache
|
||||
// Clear cache so middleware picks up change
|
||||
clearTrustedOriginsCache();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
origin: result.rows[0],
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('[TrustedOrigins] Toggle error:', error.message);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
res.json({ origin: result.rows[0] });
|
||||
} catch (error) {
|
||||
console.error('Error toggling trusted origin:', error);
|
||||
res.status(500).json({ error: 'Failed to toggle trusted origin' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user