## SEO Template Library - Add complete template library with 7 page types (state, city, category, brand, product, search, regeneration) - Add Template Library tab in SEO Orchestrator with accordion-based editors - Add template preview, validation, and variable injection engine - Add API endpoints: /api/seo/templates, preview, validate, generate, regenerate ## Discovery Pipeline - Add promotion.ts for discovery location validation and promotion - Add discover-all-states.ts script for multi-state discovery - Add promotion log migration (067) - Enhance discovery routes and types ## Orchestrator & Admin - Add crawl_enabled filter to stores page - Add API permissions page - Add job queue management - Add price analytics routes - Add markets and intelligence routes - Enhance dashboard and worker monitoring ## Infrastructure - Add migrations for worker definitions, SEO settings, field alignment - Add canonical pipeline for scraper v2 - Update hydration and sync orchestrator - Enhance multi-state query service 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
192 lines
5.7 KiB
TypeScript
192 lines
5.7 KiB
TypeScript
import { Router } from 'express';
|
|
import { authMiddleware, requireRole } from '../auth/middleware';
|
|
import { pool } from '../db/pool';
|
|
import crypto from 'crypto';
|
|
|
|
const router = Router();
|
|
router.use(authMiddleware);
|
|
|
|
// Generate secure random API key (64-character hex)
|
|
function generateApiKey(): string {
|
|
return crypto.randomBytes(32).toString('hex');
|
|
}
|
|
|
|
// Get all API permissions
|
|
router.get('/', requireRole('superadmin', 'admin'), async (req, res) => {
|
|
try {
|
|
const result = await pool.query(`
|
|
SELECT *
|
|
FROM wp_dutchie_api_permissions
|
|
ORDER BY created_at DESC
|
|
`);
|
|
|
|
res.json({ permissions: result.rows });
|
|
} catch (error) {
|
|
console.error('Error fetching API permissions:', error);
|
|
res.status(500).json({ error: 'Failed to fetch API permissions' });
|
|
}
|
|
});
|
|
|
|
// Get all dispensaries for dropdown (must be before /:id to avoid route conflict)
|
|
router.get('/dispensaries', requireRole('superadmin', 'admin'), async (req, res) => {
|
|
try {
|
|
const result = await pool.query(`
|
|
SELECT id, name
|
|
FROM dispensaries
|
|
ORDER BY name
|
|
`);
|
|
res.json({ dispensaries: result.rows });
|
|
} catch (error) {
|
|
console.error('Error fetching dispensaries:', error);
|
|
res.status(500).json({ error: 'Failed to fetch dispensaries' });
|
|
}
|
|
});
|
|
|
|
// Get single API permission
|
|
router.get('/:id', requireRole('superadmin', 'admin'), async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const result = await pool.query(`
|
|
SELECT *
|
|
FROM wp_dutchie_api_permissions
|
|
WHERE id = $1
|
|
`, [id]);
|
|
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: 'Permission not found' });
|
|
}
|
|
|
|
res.json({ permission: result.rows[0] });
|
|
} catch (error) {
|
|
console.error('Error fetching API permission:', error);
|
|
res.status(500).json({ error: 'Failed to fetch API permission' });
|
|
}
|
|
});
|
|
|
|
// Create new API permission
|
|
router.post('/', requireRole('superadmin', 'admin'), async (req, res) => {
|
|
try {
|
|
// Support both store_id (existing) and dispensary_id (for compatibility)
|
|
const { user_name, allowed_ips, allowed_domains, store_id, dispensary_id } = req.body;
|
|
const storeIdToUse = store_id || dispensary_id;
|
|
|
|
if (!user_name) {
|
|
return res.status(400).json({ error: 'User name is required' });
|
|
}
|
|
|
|
if (!storeIdToUse) {
|
|
return res.status(400).json({ error: 'Store/Dispensary is required' });
|
|
}
|
|
|
|
// Get dispensary name for display
|
|
const dispensaryResult = await pool.query('SELECT name FROM dispensaries WHERE id = $1', [storeIdToUse]);
|
|
if (dispensaryResult.rows.length === 0) {
|
|
return res.status(400).json({ error: 'Invalid store/dispensary ID' });
|
|
}
|
|
const storeName = dispensaryResult.rows[0].name;
|
|
|
|
const apiKey = generateApiKey();
|
|
|
|
const result = await pool.query(`
|
|
INSERT INTO wp_dutchie_api_permissions (
|
|
user_name,
|
|
api_key,
|
|
allowed_ips,
|
|
allowed_domains,
|
|
is_active,
|
|
store_id,
|
|
store_name
|
|
)
|
|
VALUES ($1, $2, $3, $4, 1, $5, $6)
|
|
RETURNING *
|
|
`, [
|
|
user_name,
|
|
apiKey,
|
|
allowed_ips || null,
|
|
allowed_domains || null,
|
|
storeIdToUse,
|
|
storeName
|
|
]);
|
|
|
|
res.status(201).json({
|
|
permission: result.rows[0],
|
|
message: 'API permission created successfully. Save the API key securely - it cannot be retrieved later.'
|
|
});
|
|
} catch (error) {
|
|
console.error('Error creating API permission:', error);
|
|
res.status(500).json({ error: 'Failed to create API permission' });
|
|
}
|
|
});
|
|
|
|
// Update API permission
|
|
router.put('/:id', requireRole('superadmin', 'admin'), async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { user_name, allowed_ips, allowed_domains, is_active } = req.body;
|
|
|
|
const result = await pool.query(`
|
|
UPDATE wp_dutchie_api_permissions
|
|
SET
|
|
user_name = COALESCE($1, user_name),
|
|
allowed_ips = COALESCE($2, allowed_ips),
|
|
allowed_domains = COALESCE($3, allowed_domains),
|
|
is_active = COALESCE($4, is_active)
|
|
WHERE id = $5
|
|
RETURNING *
|
|
`, [user_name, allowed_ips, allowed_domains, is_active, id]);
|
|
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: 'Permission not found' });
|
|
}
|
|
|
|
res.json({ permission: result.rows[0] });
|
|
} catch (error) {
|
|
console.error('Error updating API permission:', error);
|
|
res.status(500).json({ error: 'Failed to update API permission' });
|
|
}
|
|
});
|
|
|
|
// Toggle permission active status
|
|
router.patch('/:id/toggle', requireRole('superadmin', 'admin'), async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const result = await pool.query(`
|
|
UPDATE wp_dutchie_api_permissions
|
|
SET is_active = CASE WHEN is_active = 1 THEN 0 ELSE 1 END
|
|
WHERE id = $1
|
|
RETURNING *
|
|
`, [id]);
|
|
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: 'Permission not found' });
|
|
}
|
|
|
|
res.json({ permission: result.rows[0] });
|
|
} catch (error) {
|
|
console.error('Error toggling API permission:', error);
|
|
res.status(500).json({ error: 'Failed to toggle API permission' });
|
|
}
|
|
});
|
|
|
|
// Delete API permission
|
|
router.delete('/:id', requireRole('superadmin'), async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const result = await pool.query('DELETE FROM wp_dutchie_api_permissions WHERE id = $1 RETURNING *', [id]);
|
|
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: 'Permission not found' });
|
|
}
|
|
|
|
res.json({ message: 'API permission deleted successfully' });
|
|
} catch (error) {
|
|
console.error('Error deleting API permission:', error);
|
|
res.status(500).json({ error: 'Failed to delete API permission' });
|
|
}
|
|
});
|
|
|
|
export default router;
|