Resolve merge conflicts and integrate click-counter with existing SaaS repo
- Keep click-counter specific configuration - Merge gitignore rules from both projects - Preserve click-counter deployment documentation - Maintain docker-compose setup for click-counter app 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
commit
58e079b08f
68 changed files with 16562 additions and 27 deletions
20
.env.example
Normal file
20
.env.example
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Copy this file to .env and update with your values
|
||||
|
||||
# Database
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=saas_automation
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=password
|
||||
|
||||
# Backend
|
||||
JWT_SECRET=your_super_secret_jwt_key_change_this
|
||||
JWT_EXPIRES_IN=24h
|
||||
PORT=3001
|
||||
|
||||
# Frontend
|
||||
NEXT_PUBLIC_API_URL=http://localhost:3001
|
||||
|
||||
# n8n
|
||||
N8N_BASE_URL=http://localhost:5678
|
||||
N8N_API_KEY=your_n8n_api_key
|
||||
74
.gitignore
vendored
74
.gitignore
vendored
|
|
@ -3,6 +3,29 @@ node_modules/
|
|||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
*/node_modules/
|
||||
frontend/node_modules/
|
||||
backend/node_modules/
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
.env.development
|
||||
*.env
|
||||
|
||||
# Build outputs
|
||||
frontend/.next/
|
||||
frontend/out/
|
||||
backend/dist/
|
||||
backend/build/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
|
|
@ -38,18 +61,17 @@ typings/
|
|||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# ESLint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
|
|
@ -59,13 +81,7 @@ typings/
|
|||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# dotenv environment variables file (already covered above)
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
|
@ -80,6 +96,7 @@ dist
|
|||
# Gatsby files
|
||||
.cache/
|
||||
# public (commented out for this project as we need to track it)
|
||||
# public
|
||||
|
||||
# Storybook build outputs
|
||||
.out
|
||||
|
|
@ -89,24 +106,14 @@ dist
|
|||
tmp/
|
||||
temp/
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# IDE
|
||||
# Editor directories and files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
|
|
@ -115,17 +122,30 @@ pids
|
|||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Docker
|
||||
# Docker
|
||||
.dockerignore
|
||||
postgres_data/
|
||||
*.dockerignore
|
||||
|
||||
# Git
|
||||
.git/
|
||||
# Database
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Archives
|
||||
# Backup files
|
||||
*.backup
|
||||
*.bak
|
||||
*.tmp
|
||||
|
||||
# Archive files
|
||||
*.tar.gz
|
||||
*.zip
|
||||
*.rar
|
||||
|
||||
# Git
|
||||
.git/
|
||||
|
||||
# Project specific
|
||||
saas-automation-platform/
|
||||
aimpress-automation-hub.tar.gz
|
||||
|
|
@ -133,4 +153,4 @@ saas-deploy.tar.gz
|
|||
*-deploy.tar.gz
|
||||
|
||||
# Claude files
|
||||
.claude/
|
||||
.claude/
|
||||
|
|
|
|||
105
DEMO.md
Normal file
105
DEMO.md
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
# 🚀 Aimpress AutomationHub - Demo Guide
|
||||
|
||||
## Что показать коллегам
|
||||
|
||||
### 📱 **Главная страница** - `/`
|
||||
- Современный landing page с градиентами
|
||||
- Responsive дизайн в стиле n8n
|
||||
- Call-to-action кнопки
|
||||
|
||||
### 🔐 **Авторизация** - `/login`
|
||||
- **Админ логин:**
|
||||
- Email: `info@ai-impress.com`
|
||||
- Password: `admin123`
|
||||
- **Signup** - регистрация новых пользователей
|
||||
|
||||
### 📊 **Admin Panel** - `/admin`
|
||||
- **Analytics Dashboard** с метриками:
|
||||
- 156 пользователей (120 free, 28 pro, 6 enterprise)
|
||||
- 342 workflows, 12,847 выполнений
|
||||
- $18,750 revenue
|
||||
- **User Management** - управление пользователями и лицензиями
|
||||
- **Real-time данные** и графики
|
||||
|
||||
### 🛍️ **Template Marketplace** - `/marketplace`
|
||||
- **6 готовых шаблонов** с эмодзи иконками:
|
||||
- 🔗 Telegram to Slack Bridge
|
||||
- 📧 Gmail to Teams Notifications
|
||||
- 🎧 Customer Support Automation
|
||||
- 📱 Social Media Content Sync
|
||||
- 🛒 E-commerce Order Processing
|
||||
- ☁️ Data Backup & Sync
|
||||
- **Рейтинги, категории, фильтры**
|
||||
- **Функция установки шаблонов**
|
||||
|
||||
### 🔗 **Integrations** - `/integrations`
|
||||
- **10 популярных интеграций:**
|
||||
- Gmail, Slack, Telegram, Microsoft Teams
|
||||
- Instagram, LinkedIn, Shopify, Zoho CRM
|
||||
- Calendly, Webhooks
|
||||
- **Красивые карточки** с градиентами
|
||||
- **Mock подключения** к сервисам
|
||||
|
||||
### 💳 **Billing** - `/billing`
|
||||
- **3 тарифных плана:**
|
||||
- Free (0£/month)
|
||||
- Pro (£150/month)
|
||||
- Enterprise (contact sales)
|
||||
- **Usage tracking** и статистика
|
||||
|
||||
### 🔧 **Dashboard** - `/dashboard`
|
||||
- **Workflow management**
|
||||
- **Statistics overview**
|
||||
- **Quick actions**
|
||||
|
||||
## 🔑 **Key Features для демо:**
|
||||
|
||||
1. **🎨 Визуальный дизайн** - современные градиенты, анимации, hover-эффекты
|
||||
2. **👨💼 Admin функции** - управление пользователями, аналитика, лицензии
|
||||
3. **🛍️ Marketplace** - готовые шаблоны автоматизации
|
||||
4. **🔗 Integrations** - подключение к популярным сервисам
|
||||
5. **📱 Responsive** - работает на всех устройствах
|
||||
6. **⚡ Performance** - быстрая загрузка, плавные анимации
|
||||
|
||||
## 🚀 **Как запустить:**
|
||||
|
||||
```bash
|
||||
# В директории проекта
|
||||
docker-compose up -d
|
||||
|
||||
# Проверить что все запущено
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
**Доступ:**
|
||||
- Frontend: http://localhost:3000
|
||||
- Backend API: http://localhost:3001
|
||||
- Database: PostgreSQL на порту 5432
|
||||
- N8n: http://localhost:5678
|
||||
|
||||
## 📋 **Что показать в первую очередь:**
|
||||
|
||||
1. **Landing page** - красивый дизайн
|
||||
2. **Admin login** - войти как админ
|
||||
3. **Admin Analytics** - впечатляющая статистика
|
||||
4. **Template Marketplace** - готовые шаблоны
|
||||
5. **Integrations** - список подключений
|
||||
6. **User Management** - назначение лицензий
|
||||
|
||||
## 💡 **Фишки для демо:**
|
||||
|
||||
- **Эмодзи иконки** делают интерфейс дружелюбным
|
||||
- **Градиенты** создают современный вид
|
||||
- **Анимации** при hover и загрузке
|
||||
- **Responsive дизайн** работает на мобильных
|
||||
- **Mock данные** выглядят реалистично
|
||||
- **Функциональные кнопки** с feedback
|
||||
|
||||
## 🔧 **Технический стек:**
|
||||
|
||||
- **Frontend:** Next.js 15, TypeScript, Tailwind CSS
|
||||
- **Backend:** Node.js, Express, TypeScript
|
||||
- **Database:** PostgreSQL
|
||||
- **Container:** Docker Compose
|
||||
- **Integration:** N8n workflow automation
|
||||
- **Design:** n8n-inspired modern UI/UX
|
||||
69
README.md
Normal file
69
README.md
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# 🚀 Aimpress AutomationHub
|
||||
|
||||
> **SaaS Automation Platform** - Современная платформа для создания и управления автоматизированными workflow в стиле n8n.io
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## ✨ **Основные возможности**
|
||||
|
||||
### 🎨 **Современный UI/UX**
|
||||
- Дизайн в стиле n8n.io с градиентами и анимациями
|
||||
- Полностью responsive интерфейс
|
||||
- Интуитивная навигация
|
||||
|
||||
### 👨💼 **Admin Panel**
|
||||
- **Real-time аналитика** с красивыми графиками
|
||||
- **Управление пользователями** и назначение лицензий
|
||||
- **Статистика платформы**: 156+ пользователей, 342 workflows, $18K revenue
|
||||
- **Мониторинг активности** в реальном времени
|
||||
|
||||
### 🛍️ **Template Marketplace**
|
||||
- **6 готовых шаблонов** автоматизации с эмодзи иконками
|
||||
- **Рейтинги и отзывы** пользователей
|
||||
- **Категории и фильтры** для удобного поиска
|
||||
- **One-click установка** шаблонов
|
||||
|
||||
### 🔗 **Integrations Hub**
|
||||
- **10+ популярных интеграций**: Gmail, Slack, Telegram, Teams и др.
|
||||
- **Красивые карточки** с уникальными градиентами
|
||||
- **OAuth подключения** к внешним сервисам
|
||||
|
||||
---
|
||||
|
||||
## 🚀 **Быстрый старт**
|
||||
|
||||
### Установка и запуск
|
||||
```bash
|
||||
# Клонировать репозиторий
|
||||
git clone git@github.com:SamoilenkoVadym/SaaS.git
|
||||
cd SaaS
|
||||
|
||||
# Запустить все сервисы
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Доступ к приложению
|
||||
- **🌐 Frontend:** http://localhost:3000
|
||||
- **👨💼 Admin Panel:** http://localhost:3000/admin
|
||||
- **🛍️ Marketplace:** http://localhost:3000/marketplace
|
||||
|
||||
### Тестовые аккаунты
|
||||
**👑 Admin:**
|
||||
- Email: `info@ai-impress.com`
|
||||
- Password: `admin123`
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ **Технический стек**
|
||||
|
||||
- ⚛️ **Next.js 15** с TypeScript
|
||||
- 🟢 **Node.js** с Express
|
||||
- 🐘 **PostgreSQL 15**
|
||||
- 🐳 **Docker Compose**
|
||||
- 🎨 **Tailwind CSS**
|
||||
|
||||
---
|
||||
|
||||
Made with ❤️ by **Aimpress Team**
|
||||
236
VPS-DEPLOY.md
Normal file
236
VPS-DEPLOY.md
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
# 🚀 VPS Deployment - Aimpress AutomationHub
|
||||
|
||||
## 🌐 **Развертывание на VPS сервере 128.140.8.206**
|
||||
|
||||
### Шаг 1: Подключение к серверу
|
||||
```bash
|
||||
# Подключиться к VPS
|
||||
ssh root@128.140.8.206
|
||||
# или
|
||||
ssh your-username@128.140.8.206
|
||||
```
|
||||
|
||||
### Шаг 2: Установка Docker (если не установлен)
|
||||
```bash
|
||||
# Обновить пакеты
|
||||
apt update && apt upgrade -y
|
||||
|
||||
# Установить Docker
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
sh get-docker.sh
|
||||
|
||||
# Установить Docker Compose
|
||||
curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
chmod +x /usr/local/bin/docker-compose
|
||||
|
||||
# Проверить установку
|
||||
docker --version
|
||||
docker-compose --version
|
||||
```
|
||||
|
||||
### Шаг 3: Клонирование проекта
|
||||
```bash
|
||||
# Установить Git (если не установлен)
|
||||
apt install git -y
|
||||
|
||||
# Клонировать репозиторий
|
||||
git clone https://github.com/SamoilenkoVadym/SaaS.git
|
||||
cd SaaS
|
||||
```
|
||||
|
||||
### Шаг 4: Конфигурация для production
|
||||
```bash
|
||||
# Создать production docker-compose
|
||||
cp docker-compose.yml docker-compose.prod.yml
|
||||
|
||||
# Изменить порты для внешнего доступа
|
||||
sed -i 's/- "3000:3000"/- "80:3000"/' docker-compose.prod.yml
|
||||
sed -i 's/- "3001:3001"/- "3001:3001"/' docker-compose.prod.yml
|
||||
```
|
||||
|
||||
### Шаг 5: Настройка переменных окружения
|
||||
```bash
|
||||
# Backend environment
|
||||
cat > backend/.env << EOF
|
||||
PORT=3001
|
||||
DB_HOST=postgres
|
||||
DB_PORT=5432
|
||||
DB_NAME=saas_automation
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=secure_production_password_123
|
||||
JWT_SECRET=your_super_secret_jwt_key_production_123
|
||||
NODE_ENV=production
|
||||
EOF
|
||||
|
||||
# Frontend environment
|
||||
cat > frontend/.env.local << EOF
|
||||
NEXT_PUBLIC_API_URL=http://128.140.8.206:3001
|
||||
EOF
|
||||
```
|
||||
|
||||
### Шаг 6: Запуск приложения
|
||||
```bash
|
||||
# Запустить все сервисы
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
|
||||
# Проверить статус
|
||||
docker-compose -f docker-compose.prod.yml ps
|
||||
|
||||
# Посмотреть логи
|
||||
docker-compose -f docker-compose.prod.yml logs -f
|
||||
```
|
||||
|
||||
### Шаг 7: Настройка firewall (опционально)
|
||||
```bash
|
||||
# Открыть нужные порты
|
||||
ufw allow 80
|
||||
ufw allow 3001
|
||||
ufw allow 22
|
||||
ufw --force enable
|
||||
```
|
||||
|
||||
## 🌐 **Доступ к приложению**
|
||||
|
||||
После успешного развертывания приложение будет доступно:
|
||||
|
||||
- **🌐 Главная страница:** http://128.140.8.206
|
||||
- **👨💼 Admin Panel:** http://128.140.8.206/admin
|
||||
- **🛍️ Marketplace:** http://128.140.8.206/marketplace
|
||||
- **🔗 Integrations:** http://128.140.8.206/integrations
|
||||
- **🔧 Backend API:** http://128.140.8.206:3001
|
||||
|
||||
### 🔐 **Тестовые аккаунты**
|
||||
**Admin:**
|
||||
- Email: `info@ai-impress.com`
|
||||
- Password: `admin123`
|
||||
|
||||
## 📊 **Мониторинг и обслуживание**
|
||||
|
||||
### Проверка статуса
|
||||
```bash
|
||||
# Проверить работающие контейнеры
|
||||
docker ps
|
||||
|
||||
# Проверить логи
|
||||
docker-compose -f docker-compose.prod.yml logs frontend
|
||||
docker-compose -f docker-compose.prod.yml logs backend
|
||||
docker-compose -f docker-compose.prod.yml logs postgres
|
||||
```
|
||||
|
||||
### Обновление приложения
|
||||
```bash
|
||||
# Остановить контейнеры
|
||||
docker-compose -f docker-compose.prod.yml down
|
||||
|
||||
# Обновить код
|
||||
git pull origin main
|
||||
|
||||
# Пересобрать и запустить
|
||||
docker-compose -f docker-compose.prod.yml up -d --build
|
||||
```
|
||||
|
||||
### Резервное копирование базы данных
|
||||
```bash
|
||||
# Создать backup
|
||||
docker-compose -f docker-compose.prod.yml exec postgres pg_dump -U postgres saas_automation > backup_$(date +%Y%m%d).sql
|
||||
|
||||
# Восстановить из backup
|
||||
docker-compose -f docker-compose.prod.yml exec -T postgres psql -U postgres saas_automation < backup_20241228.sql
|
||||
```
|
||||
|
||||
## 🔒 **Безопасность для production**
|
||||
|
||||
### SSL сертификат (рекомендуется)
|
||||
```bash
|
||||
# Установить Certbot
|
||||
apt install certbot -y
|
||||
|
||||
# Если у вас есть домен, получить SSL сертификат
|
||||
# certbot --nginx -d yourdomain.com
|
||||
|
||||
# Настроить автообновление
|
||||
crontab -e
|
||||
# Добавить: 0 12 * * * /usr/bin/certbot renew --quiet
|
||||
```
|
||||
|
||||
### Изменить пароли по умолчанию
|
||||
```bash
|
||||
# Изменить пароль базы данных
|
||||
# Обновить docker-compose.prod.yml и backend/.env
|
||||
# Изменить JWT_SECRET на уникальный ключ
|
||||
```
|
||||
|
||||
## 🚨 **Troubleshooting**
|
||||
|
||||
### Проблема: Контейнеры не запускаются
|
||||
```bash
|
||||
# Проверить логи
|
||||
docker-compose -f docker-compose.prod.yml logs
|
||||
|
||||
# Очистить старые образы
|
||||
docker system prune -a
|
||||
|
||||
# Пересобрать
|
||||
docker-compose -f docker-compose.prod.yml up -d --build
|
||||
```
|
||||
|
||||
### Проблема: База данных не подключается
|
||||
```bash
|
||||
# Проверить статус PostgreSQL
|
||||
docker-compose -f docker-compose.prod.yml exec postgres pg_isready
|
||||
|
||||
# Подключиться к базе данных
|
||||
docker-compose -f docker-compose.prod.yml exec postgres psql -U postgres saas_automation
|
||||
```
|
||||
|
||||
### Проблема: Frontend не загружается
|
||||
```bash
|
||||
# Проверить переменные окружения
|
||||
cat frontend/.env.local
|
||||
|
||||
# Убедиться что API_URL указывает на правильный адрес
|
||||
# Должен быть: NEXT_PUBLIC_API_URL=http://128.140.8.206:3001
|
||||
```
|
||||
|
||||
## 📞 **Поддержка**
|
||||
|
||||
При возникновении проблем:
|
||||
1. Проверить логи: `docker-compose logs`
|
||||
2. Проверить статус: `docker-compose ps`
|
||||
3. Перезапустить: `docker-compose restart`
|
||||
4. Связаться с командой: info@ai-impress.com
|
||||
|
||||
---
|
||||
|
||||
## 🎯 **Быстрая команда для полного развертывания**
|
||||
|
||||
```bash
|
||||
# Скопировать и выполнить весь блок
|
||||
ssh root@128.140.8.206 '
|
||||
apt update && apt upgrade -y &&
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh &&
|
||||
curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose &&
|
||||
chmod +x /usr/local/bin/docker-compose &&
|
||||
apt install git -y &&
|
||||
git clone https://github.com/SamoilenkoVadym/SaaS.git &&
|
||||
cd SaaS &&
|
||||
cp docker-compose.yml docker-compose.prod.yml &&
|
||||
sed -i "s/- \"3000:3000\"/- \"80:3000\"/" docker-compose.prod.yml &&
|
||||
cat > backend/.env << EOF
|
||||
PORT=3001
|
||||
DB_HOST=postgres
|
||||
DB_PORT=5432
|
||||
DB_NAME=saas_automation
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=secure_production_password_123
|
||||
JWT_SECRET=your_super_secret_jwt_key_production_123
|
||||
NODE_ENV=production
|
||||
EOF
|
||||
cat > frontend/.env.local << EOF
|
||||
NEXT_PUBLIC_API_URL=http://128.140.8.206:3001
|
||||
EOF
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
'
|
||||
```
|
||||
|
||||
После выполнения команды приложение будет доступно по адресу: **http://128.140.8.206**
|
||||
29
backend/.env.example
Normal file
29
backend/.env.example
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
PORT=3001
|
||||
NODE_ENV=development
|
||||
|
||||
# Database
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=saas_automation
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=password
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=your_super_secret_jwt_key_change_this
|
||||
JWT_EXPIRES_IN=24h
|
||||
|
||||
# Email (for password reset)
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your_email@gmail.com
|
||||
SMTP_PASSWORD=your_app_password
|
||||
|
||||
# OAuth Secrets (add as needed)
|
||||
GOOGLE_CLIENT_ID=your_google_client_id
|
||||
GOOGLE_CLIENT_SECRET=your_google_client_secret
|
||||
INSTAGRAM_CLIENT_ID=your_instagram_client_id
|
||||
INSTAGRAM_CLIENT_SECRET=your_instagram_client_secret
|
||||
|
||||
# n8n Integration
|
||||
N8N_BASE_URL=https://bot.ai-impress.com
|
||||
N8N_API_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1OWQyN2Q0ZS1jNTU3LTQxMDUtYjZmMy05Y2JmM2U5MzU1NWUiLCJpc3MiOiJuOG4iLCJhdWQiOiJwdWJsaWMtYXBpIiwiaWF0IjoxNzU5MDc1NDI4fQ.HCnDOPdq8GcphuPpz1o9871VOLbpMvd7m8C47e2Kq50
|
||||
21
backend/Dockerfile
Normal file
21
backend/Dockerfile
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package.json and package-lock.json (if available)
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci --only=production
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN npm run build
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3001
|
||||
|
||||
# Start the application
|
||||
CMD ["npm", "start"]
|
||||
18
backend/Dockerfile.dev
Normal file
18
backend/Dockerfile.dev
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package.json and package-lock.json
|
||||
COPY package*.json ./
|
||||
|
||||
# Install all dependencies (including dev dependencies)
|
||||
RUN npm install
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3001
|
||||
|
||||
# Start the application in development mode
|
||||
CMD ["npm", "run", "dev"]
|
||||
1995
backend/package-lock.json
generated
Normal file
1995
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
38
backend/package.json
Normal file
38
backend/package.json
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"name": "backend",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"dev": "nodemon src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"axios": "^1.12.2",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.2",
|
||||
"express": "^5.1.0",
|
||||
"helmet": "^8.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"nodemailer": "^7.0.6",
|
||||
"pg": "^8.16.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^24.5.2",
|
||||
"@types/pg": "^8.11.10",
|
||||
"nodemon": "^3.1.10",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
312
backend/src/controllers/adminController.ts
Normal file
312
backend/src/controllers/adminController.ts
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
import { Request, Response } from 'express';
|
||||
import { pool } from '../db/connection';
|
||||
import { AuthRequest } from '../middleware/auth';
|
||||
|
||||
export const getAnalytics = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
// Check if user is admin
|
||||
if (req.user.role !== 'admin') {
|
||||
return res.status(403).json({ error: 'Access denied. Admin role required.' });
|
||||
}
|
||||
|
||||
// Get user statistics
|
||||
const userStatsQuery = `
|
||||
SELECT
|
||||
COUNT(*) as total_users,
|
||||
COUNT(CASE WHEN subscription_plan = 'free' THEN 1 END) as free_users,
|
||||
COUNT(CASE WHEN subscription_plan = 'pro' THEN 1 END) as pro_users,
|
||||
COUNT(CASE WHEN subscription_plan = 'enterprise' THEN 1 END) as enterprise_users,
|
||||
COUNT(CASE WHEN is_verified = true THEN 1 END) as verified_users,
|
||||
COUNT(CASE WHEN last_login_at > NOW() - INTERVAL '7 days' THEN 1 END) as active_last_7_days,
|
||||
COUNT(CASE WHEN created_at > NOW() - INTERVAL '30 days' THEN 1 END) as new_last_30_days
|
||||
FROM users
|
||||
`;
|
||||
|
||||
// Get workflow statistics
|
||||
const workflowStatsQuery = `
|
||||
SELECT
|
||||
COUNT(*) as total_workflows,
|
||||
COUNT(CASE WHEN is_active = true THEN 1 END) as active_workflows,
|
||||
COUNT(CASE WHEN created_at > NOW() - INTERVAL '30 days' THEN 1 END) as new_workflows_30_days,
|
||||
AVG(CASE WHEN is_active = true THEN 1.0 ELSE 0.0 END) * 100 as activation_rate
|
||||
FROM workflows
|
||||
`;
|
||||
|
||||
// Get execution statistics
|
||||
const executionStatsQuery = `
|
||||
SELECT
|
||||
COUNT(*) as total_executions,
|
||||
COUNT(CASE WHEN status = 'success' THEN 1 END) as successful_executions,
|
||||
COUNT(CASE WHEN created_at > NOW() - INTERVAL '24 hours' THEN 1 END) as executions_24h,
|
||||
COUNT(CASE WHEN created_at > NOW() - INTERVAL '7 days' THEN 1 END) as executions_7d
|
||||
FROM workflow_executions
|
||||
`;
|
||||
|
||||
// Get revenue statistics
|
||||
const revenueStatsQuery = `
|
||||
SELECT
|
||||
SUM(amount) as total_revenue,
|
||||
SUM(CASE WHEN created_at > NOW() - INTERVAL '30 days' THEN amount ELSE 0 END) as revenue_30_days,
|
||||
COUNT(CASE WHEN payment_status = 'completed' THEN 1 END) as successful_payments,
|
||||
AVG(amount) as average_order_value
|
||||
FROM subscription_history
|
||||
WHERE payment_status = 'completed'
|
||||
`;
|
||||
|
||||
// Get integration statistics
|
||||
const integrationStatsQuery = `
|
||||
SELECT
|
||||
COUNT(*) as total_integrations,
|
||||
COUNT(CASE WHEN is_active = true THEN 1 END) as active_integrations,
|
||||
provider,
|
||||
COUNT(*) as count
|
||||
FROM integrations
|
||||
GROUP BY provider
|
||||
`;
|
||||
|
||||
// Get recent activity
|
||||
const recentActivityQuery = `
|
||||
SELECT
|
||||
action,
|
||||
resource_type,
|
||||
created_at,
|
||||
u.email as user_email
|
||||
FROM audit_logs a
|
||||
LEFT JOIN users u ON a.user_id = u.id
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 20
|
||||
`;
|
||||
|
||||
// Get top users by workflows
|
||||
const topUsersQuery = `
|
||||
SELECT
|
||||
u.email,
|
||||
u.first_name,
|
||||
u.last_name,
|
||||
u.subscription_plan,
|
||||
COUNT(w.id) as workflow_count,
|
||||
u.last_login_at
|
||||
FROM users u
|
||||
LEFT JOIN workflows w ON u.id = w.user_id
|
||||
GROUP BY u.id, u.email, u.first_name, u.last_name, u.subscription_plan, u.last_login_at
|
||||
ORDER BY workflow_count DESC
|
||||
LIMIT 10
|
||||
`;
|
||||
|
||||
// Execute all queries
|
||||
const [
|
||||
userStats,
|
||||
workflowStats,
|
||||
executionStats,
|
||||
revenueStats,
|
||||
integrationStats,
|
||||
recentActivity,
|
||||
topUsers
|
||||
] = await Promise.all([
|
||||
pool.query(userStatsQuery),
|
||||
pool.query(workflowStatsQuery),
|
||||
pool.query(executionStatsQuery),
|
||||
pool.query(revenueStatsQuery),
|
||||
pool.query(integrationStatsQuery),
|
||||
pool.query(recentActivityQuery),
|
||||
pool.query(topUsersQuery)
|
||||
]);
|
||||
|
||||
// Get daily registrations for the last 30 days
|
||||
const dailyRegistrationsQuery = `
|
||||
SELECT
|
||||
DATE(created_at) as date,
|
||||
COUNT(*) as registrations
|
||||
FROM users
|
||||
WHERE created_at > NOW() - INTERVAL '30 days'
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date
|
||||
`;
|
||||
|
||||
const dailyRegistrations = await pool.query(dailyRegistrationsQuery);
|
||||
|
||||
// Get workflow executions trend
|
||||
const executionTrendQuery = `
|
||||
SELECT
|
||||
DATE(created_at) as date,
|
||||
COUNT(*) as executions,
|
||||
COUNT(CASE WHEN status = 'success' THEN 1 END) as successful
|
||||
FROM workflow_executions
|
||||
WHERE created_at > NOW() - INTERVAL '30 days'
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date
|
||||
`;
|
||||
|
||||
const executionTrend = await pool.query(executionTrendQuery);
|
||||
|
||||
res.json({
|
||||
userStats: userStats.rows[0],
|
||||
workflowStats: workflowStats.rows[0],
|
||||
executionStats: executionStats.rows[0],
|
||||
revenueStats: revenueStats.rows[0],
|
||||
integrationStats: integrationStats.rows,
|
||||
recentActivity: recentActivity.rows,
|
||||
topUsers: topUsers.rows,
|
||||
dailyRegistrations: dailyRegistrations.rows,
|
||||
executionTrend: executionTrend.rows
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching analytics:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch analytics data' });
|
||||
}
|
||||
};
|
||||
|
||||
export const getAllUsers = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
if (req.user.role !== 'admin') {
|
||||
return res.status(403).json({ error: 'Access denied. Admin role required.' });
|
||||
}
|
||||
|
||||
const { page = 1, limit = 20, search = '', role = '', subscription = '' } = req.query;
|
||||
const offset = (Number(page) - 1) * Number(limit);
|
||||
|
||||
let whereConditions = ['1=1'];
|
||||
const queryParams: any[] = [];
|
||||
|
||||
if (search) {
|
||||
whereConditions.push(`(email ILIKE $${queryParams.length + 1} OR first_name ILIKE $${queryParams.length + 1} OR last_name ILIKE $${queryParams.length + 1})`);
|
||||
queryParams.push(`%${search}%`);
|
||||
}
|
||||
|
||||
if (role) {
|
||||
whereConditions.push(`role = $${queryParams.length + 1}`);
|
||||
queryParams.push(role);
|
||||
}
|
||||
|
||||
if (subscription) {
|
||||
whereConditions.push(`subscription_plan = $${queryParams.length + 1}`);
|
||||
queryParams.push(subscription);
|
||||
}
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
id, email, first_name, last_name, role, subscription_plan, subscription_status,
|
||||
is_verified, last_login_at, created_at,
|
||||
(SELECT COUNT(*) FROM workflows WHERE user_id = users.id) as workflow_count
|
||||
FROM users
|
||||
WHERE ${whereConditions.join(' AND ')}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $${queryParams.length + 1} OFFSET $${queryParams.length + 2}
|
||||
`;
|
||||
|
||||
queryParams.push(Number(limit), offset);
|
||||
|
||||
const countQuery = `
|
||||
SELECT COUNT(*) FROM users WHERE ${whereConditions.join(' AND ')}
|
||||
`;
|
||||
|
||||
const [users, count] = await Promise.all([
|
||||
pool.query(query, queryParams),
|
||||
pool.query(countQuery, queryParams.slice(0, -2))
|
||||
]);
|
||||
|
||||
res.json({
|
||||
users: users.rows,
|
||||
pagination: {
|
||||
currentPage: Number(page),
|
||||
totalPages: Math.ceil(Number(count.rows[0].count) / Number(limit)),
|
||||
totalUsers: Number(count.rows[0].count),
|
||||
limit: Number(limit)
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching users:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch users' });
|
||||
}
|
||||
};
|
||||
|
||||
export const updateUserRole = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
if (req.user.role !== 'admin') {
|
||||
return res.status(403).json({ error: 'Access denied. Admin role required.' });
|
||||
}
|
||||
|
||||
const { userId } = req.params;
|
||||
const { role, subscription_plan } = req.body;
|
||||
|
||||
const query = `
|
||||
UPDATE users
|
||||
SET role = $1, subscription_plan = $2, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $3
|
||||
RETURNING id, email, role, subscription_plan
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [role, subscription_plan, userId]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
// Log the action
|
||||
await pool.query(
|
||||
'INSERT INTO audit_logs (user_id, action, resource_type, resource_id, details) VALUES ($1, $2, $3, $4, $5)',
|
||||
[req.user.id, 'update_user_role', 'user', userId, { role, subscription_plan }]
|
||||
);
|
||||
|
||||
res.json(result.rows[0]);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating user role:', error);
|
||||
res.status(500).json({ error: 'Failed to update user role' });
|
||||
}
|
||||
};
|
||||
|
||||
export const getSystemSettings = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
if (req.user.role !== 'admin') {
|
||||
return res.status(403).json({ error: 'Access denied. Admin role required.' });
|
||||
}
|
||||
|
||||
const query = 'SELECT * FROM system_settings ORDER BY key';
|
||||
const result = await pool.query(query);
|
||||
|
||||
res.json({ settings: result.rows });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching system settings:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch system settings' });
|
||||
}
|
||||
};
|
||||
|
||||
export const updateSystemSetting = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
if (req.user.role !== 'admin') {
|
||||
return res.status(403).json({ error: 'Access denied. Admin role required.' });
|
||||
}
|
||||
|
||||
const { key } = req.params;
|
||||
const { value } = req.body;
|
||||
|
||||
const query = `
|
||||
UPDATE system_settings
|
||||
SET value = $1, updated_by = $2, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE key = $3
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [JSON.stringify(value), req.user.id, key]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Setting not found' });
|
||||
}
|
||||
|
||||
// Log the action
|
||||
await pool.query(
|
||||
'INSERT INTO audit_logs (user_id, action, resource_type, resource_id, details) VALUES ($1, $2, $3, $4, $5)',
|
||||
[req.user.id, 'update_system_setting', 'system_setting', key, { value }]
|
||||
);
|
||||
|
||||
res.json(result.rows[0]);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating system setting:', error);
|
||||
res.status(500).json({ error: 'Failed to update system setting' });
|
||||
}
|
||||
};
|
||||
173
backend/src/controllers/authController.ts
Normal file
173
backend/src/controllers/authController.ts
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
import { Request, Response } from 'express';
|
||||
import { pool } from '../db/connection';
|
||||
import { hashPassword, comparePassword, generateToken, generateRandomToken } from '../utils/auth';
|
||||
|
||||
export const signup = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { email, password, first_name, last_name } = req.body;
|
||||
|
||||
if (!email || !password) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Email and password are required'
|
||||
});
|
||||
}
|
||||
|
||||
const existingUser = await pool.query('SELECT id FROM users WHERE email = $1', [email]);
|
||||
if (existingUser.rows.length > 0) {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
message: 'User already exists with this email'
|
||||
});
|
||||
}
|
||||
|
||||
const passwordHash = await hashPassword(password);
|
||||
const verificationToken = generateRandomToken();
|
||||
|
||||
const result = await pool.query(
|
||||
`INSERT INTO users (email, password_hash, first_name, last_name, verification_token, is_verified)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id, email, first_name, last_name, role, subscription_plan, subscription_status, created_at`,
|
||||
[email, passwordHash, first_name, last_name, verificationToken, false]
|
||||
);
|
||||
|
||||
const user = result.rows[0];
|
||||
const token = generateToken({
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
subscription_plan: user.subscription_plan
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
role: user.role,
|
||||
subscription_plan: user.subscription_plan,
|
||||
subscription_status: user.subscription_status,
|
||||
is_verified: false,
|
||||
created_at: user.created_at,
|
||||
updated_at: user.created_at
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Signup error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const login = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
if (!email || !password) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Email and password are required'
|
||||
});
|
||||
}
|
||||
|
||||
const result = await pool.query(
|
||||
'SELECT id, email, password_hash, first_name, last_name, is_verified, role, subscription_plan, subscription_status, created_at, updated_at FROM users WHERE email = $1',
|
||||
[email]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Invalid credentials'
|
||||
});
|
||||
}
|
||||
|
||||
const user = result.rows[0];
|
||||
const isValidPassword = await comparePassword(password, user.password_hash);
|
||||
|
||||
if (!isValidPassword) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Invalid credentials'
|
||||
});
|
||||
}
|
||||
|
||||
const token = generateToken({
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
subscription_plan: user.subscription_plan
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
is_verified: user.is_verified,
|
||||
role: user.role,
|
||||
subscription_plan: user.subscription_plan,
|
||||
subscription_status: user.subscription_status,
|
||||
created_at: user.created_at,
|
||||
updated_at: user.updated_at
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const resetPassword = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { email } = req.body;
|
||||
|
||||
if (!email) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Email is required'
|
||||
});
|
||||
}
|
||||
|
||||
const user = await pool.query('SELECT id FROM users WHERE email = $1', [email]);
|
||||
if (user.rows.length === 0) {
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'If the email exists, a reset link has been sent'
|
||||
});
|
||||
}
|
||||
|
||||
const resetToken = generateRandomToken();
|
||||
const resetExpires = new Date(Date.now() + 3600000); // 1 hour
|
||||
|
||||
await pool.query(
|
||||
'UPDATE users SET reset_password_token = $1, reset_password_expires = $2 WHERE email = $3',
|
||||
[resetToken, resetExpires, email]
|
||||
);
|
||||
|
||||
// TODO: Send email with reset link
|
||||
console.log(`Reset token for ${email}: ${resetToken}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'If the email exists, a reset link has been sent'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Reset password error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
337
backend/src/controllers/billingController.ts
Normal file
337
backend/src/controllers/billingController.ts
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
import { Request, Response } from 'express';
|
||||
import { pool } from '../db/connection';
|
||||
import { AuthRequest } from '../middleware/auth';
|
||||
|
||||
export const getSubscriptionPlans = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const query = `
|
||||
SELECT * FROM subscription_plans
|
||||
WHERE is_active = true
|
||||
ORDER BY price_monthly ASC
|
||||
`;
|
||||
|
||||
const result = await pool.query(query);
|
||||
res.json({ plans: result.rows });
|
||||
} catch (error) {
|
||||
console.error('Error fetching subscription plans:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch subscription plans' });
|
||||
}
|
||||
};
|
||||
|
||||
export const getCurrentSubscription = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const userQuery = `
|
||||
SELECT
|
||||
subscription_plan,
|
||||
subscription_status,
|
||||
subscription_starts_at,
|
||||
subscription_ends_at,
|
||||
workflow_limit,
|
||||
user_limit,
|
||||
features
|
||||
FROM users
|
||||
WHERE id = $1
|
||||
`;
|
||||
|
||||
const userResult = await pool.query(userQuery, [req.user.id]);
|
||||
|
||||
if (userResult.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
const subscription = userResult.rows[0];
|
||||
|
||||
// Get subscription history
|
||||
const historyQuery = `
|
||||
SELECT * FROM subscription_history
|
||||
WHERE user_id = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 10
|
||||
`;
|
||||
|
||||
const historyResult = await pool.query(historyQuery, [req.user.id]);
|
||||
|
||||
// Check if subscription is expired
|
||||
const now = new Date();
|
||||
const expiryDate = new Date(subscription.subscription_ends_at);
|
||||
const isExpired = subscription.subscription_ends_at && expiryDate < now;
|
||||
|
||||
res.json({
|
||||
subscription: {
|
||||
...subscription,
|
||||
is_expired: isExpired,
|
||||
days_remaining: subscription.subscription_ends_at
|
||||
? Math.max(0, Math.ceil((expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)))
|
||||
: null
|
||||
},
|
||||
history: historyResult.rows
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching current subscription:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch subscription details' });
|
||||
}
|
||||
};
|
||||
|
||||
export const upgradeSubscription = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { planName, paymentMethod = 'manual' } = req.body;
|
||||
|
||||
// Get the plan details
|
||||
const planQuery = `
|
||||
SELECT * FROM subscription_plans
|
||||
WHERE name = $1 AND is_active = true
|
||||
`;
|
||||
|
||||
const planResult = await pool.query(planQuery, [planName]);
|
||||
|
||||
if (planResult.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Subscription plan not found' });
|
||||
}
|
||||
|
||||
const plan = planResult.rows[0];
|
||||
|
||||
// For now, we'll simulate successful payment
|
||||
// In a real app, this would integrate with Stripe/PayPal/etc.
|
||||
const now = new Date();
|
||||
const endDate = new Date(now);
|
||||
endDate.setMonth(endDate.getMonth() + 1); // 1 month subscription
|
||||
|
||||
// Start transaction
|
||||
await pool.query('BEGIN');
|
||||
|
||||
try {
|
||||
// Update user subscription
|
||||
const updateUserQuery = `
|
||||
UPDATE users
|
||||
SET
|
||||
subscription_plan = $1,
|
||||
subscription_status = 'active',
|
||||
subscription_starts_at = $2,
|
||||
subscription_ends_at = $3,
|
||||
workflow_limit = $4,
|
||||
user_limit = $5,
|
||||
features = $6,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $7
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const userResult = await pool.query(updateUserQuery, [
|
||||
planName,
|
||||
now,
|
||||
endDate,
|
||||
plan.workflow_limit,
|
||||
plan.user_limit,
|
||||
plan.features,
|
||||
req.user.id
|
||||
]);
|
||||
|
||||
// Record payment history
|
||||
const historyQuery = `
|
||||
INSERT INTO subscription_history (
|
||||
user_id, plan_name, amount, currency, payment_method,
|
||||
payment_status, billing_period_start, billing_period_end
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const historyResult = await pool.query(historyQuery, [
|
||||
req.user.id,
|
||||
planName,
|
||||
plan.price_monthly,
|
||||
'GBP',
|
||||
paymentMethod,
|
||||
'completed',
|
||||
now,
|
||||
endDate
|
||||
]);
|
||||
|
||||
// Log the action
|
||||
await pool.query(
|
||||
'INSERT INTO audit_logs (user_id, action, resource_type, details) VALUES ($1, $2, $3, $4)',
|
||||
[req.user.id, 'upgrade_subscription', 'subscription', { from: req.user.subscription_plan, to: planName }]
|
||||
);
|
||||
|
||||
await pool.query('COMMIT');
|
||||
|
||||
res.json({
|
||||
message: 'Subscription upgraded successfully',
|
||||
subscription: userResult.rows[0],
|
||||
payment: historyResult.rows[0]
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
await pool.query('ROLLBACK');
|
||||
throw error;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error upgrading subscription:', error);
|
||||
res.status(500).json({ error: 'Failed to upgrade subscription' });
|
||||
}
|
||||
};
|
||||
|
||||
export const cancelSubscription = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
// Don't actually cancel immediately, just mark for cancellation at period end
|
||||
const query = `
|
||||
UPDATE users
|
||||
SET
|
||||
subscription_status = 'cancelled',
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $1
|
||||
RETURNING subscription_plan, subscription_ends_at
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [req.user.id]);
|
||||
|
||||
// Log the action
|
||||
await pool.query(
|
||||
'INSERT INTO audit_logs (user_id, action, resource_type, details) VALUES ($1, $2, $3, $4)',
|
||||
[req.user.id, 'cancel_subscription', 'subscription', { plan: result.rows[0].subscription_plan }]
|
||||
);
|
||||
|
||||
res.json({
|
||||
message: 'Subscription cancelled. Access will continue until the end of your billing period.',
|
||||
ends_at: result.rows[0].subscription_ends_at
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error cancelling subscription:', error);
|
||||
res.status(500).json({ error: 'Failed to cancel subscription' });
|
||||
}
|
||||
};
|
||||
|
||||
export const getUsageStats = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
// Get current user limits
|
||||
const userQuery = `
|
||||
SELECT workflow_limit, user_limit, features
|
||||
FROM users
|
||||
WHERE id = $1
|
||||
`;
|
||||
|
||||
const userResult = await pool.query(userQuery, [req.user.id]);
|
||||
const user = userResult.rows[0];
|
||||
|
||||
// Get current usage
|
||||
const workflowCountQuery = `
|
||||
SELECT COUNT(*) as count
|
||||
FROM workflows
|
||||
WHERE user_id = $1
|
||||
`;
|
||||
|
||||
const workflowResult = await pool.query(workflowCountQuery, [req.user.id]);
|
||||
const currentWorkflows = parseInt(workflowResult.rows[0].count);
|
||||
|
||||
// Get team members count (if applicable)
|
||||
const teamCountQuery = `
|
||||
SELECT COUNT(*) as count
|
||||
FROM user_teams
|
||||
WHERE owner_id = $1 AND status = 'active'
|
||||
`;
|
||||
|
||||
const teamResult = await pool.query(teamCountQuery, [req.user.id]);
|
||||
const currentTeamMembers = parseInt(teamResult.rows[0].count);
|
||||
|
||||
// Get execution count for current month
|
||||
const executionCountQuery = `
|
||||
SELECT COUNT(*) as count
|
||||
FROM workflow_executions
|
||||
WHERE user_id = $1
|
||||
AND created_at >= DATE_TRUNC('month', CURRENT_DATE)
|
||||
`;
|
||||
|
||||
const executionResult = await pool.query(executionCountQuery, [req.user.id]);
|
||||
const currentExecutions = parseInt(executionResult.rows[0].count);
|
||||
|
||||
// Get execution limit from features
|
||||
const executionLimit = user.features?.execution_limit || 100;
|
||||
|
||||
res.json({
|
||||
limits: {
|
||||
workflows: user.workflow_limit,
|
||||
teamMembers: user.user_limit,
|
||||
executions: executionLimit
|
||||
},
|
||||
usage: {
|
||||
workflows: currentWorkflows,
|
||||
teamMembers: currentTeamMembers,
|
||||
executions: currentExecutions
|
||||
},
|
||||
percentages: {
|
||||
workflows: user.workflow_limit === -1 ? 0 : Math.round((currentWorkflows / user.workflow_limit) * 100),
|
||||
teamMembers: user.user_limit === -1 ? 0 : Math.round((currentTeamMembers / user.user_limit) * 100),
|
||||
executions: executionLimit === -1 ? 0 : Math.round((currentExecutions / executionLimit) * 100)
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching usage stats:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch usage statistics' });
|
||||
}
|
||||
};
|
||||
|
||||
// Webhook endpoint for payment provider notifications (Stripe, PayPal, etc.)
|
||||
export const handlePaymentWebhook = async (req: Request, res: Response) => {
|
||||
try {
|
||||
// This would contain the logic to handle payment provider webhooks
|
||||
// For now, just return success
|
||||
console.log('Payment webhook received:', req.body);
|
||||
res.status(200).json({ received: true });
|
||||
} catch (error) {
|
||||
console.error('Error handling payment webhook:', error);
|
||||
res.status(500).json({ error: 'Webhook processing failed' });
|
||||
}
|
||||
};
|
||||
|
||||
export const generateInvoice = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { paymentId } = req.params;
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
sh.*,
|
||||
u.email,
|
||||
u.first_name,
|
||||
u.last_name
|
||||
FROM subscription_history sh
|
||||
JOIN users u ON sh.user_id = u.id
|
||||
WHERE sh.id = $1 AND sh.user_id = $2
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [paymentId, req.user.id]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Invoice not found' });
|
||||
}
|
||||
|
||||
const invoice = result.rows[0];
|
||||
|
||||
// In a real app, you'd generate a PDF here
|
||||
// For now, return invoice data
|
||||
res.json({
|
||||
invoice: {
|
||||
id: invoice.id,
|
||||
date: invoice.created_at,
|
||||
customer: {
|
||||
name: `${invoice.first_name} ${invoice.last_name}`,
|
||||
email: invoice.email
|
||||
},
|
||||
plan: invoice.plan_name,
|
||||
amount: invoice.amount,
|
||||
currency: invoice.currency,
|
||||
period: {
|
||||
start: invoice.billing_period_start,
|
||||
end: invoice.billing_period_end
|
||||
},
|
||||
status: invoice.payment_status
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error generating invoice:', error);
|
||||
res.status(500).json({ error: 'Failed to generate invoice' });
|
||||
}
|
||||
};
|
||||
91
backend/src/controllers/chatController.ts
Normal file
91
backend/src/controllers/chatController.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import { Response } from 'express';
|
||||
import { query } from '../utils/db';
|
||||
import { AuthRequest } from '../middleware/auth';
|
||||
import { generateRandomToken } from '../utils/auth';
|
||||
|
||||
const mockResponses: { [key: string]: string } = {
|
||||
'how to connect instagram': 'To connect Instagram, go to Integrations > Instagram > Connect. You\'ll need to authorize our app to access your Instagram account.',
|
||||
'how to create webhook': 'To create a webhook, go to Webhooks > Create New. Choose your trigger type and we\'ll generate a unique URL for you.',
|
||||
'workflow not working': 'Check the Logs section to see execution details. Common issues include expired tokens or invalid trigger conditions.',
|
||||
'default': 'I\'m here to help! You can ask me about connecting integrations, creating workflows, troubleshooting issues, or navigating the platform.'
|
||||
};
|
||||
|
||||
export const sendMessage = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { message, session_id } = req.body;
|
||||
|
||||
if (!message) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Message is required'
|
||||
});
|
||||
}
|
||||
|
||||
const sessionId = session_id || generateRandomToken();
|
||||
|
||||
await query(
|
||||
'INSERT INTO chat_history (user_id, session_id, message_type, content) VALUES ($1, $2, $3, $4)',
|
||||
[req.user?.id, sessionId, 'user', message]
|
||||
);
|
||||
|
||||
const lowerMessage = message.toLowerCase();
|
||||
let reply = mockResponses.default;
|
||||
|
||||
for (const [key, value] of Object.entries(mockResponses)) {
|
||||
if (lowerMessage.includes(key)) {
|
||||
reply = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await query(
|
||||
'INSERT INTO chat_history (user_id, session_id, message_type, content) VALUES ($1, $2, $3, $4)',
|
||||
[req.user?.id, sessionId, 'assistant', reply]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
reply,
|
||||
session_id: sessionId
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Send message error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getChatHistory = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { session_id } = req.query;
|
||||
|
||||
let queryText = `
|
||||
SELECT message_type, content, created_at
|
||||
FROM chat_history
|
||||
WHERE user_id = $1
|
||||
`;
|
||||
const params: any[] = [req.user?.id];
|
||||
|
||||
if (session_id) {
|
||||
queryText += ' AND session_id = $2';
|
||||
params.push(session_id);
|
||||
}
|
||||
|
||||
queryText += ' ORDER BY created_at ASC';
|
||||
|
||||
const result = await query(queryText, params);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
messages: result.rows
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get chat history error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
107
backend/src/controllers/integrationController.ts
Normal file
107
backend/src/controllers/integrationController.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { Response } from 'express';
|
||||
import { query } from '../utils/db';
|
||||
import { AuthRequest } from '../middleware/auth';
|
||||
|
||||
export const getIntegrations = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const result = await query(
|
||||
`SELECT id, provider, account_name, account_email, is_active, created_at, updated_at
|
||||
FROM integrations WHERE user_id = $1 ORDER BY provider`,
|
||||
[req.user?.id]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
integrations: result.rows
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get integrations error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const connectIntegration = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { provider } = req.params;
|
||||
const { access_token, refresh_token, account_name, account_email, scopes } = req.body;
|
||||
|
||||
if (!access_token) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Access token is required'
|
||||
});
|
||||
}
|
||||
|
||||
const existing = await query(
|
||||
'SELECT id FROM integrations WHERE user_id = $1 AND provider = $2',
|
||||
[req.user?.id, provider]
|
||||
);
|
||||
|
||||
if (existing.rows.length > 0) {
|
||||
const result = await query(
|
||||
`UPDATE integrations SET access_token = $1, refresh_token = $2, account_name = $3,
|
||||
account_email = $4, scopes = $5, is_active = true, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE user_id = $6 AND provider = $7
|
||||
RETURNING id, provider, account_name, account_email, is_active`,
|
||||
[access_token, refresh_token, account_name, account_email, scopes, req.user?.id, provider]
|
||||
);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'Integration updated successfully',
|
||||
integration: result.rows[0]
|
||||
});
|
||||
}
|
||||
|
||||
const result = await query(
|
||||
`INSERT INTO integrations (user_id, provider, access_token, refresh_token, account_name, account_email, scopes)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, provider, account_name, account_email, is_active, created_at`,
|
||||
[req.user?.id, provider, access_token, refresh_token, account_name, account_email, scopes]
|
||||
);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'Integration connected successfully',
|
||||
integration: result.rows[0]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Connect integration error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const disconnectIntegration = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const result = await query(
|
||||
'DELETE FROM integrations WHERE id = $1 AND user_id = $2 RETURNING id',
|
||||
[id, req.user?.id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Integration not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Integration disconnected successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Disconnect integration error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
79
backend/src/controllers/logController.ts
Normal file
79
backend/src/controllers/logController.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { Response } from 'express';
|
||||
import { query } from '../utils/db';
|
||||
import { AuthRequest } from '../middleware/auth';
|
||||
|
||||
export const getWorkflowExecutions = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { workflow_id, status, limit = 100 } = req.query;
|
||||
|
||||
let queryText = `
|
||||
SELECT we.id, we.workflow_id, w.name as workflow_name, we.status, we.trigger_data,
|
||||
we.error_message, we.started_at, we.finished_at, we.created_at
|
||||
FROM workflow_executions we
|
||||
JOIN workflows w ON we.workflow_id = w.id
|
||||
WHERE we.user_id = $1
|
||||
`;
|
||||
const params: any[] = [req.user?.id];
|
||||
let paramCount = 1;
|
||||
|
||||
if (workflow_id) {
|
||||
paramCount++;
|
||||
queryText += ` AND we.workflow_id = $${paramCount}`;
|
||||
params.push(workflow_id);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
paramCount++;
|
||||
queryText += ` AND we.status = $${paramCount}`;
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
queryText += ` ORDER BY we.created_at DESC LIMIT $${paramCount + 1}`;
|
||||
params.push(limit);
|
||||
|
||||
const result = await query(queryText, params);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
executions: result.rows
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get workflow executions error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getExecutionDetails = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const result = await query(
|
||||
`SELECT we.*, w.name as workflow_name
|
||||
FROM workflow_executions we
|
||||
JOIN workflows w ON we.workflow_id = w.id
|
||||
WHERE we.id = $1 AND we.user_id = $2`,
|
||||
[id, req.user?.id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Execution not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
execution: result.rows[0]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get execution details error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
198
backend/src/controllers/templateController.ts
Normal file
198
backend/src/controllers/templateController.ts
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
import { Request, Response } from 'express';
|
||||
import { pool } from '../db/connection';
|
||||
import { AuthRequest } from '../middleware/auth';
|
||||
import { n8nService } from '../services/n8nService';
|
||||
|
||||
export const getTemplates = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { category, search } = req.query;
|
||||
const userRole = req.user?.role || 'free';
|
||||
|
||||
// Build query for local templates
|
||||
let queryText = `
|
||||
SELECT id, name, description, category, trigger_type, tags, is_featured, install_count, created_at
|
||||
FROM templates WHERE 1=1
|
||||
`;
|
||||
const params: any[] = [];
|
||||
let paramCount = 0;
|
||||
|
||||
if (category) {
|
||||
paramCount++;
|
||||
queryText += ` AND category = $${paramCount}`;
|
||||
params.push(category);
|
||||
}
|
||||
|
||||
if (search) {
|
||||
paramCount++;
|
||||
queryText += ` AND (name ILIKE $${paramCount} OR description ILIKE $${paramCount})`;
|
||||
params.push(`%${search}%`);
|
||||
}
|
||||
|
||||
queryText += ' ORDER BY is_featured DESC, install_count DESC, created_at DESC';
|
||||
|
||||
// Get local templates
|
||||
const localResult = await pool.query(queryText, params);
|
||||
let templates = localResult.rows.map(template => ({
|
||||
...template,
|
||||
source: 'local'
|
||||
}));
|
||||
|
||||
// For admin users, also fetch templates from n8n
|
||||
if (userRole === 'admin') {
|
||||
try {
|
||||
const n8nWorkflows = await n8nService.getWorkflows();
|
||||
|
||||
const n8nTemplates = n8nWorkflows.map(workflow => ({
|
||||
id: `n8n_${workflow.id}`,
|
||||
name: workflow.name,
|
||||
description: `N8n workflow: ${workflow.name}`,
|
||||
category: 'n8n',
|
||||
trigger_type: 'n8n_workflow',
|
||||
tags: ['n8n', 'automation'],
|
||||
is_featured: false,
|
||||
install_count: 0,
|
||||
created_at: workflow.createdAt,
|
||||
source: 'n8n',
|
||||
n8n_workflow_id: workflow.id,
|
||||
n8n_data: workflow
|
||||
}));
|
||||
|
||||
// Filter n8n templates if search is provided
|
||||
if (search) {
|
||||
const searchTerm = search.toString().toLowerCase();
|
||||
const filteredN8nTemplates = n8nTemplates.filter(template =>
|
||||
template.name.toLowerCase().includes(searchTerm) ||
|
||||
template.description.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
templates = [...templates, ...filteredN8nTemplates];
|
||||
} else {
|
||||
templates = [...templates, ...n8nTemplates];
|
||||
}
|
||||
} catch (n8nError) {
|
||||
console.error('Error fetching n8n workflows:', n8nError);
|
||||
// Continue with local templates only
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
templates: templates
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get templates error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const installTemplate = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { workflow_name } = req.body;
|
||||
|
||||
// Check if it's an n8n template
|
||||
if (id.startsWith('n8n_')) {
|
||||
const n8nWorkflowId = id.replace('n8n_', '');
|
||||
|
||||
try {
|
||||
// Get the n8n workflow
|
||||
const n8nWorkflow = await n8nService.getWorkflow(n8nWorkflowId);
|
||||
|
||||
// Create workflow in our database that references the n8n workflow
|
||||
const workflowResult = await pool.query(
|
||||
`INSERT INTO workflows (user_id, name, description, trigger_type, n8n_workflow_id, status, is_active)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id`,
|
||||
[
|
||||
req.user?.id,
|
||||
workflow_name || n8nWorkflow.name,
|
||||
`N8n workflow: ${n8nWorkflow.name}`,
|
||||
'n8n_workflow',
|
||||
n8nWorkflowId,
|
||||
'connected',
|
||||
n8nWorkflow.active
|
||||
]
|
||||
);
|
||||
|
||||
const workflowId = workflowResult.rows[0].id;
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'N8n workflow connected successfully',
|
||||
workflow_id: workflowId,
|
||||
n8n_workflow_id: n8nWorkflowId
|
||||
});
|
||||
|
||||
} catch (n8nError) {
|
||||
console.error('Error connecting n8n workflow:', n8nError);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to connect n8n workflow'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Handle local template installation
|
||||
const template = await pool.query('SELECT * FROM templates WHERE id = $1', [id]);
|
||||
if (template.rows.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Template not found'
|
||||
});
|
||||
}
|
||||
|
||||
const templateData = template.rows[0];
|
||||
const workflowName = workflow_name || templateData.name;
|
||||
|
||||
// Check user's workflow limit
|
||||
const userQuery = await pool.query('SELECT workflow_limit FROM users WHERE id = $1', [req.user?.id]);
|
||||
const userWorkflowLimit = userQuery.rows[0]?.workflow_limit || 3;
|
||||
|
||||
const currentWorkflowsQuery = await pool.query(
|
||||
'SELECT COUNT(*) as count FROM workflows WHERE user_id = $1',
|
||||
[req.user?.id]
|
||||
);
|
||||
const currentWorkflowCount = parseInt(currentWorkflowsQuery.rows[0].count);
|
||||
|
||||
if (userWorkflowLimit !== -1 && currentWorkflowCount >= userWorkflowLimit) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Workflow limit reached. Please upgrade your plan to create more workflows.'
|
||||
});
|
||||
}
|
||||
|
||||
const workflowResult = await pool.query(
|
||||
`INSERT INTO workflows (user_id, name, description, trigger_type, trigger_config, actions_config)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id`,
|
||||
[req.user?.id, workflowName, templateData.description, templateData.trigger_type,
|
||||
templateData.trigger_config, templateData.actions_config]
|
||||
);
|
||||
|
||||
const workflowId = workflowResult.rows[0].id;
|
||||
|
||||
await pool.query(
|
||||
'INSERT INTO user_templates (user_id, template_id, workflow_id) VALUES ($1, $2, $3)',
|
||||
[req.user?.id, id, workflowId]
|
||||
);
|
||||
|
||||
await pool.query(
|
||||
'UPDATE templates SET install_count = install_count + 1 WHERE id = $1',
|
||||
[id]
|
||||
);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'Template installed successfully',
|
||||
workflow_id: workflowId
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Install template error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
167
backend/src/controllers/webhookController.ts
Normal file
167
backend/src/controllers/webhookController.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
import { Request, Response } from 'express';
|
||||
import { query } from '../utils/db';
|
||||
import { AuthRequest } from '../middleware/auth';
|
||||
import { generateRandomToken } from '../utils/auth';
|
||||
|
||||
export const getWebhooks = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const result = await query(
|
||||
`SELECT id, name, slug, trigger_type, url_path, is_active, created_at, updated_at
|
||||
FROM webhooks WHERE user_id = $1 ORDER BY created_at DESC`,
|
||||
[req.user?.id]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
webhooks: result.rows
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get webhooks error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const createWebhook = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { name, trigger_type } = req.body;
|
||||
|
||||
if (!name || !trigger_type) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Name and trigger_type are required'
|
||||
});
|
||||
}
|
||||
|
||||
const slug = name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
|
||||
const secretKey = generateRandomToken();
|
||||
const urlPath = `/webhook/${req.user?.id}/${slug}`;
|
||||
|
||||
const result = await query(
|
||||
`INSERT INTO webhooks (user_id, name, slug, trigger_type, secret_key, url_path)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id, name, slug, trigger_type, url_path, is_active, created_at`,
|
||||
[req.user?.id, name, slug, trigger_type, secretKey, urlPath]
|
||||
);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
webhook: {
|
||||
...result.rows[0],
|
||||
secret_key: secretKey
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Create webhook error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getWebhookLogs = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { limit = 50 } = req.query;
|
||||
|
||||
const webhook = await query(
|
||||
'SELECT id FROM webhooks WHERE id = $1 AND user_id = $2',
|
||||
[id, req.user?.id]
|
||||
);
|
||||
|
||||
if (webhook.rows.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Webhook not found'
|
||||
});
|
||||
}
|
||||
|
||||
const result = await query(
|
||||
`SELECT id, request_method, request_headers, response_status, processing_time_ms, created_at
|
||||
FROM webhook_logs WHERE webhook_id = $1 ORDER BY created_at DESC LIMIT $2`,
|
||||
[id, limit]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
logs: result.rows
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get webhook logs error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteWebhook = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const result = await query(
|
||||
'DELETE FROM webhooks WHERE id = $1 AND user_id = $2 RETURNING id',
|
||||
[id, req.user?.id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Webhook not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Webhook deleted successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Delete webhook error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const handleWebhookCall = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { user_id, slug } = req.params;
|
||||
const startTime = Date.now();
|
||||
|
||||
const webhook = await query(
|
||||
'SELECT id, secret_key FROM webhooks WHERE url_path = $1 AND is_active = true',
|
||||
[`/webhook/${user_id}/${slug}`]
|
||||
);
|
||||
|
||||
if (webhook.rows.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Webhook not found'
|
||||
});
|
||||
}
|
||||
|
||||
const processingTime = Date.now() - startTime;
|
||||
|
||||
await query(
|
||||
`INSERT INTO webhook_logs (webhook_id, request_method, request_headers, request_payload, response_status, processing_time_ms)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
[webhook.rows[0].id, req.method, req.headers, req.body, 200, processingTime]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Webhook received successfully',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Handle webhook error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
456
backend/src/controllers/workflowController.ts
Normal file
456
backend/src/controllers/workflowController.ts
Normal file
|
|
@ -0,0 +1,456 @@
|
|||
import { Response } from 'express';
|
||||
import { query } from '../utils/db';
|
||||
import { AuthRequest } from '../middleware/auth';
|
||||
import n8nService from '../services/n8nService';
|
||||
|
||||
export const getWorkflows = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
// Get workflows from n8n
|
||||
const n8nWorkflows = await n8nService.getWorkflows();
|
||||
|
||||
// Get local workflow metadata
|
||||
const localResult = await query(
|
||||
`SELECT id, name, n8n_workflow_id, description, status, trigger_type, is_active, created_at, updated_at
|
||||
FROM workflows WHERE user_id = $1 ORDER BY created_at DESC`,
|
||||
[req.user?.id]
|
||||
);
|
||||
|
||||
// Merge n8n data with local metadata
|
||||
const workflows = localResult.rows.map(localWorkflow => {
|
||||
const n8nWorkflow = n8nWorkflows.find(w => w.id === localWorkflow.n8n_workflow_id);
|
||||
return {
|
||||
...localWorkflow,
|
||||
n8n_data: n8nWorkflow,
|
||||
active: n8nWorkflow?.active || false,
|
||||
status: n8nWorkflow?.active ? 'active' : 'inactive'
|
||||
};
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
workflows,
|
||||
total_n8n_workflows: n8nWorkflows.length
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get workflows error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch workflows'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getWorkflow = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Get local workflow
|
||||
const localResult = await query(
|
||||
`SELECT * FROM workflows WHERE id = $1 AND user_id = $2`,
|
||||
[id, req.user?.id]
|
||||
);
|
||||
|
||||
if (localResult.rows.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Workflow not found'
|
||||
});
|
||||
}
|
||||
|
||||
const localWorkflow = localResult.rows[0];
|
||||
|
||||
// Get n8n workflow if exists
|
||||
let n8nWorkflow = null;
|
||||
if (localWorkflow.n8n_workflow_id) {
|
||||
try {
|
||||
n8nWorkflow = await n8nService.getWorkflow(localWorkflow.n8n_workflow_id);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch n8n workflow:', error);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
workflow: {
|
||||
...localWorkflow,
|
||||
n8n_data: n8nWorkflow
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get workflow error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const createWorkflow = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { name, description, trigger_type, trigger_config, actions_config, n8n_workflow_data } = req.body;
|
||||
|
||||
if (!name) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Name is required'
|
||||
});
|
||||
}
|
||||
|
||||
let n8nWorkflowId = null;
|
||||
|
||||
// Create workflow in n8n if workflow data provided
|
||||
if (n8n_workflow_data) {
|
||||
try {
|
||||
const n8nWorkflow = await n8nService.createWorkflow({
|
||||
name,
|
||||
active: false,
|
||||
nodes: n8n_workflow_data.nodes || [],
|
||||
connections: n8n_workflow_data.connections || {},
|
||||
settings: n8n_workflow_data.settings,
|
||||
staticData: n8n_workflow_data.staticData
|
||||
});
|
||||
n8nWorkflowId = n8nWorkflow.id;
|
||||
} catch (error) {
|
||||
console.error('Failed to create n8n workflow:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to create workflow in n8n'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Save to local database
|
||||
const result = await query(
|
||||
`INSERT INTO workflows (user_id, name, description, trigger_type, trigger_config, actions_config, n8n_workflow_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, name, description, status, trigger_type, is_active, created_at`,
|
||||
[req.user?.id, name, description, trigger_type, trigger_config, actions_config, n8nWorkflowId]
|
||||
);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
workflow: {
|
||||
...result.rows[0],
|
||||
n8n_workflow_id: n8nWorkflowId
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Create workflow error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const updateWorkflow = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { name, description, trigger_type, trigger_config, actions_config, n8n_workflow_data } = req.body;
|
||||
|
||||
// Get existing workflow
|
||||
const existingResult = await query(
|
||||
`SELECT * FROM workflows WHERE id = $1 AND user_id = $2`,
|
||||
[id, req.user?.id]
|
||||
);
|
||||
|
||||
if (existingResult.rows.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Workflow not found'
|
||||
});
|
||||
}
|
||||
|
||||
const existingWorkflow = existingResult.rows[0];
|
||||
|
||||
// Update n8n workflow if exists
|
||||
if (existingWorkflow.n8n_workflow_id && n8n_workflow_data) {
|
||||
try {
|
||||
await n8nService.updateWorkflow(existingWorkflow.n8n_workflow_id, {
|
||||
name,
|
||||
nodes: n8n_workflow_data.nodes,
|
||||
connections: n8n_workflow_data.connections,
|
||||
settings: n8n_workflow_data.settings,
|
||||
staticData: n8n_workflow_data.staticData
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to update n8n workflow:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to update workflow in n8n'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update local database
|
||||
const result = await query(
|
||||
`UPDATE workflows
|
||||
SET name = $1, description = $2, trigger_type = $3, trigger_config = $4, actions_config = $5, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $6 AND user_id = $7
|
||||
RETURNING id, name, description, status, trigger_type, is_active, created_at, updated_at`,
|
||||
[name, description, trigger_type, trigger_config, actions_config, id, req.user?.id]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
workflow: result.rows[0]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Update workflow error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const activateWorkflow = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Get workflow
|
||||
const workflowResult = await query(
|
||||
`SELECT * FROM workflows WHERE id = $1 AND user_id = $2`,
|
||||
[id, req.user?.id]
|
||||
);
|
||||
|
||||
if (workflowResult.rows.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Workflow not found'
|
||||
});
|
||||
}
|
||||
|
||||
const workflow = workflowResult.rows[0];
|
||||
|
||||
// Activate in n8n
|
||||
if (workflow.n8n_workflow_id) {
|
||||
try {
|
||||
await n8nService.activateWorkflow(workflow.n8n_workflow_id);
|
||||
} catch (error) {
|
||||
console.error('Failed to activate n8n workflow:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to activate workflow in n8n'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update local status
|
||||
await query(
|
||||
`UPDATE workflows SET is_active = true, status = 'active', updated_at = CURRENT_TIMESTAMP WHERE id = $1 AND user_id = $2`,
|
||||
[id, req.user?.id]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Workflow activated successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Activate workflow error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const deactivateWorkflow = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Get workflow
|
||||
const workflowResult = await query(
|
||||
`SELECT * FROM workflows WHERE id = $1 AND user_id = $2`,
|
||||
[id, req.user?.id]
|
||||
);
|
||||
|
||||
if (workflowResult.rows.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Workflow not found'
|
||||
});
|
||||
}
|
||||
|
||||
const workflow = workflowResult.rows[0];
|
||||
|
||||
// Deactivate in n8n
|
||||
if (workflow.n8n_workflow_id) {
|
||||
try {
|
||||
await n8nService.deactivateWorkflow(workflow.n8n_workflow_id);
|
||||
} catch (error) {
|
||||
console.error('Failed to deactivate n8n workflow:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to deactivate workflow in n8n'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update local status
|
||||
await query(
|
||||
`UPDATE workflows SET is_active = false, status = 'inactive', updated_at = CURRENT_TIMESTAMP WHERE id = $1 AND user_id = $2`,
|
||||
[id, req.user?.id]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Workflow deactivated successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Deactivate workflow error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const executeWorkflow = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { inputData } = req.body;
|
||||
|
||||
// Get workflow
|
||||
const workflowResult = await query(
|
||||
`SELECT * FROM workflows WHERE id = $1 AND user_id = $2`,
|
||||
[id, req.user?.id]
|
||||
);
|
||||
|
||||
if (workflowResult.rows.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Workflow not found'
|
||||
});
|
||||
}
|
||||
|
||||
const workflow = workflowResult.rows[0];
|
||||
|
||||
if (!workflow.n8n_workflow_id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Workflow is not connected to n8n'
|
||||
});
|
||||
}
|
||||
|
||||
// Execute in n8n
|
||||
try {
|
||||
const execution = await n8nService.executeWorkflow(workflow.n8n_workflow_id, inputData);
|
||||
|
||||
// Log execution in database
|
||||
await query(
|
||||
`INSERT INTO workflow_executions (workflow_id, user_id, n8n_execution_id, status, trigger_data, started_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
[id, req.user?.id, execution.id, execution.finished ? 'completed' : 'running', inputData, new Date()]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
execution
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to execute workflow:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to execute workflow'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Execute workflow error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteWorkflow = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Get workflow
|
||||
const workflowResult = await query(
|
||||
`SELECT * FROM workflows WHERE id = $1 AND user_id = $2`,
|
||||
[id, req.user?.id]
|
||||
);
|
||||
|
||||
if (workflowResult.rows.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Workflow not found'
|
||||
});
|
||||
}
|
||||
|
||||
const workflow = workflowResult.rows[0];
|
||||
|
||||
// Delete from n8n first
|
||||
if (workflow.n8n_workflow_id) {
|
||||
try {
|
||||
await n8nService.deleteWorkflow(workflow.n8n_workflow_id);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete n8n workflow:', error);
|
||||
// Continue with local deletion even if n8n deletion fails
|
||||
}
|
||||
}
|
||||
|
||||
// Delete from local database
|
||||
await query(
|
||||
'DELETE FROM workflows WHERE id = $1 AND user_id = $2',
|
||||
[id, req.user?.id]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Workflow deleted successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Delete workflow error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getWorkflowExecutions = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const limit = parseInt(req.query.limit as string) || 20;
|
||||
|
||||
// Get workflow
|
||||
const workflowResult = await query(
|
||||
`SELECT * FROM workflows WHERE id = $1 AND user_id = $2`,
|
||||
[id, req.user?.id]
|
||||
);
|
||||
|
||||
if (workflowResult.rows.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Workflow not found'
|
||||
});
|
||||
}
|
||||
|
||||
const workflow = workflowResult.rows[0];
|
||||
|
||||
if (!workflow.n8n_workflow_id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Workflow is not connected to n8n'
|
||||
});
|
||||
}
|
||||
|
||||
// Get executions from n8n
|
||||
const executions = await n8nService.getExecutions(workflow.n8n_workflow_id, limit);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
executions
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get workflow executions error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
}
|
||||
};
|
||||
20
backend/src/db/connection.ts
Normal file
20
backend/src/db/connection.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { Pool } from 'pg';
|
||||
|
||||
const pool = new Pool({
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT || '5432'),
|
||||
database: process.env.DB_NAME || 'saas_automation',
|
||||
user: process.env.DB_USER || 'postgres',
|
||||
password: process.env.DB_PASSWORD || 'password',
|
||||
});
|
||||
|
||||
// Test connection
|
||||
pool.on('connect', () => {
|
||||
console.log('Connected to PostgreSQL database');
|
||||
});
|
||||
|
||||
pool.on('error', (err) => {
|
||||
console.error('PostgreSQL connection error:', err);
|
||||
});
|
||||
|
||||
export { pool };
|
||||
713
backend/src/index.ts
Normal file
713
backend/src/index.ts
Normal file
|
|
@ -0,0 +1,713 @@
|
|||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import helmet from 'helmet';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
app.use(helmet());
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'OK', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Simple test route
|
||||
app.get('/test', (req, res) => {
|
||||
res.json({ message: 'Backend is working!' });
|
||||
});
|
||||
|
||||
// Simple auth endpoints for testing
|
||||
app.post('/auth/login', async (req, res) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
if (!email || !password) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Email and password are required'
|
||||
});
|
||||
}
|
||||
|
||||
// For now, just check against our known admin user
|
||||
if (email === 'info@ai-impress.com' && password === 'admin123') {
|
||||
const token = 'test-admin-token-123';
|
||||
const user = {
|
||||
id: 1,
|
||||
email: 'info@ai-impress.com',
|
||||
first_name: 'Global',
|
||||
last_name: 'Admin',
|
||||
role: 'admin',
|
||||
subscription_plan: 'enterprise',
|
||||
subscription_status: 'active',
|
||||
is_verified: true,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
token,
|
||||
user
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Invalid credentials'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/auth/signup', async (req, res) => {
|
||||
try {
|
||||
const { email, password, first_name, last_name } = req.body;
|
||||
|
||||
if (!email || !password) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Email and password are required'
|
||||
});
|
||||
}
|
||||
|
||||
// Simple mock signup
|
||||
const token = 'test-user-token-456';
|
||||
const user = {
|
||||
id: 2,
|
||||
email,
|
||||
first_name: first_name || '',
|
||||
last_name: last_name || '',
|
||||
role: 'free',
|
||||
subscription_plan: 'free',
|
||||
subscription_status: 'active',
|
||||
is_verified: false,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
token,
|
||||
user
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Signup error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Admin Analytics endpoint
|
||||
app.get('/admin/analytics', (req, res) => {
|
||||
try {
|
||||
// Mock analytics data
|
||||
const analytics = {
|
||||
userStats: {
|
||||
total_users: 156,
|
||||
free_users: 120,
|
||||
pro_users: 28,
|
||||
enterprise_users: 6,
|
||||
admin_users: 2,
|
||||
verified_users: 134,
|
||||
active_last_7_days: 89,
|
||||
new_last_30_days: 23
|
||||
},
|
||||
workflowStats: {
|
||||
total_workflows: 342,
|
||||
active_workflows: 278,
|
||||
new_workflows_30_days: 45,
|
||||
activation_rate: 81.2
|
||||
},
|
||||
executionStats: {
|
||||
total_executions: 12847,
|
||||
successful_executions: 12234,
|
||||
executions_24h: 234,
|
||||
executions_7d: 1876
|
||||
},
|
||||
revenueStats: {
|
||||
total_revenue: 18750.00,
|
||||
revenue_30_days: 4200.00,
|
||||
successful_payments: 34,
|
||||
average_order_value: 150.00
|
||||
},
|
||||
integrationStats: [
|
||||
{ provider: 'telegram', count: 45 },
|
||||
{ provider: 'slack', count: 38 },
|
||||
{ provider: 'gmail', count: 67 },
|
||||
{ provider: 'teams', count: 23 }
|
||||
],
|
||||
recentActivity: [
|
||||
{
|
||||
action: 'create_workflow',
|
||||
resource_type: 'workflow',
|
||||
created_at: new Date(Date.now() - 1000 * 60 * 5).toISOString(),
|
||||
user_email: 'john@example.com'
|
||||
},
|
||||
{
|
||||
action: 'upgrade_subscription',
|
||||
resource_type: 'subscription',
|
||||
created_at: new Date(Date.now() - 1000 * 60 * 15).toISOString(),
|
||||
user_email: 'sarah@company.com'
|
||||
},
|
||||
{
|
||||
action: 'execute_workflow',
|
||||
resource_type: 'workflow',
|
||||
created_at: new Date(Date.now() - 1000 * 60 * 8).toISOString(),
|
||||
user_email: 'mike@startup.io'
|
||||
},
|
||||
{
|
||||
action: 'create_integration',
|
||||
resource_type: 'integration',
|
||||
created_at: new Date(Date.now() - 1000 * 60 * 22).toISOString(),
|
||||
user_email: 'lisa@agency.com'
|
||||
},
|
||||
{
|
||||
action: 'login',
|
||||
resource_type: 'auth',
|
||||
created_at: new Date(Date.now() - 1000 * 60 * 3).toISOString(),
|
||||
user_email: 'admin@platform.com'
|
||||
}
|
||||
],
|
||||
topUsers: [
|
||||
{
|
||||
email: 'power.user@company.com',
|
||||
first_name: 'Alex',
|
||||
last_name: 'Johnson',
|
||||
subscription_plan: 'enterprise',
|
||||
workflow_count: 24,
|
||||
last_login_at: new Date(Date.now() - 1000 * 60 * 30).toISOString()
|
||||
},
|
||||
{
|
||||
email: 'busy.bee@startup.io',
|
||||
first_name: 'Maria',
|
||||
last_name: 'Garcia',
|
||||
subscription_plan: 'pro',
|
||||
workflow_count: 18,
|
||||
last_login_at: new Date(Date.now() - 1000 * 60 * 120).toISOString()
|
||||
},
|
||||
{
|
||||
email: 'automation.expert@tech.com',
|
||||
first_name: 'David',
|
||||
last_name: 'Chen',
|
||||
subscription_plan: 'pro',
|
||||
workflow_count: 15,
|
||||
last_login_at: new Date(Date.now() - 1000 * 60 * 45).toISOString()
|
||||
},
|
||||
{
|
||||
email: 'workflow.master@agency.com',
|
||||
first_name: 'Emma',
|
||||
last_name: 'Wilson',
|
||||
subscription_plan: 'enterprise',
|
||||
workflow_count: 12,
|
||||
last_login_at: new Date(Date.now() - 1000 * 60 * 60).toISOString()
|
||||
}
|
||||
],
|
||||
dailyRegistrations: [
|
||||
{ date: '2024-09-21', registrations: 3 },
|
||||
{ date: '2024-09-22', registrations: 5 },
|
||||
{ date: '2024-09-23', registrations: 2 },
|
||||
{ date: '2024-09-24', registrations: 8 },
|
||||
{ date: '2024-09-25', registrations: 4 },
|
||||
{ date: '2024-09-26', registrations: 6 },
|
||||
{ date: '2024-09-27', registrations: 7 },
|
||||
{ date: '2024-09-28', registrations: 9 }
|
||||
],
|
||||
executionTrend: [
|
||||
{ date: '2024-09-21', executions: 145, successful: 142 },
|
||||
{ date: '2024-09-22', executions: 167, successful: 163 },
|
||||
{ date: '2024-09-23', executions: 132, successful: 128 },
|
||||
{ date: '2024-09-24', executions: 198, successful: 189 },
|
||||
{ date: '2024-09-25', executions: 156, successful: 151 },
|
||||
{ date: '2024-09-26', executions: 203, successful: 197 },
|
||||
{ date: '2024-09-27', executions: 178, successful: 171 },
|
||||
{ date: '2024-09-28', executions: 234, successful: 227 }
|
||||
]
|
||||
};
|
||||
|
||||
res.json(analytics);
|
||||
} catch (error) {
|
||||
console.error('Analytics error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch analytics' });
|
||||
}
|
||||
});
|
||||
|
||||
// Templates/Workflows endpoint (with /api prefix)
|
||||
app.get('/api/templates', (req, res) => {
|
||||
try {
|
||||
const templates = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Telegram to Slack Bridge",
|
||||
description: "Automatically forward messages from Telegram channels to Slack channels with formatting and filtering options",
|
||||
category: "Communication",
|
||||
tags: ["telegram", "slack", "messaging", "automation"],
|
||||
complexity: "intermediate",
|
||||
estimatedTime: "15 minutes",
|
||||
useCount: 234,
|
||||
rating: 4.8,
|
||||
author: "Aimpress Team",
|
||||
thumbnail: "/templates/telegram-slack.jpg",
|
||||
icon: "🔗",
|
||||
gradient: "from-blue-500 to-purple-600",
|
||||
workflow: {
|
||||
nodes: [
|
||||
{ type: "telegram-trigger", name: "Telegram Webhook" },
|
||||
{ type: "filter", name: "Message Filter" },
|
||||
{ type: "slack", name: "Send to Slack" }
|
||||
]
|
||||
},
|
||||
createdAt: "2024-08-15T10:30:00Z",
|
||||
updatedAt: "2024-09-20T14:22:00Z"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Gmail to Teams Notifications",
|
||||
description: "Send important email notifications from Gmail to Microsoft Teams with smart filtering and priority detection",
|
||||
category: "Email & Notifications",
|
||||
tags: ["gmail", "teams", "notifications", "email-filter"],
|
||||
complexity: "beginner",
|
||||
estimatedTime: "10 minutes",
|
||||
useCount: 189,
|
||||
rating: 4.6,
|
||||
author: "Aimpress Team",
|
||||
thumbnail: "/templates/gmail-teams.jpg",
|
||||
icon: "📧",
|
||||
gradient: "from-red-500 to-blue-600",
|
||||
workflow: {
|
||||
nodes: [
|
||||
{ type: "gmail-trigger", name: "Gmail Watch" },
|
||||
{ type: "condition", name: "Priority Check" },
|
||||
{ type: "teams", name: "Teams Message" }
|
||||
]
|
||||
},
|
||||
createdAt: "2024-07-22T09:15:00Z",
|
||||
updatedAt: "2024-09-18T11:45:00Z"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Customer Support Automation",
|
||||
description: "Comprehensive customer support workflow that routes tickets, sends auto-responses, and escalates urgent issues",
|
||||
category: "Customer Service",
|
||||
tags: ["support", "automation", "tickets", "escalation"],
|
||||
complexity: "advanced",
|
||||
estimatedTime: "45 minutes",
|
||||
useCount: 156,
|
||||
rating: 4.9,
|
||||
author: "Aimpress Team",
|
||||
thumbnail: "/templates/customer-support.jpg",
|
||||
icon: "🎧",
|
||||
gradient: "from-green-500 to-teal-600",
|
||||
workflow: {
|
||||
nodes: [
|
||||
{ type: "webhook", name: "Ticket Webhook" },
|
||||
{ type: "ai-classifier", name: "Urgency Detection" },
|
||||
{ type: "condition", name: "Route Decision" },
|
||||
{ type: "email", name: "Auto Response" },
|
||||
{ type: "slack", name: "Agent Alert" }
|
||||
]
|
||||
},
|
||||
createdAt: "2024-06-10T16:20:00Z",
|
||||
updatedAt: "2024-09-25T13:30:00Z"
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Social Media Content Sync",
|
||||
description: "Automatically sync content across multiple social media platforms with custom formatting for each platform",
|
||||
category: "Social Media",
|
||||
tags: ["social-media", "content", "automation", "multi-platform"],
|
||||
complexity: "intermediate",
|
||||
estimatedTime: "30 minutes",
|
||||
useCount: 298,
|
||||
rating: 4.7,
|
||||
author: "Aimpress Team",
|
||||
thumbnail: "/templates/social-sync.jpg",
|
||||
icon: "📱",
|
||||
gradient: "from-pink-500 to-orange-600",
|
||||
workflow: {
|
||||
nodes: [
|
||||
{ type: "rss-feed", name: "Content Source" },
|
||||
{ type: "ai-formatter", name: "Platform Formatter" },
|
||||
{ type: "twitter", name: "Twitter Post" },
|
||||
{ type: "linkedin", name: "LinkedIn Post" },
|
||||
{ type: "facebook", name: "Facebook Post" }
|
||||
]
|
||||
},
|
||||
createdAt: "2024-09-01T12:00:00Z",
|
||||
updatedAt: "2024-09-27T10:15:00Z"
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "E-commerce Order Processing",
|
||||
description: "Complete order processing pipeline with inventory checks, payment verification, and shipping notifications",
|
||||
category: "E-commerce",
|
||||
tags: ["orders", "inventory", "payments", "shipping"],
|
||||
complexity: "advanced",
|
||||
estimatedTime: "60 minutes",
|
||||
useCount: 87,
|
||||
rating: 4.8,
|
||||
author: "Aimpress Team",
|
||||
thumbnail: "/templates/ecommerce-orders.jpg",
|
||||
icon: "🛒",
|
||||
gradient: "from-indigo-500 to-purple-600",
|
||||
workflow: {
|
||||
nodes: [
|
||||
{ type: "shopify-trigger", name: "New Order" },
|
||||
{ type: "inventory-check", name: "Stock Verification" },
|
||||
{ type: "payment-gateway", name: "Payment Check" },
|
||||
{ type: "shipping-api", name: "Create Shipment" },
|
||||
{ type: "email", name: "Customer Notification" }
|
||||
]
|
||||
},
|
||||
createdAt: "2024-08-05T14:30:00Z",
|
||||
updatedAt: "2024-09-22T16:45:00Z"
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Data Backup & Sync",
|
||||
description: "Automated data backup solution that syncs files across multiple cloud storage providers with versioning",
|
||||
category: "Data Management",
|
||||
tags: ["backup", "sync", "cloud-storage", "versioning"],
|
||||
complexity: "intermediate",
|
||||
estimatedTime: "25 minutes",
|
||||
useCount: 142,
|
||||
rating: 4.5,
|
||||
author: "Aimpress Team",
|
||||
thumbnail: "/templates/data-backup.jpg",
|
||||
icon: "☁️",
|
||||
gradient: "from-cyan-500 to-blue-600",
|
||||
workflow: {
|
||||
nodes: [
|
||||
{ type: "schedule", name: "Daily Trigger" },
|
||||
{ type: "google-drive", name: "Source Files" },
|
||||
{ type: "dropbox", name: "Backup to Dropbox" },
|
||||
{ type: "aws-s3", name: "Archive to S3" },
|
||||
{ type: "slack", name: "Status Report" }
|
||||
]
|
||||
},
|
||||
createdAt: "2024-07-15T11:20:00Z",
|
||||
updatedAt: "2024-09-19T09:30:00Z"
|
||||
}
|
||||
];
|
||||
|
||||
res.json({
|
||||
templates,
|
||||
categories: ["Communication", "Email & Notifications", "Customer Service", "Social Media", "E-commerce", "Data Management"],
|
||||
totalCount: templates.length
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Templates error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch templates' });
|
||||
}
|
||||
});
|
||||
|
||||
// Templates endpoint (without /api prefix for frontend compatibility)
|
||||
app.get('/templates', (req, res) => {
|
||||
try {
|
||||
const templates = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Telegram to Slack Bridge",
|
||||
description: "Automatically forward messages from Telegram channels to Slack channels with formatting and filtering options",
|
||||
category: "Communication",
|
||||
tags: ["telegram", "slack", "messaging", "automation"],
|
||||
complexity: "intermediate",
|
||||
estimatedTime: "15 minutes",
|
||||
useCount: 234,
|
||||
rating: 4.8,
|
||||
author: "Aimpress Team",
|
||||
thumbnail: "/templates/telegram-slack.jpg",
|
||||
icon: "🔗",
|
||||
gradient: "from-blue-500 to-purple-600",
|
||||
workflow: {
|
||||
nodes: [
|
||||
{ type: "telegram-trigger", name: "Telegram Webhook" },
|
||||
{ type: "filter", name: "Message Filter" },
|
||||
{ type: "slack", name: "Send to Slack" }
|
||||
]
|
||||
},
|
||||
createdAt: "2024-08-15T10:30:00Z",
|
||||
updatedAt: "2024-09-20T14:22:00Z"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Gmail to Teams Notifications",
|
||||
description: "Send important email notifications from Gmail to Microsoft Teams with smart filtering and priority detection",
|
||||
category: "Email & Notifications",
|
||||
tags: ["gmail", "teams", "notifications", "email-filter"],
|
||||
complexity: "beginner",
|
||||
estimatedTime: "10 minutes",
|
||||
useCount: 189,
|
||||
rating: 4.6,
|
||||
author: "Aimpress Team",
|
||||
thumbnail: "/templates/gmail-teams.jpg",
|
||||
icon: "📧",
|
||||
gradient: "from-red-500 to-blue-600",
|
||||
workflow: {
|
||||
nodes: [
|
||||
{ type: "gmail-trigger", name: "Gmail Watch" },
|
||||
{ type: "condition", name: "Priority Check" },
|
||||
{ type: "teams", name: "Teams Message" }
|
||||
]
|
||||
},
|
||||
createdAt: "2024-07-22T09:15:00Z",
|
||||
updatedAt: "2024-09-18T11:45:00Z"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Customer Support Automation",
|
||||
description: "Comprehensive customer support workflow that routes tickets, sends auto-responses, and escalates urgent issues",
|
||||
category: "Customer Service",
|
||||
tags: ["support", "automation", "tickets", "escalation"],
|
||||
complexity: "advanced",
|
||||
estimatedTime: "45 minutes",
|
||||
useCount: 156,
|
||||
rating: 4.9,
|
||||
author: "Aimpress Team",
|
||||
thumbnail: "/templates/customer-support.jpg",
|
||||
icon: "🎧",
|
||||
gradient: "from-green-500 to-teal-600",
|
||||
workflow: {
|
||||
nodes: [
|
||||
{ type: "webhook", name: "Ticket Webhook" },
|
||||
{ type: "ai-classifier", name: "Urgency Detection" },
|
||||
{ type: "condition", name: "Route Decision" },
|
||||
{ type: "email", name: "Auto Response" },
|
||||
{ type: "slack", name: "Agent Alert" }
|
||||
]
|
||||
},
|
||||
createdAt: "2024-06-10T16:20:00Z",
|
||||
updatedAt: "2024-09-25T13:30:00Z"
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Social Media Content Sync",
|
||||
description: "Automatically sync content across multiple social media platforms with custom formatting for each platform",
|
||||
category: "Social Media",
|
||||
tags: ["social-media", "content", "automation", "multi-platform"],
|
||||
complexity: "intermediate",
|
||||
estimatedTime: "30 minutes",
|
||||
useCount: 298,
|
||||
rating: 4.7,
|
||||
author: "Aimpress Team",
|
||||
thumbnail: "/templates/social-sync.jpg",
|
||||
icon: "📱",
|
||||
gradient: "from-pink-500 to-orange-600",
|
||||
workflow: {
|
||||
nodes: [
|
||||
{ type: "rss-feed", name: "Content Source" },
|
||||
{ type: "ai-formatter", name: "Platform Formatter" },
|
||||
{ type: "twitter", name: "Twitter Post" },
|
||||
{ type: "linkedin", name: "LinkedIn Post" },
|
||||
{ type: "facebook", name: "Facebook Post" }
|
||||
]
|
||||
},
|
||||
createdAt: "2024-09-01T12:00:00Z",
|
||||
updatedAt: "2024-09-27T10:15:00Z"
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "E-commerce Order Processing",
|
||||
description: "Complete order processing pipeline with inventory checks, payment verification, and shipping notifications",
|
||||
category: "E-commerce",
|
||||
tags: ["orders", "inventory", "payments", "shipping"],
|
||||
complexity: "advanced",
|
||||
estimatedTime: "60 minutes",
|
||||
useCount: 87,
|
||||
rating: 4.8,
|
||||
author: "Aimpress Team",
|
||||
thumbnail: "/templates/ecommerce-orders.jpg",
|
||||
icon: "🛒",
|
||||
gradient: "from-indigo-500 to-purple-600",
|
||||
workflow: {
|
||||
nodes: [
|
||||
{ type: "shopify-trigger", name: "New Order" },
|
||||
{ type: "inventory-check", name: "Stock Verification" },
|
||||
{ type: "payment-gateway", name: "Payment Check" },
|
||||
{ type: "shipping-api", name: "Create Shipment" },
|
||||
{ type: "email", name: "Customer Notification" }
|
||||
]
|
||||
},
|
||||
createdAt: "2024-08-05T14:30:00Z",
|
||||
updatedAt: "2024-09-22T16:45:00Z"
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Data Backup & Sync",
|
||||
description: "Automated data backup solution that syncs files across multiple cloud storage providers with versioning",
|
||||
category: "Data Management",
|
||||
tags: ["backup", "sync", "cloud-storage", "versioning"],
|
||||
complexity: "intermediate",
|
||||
estimatedTime: "25 minutes",
|
||||
useCount: 142,
|
||||
rating: 4.5,
|
||||
author: "Aimpress Team",
|
||||
thumbnail: "/templates/data-backup.jpg",
|
||||
icon: "☁️",
|
||||
gradient: "from-cyan-500 to-blue-600",
|
||||
workflow: {
|
||||
nodes: [
|
||||
{ type: "schedule", name: "Daily Trigger" },
|
||||
{ type: "google-drive", name: "Source Files" },
|
||||
{ type: "dropbox", name: "Backup to Dropbox" },
|
||||
{ type: "aws-s3", name: "Archive to S3" },
|
||||
{ type: "slack", name: "Status Report" }
|
||||
]
|
||||
},
|
||||
createdAt: "2024-07-15T11:20:00Z",
|
||||
updatedAt: "2024-09-19T09:30:00Z"
|
||||
}
|
||||
];
|
||||
|
||||
res.json({
|
||||
templates,
|
||||
categories: ["Communication", "Email & Notifications", "Customer Service", "Social Media", "E-commerce", "Data Management"],
|
||||
totalCount: templates.length
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Templates error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch templates' });
|
||||
}
|
||||
});
|
||||
|
||||
// Template installation endpoint
|
||||
app.post('/templates/:templateId/install', (req, res) => {
|
||||
try {
|
||||
const { templateId } = req.params;
|
||||
const { workflow_name } = req.body;
|
||||
|
||||
console.log(`Installing template ${templateId} with name: ${workflow_name}`);
|
||||
|
||||
// Mock successful installation
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Template installed successfully',
|
||||
workflow: {
|
||||
id: `workflow_${Date.now()}`,
|
||||
name: workflow_name,
|
||||
template_id: templateId,
|
||||
status: 'active',
|
||||
created_at: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Template installation error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to install template'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Admin Users endpoint
|
||||
app.get('/admin/users', (req, res) => {
|
||||
try {
|
||||
const mockUsers = [
|
||||
{
|
||||
id: '1',
|
||||
email: 'info@ai-impress.com',
|
||||
first_name: 'Global',
|
||||
last_name: 'Admin',
|
||||
role: 'admin',
|
||||
subscription_plan: 'enterprise',
|
||||
subscription_status: 'active',
|
||||
is_verified: true,
|
||||
last_login_at: new Date(Date.now() - 1000 * 60 * 10).toISOString(),
|
||||
created_at: new Date('2024-01-01').toISOString(),
|
||||
workflow_count: 5
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
email: 'john.doe@example.com',
|
||||
first_name: 'John',
|
||||
last_name: 'Doe',
|
||||
role: 'pro',
|
||||
subscription_plan: 'pro',
|
||||
subscription_status: 'active',
|
||||
is_verified: true,
|
||||
last_login_at: new Date(Date.now() - 1000 * 60 * 60).toISOString(),
|
||||
created_at: new Date('2024-02-15').toISOString(),
|
||||
workflow_count: 8
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
email: 'jane.smith@company.com',
|
||||
first_name: 'Jane',
|
||||
last_name: 'Smith',
|
||||
role: 'free',
|
||||
subscription_plan: 'free',
|
||||
subscription_status: 'active',
|
||||
is_verified: true,
|
||||
last_login_at: new Date(Date.now() - 1000 * 60 * 30).toISOString(),
|
||||
created_at: new Date('2024-03-10').toISOString(),
|
||||
workflow_count: 3
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
email: 'enterprise@bigcorp.com',
|
||||
first_name: 'Robert',
|
||||
last_name: 'Johnson',
|
||||
role: 'enterprise',
|
||||
subscription_plan: 'enterprise',
|
||||
subscription_status: 'active',
|
||||
is_verified: true,
|
||||
last_login_at: new Date(Date.now() - 1000 * 60 * 120).toISOString(),
|
||||
created_at: new Date('2024-01-20').toISOString(),
|
||||
workflow_count: 25
|
||||
}
|
||||
];
|
||||
|
||||
res.json({
|
||||
users: mockUsers,
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
totalUsers: mockUsers.length,
|
||||
limit: 20
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Users error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch users' });
|
||||
}
|
||||
});
|
||||
|
||||
// Update user role endpoint
|
||||
app.patch('/admin/users/:userId/role', (req, res) => {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
const { role, subscription_plan } = req.body;
|
||||
|
||||
console.log(`Updating user ${userId}: role=${role}, plan=${subscription_plan}`);
|
||||
|
||||
// Mock successful update
|
||||
res.json({
|
||||
id: userId,
|
||||
email: 'user@example.com',
|
||||
role,
|
||||
subscription_plan
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Update user role error:', error);
|
||||
res.status(500).json({ error: 'Failed to update user role' });
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
});
|
||||
|
||||
export default app;
|
||||
28
backend/src/index_backup.ts
Normal file
28
backend/src/index_backup.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import helmet from 'helmet';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
app.use(helmet());
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'OK', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Simple test route
|
||||
app.get('/test', (req, res) => {
|
||||
res.json({ message: 'Backend is working!' });
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
});
|
||||
|
||||
export default app;
|
||||
45
backend/src/index_full.ts
Normal file
45
backend/src/index_full.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import helmet from 'helmet';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
// Route imports
|
||||
import authRoutes from './routes/auth';
|
||||
import workflowRoutes from './routes/workflows';
|
||||
import templateRoutes from './routes/templates';
|
||||
import integrationRoutes from './routes/integrations';
|
||||
import webhookRoutes from './routes/webhooks';
|
||||
import logRoutes from './routes/logs';
|
||||
import chatRoutes from './routes/chat';
|
||||
import adminRoutes from './routes/admin';
|
||||
import billingRoutes from './routes/billing';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
app.use(helmet());
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'OK', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// API Routes
|
||||
app.use('/auth', authRoutes);
|
||||
app.use('/workflows', workflowRoutes);
|
||||
app.use('/templates', templateRoutes);
|
||||
app.use('/integrations', integrationRoutes);
|
||||
app.use('/webhooks', webhookRoutes);
|
||||
app.use('/logs', logRoutes);
|
||||
app.use('/chat', chatRoutes);
|
||||
app.use('/admin', adminRoutes);
|
||||
app.use('/billing', billingRoutes);
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
});
|
||||
|
||||
export default app;
|
||||
36
backend/src/middleware/auth.ts
Normal file
36
backend/src/middleware/auth.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { Request, Response, NextFunction } from 'express';
|
||||
import { verifyToken } from '../utils/auth';
|
||||
|
||||
export interface AuthRequest extends Request {
|
||||
user?: {
|
||||
id: number;
|
||||
email: string;
|
||||
role?: string;
|
||||
subscription_plan?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const authenticateToken = (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
const authHeader = req.headers['authorization'];
|
||||
const token = authHeader && authHeader.split(' ')[1];
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ success: false, message: 'Access token required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = verifyToken(token);
|
||||
req.user = {
|
||||
id: decoded.userId,
|
||||
email: decoded.email,
|
||||
role: decoded.role,
|
||||
subscription_plan: decoded.subscription_plan
|
||||
};
|
||||
next();
|
||||
} catch (error) {
|
||||
return res.status(403).json({ success: false, message: 'Invalid or expired token' });
|
||||
}
|
||||
};
|
||||
|
||||
// Alias for consistency
|
||||
export const auth = authenticateToken;
|
||||
27
backend/src/routes/admin.ts
Normal file
27
backend/src/routes/admin.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import express from 'express';
|
||||
import { auth } from '../middleware/auth';
|
||||
import {
|
||||
getAnalytics,
|
||||
getAllUsers,
|
||||
updateUserRole,
|
||||
getSystemSettings,
|
||||
updateSystemSetting
|
||||
} from '../controllers/adminController';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// All admin routes require authentication
|
||||
router.use(auth);
|
||||
|
||||
// Analytics
|
||||
router.get('/analytics', getAnalytics);
|
||||
|
||||
// User management
|
||||
router.get('/users', getAllUsers);
|
||||
router.patch('/users/:userId/role', updateUserRole);
|
||||
|
||||
// System settings
|
||||
router.get('/settings', getSystemSettings);
|
||||
router.patch('/settings/:key', updateSystemSetting);
|
||||
|
||||
export default router;
|
||||
10
backend/src/routes/auth.ts
Normal file
10
backend/src/routes/auth.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import express from 'express';
|
||||
import { signup, login, resetPassword } from '../controllers/authController';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/signup', signup);
|
||||
router.post('/login', login);
|
||||
router.post('/reset-password', resetPassword);
|
||||
|
||||
export default router;
|
||||
28
backend/src/routes/billing.ts
Normal file
28
backend/src/routes/billing.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import express from 'express';
|
||||
import { auth } from '../middleware/auth';
|
||||
import {
|
||||
getSubscriptionPlans,
|
||||
getCurrentSubscription,
|
||||
upgradeSubscription,
|
||||
cancelSubscription,
|
||||
getUsageStats,
|
||||
handlePaymentWebhook,
|
||||
generateInvoice
|
||||
} from '../controllers/billingController';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Public routes
|
||||
router.get('/plans', getSubscriptionPlans);
|
||||
router.post('/webhook', handlePaymentWebhook); // No auth for webhooks
|
||||
|
||||
// Protected routes
|
||||
router.use(auth);
|
||||
|
||||
router.get('/subscription', getCurrentSubscription);
|
||||
router.post('/subscription/upgrade', upgradeSubscription);
|
||||
router.post('/subscription/cancel', cancelSubscription);
|
||||
router.get('/usage', getUsageStats);
|
||||
router.get('/invoice/:paymentId', generateInvoice);
|
||||
|
||||
export default router;
|
||||
11
backend/src/routes/chat.ts
Normal file
11
backend/src/routes/chat.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import express from 'express';
|
||||
import { sendMessage, getChatHistory } from '../controllers/chatController';
|
||||
import { authenticateToken } from '../middleware/auth';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticateToken);
|
||||
router.post('/', sendMessage);
|
||||
router.get('/history', getChatHistory);
|
||||
|
||||
export default router;
|
||||
12
backend/src/routes/integrations.ts
Normal file
12
backend/src/routes/integrations.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import express from 'express';
|
||||
import { getIntegrations, connectIntegration, disconnectIntegration } from '../controllers/integrationController';
|
||||
import { authenticateToken } from '../middleware/auth';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticateToken);
|
||||
router.get('/', getIntegrations);
|
||||
router.post('/:provider/connect', connectIntegration);
|
||||
router.delete('/:id', disconnectIntegration);
|
||||
|
||||
export default router;
|
||||
11
backend/src/routes/logs.ts
Normal file
11
backend/src/routes/logs.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import express from 'express';
|
||||
import { getWorkflowExecutions, getExecutionDetails } from '../controllers/logController';
|
||||
import { authenticateToken } from '../middleware/auth';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticateToken);
|
||||
router.get('/', getWorkflowExecutions);
|
||||
router.get('/:id', getExecutionDetails);
|
||||
|
||||
export default router;
|
||||
10
backend/src/routes/templates.ts
Normal file
10
backend/src/routes/templates.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import express from 'express';
|
||||
import { getTemplates, installTemplate } from '../controllers/templateController';
|
||||
import { auth } from '../middleware/auth';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', auth, getTemplates);
|
||||
router.post('/:id/install', auth, installTemplate);
|
||||
|
||||
export default router;
|
||||
16
backend/src/routes/webhooks.ts
Normal file
16
backend/src/routes/webhooks.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import express from 'express';
|
||||
import { getWebhooks, createWebhook, getWebhookLogs, deleteWebhook, handleWebhookCall } from '../controllers/webhookController';
|
||||
import { authenticateToken } from '../middleware/auth';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticateToken);
|
||||
router.get('/', getWebhooks);
|
||||
router.post('/', createWebhook);
|
||||
router.get('/:id/logs', getWebhookLogs);
|
||||
router.delete('/:id', deleteWebhook);
|
||||
|
||||
// Public webhook endpoint
|
||||
router.post('/webhook/:user_id/:slug', handleWebhookCall);
|
||||
|
||||
export default router;
|
||||
34
backend/src/routes/workflows.ts
Normal file
34
backend/src/routes/workflows.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import express from 'express';
|
||||
import {
|
||||
getWorkflows,
|
||||
getWorkflow,
|
||||
createWorkflow,
|
||||
updateWorkflow,
|
||||
deleteWorkflow,
|
||||
activateWorkflow,
|
||||
deactivateWorkflow,
|
||||
executeWorkflow,
|
||||
getWorkflowExecutions
|
||||
} from '../controllers/workflowController';
|
||||
import { authenticateToken } from '../middleware/auth';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticateToken);
|
||||
|
||||
// Workflow CRUD
|
||||
router.get('/', getWorkflows);
|
||||
router.get('/:id', getWorkflow);
|
||||
router.post('/', createWorkflow);
|
||||
router.put('/:id', updateWorkflow);
|
||||
router.delete('/:id', deleteWorkflow);
|
||||
|
||||
// Workflow Actions
|
||||
router.post('/:id/activate', activateWorkflow);
|
||||
router.post('/:id/deactivate', deactivateWorkflow);
|
||||
router.post('/:id/execute', executeWorkflow);
|
||||
|
||||
// Workflow Executions
|
||||
router.get('/:id/executions', getWorkflowExecutions);
|
||||
|
||||
export default router;
|
||||
226
backend/src/services/n8nService.ts
Normal file
226
backend/src/services/n8nService.ts
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
import axios, { AxiosInstance } from 'axios';
|
||||
|
||||
interface N8nConfig {
|
||||
baseUrl: string;
|
||||
apiToken: string;
|
||||
}
|
||||
|
||||
interface WorkflowData {
|
||||
name: string;
|
||||
active: boolean;
|
||||
nodes: any[];
|
||||
connections: any;
|
||||
settings?: any;
|
||||
staticData?: any;
|
||||
}
|
||||
|
||||
interface N8nWorkflow {
|
||||
id: string;
|
||||
name: string;
|
||||
active: boolean;
|
||||
nodes: any[];
|
||||
connections: any;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
settings?: any;
|
||||
staticData?: any;
|
||||
}
|
||||
|
||||
interface ExecutionResult {
|
||||
id: string;
|
||||
finished: boolean;
|
||||
mode: string;
|
||||
startedAt: string;
|
||||
stoppedAt?: string;
|
||||
workflowData: any;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
class N8nService {
|
||||
private client: AxiosInstance;
|
||||
private config: N8nConfig;
|
||||
|
||||
constructor(config: N8nConfig) {
|
||||
this.config = config;
|
||||
this.client = axios.create({
|
||||
baseURL: `${config.baseUrl}/api/v1`,
|
||||
headers: {
|
||||
'X-N8N-API-KEY': config.apiToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
// Add response interceptor for error handling
|
||||
this.client.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
console.error('N8n API Error:', error.response?.data || error.message);
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Workflow Management
|
||||
async getWorkflows(): Promise<N8nWorkflow[]> {
|
||||
try {
|
||||
const response = await this.client.get('/workflows');
|
||||
return response.data.data || response.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch workflows:', error);
|
||||
throw new Error('Failed to fetch workflows from n8n');
|
||||
}
|
||||
}
|
||||
|
||||
async getWorkflow(workflowId: string): Promise<N8nWorkflow> {
|
||||
try {
|
||||
const response = await this.client.get(`/workflows/${workflowId}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch workflow ${workflowId}:`, error);
|
||||
throw new Error(`Failed to fetch workflow ${workflowId} from n8n`);
|
||||
}
|
||||
}
|
||||
|
||||
async createWorkflow(workflowData: WorkflowData): Promise<N8nWorkflow> {
|
||||
try {
|
||||
const response = await this.client.post('/workflows', workflowData);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to create workflow:', error);
|
||||
throw new Error('Failed to create workflow in n8n');
|
||||
}
|
||||
}
|
||||
|
||||
async updateWorkflow(workflowId: string, workflowData: Partial<WorkflowData>): Promise<N8nWorkflow> {
|
||||
try {
|
||||
const response = await this.client.patch(`/workflows/${workflowId}`, workflowData);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(`Failed to update workflow ${workflowId}:`, error);
|
||||
throw new Error(`Failed to update workflow ${workflowId} in n8n`);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteWorkflow(workflowId: string): Promise<void> {
|
||||
try {
|
||||
await this.client.delete(`/workflows/${workflowId}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete workflow ${workflowId}:`, error);
|
||||
throw new Error(`Failed to delete workflow ${workflowId} from n8n`);
|
||||
}
|
||||
}
|
||||
|
||||
async activateWorkflow(workflowId: string): Promise<N8nWorkflow> {
|
||||
try {
|
||||
const response = await this.client.patch(`/workflows/${workflowId}`, { active: true });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(`Failed to activate workflow ${workflowId}:`, error);
|
||||
throw new Error(`Failed to activate workflow ${workflowId} in n8n`);
|
||||
}
|
||||
}
|
||||
|
||||
async deactivateWorkflow(workflowId: string): Promise<N8nWorkflow> {
|
||||
try {
|
||||
const response = await this.client.patch(`/workflows/${workflowId}`, { active: false });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(`Failed to deactivate workflow ${workflowId}:`, error);
|
||||
throw new Error(`Failed to deactivate workflow ${workflowId} in n8n`);
|
||||
}
|
||||
}
|
||||
|
||||
// Execution Management
|
||||
async executeWorkflow(workflowId: string, inputData?: any): Promise<ExecutionResult> {
|
||||
try {
|
||||
const response = await this.client.post(`/workflows/${workflowId}/execute`, {
|
||||
...(inputData && { inputData })
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(`Failed to execute workflow ${workflowId}:`, error);
|
||||
throw new Error(`Failed to execute workflow ${workflowId} in n8n`);
|
||||
}
|
||||
}
|
||||
|
||||
async getExecutions(workflowId?: string, limit: number = 20): Promise<ExecutionResult[]> {
|
||||
try {
|
||||
const params: any = { limit };
|
||||
if (workflowId) {
|
||||
params.workflowId = workflowId;
|
||||
}
|
||||
|
||||
const response = await this.client.get('/executions', { params });
|
||||
return response.data.data || response.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch executions:', error);
|
||||
throw new Error('Failed to fetch executions from n8n');
|
||||
}
|
||||
}
|
||||
|
||||
async getExecution(executionId: string): Promise<ExecutionResult> {
|
||||
try {
|
||||
const response = await this.client.get(`/executions/${executionId}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch execution ${executionId}:`, error);
|
||||
throw new Error(`Failed to fetch execution ${executionId} from n8n`);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteExecution(executionId: string): Promise<void> {
|
||||
try {
|
||||
await this.client.delete(`/executions/${executionId}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete execution ${executionId}:`, error);
|
||||
throw new Error(`Failed to delete execution ${executionId} from n8n`);
|
||||
}
|
||||
}
|
||||
|
||||
// Health and Status
|
||||
async getHealth(): Promise<{ status: string }> {
|
||||
try {
|
||||
const response = await this.client.get('/health');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to check n8n health:', error);
|
||||
throw new Error('Failed to check n8n health');
|
||||
}
|
||||
}
|
||||
|
||||
// Credentials Management (if needed)
|
||||
async getCredentials(): Promise<any[]> {
|
||||
try {
|
||||
const response = await this.client.get('/credentials');
|
||||
return response.data.data || response.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch credentials:', error);
|
||||
throw new Error('Failed to fetch credentials from n8n');
|
||||
}
|
||||
}
|
||||
|
||||
// Test connection
|
||||
async testConnection(): Promise<boolean> {
|
||||
try {
|
||||
await this.getHealth();
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Factory function to create n8n service instance
|
||||
export const createN8nService = (config: N8nConfig): N8nService => {
|
||||
return new N8nService(config);
|
||||
};
|
||||
|
||||
// Default instance using environment variables
|
||||
export const n8nService = createN8nService({
|
||||
baseUrl: process.env.N8N_BASE_URL || 'https://bot.ai-impress.com',
|
||||
apiToken: process.env.N8N_API_TOKEN || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1OWQyN2Q0ZS1jNTU3LTQxMDUtYjZmMy05Y2JmM2U5MzU1NWUiLCJpc3MiOiJuOG4iLCJhdWQiOiJwdWJsaWMtYXBpIiwiaWF0IjoxNzU5MDc1NDI4fQ.HCnDOPdq8GcphuPpz1o9871VOLbpMvd7m8C47e2Kq50',
|
||||
});
|
||||
|
||||
export default n8nService;
|
||||
export type { N8nWorkflow, WorkflowData, ExecutionResult, N8nConfig };
|
||||
35
backend/src/types/index.ts
Normal file
35
backend/src/types/index.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
export interface User {
|
||||
id: number;
|
||||
email: string;
|
||||
password_hash: string;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
is_verified: boolean;
|
||||
role: string;
|
||||
verification_token?: string;
|
||||
reset_password_token?: string;
|
||||
reset_password_expires?: Date;
|
||||
subscription_plan: string;
|
||||
subscription_status: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
export interface LoginRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface SignupRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
}
|
||||
|
||||
export interface AuthResponse {
|
||||
success: boolean;
|
||||
token?: string;
|
||||
user?: Omit<User, 'password_hash'>;
|
||||
message?: string;
|
||||
}
|
||||
26
backend/src/utils/auth.ts
Normal file
26
backend/src/utils/auth.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import jwt, { SignOptions } from 'jsonwebtoken';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your_super_secret_jwt_key_change_this';
|
||||
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '24h';
|
||||
|
||||
export const hashPassword = async (password: string): Promise<string> => {
|
||||
const saltRounds = 10;
|
||||
return bcrypt.hash(password, saltRounds);
|
||||
};
|
||||
|
||||
export const comparePassword = async (password: string, hash: string): Promise<boolean> => {
|
||||
return bcrypt.compare(password, hash);
|
||||
};
|
||||
|
||||
export const generateToken = (payload: object): string => {
|
||||
return jwt.sign(payload, JWT_SECRET, { expiresIn: '24h' });
|
||||
};
|
||||
|
||||
export const verifyToken = (token: string): any => {
|
||||
return jwt.verify(token, JWT_SECRET);
|
||||
};
|
||||
|
||||
export const generateRandomToken = (): string => {
|
||||
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
||||
};
|
||||
18
backend/src/utils/db.ts
Normal file
18
backend/src/utils/db.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { Pool } from 'pg';
|
||||
|
||||
const pool = new Pool({
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT || '5432'),
|
||||
database: process.env.DB_NAME || 'saas_automation',
|
||||
user: process.env.DB_USER || 'postgres',
|
||||
password: process.env.DB_PASSWORD || 'password',
|
||||
max: 10,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
});
|
||||
|
||||
export const query = (text: string, params?: any[]) => {
|
||||
return pool.query(text, params);
|
||||
};
|
||||
|
||||
export default pool;
|
||||
16
backend/tsconfig.json
Normal file
16
backend/tsconfig.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2020"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
45
database/docker-compose.yml
Normal file
45
database/docker-compose.yml
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
environment:
|
||||
POSTGRES_DB: saas_automation
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: password
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./schema.sql:/docker-entrypoint-initdb.d/schema.sql
|
||||
networks:
|
||||
- saas_network
|
||||
|
||||
n8n:
|
||||
image: n8nio/n8n
|
||||
ports:
|
||||
- "5678:5678"
|
||||
environment:
|
||||
- DB_TYPE=postgresdb
|
||||
- DB_POSTGRESDB_HOST=postgres
|
||||
- DB_POSTGRESDB_PORT=5432
|
||||
- DB_POSTGRESDB_DATABASE=n8n
|
||||
- DB_POSTGRESDB_USER=postgres
|
||||
- DB_POSTGRESDB_PASSWORD=password
|
||||
- N8N_BASIC_AUTH_ACTIVE=true
|
||||
- N8N_BASIC_AUTH_USER=admin
|
||||
- N8N_BASIC_AUTH_PASSWORD=admin
|
||||
volumes:
|
||||
- n8n_data:/home/node/.n8n
|
||||
depends_on:
|
||||
- postgres
|
||||
networks:
|
||||
- saas_network
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
n8n_data:
|
||||
|
||||
networks:
|
||||
saas_network:
|
||||
driver: bridge
|
||||
290
database/schema.sql
Normal file
290
database/schema.sql
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
-- SaaS Automation Platform Database Schema
|
||||
|
||||
-- Create n8n database
|
||||
CREATE DATABASE n8n;
|
||||
|
||||
-- Users table
|
||||
CREATE TABLE users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
first_name VARCHAR(100),
|
||||
last_name VARCHAR(100),
|
||||
is_verified BOOLEAN DEFAULT false,
|
||||
verification_token VARCHAR(255),
|
||||
reset_password_token VARCHAR(255),
|
||||
reset_password_expires TIMESTAMP,
|
||||
role VARCHAR(50) DEFAULT 'free',
|
||||
subscription_plan VARCHAR(50) DEFAULT 'free',
|
||||
subscription_status VARCHAR(50) DEFAULT 'active',
|
||||
subscription_starts_at TIMESTAMP,
|
||||
subscription_ends_at TIMESTAMP,
|
||||
workflow_limit INTEGER DEFAULT 3,
|
||||
user_limit INTEGER DEFAULT 1,
|
||||
features JSONB DEFAULT '{"telegram_notifications": false, "email_notifications": true, "api_access": false, "premium_templates": false}',
|
||||
telegram_chat_id VARCHAR(255),
|
||||
notification_preferences JSONB DEFAULT '{"email": true, "telegram": false, "workflow_status": true, "system_updates": false}',
|
||||
onboarding_completed BOOLEAN DEFAULT false,
|
||||
last_login_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Workflows table
|
||||
CREATE TABLE workflows (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
n8n_workflow_id VARCHAR(255),
|
||||
status VARCHAR(50) DEFAULT 'inactive',
|
||||
trigger_type VARCHAR(100),
|
||||
trigger_config JSONB,
|
||||
actions_config JSONB,
|
||||
is_active BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Index for n8n_workflow_id
|
||||
CREATE INDEX idx_workflows_n8n_workflow_id ON workflows(n8n_workflow_id);
|
||||
|
||||
-- Templates table (automation templates)
|
||||
CREATE TABLE templates (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
category VARCHAR(100),
|
||||
trigger_type VARCHAR(100),
|
||||
trigger_config JSONB,
|
||||
actions_config JSONB,
|
||||
n8n_template JSONB,
|
||||
tags TEXT[],
|
||||
is_featured BOOLEAN DEFAULT false,
|
||||
install_count INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- User installed templates
|
||||
CREATE TABLE user_templates (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
template_id INTEGER REFERENCES templates(id) ON DELETE CASCADE,
|
||||
workflow_id INTEGER REFERENCES workflows(id) ON DELETE CASCADE,
|
||||
installed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(user_id, template_id)
|
||||
);
|
||||
|
||||
-- Integrations table (OAuth connections)
|
||||
CREATE TABLE integrations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
provider VARCHAR(100) NOT NULL,
|
||||
provider_user_id VARCHAR(255),
|
||||
access_token TEXT,
|
||||
refresh_token TEXT,
|
||||
token_expires_at TIMESTAMP,
|
||||
scopes TEXT[],
|
||||
account_name VARCHAR(255),
|
||||
account_email VARCHAR(255),
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Webhooks table
|
||||
CREATE TABLE webhooks (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
slug VARCHAR(255) NOT NULL,
|
||||
trigger_type VARCHAR(100),
|
||||
secret_key VARCHAR(255),
|
||||
url_path VARCHAR(255) UNIQUE,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Webhook logs
|
||||
CREATE TABLE webhook_logs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
webhook_id INTEGER REFERENCES webhooks(id) ON DELETE CASCADE,
|
||||
request_method VARCHAR(10),
|
||||
request_headers JSONB,
|
||||
request_payload JSONB,
|
||||
response_status INTEGER,
|
||||
response_payload JSONB,
|
||||
processing_time_ms INTEGER,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Workflow executions/logs
|
||||
CREATE TABLE workflow_executions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
workflow_id INTEGER REFERENCES workflows(id) ON DELETE CASCADE,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
n8n_execution_id VARCHAR(255),
|
||||
status VARCHAR(50),
|
||||
trigger_data JSONB,
|
||||
execution_data JSONB,
|
||||
error_message TEXT,
|
||||
started_at TIMESTAMP,
|
||||
finished_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Chat history (AI chatbot)
|
||||
CREATE TABLE chat_history (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
session_id VARCHAR(255),
|
||||
message_type VARCHAR(20) CHECK (message_type IN ('user', 'assistant')),
|
||||
content TEXT NOT NULL,
|
||||
metadata JSONB,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Indexes for performance
|
||||
CREATE INDEX idx_users_email ON users(email);
|
||||
CREATE INDEX idx_workflows_user_id ON workflows(user_id);
|
||||
CREATE INDEX idx_workflows_status ON workflows(status);
|
||||
CREATE INDEX idx_templates_category ON templates(category);
|
||||
CREATE INDEX idx_integrations_user_id ON integrations(user_id);
|
||||
CREATE INDEX idx_integrations_provider ON integrations(provider);
|
||||
CREATE INDEX idx_webhooks_user_id ON webhooks(user_id);
|
||||
CREATE INDEX idx_webhooks_url_path ON webhooks(url_path);
|
||||
CREATE INDEX idx_webhook_logs_webhook_id ON webhook_logs(webhook_id);
|
||||
CREATE INDEX idx_workflow_executions_workflow_id ON workflow_executions(workflow_id);
|
||||
CREATE INDEX idx_workflow_executions_user_id ON workflow_executions(user_id);
|
||||
CREATE INDEX idx_chat_history_user_id ON chat_history(user_id);
|
||||
CREATE INDEX idx_chat_history_session_id ON chat_history(session_id);
|
||||
|
||||
-- Subscription plans table
|
||||
CREATE TABLE subscription_plans (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
price_monthly DECIMAL(10,2) NOT NULL,
|
||||
price_yearly DECIMAL(10,2),
|
||||
workflow_limit INTEGER NOT NULL,
|
||||
user_limit INTEGER NOT NULL,
|
||||
features JSONB NOT NULL,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- User subscriptions history
|
||||
CREATE TABLE subscription_history (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
plan_name VARCHAR(100) NOT NULL,
|
||||
amount DECIMAL(10,2) NOT NULL,
|
||||
currency VARCHAR(10) DEFAULT 'GBP',
|
||||
payment_method VARCHAR(50),
|
||||
payment_status VARCHAR(50) DEFAULT 'pending',
|
||||
payment_provider_id VARCHAR(255),
|
||||
billing_period_start TIMESTAMP NOT NULL,
|
||||
billing_period_end TIMESTAMP NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- User teams table (for multi-user accounts)
|
||||
CREATE TABLE user_teams (
|
||||
id SERIAL PRIMARY KEY,
|
||||
owner_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
member_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
role VARCHAR(50) DEFAULT 'member',
|
||||
invited_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
joined_at TIMESTAMP,
|
||||
status VARCHAR(50) DEFAULT 'pending',
|
||||
UNIQUE(owner_id, member_id)
|
||||
);
|
||||
|
||||
-- Notifications table
|
||||
CREATE TABLE notifications (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
data JSONB,
|
||||
is_read BOOLEAN DEFAULT false,
|
||||
sent_via VARCHAR(50),
|
||||
sent_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- API tokens table
|
||||
CREATE TABLE api_tokens (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
token_hash VARCHAR(255) NOT NULL,
|
||||
permissions JSONB DEFAULT '["workflows:read"]',
|
||||
last_used_at TIMESTAMP,
|
||||
expires_at TIMESTAMP,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Audit logs table
|
||||
CREATE TABLE audit_logs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
action VARCHAR(100) NOT NULL,
|
||||
resource_type VARCHAR(50) NOT NULL,
|
||||
resource_id VARCHAR(100),
|
||||
details JSONB,
|
||||
ip_address INET,
|
||||
user_agent TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- System settings table
|
||||
CREATE TABLE system_settings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
key VARCHAR(100) UNIQUE NOT NULL,
|
||||
value JSONB NOT NULL,
|
||||
description TEXT,
|
||||
is_public BOOLEAN DEFAULT false,
|
||||
updated_by INTEGER REFERENCES users(id),
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Sample data
|
||||
INSERT INTO subscription_plans (name, price_monthly, price_yearly, workflow_limit, user_limit, features) VALUES
|
||||
('Free', 0.00, 0.00, 3, 1, '{"telegram_notifications": false, "email_notifications": true, "api_access": false, "premium_templates": false, "support": "community", "execution_limit": 100}'),
|
||||
('Pro', 150.00, 1500.00, 10, 2, '{"telegram_notifications": true, "email_notifications": true, "api_access": true, "premium_templates": true, "support": "email", "execution_limit": 10000}'),
|
||||
('Enterprise', 0.00, 0.00, -1, -1, '{"telegram_notifications": true, "email_notifications": true, "api_access": true, "premium_templates": true, "support": "priority", "execution_limit": -1, "custom_integrations": true}');
|
||||
|
||||
INSERT INTO system_settings (key, value, description, is_public) VALUES
|
||||
('trial_duration_days', '7', 'Trial period duration in days', false),
|
||||
('max_workflows_free', '3', 'Maximum workflows for free users', false),
|
||||
('maintenance_mode', 'false', 'System maintenance mode', true),
|
||||
('welcome_message', '"Welcome to AutomationHub! Start automating your workflows today."', 'Welcome message for new users', true);
|
||||
|
||||
INSERT INTO templates (name, description, category, trigger_type, trigger_config, actions_config, tags, is_featured) VALUES
|
||||
('Instagram to CRM', 'Automatically sync new Instagram followers to your CRM', 'Social Media', 'instagram_follower',
|
||||
'{"event": "new_follower"}',
|
||||
'{"actions": [{"type": "crm_create_contact", "fields": ["username", "profile_url"]}]}',
|
||||
ARRAY['instagram', 'crm', 'automation'], true),
|
||||
|
||||
('Email to Task Manager', 'Create tasks from important emails', 'Productivity', 'email_received',
|
||||
'{"filters": {"importance": "high"}}',
|
||||
'{"actions": [{"type": "task_create", "fields": ["subject", "sender", "body"]}]}',
|
||||
ARRAY['email', 'productivity', 'tasks'], true),
|
||||
|
||||
('Form Submission to Slack', 'Post form submissions to Slack channel', 'Communication', 'webhook',
|
||||
'{"event": "form_submit"}',
|
||||
'{"actions": [{"type": "slack_message", "channel": "#leads"}]}',
|
||||
ARRAY['webhook', 'slack', 'forms'], false);
|
||||
|
||||
-- Insert global admin (password is 'admin123')
|
||||
INSERT INTO users (email, password_hash, first_name, last_name, is_verified, role, subscription_plan, workflow_limit, user_limit, features) VALUES
|
||||
('info@ai-impress.com', '$2b$10$8K1p/a0dclxKqnvzBL5TM.N7EH1Cx0m1K2oOKy2Tc2z1H5U2N3V6K', 'Global', 'Admin', true, 'admin', 'enterprise', -1, -1, '{"telegram_notifications": true, "email_notifications": true, "api_access": true, "premium_templates": true, "support": "priority", "execution_limit": -1, "admin_panel": true, "system_settings": true}');
|
||||
|
||||
-- Insert sample user (password is 'password123')
|
||||
INSERT INTO users (email, password_hash, first_name, last_name, is_verified, subscription_starts_at, subscription_ends_at) VALUES
|
||||
('demo@example.com', '$2b$10$8K1p/a0dclxKqnvzBL5TM.N7EH1Cx0m1K2oOKy2Tc2z1H5U2N3V6K', 'Demo', 'User', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP + INTERVAL '7 days');
|
||||
41
frontend/.gitignore
vendored
Normal file
41
frontend/.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
|
||||
21
frontend/Dockerfile
Normal file
21
frontend/Dockerfile
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package.json and package-lock.json (if available)
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci --only=production
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN npm run build
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3000
|
||||
|
||||
# Start the application
|
||||
CMD ["npm", "start"]
|
||||
18
frontend/Dockerfile.dev
Normal file
18
frontend/Dockerfile.dev
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package.json and package-lock.json
|
||||
COPY package*.json ./
|
||||
|
||||
# Install all dependencies (including dev dependencies)
|
||||
RUN npm install
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3000
|
||||
|
||||
# Start the application in development mode
|
||||
CMD ["npm", "run", "dev"]
|
||||
36
frontend/README.md
Normal file
36
frontend/README.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
25
frontend/eslint.config.mjs
Normal file
25
frontend/eslint.config.mjs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
{
|
||||
ignores: [
|
||||
"node_modules/**",
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
7
frontend/next.config.ts
Normal file
7
frontend/next.config.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
6420
frontend/package-lock.json
generated
Normal file
6420
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
31
frontend/package.json
Normal file
31
frontend/package.json
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build --turbopack",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.9",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"axios": "^1.12.2",
|
||||
"lucide-react": "^0.544.0",
|
||||
"next": "15.5.4",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.5.4",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
5
frontend/postcss.config.mjs
Normal file
5
frontend/postcss.config.mjs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
};
|
||||
|
||||
export default config;
|
||||
505
frontend/src/app/admin/page.tsx
Normal file
505
frontend/src/app/admin/page.tsx
Normal file
|
|
@ -0,0 +1,505 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import api from '@/lib/api';
|
||||
import {
|
||||
UsersIcon,
|
||||
CogIcon,
|
||||
ChartBarIcon,
|
||||
CurrencyDollarIcon,
|
||||
ServerIcon,
|
||||
ClockIcon,
|
||||
CheckCircleIcon,
|
||||
XCircleIcon,
|
||||
ArrowTrendingUpIcon,
|
||||
ArrowTrendingDownIcon,
|
||||
EyeIcon,
|
||||
PencilIcon,
|
||||
ShieldCheckIcon,
|
||||
UserGroupIcon,
|
||||
BuildingOffice2Icon,
|
||||
PlayIcon,
|
||||
PauseIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
interface Analytics {
|
||||
userStats: {
|
||||
total_users: number;
|
||||
free_users: number;
|
||||
pro_users: number;
|
||||
enterprise_users: number;
|
||||
verified_users: number;
|
||||
active_last_7_days: number;
|
||||
new_last_30_days: number;
|
||||
};
|
||||
workflowStats: {
|
||||
total_workflows: number;
|
||||
active_workflows: number;
|
||||
new_workflows_30_days: number;
|
||||
activation_rate: number;
|
||||
};
|
||||
executionStats: {
|
||||
total_executions: number;
|
||||
successful_executions: number;
|
||||
executions_24h: number;
|
||||
executions_7d: number;
|
||||
};
|
||||
revenueStats: {
|
||||
total_revenue: number;
|
||||
revenue_30_days: number;
|
||||
successful_payments: number;
|
||||
average_order_value: number;
|
||||
};
|
||||
recentActivity: Array<{
|
||||
action: string;
|
||||
resource_type: string;
|
||||
created_at: string;
|
||||
user_email: string;
|
||||
}>;
|
||||
topUsers: Array<{
|
||||
email: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
subscription_plan: string;
|
||||
workflow_count: number;
|
||||
last_login_at: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
role: string;
|
||||
subscription_plan: string;
|
||||
subscription_status: string;
|
||||
is_verified: boolean;
|
||||
last_login_at: string;
|
||||
created_at: string;
|
||||
workflow_count: number;
|
||||
}
|
||||
|
||||
export default function AdminDashboard() {
|
||||
const { user } = useAuth();
|
||||
const router = useRouter();
|
||||
const [analytics, setAnalytics] = useState<Analytics | null>(null);
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [usersLoading, setUsersLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('overview');
|
||||
|
||||
useEffect(() => {
|
||||
if (!user || user.role !== 'admin') {
|
||||
router.push('/dashboard');
|
||||
return;
|
||||
}
|
||||
fetchAnalytics();
|
||||
}, [user, router]);
|
||||
|
||||
const fetchAnalytics = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.get('/admin/analytics');
|
||||
setAnalytics(response.data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching analytics:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
setUsersLoading(true);
|
||||
const response = await api.get('/admin/users?limit=50');
|
||||
setUsers(response.data.users);
|
||||
} catch (error) {
|
||||
console.error('Error fetching users:', error);
|
||||
} finally {
|
||||
setUsersLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateUserRole = async (userId: string, role: string, subscription_plan: string) => {
|
||||
try {
|
||||
await api.patch(`/admin/users/${userId}/role`, { role, subscription_plan });
|
||||
fetchUsers(); // Refresh users list
|
||||
} catch (error) {
|
||||
console.error('Error updating user role:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('en-GB', {
|
||||
style: 'currency',
|
||||
currency: 'GBP'
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
const getRoleIcon = (role: string) => {
|
||||
switch (role) {
|
||||
case 'admin':
|
||||
return <ShieldCheckIcon className="w-4 h-4 text-red-500" />;
|
||||
case 'pro':
|
||||
return <UserGroupIcon className="w-4 h-4 text-blue-500" />;
|
||||
case 'enterprise':
|
||||
return <BuildingOffice2Icon className="w-4 h-4 text-purple-500" />;
|
||||
default:
|
||||
return <UsersIcon className="w-4 h-4 text-gray-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getSubscriptionColor = (plan: string) => {
|
||||
switch (plan) {
|
||||
case 'enterprise':
|
||||
return 'bg-purple-100 text-purple-800 border-purple-200';
|
||||
case 'pro':
|
||||
return 'bg-blue-100 text-blue-800 border-blue-200';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-800 border-gray-200';
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64 bg-gradient-to-br from-orange-50 to-purple-50">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-orange-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!analytics) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-orange-50 via-white to-purple-50 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="text-center py-16">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-4">Failed to load analytics</h1>
|
||||
<button
|
||||
onClick={fetchAnalytics}
|
||||
className="gradient-primary text-white px-6 py-3 rounded-xl font-semibold"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-orange-50 via-white to-purple-50 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl font-bold gradient-text mb-2">Admin Dashboard</h1>
|
||||
<p className="text-gray-600 text-lg">
|
||||
Complete system analytics and user management
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div className="mb-8">
|
||||
<div className="flex space-x-1 bg-white p-1 rounded-xl border border-gray-200">
|
||||
<button
|
||||
onClick={() => setActiveTab('overview')}
|
||||
className={`px-6 py-2 rounded-lg font-semibold transition-all ${
|
||||
activeTab === 'overview'
|
||||
? 'gradient-primary text-white shadow-lg'
|
||||
: 'text-gray-600 hover:text-orange-600'
|
||||
}`}
|
||||
>
|
||||
Overview
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveTab('users');
|
||||
if (users.length === 0) fetchUsers();
|
||||
}}
|
||||
className={`px-6 py-2 rounded-lg font-semibold transition-all ${
|
||||
activeTab === 'users'
|
||||
? 'gradient-primary text-white shadow-lg'
|
||||
: 'text-gray-600 hover:text-orange-600'
|
||||
}`}
|
||||
>
|
||||
Users
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeTab === 'overview' && (
|
||||
<>
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
{/* User Stats */}
|
||||
<div className="bg-white rounded-2xl p-6 shadow-lg border border-gray-100">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center">
|
||||
<UsersIcon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-gray-900">
|
||||
{analytics.userStats.total_users}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="font-semibold text-gray-900 mb-2">Total Users</h3>
|
||||
<div className="text-sm text-gray-600">
|
||||
<div className="flex justify-between">
|
||||
<span>Free: {analytics.userStats.free_users}</span>
|
||||
<span>Pro: {analytics.userStats.pro_users}</span>
|
||||
</div>
|
||||
<div className="flex justify-between mt-1">
|
||||
<span>Enterprise: {analytics.userStats.enterprise_users}</span>
|
||||
<span>Active 7d: {analytics.userStats.active_last_7_days}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Workflow Stats */}
|
||||
<div className="bg-white rounded-2xl p-6 shadow-lg border border-gray-100">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-orange-500 to-orange-600 rounded-xl flex items-center justify-center">
|
||||
<CogIcon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-gray-900">
|
||||
{analytics.workflowStats.total_workflows}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="font-semibold text-gray-900 mb-2">Workflows</h3>
|
||||
<div className="text-sm text-gray-600">
|
||||
<div className="flex justify-between">
|
||||
<span>Active: {analytics.workflowStats.active_workflows}</span>
|
||||
<span>Rate: {Math.round(analytics.workflowStats.activation_rate)}%</span>
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<span>New (30d): {analytics.workflowStats.new_workflows_30_days}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Execution Stats */}
|
||||
<div className="bg-white rounded-2xl p-6 shadow-lg border border-gray-100">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center">
|
||||
<PlayIcon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-gray-900">
|
||||
{analytics.executionStats.total_executions}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="font-semibold text-gray-900 mb-2">Executions</h3>
|
||||
<div className="text-sm text-gray-600">
|
||||
<div className="flex justify-between">
|
||||
<span>Successful: {analytics.executionStats.successful_executions}</span>
|
||||
<span>24h: {analytics.executionStats.executions_24h}</span>
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<span>7d: {analytics.executionStats.executions_7d}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Revenue Stats */}
|
||||
<div className="bg-white rounded-2xl p-6 shadow-lg border border-gray-100">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-purple-500 to-purple-600 rounded-xl flex items-center justify-center">
|
||||
<CurrencyDollarIcon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-gray-900">
|
||||
{formatCurrency(analytics.revenueStats.total_revenue || 0)}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="font-semibold text-gray-900 mb-2">Revenue</h3>
|
||||
<div className="text-sm text-gray-600">
|
||||
<div className="flex justify-between">
|
||||
<span>30d: {formatCurrency(analytics.revenueStats.revenue_30_days || 0)}</span>
|
||||
<span>Payments: {analytics.revenueStats.successful_payments}</span>
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<span>AOV: {formatCurrency(analytics.revenueStats.average_order_value || 0)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Recent Activity */}
|
||||
<div className="bg-white rounded-2xl shadow-lg border border-gray-100">
|
||||
<div className="px-6 py-4 border-b border-gray-100">
|
||||
<h2 className="text-xl font-bold text-gray-900">Recent Activity</h2>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<div className="space-y-4">
|
||||
{analytics.recentActivity.slice(0, 10).map((activity, index) => (
|
||||
<div key={index} className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
|
||||
<div className="w-2 h-2 bg-orange-500 rounded-full flex-shrink-0"></div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 truncate">
|
||||
{activity.action.replace(/_/g, ' ')} {activity.resource_type}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{activity.user_email} • {formatDate(activity.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top Users */}
|
||||
<div className="bg-white rounded-2xl shadow-lg border border-gray-100">
|
||||
<div className="px-6 py-4 border-b border-gray-100">
|
||||
<h2 className="text-xl font-bold text-gray-900">Top Users by Workflows</h2>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<div className="space-y-4">
|
||||
{analytics.topUsers.slice(0, 10).map((user, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-8 h-8 bg-gradient-to-br from-orange-500 to-purple-500 rounded-full flex items-center justify-center text-white text-sm font-bold">
|
||||
{user.first_name?.charAt(0) || user.email.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
{user.first_name} {user.last_name} ({user.email})
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{user.subscription_plan} • Last login: {user.last_login_at ? formatDate(user.last_login_at) : 'Never'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-lg font-bold text-orange-600">{user.workflow_count}</p>
|
||||
<p className="text-xs text-gray-500">workflows</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === 'users' && (
|
||||
<div className="bg-white rounded-2xl shadow-lg border border-gray-100">
|
||||
<div className="px-6 py-4 border-b border-gray-100">
|
||||
<h2 className="text-xl font-bold text-gray-900">User Management</h2>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
{usersLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-orange-500"></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
User
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Role
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Subscription
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Workflows
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Last Login
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{users.map((user) => (
|
||||
<tr key={user.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-orange-500 to-purple-500 rounded-full flex items-center justify-center text-white text-sm font-bold">
|
||||
{user.first_name?.charAt(0) || user.email.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{user.first_name} {user.last_name}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">{user.email}</div>
|
||||
{user.is_verified && (
|
||||
<CheckCircleIcon className="w-4 h-4 text-green-500 inline" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
{getRoleIcon(user.role)}
|
||||
<span className="ml-2 text-sm text-gray-900 capitalize">{user.role}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border ${getSubscriptionColor(
|
||||
user.subscription_plan
|
||||
)}`}
|
||||
>
|
||||
{user.subscription_plan}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{user.workflow_count}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{user.last_login_at ? formatDate(user.last_login_at) : 'Never'}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div className="flex space-x-2">
|
||||
<select
|
||||
value={user.role}
|
||||
onChange={(e) => updateUserRole(user.id, e.target.value, user.subscription_plan)}
|
||||
className="text-xs border border-gray-300 rounded px-2 py-1"
|
||||
>
|
||||
<option value="free">Free</option>
|
||||
<option value="pro">Pro</option>
|
||||
<option value="enterprise">Enterprise</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
<select
|
||||
value={user.subscription_plan}
|
||||
onChange={(e) => updateUserRole(user.id, user.role, e.target.value)}
|
||||
className="text-xs border border-gray-300 rounded px-2 py-1"
|
||||
>
|
||||
<option value="free">Free</option>
|
||||
<option value="pro">Pro</option>
|
||||
<option value="enterprise">Enterprise</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
474
frontend/src/app/billing/page.tsx
Normal file
474
frontend/src/app/billing/page.tsx
Normal file
|
|
@ -0,0 +1,474 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import api from '@/lib/api';
|
||||
import {
|
||||
CreditCardIcon,
|
||||
CheckIcon,
|
||||
XMarkIcon,
|
||||
ClockIcon,
|
||||
ChartBarIcon,
|
||||
DocumentIcon,
|
||||
ExclamationTriangleIcon,
|
||||
SparklesIcon,
|
||||
UserGroupIcon,
|
||||
BuildingOffice2Icon,
|
||||
InformationCircleIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
|
||||
interface Plan {
|
||||
id: number;
|
||||
name: string;
|
||||
price_monthly: number;
|
||||
price_yearly: number;
|
||||
workflow_limit: number;
|
||||
user_limit: number;
|
||||
features: any;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
interface Subscription {
|
||||
subscription_plan: string;
|
||||
subscription_status: string;
|
||||
subscription_starts_at: string;
|
||||
subscription_ends_at: string;
|
||||
workflow_limit: number;
|
||||
user_limit: number;
|
||||
features: any;
|
||||
is_expired: boolean;
|
||||
days_remaining: number;
|
||||
}
|
||||
|
||||
interface UsageStats {
|
||||
limits: {
|
||||
workflows: number;
|
||||
teamMembers: number;
|
||||
executions: number;
|
||||
};
|
||||
usage: {
|
||||
workflows: number;
|
||||
teamMembers: number;
|
||||
executions: number;
|
||||
};
|
||||
percentages: {
|
||||
workflows: number;
|
||||
teamMembers: number;
|
||||
executions: number;
|
||||
};
|
||||
}
|
||||
|
||||
export default function BillingPage() {
|
||||
const { user } = useAuth();
|
||||
const [plans, setPlans] = useState<Plan[]>([]);
|
||||
const [subscription, setSubscription] = useState<Subscription | null>(null);
|
||||
const [usage, setUsage] = useState<UsageStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [upgrading, setUpgrading] = useState<string | null>(null);
|
||||
const [billingPeriod, setBillingPeriod] = useState<'monthly' | 'yearly'>('monthly');
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [plansRes, subscriptionRes, usageRes] = await Promise.all([
|
||||
api.get('/billing/plans'),
|
||||
api.get('/billing/subscription'),
|
||||
api.get('/billing/usage')
|
||||
]);
|
||||
|
||||
setPlans(plansRes.data.plans);
|
||||
setSubscription(subscriptionRes.data.subscription);
|
||||
setUsage(usageRes.data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching billing data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const upgradePlan = async (planName: string) => {
|
||||
try {
|
||||
setUpgrading(planName);
|
||||
await api.post('/billing/subscription/upgrade', {
|
||||
planName,
|
||||
paymentMethod: 'manual' // In real app, this would be from payment form
|
||||
});
|
||||
|
||||
// Refresh data
|
||||
await fetchData();
|
||||
alert('Subscription upgraded successfully!');
|
||||
} catch (error) {
|
||||
console.error('Error upgrading plan:', error);
|
||||
alert('Failed to upgrade subscription. Please try again.');
|
||||
} finally {
|
||||
setUpgrading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const cancelSubscription = async () => {
|
||||
if (!confirm('Are you sure you want to cancel your subscription?')) return;
|
||||
|
||||
try {
|
||||
await api.post('/billing/subscription/cancel');
|
||||
await fetchData();
|
||||
alert('Subscription cancelled. You will retain access until the end of your billing period.');
|
||||
} catch (error) {
|
||||
console.error('Error cancelling subscription:', error);
|
||||
alert('Failed to cancel subscription. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('en-GB', {
|
||||
style: 'currency',
|
||||
currency: 'GBP'
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
const getPlanIcon = (planName: string) => {
|
||||
switch (planName.toLowerCase()) {
|
||||
case 'free':
|
||||
return <SparklesIcon className="w-8 h-8" />;
|
||||
case 'pro':
|
||||
return <UserGroupIcon className="w-8 h-8" />;
|
||||
case 'enterprise':
|
||||
return <BuildingOffice2Icon className="w-8 h-8" />;
|
||||
default:
|
||||
return <SparklesIcon className="w-8 h-8" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getPlanColor = (planName: string) => {
|
||||
switch (planName.toLowerCase()) {
|
||||
case 'free':
|
||||
return 'from-gray-500 to-gray-600';
|
||||
case 'pro':
|
||||
return 'from-blue-500 to-blue-600';
|
||||
case 'enterprise':
|
||||
return 'from-purple-500 to-purple-600';
|
||||
default:
|
||||
return 'from-gray-500 to-gray-600';
|
||||
}
|
||||
};
|
||||
|
||||
const getUsageColor = (percentage: number) => {
|
||||
if (percentage >= 90) return 'bg-red-500';
|
||||
if (percentage >= 75) return 'bg-yellow-500';
|
||||
return 'bg-green-500';
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64 bg-gradient-to-br from-orange-50 to-purple-50">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-orange-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-orange-50 via-white to-purple-50 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl font-bold gradient-text mb-2">Billing & Subscription</h1>
|
||||
<p className="text-gray-600 text-lg">
|
||||
Manage your subscription and billing settings
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Current Subscription & Usage */}
|
||||
<div className="lg:col-span-1 space-y-6">
|
||||
{/* Current Plan */}
|
||||
{subscription && (
|
||||
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 p-6">
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-4">Current Plan</h2>
|
||||
|
||||
<div className="flex items-center space-x-3 mb-4">
|
||||
<div className={`w-12 h-12 bg-gradient-to-br ${getPlanColor(subscription.subscription_plan)} rounded-xl flex items-center justify-center text-white`}>
|
||||
{getPlanIcon(subscription.subscription_plan)}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 capitalize">
|
||||
{subscription.subscription_plan} Plan
|
||||
</h3>
|
||||
<p className={`text-sm ${
|
||||
subscription.subscription_status === 'active' ? 'text-green-600' : 'text-yellow-600'
|
||||
}`}>
|
||||
{subscription.subscription_status === 'active' ? 'Active' : 'Cancelled'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{subscription.subscription_ends_at && (
|
||||
<div className="mb-4">
|
||||
{subscription.is_expired ? (
|
||||
<div className="flex items-center space-x-2 text-red-600">
|
||||
<ExclamationTriangleIcon className="w-5 h-5" />
|
||||
<span className="text-sm font-medium">Subscription Expired</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center space-x-2 text-gray-600">
|
||||
<ClockIcon className="w-5 h-5" />
|
||||
<span className="text-sm">
|
||||
{subscription.days_remaining} days remaining
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Expires on {formatDate(subscription.subscription_ends_at)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{subscription.subscription_status === 'active' && subscription.subscription_plan !== 'free' && (
|
||||
<button
|
||||
onClick={cancelSubscription}
|
||||
className="w-full bg-red-50 border border-red-200 text-red-700 px-4 py-2 rounded-lg text-sm font-medium hover:bg-red-100 transition-colors"
|
||||
>
|
||||
Cancel Subscription
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Usage Stats */}
|
||||
{usage && (
|
||||
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 p-6">
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-4">Usage This Month</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-2">
|
||||
<span className="text-gray-600">Workflows</span>
|
||||
<span className="font-medium">
|
||||
{usage.usage.workflows}/{usage.limits.workflows === -1 ? '∞' : usage.limits.workflows}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full ${getUsageColor(usage.percentages.workflows)}`}
|
||||
style={{ width: `${Math.min(usage.percentages.workflows, 100)}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-2">
|
||||
<span className="text-gray-600">Team Members</span>
|
||||
<span className="font-medium">
|
||||
{usage.usage.teamMembers}/{usage.limits.teamMembers === -1 ? '∞' : usage.limits.teamMembers}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full ${getUsageColor(usage.percentages.teamMembers)}`}
|
||||
style={{ width: `${Math.min(usage.percentages.teamMembers, 100)}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-2">
|
||||
<span className="text-gray-600">Executions</span>
|
||||
<span className="font-medium">
|
||||
{usage.usage.executions}/{usage.limits.executions === -1 ? '∞' : usage.limits.executions}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full ${getUsageColor(usage.percentages.executions)}`}
|
||||
style={{ width: `${Math.min(usage.percentages.executions, 100)}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Subscription Plans */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-xl font-bold text-gray-900">Subscription Plans</h2>
|
||||
|
||||
<div className="flex items-center bg-gray-100 rounded-lg p-1">
|
||||
<button
|
||||
onClick={() => setBillingPeriod('monthly')}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
|
||||
billingPeriod === 'monthly'
|
||||
? 'bg-white text-gray-900 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
Monthly
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setBillingPeriod('yearly')}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
|
||||
billingPeriod === 'yearly'
|
||||
? 'bg-white text-gray-900 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
Yearly
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{plans.map((plan) => {
|
||||
const isCurrentPlan = subscription?.subscription_plan === plan.name.toLowerCase();
|
||||
const price = billingPeriod === 'yearly' ? plan.price_yearly : plan.price_monthly;
|
||||
const isFree = plan.name.toLowerCase() === 'free';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={plan.id}
|
||||
className={`relative border-2 rounded-2xl p-6 transition-all ${
|
||||
isCurrentPlan
|
||||
? 'border-orange-500 bg-orange-50'
|
||||
: 'border-gray-200 hover:border-orange-300'
|
||||
}`}
|
||||
>
|
||||
{isCurrentPlan && (
|
||||
<div className="absolute -top-3 left-1/2 transform -translate-x-1/2">
|
||||
<span className="bg-orange-500 text-white px-3 py-1 rounded-full text-xs font-medium">
|
||||
Current Plan
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-center mb-6">
|
||||
<div className={`w-16 h-16 bg-gradient-to-br ${getPlanColor(plan.name)} rounded-2xl flex items-center justify-center text-white mx-auto mb-4`}>
|
||||
{getPlanIcon(plan.name)}
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-2 capitalize">{plan.name}</h3>
|
||||
<div className="text-3xl font-bold text-gray-900">
|
||||
{isFree ? (
|
||||
'Free'
|
||||
) : plan.name.toLowerCase() === 'enterprise' ? (
|
||||
'Contact Sales'
|
||||
) : (
|
||||
<>
|
||||
{formatCurrency(price)}
|
||||
<span className="text-sm font-normal text-gray-600">
|
||||
/{billingPeriod === 'yearly' ? 'year' : 'month'}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{billingPeriod === 'yearly' && !isFree && plan.name.toLowerCase() !== 'enterprise' && (
|
||||
<p className="text-sm text-green-600 mt-1">Save 17%</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 mb-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<CheckIcon className="w-5 h-5 text-green-500 flex-shrink-0" />
|
||||
<span className="text-sm text-gray-600">
|
||||
{plan.workflow_limit === -1 ? 'Unlimited' : plan.workflow_limit} workflows
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<CheckIcon className="w-5 h-5 text-green-500 flex-shrink-0" />
|
||||
<span className="text-sm text-gray-600">
|
||||
{plan.user_limit === -1 ? 'Unlimited' : plan.user_limit} team members
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<CheckIcon className="w-5 h-5 text-green-500 flex-shrink-0" />
|
||||
<span className="text-sm text-gray-600">
|
||||
{plan.features?.execution_limit === -1 ? 'Unlimited' : plan.features?.execution_limit} executions/month
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
{plan.features?.api_access ? (
|
||||
<CheckIcon className="w-5 h-5 text-green-500 flex-shrink-0" />
|
||||
) : (
|
||||
<XMarkIcon className="w-5 h-5 text-gray-400 flex-shrink-0" />
|
||||
)}
|
||||
<span className="text-sm text-gray-600">API Access</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
{plan.features?.telegram_notifications ? (
|
||||
<CheckIcon className="w-5 h-5 text-green-500 flex-shrink-0" />
|
||||
) : (
|
||||
<XMarkIcon className="w-5 h-5 text-gray-400 flex-shrink-0" />
|
||||
)}
|
||||
<span className="text-sm text-gray-600">Telegram Notifications</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
{plan.features?.premium_templates ? (
|
||||
<CheckIcon className="w-5 h-5 text-green-500 flex-shrink-0" />
|
||||
) : (
|
||||
<XMarkIcon className="w-5 h-5 text-gray-400 flex-shrink-0" />
|
||||
)}
|
||||
<span className="text-sm text-gray-600">Premium Templates</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isCurrentPlan && (
|
||||
<button
|
||||
onClick={() => plan.name.toLowerCase() === 'enterprise' ?
|
||||
window.open('mailto:info@ai-impress.com?subject=Enterprise Plan Inquiry') :
|
||||
upgradePlan(plan.name.toLowerCase())
|
||||
}
|
||||
disabled={upgrading === plan.name.toLowerCase()}
|
||||
className={`w-full py-3 px-4 rounded-xl font-semibold transition-all transform hover:scale-105 ${
|
||||
plan.name.toLowerCase() === 'free'
|
||||
? 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
: 'gradient-primary text-white hover:shadow-lg'
|
||||
} ${upgrading === plan.name.toLowerCase() ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
{upgrading === plan.name.toLowerCase() ? (
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
<span>Processing...</span>
|
||||
</div>
|
||||
) : plan.name.toLowerCase() === 'enterprise' ? (
|
||||
'Contact Sales'
|
||||
) : plan.name.toLowerCase() === 'free' ? (
|
||||
'Downgrade'
|
||||
) : (
|
||||
'Upgrade'
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 p-4 bg-blue-50 rounded-xl border border-blue-200">
|
||||
<div className="flex items-start space-x-3">
|
||||
<InformationCircleIcon className="w-6 h-6 text-blue-500 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-semibold text-blue-900 mb-1">Free Trial Information</h4>
|
||||
<p className="text-blue-800 text-sm">
|
||||
All new accounts start with a 7-day free trial of Pro features. No credit card required.
|
||||
You can upgrade or downgrade at any time. Enterprise customers get priority support and custom integrations.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
200
frontend/src/app/chat/page.tsx
Normal file
200
frontend/src/app/chat/page.tsx
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import api from '@/lib/api';
|
||||
import { ChatMessage } from '@/types';
|
||||
import { PaperAirplaneIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
export default function ChatPage() {
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
const [inputMessage, setInputMessage] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [sessionId, setSessionId] = useState<string>('');
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchChatHistory();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const fetchChatHistory = async () => {
|
||||
try {
|
||||
const response = await api.get('/chat/history');
|
||||
setMessages(response.data.messages || []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching chat history:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const sendMessage = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!inputMessage.trim() || loading) return;
|
||||
|
||||
const userMessage = inputMessage.trim();
|
||||
setInputMessage('');
|
||||
setLoading(true);
|
||||
|
||||
// Add user message immediately
|
||||
const newUserMessage: ChatMessage = {
|
||||
message_type: 'user',
|
||||
content: userMessage,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
setMessages(prev => [...prev, newUserMessage]);
|
||||
|
||||
try {
|
||||
const response = await api.post('/chat', {
|
||||
message: userMessage,
|
||||
session_id: sessionId,
|
||||
});
|
||||
|
||||
const { reply, session_id: returnedSessionId } = response.data;
|
||||
|
||||
if (returnedSessionId && !sessionId) {
|
||||
setSessionId(returnedSessionId);
|
||||
}
|
||||
|
||||
// Add assistant response
|
||||
const assistantMessage: ChatMessage = {
|
||||
message_type: 'assistant',
|
||||
content: reply,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
setMessages(prev => [...prev, assistantMessage]);
|
||||
} catch (error: any) {
|
||||
// Add error message
|
||||
const errorMessage: ChatMessage = {
|
||||
message_type: 'assistant',
|
||||
content: 'Sorry, I encountered an error. Please try again.',
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
setMessages(prev => [...prev, errorMessage]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const suggestedQuestions = [
|
||||
'How do I connect Instagram?',
|
||||
'How to create a webhook?',
|
||||
'My workflow is not working',
|
||||
'How do I view execution logs?',
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen bg-gray-50">
|
||||
{/* Header */}
|
||||
<div className="bg-white shadow-sm border-b p-4">
|
||||
<h1 className="text-xl font-semibold text-gray-900">AI Assistant</h1>
|
||||
<p className="text-sm text-gray-600">
|
||||
Ask me anything about setting up and managing your automations.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<div className="max-w-3xl mx-auto space-y-4">
|
||||
{messages.length === 0 && (
|
||||
<div className="text-center py-8">
|
||||
<div className="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<span className="text-2xl">🤖</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
Hi! I'm your automation assistant
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-6">
|
||||
I can help you with setting up integrations, creating workflows, troubleshooting issues, and more.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{suggestedQuestions.map((question, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => setInputMessage(question)}
|
||||
className="p-3 text-left text-sm border border-gray-200 rounded-lg hover:border-blue-300 hover:bg-blue-50"
|
||||
>
|
||||
{question}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{messages.map((message, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex ${
|
||||
message.message_type === 'user' ? 'justify-end' : 'justify-start'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-xs lg:max-w-md px-4 py-2 rounded-lg ${
|
||||
message.message_type === 'user'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-white text-gray-900 shadow-sm border'
|
||||
}`}
|
||||
>
|
||||
<p className="text-sm">{message.content}</p>
|
||||
<p
|
||||
className={`text-xs mt-1 ${
|
||||
message.message_type === 'user'
|
||||
? 'text-blue-100'
|
||||
: 'text-gray-500'
|
||||
}`}
|
||||
>
|
||||
{new Date(message.created_at).toLocaleTimeString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{loading && (
|
||||
<div className="flex justify-start">
|
||||
<div className="bg-white text-gray-900 shadow-sm border max-w-xs lg:max-w-md px-4 py-2 rounded-lg">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex space-x-1">
|
||||
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
|
||||
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
|
||||
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500">AI is typing...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div className="bg-white border-t p-4">
|
||||
<form onSubmit={sendMessage} className="max-w-3xl mx-auto">
|
||||
<div className="flex space-x-4">
|
||||
<input
|
||||
type="text"
|
||||
value={inputMessage}
|
||||
onChange={(e) => setInputMessage(e.target.value)}
|
||||
placeholder="Type your question here..."
|
||||
disabled={loading}
|
||||
className="flex-1 border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!inputMessage.trim() || loading}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<PaperAirplaneIcon className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
317
frontend/src/app/dashboard/page.tsx
Normal file
317
frontend/src/app/dashboard/page.tsx
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import api from '@/lib/api';
|
||||
import { Workflow } from '@/types';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
PlusIcon,
|
||||
PlayIcon,
|
||||
PauseIcon,
|
||||
CheckCircleIcon,
|
||||
XCircleIcon,
|
||||
ClockIcon,
|
||||
SparklesIcon,
|
||||
BoltIcon,
|
||||
ChartBarIcon,
|
||||
CogIcon as CogIconHeroicon,
|
||||
ArrowTrendingUpIcon,
|
||||
EyeIcon,
|
||||
PencilIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { user } = useAuth();
|
||||
const [workflows, setWorkflows] = useState<Workflow[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchWorkflows();
|
||||
}, []);
|
||||
|
||||
const fetchWorkflows = async () => {
|
||||
try {
|
||||
const response = await api.get('/workflows');
|
||||
setWorkflows(response.data.workflows || []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching workflows:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return <CheckCircleIcon className="w-5 h-5 text-green-500" />;
|
||||
case 'inactive':
|
||||
return <PauseIcon className="w-5 h-5 text-gray-400" />;
|
||||
case 'error':
|
||||
return <XCircleIcon className="w-5 h-5 text-red-500" />;
|
||||
default:
|
||||
return <ClockIcon className="w-5 h-5 text-yellow-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return 'text-green-700 bg-green-100 border-green-200';
|
||||
case 'inactive':
|
||||
return 'text-gray-700 bg-gray-100 border-gray-200';
|
||||
case 'error':
|
||||
return 'text-red-700 bg-red-100 border-red-200';
|
||||
default:
|
||||
return 'text-yellow-700 bg-yellow-100 border-yellow-200';
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64 bg-gradient-to-br from-orange-50 to-purple-50">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-orange-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-orange-50 via-white to-purple-50 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold gradient-text mb-2">
|
||||
Welcome back, {user?.first_name || user?.email?.split('@')[0]}! 👋
|
||||
</h1>
|
||||
<p className="text-gray-600 text-lg">
|
||||
Here's what's happening with your automations today
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/workflows/new"
|
||||
className="group gradient-primary text-white px-6 py-3 rounded-xl font-semibold transition-all transform hover:scale-105 hover:shadow-lg flex items-center"
|
||||
>
|
||||
<PlusIcon className="w-5 h-5 mr-2" />
|
||||
Create Workflow
|
||||
<SparklesIcon className="w-4 h-4 ml-2 group-hover:rotate-12 transition-transform" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<div className="bg-white p-6 rounded-2xl shadow-lg border border-gray-100 hover:shadow-xl transition-all">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Total Workflows</p>
|
||||
<p className="text-3xl font-bold text-gray-900 mt-1">{workflows.length}</p>
|
||||
<p className="text-xs text-green-600 mt-1">+2 this week</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gradient-to-br from-blue-100 to-blue-200 rounded-xl">
|
||||
<CogIconHeroicon className="w-7 h-7 text-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-2xl shadow-lg border border-gray-100 hover:shadow-xl transition-all">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Active Workflows</p>
|
||||
<p className="text-3xl font-bold text-green-600 mt-1">
|
||||
{workflows.filter(w => w.is_active).length}
|
||||
</p>
|
||||
<p className="text-xs text-green-600 mt-1">Running smoothly</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gradient-to-br from-green-100 to-green-200 rounded-xl">
|
||||
<PlayIcon className="w-7 h-7 text-green-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-2xl shadow-lg border border-gray-100 hover:shadow-xl transition-all">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Monthly Executions</p>
|
||||
<p className="text-3xl font-bold text-purple-600 mt-1">1,247</p>
|
||||
<p className="text-xs text-purple-600 mt-1">↗ +18% vs last month</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gradient-to-br from-purple-100 to-purple-200 rounded-xl">
|
||||
<ArrowTrendingUpIcon className="w-7 h-7 text-purple-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-2xl shadow-lg border border-gray-100 hover:shadow-xl transition-all">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Current Plan</p>
|
||||
<p className="text-3xl font-bold gradient-text mt-1 capitalize">
|
||||
{user?.subscription_plan}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">Unlimited workflows</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gradient-to-br from-orange-100 to-orange-200 rounded-xl">
|
||||
<ChartBarIcon className="w-7 h-7 text-orange-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
||||
<Link href="/marketplace" className="group bg-white p-6 rounded-2xl shadow-lg border border-gray-100 hover:shadow-xl transition-all hover:border-orange-200">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="p-3 bg-gradient-to-br from-orange-100 to-orange-200 rounded-xl group-hover:scale-110 transition-transform">
|
||||
<SparklesIcon className="w-6 h-6 text-orange-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 group-hover:text-orange-600 transition-colors">
|
||||
Browse Templates
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600">
|
||||
Ready-made automation templates
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="/integrations" className="group bg-white p-6 rounded-2xl shadow-lg border border-gray-100 hover:shadow-xl transition-all hover:border-purple-200">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="p-3 bg-gradient-to-br from-purple-100 to-purple-200 rounded-xl group-hover:scale-110 transition-transform">
|
||||
<BoltIcon className="w-6 h-6 text-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 group-hover:text-purple-600 transition-colors">
|
||||
Connect Apps
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600">
|
||||
Integrate with your favorite tools
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="/chat" className="group bg-white p-6 rounded-2xl shadow-lg border border-gray-100 hover:shadow-xl transition-all hover:border-blue-200">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="p-3 bg-gradient-to-br from-blue-100 to-blue-200 rounded-xl group-hover:scale-110 transition-transform">
|
||||
<SparklesIcon className="w-6 h-6 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 group-hover:text-blue-600 transition-colors">
|
||||
AI Assistant
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600">
|
||||
Get help building workflows
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Workflows Section */}
|
||||
<div className="bg-white rounded-2xl shadow-lg border border-gray-100">
|
||||
<div className="px-8 py-6 border-b border-gray-100">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">Your Workflows</h2>
|
||||
<p className="text-gray-600 mt-1">Manage and monitor your automations</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<button className="px-4 py-2 text-gray-600 hover:text-gray-900 transition-colors">
|
||||
All ({workflows.length})
|
||||
</button>
|
||||
<button className="px-4 py-2 text-gray-600 hover:text-gray-900 transition-colors">
|
||||
Active ({workflows.filter(w => w.is_active).length})
|
||||
</button>
|
||||
<button className="px-4 py-2 text-gray-600 hover:text-gray-900 transition-colors">
|
||||
Inactive
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-8">
|
||||
{workflows.length === 0 ? (
|
||||
<div className="text-center py-16">
|
||||
<div className="w-24 h-24 bg-gradient-to-br from-orange-100 to-purple-100 rounded-3xl flex items-center justify-center mx-auto mb-6">
|
||||
<CogIconHeroicon className="w-12 h-12 text-gray-400" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-3">No workflows yet</h3>
|
||||
<p className="text-gray-600 mb-8 max-w-md mx-auto">
|
||||
Get started by creating your first automation workflow. Choose from templates or build from scratch.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Link
|
||||
href="/marketplace"
|
||||
className="group bg-gray-50 border-2 border-gray-200 hover:border-orange-300 text-gray-700 px-6 py-3 rounded-xl font-semibold transition-all hover:shadow-lg flex items-center justify-center"
|
||||
>
|
||||
<SparklesIcon className="w-5 h-5 mr-2" />
|
||||
Browse Templates
|
||||
</Link>
|
||||
<Link
|
||||
href="/workflows/new"
|
||||
className="group gradient-primary text-white px-6 py-3 rounded-xl font-semibold transition-all transform hover:scale-105 hover:shadow-lg flex items-center justify-center"
|
||||
>
|
||||
<PlusIcon className="w-5 h-5 mr-2" />
|
||||
Create From Scratch
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{workflows.map((workflow) => (
|
||||
<div key={workflow.id} className="group bg-gray-50 hover:bg-white border border-gray-200 hover:border-orange-200 p-6 rounded-xl transition-all hover:shadow-lg">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex-shrink-0">
|
||||
{getStatusIcon(workflow.status)}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 group-hover:text-orange-600 transition-colors">
|
||||
{workflow.name}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
{workflow.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium border ${getStatusColor(
|
||||
workflow.status
|
||||
)}`}
|
||||
>
|
||||
{workflow.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mt-4">
|
||||
<div className="flex items-center space-x-4 text-sm text-gray-500">
|
||||
<span className="bg-blue-100 text-blue-700 px-2 py-1 rounded-lg">
|
||||
{workflow.trigger_type}
|
||||
</span>
|
||||
<span>Last run: 2h ago</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
|
||||
<EyeIcon className="w-4 h-4" />
|
||||
</button>
|
||||
<Link
|
||||
href={`/workflows/${workflow.id}`}
|
||||
className="p-2 text-orange-500 hover:text-orange-600 hover:bg-orange-50 rounded-lg transition-colors"
|
||||
>
|
||||
<PencilIcon className="w-4 h-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
BIN
frontend/src/app/favicon.ico
Normal file
BIN
frontend/src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
128
frontend/src/app/globals.css
Normal file
128
frontend/src/app/globals.css
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
/* n8n.io inspired color palette */
|
||||
--background: #f8f9fa;
|
||||
--foreground: #2f3349;
|
||||
--card: #ffffff;
|
||||
--card-foreground: #2f3349;
|
||||
--primary: #ff6d5a;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #5865f2;
|
||||
--secondary-foreground: #ffffff;
|
||||
--accent: #00d4aa;
|
||||
--accent-foreground: #ffffff;
|
||||
--muted: #f1f3f5;
|
||||
--muted-foreground: #6c757d;
|
||||
--border: #e5e9f0;
|
||||
--input: #ffffff;
|
||||
--ring: #ff6d5a;
|
||||
|
||||
/* Success, warning, error colors in n8n style */
|
||||
--success: #00d4aa;
|
||||
--warning: #ffb800;
|
||||
--error: #ff4757;
|
||||
|
||||
/* Gradient colors for modern look */
|
||||
--gradient-start: #ff6d5a;
|
||||
--gradient-end: #5865f2;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-success: var(--success);
|
||||
--color-warning: var(--warning);
|
||||
--color-error: var(--error);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #1e1e2e;
|
||||
--foreground: #cdd6f4;
|
||||
--card: #282a36;
|
||||
--card-foreground: #cdd6f4;
|
||||
--primary: #ff6d5a;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #5865f2;
|
||||
--secondary-foreground: #ffffff;
|
||||
--accent: #00d4aa;
|
||||
--accent-foreground: #ffffff;
|
||||
--muted: #383a59;
|
||||
--muted-foreground: #9399b2;
|
||||
--border: #44475a;
|
||||
--input: #282a36;
|
||||
--ring: #ff6d5a;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-sans), -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Custom gradients and animations */
|
||||
.gradient-primary {
|
||||
background: linear-gradient(135deg, var(--gradient-start) 0%, var(--gradient-end) 100%);
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, var(--gradient-start) 0%, var(--gradient-end) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.animate-float {
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
}
|
||||
|
||||
.glass-effect {
|
||||
backdrop-filter: blur(10px);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--muted);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--primary);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--secondary);
|
||||
}
|
||||
334
frontend/src/app/integrations/page.tsx
Normal file
334
frontend/src/app/integrations/page.tsx
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import api from '@/lib/api';
|
||||
import { Integration } from '@/types';
|
||||
import {
|
||||
LinkIcon,
|
||||
CheckCircleIcon,
|
||||
XCircleIcon,
|
||||
SparklesIcon,
|
||||
BoltIcon,
|
||||
ChatBubbleLeftIcon,
|
||||
EnvelopeIcon,
|
||||
UserGroupIcon,
|
||||
BuildingOfficeIcon,
|
||||
GlobeAltIcon,
|
||||
CameraIcon,
|
||||
ShoppingBagIcon,
|
||||
CalendarIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
|
||||
const availableProviders = [
|
||||
{
|
||||
id: 'gmail',
|
||||
name: 'Gmail',
|
||||
description: 'Access your Gmail inbox, send emails, and automate email workflows',
|
||||
icon: EnvelopeIcon,
|
||||
color: 'from-red-500 to-red-600',
|
||||
category: 'Email'
|
||||
},
|
||||
{
|
||||
id: 'slack',
|
||||
name: 'Slack',
|
||||
description: 'Send messages, notifications, and manage team communications',
|
||||
icon: ChatBubbleLeftIcon,
|
||||
color: 'from-purple-500 to-purple-600',
|
||||
category: 'Communication'
|
||||
},
|
||||
{
|
||||
id: 'telegram',
|
||||
name: 'Telegram',
|
||||
description: 'Send messages to channels and groups, manage bots',
|
||||
icon: ChatBubbleLeftIcon,
|
||||
color: 'from-blue-500 to-blue-600',
|
||||
category: 'Communication'
|
||||
},
|
||||
{
|
||||
id: 'teams',
|
||||
name: 'Microsoft Teams',
|
||||
description: 'Collaborate with your team and automate meeting workflows',
|
||||
icon: UserGroupIcon,
|
||||
color: 'from-blue-600 to-indigo-600',
|
||||
category: 'Communication'
|
||||
},
|
||||
{
|
||||
id: 'instagram',
|
||||
name: 'Instagram',
|
||||
description: 'Manage your Instagram business account and automate posts',
|
||||
icon: CameraIcon,
|
||||
color: 'from-pink-500 to-purple-600',
|
||||
category: 'Social Media'
|
||||
},
|
||||
{
|
||||
id: 'linkedin',
|
||||
name: 'LinkedIn',
|
||||
description: 'Manage your LinkedIn connections, posts, and professional network',
|
||||
icon: UserGroupIcon,
|
||||
color: 'from-blue-700 to-blue-800',
|
||||
category: 'Social Media'
|
||||
},
|
||||
{
|
||||
id: 'shopify',
|
||||
name: 'Shopify',
|
||||
description: 'Automate your e-commerce store operations and order processing',
|
||||
icon: ShoppingBagIcon,
|
||||
color: 'from-green-500 to-green-600',
|
||||
category: 'E-commerce'
|
||||
},
|
||||
{
|
||||
id: 'zoho',
|
||||
name: 'Zoho CRM',
|
||||
description: 'Sync contacts, deals, and automate your customer relationship workflows',
|
||||
icon: BuildingOfficeIcon,
|
||||
color: 'from-orange-500 to-red-600',
|
||||
category: 'CRM'
|
||||
},
|
||||
{
|
||||
id: 'calendly',
|
||||
name: 'Calendly',
|
||||
description: 'Automate scheduling and meeting management workflows',
|
||||
icon: CalendarIcon,
|
||||
color: 'from-blue-500 to-cyan-600',
|
||||
category: 'Productivity'
|
||||
},
|
||||
{
|
||||
id: 'webhook',
|
||||
name: 'Webhooks',
|
||||
description: 'Connect any service with custom HTTP webhooks and API calls',
|
||||
icon: GlobeAltIcon,
|
||||
color: 'from-gray-500 to-gray-600',
|
||||
category: 'Developer'
|
||||
}
|
||||
];
|
||||
|
||||
export default function IntegrationsPage() {
|
||||
const [integrations, setIntegrations] = useState<Integration[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [connecting, setConnecting] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchIntegrations();
|
||||
}, []);
|
||||
|
||||
const fetchIntegrations = async () => {
|
||||
try {
|
||||
const response = await api.get('/integrations');
|
||||
setIntegrations(response.data.integrations || []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching integrations:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const connectProvider = async (providerId: string) => {
|
||||
setConnecting(providerId);
|
||||
try {
|
||||
// Mock OAuth flow - in real implementation, this would redirect to OAuth provider
|
||||
const mockToken = 'mock_access_token_' + Math.random().toString(36).substring(7);
|
||||
|
||||
await api.post(`/integrations/${providerId}/connect`, {
|
||||
access_token: mockToken,
|
||||
account_name: `Mock ${providerId} Account`,
|
||||
account_email: `user@${providerId}.com`,
|
||||
scopes: ['read', 'write']
|
||||
});
|
||||
|
||||
await fetchIntegrations();
|
||||
} catch (error: any) {
|
||||
alert(error.response?.data?.message || 'Failed to connect integration');
|
||||
} finally {
|
||||
setConnecting(null);
|
||||
}
|
||||
};
|
||||
|
||||
const disconnectIntegration = async (integrationId: number) => {
|
||||
if (!confirm('Are you sure you want to disconnect this integration?')) return;
|
||||
|
||||
try {
|
||||
await api.delete(`/integrations/${integrationId}`);
|
||||
await fetchIntegrations();
|
||||
} catch (error: any) {
|
||||
alert(error.response?.data?.message || 'Failed to disconnect integration');
|
||||
}
|
||||
};
|
||||
|
||||
const getConnectedIntegration = (providerId: string) => {
|
||||
return integrations.find(int => int.provider === providerId);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64 bg-gradient-to-br from-orange-50 to-purple-50">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-orange-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const categories = [...new Set(availableProviders.map(p => p.category))];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-orange-50 via-white to-purple-50 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl font-bold gradient-text mb-2">Integrations</h1>
|
||||
<p className="text-gray-600 text-lg">
|
||||
Connect your favorite apps and services to create powerful automation workflows.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Categories Overview */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-4 mb-8">
|
||||
{categories.map((category) => {
|
||||
const categoryCount = availableProviders.filter(p => p.category === category).length;
|
||||
return (
|
||||
<div key={category} className="bg-white rounded-xl p-4 shadow-sm border border-gray-100 text-center hover:shadow-md transition-shadow">
|
||||
<div className="w-8 h-8 gradient-primary rounded-lg flex items-center justify-center mx-auto mb-2">
|
||||
<SparklesIcon className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<h3 className="font-semibold text-gray-900 text-sm">{category}</h3>
|
||||
<p className="text-xs text-gray-500">{categoryCount} apps</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Integrations Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{availableProviders.map((provider) => {
|
||||
const connectedIntegration = getConnectedIntegration(provider.id);
|
||||
const isConnected = !!connectedIntegration;
|
||||
const IconComponent = provider.icon;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={provider.id}
|
||||
className={`bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden transition-all hover:shadow-xl transform hover:scale-105 ${
|
||||
isConnected ? 'ring-2 ring-green-200' : ''
|
||||
}`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-6 pb-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`w-12 h-12 bg-gradient-to-br ${provider.color} rounded-xl flex items-center justify-center shadow-lg`}>
|
||||
<IconComponent className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
{provider.name}
|
||||
</h3>
|
||||
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-700">
|
||||
{provider.category}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center space-y-1">
|
||||
{isConnected ? (
|
||||
<>
|
||||
<CheckCircleIcon className="w-6 h-6 text-green-500" />
|
||||
<span className="text-xs text-green-600 font-medium">Connected</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<XCircleIcon className="w-6 h-6 text-gray-400" />
|
||||
<span className="text-xs text-gray-500">Available</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-600 text-sm mb-4">
|
||||
{provider.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-6 pt-0">
|
||||
{isConnected ? (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-green-50 rounded-xl p-4 border border-green-200">
|
||||
<div className="text-sm text-green-800">
|
||||
<p className="font-semibold flex items-center">
|
||||
<CheckCircleIcon className="w-4 h-4 mr-2" />
|
||||
Connected as:
|
||||
</p>
|
||||
<p className="mt-1">{connectedIntegration.account_name}</p>
|
||||
<p className="text-xs text-green-600 mt-1">
|
||||
Connected on {new Date(connectedIntegration.created_at).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => disconnectIntegration(connectedIntegration.id)}
|
||||
className="w-full px-4 py-3 border-2 border-red-300 text-red-700 rounded-xl hover:bg-red-50 text-sm font-semibold transition-all transform hover:scale-105"
|
||||
>
|
||||
Disconnect Integration
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => connectProvider(provider.id)}
|
||||
disabled={connecting === provider.id}
|
||||
className={`w-full px-4 py-3 bg-gradient-to-r ${provider.color} text-white rounded-xl font-semibold transition-all transform hover:scale-105 hover:shadow-lg disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none`}
|
||||
>
|
||||
{connecting === provider.id ? (
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
<span>Connecting...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<BoltIcon className="w-5 h-5" />
|
||||
<span>Connect {provider.name}</span>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Info Section */}
|
||||
<div className="mt-12 bg-blue-50 rounded-2xl border border-blue-200 p-8">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-blue-500 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<SparklesIcon className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-blue-900 mb-4">Ready to Automate?</h3>
|
||||
<p className="text-blue-800 text-lg mb-6 max-w-2xl mx-auto">
|
||||
Connect your apps and start building powerful automation workflows in minutes.
|
||||
No coding required – just point, click, and automate!
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-8">
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center mx-auto mb-3">
|
||||
<BoltIcon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-blue-900 mb-2">Easy Setup</h4>
|
||||
<p className="text-sm text-blue-700">Connect in seconds with OAuth authentication</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center mx-auto mb-3">
|
||||
<CheckCircleIcon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-blue-900 mb-2">Secure</h4>
|
||||
<p className="text-sm text-blue-700">Enterprise-grade security for all connections</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center mx-auto mb-3">
|
||||
<SparklesIcon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-blue-900 mb-2">Powerful</h4>
|
||||
<p className="text-sm text-blue-700">Create complex workflows with simple drag & drop</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
38
frontend/src/app/layout.tsx
Normal file
38
frontend/src/app/layout.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import ClientProvider from "@/components/ClientProvider";
|
||||
import Layout from "@/components/Layout";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Aimpress AutomationHub - SaaS Automation Platform",
|
||||
description: "Build and manage your automation workflows with ease",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<ClientProvider>
|
||||
<Layout>{children}</Layout>
|
||||
</ClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
206
frontend/src/app/login/page.tsx
Normal file
206
frontend/src/app/login/page.tsx
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { EyeIcon, EyeSlashIcon, BoltIcon, ArrowRightIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
export default function LoginPage() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const { login } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
await login(email, password);
|
||||
router.push('/dashboard');
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-orange-50 via-white to-purple-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full">
|
||||
{/* Logo and Back to Home */}
|
||||
<div className="text-center mb-8">
|
||||
<Link href="/" className="inline-flex items-center text-gray-600 hover:text-gray-800 transition-colors mb-6">
|
||||
<ArrowRightIcon className="w-4 h-4 mr-2 rotate-180" />
|
||||
Back to Home
|
||||
</Link>
|
||||
<div className="flex items-center justify-center space-x-2 mb-4">
|
||||
<div className="w-10 h-10 gradient-primary rounded-xl flex items-center justify-center">
|
||||
<BoltIcon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<span className="text-2xl font-bold gradient-text">AutomationHub</span>
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-2">
|
||||
Welcome back
|
||||
</h2>
|
||||
<p className="text-gray-600">
|
||||
Sign in to continue building amazing automations
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Login Form */}
|
||||
<div className="bg-white shadow-xl rounded-2xl p-8 border border-gray-100">
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-xl text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent transition-all text-gray-900 placeholder-gray-500"
|
||||
placeholder="Enter your email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
id="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
autoComplete="current-password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full px-4 py-3 pr-12 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent transition-all text-gray-900 placeholder-gray-500"
|
||||
placeholder="Enter your password"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeSlashIcon className="w-5 h-5" />
|
||||
) : (
|
||||
<EyeIcon className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="remember-me"
|
||||
name="remember-me"
|
||||
type="checkbox"
|
||||
className="h-4 w-4 text-orange-500 focus:ring-orange-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label htmlFor="remember-me" className="ml-2 block text-sm text-gray-700">
|
||||
Remember me
|
||||
</label>
|
||||
</div>
|
||||
<Link
|
||||
href="/reset-password"
|
||||
className="text-sm text-orange-600 hover:text-orange-500 font-medium"
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full gradient-primary text-white py-3 px-4 rounded-xl font-semibold hover:shadow-lg disabled:opacity-50 disabled:cursor-not-allowed transition-all transform hover:scale-[1.02] flex items-center justify-center"
|
||||
>
|
||||
{loading ? (
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
|
||||
) : (
|
||||
<>
|
||||
Sign in
|
||||
<ArrowRightIcon className="w-4 h-4 ml-2" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-gray-200" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-2 bg-white text-gray-500">Or continue with</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
className="w-full inline-flex justify-center py-3 px-4 border border-gray-200 rounded-xl shadow-sm bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24">
|
||||
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
||||
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
||||
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
||||
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
||||
</svg>
|
||||
<span className="ml-2">Google</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="w-full inline-flex justify-center py-3 px-4 border border-gray-200 rounded-xl shadow-sm bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<span className="ml-2">GitHub</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-gray-600">
|
||||
Don't have an account?{' '}
|
||||
<Link
|
||||
href="/signup"
|
||||
className="font-medium text-orange-600 hover:text-orange-500 transition-colors"
|
||||
>
|
||||
Sign up for free
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Demo Credentials */}
|
||||
<div className="mt-6 bg-blue-50 border border-blue-200 rounded-xl p-4">
|
||||
<h4 className="text-sm font-medium text-blue-800 mb-2">Demo Credentials</h4>
|
||||
<p className="text-xs text-blue-600">
|
||||
Email: demo@example.com<br />
|
||||
Password: password123
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
442
frontend/src/app/marketplace/page.tsx
Normal file
442
frontend/src/app/marketplace/page.tsx
Normal file
|
|
@ -0,0 +1,442 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import api from '@/lib/api';
|
||||
import {
|
||||
MagnifyingGlassIcon,
|
||||
SparklesIcon,
|
||||
TagIcon,
|
||||
UserIcon,
|
||||
ClockIcon,
|
||||
ArrowDownIcon,
|
||||
CheckCircleIcon,
|
||||
ShieldCheckIcon,
|
||||
CogIcon,
|
||||
BoltIcon,
|
||||
CloudIcon,
|
||||
LockClosedIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
|
||||
interface Template {
|
||||
id: string | number;
|
||||
name: string;
|
||||
description: string;
|
||||
category: string;
|
||||
trigger_type?: string;
|
||||
tags: string[];
|
||||
is_featured?: boolean;
|
||||
install_count?: number;
|
||||
useCount?: number;
|
||||
rating?: number;
|
||||
author?: string;
|
||||
icon?: string;
|
||||
gradient?: string;
|
||||
complexity?: string;
|
||||
estimatedTime?: string;
|
||||
created_at?: string;
|
||||
createdAt?: string;
|
||||
source?: 'local' | 'n8n';
|
||||
n8n_workflow_id?: string;
|
||||
n8n_data?: any;
|
||||
workflow?: any;
|
||||
}
|
||||
|
||||
export default function MarketplacePage() {
|
||||
const { user } = useAuth();
|
||||
const [templates, setTemplates] = useState<Template[]>([]);
|
||||
const [filteredTemplates, setFilteredTemplates] = useState<Template[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState('all');
|
||||
const [installing, setInstalling] = useState<string | null>(null);
|
||||
|
||||
const categories = [
|
||||
{ value: 'all', label: 'All Categories', icon: SparklesIcon },
|
||||
{ value: 'Social Media', label: 'Social Media', icon: UserIcon },
|
||||
{ value: 'Productivity', label: 'Productivity', icon: ClockIcon },
|
||||
{ value: 'Communication', label: 'Communication', icon: BoltIcon },
|
||||
{ value: 'n8n', label: 'N8n Workflows', icon: CloudIcon },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
fetchTemplates();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
filterTemplates();
|
||||
}, [templates, searchTerm, selectedCategory]);
|
||||
|
||||
const fetchTemplates = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.get('/templates');
|
||||
setTemplates(response.data.templates || []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching templates:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const filterTemplates = () => {
|
||||
let filtered = templates;
|
||||
|
||||
if (searchTerm) {
|
||||
filtered = filtered.filter(template =>
|
||||
template.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
template.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
template.tags?.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedCategory !== 'all') {
|
||||
filtered = filtered.filter(template => template.category === selectedCategory);
|
||||
}
|
||||
|
||||
setFilteredTemplates(filtered);
|
||||
};
|
||||
|
||||
const installTemplate = async (templateId: string, templateName: string) => {
|
||||
try {
|
||||
setInstalling(templateId);
|
||||
const response = await api.post(`/templates/${templateId}/install`, {
|
||||
workflow_name: templateName
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
alert(`${templateName} installed successfully!`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error installing template:', error);
|
||||
if (error.response?.status === 403) {
|
||||
alert('Workflow limit reached. Please upgrade your plan to install more templates.');
|
||||
} else {
|
||||
alert('Failed to install template. Please try again.');
|
||||
}
|
||||
} finally {
|
||||
setInstalling(null);
|
||||
}
|
||||
};
|
||||
|
||||
const getSourceIcon = (template: Template) => {
|
||||
if (template.source === 'n8n') {
|
||||
return <CloudIcon className="w-5 h-5 text-blue-500" />;
|
||||
}
|
||||
return <CogIcon className="w-5 h-5 text-purple-500" />;
|
||||
};
|
||||
|
||||
const getSourceBadge = (template: Template) => {
|
||||
if (template.source === 'n8n') {
|
||||
return (
|
||||
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 border border-blue-200">
|
||||
<CloudIcon className="w-3 h-3 mr-1" />
|
||||
N8n
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-800 border border-purple-200">
|
||||
<SparklesIcon className="w-3 h-3 mr-1" />
|
||||
Local
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const canAccessTemplate = (template: Template) => {
|
||||
// N8n templates are only available to admin users
|
||||
if (template.source === 'n8n') {
|
||||
return user?.role === 'admin';
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64 bg-gradient-to-br from-orange-50 to-purple-50">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-orange-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-orange-50 via-white to-purple-50 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl font-bold gradient-text mb-2">Template Marketplace</h1>
|
||||
<p className="text-gray-600 text-lg">
|
||||
Discover and install automation templates to jumpstart your workflows
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Search and Filters */}
|
||||
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 p-6 mb-8">
|
||||
<div className="flex flex-col lg:flex-row gap-4">
|
||||
{/* Search */}
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
<MagnifyingGlassIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search templates..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category Filter */}
|
||||
<div className="lg:w-64">
|
||||
<select
|
||||
value={selectedCategory}
|
||||
onChange={(e) => setSelectedCategory(e.target.value)}
|
||||
className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent"
|
||||
>
|
||||
{categories.map((category) => (
|
||||
<option key={category.value} value={category.value}>
|
||||
{category.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="flex items-center justify-between mt-4 pt-4 border-t border-gray-100">
|
||||
<p className="text-sm text-gray-600">
|
||||
Showing {filteredTemplates.length} of {templates.length} templates
|
||||
</p>
|
||||
{user?.role === 'admin' && (
|
||||
<div className="flex items-center space-x-2 text-sm text-blue-600">
|
||||
<ShieldCheckIcon className="w-4 h-4" />
|
||||
<span>Admin: N8n templates available</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Templates Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredTemplates.map((template) => {
|
||||
const hasAccess = canAccessTemplate(template);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={template.id}
|
||||
className={`bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden transition-all hover:shadow-xl transform hover:scale-105 ${
|
||||
!hasAccess ? 'opacity-60' : ''
|
||||
}`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-6 pb-0">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`w-12 h-12 bg-gradient-to-br ${template.gradient || 'from-orange-500 to-purple-600'} rounded-xl flex items-center justify-center text-white text-2xl shadow-lg`}>
|
||||
{template.icon || getSourceIcon(template)}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 text-lg">{template.name}</h3>
|
||||
<div className="flex items-center space-x-2 mt-1">
|
||||
{getSourceBadge(template)}
|
||||
{template.is_featured && (
|
||||
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800 border border-yellow-200">
|
||||
<SparklesIcon className="w-3 h-3 mr-1" />
|
||||
Featured
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!hasAccess && (
|
||||
<LockClosedIcon className="w-5 h-5 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-gray-600 text-sm mb-4 line-clamp-3">
|
||||
{template.description}
|
||||
</p>
|
||||
|
||||
{/* Rating and Stats */}
|
||||
<div className="flex items-center space-x-4 mb-4">
|
||||
{template.rating && (
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="flex items-center">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<svg
|
||||
key={i}
|
||||
className={`w-4 h-4 ${
|
||||
i < Math.floor(template.rating!)
|
||||
? 'text-yellow-400'
|
||||
: 'text-gray-200'
|
||||
}`}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||
</svg>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-sm text-gray-600 font-medium">
|
||||
{template.rating.toFixed(1)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{template.complexity && (
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
|
||||
template.complexity === 'beginner'
|
||||
? 'bg-green-100 text-green-800'
|
||||
: template.complexity === 'intermediate'
|
||||
? 'bg-yellow-100 text-yellow-800'
|
||||
: 'bg-red-100 text-red-800'
|
||||
}`}>
|
||||
{template.complexity}
|
||||
</span>
|
||||
)}
|
||||
{template.estimatedTime && (
|
||||
<span className="flex items-center text-xs text-gray-500">
|
||||
<ClockIcon className="w-3 h-3 mr-1" />
|
||||
{template.estimatedTime}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
{template.tags && template.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{template.tags.slice(0, 3).map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="inline-flex items-center px-2 py-1 rounded-lg text-xs font-medium bg-gray-100 text-gray-700"
|
||||
>
|
||||
<TagIcon className="w-3 h-3 mr-1" />
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
{template.tags.length > 3 && (
|
||||
<span className="text-xs text-gray-500">
|
||||
+{template.tags.length - 3} more
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-6 pt-0">
|
||||
<div className="flex items-center justify-between text-sm text-gray-500 mb-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className="flex items-center">
|
||||
<ArrowDownIcon className="w-4 h-4 mr-1" />
|
||||
{template.useCount || template.install_count || 0} uses
|
||||
</span>
|
||||
{(template.created_at || template.createdAt) && (
|
||||
<span className="flex items-center">
|
||||
<ClockIcon className="w-4 h-4 mr-1" />
|
||||
{formatDate(template.created_at || template.createdAt!)}
|
||||
</span>
|
||||
)}
|
||||
{template.author && (
|
||||
<span className="flex items-center">
|
||||
<UserIcon className="w-4 h-4 mr-1" />
|
||||
{template.author}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasAccess ? (
|
||||
<button
|
||||
onClick={() => installTemplate(template.id, template.name)}
|
||||
disabled={installing === template.id}
|
||||
className="w-full gradient-primary text-white py-3 px-4 rounded-xl font-semibold transition-all transform hover:scale-105 hover:shadow-lg disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none"
|
||||
>
|
||||
{installing === template.id ? (
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
<span>Installing...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<CheckCircleIcon className="w-5 h-5" />
|
||||
<span>
|
||||
{template.source === 'n8n' ? 'Connect' : 'Install'} Template
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
) : (
|
||||
<div className="w-full bg-gray-100 border border-gray-200 text-gray-500 py-3 px-4 rounded-xl font-semibold text-center">
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<LockClosedIcon className="w-5 h-5" />
|
||||
<span>Admin Access Required</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Empty State */}
|
||||
{filteredTemplates.length === 0 && (
|
||||
<div className="text-center py-16">
|
||||
<div className="w-24 h-24 bg-gradient-to-br from-orange-100 to-purple-100 rounded-3xl flex items-center justify-center mx-auto mb-6">
|
||||
<MagnifyingGlassIcon className="w-12 h-12 text-gray-400" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-3">No templates found</h3>
|
||||
<p className="text-gray-600 mb-8 max-w-md mx-auto">
|
||||
{searchTerm || selectedCategory !== 'all'
|
||||
? 'Try adjusting your search or filter criteria.'
|
||||
: 'There are no templates available at the moment.'}
|
||||
</p>
|
||||
{(searchTerm || selectedCategory !== 'all') && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setSearchTerm('');
|
||||
setSelectedCategory('all');
|
||||
}}
|
||||
className="gradient-primary text-white px-6 py-3 rounded-xl font-semibold transition-all transform hover:scale-105 hover:shadow-lg"
|
||||
>
|
||||
Clear Filters
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Info Card */}
|
||||
<div className="mt-8 bg-blue-50 rounded-2xl border border-blue-200 p-6">
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||
<SparklesIcon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-blue-900 mb-2">About Templates</h3>
|
||||
<p className="text-blue-800 text-sm mb-3">
|
||||
Templates are pre-built automation workflows that you can install and customize for your needs.
|
||||
They include pre-configured triggers, actions, and integrations to help you get started quickly.
|
||||
</p>
|
||||
{user?.role === 'admin' && (
|
||||
<p className="text-blue-800 text-sm">
|
||||
<strong>Admin Access:</strong> You can access N8n workflows directly from your connected N8n instance.
|
||||
These appear as templates and can be connected to your automation platform.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
250
frontend/src/app/page.tsx
Normal file
250
frontend/src/app/page.tsx
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
SparklesIcon,
|
||||
BoltIcon,
|
||||
ShieldCheckIcon,
|
||||
CpuChipIcon,
|
||||
PlayIcon,
|
||||
CheckIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
|
||||
export default function Home() {
|
||||
const { user, loading } = useAuth();
|
||||
const router = useRouter();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
if (!loading && user) {
|
||||
router.push('/dashboard');
|
||||
}
|
||||
}, [user, loading, router]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-gradient-to-br from-orange-50 to-purple-50">
|
||||
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-orange-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-orange-50 via-white to-purple-50">
|
||||
{/* Navigation */}
|
||||
<nav className="container mx-auto px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-8 h-8 gradient-primary rounded-lg flex items-center justify-center">
|
||||
<BoltIcon className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<span className="text-xl font-bold gradient-text">Aimpress AutomationHub</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link
|
||||
href="/login"
|
||||
className="text-gray-600 hover:text-gray-900 transition-colors"
|
||||
>
|
||||
Sign In
|
||||
</Link>
|
||||
<Link
|
||||
href="/signup"
|
||||
className="bg-orange-500 hover:bg-orange-600 text-white px-4 py-2 rounded-lg transition-colors"
|
||||
>
|
||||
Get Started
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="container mx-auto px-6 py-20 text-center">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h1 className="text-5xl md:text-6xl font-bold mb-6">
|
||||
<span className="gradient-text">Automate Everything</span>
|
||||
<br />
|
||||
<span className="text-gray-800">Without Code</span>
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600 mb-8 max-w-2xl mx-auto">
|
||||
Connect your apps, automate workflows, and boost productivity with our
|
||||
visual automation platform. No coding required.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center mb-12">
|
||||
<Link
|
||||
href="/signup"
|
||||
className="group bg-orange-500 hover:bg-orange-600 text-white px-8 py-4 rounded-lg text-lg font-semibold transition-all transform hover:scale-105 flex items-center"
|
||||
>
|
||||
Start Building Free
|
||||
<ArrowRightIcon className="w-5 h-5 ml-2 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/demo"
|
||||
className="group bg-white border-2 border-gray-200 hover:border-orange-300 text-gray-700 px-8 py-4 rounded-lg text-lg font-semibold transition-all hover:shadow-lg flex items-center"
|
||||
>
|
||||
<PlayIcon className="w-5 h-5 mr-2" />
|
||||
Watch Demo
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Floating Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-16">
|
||||
<div className="animate-float bg-white p-6 rounded-xl shadow-lg border border-gray-100 hover:shadow-xl transition-shadow">
|
||||
<SparklesIcon className="w-8 h-8 text-orange-500 mb-4 mx-auto" />
|
||||
<h3 className="text-lg font-semibold mb-2">Visual Builder</h3>
|
||||
<p className="text-gray-600">Drag and drop to create powerful automations</p>
|
||||
</div>
|
||||
<div className="animate-float bg-white p-6 rounded-xl shadow-lg border border-gray-100 hover:shadow-xl transition-shadow" style={{animationDelay: '2s'}}>
|
||||
<BoltIcon className="w-8 h-8 text-purple-500 mb-4 mx-auto" />
|
||||
<h3 className="text-lg font-semibold mb-2">Lightning Fast</h3>
|
||||
<p className="text-gray-600">Execute workflows in seconds, not hours</p>
|
||||
</div>
|
||||
<div className="animate-float bg-white p-6 rounded-xl shadow-lg border border-gray-100 hover:shadow-xl transition-shadow" style={{animationDelay: '4s'}}>
|
||||
<ShieldCheckIcon className="w-8 h-8 text-green-500 mb-4 mx-auto" />
|
||||
<h3 className="text-lg font-semibold mb-2">Secure & Reliable</h3>
|
||||
<p className="text-gray-600">Enterprise-grade security and uptime</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Features Section */}
|
||||
<section className="bg-white py-20">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-4xl font-bold mb-4">
|
||||
<span className="gradient-text">Everything you need</span> to automate
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600 max-w-2xl mx-auto">
|
||||
From simple tasks to complex workflows, our platform scales with your needs
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{[
|
||||
{
|
||||
icon: CpuChipIcon,
|
||||
title: '500+ Integrations',
|
||||
description: 'Connect with all your favorite apps and services',
|
||||
color: 'text-blue-500',
|
||||
},
|
||||
{
|
||||
icon: BoltIcon,
|
||||
title: 'Real-time Triggers',
|
||||
description: 'Instant responses to events across your stack',
|
||||
color: 'text-orange-500',
|
||||
},
|
||||
{
|
||||
icon: SparklesIcon,
|
||||
title: 'AI-Powered',
|
||||
description: 'Smart suggestions and automated optimizations',
|
||||
color: 'text-purple-500',
|
||||
},
|
||||
{
|
||||
icon: ShieldCheckIcon,
|
||||
title: 'Enterprise Security',
|
||||
description: 'SOC 2 compliant with end-to-end encryption',
|
||||
color: 'text-green-500',
|
||||
},
|
||||
{
|
||||
icon: PlayIcon,
|
||||
title: 'One-Click Deploy',
|
||||
description: 'Deploy automations instantly with zero downtime',
|
||||
color: 'text-red-500',
|
||||
},
|
||||
{
|
||||
icon: CheckIcon,
|
||||
title: '99.9% Uptime',
|
||||
description: 'Reliable execution you can count on',
|
||||
color: 'text-teal-500',
|
||||
},
|
||||
].map((feature, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="p-6 rounded-xl border border-gray-100 hover:border-orange-200 hover:shadow-lg transition-all"
|
||||
>
|
||||
<feature.icon className={`w-8 h-8 ${feature.color} mb-4`} />
|
||||
<h3 className="text-lg font-semibold mb-2">{feature.title}</h3>
|
||||
<p className="text-gray-600">{feature.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="gradient-primary py-20">
|
||||
<div className="container mx-auto px-6 text-center">
|
||||
<h2 className="text-4xl font-bold text-white mb-4">
|
||||
Ready to transform your workflow?
|
||||
</h2>
|
||||
<p className="text-xl text-orange-100 mb-8 max-w-2xl mx-auto">
|
||||
Join thousands of teams already automating their processes
|
||||
</p>
|
||||
<Link
|
||||
href="/signup"
|
||||
className="bg-white text-orange-500 px-8 py-4 rounded-lg text-lg font-semibold hover:shadow-lg transition-all transform hover:scale-105 inline-flex items-center"
|
||||
>
|
||||
Start Your Free Trial
|
||||
<ArrowRightIcon className="w-5 h-5 ml-2" />
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="bg-gray-50 py-12">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2 mb-4">
|
||||
<div className="w-6 h-6 gradient-primary rounded flex items-center justify-center">
|
||||
<BoltIcon className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<span className="font-bold gradient-text">Aimpress AutomationHub</span>
|
||||
</div>
|
||||
<p className="text-gray-600">
|
||||
The visual automation platform for modern teams.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold mb-4">Product</h4>
|
||||
<ul className="space-y-2 text-gray-600">
|
||||
<li><Link href="/features" className="hover:text-orange-500">Features</Link></li>
|
||||
<li><Link href="/integrations" className="hover:text-orange-500">Integrations</Link></li>
|
||||
<li><Link href="/templates" className="hover:text-orange-500">Templates</Link></li>
|
||||
<li><Link href="/pricing" className="hover:text-orange-500">Pricing</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold mb-4">Support</h4>
|
||||
<ul className="space-y-2 text-gray-600">
|
||||
<li><Link href="/docs" className="hover:text-orange-500">Documentation</Link></li>
|
||||
<li><Link href="/help" className="hover:text-orange-500">Help Center</Link></li>
|
||||
<li><Link href="/community" className="hover:text-orange-500">Community</Link></li>
|
||||
<li><Link href="/contact" className="hover:text-orange-500">Contact</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold mb-4">Company</h4>
|
||||
<ul className="space-y-2 text-gray-600">
|
||||
<li><Link href="/about" className="hover:text-orange-500">About</Link></li>
|
||||
<li><Link href="/blog" className="hover:text-orange-500">Blog</Link></li>
|
||||
<li><Link href="/careers" className="hover:text-orange-500">Careers</Link></li>
|
||||
<li><Link href="/privacy" className="hover:text-orange-500">Privacy</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-gray-200 mt-8 pt-8 text-center text-gray-600">
|
||||
<p>© 2025 Aimpress LTD. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
109
frontend/src/app/signup/page.tsx
Normal file
109
frontend/src/app/signup/page.tsx
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function SignupPage() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [firstName, setFirstName] = useState('');
|
||||
const [lastName, setLastName] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const { signup } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
await signup(email, password, firstName, lastName);
|
||||
router.push('/dashboard');
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||
Create your account
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Or{' '}
|
||||
<Link href="/login" className="font-medium text-blue-600 hover:text-blue-500">
|
||||
sign in to your existing account
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-300 text-red-700 px-4 py-3 rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-4">
|
||||
<div className="flex space-x-4">
|
||||
<div className="flex-1">
|
||||
<input
|
||||
type="text"
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
className="block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
placeholder="First name"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<input
|
||||
type="text"
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
className="block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
placeholder="Last name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
placeholder="Email address"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
placeholder="Password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Creating account...' : 'Create account'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
396
frontend/src/app/workflows/page.tsx
Normal file
396
frontend/src/app/workflows/page.tsx
Normal file
|
|
@ -0,0 +1,396 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import api from '@/lib/api';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
PlusIcon,
|
||||
PlayIcon,
|
||||
PauseIcon,
|
||||
CheckCircleIcon,
|
||||
XCircleIcon,
|
||||
ClockIcon,
|
||||
SparklesIcon,
|
||||
BoltIcon,
|
||||
ChartBarIcon,
|
||||
CogIcon,
|
||||
ArrowTrendingUpIcon,
|
||||
EyeIcon,
|
||||
PencilIcon,
|
||||
TrashIcon,
|
||||
ArrowPathIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
|
||||
interface Workflow {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
status: string;
|
||||
trigger_type: string;
|
||||
is_active: boolean;
|
||||
n8n_workflow_id?: string;
|
||||
n8n_data?: any;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface Execution {
|
||||
id: string;
|
||||
finished: boolean;
|
||||
mode: string;
|
||||
startedAt: string;
|
||||
stoppedAt?: string;
|
||||
}
|
||||
|
||||
export default function WorkflowsPage() {
|
||||
const { user } = useAuth();
|
||||
const [workflows, setWorkflows] = useState<Workflow[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedWorkflow, setSelectedWorkflow] = useState<string | null>(null);
|
||||
const [executions, setExecutions] = useState<Execution[]>([]);
|
||||
const [executionsLoading, setExecutionsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchWorkflows();
|
||||
}, []);
|
||||
|
||||
const fetchWorkflows = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.get('/workflows');
|
||||
setWorkflows(response.data.workflows || []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching workflows:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchExecutions = async (workflowId: string) => {
|
||||
try {
|
||||
setExecutionsLoading(true);
|
||||
const response = await api.get(`/workflows/${workflowId}/executions`);
|
||||
setExecutions(response.data.executions || []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching executions:', error);
|
||||
} finally {
|
||||
setExecutionsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const activateWorkflow = async (workflowId: string) => {
|
||||
try {
|
||||
await api.post(`/workflows/${workflowId}/activate`);
|
||||
fetchWorkflows(); // Refresh workflows
|
||||
} catch (error) {
|
||||
console.error('Error activating workflow:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const deactivateWorkflow = async (workflowId: string) => {
|
||||
try {
|
||||
await api.post(`/workflows/${workflowId}/deactivate`);
|
||||
fetchWorkflows(); // Refresh workflows
|
||||
} catch (error) {
|
||||
console.error('Error deactivating workflow:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const executeWorkflow = async (workflowId: string) => {
|
||||
try {
|
||||
await api.post(`/workflows/${workflowId}/execute`);
|
||||
// Refresh executions if this workflow is selected
|
||||
if (selectedWorkflow === workflowId) {
|
||||
fetchExecutions(workflowId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error executing workflow:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteWorkflow = async (workflowId: string) => {
|
||||
if (!confirm('Are you sure you want to delete this workflow?')) return;
|
||||
|
||||
try {
|
||||
await api.delete(`/workflows/${workflowId}`);
|
||||
fetchWorkflows(); // Refresh workflows
|
||||
if (selectedWorkflow === workflowId) {
|
||||
setSelectedWorkflow(null);
|
||||
setExecutions([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting workflow:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (workflow: Workflow) => {
|
||||
if (workflow.n8n_data?.active || workflow.is_active) {
|
||||
return <CheckCircleIcon className="w-5 h-5 text-green-500" />;
|
||||
}
|
||||
return <PauseIcon className="w-5 h-5 text-gray-400" />;
|
||||
};
|
||||
|
||||
const getStatusColor = (workflow: Workflow) => {
|
||||
if (workflow.n8n_data?.active || workflow.is_active) {
|
||||
return 'text-green-700 bg-green-100 border-green-200';
|
||||
}
|
||||
return 'text-gray-700 bg-gray-100 border-gray-200';
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64 bg-gradient-to-br from-orange-50 to-purple-50">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-orange-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-orange-50 via-white to-purple-50 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold gradient-text mb-2">
|
||||
Workflows
|
||||
</h1>
|
||||
<p className="text-gray-600 text-lg">
|
||||
Manage your automation workflows connected to n8n
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<button
|
||||
onClick={fetchWorkflows}
|
||||
className="group bg-white border-2 border-gray-200 hover:border-orange-300 text-gray-700 px-4 py-2 rounded-xl font-semibold transition-all hover:shadow-lg flex items-center"
|
||||
>
|
||||
<ArrowPathIcon className="w-5 h-5 mr-2 group-hover:rotate-180 transition-transform duration-300" />
|
||||
Refresh
|
||||
</button>
|
||||
<Link
|
||||
href="/workflows/new"
|
||||
className="group gradient-primary text-white px-6 py-3 rounded-xl font-semibold transition-all transform hover:scale-105 hover:shadow-lg flex items-center"
|
||||
>
|
||||
<PlusIcon className="w-5 h-5 mr-2" />
|
||||
Create Workflow
|
||||
<SparklesIcon className="w-4 h-4 ml-2 group-hover:rotate-12 transition-transform" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Workflows List */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="bg-white rounded-2xl shadow-lg border border-gray-100">
|
||||
<div className="px-6 py-4 border-b border-gray-100">
|
||||
<h2 className="text-xl font-bold text-gray-900">
|
||||
Your Workflows ({workflows.length})
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
{workflows.length === 0 ? (
|
||||
<div className="text-center py-16">
|
||||
<div className="w-24 h-24 bg-gradient-to-br from-orange-100 to-purple-100 rounded-3xl flex items-center justify-center mx-auto mb-6">
|
||||
<CogIcon className="w-12 h-12 text-gray-400" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-3">No workflows yet</h3>
|
||||
<p className="text-gray-600 mb-8 max-w-md mx-auto">
|
||||
Get started by creating your first automation workflow connected to n8n.
|
||||
</p>
|
||||
<Link
|
||||
href="/workflows/new"
|
||||
className="group gradient-primary text-white px-6 py-3 rounded-xl font-semibold transition-all transform hover:scale-105 hover:shadow-lg inline-flex items-center"
|
||||
>
|
||||
<PlusIcon className="w-5 h-5 mr-2" />
|
||||
Create Your First Workflow
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{workflows.map((workflow) => (
|
||||
<div
|
||||
key={workflow.id}
|
||||
className={`group bg-gray-50 hover:bg-white border border-gray-200 hover:border-orange-200 p-6 rounded-xl transition-all hover:shadow-lg cursor-pointer ${
|
||||
selectedWorkflow === workflow.id ? 'ring-2 ring-orange-500 bg-white' : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
setSelectedWorkflow(workflow.id);
|
||||
fetchExecutions(workflow.id);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex-shrink-0">
|
||||
{getStatusIcon(workflow)}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 group-hover:text-orange-600 transition-colors">
|
||||
{workflow.name}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
{workflow.description}
|
||||
</p>
|
||||
{workflow.n8n_workflow_id && (
|
||||
<p className="text-xs text-blue-600 mt-1">
|
||||
Connected to n8n: {workflow.n8n_workflow_id}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium border ${getStatusColor(
|
||||
workflow
|
||||
)}`}
|
||||
>
|
||||
{workflow.n8n_data?.active || workflow.is_active ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4 text-sm text-gray-500">
|
||||
<span className="bg-blue-100 text-blue-700 px-2 py-1 rounded-lg">
|
||||
{workflow.trigger_type || 'Manual'}
|
||||
</span>
|
||||
<span>Updated: {formatDate(workflow.updated_at)}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
executeWorkflow(workflow.id);
|
||||
}}
|
||||
className="p-2 text-blue-500 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
||||
title="Execute"
|
||||
>
|
||||
<PlayIcon className="w-4 h-4" />
|
||||
</button>
|
||||
{workflow.n8n_data?.active || workflow.is_active ? (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deactivateWorkflow(workflow.id);
|
||||
}}
|
||||
className="p-2 text-yellow-500 hover:text-yellow-600 hover:bg-yellow-50 rounded-lg transition-colors"
|
||||
title="Deactivate"
|
||||
>
|
||||
<PauseIcon className="w-4 h-4" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
activateWorkflow(workflow.id);
|
||||
}}
|
||||
className="p-2 text-green-500 hover:text-green-600 hover:bg-green-50 rounded-lg transition-colors"
|
||||
title="Activate"
|
||||
>
|
||||
<PlayIcon className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
<Link
|
||||
href={`/workflows/${workflow.id}/edit`}
|
||||
className="p-2 text-orange-500 hover:text-orange-600 hover:bg-orange-50 rounded-lg transition-colors"
|
||||
title="Edit"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<PencilIcon className="w-4 h-4" />
|
||||
</Link>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteWorkflow(workflow.id);
|
||||
}}
|
||||
className="p-2 text-red-500 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
||||
title="Delete"
|
||||
>
|
||||
<TrashIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Executions Panel */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="bg-white rounded-2xl shadow-lg border border-gray-100">
|
||||
<div className="px-6 py-4 border-b border-gray-100">
|
||||
<h2 className="text-xl font-bold text-gray-900">
|
||||
{selectedWorkflow ? 'Recent Executions' : 'Select Workflow'}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
{!selectedWorkflow ? (
|
||||
<div className="text-center py-8">
|
||||
<ChartBarIcon className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-gray-600">
|
||||
Select a workflow to view its execution history
|
||||
</p>
|
||||
</div>
|
||||
) : executionsLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-orange-500"></div>
|
||||
</div>
|
||||
) : executions.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<ClockIcon className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-gray-600">
|
||||
No executions found for this workflow
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{executions.map((execution) => (
|
||||
<div key={execution.id} className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
{execution.finished ? (
|
||||
<CheckCircleIcon className="w-4 h-4 text-green-500" />
|
||||
) : (
|
||||
<ClockIcon className="w-4 h-4 text-yellow-500" />
|
||||
)}
|
||||
<span className="text-sm font-medium">
|
||||
{execution.finished ? 'Completed' : 'Running'}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">
|
||||
{formatDate(execution.startedAt)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-gray-600">
|
||||
Mode: {execution.mode}
|
||||
{execution.stoppedAt && (
|
||||
<span className="ml-2">
|
||||
• Duration: {Math.round((new Date(execution.stoppedAt).getTime() - new Date(execution.startedAt).getTime()) / 1000)}s
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
7
frontend/src/components/ClientProvider.tsx
Normal file
7
frontend/src/components/ClientProvider.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { AuthProvider } from '@/hooks/useAuth';
|
||||
|
||||
export default function ClientProvider({ children }: { children: React.ReactNode }) {
|
||||
return <AuthProvider>{children}</AuthProvider>;
|
||||
}
|
||||
176
frontend/src/components/Layout.tsx
Normal file
176
frontend/src/components/Layout.tsx
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
'use client';
|
||||
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import {
|
||||
HomeIcon,
|
||||
CogIcon,
|
||||
Squares2X2Icon,
|
||||
LinkIcon,
|
||||
DocumentIcon,
|
||||
ChatBubbleLeftIcon,
|
||||
ArrowRightOnRectangleIcon,
|
||||
BoltIcon,
|
||||
ChartBarIcon,
|
||||
SparklesIcon,
|
||||
UserCircleIcon,
|
||||
ShieldCheckIcon,
|
||||
CreditCardIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const navigation = [
|
||||
{ name: 'Dashboard', href: '/dashboard', icon: HomeIcon, badge: null },
|
||||
{ name: 'Workflows', href: '/workflows', icon: CogIcon, badge: null },
|
||||
{ name: 'Templates', href: '/marketplace', icon: Squares2X2Icon, badge: 'New' },
|
||||
{ name: 'Integrations', href: '/integrations', icon: LinkIcon, badge: null },
|
||||
{ name: 'Billing', href: '/billing', icon: CreditCardIcon, badge: null },
|
||||
{ name: 'Logs', href: '/logs', icon: ChartBarIcon, badge: null },
|
||||
{ name: 'AI Assistant', href: '/chat', icon: SparklesIcon, badge: 'Beta' },
|
||||
];
|
||||
|
||||
export default function Layout({ children }: LayoutProps) {
|
||||
const { user, logout } = useAuth();
|
||||
const pathname = usePathname();
|
||||
|
||||
// Admin navigation items
|
||||
const adminNavigation = user?.role === 'admin' ? [
|
||||
{ name: 'Admin Panel', href: '/admin', icon: ShieldCheckIcon, badge: 'Admin' }
|
||||
] : [];
|
||||
|
||||
// Don't show sidebar on auth pages and landing page
|
||||
if (!user || pathname === '/' || pathname === '/login' || pathname === '/signup') {
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gradient-to-br from-orange-50 via-white to-purple-50">
|
||||
{/* Sidebar */}
|
||||
<div className="flex flex-col w-72 bg-white/80 backdrop-blur-sm shadow-xl border-r border-gray-100">
|
||||
{/* Logo */}
|
||||
<div className="flex items-center px-6 py-6 border-b border-gray-100">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 gradient-primary rounded-xl flex items-center justify-center">
|
||||
<BoltIcon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold gradient-text">Aimpress</h1>
|
||||
<p className="text-xs text-gray-500">AutomationHub</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 px-4 py-6">
|
||||
<div className="space-y-2">
|
||||
{[...navigation, ...adminNavigation].map((item) => {
|
||||
const isActive = pathname === item.href || (pathname.startsWith(item.href) && item.href !== '/');
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={`group flex items-center justify-between px-4 py-3 text-sm font-medium rounded-xl transition-all ${
|
||||
isActive
|
||||
? 'bg-gradient-to-r from-orange-500 to-purple-600 text-white shadow-lg'
|
||||
: 'text-gray-700 hover:bg-gray-50 hover:text-orange-600'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<item.icon
|
||||
className={`w-5 h-5 mr-3 ${
|
||||
isActive ? 'text-white' : 'text-gray-500 group-hover:text-orange-500'
|
||||
}`}
|
||||
/>
|
||||
{item.name}
|
||||
</div>
|
||||
{item.badge && (
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${
|
||||
isActive
|
||||
? 'bg-white/20 text-white'
|
||||
: item.badge === 'Admin'
|
||||
? 'bg-red-100 text-red-600'
|
||||
: 'bg-orange-100 text-orange-600'
|
||||
}`}>
|
||||
{item.badge}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Quick Stats */}
|
||||
<div className="mt-8 p-4 bg-gradient-to-br from-blue-50 to-purple-50 rounded-xl border border-blue-100">
|
||||
<h3 className="text-sm font-semibold text-gray-800 mb-3">Quick Stats</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">Active Workflows</span>
|
||||
<span className="font-semibold text-green-600">3</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">This Month</span>
|
||||
<span className="font-semibold text-blue-600">1,247</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">Success Rate</span>
|
||||
<span className="font-semibold text-purple-600">98.5%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* User Profile */}
|
||||
<div className="p-4 border-t border-gray-100">
|
||||
<div className="flex items-center space-x-3 p-3 rounded-xl hover:bg-gray-50 transition-colors cursor-pointer group">
|
||||
<div className="relative">
|
||||
<div className="w-10 h-10 gradient-primary rounded-full flex items-center justify-center">
|
||||
<span className="text-sm font-bold text-white">
|
||||
{user.first_name?.[0] || user.email[0].toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="absolute -bottom-1 -right-1 w-4 h-4 bg-green-500 border-2 border-white rounded-full"></div>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-semibold text-gray-900 truncate">
|
||||
{user.first_name} {user.last_name}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 truncate">{user.email}</p>
|
||||
<p className="text-xs text-orange-600 font-medium capitalize">
|
||||
{user.subscription_plan} Plan
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={logout}
|
||||
className="p-2 text-gray-400 hover:text-red-500 rounded-lg hover:bg-red-50 transition-colors"
|
||||
title="Logout"
|
||||
>
|
||||
<ArrowRightOnRectangleIcon className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<main className="h-full overflow-y-auto">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Floating Action Button */}
|
||||
<div className="fixed bottom-6 right-6">
|
||||
<Link
|
||||
href="/chat"
|
||||
className="group flex items-center justify-center w-14 h-14 gradient-primary text-white rounded-2xl shadow-xl hover:shadow-2xl transition-all transform hover:scale-110"
|
||||
title="AI Assistant"
|
||||
>
|
||||
<SparklesIcon className="w-7 h-7 group-hover:rotate-12 transition-transform" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
87
frontend/src/hooks/useAuth.tsx
Normal file
87
frontend/src/hooks/useAuth.tsx
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect, createContext, useContext } from "react";
|
||||
import { User } from "@/types";
|
||||
import api from "@/lib/api";
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
token: string | null;
|
||||
login: (email: string, password: string) => Promise<void>;
|
||||
signup: (email: string, password: string, firstName?: string, lastName?: string) => Promise<void>;
|
||||
logout: () => void;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [token, setToken] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const savedToken = localStorage.getItem("token");
|
||||
const savedUser = localStorage.getItem("user");
|
||||
|
||||
if (savedToken && savedUser) {
|
||||
setToken(savedToken);
|
||||
setUser(JSON.parse(savedUser));
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
const login = async (email: string, password: string) => {
|
||||
try {
|
||||
const response = await api.post("/auth/login", { email, password });
|
||||
const { token, user } = response.data;
|
||||
|
||||
localStorage.setItem("token", token);
|
||||
localStorage.setItem("user", JSON.stringify(user));
|
||||
setToken(token);
|
||||
setUser(user);
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response?.data?.message || "Login failed");
|
||||
}
|
||||
};
|
||||
|
||||
const signup = async (email: string, password: string, firstName?: string, lastName?: string) => {
|
||||
try {
|
||||
const response = await api.post("/auth/signup", {
|
||||
email,
|
||||
password,
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
});
|
||||
const { token, user } = response.data;
|
||||
|
||||
localStorage.setItem("token", token);
|
||||
localStorage.setItem("user", JSON.stringify(user));
|
||||
setToken(token);
|
||||
setUser(user);
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response?.data?.message || "Signup failed");
|
||||
}
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("user");
|
||||
setToken(null);
|
||||
setUser(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, token, login, signup, logout, loading }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useAuth must be used within an AuthProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
32
frontend/src/lib/api.ts
Normal file
32
frontend/src/lib/api.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import axios from 'axios';
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default api;
|
||||
75
frontend/src/types/index.ts
Normal file
75
frontend/src/types/index.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
export interface User {
|
||||
id: number;
|
||||
email: string;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
is_verified: boolean;
|
||||
role: string;
|
||||
subscription_plan: string;
|
||||
subscription_status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Workflow {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
status: string;
|
||||
trigger_type: string;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Template {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
category: string;
|
||||
trigger_type: string;
|
||||
tags: string[];
|
||||
is_featured: boolean;
|
||||
install_count: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface Integration {
|
||||
id: number;
|
||||
provider: string;
|
||||
account_name?: string;
|
||||
account_email?: string;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Webhook {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
trigger_type: string;
|
||||
url_path: string;
|
||||
is_active: boolean;
|
||||
secret_key?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface WorkflowExecution {
|
||||
id: number;
|
||||
workflow_id: number;
|
||||
workflow_name: string;
|
||||
status: string;
|
||||
trigger_data: any;
|
||||
error_message?: string;
|
||||
started_at?: string;
|
||||
finished_at?: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
message_type: 'user' | 'assistant';
|
||||
content: string;
|
||||
created_at: string;
|
||||
}
|
||||
27
frontend/tsconfig.json
Normal file
27
frontend/tsconfig.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue