Initial commit - backend code

This commit is contained in:
Chandini 2025-08-28 11:14:04 +05:30
parent 80087e13ec
commit 229e0fee4e
40 changed files with 2878 additions and 278 deletions

View File

@ -1,66 +0,0 @@
# Database Configuration
POSTGRES_HOST=localhost
POSTGRES_PORT=5433
POSTGRES_DB=dev_pipeline
POSTGRES_USER=pipeline_admin
POSTGRES_PASSWORD=secure_pipeline_2024
# Redis Configuration
REDIS_PASSWORD=redis_secure_2024
# MongoDB Configuration
MONGO_INITDB_ROOT_USERNAME=pipeline_admin
MONGO_INITDB_ROOT_PASSWORD=mongo_secure_2024
# RabbitMQ Configuration
RABBITMQ_DEFAULT_USER=pipeline_admin
RABBITMQ_DEFAULT_PASS=rabbit_secure_2024
# n8n Configuration
N8N_BASIC_AUTH_USER=admin
N8N_BASIC_AUTH_PASSWORD=admin_n8n_2024
N8N_ENCRYPTION_KEY=very_secure_encryption_key_2024
# Jenkins Configuration
JENKINS_ADMIN_ID=admin
JENKINS_ADMIN_PASSWORD=jenkins_secure_2024
# Gitea Configuration
GITEA_ADMIN_USER=admin
GITEA_ADMIN_PASSWORD=gitea_secure_2024
# API Keys (add your actual keys later)
CLAUDE_API_KEY=your_claude_api_key_here
OPENAI_API_KEY=your_openai_api_key_here
CLOUDTOPIAA_API_KEY=your_cloudtopiaa_api_key_here
CLOUDTOPIAA_API_URL=https://api.cloudtopiaa.com
# JWT Configuration
JWT_SECRET=ultra_secure_jwt_secret_2024
# Environment
ENVIRONMENT=development
NODE_ENV=development
PYTHONPATH=/app/src
# Monitoring
GRAFANA_ADMIN_USER=admin
GRAFANA_ADMIN_PASSWORD=grafana_secure_2024
RABBITMQ_PASSWORD=rabbit_secure_2024
MONGODB_PASSWORD=pipeline_password
MONGODB_PASSWORD=pipeline_password
CLAUDE_API_KEY=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
CLAUDE_API_KEY=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
# SMTP Configuration (Option 1)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=frontendtechbiz@gmail.com
SMTP_PASS=oidhhjeasgzbqptq
SMTP_FROM=frontendtechbiz@gmail.com
# Gmail Configuration (Option 2 - Alternative to SMTP)
GMAIL_USER=frontendtechbiz@gmail.com
GMAIL_APP_PASSWORD=oidhhjeasgzbqptq

View File

@ -1,27 +1,27 @@
# services:
# # =====================================
# # Core Infrastructure Services
# # =====================================
services:
# =====================================
# Core Infrastructure Services
# =====================================
# postgres:
# image: postgres:15
# container_name: pipeline_postgres
# environment:
# POSTGRES_USER: pipeline_admin
# POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
# POSTGRES_DB: dev_pipeline
# volumes:
# - postgres_data:/var/lib/postgresql/data
# ports:
# - "5432:5432"
# networks:
# - pipeline_network
# healthcheck:
# test: ["CMD-SHELL", "pg_isready -U pipeline_admin -d dev_pipeline"]
# interval: 30s
# timeout: 10s
# retries: 5
# start_period: 60s
postgres:
image: postgres:15
container_name: pipeline_postgres
environment:
POSTGRES_USER: pipeline_admin
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: dev_pipeline
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
networks:
- pipeline_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U pipeline_admin -d dev_pipeline"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
# redis:
# image: redis:7-alpine
@ -366,32 +366,6 @@
# networks:
# - pipeline_network
services:
# =====================================
# Core Infrastructure Services
# =====================================
postgres:
image: postgres:15
container_name: pipeline_postgres
environment:
POSTGRES_USER: pipeline_admin
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: dev_pipeline
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5433:5432"
networks:
- pipeline_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U pipeline_admin -d dev_pipeline"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
redis:
image: redis:7-alpine
container_name: pipeline_redis
@ -794,7 +768,7 @@ services:
- JWT_REFRESH_SECRET=refresh-secret-key-2024-tech4biz-${POSTGRES_PASSWORD}
- JWT_ACCESS_EXPIRY=15m
- JWT_REFRESH_EXPIRY=7d
- FRONTEND_URL=http://localhost:3001
- FRONTEND_URL=http://localhost:3000
# Email Configuration
- SMTP_HOST=${SMTP_HOST:-smtp.gmail.com}
- SMTP_PORT=${SMTP_PORT:-587}
@ -805,6 +779,7 @@ services:
- GMAIL_USER=${GMAIL_USER:-frontendtechbiz@gmail.com}
- GMAIL_APP_PASSWORD=${GMAIL_APP_PASSWORD:-oidhhjeasgzbqptq}
- AUTH_PUBLIC_URL=http://localhost:8011
- TEMPLATE_MANAGER_URL=http://template-manager:8009
networks:
- pipeline_network
depends_on:
@ -842,6 +817,7 @@ services:
- REDIS_PORT=6379
- REDIS_PASSWORD=${REDIS_PASSWORD}
- NODE_ENV=development
- JWT_ACCESS_SECRET=access-secret-key-2024-tech4biz-secure_pipeline_2024
networks:
- pipeline_network
depends_on:

View File

@ -0,0 +1,270 @@
# Custom Templates Feature
This document explains how the Custom Templates feature works in the Template Manager service, following the same pattern as Custom Features.
## Overview
The Custom Templates feature allows users to submit custom templates that go through an admin approval workflow before becoming available in the system. This follows the exact same pattern as the existing Custom Features implementation.
## Architecture
### Database Tables
1. **`custom_templates`** - Stores custom template submissions with admin approval workflow
2. **`templates`** - Mirrors approved custom templates (with `type = 'custom_<id>'`)
### Models
- **`CustomTemplate`** - Handles custom template CRUD operations and admin workflow
- **`Template`** - Standard template model (mirrors approved custom templates)
### Routes
- **`/api/custom-templates`** - Public endpoints for creating/managing custom templates
- **`/api/admin/templates/*`** - Admin endpoints for reviewing custom templates
## How It Works
### 1. Template Submission
```
User submits custom template → CustomTemplate.create() → Admin notification → Mirror to templates table
```
### 2. Admin Review Process
```
Admin reviews → Updates status → If approved: activates mirrored template → If rejected: keeps inactive
```
### 3. Template Mirroring
- Custom templates are mirrored into the `templates` table with `type = 'custom_<id>'`
- This allows them to be used by existing template endpoints
- The mirrored template starts with `is_active = false` until approved
## API Endpoints
### Public Custom Template Endpoints
#### POST `/api/custom-templates`
Create a new custom template.
**Required fields:**
- `type` - Template type identifier
- `title` - Template title
- `category` - Template category
- `complexity` - 'low', 'medium', or 'high'
**Optional fields:**
- `description` - Template description
- `icon` - Icon identifier
- `gradient` - CSS gradient
- `border` - Border styling
- `text` - Primary text
- `subtext` - Secondary text
- `business_rules` - JSON business rules
- `technical_requirements` - JSON technical requirements
- `created_by_user_session` - User session identifier
**Response:**
```json
{
"success": true,
"data": {
"id": "uuid",
"type": "custom_type",
"title": "Custom Template",
"status": "pending",
"approved": false
},
"message": "Custom template 'Custom Template' created successfully and submitted for admin review"
}
```
#### GET `/api/custom-templates`
Get all custom templates with pagination.
**Query parameters:**
- `limit` - Number of templates to return (default: 100)
- `offset` - Number of templates to skip (default: 0)
#### GET `/api/custom-templates/search`
Search custom templates by title, description, or category.
**Query parameters:**
- `q` - Search term (required)
- `limit` - Maximum results (default: 20)
#### GET `/api/custom-templates/:id`
Get a specific custom template by ID.
#### PUT `/api/custom-templates/:id`
Update a custom template.
#### DELETE `/api/custom-templates/:id`
Delete a custom template.
#### GET `/api/custom-templates/status/:status`
Get custom templates by status.
**Valid statuses:** `pending`, `approved`, `rejected`, `duplicate`
#### GET `/api/custom-templates/stats`
Get custom template statistics.
### Admin Endpoints
#### GET `/api/admin/templates/pending`
Get pending templates for admin review.
#### GET `/api/admin/templates/status/:status`
Get templates by status (admin view).
#### POST `/api/admin/templates/:id/review`
Review a custom template.
**Request body:**
```json
{
"status": "approved|rejected|duplicate",
"admin_notes": "Optional admin notes",
"canonical_template_id": "UUID of similar template (if duplicate)"
}
```
#### GET `/api/admin/templates/stats`
Get custom template statistics for admin dashboard.
### Template Merging Endpoints
#### GET `/api/templates/merged`
Get all templates (default + approved custom) grouped by category.
This endpoint merges default templates with approved custom templates, providing a unified view.
## Database Schema
### `custom_templates` Table
```sql
CREATE TABLE custom_templates (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
type VARCHAR(100) NOT NULL,
title VARCHAR(200) NOT NULL,
description TEXT,
icon VARCHAR(50),
category VARCHAR(100) NOT NULL,
gradient VARCHAR(100),
border VARCHAR(100),
text VARCHAR(100),
subtext VARCHAR(100),
complexity VARCHAR(50) NOT NULL CHECK (complexity IN ('low', 'medium', 'high')),
business_rules JSONB,
technical_requirements JSONB,
approved BOOLEAN DEFAULT false,
usage_count INTEGER DEFAULT 1,
created_by_user_session VARCHAR(100),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
-- Admin approval workflow fields
status VARCHAR(50) DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected', 'duplicate')),
admin_notes TEXT,
admin_reviewed_at TIMESTAMP,
admin_reviewed_by VARCHAR(100),
canonical_template_id UUID REFERENCES templates(id) ON DELETE SET NULL,
similarity_score FLOAT CHECK (similarity_score >= 0 AND similarity_score <= 1)
);
```
## Admin Workflow
### 1. Template Submission
1. User creates custom template via `/api/custom-templates`
2. Template is saved with `status = 'pending'`
3. Admin notification is created
4. Template is mirrored to `templates` table with `is_active = false`
### 2. Admin Review
1. Admin views pending templates via `/api/admin/templates/pending`
2. Admin reviews template and sets status:
- **Approved**: Template becomes active, mirrored template is activated
- **Rejected**: Template remains inactive
- **Duplicate**: Template marked as duplicate with reference to canonical template
### 3. Template Activation
- Approved templates have their mirrored version activated (`is_active = true`)
- Rejected/duplicate templates remain inactive
- All templates are accessible via the merged endpoints
## Usage Examples
### Creating a Custom Template
```javascript
const response = await fetch('/api/custom-templates', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'ecommerce_custom',
title: 'Custom E-commerce Template',
description: 'A specialized e-commerce template for fashion retailers',
category: 'E-commerce',
complexity: 'medium',
business_rules: { payment_methods: ['stripe', 'paypal'] },
technical_requirements: { framework: 'react', backend: 'nodejs' }
})
});
```
### Admin Review
```javascript
const reviewResponse = await fetch('/api/admin/templates/uuid/review', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer admin-jwt-token'
},
body: JSON.stringify({
status: 'approved',
admin_notes: 'Great template design, approved for production use'
})
});
```
### Getting Merged Templates
```javascript
const mergedTemplates = await fetch('/api/templates/merged');
// Returns default + approved custom templates grouped by category
```
## Migration
To add custom templates support to an existing database:
1. Run the migration: `node src/migrations/migrate.js`
2. The migration will create the `custom_templates` table
3. Existing templates and features remain unchanged
4. New custom templates will be stored separately and mirrored
## Benefits
1. **Non-disruptive**: Existing templates and features remain unchanged
2. **Consistent Pattern**: Follows the same workflow as custom features
3. **Admin Control**: All custom templates go through approval process
4. **Unified Access**: Approved custom templates are accessible via existing endpoints
5. **Audit Trail**: Full tracking of submission, review, and approval process
## Security Considerations
1. **Admin Authentication**: All admin endpoints require JWT with admin role
2. **Input Validation**: All user inputs are validated and sanitized
3. **Status Checks**: Only approved templates become active
4. **Session Tracking**: User sessions are tracked for audit purposes
## Future Enhancements
1. **Template Similarity Detection**: Automatic duplicate detection
2. **Bulk Operations**: Approve/reject multiple templates at once
3. **Template Versioning**: Track changes and versions
4. **Template Analytics**: Usage statistics and performance metrics
5. **Template Categories**: Dynamic category management

View File

@ -2,6 +2,9 @@ FROM node:18-alpine
WORKDIR /app
# Install curl for health checks
RUN apk add --no-cache curl
# Copy package files
COPY package*.json ./

View File

@ -0,0 +1,121 @@
require('dotenv').config();
const database = require('./src/config/database');
const SAMPLE_TEMPLATES = [
{
type: 'blog_platform',
title: 'Blog Platform',
description: 'Modern blog with content management, comments, and SEO',
icon: '📝',
category: 'Content',
gradient: 'from-purple-50 to-purple-100',
border: 'border-purple-200',
text: 'text-purple-900',
subtext: 'text-purple-700'
},
{
type: 'task_manager',
title: 'Task Manager',
description: 'Project and task management with team collaboration',
icon: '✅',
category: 'Productivity',
gradient: 'from-green-50 to-green-100',
border: 'border-green-200',
text: 'text-green-900',
subtext: 'text-green-700'
},
{
type: 'analytics_dashboard',
title: 'Analytics Dashboard',
description: 'Data visualization and business intelligence platform',
icon: '📊',
category: 'Business',
gradient: 'from-blue-50 to-blue-100',
border: 'border-blue-200',
text: 'text-blue-900',
subtext: 'text-blue-700'
},
{
type: 'social_network',
title: 'Social Network',
description: 'Connect with friends, share content, and build communities',
icon: '🌐',
category: 'Social',
gradient: 'from-pink-50 to-pink-100',
border: 'border-pink-200',
text: 'text-pink-900',
subtext: 'text-pink-700'
},
{
type: 'learning_platform',
title: 'Learning Platform',
description: 'Online courses, quizzes, and educational content',
icon: '🎓',
category: 'Education',
gradient: 'from-yellow-50 to-yellow-100',
border: 'border-yellow-200',
text: 'text-yellow-900',
subtext: 'text-yellow-700'
}
];
async function addSampleTemplates() {
const client = await database.connect();
try {
await client.query('BEGIN');
console.log('🚀 Adding sample templates...');
for (const template of SAMPLE_TEMPLATES) {
const query = `
INSERT INTO templates (
id, type, title, description, icon, category,
gradient, border, text, subtext, is_active, created_at, updated_at
) VALUES (
gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, true, NOW(), NOW()
)
`;
const values = [
template.type,
template.title,
template.description,
template.icon,
template.category,
template.gradient,
template.border,
template.text,
template.subtext
];
await client.query(query, values);
console.log(`✅ Added template: ${template.title}`);
}
await client.query('COMMIT');
console.log('🎉 Sample templates added successfully!');
} catch (error) {
await client.query('ROLLBACK');
console.error('❌ Error adding sample templates:', error.message);
throw error;
} finally {
client.release();
}
}
// Run if called directly
if (require.main === module) {
addSampleTemplates()
.then(() => {
console.log('🎉 Process completed!');
process.exit(0);
})
.catch((error) => {
console.error('💥 Process failed:', error.message);
process.exit(1);
});
}
module.exports = { addSampleTemplates };

View File

@ -17,6 +17,7 @@
"morgan": "^1.10.0",
"pg": "^8.8.0",
"redis": "^4.6.0",
"socket.io": "^4.8.1",
"uuid": "^9.0.0"
},
"devDependencies": {
@ -121,6 +122,30 @@
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
"license": "BSD-3-Clause"
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"license": "MIT"
},
"node_modules/@types/cors": {
"version": "2.8.19",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "24.3.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
"integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.10.0"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@ -161,6 +186,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
"license": "MIT",
"engines": {
"node": "^4.5.0 || >= 5.9"
}
},
"node_modules/basic-auth": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
@ -452,6 +486,67 @@
"node": ">= 0.8"
}
},
"node_modules/engine.io": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
"integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
"license": "MIT",
"dependencies": {
"@types/cors": "^2.8.12",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.7.2",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1"
},
"engines": {
"node": ">=10.2.0"
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/engine.io/node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/engine.io/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/engine.io/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@ -1601,6 +1696,116 @@
"semver": "bin/semver.js"
}
},
"node_modules/socket.io": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
"integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"cors": "~2.8.5",
"debug": "~4.3.2",
"engine.io": "~6.6.0",
"socket.io-adapter": "~2.5.2",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.2.0"
}
},
"node_modules/socket.io-adapter": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
"integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
"license": "MIT",
"dependencies": {
"debug": "~4.3.4",
"ws": "~8.17.1"
}
},
"node_modules/socket.io-adapter/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-adapter/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-parser/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/socket.io/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
@ -1684,6 +1889,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/undici-types": {
"version": "7.10.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
"license": "MIT"
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@ -1724,6 +1935,27 @@
"node": ">= 0.8"
}
},
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@ -19,6 +19,7 @@
"morgan": "^1.10.0",
"pg": "^8.8.0",
"redis": "^4.6.0",
"socket.io": "^4.8.1",
"uuid": "^9.0.0"
},
"devDependencies": {

View File

@ -0,0 +1,41 @@
const fs = require('fs');
const path = require('path');
const database = require('./src/config/database');
async function runMigration() {
try {
console.log('🚀 Starting database migration...');
// Read the migration file
const migrationPath = path.join(__dirname, 'src/migrations/001_initial_schema.sql');
const migrationSQL = fs.readFileSync(migrationPath, 'utf8');
console.log('📄 Migration file loaded successfully');
// Execute the migration
const result = await database.query(migrationSQL);
console.log('✅ Migration completed successfully!');
console.log('📊 Migration result:', result.rows);
// Verify tables were created
const tablesQuery = `
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name IN ('templates', 'template_features', 'custom_features', 'feature_usage')
ORDER BY table_name;
`;
const tablesResult = await database.query(tablesQuery);
console.log('📋 Created tables:', tablesResult.rows.map(row => row.table_name));
process.exit(0);
} catch (error) {
console.error('❌ Migration failed:', error.message);
console.error('📚 Error details:', error);
process.exit(1);
}
}
runMigration();

View File

@ -3,6 +3,8 @@ const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const http = require('http');
const { Server } = require('socket.io');
// Import database
const database = require('./config/database');
@ -12,8 +14,18 @@ const templateRoutes = require('./routes/templates');
const featureRoutes = require('./routes/features');
const learningRoutes = require('./routes/learning');
const adminRoutes = require('./routes/admin');
const AdminNotification = require('./models/admin_notification');
// const customTemplateRoutes = require('./routes/custom_templates');
const app = express();
const server = http.createServer(app);
const io = new Server(server, {
cors: {
origin: process.env.FRONTEND_URL || "http://localhost:3000",
methods: ["GET", "POST"],
credentials: true
}
});
const PORT = process.env.PORT || 8009;
// Middleware
@ -23,11 +35,41 @@ app.use(morgan('combined'));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// Routes
app.use('/api/templates', templateRoutes);
app.use('/api/features', featureRoutes);
// Make io available to routes and set it in AdminNotification
app.set('io', io);
AdminNotification.setSocketIO(io);
// Routes - Order matters! More specific routes should come first
app.use('/api/learning', learningRoutes);
app.use('/api/admin', adminRoutes);
app.use('/api/templates', templateRoutes);
// Add admin routes under /api/templates to match serviceClient expectations
app.use('/api/templates/admin', adminRoutes);
// Features route must come AFTER templates to avoid route conflicts
app.use('/api/features', featureRoutes);
// Single route surface: handle custom templates via /api/templates only
// app.use('/api/custom-templates', customTemplateRoutes);
// WebSocket connection handling
io.on('connection', async (socket) => {
console.log('🔌 Admin client connected:', socket.id);
// Join admin room for notifications
socket.join('admin-notifications');
// Send initial notification count
try {
const counts = await AdminNotification.getCounts();
socket.emit('notification-count', counts);
} catch (error) {
console.error('Error getting notification counts:', error);
socket.emit('notification-count', { total: 0, unread: 0, read: 0 });
}
socket.on('disconnect', () => {
console.log('🔌 Admin client disconnected:', socket.id);
});
});
// Health check endpoint
app.get('/health', (req, res) => {
@ -57,7 +99,8 @@ app.get('/', (req, res) => {
templates: '/api/templates',
features: '/api/features',
learning: '/api/learning',
admin: '/api/admin'
admin: '/api/admin',
customTemplates: '/api/custom-templates'
}
});
});
@ -87,10 +130,11 @@ process.on('SIGINT', async () => {
});
// Start server
app.listen(PORT, '0.0.0.0', () => {
server.listen(PORT, '0.0.0.0', () => {
console.log('🚀 Template Manager Service started');
console.log(`📡 Server running on http://0.0.0.0:${PORT}`);
console.log(`🏥 Health check: http://0.0.0.0:${PORT}/health`);
console.log('🔌 WebSocket server ready for real-time notifications');
console.log('🎯 Self-learning feature database ready!');
});

View File

@ -4,7 +4,7 @@ class Database {
constructor() {
this.pool = new Pool({
host: process.env.POSTGRES_HOST || 'localhost',
port: process.env.POSTGRES_PORT || 5433,
port: process.env.POSTGRES_PORT || 5432,
database: process.env.POSTGRES_DB || 'dev_pipeline',
user: process.env.POSTGRES_USER || 'pipeline_admin',
password: process.env.POSTGRES_PASSWORD || 'secure_pipeline_2024',
@ -45,6 +45,10 @@ class Database {
return await this.pool.connect();
}
async connect() {
return await this.pool.connect();
}
async close() {
await this.pool.end();
console.log('🔌 Database connection closed');

View File

@ -7,8 +7,17 @@ DROP TABLE IF EXISTS custom_features CASCADE;
DROP TABLE IF EXISTS template_features CASCADE;
DROP TABLE IF EXISTS templates CASCADE;
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Enable UUID extension (only if we have permission)
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'uuid-ossp') THEN
BEGIN
CREATE EXTENSION "uuid-ossp";
EXCEPTION WHEN insufficient_privilege THEN
RAISE NOTICE 'uuid-ossp extension creation failed due to insufficient privileges. Using alternative UUID generation.';
END;
END IF;
END $$;
-- Templates table
CREATE TABLE templates (

View File

@ -1,31 +1,66 @@
-- Migration: Add Admin Approval for Custom Features
-- This migration adds admin approval workflow functionality to the existing template manager
-- 1. Add status and admin fields to custom_features
-- First add the columns as nullable
ALTER TABLE custom_features
ADD COLUMN status VARCHAR(20)
CHECK (status IN ('pending', 'approved', 'rejected', 'duplicate')),
ADD COLUMN admin_notes TEXT,
ADD COLUMN admin_reviewed_at TIMESTAMP,
ADD COLUMN admin_reviewed_by VARCHAR(100),
ADD COLUMN canonical_feature_id UUID REFERENCES template_features(id) ON DELETE SET NULL,
ADD COLUMN similarity_score FLOAT;
-- 1. Add status and admin fields to custom_features (only if they don't exist)
DO $$
BEGIN
-- Add status column if it doesn't exist
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'custom_features' AND column_name = 'status') THEN
ALTER TABLE custom_features ADD COLUMN status VARCHAR(20) CHECK (status IN ('pending', 'approved', 'rejected', 'duplicate'));
END IF;
-- Add admin_notes column if it doesn't exist
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'custom_features' AND column_name = 'admin_notes') THEN
ALTER TABLE custom_features ADD COLUMN admin_notes TEXT;
END IF;
-- Add admin_reviewed_at column if it doesn't exist
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'custom_features' AND column_name = 'admin_reviewed_at') THEN
ALTER TABLE custom_features ADD COLUMN admin_reviewed_at TIMESTAMP;
END IF;
-- Add admin_reviewed_by column if it doesn't exist
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'custom_features' AND column_name = 'admin_reviewed_by') THEN
ALTER TABLE custom_features ADD COLUMN admin_reviewed_by VARCHAR(100);
END IF;
-- Add canonical_feature_id column if it doesn't exist
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'custom_features' AND column_name = 'canonical_feature_id') THEN
ALTER TABLE custom_features ADD COLUMN canonical_feature_id UUID REFERENCES template_features(id) ON DELETE SET NULL;
END IF;
-- Add similarity_score column if it doesn't exist
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'custom_features' AND column_name = 'similarity_score') THEN
ALTER TABLE custom_features ADD COLUMN similarity_score FLOAT;
END IF;
END $$;
-- Set default values for existing rows
UPDATE custom_features
SET status = CASE
WHEN approved = true THEN 'approved'
ELSE 'pending'
END;
-- Set default values for existing rows (only if status column exists and has data)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'custom_features' AND column_name = 'status') THEN
UPDATE custom_features
SET status = CASE
WHEN approved = true THEN 'approved'
ELSE 'pending'
END
WHERE status IS NULL;
-- Now alter the column to be NOT NULL (only if it's not already NOT NULL)
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'custom_features' AND column_name = 'status' AND is_nullable = 'YES') THEN
ALTER TABLE custom_features ALTER COLUMN status SET NOT NULL;
END IF;
-- Set default value (only if it doesn't already have one)
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'custom_features' AND column_name = 'status' AND column_default IS NOT NULL) THEN
ALTER TABLE custom_features ALTER COLUMN status SET DEFAULT 'pending';
END IF;
END IF;
END $$;
-- Now alter the column to be NOT NULL
ALTER TABLE custom_features ALTER COLUMN status SET NOT NULL;
ALTER TABLE custom_features ALTER COLUMN status SET DEFAULT 'pending';
-- 2. Create a table for feature synonyms/aliases
CREATE TABLE feature_synonyms (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- 2. Create a table for feature synonyms/aliases (only if it doesn't exist)
CREATE TABLE IF NOT EXISTS feature_synonyms (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
feature_id UUID NOT NULL REFERENCES template_features(id) ON DELETE CASCADE,
synonym VARCHAR(200) NOT NULL,
created_by VARCHAR(100),
@ -33,14 +68,14 @@ CREATE TABLE feature_synonyms (
UNIQUE(synonym)
);
-- 3. Add index for faster lookups
CREATE INDEX idx_custom_features_status ON custom_features(status);
CREATE INDEX idx_custom_features_created_at ON custom_features(created_at DESC);
CREATE INDEX idx_feature_synonyms_synonym ON feature_synonyms(synonym);
-- 3. Add index for faster lookups (only if they don't exist)
CREATE INDEX IF NOT EXISTS idx_custom_features_status ON custom_features(status);
CREATE INDEX IF NOT EXISTS idx_custom_features_created_at ON custom_features(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_feature_synonyms_synonym ON feature_synonyms(synonym);
-- 4. Admin notifications table
CREATE TABLE admin_notifications (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- 4. Admin notifications table (only if it doesn't exist)
CREATE TABLE IF NOT EXISTS admin_notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
type VARCHAR(50) NOT NULL,
message TEXT NOT NULL,
reference_id UUID,
@ -50,25 +85,31 @@ CREATE TABLE admin_notifications (
read_at TIMESTAMP
);
-- 5. Create indexes for admin notifications
CREATE INDEX idx_admin_notifications_type ON admin_notifications(type);
CREATE INDEX idx_admin_notifications_is_read ON admin_notifications(is_read);
CREATE INDEX idx_admin_notifications_created_at ON admin_notifications(created_at DESC);
-- 5. Create indexes for admin notifications (only if they don't exist)
CREATE INDEX IF NOT EXISTS idx_admin_notifications_type ON admin_notifications(type);
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);
-- 6. Update existing custom_features to have 'approved' status if they were previously approved
UPDATE custom_features
SET status = CASE
WHEN approved = true THEN 'approved'
ELSE 'pending'
END,
admin_reviewed_at = CASE
WHEN approved = true THEN created_at
ELSE NULL
END,
admin_reviewed_by = CASE
WHEN approved = true THEN 'system_migration'
ELSE NULL
END;
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'custom_features' AND column_name = 'status') THEN
UPDATE custom_features
SET status = CASE
WHEN approved = true THEN 'approved'
ELSE 'pending'
END,
admin_reviewed_at = CASE
WHEN approved = true THEN created_at
ELSE NULL
END,
admin_reviewed_by = CASE
WHEN approved = true THEN 'system_migration'
ELSE NULL
END
WHERE status IS NULL OR admin_reviewed_at IS NULL;
END IF;
END $$;
-- 7. Insert success message
INSERT INTO templates (type, title, description, category)

View File

@ -0,0 +1,53 @@
-- Migration: Add custom_templates table
-- This follows the same pattern as custom_features but for templates
-- Note: Using gen_random_uuid() which is available by default in PostgreSQL
-- Create custom_templates table (only if it doesn't exist)
CREATE TABLE IF NOT EXISTS custom_templates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
type VARCHAR(100) NOT NULL,
title VARCHAR(200) NOT NULL,
description TEXT,
icon VARCHAR(50),
category VARCHAR(100) NOT NULL,
gradient VARCHAR(100),
border VARCHAR(100),
text VARCHAR(100),
subtext VARCHAR(100),
complexity VARCHAR(50) NOT NULL CHECK (complexity IN ('low', 'medium', 'high')),
business_rules JSONB,
technical_requirements JSONB,
approved BOOLEAN DEFAULT false,
usage_count INTEGER DEFAULT 1,
created_by_user_session VARCHAR(100),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
-- Admin approval workflow fields
status VARCHAR(50) DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected', 'duplicate')),
admin_notes TEXT,
admin_reviewed_at TIMESTAMP,
admin_reviewed_by VARCHAR(100),
canonical_template_id UUID REFERENCES templates(id) ON DELETE SET NULL,
similarity_score FLOAT CHECK (similarity_score >= 0 AND similarity_score <= 1)
);
-- Create indexes for performance (only if they don't exist)
CREATE INDEX IF NOT EXISTS idx_custom_templates_type ON custom_templates(type);
CREATE INDEX IF NOT EXISTS idx_custom_templates_category ON custom_templates(category);
CREATE INDEX IF NOT EXISTS idx_custom_templates_status ON custom_templates(status);
CREATE INDEX IF NOT EXISTS idx_custom_templates_approved ON custom_templates(approved);
CREATE INDEX IF NOT EXISTS idx_custom_templates_usage_count ON custom_templates(usage_count DESC);
CREATE INDEX IF NOT EXISTS idx_custom_templates_created_at ON custom_templates(created_at DESC);
-- Apply update trigger (only if it doesn't exist)
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'update_custom_templates_updated_at') THEN
CREATE TRIGGER update_custom_templates_updated_at BEFORE UPDATE ON custom_templates
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
END IF;
END $$;
-- Insert success message
SELECT 'Custom templates table created successfully!' as message;

View File

@ -0,0 +1,23 @@
-- Migration: Add is_custom flag to custom_templates
-- Add column if it doesn't exist
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'custom_templates'
AND column_name = 'is_custom'
) THEN
ALTER TABLE custom_templates
ADD COLUMN is_custom BOOLEAN NOT NULL DEFAULT false;
END IF;
END $$;
-- Backfill: ensure all existing rows default to false
UPDATE custom_templates SET is_custom = COALESCE(is_custom, false);
-- Success message
SELECT 'is_custom flag added to custom_templates' as message;

View File

@ -0,0 +1,17 @@
-- Migration: Add user_id to custom_templates
-- Purpose: Track which authenticated user created a custom template
-- Add column if it does not exist
ALTER TABLE IF EXISTS custom_templates
ADD COLUMN IF NOT EXISTS user_id UUID NULL;
-- Optional: add an index for filtering by user
CREATE INDEX IF NOT EXISTS idx_custom_templates_user_id ON custom_templates(user_id);
-- Note: We are not adding a foreign key constraint because the users table
-- may be managed by a different service/schema. If later a shared users table
-- is available, a FK can be added safely.
SELECT 'user_id column added to custom_templates' AS message;

View File

@ -0,0 +1,55 @@
-- Migration: Fix custom_features foreign key constraint
-- Purpose: Allow custom_features to reference both templates and custom_templates tables
-- First, drop the existing foreign key constraint
ALTER TABLE IF EXISTS custom_features
DROP CONSTRAINT IF EXISTS custom_features_template_id_fkey;
-- Add a new column to track the template type
ALTER TABLE IF EXISTS custom_features
ADD COLUMN IF NOT EXISTS template_type VARCHAR(20) DEFAULT 'default' CHECK (template_type IN ('default', 'custom'));
-- Update existing records to have the correct template_type
UPDATE custom_features
SET template_type = CASE
WHEN EXISTS (SELECT 1 FROM templates WHERE id = template_id) THEN 'default'
WHEN EXISTS (SELECT 1 FROM custom_templates WHERE id = template_id) THEN 'custom'
ELSE 'default'
END
WHERE template_type IS NULL;
-- Create a function to validate template_id references
CREATE OR REPLACE FUNCTION validate_template_reference()
RETURNS TRIGGER AS $$
BEGIN
-- Check if template_id exists in either templates or custom_templates
IF NEW.template_type = 'default' THEN
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 templates table or is not active', NEW.template_id;
END IF;
ELSIF NEW.template_type = 'custom' 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;
END IF;
ELSE
RAISE EXCEPTION 'Invalid template_type: %. Must be either "default" or "custom"', NEW.template_type;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create trigger to validate template references
DROP TRIGGER IF EXISTS validate_template_reference_trigger ON custom_features;
CREATE TRIGGER validate_template_reference_trigger
BEFORE INSERT OR UPDATE ON custom_features
FOR EACH ROW
EXECUTE FUNCTION validate_template_reference();
-- Create index for template_type
CREATE INDEX IF NOT EXISTS idx_custom_features_template_type ON custom_features(template_type);
-- Update the custom feature creation logic to automatically set template_type
-- This will be handled in the application code, but we ensure the constraint is enforced
SELECT 'Custom features foreign key constraint fixed successfully!' as message;

View File

@ -10,7 +10,10 @@ async function runMigrations() {
// Get all migration files in order
const migrationFiles = [
'001_initial_schema.sql',
'002_admin_approval_workflow.sql'
'002_admin_approval_workflow.sql',
'003_custom_templates.sql',
'004_add_is_custom_flag.sql',
'004_add_user_id_to_custom_templates.sql'
];
for (const migrationFile of migrationFiles) {
@ -39,7 +42,7 @@ async function runMigrations() {
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name IN ('templates', 'template_features', 'feature_usage', 'custom_features', 'feature_synonyms', 'admin_notifications')
AND table_name IN ('templates', 'template_features', 'feature_usage', 'custom_features', 'custom_templates', 'feature_synonyms', 'admin_notifications')
ORDER BY table_name
`);

View File

@ -1,6 +1,14 @@
const database = require('../config/database');
const { v4: uuidv4 } = require('uuid');
// Global variable to store io instance
let io = null;
// Function to set the io instance
const setSocketIO = (socketIO) => {
io = socketIO;
};
class AdminNotification {
constructor(data = {}) {
this.id = data.id;
@ -30,7 +38,18 @@ class AdminNotification {
data.is_read || false
];
const result = await database.query(query, values);
return new AdminNotification(result.rows[0]);
const notification = new AdminNotification(result.rows[0]);
// Emit real-time notification via WebSocket
if (io) {
io.to('admin-notifications').emit('new-notification', notification);
// Also emit updated count
const counts = await AdminNotification.getCounts();
io.to('admin-notifications').emit('notification-count', counts);
}
return notification;
}
static async getUnread(limit = 50) {
@ -62,7 +81,16 @@ class AdminNotification {
RETURNING *
`;
const result = await database.query(query, [id]);
return result.rows.length ? new AdminNotification(result.rows[0]) : null;
const notification = result.rows.length ? new AdminNotification(result.rows[0]) : null;
// Emit updated count via WebSocket
if (io && notification) {
const counts = await AdminNotification.getCounts();
io.to('admin-notifications').emit('notification-count', counts);
io.to('admin-notifications').emit('notification-read', { id });
}
return notification;
}
static async markAllAsRead() {
@ -72,6 +100,14 @@ class AdminNotification {
WHERE is_read = false
`;
const result = await database.query(query);
// Emit updated count via WebSocket
if (io && result.rowCount > 0) {
const counts = await AdminNotification.getCounts();
io.to('admin-notifications').emit('notification-count', counts);
io.to('admin-notifications').emit('all-notifications-read');
}
return result.rowCount;
}
@ -114,6 +150,27 @@ class AdminNotification {
reference_type: 'custom_feature'
});
}
static async notifyNewTemplate(templateId, templateName) {
return await AdminNotification.create({
type: 'new_template',
message: `New custom template submitted: "${templateName}"`,
reference_id: templateId,
reference_type: 'custom_template'
});
}
static async notifyTemplateReviewed(templateId, templateName, status) {
return await AdminNotification.create({
type: 'template_reviewed',
message: `Template "${templateName}" has been ${status}`,
reference_id: templateId,
reference_type: 'custom_template'
});
}
}
// Export the setSocketIO function along with the class
AdminNotification.setSocketIO = setSocketIO;
module.exports = AdminNotification;

View File

@ -153,8 +153,64 @@ class CustomFeature {
return await CustomFeature.update(id, updates);
}
// Count features for a custom template
static async countByTemplateId(templateId) {
const query = `SELECT COUNT(*) as count FROM custom_features WHERE template_id = $1`;
const result = await database.query(query, [templateId]);
return parseInt(result.rows[0].count) || 0;
}
// Count features for multiple custom templates at once
static async countByTemplateIds(templateIds) {
if (!templateIds || templateIds.length === 0) return {};
const placeholders = templateIds.map((_, i) => `$${i + 1}`).join(',');
const query = `
SELECT template_id, COUNT(*) as count
FROM custom_features
WHERE template_id IN (${placeholders})
GROUP BY template_id
`;
const result = await database.query(query, templateIds);
const counts = {};
result.rows.forEach(row => {
counts[row.template_id] = parseInt(row.count) || 0;
});
return counts;
}
// Get statistics for admin dashboard
static async getStats() {
const query = `
SELECT
status,
COUNT(*) as count
FROM custom_features
GROUP BY status
`;
const result = await database.query(query);
return result.rows.map(row => ({
status: row.status,
count: parseInt(row.count) || 0
}));
}
// Get all custom features with pagination
static async getAllFeatures(limit = 50, offset = 0) {
const query = `
SELECT cf.*, t.title as template_title
FROM custom_features cf
LEFT JOIN templates t ON cf.template_id = t.id
ORDER BY cf.created_at DESC
LIMIT $1 OFFSET $2
`;
const result = await database.query(query, [limit, offset]);
return result.rows.map(r => new CustomFeature(r));
}
}
module.exports = CustomFeature;

View File

@ -0,0 +1,307 @@
const database = require('../config/database');
const { v4: uuidv4 } = require('uuid');
class CustomTemplate {
constructor(data = {}) {
this.id = data.id;
this.type = data.type;
this.title = data.title;
this.description = data.description;
this.icon = data.icon;
this.category = data.category;
this.gradient = data.gradient;
this.border = data.border;
this.text = data.text;
this.subtext = data.subtext;
this.complexity = data.complexity;
this.business_rules = data.business_rules;
this.technical_requirements = data.technical_requirements;
this.approved = data.approved;
this.usage_count = data.usage_count;
this.created_by_user_session = data.created_by_user_session;
this.created_at = data.created_at;
this.updated_at = data.updated_at;
this.is_custom = data.is_custom ?? false;
// Admin approval workflow fields
this.status = data.status || 'pending';
this.admin_notes = data.admin_notes;
this.admin_reviewed_at = data.admin_reviewed_at;
this.admin_reviewed_by = data.admin_reviewed_by;
this.canonical_template_id = data.canonical_template_id;
this.similarity_score = data.similarity_score;
this.user_id = data.user_id;
}
static async getById(id) {
const result = await database.query('SELECT * FROM custom_templates WHERE id = $1', [id]);
return result.rows.length ? new CustomTemplate(result.rows[0]) : null;
}
// Check for duplicate custom templates based on title, type, category, and user_id
static async checkForDuplicate(templateData) {
console.log('[CustomTemplate.checkForDuplicate] Checking for duplicates:', {
type: templateData.type,
title: templateData.title,
category: templateData.category,
user_id: templateData.user_id
});
// Check for exact type match (globally unique)
const typeQuery = `
SELECT id, title, type, category, user_id FROM custom_templates
WHERE type = $1
`;
const typeResult = await database.query(typeQuery, [templateData.type]);
if (typeResult.rows.length > 0) {
console.log('[CustomTemplate.checkForDuplicate] Found duplicate by type:', typeResult.rows[0]);
return typeResult.rows[0];
}
// Check for same title + category for same user
if (templateData.user_id) {
const titleQuery = `
SELECT id, title, type, category, user_id FROM custom_templates
WHERE LOWER(title) = LOWER($1) AND category = $2 AND user_id = $3
`;
const titleResult = await database.query(titleQuery, [
templateData.title,
templateData.category,
templateData.user_id
]);
if (titleResult.rows.length > 0) {
console.log('[CustomTemplate.checkForDuplicate] Found duplicate by title+category+user:', titleResult.rows[0]);
return titleResult.rows[0];
}
}
console.log('[CustomTemplate.checkForDuplicate] No duplicates found');
return null;
}
// Check if template type exists in main templates table
static async checkTypeInMainTemplates(type) {
const query = `
SELECT id, title, type FROM templates
WHERE type = $1 AND is_active = true
`;
const result = await database.query(query, [type]);
return result.rows.length > 0 ? result.rows[0] : null;
}
static async create(data) {
const id = uuidv4();
console.log('[CustomTemplate.create] start - id:', id);
const query = `
INSERT INTO custom_templates (
id, type, title, description, icon, category, gradient, border, text, subtext,
complexity, business_rules, technical_requirements, approved, usage_count,
created_by_user_session, status, admin_notes, admin_reviewed_at,
admin_reviewed_by, canonical_template_id, similarity_score, is_custom, user_id
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24)
RETURNING *
`;
const values = [
id,
data.type,
data.title,
data.description || null,
data.icon || null,
data.category,
data.gradient || null,
data.border || null,
data.text || null,
data.subtext || null,
data.complexity,
data.business_rules || null,
data.technical_requirements || null,
data.approved ?? false,
data.usage_count ?? 1,
data.created_by_user_session || null,
data.status || 'pending',
data.admin_notes || null,
data.admin_reviewed_at || null,
data.admin_reviewed_by || null,
data.canonical_template_id || null,
data.similarity_score || null,
data.is_custom ?? false,
data.user_id || null,
];
console.log('[CustomTemplate.create] values prepared (truncated):', {
id: values[0],
type: values[1],
title: values[2],
is_custom: values[22],
user_id: values[23]
});
const result = await database.query(query, values);
console.log('[CustomTemplate.create] insert done - row id:', result.rows[0]?.id, 'user_id:', result.rows[0]?.user_id);
return new CustomTemplate(result.rows[0]);
}
static async update(id, updates) {
const fields = [];
const values = [];
let idx = 1;
const allowed = [
'title', 'description', 'icon', 'category', 'gradient', 'border', 'text', 'subtext',
'complexity', 'business_rules', 'technical_requirements', 'approved', 'usage_count',
'status', 'admin_notes', 'admin_reviewed_at', 'admin_reviewed_by',
'canonical_template_id', 'similarity_score', 'user_id'
];
for (const k of allowed) {
if (updates[k] !== undefined) {
fields.push(`${k} = $${idx++}`);
values.push(updates[k]);
}
}
if (fields.length === 0) return await CustomTemplate.getById(id);
const query = `UPDATE custom_templates SET ${fields.join(', ')}, updated_at = NOW() WHERE id = $${idx} RETURNING *`;
values.push(id);
const result = await database.query(query, values);
return result.rows.length ? new CustomTemplate(result.rows[0]) : null;
}
static async delete(id) {
const result = await database.query('DELETE FROM custom_templates WHERE id = $1', [id]);
return result.rowCount > 0;
}
// Admin workflow methods
static async getPendingTemplates(limit = 50, offset = 0) {
const query = `
SELECT * FROM custom_templates
WHERE status = 'pending'
ORDER BY created_at ASC
LIMIT $1 OFFSET $2
`;
const result = await database.query(query, [limit, offset]);
return result.rows.map(r => new CustomTemplate(r));
}
static async getTemplatesByStatus(status, limit = 50, offset = 0) {
const query = `
SELECT * FROM custom_templates
WHERE status = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3
`;
const result = await database.query(query, [status, limit, offset]);
return result.rows.map(r => new CustomTemplate(r));
}
// Get custom templates created by a specific user session
static async getByCreatorSession(sessionKey, limit = 100, offset = 0, status = null) {
if (!sessionKey) return [];
let query = `
SELECT * FROM custom_templates
WHERE created_by_user_session = $1
`;
const values = [sessionKey];
if (status) {
query += ` AND status = $2`;
values.push(status);
}
query += ` ORDER BY created_at DESC LIMIT ${status ? '$3' : '$2'} OFFSET ${status ? '$4' : '$3'}`;
values.push(limit, offset);
const result = await database.query(query, values);
return result.rows.map(r => new CustomTemplate(r));
}
static async getTemplateStats() {
const query = `
SELECT
status,
COUNT(*) as count
FROM custom_templates
GROUP BY status
`;
const result = await database.query(query);
return result.rows;
}
// Get custom templates by authenticated user id
static async getByUserId(userId, limit = 100, offset = 0, status = null) {
if (!userId) return [];
let query = `
SELECT * FROM custom_templates
WHERE user_id = $1
`;
const values = [userId];
if (status) {
query += ` AND status = $2`;
values.push(status);
}
query += ` ORDER BY created_at DESC LIMIT ${status ? '$3' : '$2'} OFFSET ${status ? '$4' : '$3'}`;
values.push(limit, offset);
const result = await database.query(query, values);
return result.rows.map(r => new CustomTemplate(r));
}
static async reviewTemplate(id, reviewData) {
const { status, admin_notes, canonical_template_id, admin_reviewed_by } = reviewData;
const updates = {
status,
admin_notes,
admin_reviewed_at: new Date(),
admin_reviewed_by
};
if (canonical_template_id) {
updates.canonical_template_id = canonical_template_id;
}
return await CustomTemplate.update(id, updates);
}
// Get all custom templates
static async getAll(limit = 100, offset = 0) {
const query = `
SELECT * FROM custom_templates
ORDER BY created_at DESC
LIMIT $1 OFFSET $2
`;
const result = await database.query(query, [limit, offset]);
return result.rows.map(r => new CustomTemplate(r));
}
// Search custom templates
static async search(searchTerm, limit = 20) {
const query = `
SELECT * FROM custom_templates
WHERE (title ILIKE $1 OR description ILIKE $1 OR category ILIKE $1)
ORDER BY usage_count DESC, created_at DESC
LIMIT $2
`;
const result = await database.query(query, [`%${searchTerm}%`, limit]);
return result.rows.map(r => new CustomTemplate(r));
}
// Get statistics for admin dashboard
static async getStats() {
const query = `
SELECT
status,
COUNT(*) as count
FROM custom_templates
GROUP BY status
`;
const result = await database.query(query);
return result.rows.map(row => ({
status: row.status,
count: parseInt(row.count) || 0
}));
}
// Alias for getAll method to match admin route expectations
static async getAllTemplates(limit = 50, offset = 0) {
return await CustomTemplate.getAll(limit, offset);
}
}
module.exports = CustomTemplate;

View File

@ -260,6 +260,34 @@ class Feature {
const result = await database.query(query, params);
return result.rows.map(row => new Feature(row));
}
// Count features for a template
static async countByTemplateId(templateId) {
const query = `SELECT COUNT(*) as count FROM template_features WHERE template_id = $1`;
const result = await database.query(query, [templateId]);
return parseInt(result.rows[0].count) || 0;
}
// Count features for multiple templates at once
static async countByTemplateIds(templateIds) {
if (!templateIds || templateIds.length === 0) return {};
const placeholders = templateIds.map((_, i) => `$${i + 1}`).join(',');
const query = `
SELECT template_id, COUNT(*) as count
FROM template_features
WHERE template_id IN (${placeholders})
GROUP BY template_id
`;
const result = await database.query(query, templateIds);
const counts = {};
result.rows.forEach(row => {
counts[row.template_id] = parseInt(row.count) || 0;
});
return counts;
}
}
module.exports = Feature;

View File

@ -85,6 +85,25 @@ class Template {
return result.rows.length > 0 ? new Template(result.rows[0]) : null;
}
// Check for duplicate templates based on title, type, and category
static async checkForDuplicate(templateData) {
const query = `
SELECT id, title, type, category FROM templates
WHERE is_active = true AND (
type = $1 OR
(LOWER(title) = LOWER($2) AND category = $3)
)
`;
const result = await database.query(query, [
templateData.type,
templateData.title,
templateData.category
]);
return result.rows.length > 0 ? result.rows[0] : null;
}
// Create new template
static async create(templateData) {
const id = uuidv4();

View File

@ -1,6 +1,7 @@
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');
@ -395,4 +396,332 @@ router.post('/notifications/read-all', async (req, res) => {
}
});
// ---------- 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}`);
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;

View File

@ -4,6 +4,7 @@ const Feature = require('../models/feature');
const CustomFeature = require('../models/custom_feature');
const AdminNotification = require('../models/admin_notification');
const FeatureSimilarityService = require('../services/feature_similarity');
const database = require('../config/database');
const { v4: uuidv4 } = require('uuid');
// Initialize similarity service
@ -190,14 +191,14 @@ router.get('/:id', async (req, res) => {
}
});
// POST /api/features - Create new default/suggested feature (template_features)
// POST /api/features - Create new feature directly in template_features table
router.post('/', async (req, res) => {
try {
const featureData = req.body;
console.log('🏗️ Creating new feature:', featureData.name);
// Validate required fields
const requiredFields = ['template_id', 'name', 'feature_type', 'complexity'];
const requiredFields = ['template_id', 'name', 'complexity'];
for (const field of requiredFields) {
if (!featureData[field]) {
return res.status(400).json({
@ -208,18 +209,8 @@ router.post('/', async (req, res) => {
}
}
// Validate enums
const validTypes = ['essential', 'suggested', 'custom'];
// Validate complexity
const validComplexity = ['low', 'medium', 'high'];
if (!validTypes.includes(featureData.feature_type)) {
return res.status(400).json({
success: false,
error: 'Invalid feature type',
message: `Feature type must be one of: ${validTypes.join(', ')}`
});
}
if (!validComplexity.includes(featureData.complexity)) {
return res.status(400).json({
success: false,
@ -227,28 +218,26 @@ router.post('/', async (req, res) => {
message: `Complexity must be one of: ${validComplexity.join(', ')}`
});
}
// Enforce: this endpoint only for default/suggested features
if (featureData.feature_type === 'custom') {
return res.status(400).json({
success: false,
error: 'Invalid feature type for this endpoint',
message: "Use POST /api/features/custom to create custom features"
});
}
// Insert into template_features
// Create feature directly in template_features table
const feature = await Feature.create({
...featureData,
feature_id: featureData.feature_id || `default_${uuidv4()}`,
is_default: true,
created_by_user: false,
template_id: featureData.template_id,
feature_id: featureData.feature_id || `feature_${uuidv4()}`,
name: featureData.name,
description: featureData.description,
feature_type: featureData.feature_type || 'suggested',
complexity: featureData.complexity,
business_rules: featureData.business_rules,
technical_requirements: featureData.technical_requirements,
display_order: featureData.display_order || 999,
is_default: featureData.is_default || false,
created_by_user: featureData.created_by_user || false,
});
res.status(201).json({
success: true,
data: feature,
message: `Feature '${feature.name}' created successfully`
message: `Feature '${feature.name}' created successfully in template_features table`
});
} catch (error) {
console.error('❌ Error creating feature:', error.message);
@ -347,13 +336,22 @@ router.put('/:id/rating', async (req, res) => {
}
});
// PUT /api/features/:id - Update feature
// PUT /api/features/:id - Update feature (handles both regular and custom features)
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const updateData = req.body;
const existing = await Feature.getById(id);
// First try to find as a regular feature
let existing = await Feature.getById(id);
let isCustomFeature = false;
// If not found as regular feature, try as custom feature
if (!existing) {
existing = await CustomFeature.getById(id);
isCustomFeature = true;
}
if (!existing) {
return res.status(404).json({
success: false,
@ -372,7 +370,30 @@ router.put('/:id', async (req, res) => {
return res.status(400).json({ success: false, error: 'Invalid complexity' });
}
const updated = await Feature.update(id, updateData);
let updated;
if (isCustomFeature) {
// Update custom feature
updated = await CustomFeature.update(id, updateData);
// Mirror update into template_features where feature_id = `custom_<id>`
try {
const featureId = `custom_${id}`;
const mirroredExisting = await Feature.getByFeatureId(existing.template_id, featureId);
if (mirroredExisting) {
await Feature.update(mirroredExisting.id, {
name: updateData.name ?? mirroredExisting.name,
description: updateData.description ?? mirroredExisting.description,
complexity: updateData.complexity ?? mirroredExisting.complexity,
});
}
} catch (mirrorErr) {
console.error('Failed to mirror custom feature update:', mirrorErr.message);
}
} else {
// Update regular feature
updated = await Feature.update(id, updateData);
}
res.json({
success: true,
data: updated,
@ -411,6 +432,13 @@ router.delete('/:id', async (req, res) => {
router.post('/custom', async (req, res) => {
try {
const data = req.body || {}
console.log('🔍 Custom feature creation request:', {
template_id: data.template_id,
name: data.name,
complexity: data.complexity,
description: data.description
})
const required = ['template_id', 'name', 'complexity']
for (const f of required) {
if (!data[f]) {
@ -422,6 +450,24 @@ router.post('/custom', async (req, res) => {
return res.status(400).json({ success: false, error: 'Invalid complexity' })
}
// Verify template 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
`, [data.template_id])
if (templateCheck.rows.length === 0) {
console.error('❌ Template not found in either table:', data.template_id)
return res.status(400).json({
success: false,
error: 'Template not found',
message: `Template with ID ${data.template_id} does not exist in templates or custom_templates`
})
}
console.log('✅ Template verified:', templateCheck.rows[0])
// Check for similar features before creating
let similarityInfo = null;
try {
@ -497,8 +543,8 @@ router.post('/custom', async (req, res) => {
}
})
// GET /api/templates/:id/features - merged default + custom features
router.get('/templates/:templateId/merged', async (req, res) => {
// GET /api/features/templates/:id/features - merged default + custom features
router.get('/templates/:templateId/features', async (req, res) => {
try {
const { templateId } = req.params;
const defaults = await Feature.getByTemplateId(templateId);

View File

@ -1,7 +1,11 @@
const express = require('express');
const router = express.Router();
const Template = require('../models/template');
const CustomTemplate = require('../models/custom_template');
const Feature = require('../models/feature');
const CustomFeature = require('../models/custom_feature');
const AdminNotification = require('../models/admin_notification');
const database = require('../config/database');
// GET /api/templates - Get all templates grouped by category
router.get('/', async (req, res) => {
@ -45,32 +49,348 @@ router.get('/stats', async (req, res) => {
}
});
// GET /api/templates/:id - Get specific template with features
router.get('/:id', async (req, res) => {
// GET /api/templates/combined - Built-in templates + current user's custom templates (paginated)
// Query: userId (required for user customs), status (optional for customs), limit, offset
router.get('/combined', async (req, res) => {
try {
const { id } = req.params;
console.log(`🔍 Fetching template: ${id}`);
const userId = req.query.userId || req.query.userid || req.query.user_id || null;
const limit = parseInt(req.query.limit) || 6;
const offset = parseInt(req.query.offset) || 0;
const status = req.query.status || null; // optional filter for custom templates
// Fetch built-in (admin) templates grouped by category, then flatten
const defaultByCategory = await Template.getAllByCategory();
const adminTemplates = Object.values(defaultByCategory).flat().map(t => ({
id: t.id,
type: t.type,
title: t.title,
description: t.description,
icon: t.icon,
category: t.category,
gradient: t.gradient,
border: t.border,
text: t.text,
subtext: t.subtext,
created_at: t.created_at,
updated_at: t.updated_at,
is_custom: false,
source: 'admin'
}));
// Fetch current user's custom templates (if userId provided), else empty
let userCustomTemplates = [];
if (userId) {
const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
if (!uuidV4Regex.test(userId)) {
return res.status(400).json({ success: false, error: 'Invalid userId', message: 'userId must be a valid UUID v4' });
}
const customs = await CustomTemplate.getByUserId(userId, 1000, 0, status);
userCustomTemplates = customs.map(ct => ({
id: ct.id,
type: ct.type,
title: ct.title,
description: ct.description,
icon: ct.icon,
category: ct.category,
gradient: ct.gradient,
border: ct.border,
text: ct.text,
subtext: ct.subtext,
created_at: ct.created_at,
updated_at: ct.updated_at,
is_custom: true,
status: ct.status,
user_id: ct.user_id,
source: 'user'
}));
}
// Combine and sort by created_at desc (fallback title)
const combined = [...adminTemplates, ...userCustomTemplates].sort((a, b) => {
const aTime = a.created_at ? new Date(a.created_at).getTime() : 0;
const bTime = b.created_at ? new Date(b.created_at).getTime() : 0;
if (aTime === bTime) return (a.title || '').localeCompare(b.title || '');
return bTime - aTime;
});
const total = combined.length;
const slice = combined.slice(offset, offset + limit);
const hasMore = offset + slice.length < total;
return res.json({
success: true,
data: slice,
count: slice.length,
pagination: { total, limit, offset, hasMore },
message: `Returned ${slice.length} of ${total} templates (combined admin + user)`
});
} catch (error) {
console.error('❌ Error fetching combined templates:', error.message);
return res.status(500).json({ success: false, error: 'Failed to fetch combined templates', message: error.message });
}
});
// GET /api/templates/merged - Get paginated, filtered templates (default + custom)
router.get('/merged', async (req, res) => {
try {
console.log('🚀 [MERGED-TEMPLATES] Starting template fetch operation...');
console.log('📋 [MERGED-TEMPLATES] Request parameters:', {
limit: req.query.limit || 'default: 10',
offset: req.query.offset || 'default: 0',
category: req.query.category || 'all categories',
search: req.query.search || 'no search query'
});
const template = await Template.getByIdWithFeatures(id);
const limit = parseInt(req.query.limit) || 10;
const offset = parseInt(req.query.offset) || 0;
const categoryFilter = req.query.category || null;
const searchQuery = req.query.search ? req.query.search.toLowerCase() : null;
console.log("req.query __", req.query)
if (!template) {
return res.status(404).json({
success: false,
error: 'Template not found',
message: `Template with ID ${id} does not exist`
console.log('⚙️ [MERGED-TEMPLATES] Parsed parameters:', { limit, offset, categoryFilter, searchQuery });
// Get all default templates
console.log('🏗️ [MERGED-TEMPLATES] Fetching default templates by category...');
const defaultTemplatesByCat = await Template.getAllByCategory();
console.log('📊 [MERGED-TEMPLATES] Default templates by category structure:', Object.keys(defaultTemplatesByCat));
let defaultTemplates = [];
for (const cat in defaultTemplatesByCat) {
const catTemplates = defaultTemplatesByCat[cat];
console.log(`📁 [MERGED-TEMPLATES] Category "${cat}": ${catTemplates.length} templates`);
defaultTemplates = defaultTemplates.concat(catTemplates);
}
console.log('✅ [MERGED-TEMPLATES] Total default templates collected:', defaultTemplates.length);
console.log('🔍 [MERGED-TEMPLATES] Sample default template:', defaultTemplates[0] ? {
id: defaultTemplates[0].id,
title: defaultTemplates[0].title,
category: defaultTemplates[0].category,
type: defaultTemplates[0].type
} : 'No default templates found');
// Get all custom templates for the current user
console.log('🎨 [MERGED-TEMPLATES] Fetching custom templates...');
console.log('🔍 [MERGED-TEMPLATES] Request userId:', req.query.userId);
console.log('🔍 [MERGED-TEMPLATES] Request includeOthers:', req.query.includeOthers);
let customTemplates = [];
let userOwnCustomCount = 0;
let approvedOthersCustomCount = 0;
if (req.query.userId) {
// Validate UUID v4 for userId to avoid DB errors like "invalid input syntax for type uuid"
const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
if (!uuidV4Regex.test(req.query.userId)) {
console.warn('⚠️ [MERGED-TEMPLATES] Invalid userId provided:', req.query.userId);
// Don't return error, just skip user-specific templates and continue with approved ones
console.log('⚠️ [MERGED-TEMPLATES] Continuing with approved templates only due to invalid userId');
customTemplates = await CustomTemplate.getTemplatesByStatus('approved');
approvedOthersCustomCount = customTemplates.length;
console.log('📈 [MERGED-TEMPLATES] Approved custom templates (invalid userId fallback):', approvedOthersCustomCount);
} else {
// Get ALL custom templates for this user (all statuses - approved, pending, rejected, etc.)
console.log('✅ [MERGED-TEMPLATES] Valid userId provided, fetching ALL user templates...');
customTemplates = await CustomTemplate.getByUserId(req.query.userId, 1000, 0);
userOwnCustomCount = customTemplates.length;
console.log('📈 [MERGED-TEMPLATES] ALL custom templates for THIS user:', userOwnCustomCount);
// Optionally include ALL custom templates from other users if explicitly requested
const includeOthers = String(req.query.includeOthers || '').toLowerCase() === 'true';
if (includeOthers) {
const allOtherCustomTemplates = await CustomTemplate.getAll(1000, 0);
const otherUsersTemplates = allOtherCustomTemplates.filter(t => t.user_id !== req.query.userId);
approvedOthersCustomCount = otherUsersTemplates.length;
console.log('📈 [MERGED-TEMPLATES] ALL custom templates from OTHER users (included by query):', approvedOthersCustomCount);
// Combine user's templates + all templates from others
customTemplates = [...customTemplates, ...otherUsersTemplates];
} else {
console.log(' [MERGED-TEMPLATES] Skipping custom templates from other users (includeOthers not set).');
}
}
} else {
// If no userId, get ALL custom templates regardless of status
console.log(' [MERGED-TEMPLATES] No userId provided, fetching ALL custom templates');
customTemplates = await CustomTemplate.getAll(1000, 0);
approvedOthersCustomCount = customTemplates.length;
console.log('📈 [MERGED-TEMPLATES] All custom templates (no user specified):', approvedOthersCustomCount);
}
console.log('📈 [MERGED-TEMPLATES] Totals → userOwn:', userOwnCustomCount, ', approvedOthers:', approvedOthersCustomCount, ', combinedCustoms:', customTemplates.length);
if (customTemplates.length > 0) {
console.log('🔍 [MERGED-TEMPLATES] Sample custom template:', {
id: customTemplates[0].id,
title: customTemplates[0].title,
category: customTemplates[0].category,
status: customTemplates[0].status
});
}
res.json({
success: true,
data: template,
message: `Template ${template.title} retrieved successfully`
// Convert customs to standard template format and merge into flat array
console.log('🔄 [MERGED-TEMPLATES] Converting custom templates to standard format...');
const convertedCustomTemplates = customTemplates.map(customTemplate => ({
id: customTemplate.id,
type: customTemplate.type,
title: customTemplate.title,
description: customTemplate.description,
icon: customTemplate.icon,
category: customTemplate.category,
gradient: customTemplate.gradient,
border: customTemplate.border,
text: customTemplate.text,
subtext: customTemplate.subtext,
is_active: true,
created_at: customTemplate.created_at,
updated_at: customTemplate.updated_at,
is_custom: true,
complexity: customTemplate.complexity,
business_rules: customTemplate.business_rules,
technical_requirements: customTemplate.technical_requirements
}));
console.log('✅ [MERGED-TEMPLATES] Custom templates converted:', convertedCustomTemplates.length);
let allTemplates = defaultTemplates.concat(convertedCustomTemplates);
console.log('🔗 [MERGED-TEMPLATES] Combined templates total:', allTemplates.length);
// Apply category filter if specified
if (categoryFilter && categoryFilter !== 'all') {
console.log(`🎯 [MERGED-TEMPLATES] Applying category filter: "${categoryFilter}"`);
const beforeFilter = allTemplates.length;
allTemplates = allTemplates.filter(t => t.category === categoryFilter);
const afterFilter = allTemplates.length;
console.log(`📊 [MERGED-TEMPLATES] Category filter result: ${beforeFilter}${afterFilter} templates`);
}
// Apply search filter if specified
if (searchQuery) {
console.log(`🔍 [MERGED-TEMPLATES] Applying search filter: "${searchQuery}"`);
const beforeSearch = allTemplates.length;
allTemplates = allTemplates.filter(t =>
t.title.toLowerCase().includes(searchQuery) ||
t.description.toLowerCase().includes(searchQuery)
);
const afterSearch = allTemplates.length;
console.log(`📊 [MERGED-TEMPLATES] Search filter result: ${beforeSearch}${afterSearch} templates`);
}
// Sort by created_at descending
console.log('📅 [MERGED-TEMPLATES] Sorting templates by creation date...');
allTemplates.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
console.log('✅ [MERGED-TEMPLATES] Templates sorted successfully');
// Paginate
const total = allTemplates.length;
console.log('📊 [MERGED-TEMPLATES] Final template count before pagination:', total);
console.log('📄 [MERGED-TEMPLATES] Pagination parameters:', { offset, limit, total });
const paginatedTemplates = allTemplates.slice(offset, offset + limit);
console.log('📋 [MERGED-TEMPLATES] Paginated result:', {
requested: limit,
returned: paginatedTemplates.length,
startIndex: offset,
endIndex: offset + paginatedTemplates.length - 1
});
// Add feature counts to each template
console.log('🔢 [MERGED-TEMPLATES] Fetching feature counts for templates...');
// Separate default and custom templates for feature counting
const defaultTemplateIds = paginatedTemplates.filter(t => !t.is_custom).map(t => t.id);
const customTemplateIds = paginatedTemplates.filter(t => t.is_custom).map(t => t.id);
console.log('📊 [MERGED-TEMPLATES] Template ID breakdown:', {
defaultTemplates: defaultTemplateIds.length,
customTemplates: customTemplateIds.length
});
// Fetch feature counts for both types
let defaultFeatureCounts = {};
let customFeatureCounts = {};
if (defaultTemplateIds.length > 0) {
console.log('🔍 [MERGED-TEMPLATES] Fetching default template feature counts...');
defaultFeatureCounts = await Feature.countByTemplateIds(defaultTemplateIds);
console.log('✅ [MERGED-TEMPLATES] Default feature counts:', Object.keys(defaultFeatureCounts).length, 'templates');
}
if (customTemplateIds.length > 0) {
console.log('🔍 [MERGED-TEMPLATES] Fetching custom template feature counts...');
customFeatureCounts = await CustomFeature.countByTemplateIds(customTemplateIds);
console.log('✅ [MERGED-TEMPLATES] Custom feature counts:', Object.keys(customFeatureCounts).length, 'templates');
}
// Add feature counts to each template
const templatesWithFeatureCounts = paginatedTemplates.map(template => ({
...template,
feature_count: template.is_custom
? (customFeatureCounts[template.id] || 0)
: (defaultFeatureCounts[template.id] || 0)
}));
console.log('🎯 [MERGED-TEMPLATES] Feature counts added to all templates');
// Log sample of returned templates with feature counts
if (templatesWithFeatureCounts.length > 0) {
console.log('🔍 [MERGED-TEMPLATES] First template in result:', {
id: templatesWithFeatureCounts[0].id,
title: templatesWithFeatureCounts[0].title,
category: templatesWithFeatureCounts[0].category,
is_custom: templatesWithFeatureCounts[0].is_custom,
feature_count: templatesWithFeatureCounts[0].feature_count
});
if (templatesWithFeatureCounts.length > 1) {
console.log('🔍 [MERGED-TEMPLATES] Last template in result:', {
id: templatesWithFeatureCounts[templatesWithFeatureCounts.length - 1].id,
title: templatesWithFeatureCounts[templatesWithFeatureCounts.length - 1].title,
category: templatesWithFeatureCounts[templatesWithFeatureCounts.length - 1].category,
is_custom: templatesWithFeatureCounts[templatesWithFeatureCounts.length - 1].is_custom,
feature_count: templatesWithFeatureCounts[templatesWithFeatureCounts.length - 1].feature_count
});
}
}
const responseData = {
success: true,
data: templatesWithFeatureCounts,
pagination: {
total,
offset,
limit,
hasMore: offset + limit < total
},
message: `Found ${templatesWithFeatureCounts.length} templates (out of ${total}) with feature counts`
};
console.log('🎉 [MERGED-TEMPLATES] Response prepared successfully:', {
success: responseData.success,
dataCount: responseData.data.length,
pagination: responseData.pagination,
message: responseData.message,
sampleFeatureCounts: templatesWithFeatureCounts.slice(0, 3).map(t => ({
title: t.title,
feature_count: t.feature_count,
is_custom: t.is_custom
}))
});
res.json(responseData);
} catch (error) {
console.error('❌ Error fetching template:', error.message);
console.error('💥 [MERGED-TEMPLATES] Critical error occurred:', error.message);
console.error('📚 [MERGED-TEMPLATES] Error stack:', error.stack);
console.error('🔍 [MERGED-TEMPLATES] Error details:', {
name: error.name,
code: error.code,
sqlMessage: error.sqlMessage
});
res.status(500).json({
success: false,
error: 'Failed to fetch template',
error: 'Failed to fetch merged templates',
message: error.message
});
}
@ -111,19 +431,122 @@ router.get('/type/:type', async (req, res) => {
}
});
// GET /api/templates/:id/features - Get features for a template
router.get('/:id/features', async (req, res) => {
// GET /api/templates/:id - Get specific template with features (UUID constrained)
router.get('/:id([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})', async (req, res) => {
try {
const { id } = req.params;
console.log(`🔍 Fetching template: ${id}`);
// Extra guard: ensure UUID v4 to avoid DB errors if route matching misfires
const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
if (!uuidV4Regex.test(id)) {
return res.status(400).json({
success: false,
error: 'Invalid template id',
message: 'id must be a valid UUID v4'
});
}
const template = await Template.getByIdWithFeatures(id);
if (!template) {
return res.status(404).json({
success: false,
error: 'Template not found',
message: `Template with ID ${id} does not exist`
});
}
res.json({
success: true,
data: template,
message: `Template ${template.title} retrieved successfully`
});
} catch (error) {
console.error('❌ Error fetching template:', error.message);
res.status(500).json({
success: false,
error: 'Failed to fetch template',
message: error.message
});
}
});
// GET /api/templates/:id/features - Get features for a template (UUID constrained)
router.get('/:id([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/features', async (req, res) => {
try {
const { id } = req.params;
console.log(`🎯 Fetching features for template: ${id}`);
const features = await Feature.getByTemplateId(id);
// Check if template exists in either templates or custom_templates table
console.log(`🔍 Searching for template ID: ${id}`);
// First check templates table
const defaultTemplateCheck = await database.query(`
SELECT id, title, 'default' as template_type FROM templates WHERE id = $1 AND is_active = true
`, [id]);
console.log(`📊 Default templates found: ${defaultTemplateCheck.rows.length}`);
// Then check custom_templates table
const customTemplateCheck = await database.query(`
SELECT id, title, 'custom' as template_type FROM custom_templates WHERE id = $1
`, [id]);
console.log(`📊 Custom templates found: ${customTemplateCheck.rows.length}`);
// Combine results
const templateCheck = {
rows: [...defaultTemplateCheck.rows, ...customTemplateCheck.rows]
};
if (templateCheck.rows.length === 0) {
console.log(`❌ Template not found in either table: ${id}`);
return res.status(404).json({
success: false,
error: 'Template not found',
message: `Template with ID ${id} does not exist in templates or custom_templates`
});
}
console.log(`✅ Template found: ${templateCheck.rows[0].title} (${templateCheck.rows[0].template_type})`);
let features = [];
if (templateCheck.rows[0].template_type === 'custom') {
// For custom templates, get features from custom_features table
console.log('📋 Fetching features from custom_features table for custom template');
const customFeaturesQuery = `
SELECT
cf.id,
cf.template_id,
cf.name,
cf.description,
cf.complexity,
'custom' as feature_type,
cf.created_at,
cf.updated_at,
cf.status,
cf.approved
FROM custom_features cf
WHERE cf.template_id = $1
ORDER BY cf.created_at DESC
`;
const customFeaturesResult = await database.query(customFeaturesQuery, [id]);
features = customFeaturesResult.rows;
} else {
// For default templates, get features from template_features table
console.log('📋 Fetching features from template_features table for default template');
features = await Feature.getByTemplateId(id);
}
res.json({
success: true,
data: features,
count: features.length,
message: `Found ${features.length} features for template`
message: `Found ${features.length} features for ${templateCheck.rows[0].template_type} template`,
templateInfo: templateCheck.rows[0]
});
} catch (error) {
console.error('❌ Error fetching template features:', error.message);
@ -139,7 +562,7 @@ router.get('/:id/features', async (req, res) => {
router.post('/', async (req, res) => {
try {
const templateData = req.body;
console.log('🏗️ Creating new template:', templateData.title);
console.log('🏗️ Creating new template - incoming body:', JSON.stringify(templateData));
// Validate required fields
const requiredFields = ['type', 'title', 'category'];
@ -152,6 +575,143 @@ router.post('/', async (req, res) => {
});
}
}
// Check for duplicates in regular templates first
const existingTemplate = await Template.checkForDuplicate(templateData);
if (existingTemplate) {
return res.status(409).json({
success: false,
error: 'Duplicate template detected',
message: `A template with similar characteristics already exists: "${existingTemplate.title}" (type: ${existingTemplate.type})`,
existing_template: {
id: existingTemplate.id,
title: existingTemplate.title,
type: existingTemplate.type,
category: existingTemplate.category
}
});
}
// If flagged as a custom template, store in custom_templates instead
if (templateData.isCustom === true || templateData.is_custom === true || templateData.source === 'custom') {
try {
const validComplexity = ['low', 'medium', 'high'];
const complexity = templateData.complexity || 'medium';
if (!validComplexity.includes(complexity)) {
return res.status(400).json({
success: false,
error: 'Invalid complexity',
message: `Complexity must be one of: ${validComplexity.join(', ')}`
});
}
// Check for duplicates in both regular and custom templates
const existingRegularTemplate = await CustomTemplate.checkTypeInMainTemplates(templateData.type);
if (existingRegularTemplate) {
return res.status(409).json({
success: false,
error: 'Template type already exists in main templates',
message: `A main template with type '${templateData.type}' already exists: "${existingRegularTemplate.title}"`,
existing_template: {
id: existingRegularTemplate.id,
title: existingRegularTemplate.title,
type: existingRegularTemplate.type,
source: 'main_templates'
}
});
}
const incomingUserId = templateData.user_id || templateData.userId || (req.user && (req.user.id || req.user.user_id)) || null;
// Check for duplicates in custom templates for this user
const duplicatePayload = {
type: templateData.type,
title: templateData.title,
category: templateData.category,
user_id: incomingUserId
};
const existingCustomTemplate = await CustomTemplate.checkForDuplicate(duplicatePayload);
if (existingCustomTemplate) {
return res.status(409).json({
success: false,
error: 'Duplicate custom template detected',
message: `You already have a similar template: "${existingCustomTemplate.title}" (type: ${existingCustomTemplate.type})`,
existing_template: {
id: existingCustomTemplate.id,
title: existingCustomTemplate.title,
type: existingCustomTemplate.type,
category: existingCustomTemplate.category,
user_id: existingCustomTemplate.user_id,
source: 'custom_templates'
}
});
}
// Validate user_id format if provided
if (incomingUserId) {
const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
if (!uuidV4Regex.test(incomingUserId)) {
return res.status(400).json({
success: false,
error: 'Invalid user_id',
message: 'user_id must be a valid UUID v4'
});
}
}
const isCustomValue = (templateData.is_custom !== undefined ? templateData.is_custom : (templateData.isCustom !== undefined ? templateData.isCustom : true));
const payloadToCreate = {
type: templateData.type,
title: templateData.title,
description: templateData.description,
icon: templateData.icon,
category: templateData.category,
gradient: templateData.gradient,
border: templateData.border,
text: templateData.text,
subtext: templateData.subtext,
complexity,
business_rules: templateData.business_rules,
technical_requirements: templateData.technical_requirements,
approved: false,
usage_count: 1,
created_by_user_session: templateData.created_by_user_session,
status: 'pending',
is_custom: isCustomValue,
user_id: incomingUserId
};
console.log('[Templates Route -> custom] user identification:', {
body_user_id: templateData.user_id,
body_userId: templateData.userId,
req_user: req.user ? (req.user.id || req.user.user_id) : null
});
console.log('[Templates Route -> custom] payload for create:', JSON.stringify(payloadToCreate));
const created = await CustomTemplate.create(payloadToCreate);
console.log('[Templates Route -> custom] created record summary:', { id: created.id, type: created.type, user_id: created.user_id, status: created.status });
// Create admin notification for new custom template
try {
console.log('[Templates Route -> custom] creating admin notification for template:', created.id, created.title);
const notif = await AdminNotification.notifyNewTemplate(created.id, created.title);
console.log('[Templates Route -> custom] admin notification created:', notif?.id);
} catch (notificationError) {
console.error('⚠️ Failed to create admin notification:', notificationError.message);
}
return res.status(201).json({
success: true,
data: created,
message: `Custom template '${created.title}' created successfully and submitted for admin review`
});
} catch (customErr) {
console.error('❌ Error creating custom template via templates route:', customErr.message);
return res.status(500).json({
success: false,
error: 'Failed to create custom template',
message: customErr.message
});
}
}
const template = await Template.create(templateData);
@ -229,7 +789,7 @@ router.delete('/:id', async (req, res) => {
}
// Soft delete by updating the instance
// await template.update({ is_active: false });
// await template.update({ is_active = false });
await Template.delete(id);
res.json({
@ -246,4 +806,6 @@ router.delete('/:id', async (req, res) => {
}
});
module.exports = router;
module.exports = router;

View File

@ -0,0 +1,105 @@
const axios = require('axios');
// Test configuration
const BASE_URL = 'http://localhost:3003/api/templates';
const TEST_USER_ID = '550e8400-e29b-41d4-a716-446655440000'; // Sample UUID
// Test template data
const testTemplate = {
type: 'test-duplicate-template',
title: 'Test Duplicate Template',
description: 'This is a test template for duplicate prevention',
category: 'test',
icon: 'test-icon',
gradient: 'bg-blue-500',
border: 'border-blue-200',
text: 'text-blue-800',
subtext: 'text-blue-600',
isCustom: true,
user_id: TEST_USER_ID,
complexity: 'medium'
};
async function testDuplicatePrevention() {
console.log('🧪 Testing Template Duplicate Prevention\n');
try {
// Test 1: Create first template (should succeed)
console.log('📝 Test 1: Creating first template...');
const response1 = await axios.post(BASE_URL, testTemplate);
console.log('✅ First template created successfully:', response1.data.data.id);
const firstTemplateId = response1.data.data.id;
// Test 2: Try to create exact duplicate (should fail)
console.log('\n📝 Test 2: Attempting to create exact duplicate...');
try {
await axios.post(BASE_URL, testTemplate);
console.log('❌ ERROR: Duplicate was allowed when it should have been prevented!');
} catch (error) {
if (error.response && error.response.status === 409) {
console.log('✅ Duplicate correctly prevented:', error.response.data.message);
console.log(' Existing template info:', error.response.data.existing_template);
} else {
console.log('❌ Unexpected error:', error.response?.data || error.message);
}
}
// Test 3: Try with same title but different type (should fail for same user)
console.log('\n📝 Test 3: Attempting same title, different type...');
const sameTitle = { ...testTemplate, type: 'different-type-same-title' };
try {
await axios.post(BASE_URL, sameTitle);
console.log('❌ ERROR: Same title duplicate was allowed!');
} catch (error) {
if (error.response && error.response.status === 409) {
console.log('✅ Same title duplicate correctly prevented:', error.response.data.message);
} else {
console.log('❌ Unexpected error:', error.response?.data || error.message);
}
}
// Test 4: Try with same type but different title (should fail)
console.log('\n📝 Test 4: Attempting same type, different title...');
const sameType = { ...testTemplate, title: 'Different Title Same Type' };
try {
await axios.post(BASE_URL, sameType);
console.log('❌ ERROR: Same type duplicate was allowed!');
} catch (error) {
if (error.response && error.response.status === 409) {
console.log('✅ Same type duplicate correctly prevented:', error.response.data.message);
} else {
console.log('❌ Unexpected error:', error.response?.data || error.message);
}
}
// Test 5: Different user should be able to create similar template
console.log('\n📝 Test 5: Different user creating similar template...');
const differentUser = {
...testTemplate,
user_id: '550e8400-e29b-41d4-a716-446655440001', // Different UUID
type: 'test-duplicate-template-user2'
};
try {
const response5 = await axios.post(BASE_URL, differentUser);
console.log('✅ Different user can create similar template:', response5.data.data.id);
} catch (error) {
console.log('❌ Different user blocked unexpectedly:', error.response?.data || error.message);
}
// Cleanup: Delete test templates
console.log('\n🧹 Cleaning up test templates...');
try {
await axios.delete(`${BASE_URL}/${firstTemplateId}`);
console.log('✅ Cleanup completed');
} catch (error) {
console.log('⚠️ Cleanup failed:', error.message);
}
} catch (error) {
console.log('❌ Test setup failed:', error.response?.data || error.message);
console.log('💡 Make sure the template service is running on port 3003');
}
}
// Run the test
testDuplicatePrevention();

View File

@ -16,7 +16,7 @@ GMAIL_APP_PASSWORD=your-app-password
# Service Configuration
PORT=8011
NODE_ENV=development
FRONTEND_URL=http://localhost:3001
FRONTEND_URL=http://localhost:3000
AUTH_PUBLIC_URL=http://localhost:8011
# Database Configuration

0
services/user-auth/node Normal file
View File

View File

@ -9,6 +9,7 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"axios": "^1.11.0",
"bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
@ -1314,9 +1315,19 @@
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true,
"license": "MIT"
},
"node_modules/axios": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/babel-jest": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
@ -1801,7 +1812,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
@ -1978,7 +1988,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
@ -2155,7 +2164,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@ -2402,11 +2410,30 @@
"node": ">=8"
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
@ -2653,7 +2680,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
@ -4576,6 +4602,12 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/pstree.remy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",

View File

@ -12,6 +12,7 @@
"test:watch": "jest --watch"
},
"dependencies": {
"axios": "^1.11.0",
"bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",

View File

@ -45,7 +45,7 @@ const corsOptions = {
origin: function (origin, callback) {
// Allow requests from your web-dashboard and other services
const allowedOrigins = [
'http://localhost:3001', // Web dashboard (changed from 3000 to 3001 to avoid conflict with other services)
'http://localhost:3000', // Web dashboard (changed from 3000 to 3000 to avoid conflict with other services)
'http://localhost:8008', // Dashboard service
'http://localhost:8000', // API Gateway
'http://localhost:3000', // Development React

View File

@ -4,7 +4,7 @@ class Database {
constructor() {
this.pool = new Pool({
host: process.env.POSTGRES_HOST || 'localhost',
port: process.env.POSTGRES_PORT || 5433, // changed from 5432 to 5433 to avoid conflict with other services
port: process.env.POSTGRES_PORT || 5432,
database: process.env.POSTGRES_DB || 'dev_pipeline',
user: process.env.POSTGRES_USER || 'pipeline_admin',
password: process.env.POSTGRES_PASSWORD || 'secure_pipeline_2024',

View File

@ -126,8 +126,8 @@ const passwordChangeRateLimit = createRateLimit(
);
const apiRateLimit = createRateLimit(
15 * 60 * 1000, // 15 minutes
100, // 100 requests
15 * 60 * 10000, // 15 minutes
10000, // 100 requests
'Too many API requests. Please slow down.'
);

View File

@ -8,8 +8,7 @@ DROP TABLE IF EXISTS refresh_tokens CASCADE;
DROP TABLE IF EXISTS users CASCADE;
DROP TABLE IF EXISTS user_projects CASCADE;
-- Enable UUID extension if not already enabled
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Users table - Core user accounts
CREATE TABLE users (
@ -27,9 +26,9 @@ CREATE TABLE users (
updated_at TIMESTAMP DEFAULT NOW()
);
-- Refresh tokens table - JWT refresh token management
-- Refresh tokens table
CREATE TABLE refresh_tokens (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
id UUID PRIMARY KEY,
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
token_hash VARCHAR(255) NOT NULL,
expires_at TIMESTAMP NOT NULL,
@ -38,9 +37,9 @@ CREATE TABLE refresh_tokens (
is_revoked BOOLEAN DEFAULT false
);
-- User sessions table - Track user activity and sessions
-- User sessions table
CREATE TABLE user_sessions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
id UUID PRIMARY KEY,
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
session_token VARCHAR(255) UNIQUE,
ip_address INET,
@ -51,34 +50,31 @@ CREATE TABLE user_sessions (
created_at TIMESTAMP DEFAULT NOW(),
expires_at TIMESTAMP DEFAULT NOW() + INTERVAL '30 days'
);
-- User feature preferences table - Track which features users have removed/customized
-- User feature preferences table
CREATE TABLE user_feature_preferences (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
id UUID PRIMARY KEY,
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
template_type VARCHAR(100) NOT NULL, -- 'healthcare', 'ecommerce', etc.
feature_id VARCHAR(100) NOT NULL, -- feature identifier from template-manager
template_type VARCHAR(100) NOT NULL,
feature_id VARCHAR(100) NOT NULL,
preference_type VARCHAR(20) NOT NULL CHECK (preference_type IN ('removed', 'added', 'customized')),
custom_data JSONB, -- For storing custom feature modifications
custom_data JSONB,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(user_id, template_type, feature_id, preference_type)
);
-- User project tracking - Track user's projects and their selections
-- User projects table
CREATE TABLE user_projects (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
id UUID PRIMARY KEY,
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
project_name VARCHAR(200) NOT NULL,
project_type VARCHAR(100) NOT NULL,
selected_features JSONB, -- Array of selected feature IDs
custom_features JSONB, -- Array of user-created custom features
project_data JSONB, -- Complete project configuration
selected_features JSONB,
custom_features JSONB,
project_data JSONB,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Indexes for performance
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_username ON users(username);
@ -146,11 +142,10 @@ BEGIN
END;
$$ LANGUAGE plpgsql;
-- Insert initial admin user (password: admin123 - change in production!)
-- Insert initial admin user
INSERT INTO users (
id, username, email, password_hash, first_name, last_name, role, email_verified, is_active
username, email, password_hash, first_name, last_name, role, email_verified, is_active
) VALUES (
uuid_generate_v4(),
'admin',
'admin@tech4biz.com',
'$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', -- bcrypt hash of 'admin123'
@ -161,11 +156,10 @@ INSERT INTO users (
true
) ON CONFLICT (email) DO NOTHING;
-- Insert test user for development
-- Insert test user
INSERT INTO users (
id, username, email, password_hash, first_name, last_name, role, email_verified, is_active
username, email, password_hash, first_name, last_name, role, email_verified, is_active
) VALUES (
uuid_generate_v4(),
'testuser',
'test@tech4biz.com',
'$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', -- bcrypt hash of 'admin123'
@ -175,7 +169,6 @@ INSERT INTO users (
true,
true
) ON CONFLICT (email) DO NOTHING;
-- Success message
SELECT 'User Authentication database schema created successfully!' as message;

View File

@ -82,6 +82,11 @@ async function runMigrations() {
];
try {
// Ensure required extensions exist before running migrations
console.log('🔧 Ensuring required PostgreSQL extensions...');
await database.query('CREATE EXTENSION IF NOT EXISTS "uuid-ossp";');
console.log('✅ Extensions ready');
for (const migrationFile of migrations) {
const migrationPath = path.join(__dirname, migrationFile);
if (!fs.existsSync(migrationPath)) {

View File

@ -2,6 +2,8 @@ const express = require('express');
const router = express.Router();
const authService = require('../services/authService');
const User = require('../models/user');
// Remove cross-service dependencies - use API calls instead
const serviceClient = require('../services/serviceClient');
const {
authenticateToken,
optionalAuth,
@ -63,7 +65,7 @@ router.get('/verify-email', async (req, res) => {
const { token } = req.query;
await authService.verifyEmailToken(token);
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3001';
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
const redirectUrl = `${frontendUrl}/signin?verified=true`;
// JSON fallback if not a browser navigation
if (req.get('Accept') && req.get('Accept').includes('application/json')) {
@ -71,7 +73,7 @@ router.get('/verify-email', async (req, res) => {
}
return res.redirect(302, redirectUrl);
} catch (error) {
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3001';
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
const redirectUrl = `${frontendUrl}/signin?error=${encodeURIComponent(error.message)}`;
if (req.get('Accept') && req.get('Accept').includes('application/json')) {
return res.status(400).json({ success: false, message: error.message });
@ -84,6 +86,7 @@ router.get('/verify-email', async (req, res) => {
router.post('/login', /*loginRateLimit , */validateLogin, async (req, res) => {
try {
const { email, password } = req.body;
console.log('🔑 Login attempt for: ', email, password);
const sessionInfo = {
ip_address: req.ip,
user_agent: req.get('User-Agent'),
@ -536,4 +539,136 @@ router.get('/admin/users', authenticateToken, requireAdmin, async (req, res) =>
}
});
// GET /api/auth/admin/custom-features - Get all custom features (Admin only)
router.get('/admin/custom-features', authenticateToken, requireAdmin, async (req, res) => {
try {
const { status, limit = 50, offset = 0 } = req.query;
console.log(`📋 Admin fetching custom features - status: ${status || 'all'}`);
// Extract the token from the Authorization header
const authToken = req.headers.authorization?.replace('Bearer ', '');
const result = await serviceClient.getCustomFeatures(status, parseInt(limit), parseInt(offset), authToken);
res.json({
success: true,
data: result.data,
message: 'Custom features retrieved successfully'
});
} catch (error) {
console.error('❌ Failed to fetch custom features:', error.message);
res.status(503).json({
success: false,
error: 'Service unavailable',
message: error.message
});
}
});
// POST /api/auth/admin/custom-features/:id/review - Review custom feature (Admin only)
router.post('/admin/custom-features/:id/review', authenticateToken, requireAdmin, async (req, res) => {
try {
const { id } = req.params;
const { status, admin_notes } = req.body;
if (!['approved', 'rejected', 'pending'].includes(status)) {
return res.status(400).json({
success: false,
error: 'Invalid status',
message: 'Status must be approved, rejected, or pending'
});
}
console.log(`📝 Admin reviewing custom feature ${id} - status: ${status}`);
const reviewData = {
status,
admin_notes,
admin_reviewed_by: req.user.id
};
// Extract the token from the Authorization header
const authToken = req.headers.authorization?.replace('Bearer ', '');
const result = await serviceClient.reviewCustomFeature(id, reviewData, authToken);
res.json({
success: true,
data: result.data,
message: `Custom feature ${status} successfully`
});
} catch (error) {
console.error('❌ Failed to review custom feature:', error.message);
res.status(503).json({
success: false,
error: 'Service unavailable',
message: error.message
});
}
});
// GET /api/auth/admin/custom-templates - Get all custom templates (Admin only)
router.get('/admin/custom-templates', authenticateToken, requireAdmin, async (req, res) => {
try {
const { status, limit = 50, offset = 0 } = req.query;
console.log(`📋 Admin fetching custom templates - status: ${status || 'all'}`);
// Extract the token from the Authorization header
const authToken = req.headers.authorization?.replace('Bearer ', '');
const result = await serviceClient.getCustomTemplates(status, parseInt(limit), parseInt(offset), authToken);
res.json({
success: true,
data: result.data,
message: 'Custom templates retrieved successfully'
});
} catch (error) {
console.error('❌ Failed to fetch custom templates:', error.message);
res.status(503).json({
success: false,
error: 'Service unavailable',
message: error.message
});
}
});
// POST /api/auth/admin/custom-templates/:id/review - Review custom template (Admin only)
router.post('/admin/custom-templates/:id/review', authenticateToken, requireAdmin, async (req, res) => {
try {
const { id } = req.params;
const { status, admin_notes } = req.body;
if (!['approved', 'rejected', 'pending'].includes(status)) {
return res.status(400).json({
success: false,
error: 'Invalid status',
message: 'Status must be approved, rejected, or pending'
});
}
console.log(`📝 Admin reviewing custom template ${id} - status: ${status}`);
const reviewData = {
status,
admin_notes,
admin_reviewed_by: req.user.id
};
// Extract the token from the Authorization header
const authToken = req.headers.authorization?.replace('Bearer ', '');
const result = await serviceClient.reviewCustomTemplate(id, reviewData, authToken);
res.json({
success: true,
data: result.data,
message: `Custom template ${status} successfully`
});
} catch (error) {
console.error('❌ Failed to review custom template:', error.message);
res.status(503).json({
success: false,
error: 'Service unavailable',
message: error.message
});
}
});
module.exports = router;

View File

@ -269,14 +269,15 @@ class AuthService {
async storeRefreshToken(userId, refreshToken) {
const tokenHash = await this.hashToken(refreshToken);
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
const id = uuidv4();
const query = `
INSERT INTO refresh_tokens (user_id, token_hash, expires_at)
VALUES ($1, $2, $3)
INSERT INTO refresh_tokens (id, user_id, token_hash, expires_at)
VALUES ($1, $2, $3, $4)
RETURNING id
`;
const result = await database.query(query, [userId, tokenHash, expiresAt]);
const result = await database.query(query, [id, userId, tokenHash, expiresAt]);
return result.rows[0];
}
@ -305,16 +306,18 @@ class AuthService {
// Create user session
async createSession(userId, sessionInfo) {
const sessionToken = uuidv4();
const id = uuidv4();
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days
const query = `
INSERT INTO user_sessions (
user_id, session_token, ip_address, user_agent, device_info, expires_at
) VALUES ($1, $2, $3, $4, $5, $6)
id, user_id, session_token, ip_address, user_agent, device_info, expires_at
) VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *
`;
const values = [
id,
userId,
sessionToken,
sessionInfo.ip_address,

View File

@ -0,0 +1,95 @@
const axios = require('axios');
class ServiceClient {
constructor() {
this.templateManagerUrl = process.env.TEMPLATE_MANAGER_URL || 'http://localhost:8009';
}
async getCustomFeatures(status, limit = 50, offset = 0, authToken) {
try {
const params = { limit, offset };
if (status) params.status = status;
const headers = {};
if (authToken) {
headers.Authorization = `Bearer ${authToken}`;
}
const response = await axios.get(`${this.templateManagerUrl}/api/templates/admin/custom-features`, {
params,
headers,
timeout: 5000
});
return response.data;
} catch (error) {
console.error('Failed to fetch custom features from template-manager:', error.message);
throw new Error('Template manager service unavailable');
}
}
async reviewCustomFeature(id, reviewData, authToken) {
try {
const headers = {};
if (authToken) {
headers.Authorization = `Bearer ${authToken}`;
}
const response = await axios.post(
`${this.templateManagerUrl}/api/templates/admin/custom-features/${id}/review`,
reviewData,
{ headers, timeout: 5000 }
);
return response.data;
} catch (error) {
console.error('Failed to review custom feature:', error.message);
throw new Error('Template manager service unavailable');
}
}
async getCustomTemplates(status, limit = 50, offset = 0, authToken) {
try {
const params = { limit, offset };
if (status) params.status = status;
const headers = {};
if (authToken) {
headers.Authorization = `Bearer ${authToken}`;
}
const response = await axios.get(`${this.templateManagerUrl}/api/templates/admin/custom-templates`, {
params,
headers,
timeout: 5000
});
return response.data;
} catch (error) {
console.error('Failed to fetch custom templates from template-manager:', error.message);
throw new Error('Template manager service unavailable');
}
}
async reviewCustomTemplate(id, reviewData, authToken) {
try {
const headers = {};
if (authToken) {
headers.Authorization = `Bearer ${authToken}`;
}
const response = await axios.post(
`${this.templateManagerUrl}/api/templates/admin/custom-templates/${id}/review`,
reviewData,
{ headers, timeout: 5000 }
);
return response.data;
} catch (error) {
console.error('Failed to review custom template:', error.message);
throw new Error('Template manager service unavailable');
}
}
}
module.exports = new ServiceClient();

View File

View File

@ -33,7 +33,7 @@
"zustand": "^5.0.6"
},
"scripts": {
"start": "PORT=3001 react-scripts start",
"start": "PORT=3000 react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"