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:
Claude Code 2025-09-29 10:27:54 +01:00
commit 58e079b08f
68 changed files with 16562 additions and 27 deletions

20
.env.example Normal file
View 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
View file

@ -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
View 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
View file

@ -0,0 +1,69 @@
# 🚀 Aimpress AutomationHub
> **SaaS Automation Platform** - Современная платформа для создания и управления автоматизированными workflow в стиле n8n.io
![Platform Preview](https://img.shields.io/badge/Status-Demo%20Ready-brightgreen)
![Tech Stack](https://img.shields.io/badge/Stack-Next.js%20%7C%20Node.js%20%7C%20PostgreSQL-blue)
![Docker](https://img.shields.io/badge/Docker-Ready-2496ED?logo=docker)
## ✨ **Основные возможности**
### 🎨 **Современный 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

38
backend/package.json Normal file
View 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"
}
}

View 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' });
}
};

View 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'
});
}
};

View 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' });
}
};

View 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'
});
}
};

View 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'
});
}
};

View 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'
});
}
};

View 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'
});
}
};

View 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'
});
}
};

View 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'
});
}
};

View 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
View 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;

View 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
View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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 };

View 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
View 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
View 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
View 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"]
}

View 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
View 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
View file

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

21
frontend/Dockerfile Normal file
View 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
View 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
View 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.

View 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
View 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

File diff suppressed because it is too large Load diff

31
frontend/package.json Normal file
View 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"
}
}

View file

@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View 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);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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
View 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>&copy; 2025 Aimpress LTD. All rights reserved.</p>
</div>
</div>
</footer>
</div>
);
}

View 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>
);
}

View 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>
);
}

View file

@ -0,0 +1,7 @@
'use client';
import { AuthProvider } from '@/hooks/useAuth';
export default function ClientProvider({ children }: { children: React.ReactNode }) {
return <AuthProvider>{children}</AuthProvider>;
}

View 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>
);
}

View 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
View 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;

View 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
View 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"]
}