backend changes
This commit is contained in:
parent
89cc485734
commit
f12b236e46
@ -90,9 +90,15 @@ CREATE INDEX IF NOT EXISTS idx_admin_notifications_type ON admin_notifications(t
|
|||||||
CREATE INDEX IF NOT EXISTS idx_admin_notifications_is_read ON admin_notifications(is_read);
|
CREATE INDEX IF NOT EXISTS idx_admin_notifications_is_read ON admin_notifications(is_read);
|
||||||
CREATE INDEX IF NOT EXISTS idx_admin_notifications_created_at ON admin_notifications(created_at DESC);
|
CREATE INDEX IF NOT EXISTS idx_admin_notifications_created_at ON admin_notifications(created_at DESC);
|
||||||
|
|
||||||
-- 6. Update existing custom_features to have 'approved' status if they were previously approved
|
-- 6. Clean up orphaned custom_features records before updating status
|
||||||
DO $$
|
DO $$
|
||||||
BEGIN
|
BEGIN
|
||||||
|
-- Delete custom_features records that reference non-existent templates
|
||||||
|
DELETE FROM custom_features
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM templates WHERE id = custom_features.template_id AND is_active = true)
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM custom_templates WHERE id = custom_features.template_id);
|
||||||
|
|
||||||
|
-- Update existing custom_features to have 'approved' status if they were previously approved
|
||||||
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'custom_features' AND column_name = 'status') THEN
|
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'custom_features' AND column_name = 'status') THEN
|
||||||
UPDATE custom_features
|
UPDATE custom_features
|
||||||
SET status = CASE
|
SET status = CASE
|
||||||
|
|||||||
@ -12,11 +12,18 @@ ADD COLUMN IF NOT EXISTS template_type VARCHAR(20) DEFAULT 'default' CHECK (temp
|
|||||||
-- Update existing records to have the correct template_type
|
-- Update existing records to have the correct template_type
|
||||||
UPDATE custom_features
|
UPDATE custom_features
|
||||||
SET template_type = CASE
|
SET template_type = CASE
|
||||||
WHEN EXISTS (SELECT 1 FROM templates WHERE id = template_id) THEN 'default'
|
WHEN EXISTS (SELECT 1 FROM templates WHERE id = template_id AND is_active = true) THEN 'default'
|
||||||
WHEN EXISTS (SELECT 1 FROM custom_templates WHERE id = template_id) THEN 'custom'
|
WHEN EXISTS (SELECT 1 FROM custom_templates WHERE id = template_id) THEN 'custom'
|
||||||
ELSE 'default'
|
ELSE 'default'
|
||||||
END
|
END
|
||||||
WHERE template_type IS NULL;
|
WHERE template_type IS NULL OR template_type = '';
|
||||||
|
|
||||||
|
-- Fix any existing records where template_type is 'custom' but template_id exists in templates table
|
||||||
|
UPDATE custom_features
|
||||||
|
SET template_type = 'default'
|
||||||
|
WHERE template_type = 'custom'
|
||||||
|
AND EXISTS (SELECT 1 FROM templates WHERE id = template_id AND is_active = true)
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM custom_templates WHERE id = template_id);
|
||||||
|
|
||||||
-- Create a function to validate template_id references
|
-- Create a function to validate template_id references
|
||||||
CREATE OR REPLACE FUNCTION validate_template_reference()
|
CREATE OR REPLACE FUNCTION validate_template_reference()
|
||||||
@ -28,8 +35,11 @@ BEGIN
|
|||||||
RAISE EXCEPTION 'Template ID % does not exist in templates table or is not active', NEW.template_id;
|
RAISE EXCEPTION 'Template ID % does not exist in templates table or is not active', NEW.template_id;
|
||||||
END IF;
|
END IF;
|
||||||
ELSIF NEW.template_type = 'custom' THEN
|
ELSIF NEW.template_type = 'custom' THEN
|
||||||
|
-- First check custom_templates, then fall back to templates table
|
||||||
IF NOT EXISTS (SELECT 1 FROM custom_templates WHERE id = NEW.template_id) THEN
|
IF NOT EXISTS (SELECT 1 FROM custom_templates WHERE id = NEW.template_id) THEN
|
||||||
RAISE EXCEPTION 'Custom template ID % does not exist in custom_templates table', NEW.template_id;
|
IF NOT EXISTS (SELECT 1 FROM templates WHERE id = NEW.template_id AND is_active = true) THEN
|
||||||
|
RAISE EXCEPTION 'Template ID % does not exist in custom_templates or templates table', NEW.template_id;
|
||||||
|
END IF;
|
||||||
END IF;
|
END IF;
|
||||||
ELSE
|
ELSE
|
||||||
RAISE EXCEPTION 'Invalid template_type: %. Must be either "default" or "custom"', NEW.template_type;
|
RAISE EXCEPTION 'Invalid template_type: %. Must be either "default" or "custom"', NEW.template_type;
|
||||||
|
|||||||
@ -170,9 +170,12 @@ class CustomFeature {
|
|||||||
// Admin workflow methods
|
// Admin workflow methods
|
||||||
static async getPendingFeatures(limit = 50, offset = 0) {
|
static async getPendingFeatures(limit = 50, offset = 0) {
|
||||||
const query = `
|
const query = `
|
||||||
SELECT cf.*, t.title as template_title
|
SELECT cf.*,
|
||||||
|
COALESCE(t.title, ct.title) as template_title,
|
||||||
|
COALESCE(t.type, 'custom') as template_type
|
||||||
FROM custom_features cf
|
FROM custom_features cf
|
||||||
LEFT JOIN templates t ON cf.template_id = t.id
|
LEFT JOIN templates t ON cf.template_id = t.id
|
||||||
|
LEFT JOIN custom_templates ct ON cf.template_id = ct.id
|
||||||
WHERE cf.status = 'pending'
|
WHERE cf.status = 'pending'
|
||||||
ORDER BY cf.created_at ASC
|
ORDER BY cf.created_at ASC
|
||||||
LIMIT $1 OFFSET $2
|
LIMIT $1 OFFSET $2
|
||||||
@ -183,9 +186,12 @@ class CustomFeature {
|
|||||||
|
|
||||||
static async getFeaturesByStatus(status, limit = 50, offset = 0) {
|
static async getFeaturesByStatus(status, limit = 50, offset = 0) {
|
||||||
const query = `
|
const query = `
|
||||||
SELECT cf.*, t.title as template_title
|
SELECT cf.*,
|
||||||
|
COALESCE(t.title, ct.title) as template_title,
|
||||||
|
COALESCE(t.type, 'custom') as template_type
|
||||||
FROM custom_features cf
|
FROM custom_features cf
|
||||||
LEFT JOIN templates t ON cf.template_id = t.id
|
LEFT JOIN templates t ON cf.template_id = t.id
|
||||||
|
LEFT JOIN custom_templates ct ON cf.template_id = ct.id
|
||||||
WHERE cf.status = $1
|
WHERE cf.status = $1
|
||||||
ORDER BY cf.created_at DESC
|
ORDER BY cf.created_at DESC
|
||||||
LIMIT $2 OFFSET $3
|
LIMIT $2 OFFSET $3
|
||||||
@ -229,8 +235,7 @@ class CustomFeature {
|
|||||||
|
|
||||||
const updated = await CustomFeature.update(id, updates);
|
const updated = await CustomFeature.update(id, updates);
|
||||||
|
|
||||||
// If approved, ensure a mirrored entry exists/updates in template_features
|
// If approved, create a NEW record in template_features table with feature_type='essential'
|
||||||
// Only mirror if the template_id exists in the main templates table
|
|
||||||
if (updated && status === 'approved') {
|
if (updated && status === 'approved') {
|
||||||
try {
|
try {
|
||||||
// Check if template_id exists in main templates table
|
// Check if template_id exists in main templates table
|
||||||
@ -240,39 +245,29 @@ class CustomFeature {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (templateCheck.rows.length > 0) {
|
if (templateCheck.rows.length > 0) {
|
||||||
// Template exists in main templates table, safe to mirror
|
// Template exists in main templates table, create new essential feature
|
||||||
const Feature = require('./feature');
|
const Feature = require('./feature');
|
||||||
const featureId = `custom_${updated.id}`;
|
|
||||||
const existingMirror = await Feature.getByFeatureId(updated.template_id, featureId);
|
// Create a completely new record in template_features with feature_type='essential'
|
||||||
if (existingMirror) {
|
await Feature.create({
|
||||||
await Feature.update(existingMirror.id, {
|
template_id: updated.template_id,
|
||||||
name: updated.name,
|
feature_id: `approved_custom_${updated.id}`,
|
||||||
description: updated.description,
|
name: updated.name,
|
||||||
complexity: updated.complexity,
|
description: updated.description,
|
||||||
feature_type: 'custom',
|
feature_type: 'essential',
|
||||||
is_default: false
|
complexity: updated.complexity,
|
||||||
});
|
display_order: 1, // High priority for approved features
|
||||||
console.log('✅ Updated mirrored feature in template_features for approved custom feature');
|
is_default: false,
|
||||||
} else {
|
created_by_user: false, // This is now an approved essential feature
|
||||||
await Feature.create({
|
usage_count: 1
|
||||||
template_id: updated.template_id,
|
});
|
||||||
feature_id: featureId,
|
console.log('✅ Created NEW essential feature in template_features for approved custom feature');
|
||||||
name: updated.name,
|
|
||||||
description: updated.description,
|
|
||||||
feature_type: 'custom',
|
|
||||||
complexity: updated.complexity,
|
|
||||||
display_order: 999,
|
|
||||||
is_default: false,
|
|
||||||
created_by_user: true
|
|
||||||
});
|
|
||||||
console.log('✅ Created mirrored feature in template_features for approved custom feature');
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Template is likely a custom template, don't mirror to template_features
|
// Template is likely a custom template, don't create in template_features
|
||||||
console.log('ℹ️ Custom feature approved but template_id references custom template, skipping mirror to template_features');
|
console.log('ℹ️ Custom feature approved but template_id references custom template, skipping creation in template_features');
|
||||||
}
|
}
|
||||||
} catch (mirrorErr) {
|
} catch (createError) {
|
||||||
console.error('⚠️ Failed to mirror approved custom feature into template_features:', mirrorErr.message);
|
console.error('⚠️ Failed to create new essential feature in template_features:', createError.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -327,9 +322,12 @@ class CustomFeature {
|
|||||||
// Get all custom features with pagination
|
// Get all custom features with pagination
|
||||||
static async getAllFeatures(limit = 50, offset = 0) {
|
static async getAllFeatures(limit = 50, offset = 0) {
|
||||||
const query = `
|
const query = `
|
||||||
SELECT cf.*, t.title as template_title
|
SELECT cf.*,
|
||||||
|
COALESCE(t.title, ct.title) as template_title,
|
||||||
|
COALESCE(t.type, 'custom') as template_type
|
||||||
FROM custom_features cf
|
FROM custom_features cf
|
||||||
LEFT JOIN templates t ON cf.template_id = t.id
|
LEFT JOIN templates t ON cf.template_id = t.id
|
||||||
|
LEFT JOIN custom_templates ct ON cf.template_id = ct.id
|
||||||
ORDER BY cf.created_at DESC
|
ORDER BY cf.created_at DESC
|
||||||
LIMIT $1 OFFSET $2
|
LIMIT $1 OFFSET $2
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -37,6 +37,34 @@ class CustomTemplate {
|
|||||||
return result.rows.length ? new CustomTemplate(result.rows[0]) : null;
|
return result.rows.length ? new CustomTemplate(result.rows[0]) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get custom template by ID with features
|
||||||
|
static async getByIdWithFeatures(id) {
|
||||||
|
const templateQuery = `
|
||||||
|
SELECT * FROM custom_templates
|
||||||
|
WHERE id = $1
|
||||||
|
`;
|
||||||
|
|
||||||
|
const featuresQuery = `
|
||||||
|
SELECT * FROM custom_features
|
||||||
|
WHERE template_id = $1
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`;
|
||||||
|
|
||||||
|
const [templateResult, featuresResult] = await Promise.all([
|
||||||
|
database.query(templateQuery, [id]),
|
||||||
|
database.query(featuresQuery, [id])
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (templateResult.rows.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = new CustomTemplate(templateResult.rows[0]);
|
||||||
|
template.features = featuresResult.rows;
|
||||||
|
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
// Check for duplicate custom templates based on title, type, category, and user_id
|
// Check for duplicate custom templates based on title, type, category, and user_id
|
||||||
static async checkForDuplicate(templateData) {
|
static async checkForDuplicate(templateData) {
|
||||||
const normalizedTitle = (templateData.title || '').toLowerCase();
|
const normalizedTitle = (templateData.title || '').toLowerCase();
|
||||||
|
|||||||
@ -226,12 +226,43 @@ router.post('/', async (req, res) => {
|
|||||||
return res.status(400).json({ success: false, error: 'Invalid complexity', message: `Complexity must be one of: ${validComplexity.join(', ')}` });
|
return res.status(400).json({ success: false, error: 'Invalid complexity', message: `Complexity must be one of: ${validComplexity.join(', ')}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate that template_id exists in either templates or custom_templates table
|
||||||
|
const templateCheck = await database.query(`
|
||||||
|
SELECT id, title, 'default' as template_type FROM templates WHERE id = $1 AND is_active = true
|
||||||
|
UNION
|
||||||
|
SELECT id, title, 'custom' as template_type FROM custom_templates WHERE id = $1
|
||||||
|
`, [featureData.template_id]);
|
||||||
|
|
||||||
|
if (templateCheck.rows.length === 0) {
|
||||||
|
console.error('❌ Template not found in either table:', featureData.template_id);
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Template not found',
|
||||||
|
message: `Template with ID ${featureData.template_id} does not exist in templates or custom_templates`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateType = templateCheck.rows[0].template_type;
|
||||||
|
|
||||||
|
// Allow admin-approved features to be created in template_features table regardless of template type
|
||||||
|
// Only redirect regular user-created features for custom templates
|
||||||
|
const isAdminApproval = featureData.feature_type === 'essential' && featureData.created_by_user !== true;
|
||||||
|
|
||||||
|
if (templateType === 'custom' && !isAdminApproval) {
|
||||||
|
console.log('🔄 Redirecting to custom features endpoint for custom template');
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid template type',
|
||||||
|
message: 'Features for custom templates should be created using the /api/features/custom endpoint'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const feature = await Feature.create({
|
const feature = await Feature.create({
|
||||||
template_id: featureData.template_id,
|
template_id: featureData.template_id,
|
||||||
feature_id: featureData.id,
|
feature_id: featureData.id,
|
||||||
name: featureData.name,
|
name: featureData.name,
|
||||||
description: featureData.description,
|
description: featureData.description,
|
||||||
feature_type: featureData.feature_type || 'suggested',
|
feature_type: featureData.feature_type || 'essential',
|
||||||
complexity: featureData.complexity,
|
complexity: featureData.complexity,
|
||||||
display_order: featureData.display_order || 999,
|
display_order: featureData.display_order || 999,
|
||||||
is_default: featureData.is_default || false,
|
is_default: featureData.is_default || false,
|
||||||
|
|||||||
@ -477,7 +477,16 @@ router.get('/:id([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})',
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const template = await Template.getByIdWithFeatures(id);
|
// First try to find in default templates
|
||||||
|
let template = await Template.getByIdWithFeatures(id);
|
||||||
|
let templateType = 'default';
|
||||||
|
|
||||||
|
// If not found in default templates, try custom templates
|
||||||
|
if (!template) {
|
||||||
|
const CustomTemplate = require('../models/custom_template');
|
||||||
|
template = await CustomTemplate.getByIdWithFeatures(id);
|
||||||
|
templateType = 'custom';
|
||||||
|
}
|
||||||
|
|
||||||
if (!template) {
|
if (!template) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
@ -487,9 +496,16 @@ router.get('/:id([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})',
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add template type information to response
|
||||||
|
const responseData = {
|
||||||
|
...template,
|
||||||
|
template_type: templateType,
|
||||||
|
is_custom: templateType === 'custom'
|
||||||
|
};
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: template,
|
data: responseData,
|
||||||
message: `Template ${template.title} retrieved successfully`
|
message: `Template ${template.title} retrieved successfully`
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -570,7 +586,8 @@ router.get('/:id([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/f
|
|||||||
`;
|
`;
|
||||||
const defaultFeaturesResult = await database.query(defaultFeaturesQuery, [id]);
|
const defaultFeaturesResult = await database.query(defaultFeaturesQuery, [id]);
|
||||||
const defaultFeatures = defaultFeaturesResult.rows;
|
const defaultFeatures = defaultFeaturesResult.rows;
|
||||||
console.log(`📊 Found ${defaultFeatures.length} default/suggested features`);
|
console.log(`📊 Found ${defaultFeatures.length} template features (all types)`);
|
||||||
|
console.log(`📋 Template features for ${id}:`, defaultFeatures.map(f => ({ name: f.name, type: f.feature_type, id: f.id })));
|
||||||
|
|
||||||
// Get custom features from custom_features table with business rules (if table exists)
|
// Get custom features from custom_features table with business rules (if table exists)
|
||||||
// Some environments may not have run the feature_business_rules migration yet. Probe first.
|
// Some environments may not have run the feature_business_rules migration yet. Probe first.
|
||||||
|
|||||||
@ -65,7 +65,7 @@ router.get('/verify-email', async (req, res) => {
|
|||||||
const { token } = req.query;
|
const { token } = req.query;
|
||||||
await authService.verifyEmailToken(token);
|
await authService.verifyEmailToken(token);
|
||||||
|
|
||||||
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3001';
|
const frontendUrl = process.env.FRONTEND_URL || 'http://192.168.1.20:3001';
|
||||||
const redirectUrl = `${frontendUrl}/signin?verified=true`;
|
const redirectUrl = `${frontendUrl}/signin?verified=true`;
|
||||||
// Prefer redirect by default; only return JSON if explicitly requested
|
// Prefer redirect by default; only return JSON if explicitly requested
|
||||||
if (req.query.format === 'json') {
|
if (req.query.format === 'json') {
|
||||||
@ -73,7 +73,7 @@ router.get('/verify-email', async (req, res) => {
|
|||||||
}
|
}
|
||||||
return res.redirect(302, redirectUrl);
|
return res.redirect(302, redirectUrl);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3001';
|
const frontendUrl = process.env.FRONTEND_URL || 'http://192.168.1.20:3001';
|
||||||
const redirectUrl = `${frontendUrl}/signin?error=${encodeURIComponent(error.message)}`;
|
const redirectUrl = `${frontendUrl}/signin?error=${encodeURIComponent(error.message)}`;
|
||||||
if (req.query.format === 'json') {
|
if (req.query.format === 'json') {
|
||||||
return res.status(400).json({ success: false, message: error.message, redirect: redirectUrl });
|
return res.status(400).json({ success: false, message: error.message, redirect: redirectUrl });
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user