admin changes
This commit is contained in:
parent
047f1266b9
commit
cdc68e7ae6
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
8
package-lock.json
generated
@ -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
41
services/ai-mockup-service/.gitignore
vendored
Normal 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
|
||||
114
services/ai-mockup-service/AUTH_FIX_SUMMARY.md
Normal file
114
services/ai-mockup-service/AUTH_FIX_SUMMARY.md
Normal 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
|
||||
39
services/ai-mockup-service/Dockerfile
Normal file
39
services/ai-mockup-service/Dockerfile
Normal 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"]
|
||||
228
services/ai-mockup-service/WIREFRAME_SAVING_GUIDE.md
Normal file
228
services/ai-mockup-service/WIREFRAME_SAVING_GUIDE.md
Normal 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
|
||||
227
services/ai-mockup-service/docs/IMPLEMENTATION_SUMMARY.md
Normal file
227
services/ai-mockup-service/docs/IMPLEMENTATION_SUMMARY.md
Normal 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.
|
||||
464
services/ai-mockup-service/docs/INTEGRATION_GUIDE.md
Normal file
464
services/ai-mockup-service/docs/INTEGRATION_GUIDE.md
Normal 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.**
|
||||
271
services/ai-mockup-service/docs/UI_Controller.md
Normal file
271
services/ai-mockup-service/docs/UI_Controller.md
Normal file
@ -0,0 +1,271 @@
|
||||
Here’s 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.
|
||||
|
||||
We’ve 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.
|
||||
|
||||
Here’s how you can integrate the two:
|
||||
|
||||
---
|
||||
|
||||
## 🔹 Integration Plan
|
||||
|
||||
1. **Extend your current tldraw setup**
|
||||
|
||||
* Right now your app renders `<Tldraw />` with AI-generated wireframes.
|
||||
* You’ll register your **10 custom controllers (shapes)** into the same editor.
|
||||
|
||||
2. **Add Controllers Palette**
|
||||
|
||||
* Create a sidebar/panel with the controllers (like Balsamiq’s 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?
|
||||
214
services/ai-mockup-service/docs/WIREFRAME_PERSISTENCE_README.md
Normal file
214
services/ai-mockup-service/docs/WIREFRAME_PERSISTENCE_README.md
Normal 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
|
||||
9
services/ai-mockup-service/requirements.txt
Normal file
9
services/ai-mockup-service/requirements.txt
Normal 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
|
||||
90
services/ai-mockup-service/scripts/quick-start.bat
Normal file
90
services/ai-mockup-service/scripts/quick-start.bat
Normal 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
|
||||
96
services/ai-mockup-service/scripts/quick-start.sh
Normal file
96
services/ai-mockup-service/scripts/quick-start.sh
Normal 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
|
||||
102
services/ai-mockup-service/scripts/test-tldraw-props.tsx
Normal file
102
services/ai-mockup-service/scripts/test-tldraw-props.tsx
Normal 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)
|
||||
}
|
||||
}
|
||||
399
services/ai-mockup-service/src/README.md
Normal file
399
services/ai-mockup-service/src/README.md
Normal 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.
|
||||
183
services/ai-mockup-service/src/SETUP.md
Normal file
183
services/ai-mockup-service/src/SETUP.md
Normal 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! 🎨✨
|
||||
1523
services/ai-mockup-service/src/app.py
Normal file
1523
services/ai-mockup-service/src/app.py
Normal file
File diff suppressed because it is too large
Load Diff
17
services/ai-mockup-service/src/env.example
Normal file
17
services/ai-mockup-service/src/env.example
Normal 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
|
||||
124
services/ai-mockup-service/src/migrate_database.py
Normal file
124
services/ai-mockup-service/src/migrate_database.py
Normal 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.")
|
||||
73
services/ai-mockup-service/src/run.py
Normal file
73
services/ai-mockup-service/src/run.py
Normal 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()
|
||||
97
services/ai-mockup-service/src/setup_database.py
Normal file
97
services/ai-mockup-service/src/setup_database.py
Normal 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.")
|
||||
190
services/ai-mockup-service/src/sql/001_user_auth_schema.sql
Normal file
190
services/ai-mockup-service/src/sql/001_user_auth_schema.sql
Normal 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;
|
||||
164
services/ai-mockup-service/src/sql/002_wireframe_schema.sql
Normal file
164
services/ai-mockup-service/src/sql/002_wireframe_schema.sql
Normal 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;
|
||||
35
services/ai-mockup-service/src/start_backend.py
Normal file
35
services/ai-mockup-service/src/start_backend.py
Normal 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)
|
||||
188
services/ai-mockup-service/src/test_api.py
Normal file
188
services/ai-mockup-service/src/test_api.py
Normal 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)
|
||||
172
services/ai-mockup-service/src/test_auth.py
Normal file
172
services/ai-mockup-service/src/test_auth.py
Normal 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())
|
||||
133
services/ai-mockup-service/src/test_db_connection.py
Normal file
133
services/ai-mockup-service/src/test_db_connection.py
Normal 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")
|
||||
277
services/ai-mockup-service/src/test_integration.py
Normal file
277
services/ai-mockup-service/src/test_integration.py
Normal 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()
|
||||
127
services/ai-mockup-service/src/test_svg.py
Normal file
127
services/ai-mockup-service/src/test_svg.py
Normal 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)")
|
||||
9
services/ai-mockup-service/start.bat
Normal file
9
services/ai-mockup-service/start.bat
Normal 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
|
||||
6
services/ai-mockup-service/start.sh
Normal file
6
services/ai-mockup-service/start.sh
Normal 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
|
||||
|
||||
@ -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
|
||||
52
services/api-gateway/src/middleware/cors.js
Normal file
52
services/api-gateway/src/middleware/cors.js
Normal 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;
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 $$;
|
||||
|
||||
|
||||
@ -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 $$;
|
||||
|
||||
|
||||
@ -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 $$;
|
||||
|
||||
|
||||
@ -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
|
||||
`);
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
123
services/template-manager/src/models/feature_rule.js
Normal file
123
services/template-manager/src/models/feature_rule.js
Normal 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;
|
||||
|
||||
|
||||
519
services/template-manager/src/routes/admin-templates.js
Normal file
519
services/template-manager/src/routes/admin-templates.js
Normal 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;
|
||||
@ -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;
|
||||
|
||||
@ -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');
|
||||
|
||||
|
||||
@ -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.'
|
||||
);
|
||||
|
||||
@ -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;
|
||||
@ -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...');
|
||||
|
||||
@ -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 }
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user