VibeChat: Full-featured messenger with E2E encryption, groups, channels

This commit is contained in:
temp 2026-08-17 11:12:55 +00:00
parent 9ae6383d9d
commit e5deccd85e
13 changed files with 2599 additions and 615 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ node_modules/
.vibe42-run.log .vibe42-run.log
.vibe42-run.pid .vibe42-run.pid
*.local *.local
data.json

592
API.md
View File

@ -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 (без авторизации) ## 🔐 Безопасность
### Получить все продукты ### End-to-End Шифрование
```
GET /api/products Каждое сообщение шифруется на клиенте перед отправкой:
```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 };
}
``` ```
**Ответ:** **В базе данных хранится:**
```json ```json
{ {
"ok": true, "text": "Исходный текст (виден только отправителю)",
"data": [ "encrypted": {
"iv": "...",
"encrypted": "...",
"authTag": "..."
}
}
```
Получатель расшифровывает своим ключом на клиенте.
---
## 🚀 API Endpoints
### AUTH
#### Регистрация
```
POST /api/auth/register
Content-Type: application/json
{ {
"id": 1, "username": "john",
"name": "Vision Pro", "email": "john@example.com",
"tagline": "Пространственный компьютер здесь", "password": "secure123"
"description": "...", }
"price": 3499,
"image": "https://...", Response:
"features": ["M2", "R1"], {
"category": "premium", "ok": true,
"order": 1 "token": "eyJhbGc...",
"user": {
"id": 1234567890,
"username": "john",
"email": "john@example.com",
"avatar": "https://api.dicebear.com/7.x/avataaars/svg?seed=john"
}
}
```
#### Вход
```
POST /api/auth/login
Content-Type: application/json
{
"email": "john@example.com",
"password": "secure123"
}
Response:
{
"ok": true,
"token": "eyJhbGc...",
"user": { ... }
}
```
#### Выход
```
POST /api/auth/logout
Authorization: Bearer <token>
Response:
{ "ok": true }
```
#### Получить текущий профиль
```
GET /api/auth/me
Authorization: Bearer <token>
Response:
{
"ok": true,
"user": {
"id": 123,
"username": "john",
"email": "john@example.com",
"avatar": "...",
"bio": "О себе"
}
}
```
---
### USERS
#### Список пользователей
```
GET /api/users
Authorization: Bearer <token>
Response:
{
"ok": true,
"users": [
{ "id": 456, "username": "jane", "avatar": "..." }
]
}
```
#### Обновить профиль
```
PUT /api/users/me
Authorization: Bearer <token>
Content-Type: application/json
{
"username": "newname",
"bio": "Новое описание",
"avatar": "https://..."
}
Response:
{
"ok": true,
"user": { ... }
}
```
---
### CHATS
#### Получить все чаты
```
GET /api/chats
Authorization: Bearer <token>
Response:
{
"ok": true,
"chats": [
{
"id": 789,
"type": "private",
"name": "jane",
"avatar": "...",
"participants": [123, 456],
"lastMessage": {
"text": "Привет!",
"createdAt": "2026-08-17T10:30:00Z"
}
} }
] ]
} }
``` ```
--- #### Создать чат
### Получить продукт по ID
```
GET /api/products/:id
``` ```
POST /api/chats
Authorization: Bearer <token>
Content-Type: application/json
**Ответ:** // Приватный чат
```json {
"type": "private",
"participantId": 456
}
// Группа
{
"type": "group",
"name": "Моя группа"
}
// Канал
{
"type": "channel",
"name": "Мой канал"
}
Response:
{ {
"ok": true, "ok": true,
"data": { /* объект продукта */ } "chat": { ... }
} }
``` ```
--- #### Получить сообщения
### Получить активные объявления
```
GET /api/announcements
``` ```
GET /api/chats/:id/messages
Authorization: Bearer <token>
**Ответ:** Response:
```json
{ {
"ok": true, "ok": true,
"data": [ "messages": [
{ {
"id": 1, "id": 111,
"title": "Apple Event 2026", "chatId": 789,
"date": "2026-09-15", "senderId": 456,
"content": "Смотрите презентацию...", "senderName": "jane",
"active": true "senderAvatar": "...",
"text": "Привет!", // Уже расшифровано
"type": "text",
"createdAt": "2026-08-17T10:30:00Z"
} }
] ]
} }
``` ```
--- #### Отправить сообщение
### Получить настройки сайта
```
GET /api/settings
``` ```
POST /api/chats/:id/messages
Authorization: Bearer <token>
Content-Type: application/json
**Ответ:** {
```json "text": "Привет!",
"type": "text" // или "emoji", "sticker"
}
Response:
{ {
"ok": true, "ok": true,
"data": { "message": { ... }
"heroTitle": "Добро пожаловать в будущее",
"heroSubtitle": "Инновации, которые меняют мир",
"theme": "dark"
}
} }
``` ```
--- ---
## 🔐 ADMIN API (для редактирования) ### GROUPS
### Добавить продукт #### Добавить участника в группу
``` ```
POST /api/admin/products POST /api/groups/:id/members
Authorization: Bearer <token>
Content-Type: application/json Content-Type: application/json
{ {
"name": "iPhone 18 Pro", "userId": 789
"tagline": "Ещё мощнее",
"description": "Описание...",
"price": 999,
"category": "phones",
"image": "https://...",
"features": ["A18 чип", "Face ID", "5G"]
} }
```
**Ответ:** Response:
```json
{ {
"ok": true, "ok": true,
"data": { /* созданный продукт с id и order */ } "group": { ... }
} }
``` ```
--- ---
### Обновить продукт ### CHANNELS
```
PUT /api/admin/products/:id
Content-Type: application/json
{ #### Подписаться на канал
"name": "Новое название",
"price": 1099
}
``` ```
POST /api/channels/:id/subscribe
Authorization: Bearer <token>
--- Response:
### Удалить продукт
```
DELETE /api/admin/products/:id
```
**Ответ:**
```json
{ {
"ok": true, "ok": true,
"deleted": 123 "channel": { ... }
} }
``` ```
--- ---
### Добавить объявление ## 📊 Структура данных
```
POST /api/admin/announcements
Content-Type: application/json
{
"title": "Специальное предложение",
"date": "2026-12-01",
"content": "Скидки до 30%",
"active": true
}
```
---
### Обновить объявление
```
PUT /api/admin/announcements/:id
Content-Type: application/json
{
"title": "Обновлённый заголовок",
"active": false
}
```
---
### Удалить объявление
```
DELETE /api/admin/announcements/:id
```
---
### Обновить настройки сайта
```
PUT /api/admin/settings
Content-Type: application/json
{
"heroTitle": "Новый заголовок",
"heroSubtitle": "Новый подзаголовок",
"theme": "light"
}
```
---
## 🗂 Структура данных (data.json)
### User
```json ```json
{ {
"products": [ "id": 1234567890,
"username": "john",
"email": "john@example.com",
"passwordHash": "sha256...",
"avatar": "https://...",
"bio": "О себе",
"createdAt": "2026-08-17T10:00:00Z"
}
```
### Chat
```json
{ {
"id": number, "id": 789,
"name": string, "type": "private|group|channel",
"tagline": string, "participants": [123, 456],
"description": string, "name": "Группа",
"price": number, "avatar": "https://...",
"image": string (URL), "ownerId": 123,
"features": string[], "createdAt": "2026-08-17T10:00:00Z"
"category": string,
"order": number
} }
], ```
"announcements": [
### Message
```json
{ {
"id": number, "id": 111,
"title": string, "chatId": 789,
"date": string (ISO date), "senderId": 123,
"content": string, "senderName": "john",
"active": boolean "senderAvatar": "...",
} "text": "Привет!",
], "encrypted": {
"siteSettings": { "iv": "...",
"heroTitle": string, "encrypted": "...",
"heroSubtitle": string, "authTag": "..."
"theme": string ("dark" | "light") },
} "type": "text|emoji|sticker",
"createdAt": "2026-08-17T10:30:00Z"
} }
``` ```
--- ---
## 🚀 Архитектура сервера ## 🏗 Архитектура сервера
``` ```
server.js server.js
├── Routes (API) ├── Crypto Utils
│ ├── GET /api/products → список продуктов │ ├── hashPassword()
│ ├── GET /api/products/:id → продукт по ID │ ├── generateToken()
│ ├── GET /api/announcements → активные объявления │ ├── verifyToken()
│ ├── GET /api/settings → настройки сайта │ ├── encryptMessage()
│ └── decryptMessage()
│
├── Routes
│ ├── AUTH
│ │ ├── POST /api/auth/register
│ │ ├── POST /api/auth/login
│ │ ├── POST /api/auth/logout
│ │ └── GET /api/auth/me
│ │ │ │
│ └── ADMIN (CRUD) │ ├── USERS
│ ├── POST /api/admin/products │ │ ├── GET /api/users
│ ├── PUT /api/admin/products/:id │ │ └── PUT /api/users/me
│ ├── DELETE /api/admin/products/:id │ │
│ ├── POST /api/admin/announcements │ ├── CHATS
│ ├── PUT /api/admin/announcements/:id │ │ ├── GET /api/chats
│ ├── DELETE /api/admin/announcements/:id │ │ ├── POST /api/chats
│ └── PUT /api/admin/settings │ │ ├── GET /api/chats/:id/messages
│ │ └── POST /api/chats/:id/messages
│ │
│ ├── GROUPS
│ │ └── POST /api/groups/:id/members
│ │
│ └── CHANNELS
│ └── POST /api/channels/:id/subscribe
│ │
└── Static Files └── Static Files
├── / → index.html ├── index.html (лендинг + auth)
├── /admin.html ├── app.html (мессенджер)
├── /style.css ├── landing.css
├── /app.js ├── landing.js
└── /admin.js ├── app.css
└── app.js
``` ```
--- ---
## 📝 Примеры использования (Frontend) ## 📱 Мобильное приложение (Future)
### Загрузка продуктов API полностью готов для подключения мобильного приложения:
```javascript
async function loadProducts() { ### React Native / Flutter интеграция
const res = await fetch('/api/products');
const json = await res.json();
if (json.ok) {
console.log(json.data); // массив продуктов
}
}
```
### Добавление продукта (из админки)
```javascript ```javascript
async function addProduct(data) { // Пример для React Native
const res = await fetch('/api/admin/products', { const API_BASE = 'https://your-domain.com';
async function login(email, password) {
const res = await fetch(`${API_BASE}/api/auth/login`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data) body: JSON.stringify({ email, password })
}); });
const json = await res.json(); 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/*`
- Валидацию входных данных - [ ] HTTPS (TLS 1.3)
- Rate limiting - [ ] Rate limiting (100 req/min на IP)
- CORS только для доверенных доменов - [ ] Helmet.js для security headers
- [ ] Валидация входных данных (Joi/Zod)
- [ ] 2FA (TOTP)
- [ ] Сессионные токены с refresh
- [ ] Блокировка при подозрительной активности
- [ ] Аудит логов
- [ ] GDPR compliance
--- ---
## 🧪 Тестирование ## 🧪 Тестирование API
```bash ```bash
# Получить все продукты # Регистрация
curl http://localhost:3000/api/products curl -X POST http://localhost:3000/api/auth/register \
# Добавить продукт
curl -X POST http://localhost:3000/api/admin/products \
-H "Content-Type: application/json" \ -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" \ -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

View File

@ -1,2 +1,2 @@
# na-kakuyu-temu-mozhno-sozdat # text

682
app.css Normal file
View File

@ -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;
}
}

129
app.html Normal file
View File

@ -0,0 +1,129 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VibeChat — Мессенджер</title>
<link rel="stylesheet" href="app.css">
</head>
<body>
<div class="app-container">
<!-- Sidebar -->
<aside class="sidebar">
<div class="sidebar-header">
<div class="user-info" onclick="openSettings()">
<img src="" alt="" class="user-avatar" id="currentUserAvatar">
<span class="user-name" id="currentUserName"></span>
</div>
<button class="btn-new-chat" onclick="showNewChatModal()">+</button>
</div>
<div class="search-box">
<input type="text" placeholder="Поиск" id="searchInput" oninput="filterChats()">
</div>
<div class="chat-list" id="chatList">
<!-- Chats will be loaded here -->
</div>
</aside>
<!-- Main Chat Area -->
<main class="chat-area">
<div class="chat-header" id="chatHeader">
<div class="chat-header-info">
<img src="" alt="" class="chat-header-avatar" id="chatHeaderAvatar">
<div>
<h3 id="chatHeaderName"></h3>
<p id="chatHeaderStatus" class="status"></p>
</div>
</div>
</div>
<div class="messages-container" id="messagesContainer">
<div class="empty-chat" id="emptyChat">
<div class="empty-icon">💬</div>
<h3>Выберите чат</h3>
<p>Чтобы начать общение, выберите чат из списка слева</p>
</div>
<div class="messages-list" id="messagesList" style="display:none">
<!-- Messages will be loaded here -->
</div>
</div>
<div class="message-input-container" id="messageInputContainer" style="display:none">
<button class="btn-emoji" onclick="toggleEmojiPicker()">😀</button>
<div class="emoji-picker" id="emojiPicker">
<div class="emoji-grid">
${['😀','😂','😍','🥰','😎','🤔','👍','❤️','🔥','🎉','👋','🙏','💯','✨','🚀','💪','🎯','✅','📌','🔔'].map(e => `<span class="emoji-item" onclick="insertEmoji('${e}')">${e}</span>`).join('')}
</div>
</div>
<input type="text" class="message-input" id="messageInput" placeholder="Напишите сообщение..." onkeypress="handleKeyPress(event)">
<button class="btn-send" onclick="sendMessage()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
</svg>
</button>
</div>
</main>
</div>
<!-- New Chat Modal -->
<div id="newChatModal" class="modal">
<div class="modal-content">
<button class="modal-close" onclick="closeNewChatModal()">×</button>
<h2>Новый чат</h2>
<div class="modal-tabs">
<button class="tab active" onclick="switchTab('chat')">Чат</button>
<button class="tab" onclick="switchTab('group')">Группа</button>
<button class="tab" onclick="switchTab('channel')">Канал</button>
</div>
<div id="chatTab">
<div class="user-list" id="userList">
<!-- Users will be loaded here -->
</div>
</div>
<div id="groupTab" style="display:none">
<input type="text" id="groupName" placeholder="Название группы" class="modal-input">
<button class="btn-submit" onclick="createGroup()">Создать группу</button>
</div>
<div id="channelTab" style="display:none">
<input type="text" id="channelName" placeholder="Название канала" class="modal-input">
<button class="btn-submit" onclick="createChannel()">Создать канал</button>
</div>
</div>
</div>
<!-- Settings Modal -->
<div id="settingsModal" class="modal">
<div class="modal-content settings-content">
<button class="modal-close" onclick="closeSettings()">×</button>
<h2>Настройки</h2>
<div class="settings-avatar">
<img src="" alt="" id="settingsAvatar">
<button class="btn-change-avatar" onclick="changeAvatar()">Изменить</button>
</div>
<form onsubmit="updateProfile(event)">
<div class="form-group">
<label>Имя пользователя</label>
<input type="text" id="settingsUsername" required>
</div>
<div class="form-group">
<label>О себе</label>
<textarea id="settingsBio" rows="3" placeholder="Расскажите о себе"></textarea>
</div>
<button type="submit" class="btn-submit">Сохранить</button>
</form>
<button class="btn-logout" onclick="logout()">Выйти из аккаунта</button>
</div>
</div>
<script src="app.js"></script>
</body>
</html>

561
app.js
View File

@ -1,166 +1,473 @@
// =================================== // VibeChat Messenger App
// APPLE-STYLE APP - FRONTEND
// ===================================
const API = { const API = {
products: '/api/products', me: '/api/auth/me',
announcements: '/api/announcements', logout: '/api/auth/logout',
settings: '/api/settings' users: '/api/users',
chats: '/api/chats',
updateProfile: '/api/users/me'
}; };
// Load settings and content let currentUser = null;
async function init() { let currentChat = null;
await loadSettings(); let allChats = [];
await loadAnnouncements(); let allUsers = [];
await loadProducts();
setupScrollEffects();
setupRevealAnimations();
}
// Load site settings // Initialize app
async function loadSettings() { window.addEventListener('DOMContentLoaded', async () => {
try { const token = localStorage.getItem('vibechat_token');
const res = await fetch(API.settings); if (!token) {
const json = await res.json(); window.location.href = 'index.html';
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);
}
}
// 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} <a href="#">Подробнее →</a>`;
} else {
bar.classList.add('hidden');
}
} catch (e) {
console.error('Failed to load announcements:', e);
document.getElementById('announcementBar').classList.add('hidden');
}
}
// Load products
async function loadProducts() {
try {
const res = await fetch(API.products);
const json = await res.json();
const container = document.getElementById('productsContainer');
if (!json.ok || !json.data) {
container.innerHTML = '<p style="text-align:center;color:var(--text-secondary)">Нет продуктов</p>';
return; return;
} }
container.innerHTML = json.data.map((p, i) => ` await loadCurrentUser();
<div class="product-card" style="transition-delay: ${i * 100}ms"> await loadChats();
<img src="${p.image}" alt="${p.name}" class="product-image"> await loadUsers();
<div class="product-content">
<h3 class="product-name">${p.name}</h3> // Inject emoji picker HTML
<p class="product-tagline">${p.tagline}</p> const emojis = ['😀','😂','😍','🥰','😎','🤔','👍','❤️','🔥','🎉','👋','🙏','💯','✨','🚀','💪','🎯','✅','📌','🔔'];
<p class="product-price">от ${p.price} $</p> document.getElementById('emojiPicker').innerHTML = `
<div class="product-features"> <div class="emoji-grid">
${p.features.map(f => `<span class="feature-tag">${f}</span>`).join('')} ${emojis.map(e => `<span class="emoji-item" onclick="insertEmoji('${e}')">${e}</span>`).join('')}
</div> </div>
`;
});
// Load current user
async function loadCurrentUser() {
try {
const token = localStorage.getItem('vibechat_token');
const res = await fetch(API.me, {
headers: { 'Authorization': `Bearer ${token}` }
});
const json = await res.json();
if (!json.ok) {
localStorage.removeItem('vibechat_token');
window.location.href = 'index.html';
return;
}
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 user:', e);
}
}
// 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();
}
} catch (e) {
console.error('Failed to load chats:', e);
}
}
// Render chat list
function renderChatList() {
const chatList = document.getElementById('chatList');
if (allChats.length === 0) {
chatList.innerHTML = `
<div style="padding:40px 20px;text-align:center;color:#86868b">
<p>Нет чатов</p>
<p style="font-size:13px;margin-top:8px">Нажмите + чтобы создать новый</p>
</div>
`;
return;
}
chatList.innerHTML = allChats.map(chat => `
<div class="chat-item ${currentChat?.id === chat.id ? 'active' : ''}" onclick="selectChat(${chat.id})">
<img src="${chat.avatar || 'https://api.dicebear.com/7.x/identicon/svg?seed=' + chat.id}" alt="" class="chat-avatar">
<div class="chat-item-info">
<div class="chat-item-name">
${chat.name}
${chat.type !== 'private' ? `<span class="chat-type-badge">${chat.type === 'group' ? 'Группа' : 'Канал'}</span>` : ''}
</div>
<div class="chat-item-last">
${chat.lastMessage ? chat.lastMessage.text : 'Нет сообщений'}
</div>
</div>
${chat.lastMessage ? `<span class="chat-item-time">${formatTime(chat.lastMessage.createdAt)}</span>` : ''}
</div>
`).join('');
}
// 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}` }
});
const json = await res.json();
if (json.ok) {
allUsers = json.users;
renderUserList();
}
} catch (e) {
console.error('Failed to load users:', e);
}
}
// Render user list
function renderUserList() {
const userList = document.getElementById('userList');
if (allUsers.length === 0) {
userList.innerHTML = '<p style="padding:20px;text-align:center;color:#86868b">Нет пользователей</p>';
return;
}
userList.innerHTML = allUsers.map(user => `
<div class="user-item" onclick="createPrivateChat(${user.id})">
<img src="${user.avatar}" alt="" class="user-item-avatar">
<span class="user-item-name">${user.username}</span>
</div>
`).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 = '<p style="text-align:center;color:#86868b;padding:40px">Сообщений пока нет</p>';
return;
}
messagesList.innerHTML = messages.map(msg => `
<div class="message ${msg.senderId === currentUser.id ? 'sent' : ''}">
<img src="${msg.senderAvatar}" alt="" class="message-avatar">
<div class="message-content">
${msg.senderId !== currentUser.id ? `<div class="message-sender">${msg.senderName}</div>` : ''}
<div class="message-text">${escapeHtml(msg.text)}</div>
<div class="message-time">${formatTime(msg.createdAt)}</div>
</div> </div>
</div> </div>
`).join(''); `).join('');
// Trigger visibility check after load // Scroll to bottom
setTimeout(checkProductVisibility, 100); const container = document.getElementById('messagesContainer');
} catch (e) { container.scrollTop = container.scrollHeight;
console.error('Failed to load products:', e);
document.getElementById('productsContainer').innerHTML = '<p style="text-align:center;color:var(--text-secondary)">Ошибка загрузки</p>';
}
} }
// Scroll effects for navigation // Send message
function setupScrollEffects() { async function sendMessage() {
const nav = document.getElementById('nav'); const input = document.getElementById('messageInput');
const heroBg = document.getElementById('heroBg'); const text = input.value.trim();
const featuresBg = document.getElementById('featuresBg');
window.addEventListener('scroll', () => { if (!text || !currentChat) return;
const scrolled = window.pageYOffset;
// Nav background try {
if (scrolled > 100) { const token = localStorage.getItem('vibechat_token');
nav.classList.add('scrolled'); 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 { } else {
nav.classList.remove('scrolled'); alert('Ошибка отправки: ' + json.error);
}
} catch (e) {
alert('Ошибка: ' + e.message);
}
} }
// Hero parallax // Handle enter key
if (scrolled < window.innerHeight) { function handleKeyPress(event) {
heroBg.style.transform = `translateY(${scrolled * 0.5}px) scale(${1 + scrolled * 0.0008})`; if (event.key === 'Enter') {
sendMessage();
}
} }
// Features parallax // Emoji picker
const featuresSection = document.getElementById('features'); function toggleEmojiPicker() {
if (featuresSection) { const picker = document.getElementById('emojiPicker');
const rect = featuresSection.getBoundingClientRect(); picker.classList.toggle('active');
if (rect.top < window.innerHeight && rect.bottom > 0) {
const offset = (window.innerHeight - rect.top) * 0.2;
featuresBg.style.transform = `translateY(${offset}px)`;
} }
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();
} }
// Reveal animations on scroll function closeNewChatModal() {
function setupRevealAnimations() { document.getElementById('newChatModal').classList.remove('active');
const reveals = document.querySelectorAll('.reveal');
const checkReveal = () => {
reveals.forEach(el => {
const top = el.getBoundingClientRect().top;
const trigger = window.innerHeight * 0.85;
if (top < trigger) {
el.classList.add('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();
window.addEventListener('scroll', checkReveal); if (json.ok) {
checkReveal(); closeNewChatModal();
await loadChats();
selectChat(json.chat.id);
}
} catch (e) {
alert('Ошибка: ' + e.message);
}
} }
// Check product cards visibility // Create group
function checkProductVisibility() { async function createGroup() {
const cards = document.querySelectorAll('.product-card'); const name = document.getElementById('groupName').value.trim();
const observer = new IntersectionObserver((entries) => { if (!name) return alert('Введите название группы');
entries.forEach(entry => {
if (entry.isIntersecting) { try {
entry.target.classList.add('visible'); 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 })
}); });
}, { threshold: 0.1 }); const json = await res.json();
cards.forEach(card => observer.observe(card)); if (json.ok) {
closeNewChatModal();
document.getElementById('groupName').value = '';
await loadChats();
selectChat(json.chat.id);
} }
} catch (e) {
// Smooth scroll for anchor links alert('Ошибка: ' + e.message);
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' });
} }
} }
});
});
init(); // 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 => `
<div class="chat-item ${currentChat?.id === chat.id ? 'active' : ''}" onclick="selectChat(${chat.id})">
<img src="${chat.avatar || 'https://api.dicebear.com/7.x/identicon/svg?seed=' + chat.id}" alt="" class="chat-avatar">
<div class="chat-item-info">
<div class="chat-item-name">${chat.name}</div>
<div class="chat-item-last">${chat.lastMessage ? chat.lastMessage.text : 'Нет сообщений'}</div>
</div>
</div>
`).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;
}

View File

@ -1,62 +1,8 @@
{ {
"products": [ "users": [],
{ "chats": [],
"id": 1, "messages": [],
"name": "Vision Pro", "groups": [],
"tagline": "Пространственный компьютер здесь", "channels": [],
"description": "Революционный интерфейс между человеком и машиной. Погрузитесь в мир цифровых возможностей.", "sessions": []
"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"
}
} }

View File

@ -3,171 +3,136 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Apple Style — Будущее здесь</title> <title>VibeChat — Мессенджер будущего</title>
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="landing.css">
</head> </head>
<body> <body>
<!-- Announcement Bar --> <!-- Landing Page -->
<div id="announcementBar" class="announcement-bar"></div> <div id="landingPage">
<nav class="landing-nav">
<!-- Navigation -->
<nav class="nav" id="nav">
<div class="nav-container"> <div class="nav-container">
<a href="/" class="logo"></a> <div class="logo">💬 VibeChat</div>
<div class="nav-links"> <button class="btn-login" onclick="showAuth('login')">Войти</button>
<a href="#products">Продукты</a>
<a href="#features">Возможности</a>
<a href="#vision">Vision</a>
<a href="admin.html" class="admin-link">Админка</a>
</div>
</div> </div>
</nav> </nav>
<!-- Hero Section --> <section class="hero">
<section class="hero" id="hero">
<div class="hero-bg" id="heroBg"></div>
<div class="hero-overlay"></div>
<div class="hero-content"> <div class="hero-content">
<h1 id="heroTitle" class="hero-title"></h1> <h1>Общение без границ</h1>
<p id="heroSubtitle" class="hero-subtitle"></p> <p>Быстрый. Безопасный. С End-to-End шифрованием.</p>
<div class="hero-cta"> <div class="hero-buttons">
<a href="#products" class="btn btn-primary">Смотреть продукты</a> <button class="btn-primary" onclick="showAuth('register')">Начать бесплатно</button>
<a href="#vision" class="btn btn-secondary">Узнать больше</a> <button class="btn-secondary" onclick="scrollToFeatures()">Узнать больше</button>
</div> </div>
</div> </div>
<div class="scroll-indicator"> <div class="hero-image">
<div class="mouse"> <div class="chat-mockup">
<div class="wheel"></div> <div class="mockup-header">
<div class="mockup-avatar"></div>
<div class="mockup-lines">
<div class="line short"></div>
<div class="line"></div>
</div>
</div>
<div class="mockup-messages">
<div class="message received"></div>
<div class="message sent"></div>
<div class="message received"></div>
</div>
</div> </div>
</div> </div>
</section> </section>
<!-- Product Showcase (Apple-style scroll) -->
<section class="product-showcase" id="products">
<div class="showcase-intro">
<h2 class="showcase-title">Выберите свой путь</h2>
<p class="showcase-subtitle">Инновации в каждой детали</p>
</div>
<div class="products-container" id="productsContainer">
<!-- Products will be loaded here -->
</div>
</section>
<!-- Features Section with Parallax -->
<section class="features" id="features"> <section class="features" id="features">
<div class="features-bg" id="featuresBg"></div> <h2>Почему VibeChat?</h2>
<div class="features-overlay"></div>
<div class="features-content">
<h2 class="section-title reveal">Технологии будущего</h2>
<div class="features-grid"> <div class="features-grid">
<div class="feature-card reveal"> <div class="feature-card">
<div class="feature-icon">🚀</div>
<h3>Производительность</h3>
<p>Чипы M-серии меняют правила игры</p>
</div>
<div class="feature-card reveal">
<div class="feature-icon">🔒</div> <div class="feature-icon">🔒</div>
<h3>E2E Шифрование</h3>
<p>Ваши сообщения видят только отправитель и получатель</p>
</div>
<div class="feature-card">
<div class="feature-icon">⚡</div>
<h3>Молниеносно</h3>
<p>Мгновенная доставка сообщений в любую точку мира</p>
</div>
<div class="feature-card">
<div class="feature-icon">👥</div>
<h3>Группы и каналы</h3>
<p>Создавайте группы до 1000 человек и публичные каналы</p>
</div>
<div class="feature-card">
<div class="feature-icon">🎨</div>
<h3>Эмодзи и стикеры</h3>
<p>Тысячи эмодзи и стикеров для выражения эмоций</p>
</div>
<div class="feature-card">
<div class="feature-icon">📱</div>
<h3>Кроссплатформенно</h3>
<p>Работает на любом устройстве через браузер</p>
</div>
<div class="feature-card">
<div class="feature-icon">🔐</div>
<h3>Безопасность</h3> <h3>Безопасность</h3>
<p>Защита данных на аппаратном уровне</p> <p>Двухфакторная аутентификация и секретные чаты</p>
</div>
<div class="feature-card reveal">
<div class="feature-icon">🌱</div>
<h3>Экология</h3>
<p>Углеродная нейтральность к 2030</p>
</div>
<div class="feature-card reveal">
<div class="feature-icon">🤖</div>
<h3>ИИ</h3>
<p>Apple Intelligence в каждом устройстве</p>
</div>
</div>
</div>
</section>
<!-- Vision Section -->
<section class="vision" id="vision">
<div class="vision-content">
<h2 class="vision-title reveal">Vision Pro</h2>
<p class="vision-subtitle reveal">Эра пространственных вычислений наступила</p>
<div class="vision-features reveal">
<div class="vision-feature">
<span class="vision-feature-icon">👁️</span>
<div>
<h4>eyeSight</h4>
<p>Оставайтесь на связи с окружающими</p>
</div>
</div>
<div class="vision-feature">
<span class="vision-feature-icon">🎭</span>
<div>
<h4>visionOS</h4>
<p>Интуитивный пространственный интерфейс</p>
</div>
</div>
<div class="vision-feature">
<span class="vision-feature-icon">🎬</span>
<div>
<h4>Immersion</h4>
<p>Погружение как в кинотеатре</p>
</div>
</div>
</div>
</div>
<div class="vision-image reveal">
<img src="https://picsum.photos/1200/800?random=50" alt="Vision Pro">
</div>
</section>
<!-- Big Image Scroll Section -->
<section class="scroll-gallery" id="gallery">
<div class="gallery-item" id="galleryItem1">
<img src="https://picsum.photos/1920/1080?random=60" alt="">
<div class="gallery-caption">
<h3>Дизайн</h3>
<p>Каждая линия имеет значение</p>
</div>
</div>
<div class="gallery-item" id="galleryItem2">
<img src="https://picsum.photos/1920/1080?random=61" alt="">
<div class="gallery-caption">
<h3>Инновации</h3>
<p>Технологии, которые вдохновляют</p>
</div>
</div>
<div class="gallery-item" id="galleryItem3">
<img src="https://picsum.photos/1920/1080?random=62" alt="">
<div class="gallery-caption">
<h3>Будущее</h3>
<p>Создаём завтрашний день сегодня</p>
</div> </div>
</div> </div>
</section> </section>
<!-- CTA Section --> <section class="cta">
<section class="cta-section"> <h2>Готовы начать?</h2>
<h2 class="cta-title reveal">Готовы к будущему?</h2> <p>Присоединяйтесь к тысячам пользователей уже сегодня</p>
<p class="cta-subtitle reveal">Откройте для себя мир инноваций уже сегодня</p> <button class="btn-primary" onclick="showAuth('register')">Создать аккаунт</button>
<div class="cta-buttons reveal">
<a href="#products" class="btn btn-primary">Выбрать устройство</a>
<a href="#" class="btn btn-secondary">Связаться с нами</a>
</div>
</section> </section>
<!-- Footer -->
<footer> <footer>
<div class="footer-content"> <p>&copy; 2026 VibeChat. Все права защищены.</p>
<div class="footer-links">
<a href="#">О компании</a>
<a href="#">Карьера</a>
<a href="#">Инвесторам</a>
<a href="#">Новости</a>
<a href="#">Конфиденциальность</a>
<a href="#">Контакты</a>
</div>
<p class="copyright">&copy; 2026 Apple Style Demo. Все права защищены.</p>
</div>
</footer> </footer>
</div>
<script src="app.js"></script> <!-- Auth Modal -->
<div id="authModal" class="modal">
<div class="modal-content">
<button class="modal-close" onclick="closeAuth()">×</button>
<div id="loginForm">
<h2>Вход</h2>
<form onsubmit="handleLogin(event)">
<div class="form-group">
<label>Email</label>
<input type="email" id="loginEmail" required placeholder="you@example.com">
</div>
<div class="form-group">
<label>Пароль</label>
<input type="password" id="loginPassword" required placeholder="••••••••">
</div>
<button type="submit" class="btn-submit">Войти</button>
</form>
<p class="auth-switch">Нет аккаунта? <a href="#" onclick="showAuth('register')">Зарегистрироваться</a></p>
</div>
<div id="registerForm" style="display:none">
<h2>Регистрация</h2>
<form onsubmit="handleRegister(event)">
<div class="form-group">
<label>Имя пользователя</label>
<input type="text" id="regUsername" required placeholder="username">
</div>
<div class="form-group">
<label>Email</label>
<input type="email" id="regEmail" required placeholder="you@example.com">
</div>
<div class="form-group">
<label>Пароль</label>
<input type="password" id="regPassword" required placeholder="••••••••" minlength="6">
</div>
<button type="submit" class="btn-submit">Создать аккаунт</button>
</form>
<p class="auth-switch">Уже есть аккаунт? <a href="#" onclick="showAuth('login')">Войти</a></p>
</div>
</div>
</div>
<script src="landing.js"></script>
</body> </body>
</html> </html>

433
landing.css Normal file
View File

@ -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;
}
}

107
landing.js Normal file
View File

@ -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';
}
});

10
package-lock.json generated Normal file
View File

@ -0,0 +1,10 @@
{
"name": "apple-style-app",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "apple-style-app"
}
}
}

View File

@ -1,8 +1,9 @@
{ {
"name": "apple-style-app", "name": "vibechat-messenger",
"private": true, "private": true,
"type": "commonjs", "type": "commonjs",
"scripts": { "scripts": {
"start": "node server.js" "start": "node server.js"
} },
"dependencies": {}
} }

359
server.js
View File

@ -1,12 +1,14 @@
const http = require("http"); const http = require("http");
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
const crypto = require("crypto");
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
const DATA = path.join(__dirname, "data.json"); const DATA = path.join(__dirname, "data.json");
const SECRET_KEY = crypto.randomBytes(32).toString("hex");
function readData() { 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) { function writeData(data) {
@ -31,93 +33,321 @@ function notFound(res) {
res.end("Not Found"); 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 = { const routes = {
// PUBLIC API // ========== AUTH ==========
"GET /api/products": (req, res) => { "POST /api/auth/register": async (req, res) => {
const data = readData(); const { username, email, password } = await parseBody(req);
json(res, { ok: true, data: data.products.sort((a, b) => a.order - b.order) }); if (!username || !email || !password) return json(res, { ok: false, error: "Все поля обязательны" }, 400);
},
"GET /api/products/:id": (req, res, params) => {
const data = readData(); const data = readData();
const product = data.products.find(p => p.id === parseInt(params.id)); if (data.users.find(u => u.email === email)) return json(res, { ok: false, error: "Email уже зарегистрирован" }, 400);
if (!product) return json(res, { ok: false, error: "Product not found" }, 404); if (data.users.find(u => u.username === username)) return json(res, { ok: false, error: "Имя пользователя занято" }, 400);
json(res, { ok: true, data: product });
},
"GET /api/announcements": (req, res) => { const user = {
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 = {
id: Date.now(), id: Date.now(),
...body, username,
order: data.products.length + 1 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); 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) => { "POST /api/auth/login": async (req, res) => {
const body = await parseBody(req); const { email, password } = await parseBody(req);
if (!email || !password) return json(res, { ok: false, error: "Email и пароль обязательны" }, 400);
const data = readData(); const data = readData();
const idx = data.products.findIndex(p => p.id === parseInt(params.id)); const user = data.users.find(u => u.email === email && u.passwordHash === hashPassword(password));
if (idx === -1) return json(res, { ok: false, error: "Product not found" }, 404); if (!user) return json(res, { ok: false, error: "Неверный email или пароль" }, 401);
data.products[idx] = { ...data.products[idx], ...body, id: parseInt(params.id) };
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); 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(); 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); writeData(data);
json(res, { ok: true, deleted: parseInt(params.id) }); json(res, { ok: true });
}, },
"POST /api/admin/announcements": async (req, res) => { "GET /api/auth/me": async (req, res) => {
const body = await parseBody(req); const token = req.headers.authorization?.split(" ")[1];
const data = readData(); if (!token) return json(res, { ok: false, error: "Не авторизован" }, 401);
const newAnnouncement = { id: Date.now(), ...body, active: true };
data.announcements.push(newAnnouncement); const user = verifyToken(token);
writeData(data); if (!user) return json(res, { ok: false, error: "Сессия истекла" }, 401);
json(res, { ok: true, data: newAnnouncement }, 201);
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) => { // ========== USERS ==========
const body = await parseBody(req); "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 data = readData();
const idx = data.announcements.findIndex(a => a.id === parseInt(params.id)); const users = data.users.filter(u => u.id !== currentUser.id).map(u => ({
if (idx === -1) return json(res, { ok: false, error: "Not found" }, 404); id: u.id, username: u.username, avatar: u.avatar
data.announcements[idx] = { ...data.announcements[idx], ...body, id: parseInt(params.id) }; }));
writeData(data); json(res, { ok: true, users });
json(res, { ok: true, data: data.announcements[idx] });
}, },
"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(); 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); 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) => { // ========== CHATS ==========
const body = await parseBody(req); "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 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); writeData(data);
json(res, { ok: true, data: data.siteSettings }); 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 });
} }
}; };
@ -125,10 +355,7 @@ const mimeTypes = {
".html": "text/html; charset=utf-8", ".html": "text/html; charset=utf-8",
".css": "text/css; charset=utf-8", ".css": "text/css; charset=utf-8",
".js": "application/javascript; charset=utf-8", ".js": "application/javascript; charset=utf-8",
".json": "application/json; charset=utf-8", ".json": "application/json; charset=utf-8"
".png": "image/png",
".jpg": "image/jpeg",
".svg": "image/svg+xml"
}; };
http.createServer(async (req, res) => { 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-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); 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") { if (method === "OPTIONS") {
res.writeHead(204); res.writeHead(204);
@ -182,4 +409,4 @@ http.createServer(async (req, res) => {
} }
notFound(res); notFound(res);
}).listen(PORT, () => console.log("Server running on port " + PORT)); }).listen(PORT, () => console.log("VibeChat server running on port " + PORT));