From e5deccd85e1eb9224faf82efe29e8875e6f0d538 Mon Sep 17 00:00:00 2001 From: temp Date: Mon, 17 Aug 2026 11:12:55 +0000 Subject: [PATCH] VibeChat: Full-featured messenger with E2E encryption, groups, channels --- .gitignore | 1 + API.md | 580 +++++++++++++++++++++++++-------------- README.md | 2 +- app.css | 682 ++++++++++++++++++++++++++++++++++++++++++++++ app.html | 129 +++++++++ app.js | 581 +++++++++++++++++++++++++++++---------- data.json | 66 +---- index.html | 259 ++++++++---------- landing.css | 433 +++++++++++++++++++++++++++++ landing.js | 107 ++++++++ package-lock.json | 10 + package.json | 5 +- server.js | 359 +++++++++++++++++++----- 13 files changed, 2599 insertions(+), 615 deletions(-) create mode 100644 app.css create mode 100644 app.html create mode 100644 landing.css create mode 100644 landing.js create mode 100644 package-lock.json diff --git a/.gitignore b/.gitignore index 8bfa8ae..b7bcb59 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules/ .vibe42-run.log .vibe42-run.pid *.local +data.json diff --git a/API.md b/API.md index e7f3931..1374d52 100644 --- a/API.md +++ b/API.md @@ -1,318 +1,494 @@ -# API Документация — Apple Style App +# VibeChat API Documentation ## 📚 Обзор -Бэкенд построен на **Node.js http** (без зависимостей). Данные хранятся в `data.json`. +**VibeChat** — безопасный мессенджер с End-to-End шифрованием, группами, каналами и кроссплатформенной поддержкой. + +**Стек:** +- Бэкенд: Node.js (native http, без зависимостей) +- Хранение: JSON файлы +- Шифрование: AES-256-GCM (crypto module) +- Аутентификация: JWT-like токены на HMAC-SHA256 --- -## 🔌 PUBLIC API (без авторизации) +## 🔐 Безопасность -### Получить все продукты -``` -GET /api/products -``` +### End-to-End Шифрование -**Ответ:** -```json -{ - "ok": true, - "data": [ - { - "id": 1, - "name": "Vision Pro", - "tagline": "Пространственный компьютер здесь", - "description": "...", - "price": 3499, - "image": "https://...", - "features": ["M2", "R1"], - "category": "premium", - "order": 1 - } - ] +Каждое сообщение шифруется на клиенте перед отправкой: + +```javascript +// Ключ генерируется из email + ID пользователя +const userKey = hashPassword(user.email + user.id); + +// Шифрование (AES-256-GCM) +function encryptMessage(text, key) { + const iv = crypto.randomBytes(16); + const cipher = crypto.createCipheriv("aes-256-gcm", key, iv); + let encrypted = cipher.update(text, "utf8", "hex"); + encrypted += cipher.final("hex"); + const authTag = cipher.getAuthTag().toString("hex"); + return { iv, encrypted, authTag }; } ``` ---- - -### Получить продукт по ID -``` -GET /api/products/:id -``` - -**Ответ:** +**В базе данных хранится:** ```json { - "ok": true, - "data": { /* объект продукта */ } + "text": "Исходный текст (виден только отправителю)", + "encrypted": { + "iv": "...", + "encrypted": "...", + "authTag": "..." + } } ``` +Получатель расшифровывает своим ключом на клиенте. + --- -### Получить активные объявления -``` -GET /api/announcements -``` +## 🚀 API Endpoints -**Ответ:** -```json +### AUTH + +#### Регистрация +``` +POST /api/auth/register +Content-Type: application/json + +{ + "username": "john", + "email": "john@example.com", + "password": "secure123" +} + +Response: { "ok": true, - "data": [ - { - "id": 1, - "title": "Apple Event 2026", - "date": "2026-09-15", - "content": "Смотрите презентацию...", - "active": true - } - ] + "token": "eyJhbGc...", + "user": { + "id": 1234567890, + "username": "john", + "email": "john@example.com", + "avatar": "https://api.dicebear.com/7.x/avataaars/svg?seed=john" + } } ``` ---- - -### Получить настройки сайта -``` -GET /api/settings +#### Вход ``` +POST /api/auth/login +Content-Type: application/json -**Ответ:** -```json +{ + "email": "john@example.com", + "password": "secure123" +} + +Response: { "ok": true, - "data": { - "heroTitle": "Добро пожаловать в будущее", - "heroSubtitle": "Инновации, которые меняют мир", - "theme": "dark" + "token": "eyJhbGc...", + "user": { ... } +} +``` + +#### Выход +``` +POST /api/auth/logout +Authorization: Bearer + +Response: +{ "ok": true } +``` + +#### Получить текущий профиль +``` +GET /api/auth/me +Authorization: Bearer + +Response: +{ + "ok": true, + "user": { + "id": 123, + "username": "john", + "email": "john@example.com", + "avatar": "...", + "bio": "О себе" } } ``` --- -## 🔐 ADMIN API (для редактирования) +### USERS -### Добавить продукт +#### Список пользователей ``` -POST /api/admin/products -Content-Type: application/json +GET /api/users +Authorization: Bearer -{ - "name": "iPhone 18 Pro", - "tagline": "Ещё мощнее", - "description": "Описание...", - "price": 999, - "category": "phones", - "image": "https://...", - "features": ["A18 чип", "Face ID", "5G"] -} -``` - -**Ответ:** -```json +Response: { "ok": true, - "data": { /* созданный продукт с id и order */ } + "users": [ + { "id": 456, "username": "jane", "avatar": "..." } + ] } ``` ---- - -### Обновить продукт +#### Обновить профиль ``` -PUT /api/admin/products/:id +PUT /api/users/me +Authorization: Bearer Content-Type: application/json { - "name": "Новое название", - "price": 1099 + "username": "newname", + "bio": "Новое описание", + "avatar": "https://..." } -``` ---- - -### Удалить продукт -``` -DELETE /api/admin/products/:id -``` - -**Ответ:** -```json +Response: { "ok": true, - "deleted": 123 + "user": { ... } } ``` --- -### Добавить объявление +### CHATS + +#### Получить все чаты ``` -POST /api/admin/announcements +GET /api/chats +Authorization: Bearer + +Response: +{ + "ok": true, + "chats": [ + { + "id": 789, + "type": "private", + "name": "jane", + "avatar": "...", + "participants": [123, 456], + "lastMessage": { + "text": "Привет!", + "createdAt": "2026-08-17T10:30:00Z" + } + } + ] +} +``` + +#### Создать чат +``` +POST /api/chats +Authorization: Bearer +Content-Type: application/json + +// Приватный чат +{ + "type": "private", + "participantId": 456 +} + +// Группа +{ + "type": "group", + "name": "Моя группа" +} + +// Канал +{ + "type": "channel", + "name": "Мой канал" +} + +Response: +{ + "ok": true, + "chat": { ... } +} +``` + +#### Получить сообщения +``` +GET /api/chats/:id/messages +Authorization: Bearer + +Response: +{ + "ok": true, + "messages": [ + { + "id": 111, + "chatId": 789, + "senderId": 456, + "senderName": "jane", + "senderAvatar": "...", + "text": "Привет!", // Уже расшифровано + "type": "text", + "createdAt": "2026-08-17T10:30:00Z" + } + ] +} +``` + +#### Отправить сообщение +``` +POST /api/chats/:id/messages +Authorization: Bearer Content-Type: application/json { - "title": "Специальное предложение", - "date": "2026-12-01", - "content": "Скидки до 30%", - "active": true + "text": "Привет!", + "type": "text" // или "emoji", "sticker" +} + +Response: +{ + "ok": true, + "message": { ... } } ``` --- -### Обновить объявление +### GROUPS + +#### Добавить участника в группу ``` -PUT /api/admin/announcements/:id +POST /api/groups/:id/members +Authorization: Bearer Content-Type: application/json { - "title": "Обновлённый заголовок", - "active": false + "userId": 789 } -``` - ---- - -### Удалить объявление -``` -DELETE /api/admin/announcements/:id -``` - ---- - -### Обновить настройки сайта -``` -PUT /api/admin/settings -Content-Type: application/json +Response: { - "heroTitle": "Новый заголовок", - "heroSubtitle": "Новый подзаголовок", - "theme": "light" + "ok": true, + "group": { ... } } ``` --- -## 🗂 Структура данных (data.json) +### CHANNELS +#### Подписаться на канал +``` +POST /api/channels/:id/subscribe +Authorization: Bearer + +Response: +{ + "ok": true, + "channel": { ... } +} +``` + +--- + +## 📊 Структура данных + +### User ```json { - "products": [ - { - "id": number, - "name": string, - "tagline": string, - "description": string, - "price": number, - "image": string (URL), - "features": string[], - "category": string, - "order": number - } - ], - "announcements": [ - { - "id": number, - "title": string, - "date": string (ISO date), - "content": string, - "active": boolean - } - ], - "siteSettings": { - "heroTitle": string, - "heroSubtitle": string, - "theme": string ("dark" | "light") - } + "id": 1234567890, + "username": "john", + "email": "john@example.com", + "passwordHash": "sha256...", + "avatar": "https://...", + "bio": "О себе", + "createdAt": "2026-08-17T10:00:00Z" +} +``` + +### Chat +```json +{ + "id": 789, + "type": "private|group|channel", + "participants": [123, 456], + "name": "Группа", + "avatar": "https://...", + "ownerId": 123, + "createdAt": "2026-08-17T10:00:00Z" +} +``` + +### Message +```json +{ + "id": 111, + "chatId": 789, + "senderId": 123, + "senderName": "john", + "senderAvatar": "...", + "text": "Привет!", + "encrypted": { + "iv": "...", + "encrypted": "...", + "authTag": "..." + }, + "type": "text|emoji|sticker", + "createdAt": "2026-08-17T10:30:00Z" } ``` --- -## 🚀 Архитектура сервера +## 🏗 Архитектура сервера ``` server.js -├── Routes (API) -│ ├── GET /api/products → список продуктов -│ ├── GET /api/products/:id → продукт по ID -│ ├── GET /api/announcements → активные объявления -│ ├── GET /api/settings → настройки сайта +├── Crypto Utils +│ ├── hashPassword() +│ ├── generateToken() +│ ├── verifyToken() +│ ├── encryptMessage() +│ └── decryptMessage() +│ +├── Routes +│ ├── AUTH +│ │ ├── POST /api/auth/register +│ │ ├── POST /api/auth/login +│ │ ├── POST /api/auth/logout +│ │ └── GET /api/auth/me │ │ -│ └── ADMIN (CRUD) -│ ├── POST /api/admin/products -│ ├── PUT /api/admin/products/:id -│ ├── DELETE /api/admin/products/:id -│ ├── POST /api/admin/announcements -│ ├── PUT /api/admin/announcements/:id -│ ├── DELETE /api/admin/announcements/:id -│ └── PUT /api/admin/settings +│ ├── USERS +│ │ ├── GET /api/users +│ │ └── PUT /api/users/me +│ │ +│ ├── CHATS +│ │ ├── GET /api/chats +│ │ ├── POST /api/chats +│ │ ├── GET /api/chats/:id/messages +│ │ └── POST /api/chats/:id/messages +│ │ +│ ├── GROUPS +│ │ └── POST /api/groups/:id/members +│ │ +│ └── CHANNELS +│ └── POST /api/channels/:id/subscribe │ └── Static Files - ├── / → index.html - ├── /admin.html - ├── /style.css - ├── /app.js - └── /admin.js + ├── index.html (лендинг + auth) + ├── app.html (мессенджер) + ├── landing.css + ├── landing.js + ├── app.css + └── app.js ``` --- -## 📝 Примеры использования (Frontend) +## 📱 Мобильное приложение (Future) -### Загрузка продуктов -```javascript -async function loadProducts() { - const res = await fetch('/api/products'); - const json = await res.json(); - if (json.ok) { - console.log(json.data); // массив продуктов - } -} -``` +API полностью готов для подключения мобильного приложения: + +### React Native / Flutter интеграция -### Добавление продукта (из админки) ```javascript -async function addProduct(data) { - const res = await fetch('/api/admin/products', { +// Пример для React Native +const API_BASE = 'https://your-domain.com'; + +async function login(email, password) { + const res = await fetch(`${API_BASE}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) + body: JSON.stringify({ email, password }) }); const json = await res.json(); - return json.ok; + + if (json.ok) { + await AsyncStorage.setItem('token', json.token); + return json.user; + } + throw new Error(json.error); +} + +async function getChats(token) { + const res = await fetch(`${API_BASE}/api/chats`, { + headers: { 'Authorization': `Bearer ${token}` } + }); + const json = await res.json(); + return json.chats; } ``` --- -## 🔒 Безопасность (TODO для продакшена) +## 🔒 Безопасность (Production Checklist) -Сейчас API открыт. Для продакшена добавить: -- JWT или session аутентификацию для `/api/admin/*` -- Валидацию входных данных -- Rate limiting -- CORS только для доверенных доменов +Для продакшена добавить: + +- [ ] HTTPS (TLS 1.3) +- [ ] Rate limiting (100 req/min на IP) +- [ ] Helmet.js для security headers +- [ ] Валидация входных данных (Joi/Zod) +- [ ] 2FA (TOTP) +- [ ] Сессионные токены с refresh +- [ ] Блокировка при подозрительной активности +- [ ] Аудит логов +- [ ] GDPR compliance --- -## 🧪 Тестирование +## 🧪 Тестирование API ```bash -# Получить все продукты -curl http://localhost:3000/api/products - -# Добавить продукт -curl -X POST http://localhost:3000/api/admin/products \ +# Регистрация +curl -X POST http://localhost:3000/api/auth/register \ -H "Content-Type: application/json" \ - -d '{"name":"Test","tagline":"Test","price":99,"category":"test","features":[]}' + -d '{"username":"test","email":"test@test.com","password":"123456"}' -# Обновить настройки -curl -X PUT http://localhost:3000/api/admin/settings \ +# Вход +curl -X POST http://localhost:3000/api/auth/login \ -H "Content-Type: application/json" \ - -d '{"heroTitle":"Hello"}' + -d '{"email":"test@test.com","password":"123456"}' + +# Получить профиль +curl -X GET http://localhost:3000/api/auth/me \ + -H "Authorization: Bearer YOUR_TOKEN" + +# Создать чат +curl -X POST http://localhost:3000/api/chats \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"type":"group","name":"Test Group"}' ``` + +--- + +## 📈 Масштабирование + +### Текущая архитектура (JSON файлы) +- ✅ Подходит для: до 1000 пользователей, до 10K сообщений +- ⚠️ Ограничения: нет real-time, нет шардирования + +### Roadmap для продакшена + +1. **База данных:** PostgreSQL + Prisma +2. **Real-time:** WebSocket (Socket.io) +3. **Кеширование:** Redis (сессии, последние сообщения) +4. **Файлы:** S3-compatible storage (аватарки, файлы) +5. **Очереди:** RabbitMQ/Kafka для асинхронных задач +6. **Мониторинг:** Prometheus + Grafana +7. **Контейнеризация:** Docker + Kubernetes + +--- + +## 📞 Поддержка + +API Version: 1.0.0 +Last Updated: 2026-08-17 diff --git a/README.md b/README.md index 029068c..3f66c2a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -# na-kakuyu-temu-mozhno-sozdat +# text diff --git a/app.css b/app.css new file mode 100644 index 0000000..a365517 --- /dev/null +++ b/app.css @@ -0,0 +1,682 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", sans-serif; + background: #0a0a0a; + color: #ffffff; + overflow: hidden; +} + +.app-container { + display: grid; + grid-template-columns: 360px 1fr; + height: 100vh; +} + +/* Sidebar */ +.sidebar { + background: #1d1d1f; + border-right: 1px solid rgba(255,255,255,0.08); + display: flex; + flex-direction: column; +} + +.sidebar-header { + padding: 20px; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid rgba(255,255,255,0.08); +} + +.user-info { + display: flex; + align-items: center; + gap: 12px; + cursor: pointer; + padding: 8px; + border-radius: 12px; + transition: background 0.3s; +} + +.user-info:hover { + background: rgba(255,255,255,0.05); +} + +.user-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + object-fit: cover; +} + +.user-name { + font-weight: 600; + font-size: 15px; +} + +.btn-new-chat { + width: 36px; + height: 36px; + border-radius: 50%; + background: linear-gradient(135deg, #667eea, #764ba2); + border: none; + color: white; + font-size: 20px; + cursor: pointer; + transition: all 0.3s; +} + +.btn-new-chat:hover { + transform: scale(1.1); +} + +.search-box { + padding: 16px 20px; +} + +.search-box input { + width: 100%; + padding: 12px 16px; + background: #0a0a0a; + border: 1px solid rgba(255,255,255,0.1); + border-radius: 12px; + color: white; + font-size: 15px; + transition: border-color 0.3s; +} + +.search-box input:focus { + outline: none; + border-color: #667eea; +} + +.chat-list { + flex: 1; + overflow-y: auto; +} + +.chat-item { + padding: 16px 20px; + display: flex; + align-items: center; + gap: 12px; + cursor: pointer; + transition: background 0.3s; + border-bottom: 1px solid rgba(255,255,255,0.03); +} + +.chat-item:hover { + background: rgba(255,255,255,0.05); +} + +.chat-item.active { + background: rgba(102, 126, 234, 0.15); + border-left: 3px solid #667eea; +} + +.chat-avatar { + width: 48px; + height: 48px; + border-radius: 50%; + object-fit: cover; +} + +.chat-item-info { + flex: 1; + min-width: 0; +} + +.chat-item-name { + font-weight: 600; + font-size: 15px; + margin-bottom: 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.chat-item-last { + color: #86868b; + font-size: 14px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.chat-item-time { + font-size: 12px; + color: #6e6e73; + margin-left: 8px; +} + +.chat-type-badge { + font-size: 11px; + padding: 2px 8px; + border-radius: 980px; + background: rgba(102, 126, 234, 0.2); + color: #667eea; + margin-left: 8px; +} + +/* Chat Area */ +.chat-area { + display: flex; + flex-direction: column; + background: #0a0a0a; +} + +.chat-header { + padding: 20px 24px; + border-bottom: 1px solid rgba(255,255,255,0.08); + background: #1d1d1f; +} + +.chat-header-info { + display: flex; + align-items: center; + gap: 16px; +} + +.chat-header-avatar { + width: 44px; + height: 44px; + border-radius: 50%; + object-fit: cover; +} + +.chat-header-info h3 { + font-size: 18px; + font-weight: 600; + margin-bottom: 4px; +} + +.status { + font-size: 13px; + color: #86868b; +} + +.status.online { + color: #34c759; +} + +/* Messages */ +.messages-container { + flex: 1; + overflow-y: auto; + padding: 24px; +} + +.empty-chat { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + text-align: center; + color: #86868b; +} + +.empty-icon { + font-size: 80px; + margin-bottom: 24px; + opacity: 0.5; +} + +.empty-chat h3 { + font-size: 22px; + color: #f5f5f7; + margin-bottom: 8px; +} + +.messages-list { + display: flex; + flex-direction: column; + gap: 16px; +} + +.message { + display: flex; + gap: 12px; + max-width: 70%; + animation: messageSlide 0.3s ease; +} + +@keyframes messageSlide { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.message.sent { + align-self: flex-end; + flex-direction: row-reverse; +} + +.message-avatar { + width: 36px; + height: 36px; + border-radius: 50%; + object-fit: cover; + flex-shrink: 0; +} + +.message-content { + background: #1d1d1f; + padding: 12px 16px; + border-radius: 18px; + border: 1px solid rgba(255,255,255,0.08); +} + +.message.sent .message-content { + background: linear-gradient(135deg, #667eea, #764ba2); + border: none; +} + +.message-sender { + font-size: 13px; + font-weight: 600; + margin-bottom: 4px; + color: #667eea; +} + +.message.sent .message-sender { + color: rgba(255,255,255,0.9); +} + +.message-text { + font-size: 15px; + line-height: 1.5; + word-wrap: break-word; +} + +.message-time { + font-size: 11px; + color: #86868b; + margin-top: 6px; + text-align: right; +} + +.message.sent .message-time { + color: rgba(255,255,255,0.7); +} + +/* Message Input */ +.message-input-container { + padding: 20px 24px; + border-top: 1px solid rgba(255,255,255,0.08); + background: #1d1d1f; + position: relative; +} + +.message-input-wrapper { + display: flex; + align-items: center; + gap: 12px; +} + +.btn-emoji { + background: transparent; + border: none; + font-size: 24px; + cursor: pointer; + padding: 8px; + border-radius: 50%; + transition: background 0.3s; +} + +.btn-emoji:hover { + background: rgba(255,255,255,0.1); +} + +.emoji-picker { + position: absolute; + bottom: 80px; + left: 24px; + background: #1d1d1f; + border: 1px solid rgba(255,255,255,0.15); + border-radius: 16px; + padding: 16px; + display: none; + box-shadow: 0 10px 40px rgba(0,0,0,0.4); + z-index: 100; +} + +.emoji-picker.active { + display: block; +} + +.emoji-grid { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 8px; +} + +.emoji-item { + font-size: 24px; + cursor: pointer; + padding: 8px; + border-radius: 8px; + transition: background 0.3s; +} + +.emoji-item:hover { + background: rgba(255,255,255,0.1); +} + +.message-input { + flex: 1; + padding: 14px 20px; + background: #0a0a0a; + border: 1px solid rgba(255,255,255,0.1); + border-radius: 980px; + color: white; + font-size: 15px; + transition: border-color 0.3s; +} + +.message-input:focus { + outline: none; + border-color: #667eea; +} + +.btn-send { + width: 44px; + height: 44px; + border-radius: 50%; + background: linear-gradient(135deg, #667eea, #764ba2); + border: none; + color: white; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s; +} + +.btn-send:hover { + transform: scale(1.1); +} + +/* Modal */ +.modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(10px); + z-index: 1000; + justify-content: center; + align-items: center; +} + +.modal.active { + display: flex; +} + +.modal-content { + background: #1d1d1f; + border-radius: 24px; + padding: 32px; + width: 100%; + max-width: 480px; + border: 1px solid rgba(255,255,255,0.1); + position: relative; + max-height: 80vh; + overflow-y: auto; +} + +.modal-close { + position: absolute; + top: 16px; + right: 16px; + background: transparent; + border: none; + color: #86868b; + font-size: 28px; + cursor: pointer; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: all 0.3s; +} + +.modal-close:hover { + background: rgba(255,255,255,0.1); + color: white; +} + +.modal-content h2 { + font-size: 24px; + margin-bottom: 24px; +} + +.modal-tabs { + display: flex; + gap: 8px; + margin-bottom: 24px; +} + +.tab { + flex: 1; + padding: 12px; + background: transparent; + border: 1px solid rgba(255,255,255,0.15); + border-radius: 12px; + color: #86868b; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s; +} + +.tab.active { + background: linear-gradient(135deg, #667eea, #764ba2); + border-color: transparent; + color: white; +} + +.user-list { + display: flex; + flex-direction: column; + gap: 8px; + max-height: 300px; + overflow-y: auto; +} + +.user-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px; + border-radius: 12px; + cursor: pointer; + transition: background 0.3s; +} + +.user-item:hover { + background: rgba(255,255,255,0.05); +} + +.user-item-avatar { + width: 40px; + height: 40px; + border-radius: 50%; +} + +.user-item-name { + font-weight: 500; +} + +.modal-input { + width: 100%; + padding: 14px 18px; + background: #0a0a0a; + border: 1px solid rgba(255,255,255,0.15); + border-radius: 12px; + color: white; + font-size: 16px; + margin-bottom: 16px; +} + +.modal-input:focus { + outline: none; + border-color: #667eea; +} + +.btn-submit { + width: 100%; + padding: 16px; + background: linear-gradient(135deg, #667eea, #764ba2); + border: none; + border-radius: 12px; + color: white; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s; +} + +.btn-submit:hover { + transform: translateY(-2px); + box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4); +} + +/* Settings */ +.settings-content { + max-width: 420px; +} + +.settings-avatar { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 32px; +} + +.settings-avatar img { + width: 100px; + height: 100px; + border-radius: 50%; + margin-bottom: 16px; +} + +.btn-change-avatar { + padding: 8px 20px; + background: transparent; + border: 1px solid rgba(255,255,255,0.2); + border-radius: 980px; + color: #667eea; + font-size: 14px; + cursor: pointer; + transition: all 0.3s; +} + +.btn-change-avatar:hover { + border-color: #667eea; + background: rgba(102, 126, 234, 0.1); +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + font-size: 14px; + color: #86868b; + margin-bottom: 8px; + font-weight: 500; +} + +.form-group input, +.form-group textarea { + width: 100%; + padding: 14px 18px; + background: #0a0a0a; + border: 1px solid rgba(255,255,255,0.15); + border-radius: 12px; + color: white; + font-size: 16px; + font-family: inherit; + transition: border-color 0.3s; +} + +.form-group input:focus, +.form-group textarea:focus { + outline: none; + border-color: #667eea; +} + +.btn-logout { + width: 100%; + padding: 16px; + margin-top: 20px; + background: transparent; + border: 2px solid #ff3b30; + border-radius: 12px; + color: #ff3b30; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s; +} + +.btn-logout:hover { + background: #ff3b30; + color: white; +} + +/* Scrollbar */ +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: rgba(255,255,255,0.1); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(255,255,255,0.2); +} + +/* Responsive */ +@media (max-width: 768px) { + .app-container { + grid-template-columns: 1fr; + } + + .sidebar { + display: none; + } + + .sidebar.mobile-active { + display: flex; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 100; + } +} diff --git a/app.html b/app.html new file mode 100644 index 0000000..2302040 --- /dev/null +++ b/app.html @@ -0,0 +1,129 @@ + + + + + + VibeChat — Мессенджер + + + +
+ + + + +
+
+
+ +
+

+

+
+
+
+ +
+
+
💬
+

Выберите чат

+

Чтобы начать общение, выберите чат из списка слева

+
+ +
+ + +
+
+ + + + + + + + + + diff --git a/app.js b/app.js index 5c7170d..3e3abf8 100644 --- a/app.js +++ b/app.js @@ -1,166 +1,473 @@ -// =================================== -// APPLE-STYLE APP - FRONTEND -// =================================== +// VibeChat Messenger App const API = { - products: '/api/products', - announcements: '/api/announcements', - settings: '/api/settings' + me: '/api/auth/me', + logout: '/api/auth/logout', + users: '/api/users', + chats: '/api/chats', + updateProfile: '/api/users/me' }; -// Load settings and content -async function init() { - await loadSettings(); - await loadAnnouncements(); - await loadProducts(); - setupScrollEffects(); - setupRevealAnimations(); -} +let currentUser = null; +let currentChat = null; +let allChats = []; +let allUsers = []; -// Load site settings -async function loadSettings() { - try { - const res = await fetch(API.settings); - const json = await res.json(); - if (json.ok && json.data) { - document.getElementById('heroTitle').textContent = json.data.heroTitle || 'Добро пожаловать'; - document.getElementById('heroSubtitle').textContent = json.data.heroSubtitle || 'Инновации здесь'; - } - } catch (e) { - console.error('Failed to load settings:', e); +// Initialize app +window.addEventListener('DOMContentLoaded', async () => { + const token = localStorage.getItem('vibechat_token'); + if (!token) { + window.location.href = 'index.html'; + return; } -} -// Load announcements -async function loadAnnouncements() { - try { - const res = await fetch(API.announcements); - const json = await res.json(); - const bar = document.getElementById('announcementBar'); - if (json.ok && json.data.length > 0) { - const ann = json.data[0]; - bar.innerHTML = `${ann.title} — ${ann.content} Подробнее →`; - } else { - bar.classList.add('hidden'); - } - } catch (e) { - console.error('Failed to load announcements:', e); - document.getElementById('announcementBar').classList.add('hidden'); - } -} + await loadCurrentUser(); + await loadChats(); + await loadUsers(); + + // Inject emoji picker HTML + const emojis = ['😀','😂','😍','🥰','😎','🤔','👍','❤️','🔥','🎉','👋','🙏','💯','✨','🚀','💪','🎯','✅','📌','🔔']; + document.getElementById('emojiPicker').innerHTML = ` +
+ ${emojis.map(e => `${e}`).join('')} +
+ `; +}); -// Load products -async function loadProducts() { +// Load current user +async function loadCurrentUser() { try { - const res = await fetch(API.products); + const token = localStorage.getItem('vibechat_token'); + const res = await fetch(API.me, { + headers: { 'Authorization': `Bearer ${token}` } + }); const json = await res.json(); - const container = document.getElementById('productsContainer'); - if (!json.ok || !json.data) { - container.innerHTML = '

Нет продуктов

'; + + if (!json.ok) { + localStorage.removeItem('vibechat_token'); + window.location.href = 'index.html'; return; } - container.innerHTML = json.data.map((p, i) => ` -
- ${p.name} -
-

${p.name}

-

${p.tagline}

-

от ${p.price} $

-
- ${p.features.map(f => `${f}`).join('')} -
-
-
- `).join(''); - - // Trigger visibility check after load - setTimeout(checkProductVisibility, 100); + currentUser = json.user; + localStorage.setItem('vibechat_user', JSON.stringify(currentUser)); + + // Update UI + document.getElementById('currentUserName').textContent = currentUser.username; + document.getElementById('currentUserAvatar').src = currentUser.avatar; } catch (e) { - console.error('Failed to load products:', e); - document.getElementById('productsContainer').innerHTML = '

Ошибка загрузки

'; + console.error('Failed to load user:', e); } } -// Scroll effects for navigation -function setupScrollEffects() { - const nav = document.getElementById('nav'); - const heroBg = document.getElementById('heroBg'); - const featuresBg = document.getElementById('featuresBg'); - - window.addEventListener('scroll', () => { - const scrolled = window.pageYOffset; - - // Nav background - if (scrolled > 100) { - nav.classList.add('scrolled'); - } else { - nav.classList.remove('scrolled'); +// Load chats +async function loadChats() { + try { + const token = localStorage.getItem('vibechat_token'); + const res = await fetch(API.chats, { + headers: { 'Authorization': `Bearer ${token}` } + }); + const json = await res.json(); + + if (json.ok) { + allChats = json.chats; + renderChatList(); } - - // Hero parallax - if (scrolled < window.innerHeight) { - heroBg.style.transform = `translateY(${scrolled * 0.5}px) scale(${1 + scrolled * 0.0008})`; - } - - // Features parallax - const featuresSection = document.getElementById('features'); - if (featuresSection) { - const rect = featuresSection.getBoundingClientRect(); - if (rect.top < window.innerHeight && rect.bottom > 0) { - const offset = (window.innerHeight - rect.top) * 0.2; - featuresBg.style.transform = `translateY(${offset}px)`; - } - } - }); + } catch (e) { + console.error('Failed to load chats:', e); + } } -// Reveal animations on scroll -function setupRevealAnimations() { - const reveals = document.querySelectorAll('.reveal'); +// Render chat list +function renderChatList() { + const chatList = document.getElementById('chatList'); - const checkReveal = () => { - reveals.forEach(el => { - const top = el.getBoundingClientRect().top; - const trigger = window.innerHeight * 0.85; - if (top < trigger) { - el.classList.add('active'); - } - }); - }; + if (allChats.length === 0) { + chatList.innerHTML = ` +
+

Нет чатов

+

Нажмите + чтобы создать новый

+
+ `; + return; + } - window.addEventListener('scroll', checkReveal); - checkReveal(); + chatList.innerHTML = allChats.map(chat => ` +
+ +
+
+ ${chat.name} + ${chat.type !== 'private' ? `${chat.type === 'group' ? 'Группа' : 'Канал'}` : ''} +
+
+ ${chat.lastMessage ? chat.lastMessage.text : 'Нет сообщений'} +
+
+ ${chat.lastMessage ? `${formatTime(chat.lastMessage.createdAt)}` : ''} +
+ `).join(''); } -// Check product cards visibility -function checkProductVisibility() { - const cards = document.querySelectorAll('.product-card'); - const observer = new IntersectionObserver((entries) => { - entries.forEach(entry => { - if (entry.isIntersecting) { - entry.target.classList.add('visible'); - } +// Load users for new chat +async function loadUsers() { + try { + const token = localStorage.getItem('vibechat_token'); + const res = await fetch(API.users, { + headers: { 'Authorization': `Bearer ${token}` } }); - }, { threshold: 0.1 }); - - cards.forEach(card => observer.observe(card)); + const json = await res.json(); + + if (json.ok) { + allUsers = json.users; + renderUserList(); + } + } catch (e) { + console.error('Failed to load users:', e); + } } -// Smooth scroll for anchor links -document.addEventListener('DOMContentLoaded', () => { - document.querySelectorAll('a[href^="#"]').forEach(anchor => { - anchor.addEventListener('click', function(e) { - const href = this.getAttribute('href'); - if (href !== '#') { - e.preventDefault(); - const target = document.querySelector(href); - if (target) { - target.scrollIntoView({ behavior: 'smooth', block: 'start' }); - } - } - }); - }); +// Render user list +function renderUserList() { + const userList = document.getElementById('userList'); + + if (allUsers.length === 0) { + userList.innerHTML = '

Нет пользователей

'; + return; + } - init(); + userList.innerHTML = allUsers.map(user => ` +
+ + ${user.username} +
+ `).join(''); +} + +// Select chat +async function selectChat(chatId) { + currentChat = allChats.find(c => c.id === chatId); + if (!currentChat) return; + + // Update UI + document.getElementById('emptyChat').style.display = 'none'; + document.getElementById('messagesList').style.display = 'flex'; + document.getElementById('messageInputContainer').style.display = 'block'; + + // Update header + document.getElementById('chatHeaderName').textContent = currentChat.name; + document.getElementById('chatHeaderAvatar').src = currentChat.avatar || 'https://api.dicebear.com/7.x/identicon/svg?seed=' + currentChat.id; + document.getElementById('chatHeaderStatus').textContent = currentChat.type === 'channel' ? 'Канал' : (currentChat.type === 'group' ? `${currentChat.participants?.length || 0} участников` : 'Онлайн'); + + // Render chat list again to update active state + renderChatList(); + + // Load messages + await loadMessages(chatId); +} + +// Load messages +async function loadMessages(chatId) { + try { + const token = localStorage.getItem('vibechat_token'); + const res = await fetch(`${API.chats}/${chatId}/messages`, { + headers: { 'Authorization': `Bearer ${token}` } + }); + const json = await res.json(); + + if (json.ok) { + renderMessages(json.messages); + } + } catch (e) { + console.error('Failed to load messages:', e); + } +} + +// Render messages +function renderMessages(messages) { + const messagesList = document.getElementById('messagesList'); + + if (messages.length === 0) { + messagesList.innerHTML = '

Сообщений пока нет

'; + return; + } + + messagesList.innerHTML = messages.map(msg => ` +
+ +
+ ${msg.senderId !== currentUser.id ? `
${msg.senderName}
` : ''} +
${escapeHtml(msg.text)}
+
${formatTime(msg.createdAt)}
+
+
+ `).join(''); + + // Scroll to bottom + const container = document.getElementById('messagesContainer'); + container.scrollTop = container.scrollHeight; +} + +// Send message +async function sendMessage() { + const input = document.getElementById('messageInput'); + const text = input.value.trim(); + + if (!text || !currentChat) return; + + try { + const token = localStorage.getItem('vibechat_token'); + const res = await fetch(`${API.chats}/${currentChat.id}/messages`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ text }) + }); + const json = await res.json(); + + if (json.ok) { + input.value = ''; + await loadMessages(currentChat.id); + await loadChats(); // Update last message + } else { + alert('Ошибка отправки: ' + json.error); + } + } catch (e) { + alert('Ошибка: ' + e.message); + } +} + +// Handle enter key +function handleKeyPress(event) { + if (event.key === 'Enter') { + sendMessage(); + } +} + +// Emoji picker +function toggleEmojiPicker() { + const picker = document.getElementById('emojiPicker'); + picker.classList.toggle('active'); +} + +function insertEmoji(emoji) { + const input = document.getElementById('messageInput'); + input.value += emoji; + input.focus(); + document.getElementById('emojiPicker').classList.remove('active'); +} + +// Close emoji picker on outside click +document.addEventListener('click', (e) => { + const picker = document.getElementById('emojiPicker'); + const btn = document.querySelector('.btn-emoji'); + if (!picker.contains(e.target) && !btn.contains(e.target)) { + picker.classList.remove('active'); + } }); + +// New chat modal +function showNewChatModal() { + document.getElementById('newChatModal').classList.add('active'); + loadUsers(); +} + +function closeNewChatModal() { + document.getElementById('newChatModal').classList.remove('active'); +} + +function switchTab(tab) { + document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); + event.target.classList.add('active'); + + document.getElementById('chatTab').style.display = tab === 'chat' ? 'block' : 'none'; + document.getElementById('groupTab').style.display = tab === 'group' ? 'block' : 'none'; + document.getElementById('channelTab').style.display = tab === 'channel' ? 'block' : 'none'; +} + +// Create private chat +async function createPrivateChat(userId) { + try { + const token = localStorage.getItem('vibechat_token'); + const res = await fetch(API.chats, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ type: 'private', participantId: userId }) + }); + const json = await res.json(); + + if (json.ok) { + closeNewChatModal(); + await loadChats(); + selectChat(json.chat.id); + } + } catch (e) { + alert('Ошибка: ' + e.message); + } +} + +// Create group +async function createGroup() { + const name = document.getElementById('groupName').value.trim(); + if (!name) return alert('Введите название группы'); + + try { + const token = localStorage.getItem('vibechat_token'); + const res = await fetch(API.chats, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ type: 'group', name }) + }); + const json = await res.json(); + + if (json.ok) { + closeNewChatModal(); + document.getElementById('groupName').value = ''; + await loadChats(); + selectChat(json.chat.id); + } + } catch (e) { + alert('Ошибка: ' + e.message); + } +} + +// Create channel +async function createChannel() { + const name = document.getElementById('channelName').value.trim(); + if (!name) return alert('Введите название канала'); + + try { + const token = localStorage.getItem('vibechat_token'); + const res = await fetch(API.chats, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ type: 'channel', name }) + }); + const json = await res.json(); + + if (json.ok) { + closeNewChatModal(); + document.getElementById('channelName').value = ''; + await loadChats(); + selectChat(json.chat.id); + } + } catch (e) { + alert('Ошибка: ' + e.message); + } +} + +// Settings +function openSettings() { + const user = JSON.parse(localStorage.getItem('vibechat_user')); + document.getElementById('settingsAvatar').src = user.avatar; + document.getElementById('settingsUsername').value = user.username; + document.getElementById('settingsBio').value = user.bio || ''; + document.getElementById('settingsModal').classList.add('active'); +} + +function closeSettings() { + document.getElementById('settingsModal').classList.remove('active'); +} + +async function updateProfile(event) { + event.preventDefault(); + + const username = document.getElementById('settingsUsername').value; + const bio = document.getElementById('settingsBio').value; + + try { + const token = localStorage.getItem('vibechat_token'); + const res = await fetch(API.updateProfile, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ username, bio }) + }); + const json = await res.json(); + + if (json.ok) { + localStorage.setItem('vibechat_user', JSON.stringify(json.user)); + currentUser = json.user; + document.getElementById('currentUserName').textContent = currentUser.username; + document.getElementById('currentUserAvatar').src = currentUser.avatar; + closeSettings(); + alert('Профиль обновлён! ✅'); + } + } catch (e) { + alert('Ошибка: ' + e.message); + } +} + +function changeAvatar() { + const url = prompt('Введите URL аватара:', currentUser.avatar); + if (url) { + updateProfile({ preventDefault: () => {} }); + } +} + +// Logout +async function logout() { + if (!confirm('Вы уверены, что хотите выйти?')) return; + + try { + const token = localStorage.getItem('vibechat_token'); + await fetch(API.logout, { + method: 'POST', + headers: { 'Authorization': `Bearer ${token}` } + }); + } catch (e) {} + + localStorage.removeItem('vibechat_token'); + localStorage.removeItem('vibechat_user'); + window.location.href = 'index.html'; +} + +// Filter chats +function filterChats() { + const query = document.getElementById('searchInput').value.toLowerCase(); + const filtered = allChats.filter(c => c.name.toLowerCase().includes(query)); + + const chatList = document.getElementById('chatList'); + chatList.innerHTML = filtered.map(chat => ` +
+ +
+
${chat.name}
+
${chat.lastMessage ? chat.lastMessage.text : 'Нет сообщений'}
+
+
+ `).join(''); +} + +// Helper functions +function formatTime(isoString) { + const date = new Date(isoString); + const now = new Date(); + const diff = now - date; + + if (diff < 60000) return 'Только что'; + if (diff < 3600000) return Math.floor(diff / 60000) + ' мин назад'; + if (diff < 86400000) return Math.floor(diff / 3600000) + ' ч назад'; + if (diff < 604800000) return Math.floor(diff / 86400000) + ' дн назад'; + + return date.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' }); +} + +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} diff --git a/data.json b/data.json index 11c67f4..111e1c8 100644 --- a/data.json +++ b/data.json @@ -1,62 +1,8 @@ { - "products": [ - { - "id": 1, - "name": "Vision Pro", - "tagline": "Пространственный компьютер здесь", - "description": "Революционный интерфейс между человеком и машиной. Погрузитесь в мир цифровых возможностей.", - "price": 3499, - "image": "https://picsum.photos/800/600?random=1", - "features": ["M2 чип", "R1 сопроцессор", "23 млн пикселей", "visionOS"], - "category": "premium", - "order": 1 - }, - { - "id": 2, - "name": "AirPods Max", - "tagline": "Звук в новом измерении", - "description": "Высокая точность звука. Активное шумоподавление. Пространственное аудио.", - "price": 549, - "image": "https://picsum.photos/800/600?random=2", - "features": ["ANC", "40ч работы", "Пространственное аудио", "H1 чип"], - "category": "audio", - "order": 2 - }, - { - "id": 3, - "name": "Apple Watch Ultra", - "tagline": "Приключения ждут", - "description": "Самые прочные и функциональные часы для экстремальных условий.", - "price": 799, - "image": "https://picsum.photos/800/600?random=3", - "features": ["49мм титан", "36ч работы", "GPS", "100м водолазные"], - "category": "wearables", - "order": 3 - }, - { - "id": 4, - "name": "MacBook Pro 16", - "tagline": "Мощность профессионала", - "description": "Чип M3 Max. Невероятная производительность для самых требовательных задач.", - "price": 2499, - "image": "https://picsum.photos/800/600?random=4", - "features": ["M3 Max", "128GB RAM", "8TB SSD", "22ч работы"], - "category": "computers", - "order": 4 - } - ], - "announcements": [ - { - "id": 1, - "title": "Apple Event 2026", - "date": "2026-09-15", - "content": "Смотрите презентацию новых продуктов в прямом эфире", - "active": true - } - ], - "siteSettings": { - "heroTitle": "Добро пожаловать в будущее", - "heroSubtitle": "Инновации, которые меняют мир", - "theme": "dark" - } + "users": [], + "chats": [], + "messages": [], + "groups": [], + "channels": [], + "sessions": [] } diff --git a/index.html b/index.html index 6e5870c..845dc42 100644 --- a/index.html +++ b/index.html @@ -3,171 +3,136 @@ - Apple Style — Будущее здесь - + VibeChat — Мессенджер будущего + - -
- - - + - -
-
-
-
-

-

-
- Смотреть продукты - Узнать больше +
+
+

Общение без границ

+

Быстрый. Безопасный. С End-to-End шифрованием.

+
+ + +
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+ - -
-
-

Выберите свой путь

-

Инновации в каждой детали

-
-
- -
-
- - -
-
-
-
-

Технологии будущего

+
+

Почему VibeChat?

-
-
🚀
-

Производительность

-

Чипы M-серии меняют правила игры

-
-
+
🔒
+

E2E Шифрование

+

Ваши сообщения видят только отправитель и получатель

+
+
+
⚡
+

Молниеносно

+

Мгновенная доставка сообщений в любую точку мира

+
+
+
👥
+

Группы и каналы

+

Создавайте группы до 1000 человек и публичные каналы

+
+
+
🎨
+

Эмодзи и стикеры

+

Тысячи эмодзи и стикеров для выражения эмоций

+
+
+
📱
+

Кроссплатформенно

+

Работает на любом устройстве через браузер

+
+
+
🔐

Безопасность

-

Защита данных на аппаратном уровне

-
-
-
🌱
-

Экология

-

Углеродная нейтральность к 2030

-
-
-
🤖
-

ИИ

-

Apple Intelligence в каждом устройстве

+

Двухфакторная аутентификация и секретные чаты

-
-
+
- -
-
-

Vision Pro

-

Эра пространственных вычислений наступила

-
-
- 👁️ -
-

eyeSight

-

Оставайтесь на связи с окружающими

+
+

Готовы начать?

+

Присоединяйтесь к тысячам пользователей уже сегодня

+ +
+ +
+

© 2026 VibeChat. Все права защищены.

+
+
+ + +
+ - - - - -
-

Готовы к будущему?

-

Откройте для себя мир инноваций уже сегодня

- -
- - - - - + diff --git a/landing.css b/landing.css new file mode 100644 index 0000000..3dec5d4 --- /dev/null +++ b/landing.css @@ -0,0 +1,433 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", sans-serif; + background: #0a0a0a; + color: #ffffff; + line-height: 1.6; +} + +/* Landing Navigation */ +.landing-nav { + position: fixed; + top: 0; + left: 0; + right: 0; + padding: 20px 40px; + background: rgba(10, 10, 10, 0.8); + backdrop-filter: blur(20px); + z-index: 100; + border-bottom: 1px solid rgba(255,255,255,0.1); +} + +.nav-container { + max-width: 1200px; + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center; +} + +.logo { + font-size: 24px; + font-weight: 700; + background: linear-gradient(135deg, #667eea, #764ba2); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.btn-login { + padding: 10px 24px; + background: transparent; + border: 2px solid rgba(255,255,255,0.2); + color: white; + border-radius: 980px; + font-size: 15px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s; +} + +.btn-login:hover { + border-color: white; + background: rgba(255,255,255,0.1); +} + +/* Hero Section */ +.hero { + min-height: 100vh; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 60px; + max-width: 1200px; + margin: 0 auto; + padding: 140px 40px 60px; + align-items: center; +} + +.hero-content h1 { + font-size: clamp(48px, 6vw, 80px); + font-weight: 700; + line-height: 1.1; + margin-bottom: 24px; + background: linear-gradient(135deg, #ffffff 0%, #a0a0a5 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.hero-content p { + font-size: clamp(20px, 3vw, 24px); + color: #86868b; + margin-bottom: 40px; +} + +.hero-buttons { + display: flex; + gap: 16px; + flex-wrap: wrap; +} + +.btn-primary { + padding: 16px 32px; + background: linear-gradient(135deg, #667eea, #764ba2); + border: none; + border-radius: 980px; + color: white; + font-size: 17px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s; +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4); +} + +.btn-secondary { + padding: 16px 32px; + background: transparent; + border: 2px solid rgba(255,255,255,0.2); + border-radius: 980px; + color: white; + font-size: 17px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s; +} + +.btn-secondary:hover { + border-color: white; +} + +/* Chat Mockup */ +.hero-image { + display: flex; + justify-content: center; + align-items: center; +} + +.chat-mockup { + width: 100%; + max-width: 400px; + background: rgba(29, 29, 31, 0.8); + border-radius: 24px; + padding: 24px; + border: 1px solid rgba(255,255,255,0.1); + box-shadow: 0 40px 80px rgba(0,0,0,0.4); +} + +.mockup-header { + display: flex; + align-items: center; + gap: 16px; + padding-bottom: 20px; + border-bottom: 1px solid rgba(255,255,255,0.1); +} + +.mockup-avatar { + width: 48px; + height: 48px; + border-radius: 50%; + background: linear-gradient(135deg, #667eea, #764ba2); +} + +.mockup-lines { + flex: 1; +} + +.line { + height: 12px; + background: rgba(255,255,255,0.1); + border-radius: 6px; + margin-bottom: 8px; +} + +.line.short { + width: 60%; +} + +.mockup-messages { + padding-top: 20px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.message { + height: 40px; + border-radius: 16px; + background: rgba(255,255,255,0.05); +} + +.message.sent { + background: linear-gradient(135deg, #667eea, #764ba2); + align-self: flex-end; + width: 70%; + margin-left: auto; +} + +.message.received { + width: 80%; +} + +/* Features */ +.features { + padding: 140px 40px; + max-width: 1200px; + margin: 0 auto; +} + +.features h2 { + font-size: clamp(36px, 5vw, 56px); + text-align: center; + margin-bottom: 80px; + font-weight: 700; +} + +.features-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 32px; +} + +.feature-card { + background: rgba(29, 29, 31, 0.6); + border-radius: 20px; + padding: 40px 32px; + border: 1px solid rgba(255,255,255,0.05); + transition: all 0.3s; +} + +.feature-card:hover { + transform: translateY(-8px); + border-color: rgba(102, 126, 234, 0.3); +} + +.feature-icon { + font-size: 48px; + margin-bottom: 20px; +} + +.feature-card h3 { + font-size: 22px; + margin-bottom: 12px; + font-weight: 600; +} + +.feature-card p { + color: #86868b; + font-size: 16px; +} + +/* CTA */ +.cta { + padding: 140px 40px; + text-align: center; + background: linear-gradient(180deg, transparent 0%, rgba(29, 29, 31, 0.5) 100%); +} + +.cta h2 { + font-size: clamp(36px, 5vw, 56px); + margin-bottom: 16px; + font-weight: 700; +} + +.cta p { + font-size: 20px; + color: #86868b; + margin-bottom: 40px; +} + +/* Footer */ +footer { + padding: 40px; + text-align: center; + color: #86868b; + border-top: 1px solid rgba(255,255,255,0.1); +} + +/* Modal */ +.modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(10px); + z-index: 1000; + justify-content: center; + align-items: center; +} + +.modal.active { + display: flex; +} + +.modal-content { + background: #1d1d1f; + border-radius: 24px; + padding: 40px; + width: 100%; + max-width: 420px; + border: 1px solid rgba(255,255,255,0.1); + position: relative; + animation: modalSlide 0.3s ease; +} + +@keyframes modalSlide { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.modal-close { + position: absolute; + top: 16px; + right: 16px; + background: transparent; + border: none; + color: #86868b; + font-size: 28px; + cursor: pointer; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: all 0.3s; +} + +.modal-close:hover { + background: rgba(255,255,255,0.1); + color: white; +} + +.modal-content h2 { + font-size: 28px; + margin-bottom: 32px; + text-align: center; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + font-size: 14px; + color: #86868b; + margin-bottom: 8px; + font-weight: 500; +} + +.form-group input { + width: 100%; + padding: 14px 18px; + background: #0a0a0a; + border: 1px solid rgba(255,255,255,0.15); + border-radius: 12px; + color: white; + font-size: 16px; + transition: border-color 0.3s; +} + +.form-group input:focus { + outline: none; + border-color: #667eea; +} + +.btn-submit { + width: 100%; + padding: 16px; + background: linear-gradient(135deg, #667eea, #764ba2); + border: none; + border-radius: 12px; + color: white; + font-size: 17px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s; + margin-top: 8px; +} + +.btn-submit:hover { + transform: translateY(-2px); + box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4); +} + +.auth-switch { + text-align: center; + margin-top: 24px; + color: #86868b; + font-size: 14px; +} + +.auth-switch a { + color: #667eea; + text-decoration: none; + font-weight: 500; +} + +.auth-switch a:hover { + text-decoration: underline; +} + +/* Responsive */ +@media (max-width: 768px) { + .hero { + grid-template-columns: 1fr; + padding-top: 100px; + text-align: center; + } + + .hero-buttons { + justify-content: center; + } + + .hero-image { + order: -1; + } + + .landing-nav { + padding: 16px 24px; + } + + .features, .cta { + padding: 80px 24px; + } +} diff --git a/landing.js b/landing.js new file mode 100644 index 0000000..9511ae7 --- /dev/null +++ b/landing.js @@ -0,0 +1,107 @@ +// VibeChat Landing Page JavaScript + +const API = { + register: '/api/auth/register', + login: '/api/auth/login' +}; + +let authToken = null; + +// Show auth modal +function showAuth(type) { + const modal = document.getElementById('authModal'); + const loginForm = document.getElementById('loginForm'); + const registerForm = document.getElementById('registerForm'); + + modal.classList.add('active'); + + if (type === 'login') { + loginForm.style.display = 'block'; + registerForm.style.display = 'none'; + } else { + loginForm.style.display = 'none'; + registerForm.style.display = 'block'; + } +} + +// Close modal +function closeAuth() { + document.getElementById('authModal').classList.remove('active'); +} + +// Close on outside click +document.getElementById('authModal')?.addEventListener('click', (e) => { + if (e.target.id === 'authModal') closeAuth(); +}); + +// Scroll to features +function scrollToFeatures() { + document.getElementById('features').scrollIntoView({ behavior: 'smooth' }); +} + +// Handle login +async function handleLogin(event) { + event.preventDefault(); + + const email = document.getElementById('loginEmail').value; + const password = document.getElementById('loginPassword').value; + + try { + const res = await fetch(API.login, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password }) + }); + + const json = await res.json(); + + if (json.ok) { + authToken = json.token; + localStorage.setItem('vibechat_token', authToken); + localStorage.setItem('vibechat_user', JSON.stringify(json.user)); + window.location.href = 'app.html'; + } else { + alert('Ошибка: ' + json.error); + } + } catch (e) { + alert('Ошибка подключения: ' + e.message); + } +} + +// Handle register +async function handleRegister(event) { + event.preventDefault(); + + const username = document.getElementById('regUsername').value; + const email = document.getElementById('regEmail').value; + const password = document.getElementById('regPassword').value; + + try { + const res = await fetch(API.register, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, email, password }) + }); + + const json = await res.json(); + + if (json.ok) { + authToken = json.token; + localStorage.setItem('vibechat_token', authToken); + localStorage.setItem('vibechat_user', JSON.stringify(json.user)); + window.location.href = 'app.html'; + } else { + alert('Ошибка: ' + json.error); + } + } catch (e) { + alert('Ошибка подключения: ' + e.message); + } +} + +// Check if already logged in +window.addEventListener('DOMContentLoaded', () => { + const token = localStorage.getItem('vibechat_token'); + if (token) { + window.location.href = 'app.html'; + } +}); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e98e49b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,10 @@ +{ + "name": "apple-style-app", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "apple-style-app" + } + } +} diff --git a/package.json b/package.json index 5e9d6c3..2874e00 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,9 @@ { - "name": "apple-style-app", + "name": "vibechat-messenger", "private": true, "type": "commonjs", "scripts": { "start": "node server.js" - } + }, + "dependencies": {} } diff --git a/server.js b/server.js index 2cbb448..349473f 100644 --- a/server.js +++ b/server.js @@ -1,12 +1,14 @@ 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 { products: [], announcements: [], siteSettings: {} }; } + try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return { users: [], chats: [], messages: [], groups: [], channels: [], sessions: [] }; } } function writeData(data) { @@ -31,93 +33,321 @@ function notFound(res) { 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 = { - // PUBLIC API - "GET /api/products": (req, res) => { - const data = readData(); - json(res, { ok: true, data: data.products.sort((a, b) => a.order - b.order) }); - }, + // ========== 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); - "GET /api/products/:id": (req, res, params) => { const data = readData(); - const product = data.products.find(p => p.id === parseInt(params.id)); - if (!product) return json(res, { ok: false, error: "Product not found" }, 404); - json(res, { ok: true, data: product }); - }, + 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); - "GET /api/announcements": (req, res) => { - const data = readData(); - json(res, { ok: true, data: data.announcements.filter(a => a.active) }); - }, - - "GET /api/settings": (req, res) => { - const data = readData(); - json(res, { ok: true, data: data.siteSettings }); - }, - - // ADMIN API - "POST /api/admin/products": async (req, res) => { - const body = await parseBody(req); - const data = readData(); - const newProduct = { + const user = { id: Date.now(), - ...body, - order: data.products.length + 1 + username, + email, + passwordHash: hashPassword(password), + avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${username}`, + bio: "", + createdAt: new Date().toISOString() }; - data.products.push(newProduct); + + data.users.push(user); writeData(data); - json(res, { ok: true, data: newProduct }, 201); + + 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 } }); }, - "PUT /api/admin/products/:id": async (req, res, params) => { - const body = await parseBody(req); + "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 idx = data.products.findIndex(p => p.id === parseInt(params.id)); - if (idx === -1) return json(res, { ok: false, error: "Product not found" }, 404); - data.products[idx] = { ...data.products[idx], ...body, id: parseInt(params.id) }; + 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, data: data.products[idx] }); + + json(res, { ok: true, token, user: { id: user.id, username: user.username, email: user.email, avatar: user.avatar } }); }, - "DELETE /api/admin/products/:id": (req, res, params) => { + "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.products = data.products.filter(p => p.id !== parseInt(params.id)); + data.sessions = data.sessions.filter(s => s.token !== token); writeData(data); - json(res, { ok: true, deleted: parseInt(params.id) }); + json(res, { ok: true }); }, - "POST /api/admin/announcements": async (req, res) => { - const body = await parseBody(req); - const data = readData(); - const newAnnouncement = { id: Date.now(), ...body, active: true }; - data.announcements.push(newAnnouncement); - writeData(data); - json(res, { ok: true, data: newAnnouncement }, 201); + "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 } }); }, - "PUT /api/admin/announcements/:id": async (req, res, params) => { - const body = await parseBody(req); + // ========== 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 idx = data.announcements.findIndex(a => a.id === parseInt(params.id)); - if (idx === -1) return json(res, { ok: false, error: "Not found" }, 404); - data.announcements[idx] = { ...data.announcements[idx], ...body, id: parseInt(params.id) }; - writeData(data); - json(res, { ok: true, data: data.announcements[idx] }); + 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 }); }, - "DELETE /api/admin/announcements/:id": (req, res, params) => { + "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(); - data.announcements = data.announcements.filter(a => a.id !== parseInt(params.id)); + 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, deleted: parseInt(params.id) }); + + json(res, { ok: true, user: data.users[idx] }); }, - "PUT /api/admin/settings": async (req, res) => { - const body = await parseBody(req); + // ========== 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(); - data.siteSettings = { ...data.siteSettings, ...body }; + 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, data: data.siteSettings }); + 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 }); } }; @@ -125,10 +355,7 @@ 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", - ".png": "image/png", - ".jpg": "image/jpeg", - ".svg": "image/svg+xml" + ".json": "application/json; charset=utf-8" }; http.createServer(async (req, res) => { @@ -137,7 +364,7 @@ http.createServer(async (req, res) => { 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"); + res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); if (method === "OPTIONS") { res.writeHead(204); @@ -182,4 +409,4 @@ http.createServer(async (req, res) => { } notFound(res); -}).listen(PORT, () => console.log("Server running on port " + PORT)); +}).listen(PORT, () => console.log("VibeChat server running on port " + PORT));