Add typeform server
This commit is contained in:
@@ -27,6 +27,7 @@ const metaEnv = loadEnvFile(path.resolve(__dirname, 'meta-server/.env'));
|
|||||||
const googleAnalyticsEnv = require('dotenv').config({
|
const googleAnalyticsEnv = require('dotenv').config({
|
||||||
path: path.resolve(__dirname, 'google-server/.env')
|
path: path.resolve(__dirname, 'google-server/.env')
|
||||||
}).parsed || {};
|
}).parsed || {};
|
||||||
|
const typeformEnv = loadEnvFile(path.resolve(__dirname, 'typeform-server/.env'));
|
||||||
|
|
||||||
// Common log settings for all apps
|
// Common log settings for all apps
|
||||||
const logSettings = {
|
const logSettings = {
|
||||||
@@ -151,7 +152,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
...commonSettings,
|
...commonSettings,
|
||||||
name: 'google-analytics-server',
|
name: 'google-server',
|
||||||
script: path.resolve(__dirname, './google-server/server.js'),
|
script: path.resolve(__dirname, './google-server/server.js'),
|
||||||
watch: false,
|
watch: false,
|
||||||
env: {
|
env: {
|
||||||
@@ -166,6 +167,23 @@ module.exports = {
|
|||||||
NODE_ENV: 'production',
|
NODE_ENV: 'production',
|
||||||
GOOGLE_ANALYTICS_PORT: 3007
|
GOOGLE_ANALYTICS_PORT: 3007
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...commonSettings,
|
||||||
|
name: 'typeform-server',
|
||||||
|
script: './typeform-server/server.js',
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
TYPEFORM_PORT: 3008,
|
||||||
|
...typeformEnv
|
||||||
|
},
|
||||||
|
error_file: 'typeform-server/logs/pm2/err.log',
|
||||||
|
out_file: 'typeform-server/logs/pm2/out.log',
|
||||||
|
log_file: 'typeform-server/logs/pm2/combined.log',
|
||||||
|
env_production: {
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
TYPEFORM_PORT: 3008
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
13
dashboard-server/typeform-server/.env.example
Normal file
13
dashboard-server/typeform-server/.env.example
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Server Configuration
|
||||||
|
NODE_ENV=development
|
||||||
|
TYPEFORM_PORT=3008
|
||||||
|
|
||||||
|
# Redis Configuration
|
||||||
|
REDIS_URL=redis://localhost:6379
|
||||||
|
|
||||||
|
# Typeform API Configuration
|
||||||
|
TYPEFORM_ACCESS_TOKEN=your_typeform_access_token_here
|
||||||
|
|
||||||
|
# Optional: Form IDs (if you want to store them in env)
|
||||||
|
TYPEFORM_FORM_ID_1=your_first_form_id
|
||||||
|
TYPEFORM_FORM_ID_2=your_second_form_id
|
||||||
1411
dashboard-server/typeform-server/package-lock.json
generated
Normal file
1411
dashboard-server/typeform-server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
dashboard-server/typeform-server/package.json
Normal file
20
dashboard-server/typeform-server/package.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "typeform-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Typeform API integration server",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "nodemon server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.6.2",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"redis": "^4.6.11"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
121
dashboard-server/typeform-server/routes/typeform.routes.js
Normal file
121
dashboard-server/typeform-server/routes/typeform.routes.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const typeformService = require('../services/typeform.service');
|
||||||
|
|
||||||
|
// Get form responses
|
||||||
|
router.get('/forms/:formId/responses', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { formId } = req.params;
|
||||||
|
const filters = req.query;
|
||||||
|
|
||||||
|
console.log(`Fetching responses for form ${formId} with filters:`, filters);
|
||||||
|
|
||||||
|
if (!formId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Missing form ID',
|
||||||
|
details: 'The form ID parameter is required'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await typeformService.getFormResponsesWithFilters(formId, filters);
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return res.status(404).json({
|
||||||
|
error: 'No data found',
|
||||||
|
details: `No responses found for form ${formId}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Form responses error:', {
|
||||||
|
formId: req.params.formId,
|
||||||
|
filters: req.query,
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
response: error.response?.data
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle specific error cases
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: 'Authentication failed',
|
||||||
|
details: 'Invalid Typeform API credentials'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.response?.status === 404) {
|
||||||
|
return res.status(404).json({
|
||||||
|
error: 'Not found',
|
||||||
|
details: `Form '${req.params.formId}' not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.response?.status === 400) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Invalid request',
|
||||||
|
details: error.response?.data?.message || 'The request was invalid',
|
||||||
|
data: error.response?.data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Failed to fetch form responses',
|
||||||
|
details: error.response?.data?.message || error.message,
|
||||||
|
data: error.response?.data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get form insights
|
||||||
|
router.get('/forms/:formId/insights', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { formId } = req.params;
|
||||||
|
|
||||||
|
if (!formId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Missing form ID',
|
||||||
|
details: 'The form ID parameter is required'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await typeformService.getFormInsights(formId);
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return res.status(404).json({
|
||||||
|
error: 'No data found',
|
||||||
|
details: `No insights found for form ${formId}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Form insights error:', {
|
||||||
|
formId: req.params.formId,
|
||||||
|
error: error.message,
|
||||||
|
response: error.response?.data
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: 'Authentication failed',
|
||||||
|
details: 'Invalid Typeform API credentials'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.response?.status === 404) {
|
||||||
|
return res.status(404).json({
|
||||||
|
error: 'Not found',
|
||||||
|
details: `Form '${req.params.formId}' not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Failed to fetch form insights',
|
||||||
|
details: error.response?.data?.message || error.message,
|
||||||
|
data: error.response?.data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
31
dashboard-server/typeform-server/server.js
Normal file
31
dashboard-server/typeform-server/server.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const cors = require('cors');
|
||||||
|
const path = require('path');
|
||||||
|
require('dotenv').config({
|
||||||
|
path: path.resolve(__dirname, '.env')
|
||||||
|
});
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const port = process.env.TYPEFORM_PORT || 3008;
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Import routes
|
||||||
|
const typeformRoutes = require('./routes/typeform.routes');
|
||||||
|
|
||||||
|
// Use routes
|
||||||
|
app.use('/api/typeform', typeformRoutes);
|
||||||
|
|
||||||
|
// Error handling middleware
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
console.error(err.stack);
|
||||||
|
res.status(500).json({ error: 'Something went wrong!' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Typeform API server running on port ${port}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = app;
|
||||||
105
dashboard-server/typeform-server/services/typeform.service.js
Normal file
105
dashboard-server/typeform-server/services/typeform.service.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
const { createClient } = require('redis');
|
||||||
|
|
||||||
|
class TypeformService {
|
||||||
|
constructor() {
|
||||||
|
this.redis = createClient({
|
||||||
|
url: process.env.REDIS_URL
|
||||||
|
});
|
||||||
|
|
||||||
|
this.redis.on('error', err => console.error('Redis Client Error:', err));
|
||||||
|
this.redis.connect().catch(err => console.error('Redis connection error:', err));
|
||||||
|
|
||||||
|
this.apiClient = axios.create({
|
||||||
|
baseURL: 'https://api.typeform.com',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${process.env.TYPEFORM_ACCESS_TOKEN}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFormResponses(formId, params = {}) {
|
||||||
|
const cacheKey = `typeform:responses:${formId}:${JSON.stringify(params)}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try Redis first
|
||||||
|
const cachedData = await this.redis.get(cacheKey);
|
||||||
|
if (cachedData) {
|
||||||
|
console.log(`Form responses for ${formId} found in Redis cache`);
|
||||||
|
return JSON.parse(cachedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch from API
|
||||||
|
const response = await this.apiClient.get(`/forms/${formId}/responses`, { params });
|
||||||
|
const data = response.data;
|
||||||
|
|
||||||
|
// Save to Redis with 5 minute expiry
|
||||||
|
await this.redis.set(cacheKey, JSON.stringify(data), {
|
||||||
|
EX: 300 // 5 minutes
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching form responses for ${formId}:`, {
|
||||||
|
error: error.message,
|
||||||
|
params,
|
||||||
|
response: error.response?.data
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFormInsights(formId) {
|
||||||
|
const cacheKey = `typeform:insights:${formId}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try Redis first
|
||||||
|
const cachedData = await this.redis.get(cacheKey);
|
||||||
|
if (cachedData) {
|
||||||
|
console.log(`Form insights for ${formId} found in Redis cache`);
|
||||||
|
return JSON.parse(cachedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch from API
|
||||||
|
const response = await this.apiClient.get(`/insights/${formId}/summary`);
|
||||||
|
const data = response.data;
|
||||||
|
|
||||||
|
// Save to Redis with 5 minute expiry
|
||||||
|
await this.redis.set(cacheKey, JSON.stringify(data), {
|
||||||
|
EX: 300 // 5 minutes
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching form insights for ${formId}:`, {
|
||||||
|
error: error.message,
|
||||||
|
response: error.response?.data
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFormResponsesWithFilters(formId, { since, until, pageSize = 25, ...otherParams } = {}) {
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
page_size: pageSize,
|
||||||
|
...otherParams
|
||||||
|
};
|
||||||
|
|
||||||
|
if (since) {
|
||||||
|
params.since = new Date(since).toISOString();
|
||||||
|
}
|
||||||
|
if (until) {
|
||||||
|
params.until = new Date(until).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.getFormResponses(formId, params);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in getFormResponsesWithFilters:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new TypeformService();
|
||||||
28
nginx.conf
28
nginx.conf
@@ -53,3 +53,31 @@ location /api/analytics/ {
|
|||||||
return 204;
|
return 204;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Typeform API endpoints
|
||||||
|
location /api/typeform/ {
|
||||||
|
proxy_pass http://localhost:3008/api/typeform/;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|
||||||
|
# CORS headers
|
||||||
|
add_header 'Access-Control-Allow-Origin' '*';
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
|
||||||
|
|
||||||
|
# Handle OPTIONS method
|
||||||
|
if ($request_method = 'OPTIONS') {
|
||||||
|
add_header 'Access-Control-Allow-Origin' '*';
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
|
||||||
|
add_header 'Access-Control-Max-Age' 1728000;
|
||||||
|
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||||
|
add_header 'Content-Length' 0;
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user