Files
min-chat/server/server.js
T
Комаров Данил Анатольевич 6 63bf73d2c4 Полный рефакторинг сервера и клиента, голосовые сообщения, аудио/видео, Docker
Сервер:
- Вынесена конфигурация в server/config.js (порт, размер файла из env)
- Вынесена аутентификация в server/auth.js (загрузка users.json, authenticate())
- Вынесено управление соединениями в server/clients.js (createJsonSender, broadcast, validateFile)
- server/server.js стал точкой входа — минимальный код

Клиент:
- Вынесены хуки: hooks/useWebSocket.js (WebSocket + auth), hooks/useChatMessages.js (сообщения + уведомления)
- Вынесены утилиты: utils/blob.js (base64 → Blob URL), utils/linkify.jsx (URL → ссылки), utils/notify.js (звук + favicon)

Новые функции:
- VoiceRecorder — запись голоса через MediaRecorder, отправка как файл
- Аудио/видео плеер в Message (audio/*, video/* с controls)
- URL linkification — http/https ссылки автоматически кликабельны
- Звуковое уведомление (Web Audio API) при сообщении на неактивной вкладке
- Красная точка на favicon при непрочитанных сообщениях

Инфраструктура:
- docker-entrypoint.sh: авто-перезапуск Node.js сервера при падении
- Обновлён README.md: новая структура проекта, список функций, примеры
- Кастомный тонкий скроллбар в Message.css
2026-05-29 05:23:22 +03:00

69 lines
2.3 KiB
JavaScript

const { WebSocketServer } = require('ws');
const config = require('./config');
const { authenticate } = require('./auth');
const { createJsonSender, createClientHandlers, validateFile, broadcast } = require('./clients');
const server = new WebSocketServer({ port: config.port });
console.log(`WebSocket server running on ws://0.0.0.0:${config.port}`);
server.on('connection', (ws, req) => {
const clientIp = req.socket.remoteAddress;
console.log(`Connection from ${clientIp}`);
const send = createJsonSender(ws);
const client = createClientHandlers(ws, send);
const handle = client.wrapHandler((parsed) => {
switch (parsed.type) {
case 'auth': {
const user = authenticate(parsed.login, parsed.password);
if (user) {
return client.handleAuth(user.login);
}
client.handleAuthFail('Invalid login or password');
return null;
}
case 'text': {
if (!parsed.text || typeof parsed.text !== 'string') {
send({ type: 'error', message: 'Invalid text message' });
return null;
}
console.log(`Text from ${client.getLogin()}: ${parsed.text.substring(0, 50)}`);
return { type: 'text', from: client.getLogin(), timestamp: Date.now(), text: parsed.text };
}
case 'file': {
const error = validateFile(parsed);
if (error) {
send({ type: 'error', message: error });
return null;
}
console.log(`File from ${client.getLogin()}: ${parsed.filename} (${parsed.mime})`);
return {
type: 'file', from: client.getLogin(), timestamp: Date.now(),
filename: parsed.filename, mime: parsed.mime, data: parsed.data,
};
}
default:
send({ type: 'error', message: 'Unknown message type' });
return null;
}
});
ws.on('message', (raw) => {
const msg = handle(raw);
if (msg) {
broadcast(server, msg, null);
}
});
ws.on('close', (code) => {
console.log(`Disconnected: ${client.getLogin() || 'unauthenticated'} (code: ${code})`);
if (client.getLogin()) {
broadcast(server, { type: 'system', text: `${client.getLogin()} left the chat` }, null);
}
});
ws.on('error', (err) => {
console.error(`Socket error (${client.getLogin() || 'unknown'}):`, err.message);
});
});