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:
Kelly
2025-12-13 19:54:26 -07:00
parent eb5b2a876e
commit b456fe5097
6 changed files with 643 additions and 161 deletions

View File

@@ -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)

View File

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