require("dotenv").config();
const express = require("express");
const bodyParser = require("body-parser");
const Pusher = require("pusher");
const crypto = require("crypto");
const app = express();
const port = process.env.BACKEND_PORT || 3000;
const pusher = new Pusher({
appId: process.env.PUSHER_APP_ID,
key: process.env.PUSHER_APP_KEY,
secret: process.env.PUSHER_APP_SECRET,
cluster: "mt1",
host: process.env.PUSHER_HOST,
port: process.env.PUSHER_PORT,
useTLS: process.env.PUSHER_USE_TLS === "true",
});
console.log("Pusher Server SDK configured for:", {
appId: process.env.PUSHER_APP_ID,
key: process.env.PUSHER_APP_KEY ? "***" : "Not Set",
secret: process.env.PUSHER_APP_SECRET ? "***" : "Not Set",
host: process.env.PUSHER_HOST,
port: process.env.PUSHER_PORT,
useTLS: process.env.PUSHER_USE_TLS === "true",
});
let receivedWebhooks = [];
let activeChannels = new Set();
let eventHistory = [];
app.use(express.static("public"));
app.use(
bodyParser.json({
verify: (req, res, buf) => {
req.rawBody = buf;
},
})
);
app.use(bodyParser.urlencoded({ extended: false }));
app.get("/config", (req, res) => {
res.json({
pusherKey: process.env.PUSHER_APP_KEY,
pusherHost: process.env.PUSHER_HOST,
pusherPort: process.env.PUSHER_PORT,
pusherUseTLS: process.env.PUSHER_USE_TLS === "true",
authEndpoint: `${process.env.BACKEND_BASE_URL}/pusher/auth`,
pusherCluster: "mt1",
});
});
app.post("/pusher/auth", (req, res) => {
const socketId = req.body.socket_id;
const channel = req.body.channel_name;
console.log(`Auth attempt for socket_id: ${socketId}, channel: ${channel}`);
const MOCK_USER_ID = `user_${Math.random().toString(36).substr(2, 9)}`;
const MOCK_USER_INFO = {
name: `Test User ${MOCK_USER_ID.split("_")[1]}`,
id: MOCK_USER_ID,
avatar: `https://ui-avatars.com/api/?name=${MOCK_USER_ID}&background=random`,
};
try {
if (channel.startsWith("private-")) {
const authResponse = pusher.authorizeChannel(socketId, channel);
console.log("Auth success (private):", authResponse);
res.send(authResponse);
} else if (channel.startsWith("presence-")) {
const presenceData = {
user_id: MOCK_USER_ID,
user_info: MOCK_USER_INFO,
};
const authResponse = pusher.authorizeChannel(
socketId,
channel,
presenceData
);
console.log("Auth success (presence):", authResponse);
res.send(authResponse);
} else {
console.error(
`Auth failed: Channel ${channel} is not private or presence.`
);
res.status(403).send("Forbidden: Channel is not private or presence");
}
} catch (error) {
console.error("Auth error:", error);
res.status(500).send(`Authentication error: ${error.message}`);
}
});
app.post("/pusher/webhooks", (req, res) => {
console.log("\n--- Webhook Received ---");
const receivedSignature = req.headers["x-pusher-signature"];
const expectedSignature = crypto
.createHmac("sha256", process.env.PUSHER_APP_SECRET)
.update(req.rawBody)
.digest("hex");
if (receivedSignature && receivedSignature !== expectedSignature) {
console.error("Webhook signature invalid!");
return res.status(403).send("Webhook signature invalid");
}
const webhookBody = req.body;
console.log("Webhook Body:", JSON.stringify(webhookBody, null, 2));
const webhookData = {
timestamp: new Date().toISOString(),
headers: req.headers,
body: webhookBody,
};
receivedWebhooks.unshift(webhookData);
if (receivedWebhooks.length > 100) {
receivedWebhooks.pop();
}
res.status(200).json({ message: "Webhook received" });
});
app.post("/trigger-event", async (req, res) => {
try {
const { channel, event, data } = req.body;
if (!channel || !event) {
return res.status(400).json({ error: "Channel and event are required" });
}
console.log(`Triggering server event: ${event} on ${channel}`);
const result = await pusher.trigger(channel, event, data || {});
const eventRecord = {
timestamp: new Date().toISOString(),
type: "server-triggered",
channel,
event,
data: data || {},
result,
};
eventHistory.unshift(eventRecord);
if (eventHistory.length > 100) {
eventHistory.pop();
}
res.json({ success: true, result });
} catch (error) {
console.error("Error triggering event:", error);
res.status(500).json({ error: error.message });
}
});
app.post("/trigger-batch-events", async (req, res) => {
try {
const { channel, count = 5, delay = 1000 } = req.body;
if (!channel) {
return res.status(400).json({ error: "Channel is required" });
}
res.json({ success: true, message: `Triggering ${count} events...` });
for (let i = 1; i <= count; i++) {
setTimeout(async () => {
try {
await pusher.trigger(channel, "batch-test", {
message: `Batch message ${i} of ${count}`,
timestamp: new Date().toISOString(),
sequence: i,
});
console.log(`Batch event ${i}/${count} sent to ${channel}`);
} catch (error) {
console.error(`Error sending batch event ${i}:`, error);
}
}, (i - 1) * delay);
}
} catch (error) {
console.error("Error triggering batch events:", error);
res.status(500).json({ error: error.message });
}
});
app.get("/webhooks-log", (req, res) => {
res.json(receivedWebhooks);
});
app.get("/event-history", (req, res) => {
res.json(eventHistory);
});
app.get("/channel-info", async (req, res) => {
try {
const { channel } = req.query;
if (!channel) {
return res.status(400).json({ error: "Channel parameter required" });
}
const info = await pusher.get({ path: `/channels/${channel}` });
res.json(info);
} catch (error) {
console.error("Error getting channel info:", error);
res.status(500).json({ error: error.message });
}
});
app.get("/channels", async (req, res) => {
try {
const channels = await pusher.get({ path: "/channels" });
res.json(channels);
} catch (error) {
console.error("Error getting channels:", error);
res.status(500).json({ error: error.message });
}
});
app.listen(port, () => {
console.log(`\n🚀 Pusher Test Backend Server`);
console.log(`📡 Running at ${process.env.BACKEND_BASE_URL}`);
console.log(`🔐 Auth Endpoint: POST /pusher/auth`);
console.log(`🪝 Webhook Endpoint: POST /pusher/webhooks`);
console.log(`⚡ Trigger Event: POST /trigger-event`);
console.log(`📊 Dashboard available at: ${process.env.BACKEND_BASE_URL}`);
console.log(`================================\n`);
});