admin changes

This commit is contained in:
Chandini 2025-09-09 11:22:09 +05:30
parent 047f1266b9
commit cdc68e7ae6
53 changed files with 6885 additions and 162 deletions

View File

@ -441,6 +441,7 @@ services:
- REDIS_PORT=6379
- REDIS_PASSWORD=${REDIS_PASSWORD}
- NODE_ENV=development
- DATABASE_URL=postgresql://pipeline_admin:${POSTGRES_PASSWORD}@postgres:5432/dev_pipeline
entrypoint: ["/bin/sh", "-c", "chmod +x ./scripts/migrate-all.sh && ./scripts/migrate-all.sh"]
depends_on:
postgres:
@ -573,7 +574,10 @@ services:
- NODE_ENV=development
- PORT=8000
- HOST=0.0.0.0
- FRONTEND_URL=http://localhost:3000
- FRONTEND_URL=http://localhost:3001 # Make sure this matches your frontend URL
- CORS_ORIGINS=http://localhost:3001 # Add this line
- CORS_METHODS=GET,POST,PUT,DELETE,PATCH,OPTIONS # Add this line
- CORS_CREDENTIALS=true # Add this line
# Database connections
- POSTGRES_HOST=postgres
- POSTGRES_PORT=5432
@ -589,8 +593,10 @@ services:
- RABBITMQ_USER=pipeline_admin
- RABBITMQ_PASSWORD=${RABBITMQ_PASSWORD}
# JWT configuration
- JWT_ACCESS_SECRET=access-secret-key-2024-tech4biz-${POSTGRES_PASSWORD}
- JWT_REFRESH_SECRET=refresh-secret-key-2024-tech4biz-${POSTGRES_PASSWORD}
- JWT_ACCESS_SECRET=access-secret-key-2024-tech4biz-secure_pipeline_2024
- JWT_SECRET=access-secret-key-2024-tech4biz-secure_pipeline_2024
# - JWT_ACCESS_SECRET=access-secret-key-2024-tech4biz-${POSTGRES_PASSWORD}
# - JWT_REFRESH_SECRET=refresh-secret-key-2024-tech4biz-${POSTGRES_PASSWORD}
# Service URLs
- USER_AUTH_URL=http://user-auth:8011
- TEMPLATE_MANAGER_URL=http://template-manager:8009
@ -838,9 +844,9 @@ services:
- REDIS_PASSWORD=${REDIS_PASSWORD}
- JWT_ACCESS_SECRET=access-secret-key-2024-tech4biz-${POSTGRES_PASSWORD}
- JWT_REFRESH_SECRET=refresh-secret-key-2024-tech4biz-${POSTGRES_PASSWORD}
- JWT_ACCESS_EXPIRY=15m
- JWT_ACCESS_EXPIRY=24h
- JWT_REFRESH_EXPIRY=7d
- FRONTEND_URL=http://localhost:3000
- FRONTEND_URL=http://localhost:3001
# Email Configuration
- SMTP_HOST=${SMTP_HOST:-smtp.gmail.com}
- SMTP_PORT=${SMTP_PORT:-587}
@ -850,7 +856,7 @@ services:
- SMTP_FROM=${SMTP_FROM:-frontendtechbiz@gmail.com}
- GMAIL_USER=${GMAIL_USER:-frontendtechbiz@gmail.com}
- GMAIL_APP_PASSWORD=${GMAIL_APP_PASSWORD:-oidhhjeasgzbqptq}
- AUTH_PUBLIC_URL=http://localhost:8011
- AUTH_PUBLIC_URL=http://localhost:3001
- TEMPLATE_MANAGER_URL=http://template-manager:8009
networks:
- pipeline_network
@ -862,7 +868,7 @@ services:
migrations:
condition: service_completed_successfully
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8011/health"]
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:8011/health"]
interval: 30s
timeout: 10s
retries: 3
@ -909,13 +915,46 @@ services:
start_period: 40s
restart: unless-stopped
# AI Mockup / Wireframe Generation Service
ai-mockup-service:
build: ./services/ai-mockup-service
container_name: pipeline_ai_mockup_service
ports:
- "8021:8021"
environment:
- PORT=8021
- HOST=0.0.0.0
- CLAUDE_API_KEY=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
- POSTGRES_HOST=postgres
- POSTGRES_PORT=5432
- POSTGRES_DB=dev_pipeline
- POSTGRES_USER=pipeline_admin
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- REDIS_HOST=redis
- REDIS_PORT=6379
- REDIS_PASSWORD=${REDIS_PASSWORD}
- JWT_ACCESS_SECRET=access-secret-key-2024-tech4biz-${POSTGRES_PASSWORD}
- USER_AUTH_SERVICE_URL=http://user-auth:8011
- FLASK_ENV=development
networks:
- pipeline_network
depends_on:
postgres:
condition: service_healthy
user-auth:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8021/health"]
interval: 30s
timeout: 10s
retries: 3
git-integration:
build: ./services/git-integration
container_name: pipeline_git_integration
ports:
- "8012:8012"
env_file:
- ./services/git-integration/.env
environment:
- PORT=8012
- HOST=0.0.0.0

View File

@ -1,5 +1,5 @@
# Server Configuration
PORT=3000
PORT=3001
NODE_ENV=development
JWT_SECRET=your_jwt_secret_key
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001

View File

@ -1,7 +1,7 @@
# Server Configuration
PORT=3000
PORT=3001
NODE_ENV=development
ALLOWED_ORIGINS=http://localhost:3000,https://yourdomain.com
ALLOWED_ORIGINS=http://localhost:3001,https://yourdomain.com
# Database Configuration
DB_HOST=localhost

8
package-lock.json generated
View File

@ -1,6 +1,12 @@
{
"name": "codenuk-backend-live",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {}
"packages": {
"": {
"name": "codenuk-backend-live",
"version": "1.0.0"
}
}
}

41
services/ai-mockup-service/.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@ -0,0 +1,114 @@
# Authentication Fix Summary
## Problem Identified
The ai-mockup-service was failing with a 401 error "Unable to verify token with auth service" when trying to save wireframes. This was caused by:
1. **Missing `/api/auth/verify` endpoint** in the user-auth service
2. **JWT secret mismatch** between services
3. **Incorrect token verification flow** in ai-mockup-service
4. **User ID extraction issues** in protected endpoints
## Fixes Implemented
### 1. Added Missing Token Verification Endpoint
- **File**: `automated-dev-pipeline/services/user-auth/src/routes/auth.js`
- **Added**: `GET /api/auth/verify` endpoint
- **Purpose**: Allows ai-mockup-service to verify JWT tokens remotely
### 2. Fixed JWT Secret Configuration
- **File**: `automated-dev-pipeline/services/ai-mockup-service/src/app.py`
- **Changed**: `JWT_SECRET` from `'your-jwt-secret-key-change-in-production'` to `'access-secret-key-2024-tech4biz'`
- **Purpose**: Ensures both services use the same JWT secret for local verification
### 3. Improved Token Verification Logic
- **File**: `automated-dev-pipeline/services/ai-mockup-service/src/app.py`
- **Enhanced**: `verify_jwt_token()` function with better error handling and logging
- **Added**: Fallback to remote verification when local verification fails
- **Improved**: Error messages and debugging information
### 4. Fixed User ID Extraction
- **Files**: All protected endpoints in ai-mockup-service
- **Changed**: User ID extraction to handle both local and remote JWT verification
- **Added**: Support for multiple user ID field names (`id`, `userId`, `user_id`)
- **Enhanced**: Error messages for authentication failures
### 5. Enhanced Frontend Error Handling
- **File**: `codenuk-frontend-dark-theme/src/components/wireframe-canvas.tsx`
- **Improved**: Error handling in `saveWireframe()` function
- **Added**: Specific error messages for different HTTP status codes
- **Enhanced**: User-friendly error messages for authentication issues
### 6. Updated Environment Configuration
- **File**: `automated-dev-pipeline/services/ai-mockup-service/src/env.example`
- **Updated**: JWT configuration to match user-auth service
## How It Works Now
### Token Verification Flow
1. **Local Verification**: ai-mockup-service first tries to verify JWT tokens locally using the shared secret
2. **Remote Verification**: If local verification fails, it calls the user-auth service's `/api/auth/verify` endpoint
3. **User Data**: Both methods return user data that can be used for authorization
### Authentication Process
1. User logs in through frontend → receives JWT token from user-auth service
2. Frontend sends requests to ai-mockup-service with JWT token in Authorization header
3. ai-mockup-service verifies token (locally or remotely) and extracts user information
4. Protected endpoints check user ID and permissions before proceeding
## Testing the Fixes
### 1. Run the Authentication Test
```bash
cd automated-dev-pipeline/services/ai-mockup-service/src
python test_auth.py
```
### 2. Test Wireframe Generation (No Auth Required)
```bash
curl -X POST http://localhost:8021/generate-wireframe/desktop \
-H "Content-Type: application/json" \
-d '{"prompt": "Simple login form"}'
```
### 3. Test Wireframe Saving (Auth Required)
```bash
# First get a valid JWT token from user-auth service
# Then use it to save a wireframe
curl -X POST http://localhost:8021/api/wireframes \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-d '{"wireframe": {"name": "Test"}, "elements": []}'
```
## Environment Variables Required
### ai-mockup-service (.env)
```bash
JWT_SECRET=access-secret-key-2024-tech4biz
USER_AUTH_SERVICE_URL=http://localhost:8011
```
### user-auth-service (.env)
```bash
JWT_ACCESS_SECRET=access-secret-key-2024-tech4biz
JWT_REFRESH_SECRET=refresh-secret-key-2024-tech4biz
```
## Troubleshooting
### Common Issues
1. **401 Unauthorized**: Check if JWT tokens are being sent correctly
2. **Token verification failed**: Verify both services are running and accessible
3. **User ID not found**: Check JWT payload structure and user ID field names
### Debug Steps
1. Check service logs for detailed error messages
2. Verify environment variables are set correctly
3. Ensure both services are running on expected ports
4. Test token verification endpoint directly
## Next Steps
1. Test the authentication flow end-to-end
2. Monitor logs for any remaining issues
3. Consider adding more comprehensive error handling
4. Implement token refresh logic if needed

View File

@ -0,0 +1,39 @@
# Use official Python runtime as a parent image
FROM python:3.9-slim
# Set the working directory in the container
WORKDIR /app
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV FLASK_APP=src/app.py
ENV FLASK_ENV=production
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
curl \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy the current directory contents into the container at /app
COPY . .
# Create startup script
RUN echo '#!/bin/bash\n\
echo "Setting up database..."\n\
python src/setup_database.py\n\
echo "Starting AI Mockup Service..."\n\
gunicorn --bind 0.0.0.0:8021 src.app:app\n\
' > /app/start.sh && chmod +x /app/start.sh
# Expose the port the app runs on
EXPOSE 8021
# Run startup script
CMD ["/app/start.sh"]

View File

@ -0,0 +1,228 @@
# AI Mockup Service - Wireframe Saving Implementation Guide
## 🎯 **Overview**
This guide explains the complete implementation of wireframe saving functionality with user authentication in the CodeNuk AI Mockup Service.
## 🔧 **Problem Solved**
- **Issue**: AI mockup service was failing to connect to user-auth service for JWT verification
- **Root Cause**: Service communication issues and JWT secret mismatches
- **Solution**: Implemented robust authentication with fallback mechanisms and proper service coordination
## 🏗️ **Architecture**
### **Services Involved**:
1. **AI Mockup Service** (Port 8021) - Handles wireframe generation and storage
2. **User Auth Service** (Port 8011) - Manages user authentication and JWT tokens
3. **PostgreSQL Database** (Port 5433) - Stores wireframes and user data
4. **Frontend** (Port 3001) - React application with wireframe canvas
### **Data Flow**:
```
Frontend → User Auth Service → AI Mockup Service → PostgreSQL
↓ ↓ ↓ ↓
Canvas JWT Token Wireframe Data Persistent Storage
```
## 🔐 **Authentication Implementation**
### **JWT Configuration**:
- **Secret**: `access-secret-key-2024-tech4biz-${POSTGRES_PASSWORD}`
- **Algorithm**: HS256
- **Expiry**: 15 minutes (access), 7 days (refresh)
### **Verification Strategy**:
1. **Local Verification**: Try to verify JWT with local secret first
2. **Remote Verification**: If local fails, call user-auth service
3. **Fallback**: Continue with local verification if remote service unavailable
### **User ID Extraction**:
```python
def extract_user_id_from_token(user_data):
"""Extract user ID from various possible token formats"""
return (user_data.get('id') or
user_data.get('userId') or
user_data.get('user_id') or
user_data.get('sub') or
user_data.get('user', {}).get('id'))
```
## 💾 **Database Schema**
### **Wireframes Table**:
```sql
CREATE TABLE wireframes (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
project_id UUID REFERENCES user_projects(id) ON DELETE SET NULL,
name VARCHAR(200) NOT NULL,
description TEXT,
device_type VARCHAR(20) DEFAULT 'desktop',
dimensions JSONB NOT NULL,
metadata JSONB,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
### **Wireframe Elements Table**:
```sql
CREATE TABLE wireframe_elements (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
wireframe_id UUID REFERENCES wireframes(id) ON DELETE CASCADE,
element_type VARCHAR(50) NOT NULL,
element_data JSONB NOT NULL,
position JSONB NOT NULL,
size JSONB,
style JSONB,
parent_id UUID REFERENCES wireframe_elements(id) ON DELETE CASCADE,
z_index INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
## 🚀 **Deployment Steps**
### **1. Update Docker Compose**:
```yaml
ai-mockup-service:
environment:
- JWT_ACCESS_SECRET=access-secret-key-2024-tech4biz-${POSTGRES_PASSWORD}
- USER_AUTH_SERVICE_URL=http://user-auth:8011
depends_on:
user-auth:
condition: service_healthy
```
### **2. Start Services**:
```bash
cd automated-dev-pipeline
docker compose up -d user-auth ai-mockup-service
```
### **3. Verify Health**:
```bash
curl http://localhost:8011/health # User Auth Service
curl http://localhost:8021/health # AI Mockup Service
```
### **4. Test Integration**:
```bash
cd services/ai-mockup-service/src
python test_integration.py
```
## 🎨 **Frontend Integration**
### **Wireframe Canvas Component**:
- **Auto-save**: Automatically saves wireframes every 30 seconds
- **Authentication**: Uses JWT tokens from auth context
- **Error Handling**: Graceful fallback for authentication failures
### **Key Features**:
- **Real-time Saving**: Wireframes saved as user creates them
- **User Isolation**: Each user only sees their own wireframes
- **Version Control**: Automatic versioning of wireframe changes
- **Multi-device Support**: Desktop, tablet, and mobile wireframes
## 🔍 **Testing**
### **Manual Testing**:
1. **Register/Login**: Create account at `http://localhost:3001/signup`
2. **Create Wireframe**: Go to project builder → AI Mockup step
3. **Generate Wireframe**: Use AI prompt to generate wireframe
4. **Save Wireframe**: Canvas automatically saves to database
5. **Verify Storage**: Check database for saved wireframe data
### **Automated Testing**:
```bash
# Run integration tests
python test_integration.py
# Expected output:
# ✅ AI Mockup Service is healthy
# ✅ User Auth Service is healthy
# ✅ User registration successful
# ✅ Wireframe generation successful
# ✅ Wireframe saved successfully
# ✅ Wireframe retrieved successfully
```
## 🛠️ **Troubleshooting**
### **Common Issues**:
1. **Connection Refused (Port 8011)**:
```bash
# Check if user-auth service is running
docker compose ps user-auth
# Restart if needed
docker compose restart user-auth
```
2. **JWT Verification Failed**:
```bash
# Check JWT secrets match
docker compose exec user-auth env | grep JWT_ACCESS_SECRET
docker compose exec ai-mockup-service env | grep JWT_ACCESS_SECRET
```
3. **Database Connection Failed**:
```bash
# Check PostgreSQL is running
docker compose ps postgres
# Run database setup
docker compose exec ai-mockup-service python src/setup_database.py
```
### **Debug Commands**:
```bash
# View service logs
docker compose logs -f ai-mockup-service
docker compose logs -f user-auth
# Check database tables
docker compose exec postgres psql -U pipeline_admin -d dev_pipeline -c "\dt"
# Test authentication endpoint
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:8011/api/auth/verify
```
## 📊 **Monitoring**
### **Health Endpoints**:
- **AI Mockup Service**: `http://localhost:8021/health`
- **User Auth Service**: `http://localhost:8011/health`
### **Key Metrics**:
- **Database Connection**: Status of PostgreSQL connection
- **Auth Service**: Status of user-auth service communication
- **Wireframe Count**: Number of wireframes saved per user
- **Generation Success Rate**: Percentage of successful wireframe generations
## 🎯 **Success Criteria**
**Authentication**: Users can register/login and receive valid JWT tokens
**Wireframe Generation**: AI generates wireframes based on user prompts
**Wireframe Saving**: Wireframes are saved to database with user association
**Wireframe Retrieval**: Users can load their previously saved wireframes
**User Isolation**: Users only see their own wireframes
**Error Handling**: Graceful handling of service failures
**Real-time Updates**: Frontend updates reflect saved state
## 🔮 **Future Enhancements**
1. **Collaborative Editing**: Multiple users editing same wireframe
2. **Version History**: Detailed version control with diff views
3. **Export Options**: Export wireframes as PNG, PDF, or code
4. **Templates**: Pre-built wireframe templates
5. **Analytics**: Usage analytics and performance metrics
---
**Implementation Status**: ✅ **COMPLETE**
**Last Updated**: $(date)
**Version**: 1.0.0

View File

@ -0,0 +1,227 @@
# 🎉 SVG-Based Wireframe Generation - Implementation Complete!
## ✅ **What Has Been Implemented**
### **1. Backend SVG Generation** 🏗️
- **Flask Application**: Updated `app.py` to generate SVG wireframes
- **SVG Functions**: Complete set of SVG generation functions for all wireframe elements
- **Response Types**: Primary SVG response with JSON fallback
- **Error Handling**: Graceful fallback when SVG generation fails
### **2. Frontend SVG Parsing** 🎨
- **SVG Parser**: Complete SVG parsing and rendering system
- **tldraw Integration**: Converts SVG elements to interactive tldraw shapes
- **Response Detection**: Automatically detects SVG vs JSON responses
- **Fallback System**: Maintains backward compatibility
### **3. Comprehensive Documentation** 📚
- **Frontend README**: Complete setup and usage guide
- **Backend README**: Flask implementation details
- **Integration Guide**: Step-by-step implementation walkthrough
- **Implementation Summary**: This document
## 🚀 **How It Works Now**
### **Complete Flow:**
```
User Prompt → Backend → Claude AI → Layout Spec → SVG Generation → Frontend → SVG Parsing → tldraw Canvas
```
### **Response Types:**
1. **SVG Response** (Primary): `Content-Type: image/svg+xml`
2. **JSON Response** (Fallback): `Content-Type: application/json`
### **SVG Elements Supported:**
- **Rectangles**: Headers, sidebars, content areas, cards
- **Text**: Labels, titles, descriptions
- **Groups**: Logical sections and containers
- **Shadows**: Drop shadows and card shadows
- **Styling**: Colors, fonts, borders, and spacing
## 🔧 **Backend Implementation Details**
### **Key Functions:**
- `generate_svg_wireframe()` - Main SVG generator
- `generate_header()` - Header section rendering
- `generate_sidebar()` - Sidebar rendering
- `generate_hero()` - Hero section rendering
- `generate_section()` - Main content sections
- `generate_grid_section()` - Grid layouts
- `generate_form_section()` - Form elements
- `generate_footer()` - Footer rendering
### **SVG Features:**
- **Filters**: Shadow effects for cards and hero sections
- **Styling**: Consistent color schemes and typography
- **Layout**: Precise positioning and spacing
- **Responsiveness**: Scalable vector graphics
### **API Endpoints:**
- `POST /generate-wireframe` - Generate SVG wireframe
- `GET /health` - Health check endpoint
## 🎯 **Frontend Implementation Details**
### **SVG Parsing Functions:**
- `parseSVGAndRender()` - Main SVG parser
- `renderSVGElements()` - Element iteration and routing
- `renderSVGRect()` - Rectangle rendering
- `renderSVGCircle()` - Circle rendering
- `renderSVGText()` - Text rendering
- `renderSVGPath()` - Path handling
### **Response Handling:**
```typescript
// Check response type
const contentType = response.headers.get('content-type')
if (contentType && contentType.includes('image/svg+xml')) {
// Handle SVG response
const svgString = await response.text()
await parseSVGAndRender(editor, svgString)
} else {
// Fallback to JSON
const data = await response.json()
await generateWireframeFromSpec(editor, data.wireframe)
}
```
## 📁 **File Structure**
```
my-app/
├── components/
│ └── wireframe-canvas.tsx # Updated with SVG parsing
├── lib/
│ └── config.ts # Updated endpoints
├── backend/
│ ├── app.py # SVG generation backend
│ ├── requirements.txt # Updated dependencies
│ ├── start_backend.py # Startup script
│ └── README.md # Backend documentation
├── README.md # Frontend documentation
├── INTEGRATION_GUIDE.md # Implementation guide
└── IMPLEMENTATION_SUMMARY.md # This document
```
## 🧪 **Testing & Validation**
### **Backend Testing:**
- ✅ SVG generation functions work correctly
- ✅ All wireframe elements render properly
- ✅ Error handling and fallbacks work
- ✅ Response headers are set correctly
### **Frontend Testing:**
- ✅ TypeScript compilation passes
- ✅ SVG parsing functions are implemented
- ✅ Response type detection works
- ✅ Fallback mechanisms are in place
## 🚀 **Getting Started**
### **1. Start Backend:**
```bash
cd backend
pip install -r requirements.txt
python start_backend.py
```
### **2. Start Frontend:**
```bash
cd my-app
npm install
npm run dev
```
### **3. Test Generation:**
1. Open the application
2. Enter a prompt: "Dashboard with header, sidebar, and 3 stats cards"
3. Click "Generate with AI"
4. View the SVG-generated wireframe on the canvas
## 🎨 **Example Prompts**
- **Dashboard**: "Dashboard with header, left sidebar, 3 stats cards, line chart, and footer"
- **Landing Page**: "Landing page with hero section, feature grid, and contact form"
- **E-commerce**: "Product page with image gallery, product details, and reviews"
- **Form**: "Contact form with name, email, message, and submit button"
## 🔮 **Benefits of This Implementation**
### **1. Precision & Quality:**
- **Exact Positioning**: SVG provides pixel-perfect layouts
- **Rich Styling**: Full support for colors, shadows, and effects
- **Scalable Graphics**: Vector-based, resolution-independent
### **2. Performance:**
- **Faster Rendering**: Direct SVG parsing vs complex JSON processing
- **Better Memory Usage**: Efficient SVG element handling
- **Reduced Complexity**: Simpler frontend logic
### **3. Maintainability:**
- **Backend Logic**: SVG generation logic centralized in backend
- **Frontend Simplicity**: Clean SVG parsing and rendering
- **Error Handling**: Robust fallback mechanisms
## 🐛 **Troubleshooting**
### **Common Issues:**
1. **SVG Not Rendering**: Check content-type headers
2. **Parsing Errors**: Validate SVG XML structure
3. **Backend Connection**: Verify backend URL in config
4. **CORS Issues**: Ensure backend CORS is configured
### **Debug Tips:**
- Check browser network tab for response types
- Verify SVG content in browser dev tools
- Monitor backend console for generation errors
- Test with simple prompts first
## 📈 **Future Enhancements**
### **Planned Features:**
- **Advanced SVG Elements**: Complex paths, gradients, animations
- **Template System**: Pre-built wireframe templates
- **Custom Styling**: User-defined themes and color schemes
- **Export Options**: PNG, PDF, and other formats
- **Collaboration**: Real-time editing and sharing
### **Performance Optimizations:**
- **SVG Caching**: Cache generated SVGs for repeated prompts
- **Lazy Loading**: Load complex elements on demand
- **Compression**: Optimize SVG file sizes
- **CDN Integration**: Global content delivery
## 🎯 **Success Metrics**
### **What We've Achieved:**
- ✅ **SVG Generation**: Complete backend SVG generation system
- ✅ **Frontend Integration**: Full SVG parsing and rendering
- ✅ **Response Handling**: Dual response type support
- ✅ **Error Handling**: Robust fallback mechanisms
- ✅ **Documentation**: Comprehensive guides and examples
- ✅ **Testing**: Validated functionality and performance
### **Quality Improvements:**
- **Precision**: From approximate to exact positioning
- **Performance**: Faster rendering and better memory usage
- **Styling**: Rich visual effects and consistent design
- **Maintainability**: Cleaner, more organized codebase
## 🏆 **Conclusion**
The SVG-based wireframe generation system is now **fully implemented and operational**. This represents a significant improvement over the previous JSON-based approach, providing:
- **Better Performance**: Faster rendering and reduced complexity
- **Higher Quality**: Precise positioning and rich styling
- **Improved UX**: More accurate and visually appealing wireframes
- **Future-Proof**: Scalable architecture for enhancements
The system successfully bridges the gap between AI-generated wireframe specifications and interactive tldraw canvases, delivering professional-quality wireframes from natural language prompts.
---
**🎉 Ready for Production Use! 🎉**
Your wireframe generation tool now produces high-quality SVG wireframes that render perfectly in the frontend, providing users with precise, scalable, and visually appealing wireframe layouts.

View File

@ -0,0 +1,464 @@
# SVG-Based Wireframe Generation - Integration Guide
This guide explains how to implement and integrate the SVG-based wireframe generation system that converts natural language prompts into precise, scalable vector graphics.
## 🎯 **Why SVG Instead of JSON?**
### **Advantages of SVG Approach:**
1. **Precise Positioning**: Exact coordinates and dimensions
2. **Better Performance**: Direct rendering without parsing overhead
3. **Scalable Graphics**: Vector-based, resolution-independent
4. **Rich Styling**: Colors, gradients, shadows, and effects
5. **Standard Format**: Widely supported across platforms
### **Comparison:**
| Aspect | JSON Approach | SVG Approach |
|--------|---------------|--------------|
| **Precision** | Approximate positioning | Exact positioning |
| **Performance** | Slower (parsing + generation) | Faster (direct rendering) |
| **Styling** | Limited color options | Full CSS styling support |
| **Complexity** | Simple shapes only | Complex paths and effects |
| **Maintenance** | Frontend logic heavy | Backend logic heavy |
## 🏗️ **System Architecture**
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Frontend │ │ Backend │ │ Claude AI │
│ (React) │◄──►│ (Flask) │◄──►│ (API) │
│ │ │ │ │ │
│ • tldraw Canvas │ │ • Prompt │ │ • Natural │
│ • SVG Parser │ │ Processing │ │ Language │
│ • Response │ │ • SVG Generation │ │ Analysis │
│ Handler │ │ • Response │ │ • Layout │
└─────────────────┘ │ Routing │ │ Generation │
└──────────────────┘ └─────────────────┘
```
## 🔄 **Data Flow**
### **1. User Input**
```
User types: "Dashboard with header, sidebar, and 3 stats cards"
```
### **2. Frontend Request**
```typescript
const response = await fetch('/generate-wireframe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: userPrompt })
})
```
### **3. Backend Processing**
```python
# Flask backend receives prompt
@app.route('/generate-wireframe', methods=['POST'])
def generate_wireframe():
prompt = request.json.get('prompt')
# Send to Claude AI
claude_response = call_claude_api(prompt)
# Generate SVG from AI response
svg_content = generate_svg_wireframe(claude_response)
# Return SVG with proper content type
return svg_content, 200, {'Content-Type': 'image/svg+xml'}
```
### **4. SVG Response**
```xml
<svg width="800" height="600" viewBox="0 0 800 600">
<defs>
<filter id="shadow" y="-40%" x="-40%" width="180%" height="180%">
<feDropShadow dx="1" dy="1" stdDeviation="1.2" flood-opacity=".5"/>
</filter>
</defs>
<g>
<!-- Header -->
<rect x="0" y="0" width="800" height="60" fill="#f0f0f0"/>
<text x="20" y="35" font-family="Arial" font-size="16">Dashboard Header</text>
<!-- Sidebar -->
<rect x="0" y="60" width="200" height="540" fill="#e0e0e0"/>
<text x="20" y="85" font-family="Arial" font-size="14">Navigation</text>
<!-- Stats Cards -->
<rect x="220" y="80" width="160" height="120" fill="#ffffff" filter="url(#shadow)"/>
<text x="240" y="100" font-family="Arial" font-size="12">Stats Card 1</text>
<rect x="400" y="80" width="160" height="120" fill="#ffffff" filter="url(#shadow)"/>
<text x="420" y="100" font-family="Arial" font-size="12">Stats Card 2</text>
<rect x="580" y="80" width="160" height="120" fill="#ffffff" filter="url(#shadow)"/>
<text x="600" y="100" font-family="Arial" font-size="12">Stats Card 3</text>
</g>
</svg>
```
### **5. Frontend Rendering**
```typescript
// Check response type
const contentType = response.headers.get('content-type')
if (contentType && contentType.includes('image/svg+xml')) {
// Handle SVG response
const svgString = await response.text()
await parseSVGAndRender(editor, svgString)
} else {
// Fallback to JSON
const data = await response.json()
await generateWireframeFromSpec(editor, data.wireframe)
}
```
## 🔧 **Implementation Steps**
### **Step 1: Backend SVG Generation**
#### **1.1 Install Dependencies**
```bash
pip install flask flask-cors anthropic
```
#### **1.2 Create SVG Generator**
```python
import xml.etree.ElementTree as ET
def generate_svg_wireframe(layout_spec):
"""Generate SVG wireframe from layout specification"""
# Create SVG root element
svg = ET.Element('svg', {
'width': '800',
'height': '600',
'viewBox': '0 0 800 600',
'xmlns': 'http://www.w3.org/2000/svg'
})
# Add definitions (filters, gradients)
defs = ET.SubElement(svg, 'defs')
shadow_filter = ET.SubElement(defs, 'filter', {
'id': 'shadow',
'y': '-40%', 'x': '-40%',
'width': '180%', 'height': '180%'
})
ET.SubElement(shadow_filter, 'feDropShadow', {
'dx': '1', 'dy': '1',
'stdDeviation': '1.2',
'flood-opacity': '.5'
})
# Create main group
main_group = ET.SubElement(svg, 'g')
# Generate layout elements
generate_header(main_group, layout_spec.get('header', {}))
generate_sidebar(main_group, layout_spec.get('sidebar', {}))
generate_main_content(main_group, layout_spec.get('main_content', {}))
generate_footer(main_group, layout_spec.get('footer', {}))
return ET.tostring(svg, encoding='unicode')
def generate_header(group, header_spec):
"""Generate header section"""
if not header_spec.get('enabled', False):
return
# Header background
ET.SubElement(group, 'rect', {
'x': '0', 'y': '0',
'width': '800', 'height': '60',
'fill': '#f0f0f0'
})
# Header text
ET.SubElement(group, 'text', {
'x': '20', 'y': '35',
'font-family': 'Arial',
'font-size': '16',
'fill': '#333333'
}).text = header_spec.get('title', 'Header')
```
#### **1.3 Update Flask Endpoint**
```python
@app.route('/generate-wireframe', methods=['POST'])
def generate_wireframe():
try:
prompt = request.json.get('prompt')
if not prompt:
return jsonify({'error': 'Prompt is required'}), 400
# Call Claude AI
claude_response = call_claude_api(prompt)
# Parse AI response and generate SVG
layout_spec = parse_claude_response(claude_response)
svg_content = generate_svg_wireframe(layout_spec)
# Return SVG with proper headers
response = make_response(svg_content)
response.headers['Content-Type'] = 'image/svg+xml'
response.headers['Cache-Control'] = 'no-cache'
return response
except Exception as e:
logger.error(f"Error generating wireframe: {str(e)}")
return jsonify({'error': 'Internal server error'}), 500
```
### **Step 2: Frontend SVG Parsing**
#### **2.1 SVG Parser Functions**
```typescript
const parseSVGAndRender = async (editor: Editor, svgString: string) => {
try {
// Parse SVG string
const parser = new DOMParser()
const svgDoc = parser.parseFromString(svgString, 'image/svg+xml')
const svgElement = svgDoc.querySelector('svg')
if (!svgElement) {
throw new Error('Invalid SVG content')
}
// Get dimensions
const viewBox = svgElement.getAttribute('viewBox')?.split(' ').map(Number) || [0, 0, 800, 600]
const [, , svgWidth, svgHeight] = viewBox
// Create main frame
editor.createShape({
id: createShapeId(),
type: "frame",
x: 50, y: 50,
props: {
w: Math.max(800, svgWidth),
h: Math.max(600, svgHeight),
name: "SVG Wireframe",
},
})
// Render SVG elements
await renderSVGElements(editor, svgElement, 50, 50, svgWidth, svgHeight)
} catch (error) {
console.error('SVG parsing error:', error)
// Fallback to basic wireframe
await generateFallbackWireframe(editor, "SVG parsing failed")
}
}
```
#### **2.2 Element Renderers**
```typescript
const renderSVGRect = async (editor: Editor, element: SVGElement, offsetX: number, offsetY: number) => {
const x = parseFloat(element.getAttribute('x') || '0') + offsetX
const y = parseFloat(element.getAttribute('y') || '0') + offsetY
const width = parseFloat(element.getAttribute('width') || '100')
const height = parseFloat(element.getAttribute('height') || '100')
const fill = element.getAttribute('fill') || 'none'
const stroke = element.getAttribute('stroke') || 'black'
editor.createShape({
id: createShapeId(),
type: "geo",
x, y,
props: {
w: Math.max(10, width),
h: Math.max(10, height),
geo: "rectangle",
fill: fill === 'none' ? 'none' : 'semi',
color: mapColorToTldraw(stroke),
},
})
}
```
## 🎨 **SVG Styling and Effects**
### **Shadows and Filters**
```xml
<defs>
<filter id="shadow" y="-40%" x="-40%" width="180%" height="180%">
<feDropShadow dx="1" dy="1" stdDeviation="1.2" flood-opacity=".5"/>
</filter>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
```
### **Gradients**
```xml
<defs>
<linearGradient id="headerGradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#4facfe;stop-opacity:1" />
<stop offset="100%" style="stop-color:#00f2fe;stop-opacity:1" />
</linearGradient>
</defs>
<rect x="0" y="0" width="800" height="60" fill="url(#headerGradient)"/>
```
### **Text Styling**
```xml
<text x="20" y="35"
font-family="Arial, sans-serif"
font-size="16"
font-weight="bold"
fill="#333333"
text-anchor="start">
Dashboard Header
</text>
```
## 🔄 **Response Type Detection**
### **Content-Type Based Routing**
```typescript
const generateFromPrompt = async (prompt: string) => {
try {
const response = await fetch('/generate-wireframe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt })
})
// Detect response type
const contentType = response.headers.get('content-type')
if (contentType && contentType.includes('image/svg+xml')) {
// SVG response - parse and render
const svgString = await response.text()
await parseSVGAndRender(editor, svgString)
} else {
// JSON response - fallback processing
const data = await response.json()
await generateWireframeFromSpec(editor, data.wireframe)
}
} catch (error) {
console.error('Generation error:', error)
await generateFallbackWireframe(editor, prompt)
}
}
```
## 🧪 **Testing and Validation**
### **Backend Testing**
```python
def test_svg_generation():
"""Test SVG generation functionality"""
# Test layout specification
layout_spec = {
'header': {'enabled': True, 'title': 'Test Header'},
'sidebar': {'enabled': True, 'width': 200},
'main_content': {'sections': []},
'footer': {'enabled': True, 'height': 60}
}
# Generate SVG
svg_content = generate_svg_wireframe(layout_spec)
# Validate SVG structure
assert '<svg' in svg_content
assert 'width="800"' in svg_content
assert 'height="600"' in svg_content
assert 'Test Header' in svg_content
print("✅ SVG generation test passed")
if __name__ == '__main__':
test_svg_generation()
```
### **Frontend Testing**
```typescript
const testSVGParsing = async () => {
const testSVG = `
<svg width="100" height="100" viewBox="0 0 100 100">
<rect x="10" y="10" width="80" height="80" fill="#f0f0f0"/>
<text x="20" y="60">Test</text>
</svg>
`
try {
await parseSVGAndRender(mockEditor, testSVG)
console.log('✅ SVG parsing test passed')
} catch (error) {
console.error('❌ SVG parsing test failed:', error)
}
}
```
## 🚀 **Performance Optimization**
### **SVG Optimization Techniques**
1. **Minimize DOM Elements**: Use groups for related elements
2. **Optimize Paths**: Simplify complex paths
3. **Reduce Attributes**: Use CSS classes for common styles
4. **Compression**: Gzip SVG responses
### **Caching Strategies**
```python
from functools import lru_cache
@lru_cache(maxsize=100)
def generate_cached_svg(prompt_hash):
"""Cache SVG generation for repeated prompts"""
return generate_svg_wireframe(get_cached_layout(prompt_hash))
```
## 🔮 **Future Enhancements**
### **Advanced SVG Features**
- **Animations**: CSS animations and transitions
- **Interactivity**: Click handlers and hover effects
- **Responsive Design**: ViewBox scaling and media queries
- **Accessibility**: ARIA labels and screen reader support
### **Integration Possibilities**
- **Design Systems**: Consistent component libraries
- **Export Options**: PNG, PDF, and other formats
- **Collaboration**: Real-time editing and version control
- **Analytics**: Usage tracking and performance metrics
---
## 📋 **Implementation Checklist**
- [ ] Backend SVG generation functions
- [ ] Frontend SVG parsing and rendering
- [ ] Response type detection and routing
- [ ] Error handling and fallback mechanisms
- [ ] Testing and validation
- [ ] Performance optimization
- [ ] Documentation and examples
## 🆘 **Troubleshooting**
### **Common Issues**
1. **SVG Not Rendering**: Check content-type headers
2. **Parsing Errors**: Validate SVG XML structure
3. **Performance Issues**: Optimize SVG complexity
4. **CORS Problems**: Configure proper origins
### **Debug Tips**
- Use browser dev tools to inspect SVG responses
- Check network tab for content-type headers
- Validate SVG content with online validators
- Monitor console for parsing errors
---
**This integration guide provides a comprehensive approach to implementing SVG-based wireframe generation. The system offers better performance, precision, and styling capabilities compared to JSON-based approaches.**

View File

@ -0,0 +1,271 @@
Heres a complete README draft you can use for your project:
---
# 🖌️ tldraw Interactive UI Controllers
This project extends [tldraw](https://tldraw.dev) to support **interactive UI components** (similar to Balsamiq) that can be dropped into the canvas and interacted with directly.
Weve built **10 controllers**:
1. ✅ **Checkbox**
2. 🔘 **Radio Group**
3. ✏️ **Text Input**
4. 📝 **Textarea**
5. ⏹ **Button**
6. 🔄 **Toggle Switch**
7. 📅 **Date Picker**
8. 🔽 **ComboBox (Select Dropdown)**
9. 📊 **Data Grid (Table)**
10. 📦 **Form Container** (groups other controls)
All controllers are **fully interactive** inside the canvas, not just static wireframes.
---
## 🚀 Features
* Drag & drop controllers into the tldraw canvas.
* Controls retain **state** (e.g., checkbox checked, input text, dropdown selection).
* Controls are **resizable & draggable** like normal shapes.
* Real **HTML elements embedded in SVG** via `foreignObject`.
* Can be extended with new components easily.
---
## 📂 Project Structure
```
src/
├─ shapes/
│ ├─ ButtonShape.tsx
│ ├─ CheckboxShape.tsx
│ ├─ ComboBoxShape.tsx
│ ├─ DataGridShape.tsx
│ ├─ DatePickerShape.tsx
│ ├─ FormShape.tsx
│ ├─ InputShape.tsx
│ ├─ RadioGroupShape.tsx
│ ├─ TextAreaShape.tsx
│ └─ ToggleShape.tsx
├─ components/
│ └─ ControlsPalette.tsx
├─ App.tsx
└─ main.tsx
```
---
## ⚡ Installation
```bash
git clone https://github.com/your-org/tldraw-ui-controllers.git
cd tldraw-ui-controllers
npm install
npm run dev
```
Open [http://localhost:5173](http://localhost:5173) in your browser.
---
## 🛠️ Usage
### Adding a Control
Each control is implemented as a **custom shape**.
From the **palette sidebar**, you can click any control to insert it:
```tsx
editor.createShape({
type: "checkbox",
x: 200,
y: 200,
props: { checked: false, label: "Accept Terms" },
});
```
### Example: Checkbox Implementation
```tsx
type CheckboxShape = TLBaseShape<"checkbox", { checked: boolean; label: string }>;
class CheckboxShapeUtil extends ShapeUtil<CheckboxShape> {
static override type = "checkbox";
override render(shape: CheckboxShape) {
return (
<foreignObject width={200} height={40}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<input
type="checkbox"
checked={shape.props.checked}
onChange={(e) =>
this.editor.updateShape({
...shape,
props: { ...shape.props, checked: e.target.checked },
})
}
/>
<span>{shape.props.label}</span>
</div>
</foreignObject>
);
}
}
```
---
## 🎛️ Controllers
| Control | Description | Example Props |
| ------------------ | -------------------------- | ---------------------------------------------- |
| **Button** | Clickable button | `{ label: "Submit" }` |
| **Checkbox** | Standard checkbox | `{ checked: false, label: "Accept Terms" }` |
| **Radio Group** | Multiple exclusive options | `{ options: ["A", "B", "C"], selected: "A" }` |
| **Text Input** | Single-line input | `{ value: "", placeholder: "Enter text" }` |
| **Textarea** | Multi-line input | `{ value: "", placeholder: "Write here..." }` |
| **Toggle Switch** | On/Off toggle | `{ on: true }` |
| **Date Picker** | Calendar input | `{ date: "2025-09-01" }` |
| **ComboBox** | Dropdown list | `{ options: ["One", "Two"], selected: "One" }` |
| **Data Grid** | Simple editable table | `{ rows: [["A1","B1"],["A2","B2"]] }` |
| **Form Container** | Holds other shapes | `{ title: "User Form" }` |
---
## 🧩 Extending with New Controls
To add a new control:
1. Create a new `ShapeUtil` subclass in `src/shapes/`.
2. Use `<foreignObject>` to render any HTML element.
3. Update `App.tsx` to register it in `shapeUtils`.
4. Add it to the **ControlsPalette**.
---
## 📸 Preview
* Palette on the left with draggable controllers.
* tldraw canvas on the right.
* Controls behave just like Balsamiq but **real & interactive**.
---
Got it ✅ I see your **Prompt-to-Wireframe (tldraw)** app running locally — it already generates wireframes on the canvas. Now you want to **integrate the interactive controllers (button, forms, data grid, date picker, etc.)** into this environment.
Heres how you can integrate the two:
---
## 🔹 Integration Plan
1. **Extend your current tldraw setup**
* Right now your app renders `<Tldraw />` with AI-generated wireframes.
* Youll register your **10 custom controllers (shapes)** into the same editor.
2. **Add Controllers Palette**
* Create a sidebar/panel with the controllers (like Balsamiqs top bar).
* Each controller button inserts its shape into the tldraw canvas.
3. **Register Custom Shapes**
* In your `App.tsx` (or wherever `<Tldraw />` is rendered), pass `shapeUtils` with all the controllers you built:
```tsx
import {
ButtonShapeUtil,
CheckboxShapeUtil,
ComboBoxShapeUtil,
DataGridShapeUtil,
DatePickerShapeUtil,
FormShapeUtil,
InputShapeUtil,
RadioGroupShapeUtil,
TextAreaShapeUtil,
ToggleShapeUtil,
} from "./shapes";
<Tldraw shapeUtils={[
ButtonShapeUtil,
CheckboxShapeUtil,
ComboBoxShapeUtil,
DataGridShapeUtil,
DatePickerShapeUtil,
FormShapeUtil,
InputShapeUtil,
RadioGroupShapeUtil,
TextAreaShapeUtil,
ToggleShapeUtil,
]} />
```
4. **Connect Palette → Shape Creation**
Example for a button in your palette:
```tsx
function ControlsPalette({ editor }) {
return (
<div className="palette">
<button
onClick={() =>
editor.createShape({
type: "button",
x: 200,
y: 200,
props: { label: "Click Me" },
})
}
>
Button
</button>
</div>
);
}
```
Add similar buttons for checkbox, date picker, grid, etc.
5. **Combine With Prompt-to-Wireframe Flow**
* When your AI generates wireframes, they appear as usual.
* The user can then drag in **interactive controllers** to replace/augment them.
* Example: AI generates a rectangle with label "DATA TABLE" → user deletes it and inserts a real **DataGridShape**.
---
## 🔹 Updated Project Structure
```
src/
├─ shapes/ # all 10 controllers
│ ├─ ButtonShape.tsx
│ ├─ CheckboxShape.tsx
│ ├─ ...
├─ components/
│ ├─ ControlsPalette.tsx
│ └─ WireframeGenerator.tsx # your existing AI integration
├─ App.tsx
└─ main.tsx
```
---
## 🔹 User Flow After Integration
1. User enters a **prompt** → AI generates a wireframe layout (as in your screenshot).
2. User sees a **palette of interactive controllers**.
3. User drags/drops or clicks to insert **real interactive controls** (button, forms, date pickers, data grid).
4. Wireframe evolves into a **clickable mockup**, not just static boxes.
---
## 📜 License
MIT License © 2025
---
👉 Do you want me to **include example code for all 10 controllers in the README** (full implementations), or just keep this README as a **setup + usage guide** and document the shape types in a separate file?

View File

@ -0,0 +1,214 @@
# Wireframe Persistence System
This document explains the new wireframe persistence system that automatically saves and loads wireframes to prevent data loss on page refresh.
## Overview
The wireframe persistence system consists of:
1. **PostgreSQL Database Schema** - Stores wireframes, elements, and versions
2. **Backend API Endpoints** - Handle CRUD operations for wireframes
3. **Frontend Auto-save** - Automatically saves wireframes every 30 seconds
4. **Manual Save Controls** - Manual save button and keyboard shortcuts
## Database Schema
### Tables Created
1. **`wireframes`** - Main wireframe metadata
- `id` - Unique identifier
- `user_id` - Reference to user
- `project_id` - Optional project reference
- `name` - Wireframe name
- `description` - Wireframe description
- `device_type` - mobile/tablet/desktop
- `dimensions` - Width and height
- `metadata` - Additional data (prompt, generation settings)
- `is_active` - Soft delete flag
2. **`wireframe_elements`** - Individual shapes/elements
- `id` - Element identifier
- `wireframe_id` - Reference to wireframe
- `element_type` - Type of element (shape, text, image, group)
- `element_data` - Complete TLDraw element data
- `position` - X, Y coordinates
- `size` - Width and height
- `style` - Color, stroke width, fill
- `parent_id` - For grouped elements
- `z_index` - Layering order
3. **`wireframe_versions`** - Version control
- `id` - Version identifier
- `wireframe_id` - Reference to wireframe
- `version_number` - Sequential version number
- `version_name` - Human-readable version name
- `snapshot_data` - Complete wireframe state at version
- `created_by` - User who created version
## Setup Instructions
### 1. Database Setup
```bash
# Install PostgreSQL dependencies
cd backend
pip install -r requirements.txt
# Copy and configure environment variables
cp env.example .env
# Edit .env with your database credentials
# Run database setup script
python setup_database.py
```
### 2. Environment Variables
Create a `.env` file in the `backend/` directory:
```env
# Claude API Configuration
CLAUDE_API_KEY=your-claude-api-key-here
# Flask Configuration
FLASK_ENV=development
PORT=5000
# Database Configuration
DB_HOST=localhost
DB_NAME=tech4biz_wireframes
DB_USER=postgres
DB_PASSWORD=your-database-password
DB_PORT=5432
```
### 3. Start Backend
```bash
cd backend
python app.py
```
## API Endpoints
### Save Wireframe
```http
POST /api/wireframes
Content-Type: application/json
{
"wireframe": {
"name": "Wireframe Name",
"description": "Description",
"device_type": "desktop",
"dimensions": {"width": 1440, "height": 1024},
"metadata": {"prompt": "User prompt"}
},
"elements": [...],
"user_id": "user-uuid",
"project_id": "project-uuid"
}
```
### Get Wireframe
```http
GET /api/wireframes/{wireframe_id}
```
### Update Wireframe
```http
PUT /api/wireframes/{wireframe_id}
Content-Type: application/json
{
"name": "Updated Name",
"description": "Updated Description",
"elements": [...],
"user_id": "user-uuid"
}
```
### Delete Wireframe
```http
DELETE /api/wireframes/{wireframe_id}
```
### Get User Wireframes
```http
GET /api/wireframes/user/{user_id}
```
## Frontend Features
### Auto-save
- Wireframes are automatically saved every 30 seconds
- Auto-save can be toggled on/off
- Last save time is displayed
### Manual Save
- Manual save button in top-right corner
- Keyboard shortcut: `Ctrl+S` (or `Cmd+S` on Mac)
### Save Status
- Green indicator shows last save time
- Auto-save toggle checkbox
- Manual save button
## Usage
### Creating Wireframes
1. Generate wireframe using AI prompt
2. Wireframe is automatically saved to database
3. Continue editing - changes are auto-saved
### Loading Wireframes
1. Wireframes are automatically loaded on page refresh
2. Use API endpoints to load specific wireframes
3. Version history is maintained
### Keyboard Shortcuts
- `Ctrl+S` - Save wireframe
- `Ctrl+K` - Trigger prompt input (planned)
- `Ctrl+Delete` - Clear canvas
## Data Flow
1. **User creates/edits wireframe** → TLDraw editor
2. **Auto-save triggers** → Every 30 seconds
3. **Data serialized** → Convert TLDraw shapes to database format
4. **API call** → Send to backend
5. **Database storage** → Save to PostgreSQL
6. **Version created** → New version entry for tracking
## Benefits
- **No data loss** on page refresh
- **Automatic backup** every 30 seconds
- **Version control** for wireframe changes
- **User isolation** - each user sees only their wireframes
- **Project organization** - wireframes can be grouped by project
- **Scalable storage** - PostgreSQL handles large wireframes efficiently
## Troubleshooting
### Database Connection Issues
- Check PostgreSQL is running
- Verify database credentials in `.env`
- Ensure database `tech4biz_wireframes` exists
### Auto-save Not Working
- Check browser console for errors
- Verify backend is running on correct port
- Check network tab for failed API calls
### Wireframes Not Loading
- Check if wireframe exists in database
- Verify user_id matches
- Check API endpoint responses
## Future Enhancements
- **Real-time collaboration** - Multiple users editing same wireframe
- **Export formats** - PNG, PDF, HTML export
- **Template library** - Reusable wireframe components
- **Advanced versioning** - Branch and merge wireframes
- **Search and filtering** - Find wireframes by content or metadata

View File

@ -0,0 +1,9 @@
flask==3.0.0
flask-cors==4.0.0
anthropic
python-dotenv==1.0.0
psycopg2-binary==2.9.9
requests==2.31.0
gunicorn==21.2.0
PyJWT==2.8.0
cryptography==41.0.7

View File

@ -0,0 +1,90 @@
@echo off
echo 🚀 Quick Start - AI Wireframe Generator
echo ======================================
echo.
echo 📋 Checking prerequisites...
echo.
REM Check if Python is installed
python --version >nul 2>&1
if errorlevel 1 (
echo ❌ Python is not installed or not in PATH
echo Please install Python 3.8+ and try again
pause
exit /b 1
)
REM Check if Node.js is installed
node --version >nul 2>&1
if errorlevel 1 (
echo ❌ Node.js is not installed or not in PATH
echo Please install Node.js 18+ and try again
pause
exit /b 1
)
echo ✅ Python and Node.js are installed
echo.
echo 🔧 Setting up backend...
cd backend
REM Check if .env exists
if not exist .env (
echo 📝 Creating .env file...
copy env.example .env
echo ⚠️ Please edit .env and add your Claude API key
echo Then restart this script
pause
exit /b 1
)
REM Check if requirements are installed
pip show flask >nul 2>&1
if errorlevel 1 (
echo 📦 Installing Python dependencies...
pip install -r requirements.txt
if errorlevel 1 (
echo ❌ Failed to install dependencies
pause
exit /b 1
)
)
echo ✅ Backend setup complete
echo.
echo 🚀 Starting backend in background...
start "Flask Backend" cmd /k "python run.py"
echo ⏳ Waiting for backend to start...
timeout /t 5 /nobreak >nul
echo 🌐 Backend should be running on http://localhost:5000
echo.
echo 🚀 Starting frontend...
cd ..
start "Next.js Frontend" cmd /k "npm run dev"
echo.
echo 🎉 Both services are starting!
echo.
echo 📱 Frontend: http://localhost:3000
echo 🔧 Backend: http://localhost:5000
echo.
echo 💡 Tips:
echo - Wait for both services to fully start
echo - Check the right sidebar for backend status
echo - Try generating a wireframe with AI
echo.
echo Press any key to open the frontend in your browser...
pause >nul
start http://localhost:3000
echo.
echo 🎨 Happy wireframing! The AI will help you create professional layouts.
echo.
pause

View File

@ -0,0 +1,96 @@
#!/bin/bash
echo "🚀 Quick Start - AI Wireframe Generator"
echo "======================================"
echo
echo "📋 Checking prerequisites..."
echo
# Check if Python is installed
if ! command -v python3 &> /dev/null; then
echo "❌ Python 3 is not installed or not in PATH"
echo "Please install Python 3.8+ and try again"
exit 1
fi
# Check if Node.js is installed
if ! command -v node &> /dev/null; then
echo "❌ Node.js is not installed or not in PATH"
echo "Please install Node.js 18+ and try again"
exit 1
fi
echo "✅ Python and Node.js are installed"
echo
echo "🔧 Setting up backend..."
cd backend
# Check if .env exists
if [ ! -f .env ]; then
echo "📝 Creating .env file..."
cp env.example .env
echo "⚠️ Please edit .env and add your Claude API key"
echo " Then restart this script"
exit 1
fi
# Check if requirements are installed
if ! python3 -c "import flask" &> /dev/null; then
echo "📦 Installing Python dependencies..."
pip3 install -r requirements.txt
if [ $? -ne 0 ]; then
echo "❌ Failed to install dependencies"
exit 1
fi
fi
echo "✅ Backend setup complete"
echo
echo "🚀 Starting backend in background..."
python3 run.py &
BACKEND_PID=$!
echo "⏳ Waiting for backend to start..."
sleep 5
echo "🌐 Backend should be running on http://localhost:5000"
echo
echo "🚀 Starting frontend..."
cd ..
npm run dev &
FRONTEND_PID=$!
echo
echo "🎉 Both services are starting!"
echo
echo "📱 Frontend: http://localhost:3000"
echo "🔧 Backend: http://localhost:5000"
echo
echo "💡 Tips:"
echo " - Wait for both services to fully start"
echo " - Check the right sidebar for backend status"
echo " - Try generating a wireframe with AI"
echo
# Function to cleanup background processes
cleanup() {
echo
echo "🛑 Stopping services..."
kill $BACKEND_PID 2>/dev/null
kill $FRONTEND_PID 2>/dev/null
echo "✅ Services stopped"
exit 0
}
# Set trap to cleanup on script exit
trap cleanup SIGINT SIGTERM
echo "Press Ctrl+C to stop both services"
echo
# Wait for user to stop
wait

View File

@ -0,0 +1,102 @@
import { Editor, createShapeId } from "@tldraw/tldraw"
// Test function to find the correct tldraw v3 properties
export function testTldrawProps(editor: Editor) {
try {
// Test 1: Basic rectangle with minimal properties
const rectId1 = createShapeId()
editor.createShape({
id: rectId1,
type: "geo",
x: 100,
y: 100,
props: {
w: 100,
h: 100,
geo: "rectangle",
},
})
console.log("✅ Basic rectangle created successfully")
// Test 2: Rectangle with fill
const rectId2 = createShapeId()
editor.createShape({
id: rectId2,
type: "geo",
x: 250,
y: 100,
props: {
w: 100,
h: 100,
geo: "rectangle",
fill: "none",
},
})
console.log("✅ Rectangle with fill created successfully")
// Test 3: Rectangle with color
const rectId3 = createShapeId()
editor.createShape({
id: rectId3,
type: "geo",
x: 400,
y: 100,
props: {
w: 100,
h: 100,
geo: "rectangle",
fill: "none",
color: "black",
},
})
console.log("✅ Rectangle with color created successfully")
// Test 4: Text with minimal properties
const textId1 = createShapeId()
editor.createShape({
id: textId1,
type: "text",
x: 100,
y: 250,
props: {
text: "Test Text",
},
})
console.log("✅ Basic text created successfully")
// Test 5: Text with size
const textId2 = createShapeId()
editor.createShape({
id: textId2,
type: "text",
x: 250,
y: 250,
props: {
text: "Test Text",
w: 100,
h: 50,
},
})
console.log("✅ Text with size created successfully")
// Test 6: Text with font properties
const textId3 = createShapeId()
editor.createShape({
id: textId3,
type: "text",
x: 400,
y: 250,
props: {
text: "Test Text",
w: 100,
h: 50,
fontSize: 16,
color: "black",
},
})
console.log("✅ Text with font properties created successfully")
} catch (error) {
console.error("❌ Error creating shape:", error)
}
}

View File

@ -0,0 +1,399 @@
# Prompt to Wireframe - Backend
A Flask-based backend service that generates SVG wireframes from natural language prompts using Claude AI. The system converts user descriptions into precise, scalable vector graphics that can be rendered directly in the frontend.
## 🚀 Features
- **AI-Powered Generation**: Uses Claude AI to analyze prompts and create wireframe layouts
- **SVG Output**: Generates precise SVG wireframes with proper positioning and styling
- **Flexible Response Types**: Supports both SVG and JSON responses for compatibility
- **Real-time Processing**: Fast wireframe generation with minimal latency
- **Scalable Architecture**: Built with Flask for easy deployment and scaling
## 🏗️ Architecture
### Backend Stack
- **Flask 3.0** - Web framework
- **Claude AI** - Natural language processing
- **SVG Generation** - Vector graphics creation
- **Python 3.9+** - Runtime environment
### Response System
The backend can generate two types of responses:
1. **SVG Response** (Primary)
- Direct SVG content
- Precise positioning and styling
- Better frontend rendering performance
2. **JSON Response** (Fallback)
- Structured wireframe specifications
- Compatible with existing frontend logic
- Used when SVG generation fails
## 📁 Project Structure
```
backend/
├── app.py # Main Flask application
├── requirements.txt # Python dependencies
├── run.py # Application entry point
├── env.example # Environment variables template
├── start_backend.bat # Windows startup script
├── start_backend.sh # Unix startup script
└── test_api.py # API testing script
```
## 🔧 Installation
### Prerequisites
- Python 3.9 or higher
- pip package manager
- Claude AI API access
### Setup Steps
1. **Clone the repository**
```bash
git clone <repository-url>
cd wireframe-tool/tldraw-editor/backend
```
2. **Create virtual environment**
```bash
python -m venv venv
# Windows
venv\Scripts\activate
# Unix/Mac
source venv/bin/activate
```
3. **Install dependencies**
```bash
pip install -r requirements.txt
```
4. **Configure environment**
```bash
cp env.example .env
# Edit .env with your Claude AI API key
```
5. **Start the server**
```bash
# Windows
start_backend.bat
# Unix/Mac
./start_backend.sh
# Or directly
python run.py
```
## 🔌 API Endpoints
### Generate Wireframe
**POST** `/generate-wireframe`
Generates a wireframe from a natural language prompt.
#### Request Body
```json
{
"prompt": "Dashboard with header, sidebar, and 3 stats cards"
}
```
#### Response Types
**SVG Response** (Preferred)
```
Content-Type: image/svg+xml
<svg width="800" height="600" viewBox="0 0 800 600">
<!-- SVG wireframe content -->
</svg>
```
**JSON Response** (Fallback)
```
Content-Type: application/json
{
"success": true,
"wireframe": {
"layout": { ... },
"styling": { ... },
"annotations": { ... }
}
}
```
### Health Check
**GET** `/health`
Returns server status and health information.
## 🎯 SVG Generation
### SVG Structure
The generated SVGs follow a consistent structure:
```xml
<svg width="800" height="600" viewBox="0 0 800 600">
<defs>
<!-- Filters, gradients, and definitions -->
</defs>
<g>
<!-- Header section -->
<rect x="0" y="0" width="800" height="60" fill="#f0f0f0"/>
<text x="20" y="35">Header</text>
<!-- Sidebar -->
<rect x="0" y="60" width="200" height="540" fill="#e0e0e0"/>
<!-- Main content -->
<rect x="220" y="60" width="580" height="540" fill="#ffffff"/>
<!-- Content elements -->
<rect x="240" y="80" width="160" height="120" fill="#f8f9fa"/>
<text x="250" y="100">Stats Card 1</text>
</g>
</svg>
```
### Element Types Supported
- **Rectangles**: Header, sidebar, content areas, cards
- **Text**: Labels, titles, descriptions
- **Groups**: Logical sections and containers
- **Paths**: Complex shapes and icons
- **Circles/Ellipses**: Icons and decorative elements
## 🤖 AI Integration
### Claude AI Processing
The backend uses Claude AI to:
1. **Analyze Prompts**: Understand user requirements
2. **Generate Layouts**: Create logical wireframe structures
3. **Apply UX Principles**: Follow design best practices
4. **Output SVG**: Generate precise vector graphics
### Prompt Processing Flow
```
User Prompt → Claude AI → Layout Analysis → SVG Generation → Response
```
### Example Prompts
- "Dashboard with header, left sidebar, 3 stats cards, line chart, and footer"
- "Landing page with hero section, feature grid, and contact form"
- "E-commerce product page with image gallery and product details"
## 🔧 Configuration
### Environment Variables
```bash
# Claude AI Configuration
CLAUDE_API_KEY=your_api_key_here
CLAUDE_MODEL=claude-3-sonnet-20240229
# Server Configuration
FLASK_ENV=development
FLASK_DEBUG=True
PORT=5000
# CORS Configuration
CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
```
### API Configuration
```python
# app.py
app.config['CLAUDE_API_KEY'] = os.getenv('CLAUDE_API_KEY')
app.config['CLAUDE_MODEL'] = os.getenv('CLAUDE_MODEL', 'claude-3-sonnet-20240229')
app.config['MAX_PROMPT_LENGTH'] = 1000
```
## 🚀 Deployment
### Development
```bash
python run.py
# Server runs on http://localhost:5000
```
### Production
```bash
# Using Gunicorn
gunicorn -w 4 -b 0.0.0.0:5000 run:app
# Using uWSGI
uwsgi --http :5000 --module run:app
```
### Docker Deployment
```dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "run.py"]
```
## 🧪 Testing
### API Testing
```bash
python test_api.py
```
### Manual Testing
```bash
# Test with curl
curl -X POST http://localhost:5000/generate-wireframe \
-H "Content-Type: application/json" \
-d '{"prompt": "Simple dashboard with header and sidebar"}'
# Test health endpoint
curl http://localhost:5000/health
```
### Load Testing
```bash
# Using Apache Bench
ab -n 100 -c 10 -p test_data.json \
-T application/json \
http://localhost:5000/generate-wireframe
```
## 📊 Monitoring
### Health Checks
- Server status monitoring
- API response time tracking
- Error rate monitoring
- Resource usage tracking
### Logging
```python
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@app.route('/generate-wireframe', methods=['POST'])
def generate_wireframe():
logger.info(f"Received prompt: {request.json.get('prompt', '')[:100]}...")
# ... processing logic
```
## 🔒 Security
### API Key Management
- Secure storage of Claude AI API keys
- Environment variable protection
- Key rotation support
### Input Validation
- Prompt length limits
- Content sanitization
- Rate limiting support
### CORS Configuration
```python
from flask_cors import CORS
CORS(app, origins=os.getenv('CORS_ORIGINS', '').split(','))
```
## 🐛 Troubleshooting
### Common Issues
1. **Claude AI API Errors**
- Verify API key is valid
- Check API quota and limits
- Ensure proper model access
2. **SVG Generation Failures**
- Check prompt complexity
- Verify SVG output format
- Review error logs
3. **Performance Issues**
- Monitor response times
- Check server resources
- Optimize AI model usage
### Debug Mode
Enable debug logging:
```python
app.config['DEBUG'] = True
logging.getLogger().setLevel(logging.DEBUG)
```
## 📈 Performance Optimization
### Caching Strategies
- Response caching for similar prompts
- SVG template caching
- AI response caching
### Async Processing
- Background SVG generation
- Queue-based processing
- WebSocket updates
### Resource Management
- Connection pooling
- Memory optimization
- CPU usage monitoring
## 🔮 Future Enhancements
### Planned Features
- **Template System**: Pre-built wireframe templates
- **Custom Styling**: User-defined color schemes
- **Export Options**: PNG, PDF, and other formats
- **Collaboration**: Real-time editing and sharing
- **Version Control**: Wireframe history and branching
### Scalability Improvements
- **Microservices**: Separate AI and SVG services
- **Load Balancing**: Multiple backend instances
- **CDN Integration**: Global content delivery
- **Database Storage**: Wireframe persistence
## 🤝 Contributing
1. Fork the repository
2. Create a feature branch
3. Implement your changes
4. Add tests and documentation
5. Submit a pull request
### Development Guidelines
- Follow PEP 8 style guidelines
- Add type hints for new functions
- Include docstrings for all functions
- Write tests for new features
## 📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
## 🆘 Support
For support and questions:
- Create an issue in the repository
- Check the troubleshooting section
- Review the frontend documentation
- Contact the development team
---
**Note**: This backend service is designed to work with the Prompt to Wireframe frontend application, providing SVG wireframe generation capabilities through Claude AI integration.

View File

@ -0,0 +1,183 @@
# 🚀 Quick Setup Guide
## Prerequisites
- **Python 3.8+** installed on your system
- **Claude API key** from Anthropic
- **Git** (optional, for cloning)
## 🎯 Step-by-Step Setup
### 1. Get Your Claude API Key
1. Go to [Anthropic Console](https://console.anthropic.com/)
2. Sign up/Login and create an API key
3. Copy your API key (starts with `sk-ant-api03-...`)
### 2. Install Python Dependencies
```bash
cd backend
pip install -r requirements.txt
```
### 3. Configure Environment
```bash
# Copy the example environment file
cp env.example .env
# Edit .env and add your API key
# Replace "your-claude-api-key-here" with your actual key
```
**Example .env file:**
```env
CLAUDE_API_KEY=sk-ant-api03-your-actual-key-here
FLASK_ENV=development
PORT=5000
```
### 4. Start the Backend
#### **Windows Users:**
```bash
# Double-click start_backend.bat
# OR run in command prompt:
start_backend.bat
```
#### **Mac/Linux Users:**
```bash
# Make script executable
chmod +x start_backend.sh
# Run the script
./start_backend.sh
```
#### **Manual Start:**
```bash
python run.py
```
### 5. Verify Backend is Running
- Backend should start on `http://localhost:5000`
- You should see: "🌐 Backend starting on http://localhost:5000"
- Frontend can now connect to this backend
## 🧪 Testing the Backend
### Run the Test Suite
```bash
python test_api.py
```
This will test:
- ✅ Health endpoint
- ✅ Wireframe generation
- ✅ Error handling
- ✅ API responses
### Manual API Testing
```bash
# Health check
curl http://localhost:5000/api/health
# Generate wireframe
curl -X POST http://localhost:5000/api/generate-wireframe \
-H "Content-Type: application/json" \
-d '{"prompt": "Dashboard with header and sidebar"}'
```
## 🔧 Troubleshooting
### Common Issues
#### **"Module not found" errors**
```bash
# Reinstall dependencies
pip install -r requirements.txt --force-reinstall
```
#### **"Cannot connect to backend"**
- Check if backend is running on port 5000
- Verify no firewall blocking the port
- Check console for error messages
#### **"Claude API key not configured"**
- Ensure `.env` file exists in backend folder
- Verify API key is correct and not placeholder text
- Restart backend after changing `.env`
#### **Port already in use**
```bash
# Find process using port 5000
netstat -ano | findstr :5000 # Windows
lsof -i :5000 # Mac/Linux
# Kill the process or change port in .env
```
### Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `CLAUDE_API_KEY` | Your Anthropic API key | Required |
| `PORT` | Backend port | 5000 |
| `FLASK_ENV` | Flask environment | development |
## 📱 Frontend Integration
Once backend is running, your Next.js frontend can:
1. **Send prompts** to `/api/generate-wireframe`
2. **Receive structured wireframe data** in JSON format
3. **Render wireframes** using the AI-generated specifications
4. **Handle errors** gracefully with fallback options
## 🎨 API Response Format
The backend returns structured wireframe data:
```json
{
"success": true,
"wireframe": {
"layout": {
"page": {"width": 1200, "height": 800},
"header": {"enabled": true, "height": 72},
"sidebar": {"enabled": true, "width": 240},
"main_content": {
"sections": [
{
"type": "grid",
"rows": 2,
"cols": 3,
"height": 200
}
]
},
"footer": {"enabled": true, "height": 64}
},
"styling": {
"theme": "modern",
"colors": {"primary": "#3B82F6"},
"spacing": {"gap": 16, "padding": 20}
}
}
}
```
## 🚀 Next Steps
1. **Test the backend** with `python test_api.py`
2. **Modify the frontend** to use the new API
3. **Customize wireframe generation** by editing `app.py`
4. **Add more AI features** like layout optimization
5. **Deploy to production** with proper environment setup
## 📞 Support
If you encounter issues:
1. Check the console output for error messages
2. Verify your Claude API key is valid
3. Ensure all dependencies are installed
4. Check if port 5000 is available
Happy wireframing! 🎨✨

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,17 @@
# Claude API Configuration
CLAUDE_API_KEY=your-claude-api-key-here
# Flask Configuration
FLASK_ENV=development
PORT=5000
# Database Configuration
POSTGRES_HOST=postgres
POSTGRES_DB=dev_pipeline
POSTGRES_USER=pipeline_admin
POSTGRES_PASSWORD=secure_pipeline_2024
POSTGRES_PORT=5433
# JWT Configuration
JWT_SECRET=access-secret-key-2024-tech4biz
JWT_ALGORITHM=HS256

View File

@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""
Database migration script to fix TLDraw ID issues
"""
import os
import psycopg2
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
def migrate_database():
"""Migrate the database to fix TLDraw ID issues"""
# Database connection details
db_host = os.getenv('POSTGRES_HOST', 'localhost')
db_user = os.getenv('POSTGRES_USER', 'pipeline_admin')
db_password = os.getenv('POSTGRES_PASSWORD', 'secure_pipeline_2024')
db_port = os.getenv('POSTGRES_PORT', '5433')
db_name = os.getenv('POSTGRES_DB', 'dev_pipeline')
try:
conn = psycopg2.connect(
host=db_host,
user=db_user,
password=db_password,
port=db_port,
database=db_name
)
with conn.cursor() as cur:
print("🔄 Migrating database to fix TLDraw ID issues...")
# Check if tldraw_id column exists
cur.execute("""
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'wireframe_elements'
AND column_name = 'tldraw_id'
""")
if not cur.fetchone():
print(" Adding tldraw_id column...")
cur.execute("ALTER TABLE wireframe_elements ADD COLUMN tldraw_id VARCHAR(255)")
# Check if parent_id is already VARCHAR
cur.execute("""
SELECT data_type
FROM information_schema.columns
WHERE table_name = 'wireframe_elements'
AND column_name = 'parent_id'
""")
column_info = cur.fetchone()
if column_info and column_info[0] == 'uuid':
print(" Converting parent_id from UUID to VARCHAR...")
# Drop the foreign key constraint first
cur.execute("""
ALTER TABLE wireframe_elements
DROP CONSTRAINT IF EXISTS wireframe_elements_parent_id_fkey
""")
# Change the column type
cur.execute("""
ALTER TABLE wireframe_elements
ALTER COLUMN parent_id TYPE VARCHAR(255)
""")
# Update the function
print(" Updating get_wireframe_with_elements function...")
cur.execute("""
CREATE OR REPLACE FUNCTION get_wireframe_with_elements(p_wireframe_id UUID)
RETURNS TABLE(
wireframe_data JSONB,
elements_data JSONB
) AS $$
BEGIN
RETURN QUERY
SELECT
to_jsonb(w.*) as wireframe_data,
COALESCE(
jsonb_agg(
jsonb_build_object(
'id', we.id,
'tldraw_id', we.tldraw_id,
'type', we.element_type,
'data', we.element_data,
'position', we.position,
'size', we.size,
'style', we.style,
'parent_id', we.parent_id,
'z_index', we.z_index
) ORDER BY we.z_index, we.created_at
) FILTER (WHERE we.id IS NOT NULL),
'[]'::jsonb
) as elements_data
FROM wireframes w
LEFT JOIN wireframe_elements we ON w.id = we.wireframe_id
WHERE w.id = p_wireframe_id
GROUP BY w.id, w.user_id, w.project_id, w.name, w.description,
w.device_type, w.dimensions, w.metadata, w.is_active,
w.created_at, w.updated_at;
END;
$$ LANGUAGE plpgsql;
""")
conn.commit()
print("✅ Database migration completed successfully!")
conn.close()
return True
except Exception as e:
print(f"❌ Database migration failed: {e}")
return False
if __name__ == "__main__":
print("🚀 Starting database migration...")
success = migrate_database()
if success:
print("\n✅ Migration completed successfully!")
print("The database now supports TLDraw string IDs properly.")
else:
print("\n❌ Migration failed!")
print("Please check the database connection and try again.")

View File

@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""
Startup script for the Wireframe Generator Backend
"""
import os
import sys
from pathlib import Path
def check_dependencies():
"""Check if required packages are installed"""
try:
import flask
import anthropic
import dotenv
print("✅ All dependencies are installed")
return True
except ImportError as e:
print(f"❌ Missing dependency: {e}")
print("Please run: pip install -r requirements.txt")
return False
def check_env_file():
"""Check if .env file exists and has API key"""
env_path = Path(".env")
if not env_path.exists():
print("⚠️ No .env file found")
print("Please copy env.example to .env and add your Claude API key")
return False
# Check if API key is set
from dotenv import load_dotenv
load_dotenv()
api_key = os.getenv("CLAUDE_API_KEY")
if not api_key or api_key == "your-claude-api-key-here":
print("⚠️ Claude API key not configured")
print("Please add your actual API key to the .env file")
return False
print("✅ Environment configured")
return True
def main():
"""Main startup function"""
print("🚀 Starting Wireframe Generator Backend...")
# Check dependencies
if not check_dependencies():
sys.exit(1)
# Check environment
if not check_env_file():
print("\nTo continue without API key (fallback mode), press Enter...")
input()
# Import and run the app
try:
from app import app
port = int(os.environ.get('PORT', 5000))
print(f"🌐 Backend starting on http://localhost:{port}")
print("📱 Frontend can connect to this backend")
print("🔄 Press Ctrl+C to stop the server")
app.run(debug=True, host='0.0.0.0', port=port)
except Exception as e:
print(f"❌ Failed to start backend: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,97 @@
#!/usr/bin/env python3
"""
Database setup script for Tech4biz Wireframe Generator
This script creates the database and runs the schema files
"""
import os
import psycopg2
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
from dotenv import load_dotenv
def setup_database():
"""Setup the database and create tables"""
# Load environment variables
load_dotenv()
# Database connection details
db_host = os.getenv('POSTGRES_HOST', 'localhost')
db_user = os.getenv('POSTGRES_USER', 'pipeline_admin')
db_password = os.getenv('POSTGRES_PASSWORD', 'secure_pipeline_2024')
db_port = os.getenv('POSTGRES_PORT', '5432') # Changed to 5432 for Docker
db_name = os.getenv('POSTGRES_DB', 'dev_pipeline')
# First connect to postgres to create database
try:
conn = psycopg2.connect(
host=db_host,
user=db_user,
password=db_password,
port=db_port,
database='postgres' # Connect to default postgres database first
)
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
with conn.cursor() as cur:
# Check if database exists
cur.execute("SELECT 1 FROM pg_database WHERE datname = %s", (db_name,))
if not cur.fetchone():
print(f"Creating database '{db_name}'...")
cur.execute(f"CREATE DATABASE {db_name}")
print("Database created successfully!")
else:
print(f"Database '{db_name}' already exists")
conn.close()
except Exception as e:
print(f"Error creating database: {e}")
return False
# Now connect to the new database and run schema files
try:
conn = psycopg2.connect(
host=db_host,
user=db_user,
password=db_password,
port=db_port,
database=db_name
)
with conn.cursor() as cur:
print("Running user authentication schema...")
schema_file = os.path.join(os.path.dirname(__file__), 'sql', '001_user_auth_schema.sql')
with open(schema_file, 'r') as f:
schema_sql = f.read()
cur.execute(schema_sql)
print("Running wireframe schema...")
schema_file = os.path.join(os.path.dirname(__file__), 'sql', '002_wireframe_schema.sql')
with open(schema_file, 'r') as f:
schema_sql = f.read()
cur.execute(schema_sql)
conn.commit()
print("Database setup completed successfully!")
conn.close()
return True
except Exception as e:
print(f"Error setting up database: {e}")
return False
if __name__ == "__main__":
print("Setting up Tech4biz Wireframe Generator database...")
success = setup_database()
if success:
print("\n✅ Database setup completed successfully!")
print("\nNext steps:")
print("1. Make sure your .env file has the correct database credentials")
print("2. Start the backend server with: python app.py")
print("3. The wireframes will now be automatically saved and loaded!")
else:
print("\n❌ Database setup failed!")
print("Please check your database connection settings and try again.")

View File

@ -0,0 +1,190 @@
-- User Authentication Database Schema
-- JWT-based authentication with user preferences for template features
-- Drop tables if they exist (for development)
DROP TABLE IF EXISTS user_feature_preferences CASCADE;
DROP TABLE IF EXISTS user_sessions CASCADE;
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 (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
first_name VARCHAR(100),
last_name VARCHAR(100),
role VARCHAR(20) DEFAULT 'user' CHECK (role IN ('user', 'admin', 'moderator')),
email_verified BOOLEAN DEFAULT false,
is_active BOOLEAN DEFAULT true,
last_login TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Refresh tokens table - JWT refresh token management
CREATE TABLE refresh_tokens (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
token_hash VARCHAR(255) NOT NULL,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
revoked_at TIMESTAMP,
is_revoked BOOLEAN DEFAULT false
);
-- User sessions table - Track user activity and sessions
CREATE TABLE user_sessions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
session_token VARCHAR(255) UNIQUE,
ip_address INET,
user_agent TEXT,
device_info JSONB,
is_active BOOLEAN DEFAULT true,
last_activity TIMESTAMP DEFAULT NOW(),
created_at TIMESTAMP DEFAULT NOW(),
expires_at TIMESTAMP DEFAULT NOW() + INTERVAL '30 days'
);
-- User feature preferences table - Track which features users have removed/customized
CREATE TABLE user_feature_preferences (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
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
preference_type VARCHAR(20) NOT NULL CHECK (preference_type IN ('removed', 'added', 'customized')),
custom_data JSONB, -- For storing custom feature modifications
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
CREATE TABLE user_projects (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
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
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);
CREATE INDEX idx_users_active ON users(is_active);
CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id);
CREATE INDEX idx_refresh_tokens_expires_at ON refresh_tokens(expires_at);
CREATE INDEX idx_refresh_tokens_revoked ON refresh_tokens(is_revoked);
CREATE INDEX idx_user_sessions_user_id ON user_sessions(user_id);
CREATE INDEX idx_user_sessions_active ON user_sessions(is_active);
CREATE INDEX idx_user_sessions_token ON user_sessions(session_token);
CREATE INDEX idx_user_feature_preferences_user_id ON user_feature_preferences(user_id);
CREATE INDEX idx_user_feature_preferences_template ON user_feature_preferences(template_type);
CREATE INDEX idx_user_projects_user_id ON user_projects(user_id);
CREATE INDEX idx_user_projects_active ON user_projects(is_active);
-- Update timestamps trigger function (reuse from template-manager)
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
-- Apply triggers for updated_at columns
CREATE TRIGGER update_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_user_feature_preferences_updated_at
BEFORE UPDATE ON user_feature_preferences
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_user_projects_updated_at
BEFORE UPDATE ON user_projects
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- Functions for cleanup and maintenance
CREATE OR REPLACE FUNCTION cleanup_expired_tokens()
RETURNS INTEGER AS $$
DECLARE
deleted_count INTEGER;
BEGIN
DELETE FROM refresh_tokens
WHERE expires_at < NOW() OR is_revoked = true;
GET DIAGNOSTICS deleted_count = ROW_COUNT;
RETURN deleted_count;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION cleanup_inactive_sessions()
RETURNS INTEGER AS $$
DECLARE
deleted_count INTEGER;
BEGIN
UPDATE user_sessions
SET is_active = false
WHERE expires_at < NOW() OR last_activity < NOW() - INTERVAL '7 days';
GET DIAGNOSTICS deleted_count = ROW_COUNT;
RETURN deleted_count;
END;
$$ LANGUAGE plpgsql;
-- Insert initial admin user (password: admin123 - change in production!)
INSERT INTO users (
id, 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'
'System',
'Administrator',
'admin',
true,
true
) ON CONFLICT (email) DO NOTHING;
-- Insert test user for development
INSERT INTO users (
id, 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'
'Test',
'User',
'user',
true,
true
) ON CONFLICT (email) DO NOTHING;
-- Success message
SELECT 'User Authentication database schema created successfully!' as message;
-- Display created tables
SELECT
schemaname,
tablename,
tableowner
FROM pg_tables
WHERE schemaname = 'public'
AND tablename IN ('users', 'refresh_tokens', 'user_sessions', 'user_feature_preferences', 'user_projects')
ORDER BY tablename;

View File

@ -0,0 +1,164 @@
-- Wireframe Storage Database Schema
-- Extends the user authentication schema to store wireframe data
-- Drop tables if they exist (for development)
DROP TABLE IF EXISTS wireframe_versions CASCADE;
DROP TABLE IF EXISTS wireframe_elements CASCADE;
DROP TABLE IF EXISTS wireframes CASCADE;
-- Wireframes table - Main wireframe storage
CREATE TABLE wireframes (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
project_id UUID REFERENCES user_projects(id) ON DELETE SET NULL,
name VARCHAR(200) NOT NULL,
description TEXT,
device_type VARCHAR(20) DEFAULT 'desktop' CHECK (device_type IN ('mobile', 'tablet', 'desktop')),
dimensions JSONB NOT NULL, -- {width: number, height: number}
metadata JSONB, -- Additional metadata like prompt, generation settings
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Wireframe elements table - Store individual elements/shapes
CREATE TABLE wireframe_elements (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
wireframe_id UUID REFERENCES wireframes(id) ON DELETE CASCADE,
element_type VARCHAR(50) NOT NULL, -- 'shape', 'text', 'image', 'group'
element_data JSONB NOT NULL, -- TLDraw element data
position JSONB NOT NULL, -- {x: number, y: number}
size JSONB, -- {width: number, height: number}
style JSONB, -- {color, strokeWidth, etc.}
parent_id VARCHAR(255), -- TLDraw uses string IDs like "page:page", not UUIDs
tldraw_id VARCHAR(255), -- Store the original TLDraw ID
z_index INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Wireframe versions table - Version control for wireframes
CREATE TABLE wireframe_versions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
wireframe_id UUID REFERENCES wireframes(id) ON DELETE CASCADE,
version_number INTEGER NOT NULL,
version_name VARCHAR(100),
version_description TEXT,
snapshot_data JSONB NOT NULL, -- Complete wireframe state at this version
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(wireframe_id, version_number)
);
-- Indexes for performance
CREATE INDEX idx_wireframes_user_id ON wireframes(user_id);
CREATE INDEX idx_wireframes_project_id ON wireframes(project_id);
CREATE INDEX idx_wireframes_active ON wireframes(is_active);
CREATE INDEX idx_wireframes_device_type ON wireframes(device_type);
CREATE INDEX idx_wireframe_elements_wireframe_id ON wireframe_elements(wireframe_id);
CREATE INDEX idx_wireframe_elements_parent_id ON wireframe_elements(parent_id);
CREATE INDEX idx_wireframe_elements_type ON wireframe_elements(element_type);
CREATE INDEX idx_wireframe_versions_wireframe_id ON wireframe_versions(wireframe_id);
CREATE INDEX idx_wireframe_versions_number ON wireframe_versions(version_number);
-- Apply triggers for updated_at columns
CREATE TRIGGER update_wireframes_updated_at
BEFORE UPDATE ON wireframes
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_wireframe_elements_updated_at
BEFORE UPDATE ON wireframe_elements
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- Functions for wireframe management
CREATE OR REPLACE FUNCTION create_wireframe_version(
p_wireframe_id UUID,
p_version_name VARCHAR(100),
p_version_description TEXT,
p_snapshot_data JSONB,
p_created_by UUID
)
RETURNS UUID AS $$
DECLARE
next_version INTEGER;
new_version_id UUID;
BEGIN
-- Get next version number
SELECT COALESCE(MAX(version_number), 0) + 1
INTO next_version
FROM wireframe_versions
WHERE wireframe_id = p_wireframe_id;
-- Create new version
INSERT INTO wireframe_versions (
wireframe_id, version_number, version_name,
version_description, snapshot_data, created_by
) VALUES (
p_wireframe_id, next_version, p_version_name,
p_version_description, p_snapshot_data, p_created_by
) RETURNING id INTO new_version_id;
RETURN new_version_id;
END;
$$ LANGUAGE plpgsql;
-- Function to get wireframe with all elements
CREATE OR REPLACE FUNCTION get_wireframe_with_elements(p_wireframe_id UUID)
RETURNS TABLE(
wireframe_data JSONB,
elements_data JSONB
) AS $$
BEGIN
RETURN QUERY
SELECT
to_jsonb(w.*) as wireframe_data,
COALESCE(
jsonb_agg(
jsonb_build_object(
'id', we.id,
'tldraw_id', we.tldraw_id,
'type', we.element_type,
'data', we.element_data,
'position', we.position,
'size', we.size,
'style', we.style,
'parent_id', we.parent_id,
'z_index', we.z_index
) ORDER BY we.z_index, we.created_at
) FILTER (WHERE we.id IS NOT NULL),
'[]'::jsonb
) as elements_data
FROM wireframes w
LEFT JOIN wireframe_elements we ON w.id = we.wireframe_id
WHERE w.id = p_wireframe_id
GROUP BY w.id, w.user_id, w.project_id, w.name, w.description,
w.device_type, w.dimensions, w.metadata, w.is_active,
w.created_at, w.updated_at;
END;
$$ LANGUAGE plpgsql;
-- Insert sample wireframe for testing
INSERT INTO wireframes (
id, user_id, name, description, device_type, dimensions, metadata
) VALUES (
uuid_generate_v4(),
(SELECT id FROM users WHERE username = 'testuser' LIMIT 1),
'Sample Wireframe',
'A sample wireframe for testing',
'desktop',
'{"width": 1440, "height": 1024}'::jsonb,
'{"prompt": "Sample prompt", "generator": "ai"}'::jsonb
) ON CONFLICT DO NOTHING;
-- Success message
SELECT 'Wireframe database schema created successfully!' as message;
-- Display created tables
SELECT
schemaname,
tablename,
tableowner
FROM pg_tables
WHERE schemaname = 'public'
AND tablename IN ('wireframes', 'wireframe_elements', 'wireframe_versions')
ORDER BY tablename;

View File

@ -0,0 +1,35 @@
#!/usr/bin/env python3
"""
Startup script for the SVG Wireframe Generator Backend
"""
import os
from dotenv import load_dotenv
from app import app
if __name__ == '__main__':
# Load environment variables
load_dotenv()
# Get configuration
port = int(os.environ.get('PORT', 5000))
debug = os.environ.get('FLASK_DEBUG', 'True').lower() == 'true'
print("🚀 Starting SVG Wireframe Generator Backend...")
print(f"📍 Port: {port}")
print(f"🔧 Debug: {debug}")
print(f"🌐 URL: http://localhost:{port}")
print("=" * 50)
try:
app.run(
debug=debug,
host='0.0.0.0',
port=port,
use_reloader=debug
)
except KeyboardInterrupt:
print("\n🛑 Server stopped by user")
except Exception as e:
print(f"❌ Error starting server: {e}")
exit(1)

View File

@ -0,0 +1,188 @@
#!/usr/bin/env python3
"""
Test script for the wireframe generation API endpoints
Tests both the universal and device-specific endpoints
"""
import requests
import json
import sys
import os
# Configuration
BASE_URL = "http://localhost:5000"
TEST_PROMPT = "Dashboard with header, left sidebar, 3 stats cards, and footer"
def test_health_endpoint():
"""Test the health check endpoint"""
print("🔍 Testing health endpoint...")
try:
response = requests.get(f"{BASE_URL}/api/health", timeout=10)
if response.status_code == 200:
data = response.json()
print(f"✅ Health check passed: {data}")
return True
else:
print(f"❌ Health check failed: {response.status_code}")
return False
except Exception as e:
print(f"❌ Health check error: {e}")
return False
def test_device_specific_endpoint(device_type):
"""Test device-specific wireframe generation"""
print(f"🔍 Testing {device_type} endpoint...")
try:
response = requests.post(
f"{BASE_URL}/generate-wireframe/{device_type}",
json={"prompt": TEST_PROMPT},
headers={"Content-Type": "application/json"},
timeout=30
)
if response.status_code == 200:
content_type = response.headers.get('content-type', '')
if 'image/svg+xml' in content_type:
print(f"{device_type} endpoint: SVG generated successfully")
print(f" Content-Type: {content_type}")
print(f" Response length: {len(response.text)} characters")
return True
else:
print(f"⚠️ {device_type} endpoint: Unexpected content type: {content_type}")
return False
else:
print(f"{device_type} endpoint failed: {response.status_code}")
try:
error_data = response.json()
print(f" Error: {error_data}")
except:
print(f" Error text: {response.text}")
return False
except Exception as e:
print(f"{device_type} endpoint error: {e}")
return False
def test_universal_endpoint():
"""Test the universal wireframe generation endpoint"""
print("🔍 Testing universal endpoint...")
try:
response = requests.post(
f"{BASE_URL}/generate-wireframe",
json={"prompt": TEST_PROMPT, "device": "desktop"},
headers={"Content-Type": "application/json"},
timeout=30
)
if response.status_code == 200:
content_type = response.headers.get('content-type', '')
if 'image/svg+xml' in content_type:
print(f"✅ Universal endpoint: SVG generated successfully")
print(f" Content-Type: {content_type}")
print(f" Response length: {len(response.text)} characters")
return True
else:
print(f"⚠️ Universal endpoint: Unexpected content type: {content_type}")
return False
else:
print(f"❌ Universal endpoint failed: {response.status_code}")
try:
error_data = response.json()
print(f" Error: {error_data}")
except:
print(f" Error text: {response.text}")
return False
except Exception as e:
print(f"❌ Universal endpoint error: {e}")
return False
def test_all_devices_endpoint():
"""Test the all devices metadata endpoint"""
print("🔍 Testing all devices endpoint...")
try:
response = requests.post(
f"{BASE_URL}/generate-all-devices",
json={"prompt": TEST_PROMPT},
headers={"Content-Type": "application/json"},
timeout=10
)
if response.status_code == 200:
data = response.json()
print(f"✅ All devices endpoint: {data.get('message', 'Success')}")
if 'device_endpoints' in data:
for device, endpoint in data['device_endpoints'].items():
print(f" {device}: {endpoint}")
return True
else:
print(f"❌ All devices endpoint failed: {response.status_code}")
try:
error_data = response.json()
print(f" Error: {error_data}")
except:
print(f" Error text: {response.text}")
return False
except Exception as e:
print(f"❌ All devices endpoint error: {e}")
return False
def main():
"""Run all tests"""
print("🚀 Starting API endpoint tests...")
print(f"📍 Base URL: {BASE_URL}")
print(f"📝 Test Prompt: {TEST_PROMPT}")
print("=" * 60)
# Test health endpoint first
if not test_health_endpoint():
print("❌ Health check failed. Is the backend running?")
sys.exit(1)
print()
# Test device-specific endpoints
devices = ['desktop', 'tablet', 'mobile']
device_results = {}
for device in devices:
device_results[device] = test_device_specific_endpoint(device)
print()
# Test universal endpoint
universal_result = test_universal_endpoint()
print()
# Test all devices endpoint
all_devices_result = test_all_devices_endpoint()
print()
# Summary
print("=" * 60)
print("📊 Test Results Summary:")
print(f" Health Check: {'✅ PASS' if True else '❌ FAIL'}")
for device, result in device_results.items():
status = "✅ PASS" if result else "❌ FAIL"
print(f" {device.capitalize()} Endpoint: {status}")
print(f" Universal Endpoint: {'✅ PASS' if universal_result else '❌ FAIL'}")
print(f" All Devices Endpoint: {'✅ PASS' if all_devices_result else '❌ FAIL'}")
# Overall success
all_passed = all(device_results.values()) and universal_result and all_devices_result
if all_passed:
print("\n🎉 All tests passed! The API is working correctly.")
else:
print("\n⚠️ Some tests failed. Check the output above for details.")
return 0 if all_passed else 1
if __name__ == "__main__":
try:
exit_code = main()
sys.exit(exit_code)
except KeyboardInterrupt:
print("\n\n⏹️ Tests interrupted by user")
sys.exit(1)
except Exception as e:
print(f"\n💥 Unexpected error: {e}")
sys.exit(1)

View File

@ -0,0 +1,172 @@
#!/usr/bin/env python3
"""
Test script for authentication functionality
Tests JWT token verification with both local and remote auth service
"""
import requests
import json
import sys
import os
import jwt
from datetime import datetime, timedelta
# Configuration
AI_MOCKUP_URL = "http://localhost:8021"
USER_AUTH_URL = "http://localhost:8011"
TEST_USER_EMAIL = "test@example.com"
TEST_USER_PASSWORD = "testpassword123"
def test_user_auth_service():
"""Test if user-auth service is running and accessible"""
print("🔍 Testing user-auth service...")
try:
response = requests.get(f"{USER_AUTH_URL}/health", timeout=10)
if response.status_code == 200:
print(f"✅ User-auth service is running: {response.json()}")
return True
else:
print(f"❌ User-auth service health check failed: {response.status_code}")
return False
except Exception as e:
print(f"❌ User-auth service error: {e}")
return False
def test_ai_mockup_service():
"""Test if ai-mockup service is running and accessible"""
print("🔍 Testing ai-mockup service...")
try:
response = requests.get(f"{AI_MOCKUP_URL}/api/health", timeout=10)
if response.status_code == 200:
print(f"✅ AI-mockup service is running: {response.json()}")
return True
else:
print(f"❌ AI-mockup service health check failed: {response.status_code}")
return False
except Exception as e:
print(f"❌ AI-mockup service error: {e}")
return False
def test_token_verification_endpoint():
"""Test the token verification endpoint in user-auth service"""
print("🔍 Testing token verification endpoint...")
# Create a test JWT token
test_payload = {
'userId': 'test-user-id',
'email': TEST_USER_EMAIL,
'username': 'testuser',
'role': 'user',
'exp': datetime.utcnow() + timedelta(hours=1),
'iat': datetime.utcnow(),
'iss': 'tech4biz-auth',
'aud': 'tech4biz-users'
}
# Use the same secret as configured in the services
test_secret = 'access-secret-key-2024-tech4biz'
test_token = jwt.encode(test_payload, test_secret, algorithm='HS256')
try:
response = requests.get(
f"{USER_AUTH_URL}/api/auth/verify",
headers={'Authorization': f'Bearer {test_token}'},
timeout=10
)
if response.status_code == 200:
result = response.json()
if result.get('success'):
print(f"✅ Token verification endpoint working: {result}")
return True
else:
print(f"❌ Token verification failed: {result}")
return False
else:
print(f"❌ Token verification endpoint failed: {response.status_code}")
try:
error_data = response.json()
print(f" Error: {error_data}")
except:
print(f" Error text: {response.text}")
return False
except Exception as e:
print(f"❌ Token verification test error: {e}")
return False
def test_ai_mockup_with_auth():
"""Test ai-mockup service with authentication"""
print("🔍 Testing ai-mockup service with authentication...")
# Create a test JWT token
test_payload = {
'userId': 'test-user-id',
'email': TEST_USER_EMAIL,
'username': 'testuser',
'role': 'user',
'exp': datetime.utcnow() + timedelta(hours=1),
'iat': datetime.utcnow(),
'iss': 'tech4biz-auth',
'aud': 'tech4biz-users'
}
test_secret = 'access-secret-key-2024-tech4biz'
test_token = jwt.encode(test_payload, test_secret, algorithm='HS256')
try:
# Test a protected endpoint (assuming there's one)
response = requests.get(
f"{AI_MOCKUP_URL}/api/protected-endpoint",
headers={'Authorization': f'Bearer {test_token}'},
timeout=10
)
# This might return 404 if the endpoint doesn't exist, but should not return 401
if response.status_code == 401:
print(f"❌ Authentication still failing: {response.status_code}")
try:
error_data = response.json()
print(f" Error: {error_data}")
except:
print(f" Error text: {response.text}")
return False
else:
print(f"✅ Authentication working (status: {response.status_code})")
return True
except Exception as e:
print(f"❌ AI-mockup auth test error: {e}")
return False
def main():
"""Run all authentication tests"""
print("🚀 Starting authentication tests...\n")
tests = [
test_user_auth_service,
test_ai_mockup_service,
test_token_verification_endpoint,
test_ai_mockup_with_auth
]
passed = 0
total = len(tests)
for test in tests:
try:
if test():
passed += 1
print()
except Exception as e:
print(f"❌ Test {test.__name__} crashed: {e}\n")
print(f"📊 Test Results: {passed}/{total} tests passed")
if passed == total:
print("🎉 All authentication tests passed!")
return 0
else:
print("⚠️ Some authentication tests failed. Check the logs above.")
return 1
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,133 @@
#!/usr/bin/env python3
"""
Test database connection and wireframe saving functionality
"""
import os
import psycopg2
from psycopg2.extras import RealDictCursor
from dotenv import load_dotenv
import json
def test_database_connection():
"""Test if we can connect to the database"""
# Load environment variables
load_dotenv()
# Database connection details
db_config = {
'host': os.getenv('POSTGRES_HOST', 'localhost'),
'database': os.getenv('POSTGRES_DB', 'dev_pipeline'),
'user': os.getenv('POSTGRES_USER', 'pipeline_admin'),
'password': os.getenv('POSTGRES_PASSWORD', 'secure_pipeline_2024'),
'port': os.getenv('POSTGRES_PORT', '5433')
}
print("Testing database connection with config:")
for key, value in db_config.items():
if key == 'password':
print(f" {key}: {'*' * len(str(value))}")
else:
print(f" {key}: {value}")
try:
# Test connection
conn = psycopg2.connect(**db_config)
print("✅ Database connection successful!")
# Test if wireframes table exists
with conn.cursor() as cur:
cur.execute("""
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'wireframes'
);
""")
table_exists = cur.fetchone()[0]
if table_exists:
print("✅ Wireframes table exists!")
# Test inserting a sample wireframe
cur.execute("""
INSERT INTO wireframes (user_id, name, description, device_type, dimensions, metadata)
VALUES (%s, %s, %s, %s, %s, %s)
RETURNING id
""", (
'testuser',
'Test Wireframe',
'Test wireframe for connection testing',
'desktop',
json.dumps({'width': 1440, 'height': 1024}),
json.dumps({'test': True, 'timestamp': '2024-01-01'})
))
wireframe_id = cur.fetchone()[0]
print(f"✅ Test wireframe inserted with ID: {wireframe_id}")
# Clean up test data
cur.execute("DELETE FROM wireframes WHERE id = %s", (wireframe_id,))
print("✅ Test wireframe cleaned up")
else:
print("❌ Wireframes table does not exist!")
print("Please run the database setup script first:")
print(" python setup_database.py")
conn.close()
return True
except Exception as e:
print(f"❌ Database connection failed: {e}")
return False
def test_api_endpoint():
"""Test if the API endpoint is accessible"""
import requests
try:
response = requests.get('http://localhost:5000/api/health')
if response.status_code == 200:
print("✅ API endpoint is accessible!")
print(f"Response: {response.json()}")
return True
else:
print(f"❌ API endpoint returned status: {response.status_code}")
return False
except requests.exceptions.ConnectionError:
print("❌ Cannot connect to API endpoint. Is the backend running?")
print("Start the backend with: python app.py")
return False
except Exception as e:
print(f"❌ API test failed: {e}")
return False
if __name__ == "__main__":
print("Testing Tech4biz Wireframe Generator...")
print("=" * 50)
# Test database connection
db_ok = test_database_connection()
print()
# Test API endpoint
api_ok = test_api_endpoint()
print()
if db_ok and api_ok:
print("🎉 All tests passed! The system is ready to use.")
else:
print("❌ Some tests failed. Please fix the issues above.")
if not db_ok:
print("\nTo fix database issues:")
print("1. Make sure PostgreSQL is running")
print("2. Check your environment variables")
print("3. Run: python setup_database.py")
if not api_ok:
print("\nTo fix API issues:")
print("1. Start the backend: python app.py")
print("2. Make sure it's running on port 5000")

View File

@ -0,0 +1,277 @@
#!/usr/bin/env python3
"""
Test script for AI Mockup Service Authentication and Wireframe Saving
This script tests the complete flow from authentication to wireframe saving
"""
import requests
import json
import time
import os
from datetime import datetime
# Configuration
AI_MOCKUP_SERVICE_URL = "http://localhost:8021"
USER_AUTH_SERVICE_URL = "http://localhost:8011"
POSTGRES_HOST = "localhost"
POSTGRES_PORT = "5433" # Docker mapped port
POSTGRES_DB = "dev_pipeline"
POSTGRES_USER = "pipeline_admin"
POSTGRES_PASSWORD = "secure_pipeline_2024"
def test_health_checks():
"""Test health endpoints"""
print("🔍 Testing health checks...")
# Test AI Mockup Service health
try:
response = requests.get(f"{AI_MOCKUP_SERVICE_URL}/health", timeout=10)
if response.status_code == 200:
print("✅ AI Mockup Service is healthy")
print(f" Status: {response.json().get('status')}")
print(f" Database: {response.json().get('services', {}).get('database')}")
print(f" User Auth: {response.json().get('services', {}).get('user_auth')}")
else:
print(f"❌ AI Mockup Service health check failed: {response.status_code}")
return False
except Exception as e:
print(f"❌ AI Mockup Service health check error: {e}")
return False
# Test User Auth Service health
try:
response = requests.get(f"{USER_AUTH_SERVICE_URL}/health", timeout=10)
if response.status_code == 200:
print("✅ User Auth Service is healthy")
else:
print(f"❌ User Auth Service health check failed: {response.status_code}")
return False
except Exception as e:
print(f"❌ User Auth Service health check error: {e}")
return False
return True
def test_authentication():
"""Test authentication flow"""
print("\n🔐 Testing authentication...")
# Test registration
test_user = {
"username": f"testuser_{int(time.time())}",
"email": f"testuser_{int(time.time())}@example.com",
"password": "TestPassword123!",
"first_name": "Test",
"last_name": "User"
}
try:
response = requests.post(f"{USER_AUTH_SERVICE_URL}/api/auth/register",
json=test_user, timeout=10)
if response.status_code == 201:
print("✅ User registration successful")
user_data = response.json().get('data', {})
user_id = user_data.get('user', {}).get('id')
if user_id:
print("✅ User ID received")
# For testing purposes, manually verify the email by updating the database
# This bypasses the email verification requirement
try:
import psycopg2
conn = psycopg2.connect(
host=POSTGRES_HOST,
database=POSTGRES_DB,
user=POSTGRES_USER,
password=POSTGRES_PASSWORD,
port=POSTGRES_PORT
)
with conn.cursor() as cur:
cur.execute("UPDATE users SET email_verified = true WHERE id = %s", (user_id,))
conn.commit()
conn.close()
print("✅ Email verification bypassed for testing")
except Exception as e:
print(f"⚠️ Could not bypass email verification: {e}")
return None, None
# Now try to login
login_response = requests.post(f"{USER_AUTH_SERVICE_URL}/api/auth/login",
json={"email": test_user["email"], "password": test_user["password"]},
timeout=10)
if login_response.status_code == 200:
login_data = login_response.json().get('data', {})
access_token = login_data.get('tokens', {}).get('accessToken')
if access_token:
print("✅ Access token received")
return access_token, test_user
else:
print("❌ No access token in login response")
return None, None
else:
print(f"❌ Login failed: {login_response.status_code}")
print(f" Response: {login_response.text}")
return None, None
else:
print("❌ No user ID in response")
return None, None
else:
print(f"❌ User registration failed: {response.status_code}")
print(f" Response: {response.text}")
return None, None
except Exception as e:
print(f"❌ User registration error: {e}")
return None, None
def test_wireframe_generation(access_token):
"""Test wireframe generation"""
print("\n🎨 Testing wireframe generation...")
headers = {"Authorization": f"Bearer {access_token}"}
prompt = "Create a simple landing page with header, hero section, and footer"
try:
response = requests.post(f"{AI_MOCKUP_SERVICE_URL}/generate-wireframe/desktop",
json={"prompt": prompt},
headers=headers,
timeout=30)
if response.status_code == 200:
print("✅ Wireframe generation successful")
result = response.json()
if result.get('svg'):
print("✅ SVG wireframe received")
return result.get('svg')
else:
print("❌ No SVG in response")
return None
else:
print(f"❌ Wireframe generation failed: {response.status_code}")
print(f" Response: {response.text}")
return None
except Exception as e:
print(f"❌ Wireframe generation error: {e}")
return None
def test_wireframe_saving(access_token, svg_data):
"""Test wireframe saving"""
print("\n💾 Testing wireframe saving...")
headers = {"Authorization": f"Bearer {access_token}"}
wireframe_data = {
"wireframe": {
"name": f"Test Wireframe {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
"description": "Test wireframe created by automated test",
"device_type": "desktop",
"dimensions": {"width": 1440, "height": 1024},
"metadata": {"prompt": "Test prompt", "generator": "test"}
},
"elements": [
{
"id": "test-element-1",
"type": "shape",
"data": {"type": "rectangle", "props": {"w": 200, "h": 100}},
"position": {"x": 100, "y": 100},
"size": {"width": 200, "height": 100},
"style": {"color": "#3B82F6", "fill": "#EFF6FF"},
"parent_id": None,
"z_index": 0
}
]
}
try:
response = requests.post(f"{AI_MOCKUP_SERVICE_URL}/api/wireframes",
json=wireframe_data,
headers=headers,
timeout=10)
if response.status_code == 201:
print("✅ Wireframe saved successfully")
result = response.json()
wireframe_id = result.get('wireframe_id')
if wireframe_id:
print(f"✅ Wireframe ID: {wireframe_id}")
return wireframe_id
else:
print("❌ No wireframe ID in response")
return None
else:
print(f"❌ Wireframe saving failed: {response.status_code}")
print(f" Response: {response.text}")
return None
except Exception as e:
print(f"❌ Wireframe saving error: {e}")
return None
def test_wireframe_retrieval(access_token, wireframe_id):
"""Test wireframe retrieval"""
print("\n📖 Testing wireframe retrieval...")
headers = {"Authorization": f"Bearer {access_token}"}
try:
response = requests.get(f"{AI_MOCKUP_SERVICE_URL}/api/wireframes/{wireframe_id}",
headers=headers,
timeout=10)
if response.status_code == 200:
print("✅ Wireframe retrieved successfully")
result = response.json()
if result.get('wireframe') and result.get('elements'):
print("✅ Wireframe data and elements received")
return True
else:
print("❌ Incomplete wireframe data")
return False
else:
print(f"❌ Wireframe retrieval failed: {response.status_code}")
print(f" Response: {response.text}")
return False
except Exception as e:
print(f"❌ Wireframe retrieval error: {e}")
return False
def main():
"""Run all tests"""
print("🚀 Starting AI Mockup Service Integration Tests")
print("=" * 50)
# Test 1: Health checks
if not test_health_checks():
print("\n❌ Health checks failed. Please ensure all services are running.")
return
# Test 2: Authentication
access_token, user_data = test_authentication()
if not access_token:
print("\n❌ Authentication failed. Please check user-auth service.")
return
# Test 3: Wireframe generation
svg_data = test_wireframe_generation(access_token)
if not svg_data:
print("\n❌ Wireframe generation failed.")
return
# Test 4: Wireframe saving
wireframe_id = test_wireframe_saving(access_token, svg_data)
if not wireframe_id:
print("\n❌ Wireframe saving failed.")
return
# Test 5: Wireframe retrieval
if not test_wireframe_retrieval(access_token, wireframe_id):
print("\n❌ Wireframe retrieval failed.")
return
print("\n" + "=" * 50)
print("🎉 All tests passed! Wireframe saving is working correctly.")
print(f"✅ User: {user_data.get('username')}")
print(f"✅ Wireframe ID: {wireframe_id}")
print("✅ Authentication, generation, saving, and retrieval all working")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,127 @@
#!/usr/bin/env python3
"""
Test script for SVG wireframe generation
"""
import sys
import os
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from app import generate_svg_wireframe
def test_svg_generation():
"""Test SVG generation with sample data"""
# Test layout specification
test_layout = {
"layout": {
"page": {"width": 1200, "height": 800},
"header": {"enabled": True, "height": 72, "elements": ["Logo", "Navigation", "CTA"]},
"sidebar": {"enabled": True, "width": 240, "position": "left", "elements": ["Menu", "Filters"]},
"hero": {"enabled": True, "height": 200, "elements": ["Hero Title", "Hero Subtitle", "Button"]},
"main_content": {
"sections": [
{
"type": "grid",
"rows": 2,
"cols": 3,
"height": 200,
"elements": ["Card 1", "Card 2", "Card 3", "Card 4", "Card 5", "Card 6"]
},
{
"type": "form",
"height": 300,
"fields": ["Name", "Email", "Message", "submit"]
}
]
},
"footer": {"enabled": True, "height": 64, "elements": ["Links", "Copyright"]}
},
"styling": {
"theme": "modern",
"colors": {
"primary": "#3B82F6",
"secondary": "#6B7280",
"background": "#FFFFFF",
"card": "#F8FAFC",
"text": "#1F2937"
},
"spacing": {"gap": 16, "padding": 20}
},
"annotations": {
"title": "Test Wireframe",
"description": "Test SVG generation"
}
}
try:
# Generate SVG
svg_content = generate_svg_wireframe(test_layout)
# Save to file for inspection
with open('test_wireframe.svg', 'w', encoding='utf-8') as f:
f.write(svg_content)
print("✅ SVG generation test passed!")
print(f"Generated SVG: {len(svg_content)} characters")
print("Saved to: test_wireframe.svg")
# Basic validation
assert '<svg' in svg_content
assert 'width="1200"' in svg_content
assert 'height="800"' in svg_content
assert 'Logo' in svg_content
assert 'Card 1' in svg_content
print("✅ SVG validation passed!")
except Exception as e:
print(f"❌ SVG generation test failed: {e}")
return False
return True
def test_fallback_spec():
"""Test fallback specification generation"""
from app import create_fallback_spec
try:
fallback_spec = create_fallback_spec("Dashboard with header and sidebar")
# Generate SVG from fallback
svg_content = generate_svg_wireframe(fallback_spec)
with open('test_fallback.svg', 'w', encoding='utf-8') as f:
f.write(svg_content)
print("✅ Fallback SVG generation test passed!")
print(f"Generated fallback SVG: {len(svg_content)} characters")
print("Saved to: test_fallback.svg")
except Exception as e:
print(f"❌ Fallback SVG generation test failed: {e}")
return False
return True
if __name__ == '__main__':
print("🧪 Testing SVG Wireframe Generation...")
print("=" * 50)
# Test main SVG generation
test1_passed = test_svg_generation()
# Test fallback generation
test2_passed = test_fallback_spec()
print("=" * 50)
if test1_passed and test2_passed:
print("🎉 All tests passed! SVG generation is working correctly.")
else:
print("❌ Some tests failed. Check the output above for details.")
print("\n📁 Generated files:")
if os.path.exists('test_wireframe.svg'):
print(" - test_wireframe.svg (main test)")
if os.path.exists('test_fallback.svg'):
print(" - test_fallback.svg (fallback test)")

View File

@ -0,0 +1,9 @@
@echo off
echo Setting up database...
python src\setup_database.py
echo Starting AI Mockup Service...
@REM gunicorn --bind 0.0.0.0:8021 src.app:app
@REM python -m flask --app src.app run --host=0.0.0.0 --port=8021
python -m waitress --listen=0.0.0.0:8021 src.app:app
pause

View File

@ -0,0 +1,6 @@
#!/bin/bash
echo "Setting up database..."
python src/setup_database.py
echo "Starting AI Mockup Service..."
gunicorn --bind 0.0.0.0:8021 src.app:app

View File

@ -28,4 +28,9 @@ RABBITMQ_USER=pipeline_admin
RABBITMQ_PASSWORD=secure_rabbitmq_password
# CORS
FRONTEND_URL=https://dashboard.codenuk.com
FRONTEND_URL=https://dashboard.codenuk.com
# CORS Configuration
CORS_ORIGIN=http://localhost:3001
CORS_METHODS=GET,POST,PUT,DELETE,PATCH,OPTIONS
CORS_CREDENTIALS=true

View File

@ -0,0 +1,52 @@
const cors = require('cors');
const corsMiddleware = cors({
origin: function (origin, callback) {
// Allow requests from your frontend and other services
const allowedOrigins = [
'http://localhost:3001', // Frontend (CodeNuk)
'http://localhost:3000', // Alternative frontend port
'http://localhost:8008', // Dashboard service
'http://localhost:8000', // API Gateway
process.env.CORS_ORIGIN,
process.env.FRONTEND_URL
].filter(Boolean);
// Allow requests with no origin (mobile apps, etc.) or from allowed origins
if (!origin || allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
methods: process.env.CORS_METHODS?.split(',') || ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
credentials: process.env.CORS_CREDENTIALS === 'true' || true,
allowedHeaders: [
'Content-Type',
'Authorization',
'X-Requested-With',
'Origin',
'X-Gateway-Request-ID',
'X-Gateway-Timestamp',
'X-Forwarded-By',
'X-Forwarded-For',
'X-Forwarded-Proto',
'X-Forwarded-Host',
'X-Session-Token',
'X-Platform',
'X-App-Version'
],
exposedHeaders: [
'Content-Length',
'X-Total-Count',
'X-Gateway-Request-ID',
'X-Gateway-Timestamp',
'X-Forwarded-By',
'X-Forwarded-For',
'X-Forwarded-Proto',
'X-Forwarded-Host'
],
maxAge: 86400 // 24 hours
});
module.exports = corsMiddleware;

View File

@ -9,7 +9,13 @@ const authenticateSocket = (socket, next) => {
return next(new Error('Authentication token required'));
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const jwtSecret = process.env.JWT_ACCESS_SECRET || process.env.JWT_SECRET;
if (!jwtSecret) {
console.error('WebSocket authentication failed: JWT secret not configured');
return next(new Error('Authentication failed'));
}
const decoded = jwt.verify(token, jwtSecret);
socket.user = decoded;
socket.userId = decoded.id || decoded.userId;

View File

@ -1,3 +1,5 @@
require('dotenv').config();
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
@ -8,9 +10,9 @@ const rateLimit = require('express-rate-limit');
const { createProxyMiddleware } = require('http-proxy-middleware');
const jwt = require('jsonwebtoken');
const axios = require('axios');
require('dotenv').config();
// Import middleware
const corsMiddleware = require('./middleware/cors');
const authMiddleware = require('./middleware/authentication');
const serviceHealthMiddleware = require('./middleware/serviceHealth');
const requestLogger = require('./middleware/requestLogger');
@ -22,13 +24,21 @@ const healthRouter = require('./routes/healthRouter');
const websocketRouter = require('./routes/websocketRouter');
const app = express();
// Apply CORS middleware before other middleware
app.use(corsMiddleware);
const server = http.createServer(app);
const PORT = process.env.PORT || 8000;
// Initialize Socket.IO with CORS
const io = socketIo(server, {
cors: {
origin: process.env.FRONTEND_URL || "*",
origin: [
'http://localhost:3001', // Frontend (CodeNuk)
'http://localhost:3000', // Alternative frontend port
'http://localhost:8008', // Dashboard service
'http://localhost:8000', // API Gateway
process.env.FRONTEND_URL
].filter(Boolean),
credentials: true,
methods: ['GET', 'POST']
},
@ -70,23 +80,7 @@ app.use(helmet({
}
}));
// CORS configuration
app.use(cors({
origin: process.env.FRONTEND_URL || "*",
credentials: true,
optionsSuccessStatus: 200,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: [
'Origin', 'X-Requested-With', 'Content-Type', 'Accept', 'Authorization',
'X-Gateway-Request-ID', 'X-Gateway-Timestamp', 'X-Forwarded-By',
'X-Forwarded-For', 'X-Forwarded-Proto', 'X-Forwarded-Host',
'X-Session-Token', 'X-Platform', 'X-App-Version'
],
exposedHeaders: [
'X-Gateway-Request-ID', 'X-Gateway-Timestamp', 'X-Forwarded-By',
'X-Forwarded-For', 'X-Forwarded-Proto', 'X-Forwarded-Host'
]
}));
// CORS is already configured via corsMiddleware above
// Request parsing middleware - only for non-proxy routes
app.use('/api/websocket', express.json({ limit: '10mb' }));
@ -178,7 +172,9 @@ app.use('/api/auth', (req, res, next) => {
headers: {
'Content-Type': 'application/json',
'User-Agent': 'API-Gateway/1.0',
'Connection': 'keep-alive'
'Connection': 'keep-alive',
// Forward Authorization header so protected auth-admin routes work
'Authorization': req.headers.authorization
},
timeout: 8000,
validateStatus: () => true,
@ -290,6 +286,71 @@ app.use('/api/templates',
}
);
// Admin endpoints (Template Manager) - expose /api/admin via gateway
console.log('🔧 Registering /api/admin proxy route...');
app.use('/api/admin',
createServiceLimiter(300),
// Public proxy from gateway perspective; downstream service enforces JWT admin check
(req, res, next) => {
console.log(`🟠 [ADMIN PROXY] ${req.method} ${req.originalUrl}`);
return next();
},
(req, res, next) => {
const adminServiceUrl = serviceTargets.TEMPLATE_MANAGER_URL;
const targetUrl = `${adminServiceUrl}${req.originalUrl}`;
console.log(`🔥 [ADMIN PROXY] ${req.method} ${req.originalUrl}${targetUrl}`);
res.setTimeout(15000, () => {
console.error('❌ [ADMIN PROXY] Response timeout');
if (!res.headersSent) {
res.status(504).json({ error: 'Gateway timeout', service: 'template-manager(admin)' });
}
});
const options = {
method: req.method,
url: targetUrl,
headers: {
'Content-Type': 'application/json',
'User-Agent': 'API-Gateway/1.0',
'Connection': 'keep-alive',
// Forward Authorization header for admin JWT check
'Authorization': req.headers.authorization
},
timeout: 8000,
validateStatus: () => true,
maxRedirects: 0
};
if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') {
options.data = req.body || {};
console.log(`📦 [ADMIN PROXY] Request body:`, JSON.stringify(req.body));
}
axios(options)
.then(response => {
console.log(`✅ [ADMIN PROXY] Response: ${response.status} for ${req.method} ${req.originalUrl}`);
if (!res.headersSent) {
res.status(response.status).json(response.data);
}
})
.catch(error => {
console.error(`❌ [ADMIN PROXY ERROR]:`, error.message);
if (!res.headersSent) {
if (error.response) {
res.status(error.response.status).json(error.response.data);
} else {
res.status(502).json({
error: 'Admin endpoints unavailable',
message: error.code || error.message,
service: 'template-manager(admin)'
});
}
}
});
}
);
// Requirement Processor Service
app.use('/api/requirements',
createServiceLimiter(300),

View File

@ -14,6 +14,7 @@ const templateRoutes = require('./routes/templates');
const featureRoutes = require('./routes/features');
const learningRoutes = require('./routes/learning');
const adminRoutes = require('./routes/admin');
const adminTemplateRoutes = require('./routes/admin-templates');
const AdminNotification = require('./models/admin_notification');
// const customTemplateRoutes = require('./routes/custom_templates');
@ -21,7 +22,11 @@ const app = express();
const server = http.createServer(app);
const io = new Server(server, {
cors: {
origin: process.env.FRONTEND_URL || "http://localhost:3000",
origin: [
'http://localhost:3001', // Frontend (CodeNuk)
'http://localhost:3000', // Alternative frontend port
process.env.FRONTEND_URL
].filter(Boolean),
methods: ["GET", "POST"],
credentials: true
}
@ -30,7 +35,17 @@ const PORT = process.env.PORT || 8009;
// Middleware
app.use(helmet());
app.use(cors());
app.use(cors({
origin: [
'http://localhost:3001', // Frontend (CodeNuk)
'http://localhost:3000', // Alternative frontend port
'http://localhost:8000', // API Gateway
process.env.FRONTEND_URL
].filter(Boolean),
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-User-ID', 'X-User-Role']
}));
app.use(morgan('combined'));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
@ -42,6 +57,7 @@ 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/admin/templates', adminTemplateRoutes);
app.use('/api/templates', templateRoutes);
// Add admin routes under /api/templates to match serviceClient expectations
app.use('/api/templates/admin', adminRoutes);

View File

@ -0,0 +1,22 @@
-- Add JSONB storage for feature rules without breaking existing TEXT usage
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'public' AND tablename = 'feature_rules') THEN
CREATE TABLE feature_rules (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
template_id UUID NOT NULL REFERENCES templates(id) ON DELETE CASCADE,
template_feature_id UUID REFERENCES template_features(id) ON DELETE CASCADE,
feature_id VARCHAR(100) NOT NULL,
rule_text TEXT NOT NULL,
rule_order INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW()
);
-- Helpful index to quickly find rules for a feature
CREATE INDEX IF NOT EXISTS idx_feature_rules_by_template_and_feature
ON feature_rules (template_id, feature_id, rule_order);
END IF;
END $$;

View File

@ -0,0 +1,31 @@
-- Add JSONB storage for feature rules without breaking existing TEXT usage
DO $$
BEGIN
-- Add JSONB column if missing
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'feature_rules'
AND column_name = 'rule_json'
) THEN
ALTER TABLE feature_rules
ADD COLUMN rule_json JSONB;
END IF;
-- Best-effort backfill: if rule_text looks like JSON, cast it
-- This avoids errors casting arbitrary text
UPDATE feature_rules
SET rule_json = rule_text::jsonb
WHERE rule_json IS NULL
AND (
(rule_text IS NOT NULL AND LEFT(TRIM(rule_text), 1) = '{') OR
(rule_text IS NOT NULL AND LEFT(TRIM(rule_text), 1) = '[')
);
-- Helpful GIN index for future querying by JSON keys/values
CREATE INDEX IF NOT EXISTS idx_feature_rules_rule_json_gin
ON feature_rules USING GIN (rule_json);
END $$;

View File

@ -0,0 +1,21 @@
-- Aggregated business rules per (template_id, feature_id)
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'feature_business_rules'
) THEN
CREATE TABLE feature_business_rules (
template_id UUID NOT NULL REFERENCES templates(id) ON DELETE CASCADE,
feature_id VARCHAR(100) NOT NULL,
business_rules JSONB NOT NULL,
updated_at TIMESTAMP DEFAULT NOW(),
PRIMARY KEY (template_id, feature_id)
);
CREATE INDEX IF NOT EXISTS idx_feature_business_rules_t_f ON feature_business_rules (template_id, feature_id);
CREATE INDEX IF NOT EXISTS idx_feature_business_rules_gin ON feature_business_rules USING GIN (business_rules);
END IF;
END $$;

View File

@ -8,14 +8,31 @@ async function runMigrations() {
try {
// Get all migration files in order
const migrationFiles = [
let migrationFiles = [
'001_initial_schema.sql',
'002_admin_approval_workflow.sql',
'003_custom_templates.sql',
'004_add_is_custom_flag.sql',
'004_add_user_id_to_custom_templates.sql',
'005_fix_custom_features_foreign_key.sql'
'005_fix_custom_features_foreign_key.sql',
// Intentionally skip feature_rules migrations per updated design
'008_feature_business_rules.sql'
];
// Safety: if core tables already exist, skip the destructive 001 file
try {
const existing = await database.query(`
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public' AND table_name IN ('templates','template_features')
`);
const hasCoreTables = existing.rows && existing.rows.length >= 1;
if (hasCoreTables) {
migrationFiles = migrationFiles.filter((f) => f !== '001_initial_schema.sql');
console.log('⚠️ Core tables detected; skipping 001_initial_schema.sql to avoid destructive drops.');
}
} catch (probeErr) {
console.warn('Could not probe existing tables; proceeding with full migration list:', probeErr.message);
}
for (const migrationFile of migrationFiles) {
const migrationPath = path.join(__dirname, migrationFile);
@ -26,9 +43,17 @@ async function runMigrations() {
continue;
}
console.log(`Running migration: ${migrationFile}`);
const migrationSQL = fs.readFileSync(migrationPath, 'utf8');
// Skip destructive migrations unless explicitly allowed
const containsDrop = /\bdrop\s+table\b/i.test(migrationSQL);
const allowDestructive = String(process.env.ALLOW_DESTRUCTIVE_MIGRATIONS || '').toLowerCase() === 'true';
if (containsDrop && !allowDestructive) {
console.log(`⏭️ Skipping potentially destructive migration (set ALLOW_DESTRUCTIVE_MIGRATIONS=true to run): ${migrationFile}`);
continue;
}
console.log(`Running migration: ${migrationFile}`);
// Execute the migration
await database.query(migrationSQL);
@ -43,7 +68,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', 'custom_templates', 'feature_synonyms', 'admin_notifications')
AND table_name IN ('templates', 'template_features', 'feature_business_rules', 'feature_usage', 'custom_features', 'custom_templates', 'feature_synonyms', 'admin_notifications')
ORDER BY table_name
`);

View File

@ -154,7 +154,48 @@ class CustomFeature {
updates.canonical_feature_id = canonical_feature_id;
}
return await CustomFeature.update(id, updates);
// Maintain legacy approved boolean alongside status for easier filtering
if (status === 'approved') {
updates.approved = true;
} else if (status === 'rejected' || status === 'duplicate') {
updates.approved = false;
}
const updated = await CustomFeature.update(id, updates);
// If approved, ensure a mirrored entry exists/updates in template_features
if (updated && status === 'approved') {
try {
const Feature = require('./feature');
const featureId = `custom_${updated.id}`;
const existingMirror = await Feature.getByFeatureId(updated.template_id, featureId);
if (existingMirror) {
await Feature.update(existingMirror.id, {
name: updated.name,
description: updated.description,
complexity: updated.complexity,
feature_type: 'custom',
is_default: false
});
} else {
await Feature.create({
template_id: updated.template_id,
feature_id: featureId,
name: updated.name,
description: updated.description,
feature_type: 'custom',
complexity: updated.complexity,
display_order: 999,
is_default: false,
created_by_user: true
});
}
} catch (mirrorErr) {
console.error('⚠️ Failed to mirror approved custom feature into template_features:', mirrorErr.message);
}
}
return updated;
}
// Count features for a custom template

View File

@ -1,5 +1,7 @@
const database = require('../config/database');
const { v4: uuidv4 } = require('uuid');
const FeatureRule = require('./feature_rule');
const FeatureBusinessRules = require('./feature_business_rules');
class Feature {
constructor(data = {}) {
@ -126,7 +128,18 @@ class Feature {
];
const result = await database.query(query, values);
return new Feature(result.rows[0]);
const created = new Feature(result.rows[0]);
// Persist rules (aggregated JSONB) if provided
try {
const rawRules = featureData.logic_rules ?? featureData.business_rules ?? [];
await FeatureBusinessRules.upsert(created.template_id, created.feature_id, rawRules);
} catch (ruleErr) {
// Do not block feature creation if rules fail; log and continue
console.error('⚠️ Failed to persist aggregated business rules:', ruleErr.message);
}
return created;
}
// Increment usage count

View File

@ -0,0 +1,44 @@
const database = require('../config/database');
class FeatureBusinessRules {
static async upsert(template_id, feature_id, rules) {
// Normalize to JSON array
let businessRules;
if (Array.isArray(rules)) {
businessRules = rules.map((r) => (typeof r === 'string' ? tryParse(r) ?? r : r));
} else if (typeof rules === 'string') {
const parsed = tryParse(rules);
businessRules = Array.isArray(parsed) ? parsed : [parsed ?? rules];
} else if (rules && typeof rules === 'object') {
businessRules = [rules];
} else {
businessRules = [];
}
const sql = `
INSERT INTO feature_business_rules (template_id, feature_id, business_rules, updated_at)
VALUES ($1, $2, $3::jsonb, NOW())
ON CONFLICT (template_id, feature_id)
DO UPDATE SET business_rules = EXCLUDED.business_rules, updated_at = NOW()
RETURNING *
`;
const result = await database.query(sql, [template_id, feature_id, JSON.stringify(businessRules)]);
return result.rows[0];
}
}
function tryParse(s) {
try {
const t = String(s).trim();
if ((t.startsWith('{') && t.endsWith('}')) || (t.startsWith('[') && t.endsWith(']'))) {
return JSON.parse(t);
}
return null;
} catch {
return null;
}
}
module.exports = FeatureBusinessRules;

View File

@ -0,0 +1,123 @@
const database = require('../config/database');
const { v4: uuidv4 } = require('uuid');
let cachedHasRuleJson = null;
async function hasRuleJsonColumn() {
if (cachedHasRuleJson !== null) return cachedHasRuleJson;
try {
const q = `
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'feature_rules' AND column_name = 'rule_json'
LIMIT 1
`;
const result = await database.query(q);
cachedHasRuleJson = result.rows.length > 0;
} catch (e) {
cachedHasRuleJson = false;
}
return cachedHasRuleJson;
}
class FeatureRule {
constructor(data = {}) {
this.id = data.id;
this.template_id = data.template_id;
this.template_feature_id = data.template_feature_id;
this.feature_id = data.feature_id;
this.rule_text = data.rule_text;
this.rule_order = data.rule_order;
this.created_at = data.created_at;
}
static async createMany(params) {
const { template_id, template_feature_id, feature_id, rules } = params;
if (!Array.isArray(rules) || rules.length === 0) return [];
const includeJson = await hasRuleJsonColumn();
try {
console.log('[FeatureRule.createMany] start', {
template_id,
template_feature_id,
feature_id,
rules_count: rules.length,
has_rule_json_column: includeJson
});
} catch {}
const values = [];
const placeholders = [];
let idx = 1;
for (let i = 0; i < rules.length; i++) {
const id = uuidv4();
if (includeJson) {
placeholders.push(`($${idx++}, $${idx++}, $${idx++}, $${idx++}, $${idx++}, $${idx++}, $${idx++})`);
} else {
placeholders.push(`($${idx++}, $${idx++}, $${idx++}, $${idx++}, $${idx++}, $${idx++})`);
}
// rules[i] can be string or object; store string in rule_text, and JSON when available
const rule = rules[i];
let ruleText;
let ruleJson = null;
if (typeof rule === 'object' && rule !== null) {
ruleText = JSON.stringify(rule);
ruleJson = rule;
} else if (typeof rule === 'string') {
ruleText = rule;
const trimmed = rule.trim();
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
try {
ruleJson = JSON.parse(trimmed);
} catch (e) {
ruleJson = null; // keep as plain text if not valid JSON
}
}
} else {
ruleText = String(rule);
}
if (includeJson) {
values.push(id, template_id, template_feature_id || null, feature_id, ruleText, i, ruleJson);
} else {
values.push(id, template_id, template_feature_id || null, feature_id, ruleText, i);
}
}
const insertSql = includeJson
? `
INSERT INTO feature_rules (id, template_id, template_feature_id, feature_id, rule_text, rule_order, rule_json)
VALUES ${placeholders.join(', ')}
RETURNING *
`
: `
INSERT INTO feature_rules (id, template_id, template_feature_id, feature_id, rule_text, rule_order)
VALUES ${placeholders.join(', ')}
RETURNING *
`;
try {
console.log('[FeatureRule.createMany] executing insert', {
columns: includeJson
? ['id','template_id','template_feature_id','feature_id','rule_text','rule_order','rule_json']
: ['id','template_id','template_feature_id','feature_id','rule_text','rule_order'],
rows: rules.length
});
const result = await database.query(insertSql, values);
console.log('[FeatureRule.createMany] success', { inserted: result.rows.length });
return result.rows.map((r) => new FeatureRule(r));
} catch (e) {
console.error('[FeatureRule.createMany] DB error:', e.message);
throw e;
}
}
static async getByTemplateFeatureId(template_feature_id) {
const result = await database.query(
`SELECT * FROM feature_rules WHERE template_feature_id = $1 ORDER BY rule_order ASC`,
[template_feature_id]
);
return result.rows.map((r) => new FeatureRule(r));
}
}
module.exports = FeatureRule;

View File

@ -0,0 +1,519 @@
const express = require('express');
const router = express.Router();
const Template = require('../models/template');
const Feature = require('../models/feature');
const database = require('../config/database');
// GET /api/admin/templates - Get all templates for admin management
router.get('/', async (req, res) => {
try {
console.log('🔧 [ADMIN-TEMPLATES] Fetching all templates for admin management...');
const limit = parseInt(req.query.limit) || 50;
const offset = parseInt(req.query.offset) || 0;
const category = req.query.category || null;
const search = req.query.search || null;
console.log('📋 [ADMIN-TEMPLATES] Query parameters:', { limit, offset, category, search });
// Build the query with optional filters
let whereClause = 'WHERE t.is_active = true AND t.type != \'_migration_test\'';
let queryParams = [];
let paramIndex = 1;
if (category && category !== 'all') {
whereClause += ` AND t.category = $${paramIndex}`;
queryParams.push(category);
paramIndex++;
}
if (search) {
whereClause += ` AND (LOWER(t.title) LIKE LOWER($${paramIndex}) OR LOWER(t.description) LIKE LOWER($${paramIndex + 1}))`;
queryParams.push(`%${search}%`, `%${search}%`);
paramIndex += 2;
}
// Add pagination
const limitClause = `LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
queryParams.push(limit, offset);
const query = `
SELECT
t.*,
COUNT(tf.id) as feature_count,
AVG(tf.user_rating) as avg_rating
FROM templates t
LEFT JOIN template_features tf ON t.id = tf.template_id
${whereClause}
GROUP BY t.id
ORDER BY t.created_at DESC, t.title
${limitClause}
`;
console.log('🔍 [ADMIN-TEMPLATES] Executing query:', query);
console.log('📊 [ADMIN-TEMPLATES] Query params:', queryParams);
const result = await database.query(query, queryParams);
const templates = result.rows.map(row => ({
id: row.id,
type: row.type,
title: row.title,
description: row.description,
icon: row.icon,
category: row.category,
gradient: row.gradient,
border: row.border,
text: row.text,
subtext: row.subtext,
is_active: row.is_active,
created_at: row.created_at,
updated_at: row.updated_at,
feature_count: parseInt(row.feature_count) || 0,
avg_rating: parseFloat(row.avg_rating) || 0
}));
// Get total count for pagination
let countWhereClause = 'WHERE is_active = true AND type != \'_migration_test\'';
let countParams = [];
let countParamIndex = 1;
if (category && category !== 'all') {
countWhereClause += ` AND category = $${countParamIndex}`;
countParams.push(category);
countParamIndex++;
}
if (search) {
countWhereClause += ` AND (LOWER(title) LIKE LOWER($${countParamIndex}) OR LOWER(description) LIKE LOWER($${countParamIndex + 1}))`;
countParams.push(`%${search}%`, `%${search}%`);
}
const countQuery = `SELECT COUNT(*) as total FROM templates ${countWhereClause}`;
const countResult = await database.query(countQuery, countParams);
const total = parseInt(countResult.rows[0].total);
console.log('✅ [ADMIN-TEMPLATES] Found templates:', {
returned: templates.length,
total,
hasMore: offset + templates.length < total
});
res.json({
success: true,
data: templates,
pagination: {
total,
limit,
offset,
hasMore: offset + templates.length < total
},
message: `Found ${templates.length} templates for admin management`
});
} catch (error) {
console.error('❌ [ADMIN-TEMPLATES] Error fetching templates:', error.message);
res.status(500).json({
success: false,
error: 'Failed to fetch admin templates',
message: error.message
});
}
});
// GET /api/admin/templates/stats - Get template statistics for admin
router.get('/stats', async (req, res) => {
try {
console.log('📊 [ADMIN-TEMPLATES] Fetching template statistics...');
const statsQuery = `
SELECT
COUNT(*) as total_templates,
COUNT(DISTINCT category) as total_categories,
AVG(feature_counts.feature_count) as avg_features_per_template,
COUNT(CASE WHEN feature_counts.feature_count = 0 THEN 1 END) as templates_without_features,
COUNT(CASE WHEN feature_counts.feature_count > 0 THEN 1 END) as templates_with_features
FROM (
SELECT
t.id,
t.category,
COUNT(tf.id) as feature_count
FROM templates t
LEFT JOIN template_features tf ON t.id = tf.template_id
WHERE t.is_active = true AND t.type != '_migration_test'
GROUP BY t.id, t.category
) feature_counts
`;
const categoryStatsQuery = `
SELECT
category,
COUNT(*) as template_count,
AVG(feature_counts.feature_count) as avg_features
FROM (
SELECT
t.id,
t.category,
COUNT(tf.id) as feature_count
FROM templates t
LEFT JOIN template_features tf ON t.id = tf.template_id
WHERE t.is_active = true AND t.type != '_migration_test'
GROUP BY t.id, t.category
) feature_counts
GROUP BY category
ORDER BY template_count DESC
`;
const [statsResult, categoryStatsResult] = await Promise.all([
database.query(statsQuery),
database.query(categoryStatsQuery)
]);
const stats = {
...statsResult.rows[0],
avg_features_per_template: parseFloat(statsResult.rows[0].avg_features_per_template) || 0,
categories: categoryStatsResult.rows.map(row => ({
category: row.category,
template_count: parseInt(row.template_count),
avg_features: parseFloat(row.avg_features) || 0
}))
};
console.log('✅ [ADMIN-TEMPLATES] Template statistics:', stats);
res.json({
success: true,
data: stats,
message: 'Template statistics retrieved successfully'
});
} catch (error) {
console.error('❌ [ADMIN-TEMPLATES] Error fetching template stats:', error.message);
res.status(500).json({
success: false,
error: 'Failed to fetch template statistics',
message: error.message
});
}
});
// GET /api/admin/templates/:id/features - Get features for a template
router.get('/:id/features', async (req, res) => {
try {
const { id } = req.params;
console.log('🔍 [ADMIN-TEMPLATES] Fetching features for template:', id);
// Validate template exists
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`
});
}
// Get features for the template
const featuresQuery = `
SELECT
tf.*,
f.name,
f.description,
f.complexity,
f.business_rules,
f.technical_requirements
FROM template_features tf
LEFT JOIN features f ON tf.feature_id = f.id
WHERE tf.template_id = $1
ORDER BY tf.display_order, tf.created_at
`;
const result = await database.query(featuresQuery, [id]);
const features = result.rows.map(row => ({
id: row.id,
template_id: row.template_id,
feature_id: row.feature_id,
name: row.name,
description: row.description,
feature_type: row.feature_type || 'suggested',
complexity: row.complexity || 'medium',
display_order: row.display_order,
usage_count: row.usage_count || 0,
user_rating: row.user_rating || 0,
is_default: row.is_default || false,
created_by_user: row.created_by_user || false,
created_at: row.created_at,
updated_at: row.updated_at,
business_rules: row.business_rules,
technical_requirements: row.technical_requirements
}));
console.log('✅ [ADMIN-TEMPLATES] Found features:', features.length);
res.json({
success: true,
data: features,
message: `Found ${features.length} features for template '${template.title}'`
});
} catch (error) {
console.error('❌ [ADMIN-TEMPLATES] Error fetching template features:', error.message);
res.status(500).json({
success: false,
error: 'Failed to fetch template features',
message: error.message
});
}
});
// POST /api/admin/templates/:id/features - Add feature to template
router.post('/:id/features', async (req, res) => {
try {
const { id } = req.params;
const featureData = req.body;
console.log(' [ADMIN-TEMPLATES] Adding feature to template:', id);
console.log('📋 [ADMIN-TEMPLATES] Feature data:', featureData);
// Validate template exists
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`
});
}
// Create the feature in template_features table
const insertQuery = `
INSERT INTO template_features (
template_id, name, description, feature_type, complexity,
display_order, is_default, created_by_user, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), NOW())
RETURNING *
`;
const displayOrder = template.features ? template.features.length + 1 : 1;
const result = await database.query(insertQuery, [
id,
featureData.name,
featureData.description || '',
featureData.feature_type || 'custom',
featureData.complexity || 'medium',
displayOrder,
featureData.is_default || false,
featureData.created_by_user || true
]);
const feature = result.rows[0];
console.log('✅ [ADMIN-TEMPLATES] Feature created:', feature.id);
res.status(201).json({
success: true,
data: feature,
message: `Feature '${feature.name}' added to template '${template.title}'`
});
} catch (error) {
console.error('❌ [ADMIN-TEMPLATES] Error adding feature:', error.message);
res.status(500).json({
success: false,
error: 'Failed to add feature to template',
message: error.message
});
}
});
// PUT /api/admin/templates/:templateId/features/:featureId - Update feature
router.put('/:templateId/features/:featureId', async (req, res) => {
try {
const { templateId, featureId } = req.params;
const updateData = req.body;
console.log('✏️ [ADMIN-TEMPLATES] Updating feature:', featureId, 'in template:', templateId);
// Validate template exists
const template = await Template.getByIdWithFeatures(templateId);
if (!template) {
return res.status(404).json({
success: false,
error: 'Template not found',
message: `Template with ID ${templateId} does not exist`
});
}
// Update the feature in template_features table
const updateQuery = `
UPDATE template_features
SET name = $1, description = $2, complexity = $3, updated_at = NOW()
WHERE id = $4 AND template_id = $5
RETURNING *
`;
const result = await database.query(updateQuery, [
updateData.name,
updateData.description || '',
updateData.complexity || 'medium',
featureId,
templateId
]);
if (result.rows.length === 0) {
return res.status(404).json({
success: false,
error: 'Feature not found',
message: `Feature with ID ${featureId} does not exist in template ${templateId}`
});
}
const updatedFeature = result.rows[0];
console.log('✅ [ADMIN-TEMPLATES] Feature updated:', updatedFeature.id);
res.json({
success: true,
data: updatedFeature,
message: `Feature '${updatedFeature.name}' updated successfully`
});
} catch (error) {
console.error('❌ [ADMIN-TEMPLATES] Error updating feature:', error.message);
res.status(500).json({
success: false,
error: 'Failed to update feature',
message: error.message
});
}
});
// DELETE /api/admin/templates/:templateId/features/:featureId - Remove feature
router.delete('/:templateId/features/:featureId', async (req, res) => {
try {
const { templateId, featureId } = req.params;
console.log('🗑️ [ADMIN-TEMPLATES] Removing feature:', featureId, 'from template:', templateId);
// Validate template exists
const template = await Template.getByIdWithFeatures(templateId);
if (!template) {
return res.status(404).json({
success: false,
error: 'Template not found',
message: `Template with ID ${templateId} does not exist`
});
}
// Delete the feature from template_features table
const deleteQuery = `
DELETE FROM template_features
WHERE id = $1 AND template_id = $2
RETURNING id
`;
const result = await database.query(deleteQuery, [featureId, templateId]);
if (result.rows.length === 0) {
return res.status(404).json({
success: false,
error: 'Feature not found',
message: `Feature with ID ${featureId} does not exist in template ${templateId}`
});
}
console.log('✅ [ADMIN-TEMPLATES] Feature deleted:', featureId);
res.json({
success: true,
message: 'Feature removed successfully'
});
} catch (error) {
console.error('❌ [ADMIN-TEMPLATES] Error removing feature:', error.message);
res.status(500).json({
success: false,
error: 'Failed to remove feature',
message: error.message
});
}
});
// POST /api/admin/templates/:id/features/bulk - Bulk add features to template
router.post('/:id/features/bulk', async (req, res) => {
try {
const { id } = req.params;
const { features } = req.body;
console.log('📦 [ADMIN-TEMPLATES] Bulk adding features to template:', id);
console.log('📋 [ADMIN-TEMPLATES] Features count:', features?.length || 0);
if (!features || !Array.isArray(features) || features.length === 0) {
return res.status(400).json({
success: false,
error: 'Invalid features data',
message: 'Features array is required and must not be empty'
});
}
// Validate template exists
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`
});
}
// Create all features in template_features table
const createdFeatures = [];
let displayOrder = template.features ? template.features.length + 1 : 1;
for (const featureData of features) {
try {
const insertQuery = `
INSERT INTO template_features (
template_id, name, description, feature_type, complexity,
display_order, is_default, created_by_user, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), NOW())
RETURNING *
`;
const result = await database.query(insertQuery, [
id,
featureData.name,
featureData.description || '',
featureData.feature_type || 'custom',
featureData.complexity || 'medium',
displayOrder++,
featureData.is_default || false,
featureData.created_by_user || true
]);
createdFeatures.push(result.rows[0]);
} catch (featureError) {
console.error('⚠️ [ADMIN-TEMPLATES] Error creating feature:', featureData.name, featureError.message);
// Continue with other features instead of failing completely
}
}
console.log('✅ [ADMIN-TEMPLATES] Bulk features created:', createdFeatures.length, 'out of', features.length);
res.status(201).json({
success: true,
data: createdFeatures,
message: `${createdFeatures.length} features added to template '${template.title}'`
});
} catch (error) {
console.error('❌ [ADMIN-TEMPLATES] Error bulk adding features:', error.message);
res.status(500).json({
success: false,
error: 'Failed to bulk add features',
message: error.message
});
}
});
module.exports = router;

View File

@ -2,10 +2,12 @@ const express = require('express');
const router = express.Router();
const Feature = require('../models/feature');
const CustomFeature = require('../models/custom_feature');
const FeatureRule = require('../models/feature_rule');
const AdminNotification = require('../models/admin_notification');
const FeatureSimilarityService = require('../services/feature_similarity');
const database = require('../config/database');
const { v4: uuidv4 } = require('uuid');
const FeatureBusinessRules = require('../models/feature_business_rules');
// Initialize similarity service
const similarityService = new FeatureSimilarityService();
@ -196,30 +198,17 @@ 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', 'complexity'];
for (const field of requiredFields) {
if (!featureData[field]) {
return res.status(400).json({
success: false,
error: 'Validation error',
message: `Field '${field}' is required`
});
return res.status(400).json({ success: false, error: 'Validation error', message: `Field '${field}' is required` });
}
}
// Validate complexity
const validComplexity = ['low', 'medium', 'high'];
if (!validComplexity.includes(featureData.complexity)) {
return res.status(400).json({
success: false,
error: 'Invalid complexity',
message: `Complexity must be one of: ${validComplexity.join(', ')}`
});
return res.status(400).json({ success: false, error: 'Invalid complexity', message: `Complexity must be one of: ${validComplexity.join(', ')}` });
}
// Create feature directly in template_features table
const feature = await Feature.create({
template_id: featureData.template_id,
feature_id: featureData.feature_id || `feature_${uuidv4()}`,
@ -227,35 +216,28 @@ router.post('/', async (req, res) => {
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 in template_features table`
});
// Persist aggregated rules
try {
const rules = Array.isArray(featureData.logic_rules)
? featureData.logic_rules
: featureData.business_rules ?? [];
await FeatureBusinessRules.upsert(feature.template_id, feature.feature_id, rules);
} catch (ruleErr) {
console.error('⚠️ Failed to persist feature business rules:', ruleErr.message);
}
res.status(201).json({ success: true, data: feature, message: `Feature '${feature.name}' created successfully in template_features table` });
} catch (error) {
console.error('❌ Error creating feature:', error.message);
// Handle unique constraint violation
if (error.code === '23505') {
return res.status(409).json({
success: false,
error: 'Feature already exists',
message: 'A feature with this ID already exists for this template'
});
return res.status(409).json({ success: false, error: 'Feature already exists', message: 'A feature with this ID already exists for this template' });
}
res.status(500).json({
success: false,
error: 'Failed to create feature',
message: error.message
});
res.status(500).json({ success: false, error: 'Failed to create feature', message: error.message });
}
});
@ -432,13 +414,7 @@ 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
})
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]) {
@ -450,39 +426,25 @@ 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`
})
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 {
const duplicateCheck = await similarityService.checkForDuplicates(data.name, 0.8);
if (duplicateCheck.isDuplicate) {
similarityInfo = {
isDuplicate: true,
canonicalFeature: duplicateCheck.canonicalFeature,
similarityScore: duplicateCheck.similarityScore,
matchType: duplicateCheck.matchType
};
similarityInfo = { isDuplicate: true, canonicalFeature: duplicateCheck.canonicalFeature, similarityScore: duplicateCheck.similarityScore, matchType: duplicateCheck.matchType };
}
} catch (similarityError) {
console.error('Error checking for duplicates:', similarityError.message);
// Continue with feature creation even if similarity check fails
}
const created = await CustomFeature.create({
@ -491,8 +453,6 @@ router.post('/custom', async (req, res) => {
name: data.name,
description: data.description,
complexity: data.complexity,
business_rules: data.business_rules,
technical_requirements: data.technical_requirements,
approved: false,
usage_count: 1,
created_by_user_session: data.created_by_user_session,
@ -501,14 +461,8 @@ router.post('/custom', async (req, res) => {
canonical_feature_id: similarityInfo?.canonicalFeature?.id || null,
})
// Create admin notification for new feature
try {
await AdminNotification.notifyNewFeature(created.id, created.name);
} catch (notificationError) {
console.error('⚠️ Failed to create admin notification:', notificationError.message);
}
try { await AdminNotification.notifyNewFeature(created.id, created.name); } catch (e) { console.error('⚠️ Failed to create admin notification:', e.message); }
// Mirror into template_features with stable feature_id
try {
await Feature.create({
template_id: data.template_id,
@ -521,22 +475,18 @@ router.post('/custom', async (req, res) => {
is_default: false,
created_by_user: true
})
} catch (mirrorErr) {
console.error('Failed to mirror custom feature into template_features:', mirrorErr.message)
}
const response = {
success: true,
data: created,
message: `Custom feature '${created.name}' created successfully and submitted for admin review`
};
// Include similarity info in response if duplicates were found
if (similarityInfo) {
response.similarityInfo = similarityInfo;
response.message += '. Similar features were found and will be reviewed by admin.';
} catch (mirrorErr) { console.error('Failed to mirror custom feature into template_features:', mirrorErr.message) }
// Persist aggregated rules
try {
const rules = Array.isArray(data.logic_rules) ? data.logic_rules : data.business_rules ?? [];
await FeatureBusinessRules.upsert(data.template_id, `custom_${created.id}`, rules);
} catch (ruleErr) {
console.error('⚠️ Failed to persist custom feature business rules:', ruleErr.message);
}
const response = { success: true, data: created, message: `Custom feature '${created.name}' created successfully and submitted for admin review` };
if (similarityInfo) { response.similarityInfo = similarityInfo; response.message += '. Similar features were found and will be reviewed by admin.'; }
return res.status(201).json(response);
} catch (e) {
console.error('Error creating custom feature:', e.message)
@ -573,7 +523,7 @@ router.get('/templates/:templateId/features', async (req, res) => {
}
});
// PUT /api/custom-features/:id - update custom feature
// PUT /api/features/custom/:id - update custom feature
router.put('/custom/:id', async (req, res) => {
try {
const { id } = req.params;
@ -601,7 +551,7 @@ router.put('/custom/:id', async (req, res) => {
}
});
// DELETE /api/custom-features/:id - delete custom feature
// DELETE /api/features/custom/:id - delete custom feature
router.delete('/custom/:id', async (req, res) => {
try {
const { id } = req.params;

View File

@ -45,10 +45,10 @@ const corsOptions = {
origin: function (origin, callback) {
// Allow requests from your web-dashboard and other services
const allowedOrigins = [
'http://localhost:3000', // Web dashboard (changed from 3000 to 3000 to avoid conflict with other services)
'http://localhost:3001', // Frontend (CodeNuk)
'http://localhost:3000', // Alternative frontend port
'http://localhost:8008', // Dashboard service
'http://localhost:8000', // API Gateway
'http://localhost:3000', // Development React
process.env.FRONTEND_URL
].filter(Boolean);
@ -213,7 +213,13 @@ process.on('unhandledRejection', (reason, promise) => {
// ========================================
// Background cleanup task (runs every hour)
// Controlled by AUTH_CLEANUP_ENABLED env. Set to 'false' to disable automatic DB cleanup.
const startBackgroundTasks = () => {
const enabled = (process.env.AUTH_CLEANUP_ENABLED || 'true').toLowerCase() !== 'false';
if (!enabled) {
console.log('⏸️ Background auth cleanup is disabled (AUTH_CLEANUP_ENABLED=false)');
return;
}
setInterval(async () => {
try {
console.log('🧹 Running background auth cleanup...');
@ -234,7 +240,7 @@ app.listen(PORT, '0.0.0.0', () => {
console.log('🔐 JWT-based authentication ready!');
console.log('👥 User registration and feature preferences enabled');
// Start background tasks
// Start background tasks (may be disabled by env)
startBackgroundTasks();
console.log('⏰ Background cleanup tasks scheduled');

View File

@ -109,24 +109,24 @@ const createRateLimit = (windowMs, max, message) => {
// Specific rate limiters
const loginRateLimit = createRateLimit(
15 * 60 * 1000, // 15 minutes
5, // 5 attempts
'Too many login attempts. Please try again in 15 minutes.'
10000, // 5 attempts
'Too many login attempts. Please try again.'
);
const registerRateLimit = createRateLimit(
60 * 60 * 1000, // 1 hour
3, // 3 registrations
10000, // 3 registrations
'Too many registration attempts. Please try again in 1 hour.'
);
const passwordChangeRateLimit = createRateLimit(
60 * 60 * 1000, // 1 hour
3, // 3 password changes
10000, // 3 password changes
'Too many password change attempts. Please try again in 1 hour.'
);
const apiRateLimit = createRateLimit(
15 * 60 * 10000, // 15 minutes
15 * 60 * 1000, // 15 minutes
10000, // 100 requests
'Too many API requests. Please slow down.'
);

View File

@ -27,7 +27,7 @@ router.use(logAuthRequests);
// ========================================
// POST /api/auth/register - Register new user
router.post('/register', /*registerRateLimit,*/ validateRegistration, async (req, res) => {
router.post('/register', registerRateLimit, validateRegistration, async (req, res) => {
try {
const { username, email, password, first_name, last_name } = req.body;
@ -65,25 +65,25 @@ router.get('/verify-email', async (req, res) => {
const { token } = req.query;
await authService.verifyEmailToken(token);
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3001';
const redirectUrl = `${frontendUrl}/signin?verified=true`;
// JSON fallback if not a browser navigation
if (req.get('Accept') && req.get('Accept').includes('application/json')) {
// Prefer redirect by default; only return JSON if explicitly requested
if (req.query.format === 'json') {
return res.json({ success: true, message: 'Email verified successfully', redirect: redirectUrl });
}
return res.redirect(302, redirectUrl);
} catch (error) {
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3001';
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 });
if (req.query.format === 'json') {
return res.status(400).json({ success: false, message: error.message, redirect: redirectUrl });
}
return res.redirect(302, redirectUrl);
}
});
// POST /api/auth/login - User login
router.post('/login', /*loginRateLimit , */validateLogin, async (req, res) => {
router.post('/login', loginRateLimit , validateLogin, async (req, res) => {
try {
const { email, password } = req.body;
console.log('🔑 Login attempt for: ', email, password);
@ -570,11 +570,11 @@ router.post('/admin/custom-features/:id/review', authenticateToken, requireAdmin
const { id } = req.params;
const { status, admin_notes } = req.body;
if (!['approved', 'rejected', 'pending'].includes(status)) {
if (!['approved', 'rejected', 'duplicate'].includes(status)) {
return res.status(400).json({
success: false,
error: 'Invalid status',
message: 'Status must be approved, rejected, or pending'
message: 'Status must be approved, rejected, or duplicate'
});
}
@ -605,6 +605,46 @@ router.post('/admin/custom-features/:id/review', authenticateToken, requireAdmin
}
});
// PUT /api/auth/admin/custom-features/:id/review - Review custom feature (Admin only)
router.put('/admin/custom-features/:id/review', authenticateToken, requireAdmin, async (req, res) => {
try {
const { id } = req.params;
const { status, admin_notes } = req.body;
if (!['approved', 'rejected', 'duplicate'].includes(status)) {
return res.status(400).json({
success: false,
error: 'Invalid status',
message: 'Status must be approved, rejected, or duplicate'
});
}
console.log(`📝 (PUT) Admin reviewing custom feature ${id} - status: ${status}`);
const reviewData = {
status,
admin_notes,
admin_reviewed_by: req.user.id
};
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 (PUT):', 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 {
@ -636,11 +676,11 @@ router.post('/admin/custom-templates/:id/review', authenticateToken, requireAdmi
const { id } = req.params;
const { status, admin_notes } = req.body;
if (!['approved', 'rejected', 'pending'].includes(status)) {
if (!['approved', 'rejected', 'duplicate'].includes(status)) {
return res.status(400).json({
success: false,
error: 'Invalid status',
message: 'Status must be approved, rejected, or pending'
message: 'Status must be approved, rejected, or duplicate'
});
}
@ -671,4 +711,44 @@ router.post('/admin/custom-templates/:id/review', authenticateToken, requireAdmi
}
});
// PUT /api/auth/admin/custom-templates/:id/review - Review custom template (Admin only)
router.put('/admin/custom-templates/:id/review', authenticateToken, requireAdmin, async (req, res) => {
try {
const { id } = req.params;
const { status, admin_notes } = req.body;
if (!['approved', 'rejected', 'duplicate'].includes(status)) {
return res.status(400).json({
success: false,
error: 'Invalid status',
message: 'Status must be approved, rejected, or duplicate'
});
}
console.log(`📝 (PUT) Admin reviewing custom template ${id} - status: ${status}`);
const reviewData = {
status,
admin_notes,
admin_reviewed_by: req.user.id
};
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 (PUT):', error.message);
res.status(503).json({
success: false,
error: 'Service unavailable',
message: error.message
});
}
});
module.exports = router;

View File

@ -148,8 +148,9 @@ class AuthService {
async sendVerificationEmail(user) {
const token = await this.createEmailVerificationToken(user.id);
const serviceBaseUrl = process.env.AUTH_PUBLIC_URL || `http://localhost:${process.env.PORT || 8011}`;
const verifyUrl = `${serviceBaseUrl}/api/auth/verify-email?token=${encodeURIComponent(token)}`;
// Send users to the frontend verification page; the frontend will call the backend and handle redirects
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3001';
const verifyUrl = `${frontendUrl}/verify-email?token=${encodeURIComponent(token)}`;
const today = new Date();
const dateString = today.toLocaleDateString('en-US');
@ -217,9 +218,8 @@ class AuthService {
throw new Error('Invalid refresh token');
}
// Check if token exists and is not revoked
const tokenHash = await this.hashToken(refreshToken);
const storedToken = await this.getRefreshToken(tokenHash);
// Check if token exists and is not revoked (support deterministic + legacy bcrypt storage)
const storedToken = await this.findStoredRefreshToken(decoded.userId, refreshToken);
if (!storedToken || storedToken.is_revoked) {
throw new Error('Refresh token is revoked or invalid');
@ -239,7 +239,7 @@ class AuthService {
const tokens = jwtConfig.generateTokenPair(user);
// Revoke old refresh token and store new one
await this.revokeRefreshToken(tokenHash);
await this.revokeRefreshTokenById(storedToken.id);
await this.storeRefreshToken(user.id, tokens.refreshToken);
console.log(`🔄 Token refreshed for user: ${user.email}`);
@ -253,8 +253,15 @@ class AuthService {
// Logout user
async logout(refreshToken, sessionToken = null) {
if (refreshToken) {
const tokenHash = await this.hashToken(refreshToken);
await this.revokeRefreshToken(tokenHash);
try {
const decoded = jwtConfig.verifyRefreshToken(refreshToken);
const storedToken = await this.findStoredRefreshToken(decoded.userId, refreshToken);
if (storedToken) {
await this.revokeRefreshTokenById(storedToken.id);
}
} catch (e) {
console.warn('⚠️ Logout could not find refresh token to revoke:', e.message);
}
}
if (sessionToken) {
@ -267,7 +274,7 @@ class AuthService {
// Store refresh token
async storeRefreshToken(userId, refreshToken) {
const tokenHash = await this.hashToken(refreshToken);
const tokenHash = this.hashDeterministic(refreshToken);
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
const id = uuidv4();
@ -303,6 +310,16 @@ class AuthService {
await database.query(query, [tokenHash]);
}
// Revoke refresh token by id (preferred)
async revokeRefreshTokenById(id) {
const query = `
UPDATE refresh_tokens
SET is_revoked = true, revoked_at = NOW()
WHERE id = $1
`;
await database.query(query, [id]);
}
// Create user session
async createSession(userId, sessionInfo) {
const sessionToken = uuidv4();
@ -388,6 +405,40 @@ class AuthService {
return await bcrypt.hash(token, saltRounds);
}
// Find stored refresh token using deterministic SHA-256 first, then legacy bcrypt compare
async findStoredRefreshToken(userId, refreshToken) {
const sha256 = this.hashDeterministic(refreshToken);
// Try deterministic exact match
let result = await database.query(
`SELECT * FROM refresh_tokens WHERE token_hash = $1 LIMIT 1`,
[sha256]
);
if (result.rows.length > 0) {
return result.rows[0];
}
// Fallback: try to match legacy bcrypt-hashed tokens for this user
const candidates = await database.query(
`SELECT * FROM refresh_tokens
WHERE user_id = $1 AND is_revoked = false AND expires_at > NOW()
ORDER BY created_at DESC LIMIT 100`,
[userId]
);
for (const row of candidates.rows) {
try {
if (row.token_hash && row.token_hash.startsWith('$2')) { // bcrypt hashed
const match = await bcrypt.compare(refreshToken, row.token_hash);
if (match) {
return row;
}
}
} catch (err) {
// ignore and continue
}
}
return null;
}
// Cleanup expired tokens and sessions
async cleanup() {
console.log('🧹 Starting auth cleanup...');

View File

@ -15,7 +15,7 @@ class ServiceClient {
headers.Authorization = `Bearer ${authToken}`;
}
const response = await axios.get(`${this.templateManagerUrl}/api/templates/admin/custom-features`, {
const response = await axios.get(`${this.templateManagerUrl}/api/admin/custom-features`, {
params,
headers,
timeout: 5000
@ -36,7 +36,7 @@ class ServiceClient {
}
const response = await axios.post(
`${this.templateManagerUrl}/api/templates/admin/custom-features/${id}/review`,
`${this.templateManagerUrl}/api/admin/custom-features/${id}/review`,
reviewData,
{ headers, timeout: 5000 }
);
@ -58,7 +58,7 @@ class ServiceClient {
headers.Authorization = `Bearer ${authToken}`;
}
const response = await axios.get(`${this.templateManagerUrl}/api/templates/admin/custom-templates`, {
const response = await axios.get(`${this.templateManagerUrl}/api/admin/custom-templates`, {
params,
headers,
timeout: 5000
@ -79,7 +79,7 @@ class ServiceClient {
}
const response = await axios.post(
`${this.templateManagerUrl}/api/templates/admin/custom-templates/${id}/review`,
`${this.templateManagerUrl}/api/admin/custom-templates/${id}/review`,
reviewData,
{ headers, timeout: 5000 }
);