const http = require("http"); const fs = require("fs"); const path = require("path"); const crypto = require("crypto"); const PORT = process.env.PORT || 3000; const DATA = path.join(__dirname, "data.json"); const SECRET_KEY = crypto.randomBytes(32).toString("hex"); function readData() { try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return { users: [], chats: [], messages: [], groups: [], channels: [], sessions: [] }; } } function writeData(data) { fs.writeFileSync(DATA, JSON.stringify(data, null, 2)); } function parseBody(req) { return new Promise((resolve) => { let s = ""; req.on("data", (c) => (s += c)); req.on("end", () => { try { resolve(JSON.parse(s || "{}")); } catch (_) { resolve({}); } }); }); } function json(res, data, status = 200) { res.writeHead(status, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }); res.end(JSON.stringify(data)); } function notFound(res) { res.writeHead(404, { "Content-Type": "text/plain" }); res.end("Not Found"); } // Генерация токена сессии function generateToken(userId) { return crypto.createHmac("sha256", SECRET_KEY).update(userId + Date.now()).digest("hex"); } // Проверка токена function verifyToken(token) { const data = readData(); const session = data.sessions.find(s => s.token === token && new Date(s.expires) > new Date()); if (!session) return null; return data.users.find(u => u.id === session.userId); } // Хеширование пароля function hashPassword(password) { return crypto.createHash("sha256").update(password + "vibechat_salt").digest("hex"); } // E2E шифрование (упрощённое AES-GCM) function encryptMessage(text, key) { const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv("aes-256-gcm", Buffer.from(key.slice(0, 32).padEnd(32, "0")), iv); let encrypted = cipher.update(text, "utf8", "hex"); encrypted += cipher.final("hex"); const authTag = cipher.getAuthTag().toString("hex"); return { iv: iv.toString("hex"), encrypted, authTag }; } function decryptMessage(encryptedData, key) { try { const decipher = crypto.createDecipheriv("aes-256-gcm", Buffer.from(key.slice(0, 32).padEnd(32, "0")), Buffer.from(encryptedData.iv, "hex")); decipher.setAuthTag(Buffer.from(encryptedData.authTag, "hex")); let decrypted = decipher.update(encryptedData.encrypted, "hex", "utf8"); decrypted += decipher.final("utf8"); return decrypted; } catch (e) { return "[Ошибка расшифровки]"; } } const routes = { // ========== AUTH ========== "POST /api/auth/register": async (req, res) => { const { username, email, password } = await parseBody(req); if (!username || !email || !password) return json(res, { ok: false, error: "Все поля обязательны" }, 400); const data = readData(); if (data.users.find(u => u.email === email)) return json(res, { ok: false, error: "Email уже зарегистрирован" }, 400); if (data.users.find(u => u.username === username)) return json(res, { ok: false, error: "Имя пользователя занято" }, 400); const user = { id: Date.now(), username, email, passwordHash: hashPassword(password), avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${username}`, bio: "", createdAt: new Date().toISOString() }; data.users.push(user); writeData(data); const token = generateToken(user.id); data.sessions.push({ token, userId: user.id, expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString() }); writeData(data); json(res, { ok: true, token, user: { id: user.id, username: user.username, email: user.email, avatar: user.avatar } }); }, "POST /api/auth/login": async (req, res) => { const { email, password } = await parseBody(req); if (!email || !password) return json(res, { ok: false, error: "Email и пароль обязательны" }, 400); const data = readData(); const user = data.users.find(u => u.email === email && u.passwordHash === hashPassword(password)); if (!user) return json(res, { ok: false, error: "Неверный email или пароль" }, 401); const token = generateToken(user.id); data.sessions.push({ token, userId: user.id, expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString() }); writeData(data); json(res, { ok: true, token, user: { id: user.id, username: user.username, email: user.email, avatar: user.avatar } }); }, "POST /api/auth/logout": async (req, res) => { const token = req.headers.authorization?.split(" ")[1]; if (!token) return json(res, { ok: false, error: "Токен не предоставлен" }, 401); const data = readData(); data.sessions = data.sessions.filter(s => s.token !== token); writeData(data); json(res, { ok: true }); }, "GET /api/auth/me": async (req, res) => { const token = req.headers.authorization?.split(" ")[1]; if (!token) return json(res, { ok: false, error: "Не авторизован" }, 401); const user = verifyToken(token); if (!user) return json(res, { ok: false, error: "Сессия истекла" }, 401); json(res, { ok: true, user: { id: user.id, username: user.username, email: user.email, avatar: user.avatar, bio: user.bio } }); }, // ========== USERS ========== "GET /api/users": async (req, res) => { const token = req.headers.authorization?.split(" ")[1]; if (!token) return json(res, { ok: false, error: "Не авторизован" }, 401); const currentUser = verifyToken(token); if (!currentUser) return json(res, { ok: false, error: "Сессия истекла" }, 401); const data = readData(); const users = data.users.filter(u => u.id !== currentUser.id).map(u => ({ id: u.id, username: u.username, avatar: u.avatar })); json(res, { ok: true, users }); }, "PUT /api/users/me": async (req, res) => { const token = req.headers.authorization?.split(" ")[1]; if (!token) return json(res, { ok: false, error: "Не авторизован" }, 401); const currentUser = verifyToken(token); if (!currentUser) return json(res, { ok: false, error: "Сессия истекла" }, 401); const { username, bio, avatar } = await parseBody(req); const data = readData(); const idx = data.users.findIndex(u => u.id === currentUser.id); if (idx === -1) return json(res, { ok: false, error: "Пользователь не найден" }, 404); if (username) data.users[idx].username = username; if (bio !== undefined) data.users[idx].bio = bio; if (avatar) data.users[idx].avatar = avatar; writeData(data); json(res, { ok: true, user: data.users[idx] }); }, // ========== CHATS ========== "GET /api/chats": async (req, res) => { const token = req.headers.authorization?.split(" ")[1]; if (!token) return json(res, { ok: false, error: "Не авторизован" }, 401); const currentUser = verifyToken(token); if (!currentUser) return json(res, { ok: false, error: "Сессия истекла" }, 401); const data = readData(); const userChats = data.chats.filter(c => c.participants.includes(currentUser.id)); const chatsWithDetails = userChats.map(chat => { const otherUserId = chat.type === "private" ? chat.participants.find(p => p !== currentUser.id) : null; const otherUser = otherUserId ? data.users.find(u => u.id === otherUserId) : null; const lastMessage = data.messages.filter(m => m.chatId === chat.id).sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))[0]; return { ...chat, name: chat.type === "private" ? otherUser?.username : chat.name, avatar: chat.type === "private" ? otherUser?.avatar : chat.avatar, lastMessage: lastMessage ? { text: lastMessage.text, createdAt: lastMessage.createdAt } : null }; }); json(res, { ok: true, chats: chatsWithDetails }); }, "POST /api/chats": async (req, res) => { const token = req.headers.authorization?.split(" ")[1]; if (!token) return json(res, { ok: false, error: "Не авторизован" }, 401); const currentUser = verifyToken(token); if (!currentUser) return json(res, { ok: false, error: "Сессия истекла" }, 401); const { type, participantId, name } = await parseBody(req); const data = readData(); if (type === "private") { const existingChat = data.chats.find(c => c.type === "private" && c.participants.includes(currentUser.id) && c.participants.includes(participantId)); if (existingChat) return json(res, { ok: true, chat: existingChat }); const chat = { id: Date.now(), type: "private", participants: [currentUser.id, participantId], createdAt: new Date().toISOString() }; data.chats.push(chat); writeData(data); json(res, { ok: true, chat }); } else if (type === "group") { const chat = { id: Date.now(), type: "group", name: name || "Группа", participants: [currentUser.id], avatar: `https://api.dicebear.com/7.x/identicon/svg?seed=${Date.now()}`, createdAt: new Date().toISOString() }; data.chats.push(chat); writeData(data); json(res, { ok: true, chat }); } else if (type === "channel") { const channel = { id: Date.now(), type: "channel", name: name || "Канал", ownerId: currentUser.id, subscribers: [currentUser.id], avatar: `https://api.dicebear.com/7.x/identicon/svg?seed=${Date.now()}`, createdAt: new Date().toISOString() }; data.channels.push(channel); data.chats.push({ id: channel.id, type: "channel", name: channel.name, ownerId: channel.id, subscribers: [currentUser.id], avatar: channel.avatar, createdAt: channel.createdAt }); writeData(data); json(res, { ok: true, chat: channel }); } }, "GET /api/chats/:id/messages": async (req, res, params) => { const token = req.headers.authorization?.split(" ")[1]; if (!token) return json(res, { ok: false, error: "Не авторизован" }, 401); const currentUser = verifyToken(token); if (!currentUser) return json(res, { ok: false, error: "Сессия истекла" }, 401); const chatId = parseInt(params.id); const data = readData(); const chat = data.chats.find(c => c.id === chatId); if (!chat || !chat.participants.includes(currentUser.id)) return json(res, { ok: false, error: "Чат не найден" }, 404); const messages = data.messages.filter(m => m.chatId === chatId).sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt)); // E2E расшифровка const userKey = hashPassword(currentUser.email + currentUser.id); const decryptedMessages = messages.map(m => ({ ...m, text: decryptMessage(m.encrypted, userKey) })); json(res, { ok: true, messages: decryptedMessages }); }, "POST /api/chats/:id/messages": async (req, res, params) => { const token = req.headers.authorization?.split(" ")[1]; if (!token) return json(res, { ok: false, error: "Не авторизован" }, 401); const currentUser = verifyToken(token); if (!currentUser) return json(res, { ok: false, error: "Сессия истекла" }, 401); const chatId = parseInt(params.id); const { text, type = "text" } = await parseBody(req); const data = readData(); const chat = data.chats.find(c => c.id === chatId); if (!chat || !chat.participants.includes(currentUser.id)) return json(res, { ok: false, error: "Чат не найден" }, 404); // E2E шифрование const userKey = hashPassword(currentUser.email + currentUser.id); const encrypted = encryptMessage(text, userKey); const message = { id: Date.now(), chatId, senderId: currentUser.id, senderName: currentUser.username, senderAvatar: currentUser.avatar, text, encrypted, type, createdAt: new Date().toISOString() }; data.messages.push(message); writeData(data); json(res, { ok: true, message: { ...message, text } }); }, // ========== GROUPS ========== "POST /api/groups/:id/members": async (req, res, params) => { const token = req.headers.authorization?.split(" ")[1]; if (!token) return json(res, { ok: false, error: "Не авторизован" }, 401); const currentUser = verifyToken(token); if (!currentUser) return json(res, { ok: false, error: "Сессия истекла" }, 401); const groupId = parseInt(params.id); const { userId } = await parseBody(req); const data = readData(); const group = data.chats.find(c => c.id === groupId && c.type === "group"); if (!group || !group.participants.includes(currentUser.id)) return json(res, { ok: false, error: "Группа не найдена" }, 404); if (!group.participants.includes(userId)) { group.participants.push(userId); writeData(data); } json(res, { ok: true, group }); }, // ========== CHANNELS ========== "POST /api/channels/:id/subscribe": async (req, res, params) => { const token = req.headers.authorization?.split(" ")[1]; if (!token) return json(res, { ok: false, error: "Не авторизован" }, 401); const currentUser = verifyToken(token); if (!currentUser) return json(res, { ok: false, error: "Сессия истекла" }, 401); const channelId = parseInt(params.id); const data = readData(); const channel = data.channels.find(c => c.id === channelId); if (!channel) return json(res, { ok: false, error: "Канал не найден" }, 404); if (!channel.subscribers.includes(currentUser.id)) { channel.subscribers.push(currentUser.id); const chatIdx = data.chats.findIndex(c => c.id === channelId); if (chatIdx !== -1 && !data.chats[chatIdx].subscribers.includes(currentUser.id)) { data.chats[chatIdx].subscribers.push(currentUser.id); } writeData(data); } json(res, { ok: true, channel }); } }; const mimeTypes = { ".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8", ".js": "application/javascript; charset=utf-8", ".json": "application/json; charset=utf-8" }; http.createServer(async (req, res) => { const url = req.url.split("?")[0]; const method = req.method; res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); if (method === "OPTIONS") { res.writeHead(204); return res.end(); } // API ROUTING for (const [route, handler] of Object.entries(routes)) { const [m, p] = route.split(" "); if (method !== m) continue; const routeParts = p.split("/"); const urlParts = url.split("/"); if (routeParts.length !== urlParts.length) continue; const params = {}; let match = true; for (let i = 0; i < routeParts.length; i++) { if (routeParts[i].startsWith(":")) { params[routeParts[i].slice(1)] = urlParts[i]; } else if (routeParts[i] !== urlParts[i]) { match = false; break; } } if (match) { return handler(req, res, params); } } // STATIC FILES let file = url === "/" ? "/index.html" : url; const full = path.join(__dirname, file); if (full.startsWith(__dirname) && fs.existsSync(full) && fs.statSync(full).isFile()) { const ext = path.extname(full); const type = mimeTypes[ext] || "application/octet-stream"; res.writeHead(200, { "Content-Type": type }); return res.end(fs.readFileSync(full)); } notFound(res); }).listen(PORT, () => console.log("VibeChat server running on port " + PORT));