const express = require('express'); const router = express.Router(); const CustomFeature = require('../models/custom_feature'); const CustomTemplate = require('../models/custom_template'); const AdminNotification = require('../models/admin_notification'); const FeatureSimilarityService = require('../services/feature_similarity'); const jwt = require('jsonwebtoken'); const Joi = require('joi'); // Initialize similarity service const similarityService = new FeatureSimilarityService(); // Middleware to check if user is admin using JWT from Authorization header const requireAdmin = (req, res, next) => { try { const authHeader = req.headers.authorization || ''; if (!authHeader.startsWith('Bearer ')) { return res.status(401).json({ success: false, error: 'Authentication required', message: 'Missing or invalid Authorization header' }); } const token = authHeader.substring(7); const decoded = jwt.verify( token, process.env.JWT_ACCESS_SECRET || 'access-secret-key-2024-tech4biz', { issuer: 'tech4biz-auth', audience: 'tech4biz-users' } ); if (!decoded || decoded.role !== 'admin') { return res.status(403).json({ success: false, error: 'Insufficient permissions', message: 'Admin role required' }); } req.user = decoded; next(); } catch (err) { return res.status(401).json({ success: false, error: 'Invalid token', message: 'Failed to authenticate admin' }); } }; // Apply admin middleware to all routes router.use(requireAdmin); // GET /api/admin/custom-features - Proxy to existing custom features functionality router.get('/custom-features', async (req, res) => { try { const limit = parseInt(req.query.limit) || 50; const offset = parseInt(req.query.offset) || 0; const status = req.query.status; console.log(`Admin: Fetching custom features (status: ${status || 'all'}, limit: ${limit}, offset: ${offset})`); let features; if (status) { features = await CustomFeature.getFeaturesByStatus(status, limit, offset); } else { features = await CustomFeature.getAllFeatures(limit, offset); } res.json({ success: true, data: features, count: features.length, message: `Found ${features.length} custom features` }); } catch (error) { console.error('Error fetching custom features:', error.message); res.status(500).json({ success: false, error: 'Failed to fetch custom features', message: error.message }); } }); // POST /api/admin/custom-features/:id/review - Review custom feature router.post('/custom-features/:id/review', async (req, res) => { try { const { id } = req.params; const { status, admin_notes, admin_reviewed_by } = req.body; const validStatuses = ['approved', 'rejected', 'duplicate']; if (!validStatuses.includes(status)) { return res.status(400).json({ success: false, error: 'Invalid status', message: `Status must be one of: ${validStatuses.join(', ')}` }); } console.log(`🔍 Admin: Reviewing custom feature ${id} with status: ${status}`); const feature = await CustomFeature.getById(id); if (!feature) { return res.status(404).json({ success: false, error: 'Feature not found', message: 'The specified feature does not exist' }); } const reviewData = { status, admin_notes, admin_reviewed_by: admin_reviewed_by || req.user.id }; const updatedFeature = await CustomFeature.reviewFeature(id, reviewData); try { await AdminNotification.notifyFeatureReviewed(id, feature.name, status); } catch (notifError) { console.warn('⚠️ Failed to create notification:', notifError.message); } res.json({ success: true, data: updatedFeature, message: `Feature "${feature.name}" has been ${status}` }); } catch (error) { console.error('❌ Error reviewing custom feature:', error.message); res.status(500).json({ success: false, error: 'Failed to review custom feature', message: error.message }); } }); // GET /api/admin/custom-templates - Get custom templates router.get('/custom-templates', async (req, res) => { try { const limit = parseInt(req.query.limit) || 50; const offset = parseInt(req.query.offset) || 0; const status = req.query.status; console.log(`Admin: Fetching custom templates (status: ${status || 'all'}, limit: ${limit}, offset: ${offset})`); let templates; if (status) { templates = await CustomTemplate.getTemplatesByStatus(status, limit, offset); } else { templates = await CustomTemplate.getAllTemplates(limit, offset); } res.json({ success: true, data: templates, count: templates.length, message: `Found ${templates.length} custom templates` }); } catch (error) { console.error('Error fetching custom templates:', error.message); res.status(500).json({ success: false, error: 'Failed to fetch custom templates', message: error.message }); } }); // POST /api/admin/custom-templates/:id/review - Review custom template router.post('/custom-templates/:id/review', async (req, res) => { try { const { id } = req.params; const { status, admin_notes, admin_reviewed_by } = req.body; const validStatuses = ['approved', 'rejected', 'duplicate']; if (!validStatuses.includes(status)) { return res.status(400).json({ success: false, error: 'Invalid status', message: `Status must be one of: ${validStatuses.join(', ')}` }); } console.log(`🔍 Admin: Reviewing custom template ${id} with status: ${status}`); const template = await CustomTemplate.getById(id); if (!template) { return res.status(404).json({ success: false, error: 'Template not found', message: 'The specified template does not exist' }); } const reviewData = { status, admin_notes, admin_reviewed_by: admin_reviewed_by || req.user.id }; const updatedTemplate = await CustomTemplate.reviewTemplate(id, reviewData); try { await AdminNotification.notifyTemplateReviewed(id, template.title, status); } catch (notifError) { console.warn('⚠️ Failed to create notification:', notifError.message); } res.json({ success: true, data: updatedTemplate, message: `Template "${template.title}" has been ${status}` }); } catch (error) { console.error('❌ Error reviewing custom template:', error.message); res.status(500).json({ success: false, error: 'Failed to review custom template', message: error.message }); } }); // GET /api/admin/templates/stats - Get template statistics router.get('/templates/stats', async (req, res) => { try { console.log('📊 Admin: Fetching template statistics...'); const stats = await CustomTemplate.getTemplateStats(); const notificationCounts = await AdminNotification.getCounts(); res.json({ success: true, data: { templates: stats, notifications: notificationCounts }, message: 'Template statistics retrieved successfully' }); } catch (error) { console.error('❌ Error fetching template stats:', error.message); res.status(500).json({ success: false, error: 'Failed to fetch template statistics', message: error.message }); } }); // GET /api/admin/features/pending - Get pending features for review router.get('/features/pending', async (req, res) => { try { const limit = parseInt(req.query.limit) || 50; const offset = parseInt(req.query.offset) || 0; console.log(`Admin: Fetching pending features (limit: ${limit}, offset: ${offset})`); const features = await CustomFeature.getPendingFeatures(limit, offset); res.json({ success: true, data: features, count: features.length, message: `Found ${features.length} pending features` }); } catch (error) { console.error('Error fetching pending features:', error.message); res.status(500).json({ success: false, error: 'Failed to fetch pending features', message: error.message }); } }); // GET /api/admin/features/status/:status - Get features by status router.get('/features/status/:status', async (req, res) => { try { const { status } = req.params; const limit = parseInt(req.query.limit) || 50; const offset = parseInt(req.query.offset) || 0; const validStatuses = ['pending', 'approved', 'rejected', 'duplicate']; if (!validStatuses.includes(status)) { return res.status(400).json({ success: false, error: 'Invalid status', message: `Status must be one of: ${validStatuses.join(', ')}` }); } console.log(`🔍 Admin: Fetching ${status} features (limit: ${limit}, offset: ${offset})`); const features = await CustomFeature.getFeaturesByStatus(status, limit, offset); res.json({ success: true, data: features, count: features.length, message: `Found ${features.length} ${status} features` }); } catch (error) { console.error('❌ Error fetching features by status:', error.message); res.status(500).json({ success: false, error: 'Failed to fetch features by status', message: error.message }); } }); // GET /api/admin/features/stats - Get feature statistics router.get('/features/stats', async (req, res) => { try { console.log('📊 Admin: Fetching feature statistics...'); const stats = await CustomFeature.getFeatureStats(); const notificationCounts = await AdminNotification.getCounts(); res.json({ success: true, data: { features: stats, notifications: notificationCounts }, message: 'Feature statistics retrieved successfully' }); } catch (error) { console.error('❌ Error fetching feature stats:', error.message); res.status(500).json({ success: false, error: 'Failed to fetch feature statistics', message: error.message }); } }); // POST /api/admin/features/:id/review - Review a feature router.post('/features/:id/review', async (req, res) => { try { const { id } = req.params; const { status, notes, canonical_feature_id, admin_reviewed_by } = req.body; // Validate input const schema = Joi.object({ status: Joi.string().valid('approved', 'rejected', 'duplicate').required(), notes: Joi.string().optional(), canonical_feature_id: Joi.string().uuid().optional(), admin_reviewed_by: Joi.string().required() }); const { error } = schema.validate(req.body); if (error) { return res.status(400).json({ success: false, error: 'Validation error', message: error.details[0].message }); } // Validate canonical_feature_id is provided for duplicate status if (status === 'duplicate' && !canonical_feature_id) { return res.status(400).json({ success: false, error: 'Missing canonical feature ID', message: 'Canonical feature ID is required when marking as duplicate' }); } console.log(`🔍 Admin: Reviewing feature ${id} with status: ${status}`); // Get the feature first to get its name for notification const feature = await CustomFeature.getById(id); if (!feature) { return res.status(404).json({ success: false, error: 'Feature not found', message: 'The specified feature does not exist' }); } // Review the feature const reviewData = { status, admin_notes: notes, canonical_feature_id, admin_reviewed_by }; const updatedFeature = await CustomFeature.reviewFeature(id, reviewData); // Create notification await AdminNotification.notifyFeatureReviewed(id, feature.name, status); res.json({ success: true, data: updatedFeature, message: `Feature "${feature.name}" has been ${status}` }); } catch (error) { console.error('❌ Error reviewing feature:', error.message); res.status(500).json({ success: false, error: 'Failed to review feature', message: error.message }); } }); // GET /api/admin/features/similar - Find similar features router.get('/features/similar', async (req, res) => { try { const { q: query, threshold = 0.7, limit = 5 } = req.query; if (!query) { return res.status(400).json({ success: false, error: 'Query parameter required', message: 'Please provide a query parameter "q"' }); } console.log(`🔍 Admin: Finding similar features for "${query}"`); const similarFeatures = await similarityService.findSimilarFeatures( query, parseFloat(threshold), parseInt(limit) ); res.json({ success: true, data: similarFeatures, count: similarFeatures.length, message: `Found ${similarFeatures.length} similar features` }); } catch (error) { console.error('❌ Error finding similar features:', error.message); res.status(500).json({ success: false, error: 'Failed to find similar features', message: error.message }); } }); // POST /api/admin/features/:id/synonyms - Add feature synonym router.post('/features/:id/synonyms', async (req, res) => { try { const { id } = req.params; const { synonym, created_by } = req.body; // Validate input const schema = Joi.object({ synonym: Joi.string().min(1).max(200).required(), created_by: Joi.string().optional() }); const { error } = schema.validate(req.body); if (error) { return res.status(400).json({ success: false, error: 'Validation error', message: error.details[0].message }); } console.log(`🔍 Admin: Adding synonym "${synonym}" to feature ${id}`); const newSynonym = await similarityService.addSynonym(id, synonym, created_by || 'admin'); res.json({ success: true, data: newSynonym, message: `Synonym "${synonym}" added successfully` }); } catch (error) { console.error('❌ Error adding synonym:', error.message); res.status(500).json({ success: false, error: 'Failed to add synonym', message: error.message }); } }); // GET /api/admin/features/:id/synonyms - Get feature synonyms router.get('/features/:id/synonyms', async (req, res) => { try { const { id } = req.params; console.log(`🔍 Admin: Getting synonyms for feature ${id}`); const synonyms = await similarityService.getSynonyms(id); res.json({ success: true, data: synonyms, count: synonyms.length, message: `Found ${synonyms.length} synonyms` }); } catch (error) { console.error('❌ Error getting synonyms:', error.message); res.status(500).json({ success: false, error: 'Failed to get synonyms', message: error.message }); } }); // GET /api/admin/notifications - Get admin notifications router.get('/notifications', async (req, res) => { try { const { unread_only = 'false' } = req.query; const limit = parseInt(req.query.limit) || 50; const offset = parseInt(req.query.offset) || 0; console.log(`🔍 Admin: Fetching notifications (unread_only: ${unread_only})`); let notifications; if (unread_only === 'true') { notifications = await AdminNotification.getUnread(limit); } else { notifications = await AdminNotification.getAll(limit, offset); } res.json({ success: true, data: notifications, count: notifications.length, message: `Found ${notifications.length} notifications` }); } catch (error) { console.error('❌ Error fetching notifications:', error.message); res.status(500).json({ success: false, error: 'Failed to fetch notifications', message: error.message }); } }); // POST /api/admin/notifications/:id/read - Mark notification as read router.post('/notifications/:id/read', async (req, res) => { try { const { id } = req.params; console.log(`🔍 Admin: Marking notification ${id} as read`); const notification = await AdminNotification.markAsRead(id); if (!notification) { return res.status(404).json({ success: false, error: 'Notification not found', message: 'The specified notification does not exist' }); } res.json({ success: true, data: notification, message: 'Notification marked as read' }); } catch (error) { console.error('❌ Error marking notification as read:', error.message); res.status(500).json({ success: false, error: 'Failed to mark notification as read', message: error.message }); } }); // POST /api/admin/notifications/read-all - Mark all notifications as read router.post('/notifications/read-all', async (req, res) => { try { console.log('🔍 Admin: Marking all notifications as read'); const count = await AdminNotification.markAllAsRead(); res.json({ success: true, data: { count }, message: `${count} notifications marked as read` }); } catch (error) { console.error('❌ Error marking all notifications as read:', error.message); res.status(500).json({ success: false, error: 'Failed to mark all notifications as read', message: error.message }); } }); // ---------- CUSTOM TEMPLATES ADMIN ROUTES ---------- // GET /api/admin/templates/pending - Get pending templates for review router.get('/templates/pending', async (req, res) => { try { const limit = parseInt(req.query.limit) || 50; const offset = parseInt(req.query.offset) || 0; console.log(`Admin: Fetching pending templates (limit: ${limit}, offset: ${offset})`); const templates = await CustomTemplate.getPendingTemplates(limit, offset); res.json({ success: true, data: templates, count: templates.length, message: `Found ${templates.length} pending templates` }); } catch (error) { console.error('Error fetching pending templates:', error.message); res.status(500).json({ success: false, error: 'Failed to fetch pending templates', message: error.message }); } }); // GET /api/admin/templates/status/:status - Get templates by status router.get('/templates/status/:status', async (req, res) => { try { const { status } = req.params; const limit = parseInt(req.query.limit) || 50; const offset = parseInt(req.query.offset) || 0; const validStatuses = ['pending', 'approved', 'rejected', 'duplicate']; if (!validStatuses.includes(status)) { return res.status(400).json({ success: false, error: 'Invalid status', message: `Status must be one of: ${validStatuses.join(', ')}` }); } console.log(`🔍 Admin: Fetching ${status} templates (limit: ${limit}, offset: ${offset})`); const templates = await CustomTemplate.getTemplatesByStatus(status, limit, offset); res.json({ success: true, data: templates, count: templates.length, message: `Found ${templates.length} ${status} templates` }); } catch (error) { console.error('❌ Error fetching templates by status:', error.message); res.status(500).json({ success: false, error: 'Failed to fetch templates by status', message: error.message }); } }); // POST /api/admin/templates/:id/review - Review custom template router.post('/templates/:id/review', async (req, res) => { try { const { id } = req.params; const { status, admin_notes, canonical_template_id } = req.body; if (!status) { return res.status(400).json({ success: false, error: 'Status required', message: 'Status is required for template review' }); } const validStatuses = ['approved', 'rejected', 'duplicate']; if (!validStatuses.includes(status)) { return res.status(400).json({ success: false, error: 'Invalid status', message: `Status must be one of: ${validStatuses.join(', ')}` }); } console.log(`🔍 Admin: Reviewing template ${id} with status: ${status}`); // If approving, also create a main template entry and link it, atomically if (status === 'approved') { const existingCustom = await CustomTemplate.getById(id); if (!existingCustom) { return res.status(404).json({ success: false, error: 'Template not found', message: 'The specified template does not exist' }); } // Create in main templates const Template = require('../models/template'); const payload = { type: existingCustom.type, title: existingCustom.title, description: existingCustom.description, icon: existingCustom.icon, category: existingCustom.category, gradient: existingCustom.gradient, border: existingCustom.border, text: existingCustom.text, subtext: existingCustom.subtext, }; const created = await Template.create(payload); // Update custom template flags and link canonical_template_id const updatedCustom = await CustomTemplate.reviewTemplate(id, { status: 'approved', admin_notes, canonical_template_id: created.id, admin_reviewed_by: req.user.username || req.user.email }); return res.json({ success: true, data: { custom_template: updatedCustom, template: created }, message: `Template '${created.title}' created and custom template approved` }); } const template = await CustomTemplate.reviewTemplate(id, { status, admin_notes, canonical_template_id, admin_reviewed_by: req.user.username || req.user.email }); if (!template) { return res.status(404).json({ success: false, error: 'Template not found', message: 'The specified template does not exist' }); } // If approved, activate the mirrored template if (status === 'approved') { try { const Template = require('../models/template'); const mirroredTemplate = await Template.getByType(`custom_${id}`); if (mirroredTemplate) { await mirroredTemplate.update({ is_active: true }); } } catch (activateErr) { console.error('Failed to activate approved template:', activateErr.message); } } res.json({ success: true, data: template, message: `Template '${template.title}' ${status} successfully` }); } catch (error) { console.error('❌ Error reviewing template:', error.message); res.status(500).json({ success: false, error: 'Failed to review template', message: error.message }); } }); // GET /api/admin/templates/stats - Get custom template statistics router.get('/templates/stats', async (req, res) => { try { console.log('📊 Admin: Fetching custom template statistics...'); const stats = await CustomTemplate.getTemplateStats(); res.json({ success: true, data: stats, message: 'Custom template statistics retrieved successfully' }); } catch (error) { console.error('❌ Error fetching custom template stats:', error.message); res.status(500).json({ success: false, error: 'Failed to fetch custom template statistics', message: error.message }); } }); // GET /api/admin/custom-features - Get all custom features (Admin only) router.get('/custom-features', async (req, res) => { try { const { status, limit = 50, offset = 0 } = req.query; const limitNum = parseInt(limit); const offsetNum = parseInt(offset); console.log(`🔍 Admin: Fetching custom features (status: ${status || 'all'}, limit: ${limitNum}, offset: ${offsetNum})`); let features; if (status) { features = await CustomFeature.getFeaturesByStatus(status, limitNum, offsetNum); } else { features = await CustomFeature.getAllFeatures(limitNum, offsetNum); } res.json({ success: true, data: features, count: features.length, message: `Found ${features.length} custom features` }); } catch (error) { console.error('❌ Error fetching custom features:', error.message); res.status(500).json({ success: false, error: 'Failed to fetch custom features', message: error.message }); } }); // POST /api/admin/custom-features/:id/review - Review custom feature (Admin only) router.post('/custom-features/:id/review', async (req, res) => { try { const { id } = req.params; const { status, admin_notes, canonical_feature_id } = req.body; if (!status) { return res.status(400).json({ success: false, error: 'Status required', message: 'Status is required for feature review' }); } const validStatuses = ['approved', 'rejected', 'duplicate']; if (!validStatuses.includes(status)) { return res.status(400).json({ success: false, error: 'Invalid status', message: `Status must be one of: ${validStatuses.join(', ')}` }); } console.log(`🔍 Admin: Reviewing custom feature ${id} with status: ${status}`); const feature = await CustomFeature.reviewFeature(id, { status, admin_notes, canonical_feature_id, admin_reviewed_by: req.user.username || req.user.email }); if (!feature) { return res.status(404).json({ success: false, error: 'Feature not found', message: 'The specified feature does not exist' }); } res.json({ success: true, data: feature, message: `Feature '${feature.name}' ${status} successfully` }); } catch (error) { console.error('❌ Error reviewing custom feature:', error.message); res.status(500).json({ success: false, error: 'Failed to review custom feature', message: error.message }); } }); // GET /api/admin/custom-templates - Get all custom templates (Admin only) router.get('/custom-templates', async (req, res) => { try { const { status, limit = 50, offset = 0 } = req.query; const limitNum = parseInt(limit); const offsetNum = parseInt(offset); console.log(`🔍 Admin: Fetching custom templates (status: ${status || 'all'}, limit: ${limitNum}, offset: ${offsetNum})`); let templates; if (status) { templates = await CustomTemplate.getTemplatesByStatus(status, limitNum, offsetNum); } else { templates = await CustomTemplate.getAllTemplates(limitNum, offsetNum); } res.json({ success: true, data: templates, count: templates.length, message: `Found ${templates.length} custom templates` }); } catch (error) { console.error('❌ Error fetching custom templates:', error.message); res.status(500).json({ success: false, error: 'Failed to fetch custom templates', message: error.message }); } }); // POST /api/admin/custom-templates/:id/review - Review custom template (Admin only) router.post('/custom-templates/:id/review', async (req, res) => { try { const { id } = req.params; const { status, admin_notes, canonical_template_id } = req.body; if (!status) { return res.status(400).json({ success: false, error: 'Status required', message: 'Status is required for template review' }); } const validStatuses = ['approved', 'rejected', 'duplicate']; if (!validStatuses.includes(status)) { return res.status(400).json({ success: false, error: 'Invalid status', message: `Status must be one of: ${validStatuses.join(', ')}` }); } console.log(`🔍 Admin: Reviewing custom template ${id} with status: ${status}`); const template = await CustomTemplate.reviewTemplate(id, { status, admin_notes, canonical_template_id, admin_reviewed_by: req.user.username || req.user.email }); if (!template) { return res.status(404).json({ success: false, error: 'Template not found', message: 'The specified template does not exist' }); } res.json({ success: true, data: template, message: `Template '${template.title}' ${status} successfully` }); } catch (error) { console.error('❌ Error reviewing custom template:', error.message); res.status(500).json({ success: false, error: 'Failed to review custom template', message: error.message }); } }); module.exports = router;