https://hrubos.tech/blogy/content/images/20260314074000-appchat1.png
Ták náš nový facebook type chat pokec je hotový. Už len skúšajte a kecajte: https://app.hrubos.tech/
https://hrubos.tech/blogy/content/images/20260314074148-appchat2.jpg

Obsluhuje Vás jednoduchý JS skriptík do 200 riadkov na backende:
const express = require("express");
const http = require("http");
const { Server } = require("socket.io");
const multer = require("multer");
const path = require("path");
const fs = require("fs");
const rateLimit = require("express-rate-limit");
const cors = require("cors");
const app = express();
const server = http.createServer(app);
const io = new Server(server, {
cors: {
origin: ["http://localhost:3000", "http://127.0.0.1:3000"],
credentials: true
}
});
const PORT = process.env.PORT || 3000;
const MAX_HISTORY = 100;
let messagesHistory = [];
const uploadDir = path.join(__dirname, "uploads");
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true, mode: 0o775 });
}
// ✅ FILE TYPE VALIDATION + RATE LIMITING
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, uploadDir),
filename: (req, file, cb) => cb(null, Date.now() + "_" + Math.random().toString(36).substr(2, 9) + path.extname(file.originalname))
});
const upload = multer({
storage,
limits: { fileSize: 5 * 1024 * 1024 },
fileFilter: (req, file, cb) => {
const allowedTypes = /jpeg|jpg|png|gif|webp|bmp/;
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = allowedTypes.test(file.mimetype);
if (extname && mimetype) {
cb(null, true);
} else {
cb(new Error("Povolené súbor: JPG, PNG, GIF, WebP (max 5MB)"), false);
}
}
}).single("image");
// Rate limiting
const uploadLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minúta
max: 5, // max 5 uploads za minútu
message: { error: "Príliš veľa uploadov, skús neskôr!" },
standardHeaders: true,
legacyHeaders: false,
});
const messageLimiter = rateLimit({
windowMs: 60 * 1000,
max: 20,
message: { error: "Príliš veľa správ, spomaľ!" }
});
// Routes
app.get("/", (req, res) => res.sendFile(path.join(__dirname, "client", "index.html")));
app.get("/reset", (req, res) => {
messagesHistory = [];
fs.readdirSync(uploadDir).forEach(file => fs.unlinkSync(path.join(uploadDir, file)));
res.send("✅ Reset OK");
});
app.get("/health", (req, res) => res.json({ status: "OK", messages: messagesHistory.length }));
// Middleware
app.use(cors({
origin: ["http://localhost:3000", "http://127.0.0.1:3000"],
credentials: true
}));
app.use(express.json({ limit: "10mb" }));
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
app.use(express.static(path.join(__dirname, "client")));
app.use("/uploads", express.static(uploadDir));
// Upload endpoint s rate limiting
app.post("/upload", uploadLimiter, (req, res) => {
console.log("📤 Upload request");
upload(req, res, (err) => {
if (err) {
console.error("❌ Upload error:", err.message);
if (err.code === "LIMIT_FILE_SIZE") {
return res.status(400).json({ error: "Súbor je príliš veľký (max 5MB)" });
}
return res.status(500).json({ error: err.message || "Chyba pri nahrávaní" });
}
if (!req.file) {
return res.status(400).json({ error: "Žiadny obrázok!" });
}
console.log("✅ Nahral sa:", req.file.filename);
res.json({ url: `/uploads/${req.file.filename}` });
});
});
// Socket.IO s pokročilým rate limiting
io.on("connection", (socket) => {
console.log("👤 Pripojený:", socket.id);
// Poslať históriu
socket.emit("history", messagesHistory.slice(-50)); // posledných 50 správ
// Rate limiting pre jednotlivých klientov
socket.use((event, next) => {
if (event[0] === "chat message") {
const now = Date.now();
const clientMessages = socket.data.messages || [];
clientMessages.push(now);
socket.data.messages = clientMessages.filter(time => now - time < 5000); // 5s okno
if (socket.data.messages.length > 10) {
return next(new Error("Príliš rýchlo píšeš!"));
}
}
next();
});
socket.on("chat message", (data) => {
// Validácia
if (!data || typeof data.text !== "string") {
socket.emit("error", "Neplatná správa");
return;
}
if (data.text.length > 1000) {
socket.emit("error", "Správa je príliš dlhá (max 1000 znakov)");
return;
}
// XSS ochrana
function escapeHTML(str) {
const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
return str.replace(/[&<>"']/g, m => map[m]);
}
const safeMsg = {
user: escapeHTML(data.user?.slice(0, 20) || "Anon"),
text: escapeHTML(data.text),
image: data.image ? escapeHTML(data.image) : null,
time: new Date().toLocaleString("sk-SK", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit"
})
};
// Uložiť do histórie
messagesHistory.push(safeMsg);
if (messagesHistory.length > MAX_HISTORY) {
messagesHistory.shift();
}
// Poslať všetkým
io.emit("chat message", safeMsg);
console.log("💬", safeMsg.user, ":", safeMsg.text?.slice(0, 50));
});
socket.on("disconnect", () => {
console.log("👋 Odpojil sa:", socket.id);
});
});
// Graceful shutdown
function gracefulShutdown() {
console.log("\n🛑 Ukončujem server...");
io.close(() => {
server.close(() => {
console.log("✅ Server ukončený");
process.exit(0);
});
});
}
process.on("SIGTERM", gracefulShutdown);
process.on("SIGINT", gracefulShutdown);
server.listen(PORT, () => {
console.log(`🚀 Chat server beží na http://localhost:${PORT}`);
console.log(`🔧 Health: http://localhost:${PORT}/health`);
console.log(`🔄 Reset: http://localhost:${PORT}/reset`);
});
Na front ende: Viďte zdroják HTML5 stránky ;)

Comments “Ták náš nový app chat pokec je hotový, už len skúšajte a kecajte: https://app.hrubos.tech/”