Initial commit - backend code
This commit is contained in:
parent
80087e13ec
commit
229e0fee4e
66
.env.backup
66
.env.backup
@ -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
|
||||
@ -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:
|
||||
|
||||
270
services/template-manager/CUSTOM_TEMPLATES_README.md
Normal file
270
services/template-manager/CUSTOM_TEMPLATES_README.md
Normal 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
|
||||
@ -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 ./
|
||||
|
||||
|
||||
121
services/template-manager/add-sample-templates.js
Normal file
121
services/template-manager/add-sample-templates.js
Normal 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 };
|
||||
232
services/template-manager/package-lock.json
generated
232
services/template-manager/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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": {
|
||||
|
||||
41
services/template-manager/run-migration.js
Normal file
41
services/template-manager/run-migration.js
Normal 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();
|
||||
@ -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!');
|
||||
});
|
||||
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -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
|
||||
`);
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
|
||||
307
services/template-manager/src/models/custom_template.js
Normal file
307
services/template-manager/src/models/custom_template.js
Normal 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;
|
||||
@ -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;
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
105
services/template-manager/test_duplicate_prevention.js
Normal file
105
services/template-manager/test_duplicate_prevention.js
Normal 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();
|
||||
@ -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
0
services/user-auth/node
Normal file
44
services/user-auth/package-lock.json
generated
44
services/user-auth/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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.'
|
||||
);
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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;
|
||||
@ -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,
|
||||
|
||||
95
services/user-auth/src/services/serviceClient.js
Normal file
95
services/user-auth/src/services/serviceClient.js
Normal 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();
|
||||
0
services/user-auth/user-auth@1.0.0
Normal file
0
services/user-auth/user-auth@1.0.0
Normal 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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user