This commit is contained in:
44
.gitea/workflows/build-deploy.yml
Normal file
44
.gitea/workflows/build-deploy.yml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
name: Build and Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Convert repository name to lowercase
|
||||||
|
run: echo "repo_name=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Login to Gitea Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: gitea.blyzer.com.br
|
||||||
|
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
gitea.blyzer.com.br/${{ env.repo_name }}:latest
|
||||||
|
gitea.blyzer.com.br/${{ env.repo_name }}:${{ gitea.sha }}
|
||||||
|
|
||||||
|
- name: Deploy to Portainer
|
||||||
|
run: |
|
||||||
|
if [ -n "${{ secrets.PORTAINER_WEBHOOK }}" ]; then
|
||||||
|
curl -k -X POST "${{ secrets.PORTAINER_WEBHOOK }}"
|
||||||
|
else
|
||||||
|
echo "PORTAINER_WEBHOOK secret not set, skipping deployment trigger."
|
||||||
|
fi
|
||||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.env
|
||||||
|
package.json
|
||||||
|
package-lock.json
|
||||||
|
tsconfig.json
|
||||||
|
node_modules
|
||||||
30
Dockerfile
Normal file
30
Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files and install all dependencies (including devDependencies for TypeScript)
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Copy source code and build
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files and install ONLY production dependencies
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
|
# Copy built code from the builder stage
|
||||||
|
COPY --from=builder /app/dist ./dist
|
||||||
|
|
||||||
|
# Expose the port the app runs on
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Command to run the application
|
||||||
|
CMD ["npm", "start"]
|
||||||
40
dist/controllers/webhook.controller.js
vendored
Normal file
40
dist/controllers/webhook.controller.js
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.handleTinyOrderUpdate = void 0;
|
||||||
|
const n8n_service_1 = require("../services/n8n.service");
|
||||||
|
const handleTinyOrderUpdate = async (req, res) => {
|
||||||
|
try {
|
||||||
|
let payload = req.body;
|
||||||
|
// Parse 'dados' if it comes as a JSON string
|
||||||
|
if (typeof payload.dados === 'string') {
|
||||||
|
try {
|
||||||
|
payload.dados = JSON.parse(payload.dados);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error('Could not parse "dados" as JSON string.', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('Received webhook from Tiny:', JSON.stringify(payload, null, 2));
|
||||||
|
// Acknowledge Tiny immediately
|
||||||
|
res.status(200).send('OK');
|
||||||
|
if (!payload || Object.keys(payload).length === 0) {
|
||||||
|
console.warn('Received empty payload from Tiny webhook.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const transformedData = {
|
||||||
|
source: 'tiny_webhook_middleware',
|
||||||
|
event: payload.tipo || 'atualizacao_pedido',
|
||||||
|
dados: payload.dados,
|
||||||
|
raw_payload: payload
|
||||||
|
};
|
||||||
|
// Forward to n8n asynchronously
|
||||||
|
await (0, n8n_service_1.sendToN8n)(transformedData);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('Error handling Tiny webhook:', error);
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.status(500).json({ error: 'Internal server error processing webhook.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
exports.handleTinyOrderUpdate = handleTinyOrderUpdate;
|
||||||
20
dist/index.js
vendored
Normal file
20
dist/index.js
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
"use strict";
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const express_1 = __importDefault(require("express"));
|
||||||
|
const dotenv_1 = __importDefault(require("dotenv"));
|
||||||
|
const webhook_route_1 = __importDefault(require("./routes/webhook.route"));
|
||||||
|
dotenv_1.default.config();
|
||||||
|
const app = (0, express_1.default)();
|
||||||
|
const port = process.env.PORT || 3000;
|
||||||
|
app.use(express_1.default.json());
|
||||||
|
app.use(express_1.default.urlencoded({ extended: true }));
|
||||||
|
app.use('/api/webhooks', webhook_route_1.default);
|
||||||
|
app.get('/health', (req, res) => {
|
||||||
|
res.status(200).json({ status: 'OK', message: 'Tiny-n8n middleware is running.' });
|
||||||
|
});
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`[server]: Middleware server is running at http://localhost:${port}`);
|
||||||
|
});
|
||||||
7
dist/routes/webhook.route.js
vendored
Normal file
7
dist/routes/webhook.route.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const express_1 = require("express");
|
||||||
|
const webhook_controller_1 = require("../controllers/webhook.controller");
|
||||||
|
const router = (0, express_1.Router)();
|
||||||
|
router.post('/tiny/order-status', webhook_controller_1.handleTinyOrderUpdate);
|
||||||
|
exports.default = router;
|
||||||
30
dist/services/n8n.service.js
vendored
Normal file
30
dist/services/n8n.service.js
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
"use strict";
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.sendToN8n = void 0;
|
||||||
|
const axios_1 = __importDefault(require("axios"));
|
||||||
|
const sendToN8n = async (data) => {
|
||||||
|
const n8nWebhookUrl = process.env.N8N_WEBHOOK_URL;
|
||||||
|
if (!n8nWebhookUrl) {
|
||||||
|
console.error('N8N_WEBHOOK_URL is not defined in environment variables.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
console.log(`Forwarding payload to n8n at ${n8nWebhookUrl}...`);
|
||||||
|
const response = await axios_1.default.post(n8nWebhookUrl, data, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(`Successfully forwarded to n8n. Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('Failed to forward payload to n8n.', error.message);
|
||||||
|
if (error.response) {
|
||||||
|
console.error('n8n Response:', error.response.status, error.response.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
exports.sendToN8n = sendToN8n;
|
||||||
22
docker-compose.yml
Normal file
22
docker-compose.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
middleware:
|
||||||
|
# If using CI/CD, you will replace 'build: .' with your Gitea registry image:
|
||||||
|
# image: gitea.yourdomain.com/youruser/api-tiny-n8n:latest
|
||||||
|
build: .
|
||||||
|
container_name: api-tiny-n8n
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
# Maps port 3030 on your host to 3000 inside the container.
|
||||||
|
# Nginx Proxy Manager will point to YOUR_SERVER_IP:3030
|
||||||
|
- "3030:3000"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
# Optional: If you use a shared Docker network for Nginx Proxy Manager, you can attach it here:
|
||||||
|
# networks:
|
||||||
|
# - nginx-proxy-manager-network
|
||||||
|
|
||||||
|
# networks:
|
||||||
|
# nginx-proxy-manager-network:
|
||||||
|
# external: true
|
||||||
53
src/controllers/webhook.controller.ts
Normal file
53
src/controllers/webhook.controller.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { sendToN8n } from '../services/n8n.service';
|
||||||
|
|
||||||
|
export const handleTinyOrderUpdate = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
// 1. Security Check: Verify token from Tiny
|
||||||
|
const expectedToken = process.env.TINY_WEBHOOK_SECRET;
|
||||||
|
const providedToken = req.query.token;
|
||||||
|
|
||||||
|
if (expectedToken && providedToken !== expectedToken) {
|
||||||
|
console.warn('Unauthorized webhook attempt. Invalid or missing token.');
|
||||||
|
res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload = req.body;
|
||||||
|
|
||||||
|
// Parse 'dados' if it comes as a JSON string
|
||||||
|
if (typeof payload.dados === 'string') {
|
||||||
|
try {
|
||||||
|
payload.dados = JSON.parse(payload.dados);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Could not parse "dados" as JSON string.', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Received webhook from Tiny:', JSON.stringify(payload, null, 2));
|
||||||
|
|
||||||
|
// Acknowledge Tiny immediately
|
||||||
|
res.status(200).send('OK');
|
||||||
|
|
||||||
|
if (!payload || Object.keys(payload).length === 0) {
|
||||||
|
console.warn('Received empty payload from Tiny webhook.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const transformedData = {
|
||||||
|
source: 'tiny_webhook_middleware',
|
||||||
|
event: payload.tipo || 'atualizacao_pedido',
|
||||||
|
dados: payload.dados,
|
||||||
|
raw_payload: payload
|
||||||
|
};
|
||||||
|
|
||||||
|
// Forward to n8n asynchronously
|
||||||
|
await sendToN8n(transformedData);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error handling Tiny webhook:', error);
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.status(500).json({ error: 'Internal server error processing webhook.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
21
src/index.ts
Normal file
21
src/index.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import express, { Express, Request, Response } from 'express';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import webhookRoutes from './routes/webhook.route';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const app: Express = express();
|
||||||
|
const port = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
|
app.use('/api/webhooks', webhookRoutes);
|
||||||
|
|
||||||
|
app.get('/health', (req: Request, res: Response) => {
|
||||||
|
res.status(200).json({ status: 'OK', message: 'Tiny-n8n middleware is running.' });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`[server]: Middleware server is running at http://localhost:${port}`);
|
||||||
|
});
|
||||||
8
src/routes/webhook.route.ts
Normal file
8
src/routes/webhook.route.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { handleTinyOrderUpdate } from '../controllers/webhook.controller';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.post('/tiny/order-status', handleTinyOrderUpdate);
|
||||||
|
|
||||||
|
export default router;
|
||||||
25
src/services/n8n.service.ts
Normal file
25
src/services/n8n.service.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export const sendToN8n = async (data: any): Promise<void> => {
|
||||||
|
const n8nWebhookUrl = process.env.N8N_WEBHOOK_URL;
|
||||||
|
|
||||||
|
if (!n8nWebhookUrl) {
|
||||||
|
console.error('N8N_WEBHOOK_URL is not defined in environment variables.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`Forwarding payload to n8n at ${n8nWebhookUrl}...`);
|
||||||
|
const response = await axios.post(n8nWebhookUrl, data, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(`Successfully forwarded to n8n. Status: ${response.status}`);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to forward payload to n8n.', error.message);
|
||||||
|
if (error.response) {
|
||||||
|
console.error('n8n Response:', error.response.status, error.response.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user