na-kakuyu-temu-mozhno-sozdat/server.js

413 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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));