Сообщения работают

This commit is contained in:
Artur 2026-04-24 22:57:55 +05:00
parent 8a0a237e18
commit 40d715539e
20 changed files with 593 additions and 51 deletions

View File

@ -1,6 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="chepuhagram"
android:label="Chepuhagram"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity

View File

@ -0,0 +1,78 @@
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import 'package:chepuhagram/data/models/message_model.dart';
class LocalDbService {
static final LocalDbService _instance = LocalDbService._internal();
static Database? _database;
factory LocalDbService() => _instance;
LocalDbService._internal();
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDb();
return _database!;
}
Future<Database> _initDb() async {
String path = join(await getDatabasesPath(), 'chat_app.db');
return await openDatabase(
path,
version: 1,
onCreate: (db, version) async {
await db.execute('''
CREATE TABLE messages(
id INTEGER PRIMARY KEY,
sender_id INTEGER,
receiver_id INTEGER,
content TEXT,
timestamp TEXT
)
''');
},
);
}
// Сохранение списка сообщений (из истории)
Future<void> saveMessages(List<dynamic> messages) async {
final db = await database;
Batch batch = db.batch();
for (var msg in messages) {
if (msg is MessageModel) {
batch.insert('messages', {
'id': msg.id,
'sender_id': msg.senderId,
'receiver_id': msg.receiverId,
'content': msg.text, // ВАЖНО: сохраняй зашифрованный текст!
'timestamp': msg.createdAt.toIso8601String(),
}, conflictAlgorithm: ConflictAlgorithm.replace);
} else {
// Если это Map из API
batch.insert('messages', {
'id': msg['id'],
'sender_id': msg['sender_id'],
'receiver_id': msg['receiver_id'], // Убедись, что ключ совпадает с API
'content': msg['content'],
'timestamp': msg['timestamp'],
}, conflictAlgorithm: ConflictAlgorithm.replace);
}
}
await batch.commit(noResult: true);
}
// Получение сообщений конкретного чата
Future<List<Map<String, dynamic>>> getChatHistory(
int contactId,
int myId,
) async {
final db = await database;
return await db.query(
'messages',
where:
'(sender_id = ? AND receiver_id = ?) OR (sender_id = ? AND receiver_id = ?)',
whereArgs: [contactId, myId, myId, contactId],
orderBy: 'timestamp ASC',
);
}
}

View File

@ -6,6 +6,13 @@ import 'package:web_socket_channel/status.dart' as status;
import 'package:chepuhagram/core/constants.dart';
class SocketService {
static final SocketService _instance = SocketService._internal();
factory SocketService() {
return _instance;
}
SocketService._internal();
WebSocketChannel? _channel;
final StreamController<Map<String, dynamic>> _messageController =
StreamController<Map<String, dynamic>>.broadcast();
@ -38,7 +45,21 @@ class SocketService {
}
void sendMessage(Map<String, dynamic> data) {
_channel?.sink.add(jsonEncode(data));
if (_channel == null) {
print("❌ ОШИБКА: Попытка отправить сообщение через NULL канал.");
return;
}
try {
final encodedData = jsonEncode(data);
// 1. Проверяем, не закрыт ли sink (у некоторых провайдеров это доступно)
_channel!.sink.add(encodedData);
// 2. Добавляем принт подтверждения
print("🚀 СООБЩЕНИЕ ОТПРАВЛЕНО В SINK: $encodedData");
} catch (e) {
print("❌ КРИТИЧЕСКАЯ ОШИБКА ПРИ ОТПРАВКЕ: $e");
}
}
void disconnect() {

View File

@ -8,6 +8,7 @@ class Contact {
final DateTime? lastMessageTime;
final bool isOnline;
final int unreadCount;
final String? publicKey;
Contact({
required this.id,
@ -19,6 +20,7 @@ class Contact {
this.lastMessageTime,
this.isOnline = false,
this.unreadCount = 0,
this.publicKey,
});
factory Contact.fromJson(Map<String, dynamic> json) {
@ -27,7 +29,7 @@ class Contact {
username: json['username'] ?? 'Unknown',
name: json['name'] ?? 'Unknown',
surname: json['surname'] ?? 'Unknown',
// Другие поля можно добавить позже
publicKey: json['public_key'],
);
}
}

View File

@ -1,6 +1,7 @@
class MessageModel {
final int? id; // ID из базы данных (null, если сообщение еще не отправлено)
final int senderId; // ID отправителя
final int receiverId; // ID отправителя
final String text; // Текст сообщения
final DateTime createdAt; // Время отправки
final bool isMe; // Локальный флаг для UI (мое/чужое)
@ -8,6 +9,7 @@ class MessageModel {
MessageModel({
this.id,
required this.senderId,
required this.receiverId,
required this.text,
required this.createdAt,
this.isMe = false,
@ -18,6 +20,7 @@ class MessageModel {
return MessageModel(
id: json['id'],
senderId: json['sender_id'],
receiverId: json['receiverId'],
text: json['text'] ?? '',
// Парсим дату из ISO строки или временной метки
createdAt: DateTime.parse(json['created_at']),

View File

@ -29,4 +29,22 @@ class ContactRepository {
throw Exception('Failed to load contacts');
}
}
Future<Contact> fetchContactById(int userId) async {
final token = await _apiService.getAccessToken();
final response = await _client.get(
Uri.http(AppConstants.baseUrl, 'users/$userId'),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
);
if (response.statusCode == 200) {
final data = jsonDecode(utf8.decode(response.bodyBytes));
return Contact.fromJson(data);
} else {
throw Exception('Не удалось загрузить данные контакта');
}
}
}

View File

@ -90,4 +90,19 @@ class ApiService extends ChangeNotifier {
notifyListeners();
}
}
Future<List<dynamic>> getChatHistory(int contactId) async {
final token = await getAccessToken();
final response = await http.get(
Uri.http(
AppConstants.baseUrl,
'messages/history/${contactId.toString()}',
),
headers: {
'Content-Type': 'application/json',
"Authorization": "Bearer $token",
},
);
return jsonDecode(response.body) as List<dynamic>;
}
}

View File

@ -20,6 +20,10 @@ class ContactProvider extends ChangeNotifier {
notifyListeners();
}
int? getCurrentUserId() {
return _currentUserId;
}
Future<void> loadContacts() async {
_isLoading = true;
_error = null;

View File

@ -1,7 +1,7 @@
import 'package:chepuhagram/presentation/screens/splash_screen.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'data/datasources/ws_client.dart';
import 'logic/auth_provider.dart';
import 'logic/contact_provider.dart';
import 'core/theme_manager.dart';
@ -13,7 +13,10 @@ void main() {
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AuthProvider()),
ChangeNotifierProvider(create: (_) => ThemeProvider()), ChangeNotifierProvider(create: (_) => ContactProvider()), ],
ChangeNotifierProvider(create: (_) => ThemeProvider()),
ChangeNotifierProvider(create: (_) => ContactProvider()),
Provider(create: (_) => SocketService()),
],
child: const MyApp(),
),
);

View File

@ -2,6 +2,14 @@ import 'package:flutter/material.dart';
import '/data/models/message_model.dart';
import '/data/models/contact_model.dart';
import 'package:chepuhagram/presentation/widgets/message_bubble.dart';
import 'package:chepuhagram/data/repositories/contact_repository.dart';
import 'package:chepuhagram/domain/services/crypto_service.dart';
import 'package:chepuhagram/data/datasources/ws_client.dart';
import 'dart:convert';
import 'package:provider/provider.dart';
import '/logic/contact_provider.dart';
import '../../domain/services/api_service.dart';
import 'package:chepuhagram/data/datasources/local_db_service.dart';
class ChatScreen extends StatefulWidget {
final Contact contact;
@ -13,8 +21,48 @@ class ChatScreen extends StatefulWidget {
}
class _ChatScreenState extends State<ChatScreen> {
int myId = 0;
late Contact _currentContact;
bool _isKeyLoading = false;
final TextEditingController _controller = TextEditingController();
final List<MessageModel> messages = [];
final ContactRepository _contactRepository = ContactRepository();
final apiService = ApiService();
final CryptoService _cryptoService = CryptoService();
List<MessageModel> messages = [];
@override
void initState() {
super.initState();
_currentContact = widget.contact;
final contactProvider = context.read<ContactProvider>();
myId = contactProvider.getCurrentUserId() ?? 0;
_loadHistory();
// Если ключа нет, загружаем его при входе
if (_currentContact.publicKey == null) {
_loadContactKey();
}
}
Future<void> _loadContactKey() async {
setState(() => _isKeyLoading = true);
try {
final updatedContact = await _contactRepository.fetchContactById(
_currentContact.id,
);
setState(() {
_currentContact = updatedContact;
_isKeyLoading = false;
});
print(updatedContact.publicKey);
} catch (e) {
setState(() => _isKeyLoading = false);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Не удалось получить ключ шифрования собеседника"),
),
);
}
}
@override
void dispose() {
@ -25,7 +73,7 @@ class _ChatScreenState extends State<ChatScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.contact.name)),
appBar: AppBar(title: Text(_currentContact.name)),
body: Column(
children: [
Expanded(
@ -49,25 +97,210 @@ class _ChatScreenState extends State<ChatScreen> {
}
Widget _buildMessageInput() {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: const InputDecoration(hintText: "Напиши сообщение..."),
return SafeArea(
// Добавляем SafeArea здесь
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: const InputDecoration(
hintText: "Напиши сообщение...",
),
),
),
),
IconButton(
icon: const Icon(Icons.send),
onPressed: () {
// Логика отправки через WebSocket или API
_controller.clear();
},
),
],
IconButton(
icon: const Icon(Icons.send),
onPressed: () {
_sendMessage();
},
),
],
),
),
);
}
Future<void> _sendMessage() async {
final rawText = _controller.text.trim();
if (rawText.isEmpty) return;
_controller.clear();
if (_currentContact.publicKey == null) {
await _loadContactKey();
if (_currentContact.publicKey == null) return;
}
try {
final myPrivKey = await _cryptoService.getPrivateKey();
final sharedSecret = await _cryptoService.deriveSharedSecret(
myPrivKey!,
_currentContact.publicKey!,
);
final encryptedText = await _cryptoService.encryptMessage(
rawText,
sharedSecret,
);
// Формируем payload для сервера
final payload = {
"type": "private_message",
"receiver_id": _currentContact.id,
"content": encryptedText,
};
// Отправляем
print("ОТПРАВКА: $payload");
Provider.of<SocketService>(context, listen: false).sendMessage(payload);
// Обновляем UI (себе показываем расшифрованный текст)
setState(() {
messages.add(
MessageModel(
text: rawText,
isMe: true,
senderId: myId,
receiverId: _currentContact.id,
createdAt: DateTime.now(),
),
);
});
_controller.clear();
} catch (e) {
_controller.text = rawText;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text("Ошибка шифрования: $e")));
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Подписываемся на поток сообщений из сокета
final socketService = Provider.of<SocketService>(context, listen: false);
socketService.messages.listen((rawData) {
_handleIncomingMessage(rawData);
});
}
void _handleIncomingMessage(Map<String, dynamic> data) async {
if (data['type'] == 'private_message') {
final int senderId = int.parse(data['sender_id'].toString());
// 1. Проверяем, что сообщение именно от того, с кем мы сейчас общаемся
if (senderId == widget.contact.id) {
try {
final myPrivKey = await _cryptoService.getPrivateKey();
// 2. Вычисляем общий секрет для расшифровки
final sharedSecret = await _cryptoService.deriveSharedSecret(
myPrivKey!,
widget.contact.publicKey!,
);
// 3. Расшифровываем контент
final decryptedText = await _cryptoService.decryptMessage(
data['content'],
sharedSecret,
);
// 4. Добавляем в список и обновляем экран
await LocalDbService().saveMessages([data]);
setState(() {
messages.add(
MessageModel(
text: decryptedText,
isMe: false,
senderId: senderId,
receiverId: myId,
createdAt: DateTime.parse(data['timestamp']),
),
);
});
} catch (e) {
print("Ошибка расшифровки входящего сообщения: $e");
}
} else {
print(
"Сообщение от другого пользователя (ID: $senderId), игнорируем в этом чате",
);
// Тут можно добавить логику уведомления для списка чатов
}
}
}
Future<void> _loadHistory() async {
try {
final myPrivKey = await _cryptoService.getPrivateKey();
final sharedSecret = await _cryptoService.deriveSharedSecret(
myPrivKey!,
widget.contact.publicKey!,
);
final localDb = LocalDbService();
final cached = await localDb.getChatHistory(widget.contact.id, myId);
try {
List<MessageModel> loadedLocalMessages = [];
for (var msg in cached) {
final decrypted = await _cryptoService.decryptMessage(
msg['content'],
sharedSecret,
);
loadedLocalMessages.add(
MessageModel(
text: decrypted,
isMe: msg['sender_id'] == myId,
senderId: msg['sender_id'],
receiverId: msg['receiver_id'],
createdAt: DateTime.parse(msg['timestamp']),
),
);
}
if (cached.isNotEmpty) {
setState(() {
messages = loadedLocalMessages;
_isKeyLoading = false;
});
}
} catch (e) {
print(e);
}
final history = await apiService.getChatHistory(widget.contact.id);
List<MessageModel> loadedMessages = [];
for (var msg in history) {
final decrypted = await _cryptoService.decryptMessage(
msg['content'],
sharedSecret,
);
loadedMessages.add(
MessageModel(
text: decrypted,
isMe: msg['sender_id'] == myId,
senderId: msg['sender_id'],
receiverId: msg['receiver_id'],
createdAt: DateTime.parse(msg['timestamp']),
),
);
}
await localDb.saveMessages(history);
setState(() {
messages = loadedMessages;
_isKeyLoading = false;
});
} catch (e) {
print("Ошибка загрузки истории: $e");
setState(() => _isKeyLoading = false);
}
}
}

View File

@ -7,8 +7,10 @@ import Foundation
import flutter_secure_storage_darwin
import path_provider_foundation
import sqflite_darwin
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
}

View File

@ -273,7 +273,7 @@ packages:
source: hosted
version: "2.2.0"
path:
dependency: transitive
dependency: "direct main"
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
@ -365,6 +365,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.2"
sqflite:
dependency: "direct main"
description:
name: sqflite
sha256: "564cfed0746fe53140c23b70b308e045c3b31f17778f2f326ccb7d804ea0250a"
url: "https://pub.dev"
source: hosted
version: "2.4.2+1"
sqflite_android:
dependency: transitive
description:
name: sqflite_android
sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40"
url: "https://pub.dev"
source: hosted
version: "2.4.2+3"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "5e8377564d95166761a968ed96104e0569b6b6cc611faac92a36ab8a169112c3"
url: "https://pub.dev"
source: hosted
version: "2.5.6+1"
sqflite_darwin:
dependency: transitive
description:
name: sqflite_darwin
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
sqflite_platform_interface:
dependency: transitive
description:
name: sqflite_platform_interface
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
stack_trace:
dependency: transitive
description:
@ -389,6 +429,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.1"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
url: "https://pub.dev"
source: hosted
version: "3.4.0"
term_glyph:
dependency: transitive
description:
@ -471,4 +519,4 @@ packages:
version: "1.1.0"
sdks:
dart: ">=3.10.0 <4.0.0"
flutter: ">=3.35.6"
flutter: ">=3.38.0"

View File

@ -40,6 +40,8 @@ dependencies:
jwt_decoder: ^2.0.1
web_socket_channel: ^3.0.3
cryptography: ^2.5.0
sqflite: ^2.3.0
path: ^1.9.0
dev_dependencies:
flutter_test:

View File

@ -0,0 +1,35 @@
from fastapi import Depends, APIRouter, HTTPException, Depends
from sqlalchemy.orm import Session
from app.db import models
from app.core.security import get_current_user
from app.api import schemas
# бд
def get_db():
db = models.SessionLocal()
try:
yield db
finally:
db.close()
messagesRouter = APIRouter(
prefix="/messages",
tags=[],
)
@messagesRouter.get("/history/{contact_id}")
async def get_chat_history(
contact_id: int,
current_user: models.User = Depends(get_current_user),
db: Session = Depends(get_db),
limit: int = 50
):
messages = db.query(models.Message).filter(
(models.Message.sender_id == current_user.id) & (models.Message.receiver_id == contact_id) |
(models.Message.sender_id == contact_id) & (models.Message.receiver_id == current_user.id)
).order_by(models.Message.timestamp.asc()).limit(limit).all()
return messages

View File

@ -1,8 +1,9 @@
from fastapi import Depends, APIRouter
from fastapi import Depends, APIRouter, HTTPException, Depends
from sqlalchemy.orm import Session
from app.db import models
from app.core.security import get_current_user
from app.api import schemas
# бд
@ -13,17 +14,38 @@ def get_db():
finally:
db.close()
usersRouter = APIRouter(
prefix="/users",
tags=[],
)
# Пример защищенного роута
@usersRouter.get("/me")
async def read_users_me(current_user: models.User = Depends(get_current_user)):
return {"id": current_user.id, "username": current_user.username, "first_name": current_user.first_name, "last_name": current_user.last_name, "public_key": current_user.public_key, "encrypted_private_key": current_user.encrypted_private_key}
@usersRouter.get("/all")
async def read_users_all(current_user: models.User = Depends(get_current_user), db: Session = Depends(get_db)):
users = db.query(models.User).all()
return [{"id": user.id, "username": user.username, "name": f"{user.first_name} {user.last_name or ''}".strip(), "public_key": user.public_key} for user in users]
@usersRouter.get("/{user_id}", response_model=schemas.UserPublic)
def get_user_by_id(
user_id: int,
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user)
):
"""
Получить публичную информацию о пользователе, включая его публичный ключ.
"""
user = db.query(models.User).filter(models.User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="Пользователь не найден")
return user

View File

@ -1,4 +1,5 @@
from pydantic import BaseModel
from typing import Optional
class SetPublicKey(BaseModel):
public_key: str
@ -11,3 +12,13 @@ class SetupAccount(BaseModel):
last_name: str
public_key: str
encrypted_private_key: str
class UserPublic(BaseModel):
id: int
username: str
first_name: Optional[str] = None
last_name: Optional[str] = None
public_key: Optional[str] = None
class Config:
from_attributes = True

View File

@ -1,6 +1,8 @@
from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy import Column, Integer, Text, ForeignKey, DateTime
from sqlalchemy.sql import func
SQLALCHEMY_DATABASE_URL = "sqlite:///./chepuhagram.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
@ -21,4 +23,12 @@ class User(Base):
public_key = Column(String, nullable=True)
encrypted_private_key = Column(String, nullable=True)
class Message(Base):
__tablename__ = "messages"
id = Column(Integer, primary_key=True, index=True)
sender_id = Column(Integer, ForeignKey("users.id"))
receiver_id = Column(Integer, ForeignKey("users.id"))
content = Column(Text)
timestamp = Column(DateTime(timezone=True), server_default=func.now())
Base.metadata.create_all(bind=engine)

View File

@ -1,16 +1,28 @@
from fastapi import HTTPException, status, APIRouter, WebSocket, WebSocketDisconnect, Query
from fastapi import HTTPException, status, APIRouter, WebSocket, WebSocketDisconnect, Query, Depends
from app.core.security import test_token
from typing import Dict
from datetime import datetime
import json
from sqlalchemy.orm import Session
from app.db import models
# бд
def get_db():
db = models.SessionLocal()
try:
yield db
finally:
db.close()
wsRouter = APIRouter(
prefix="/ws",
tags=[],
prefix='/ws'
)
@wsRouter.websocket("")
async def websocket_endpoint(websocket: WebSocket, token: str = Query(None)):
async def websocket_endpoint(websocket: WebSocket, token: str = Query(None), db: Session = Depends(get_db)):
if token is None:
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
return
@ -19,20 +31,39 @@ async def websocket_endpoint(websocket: WebSocket, token: str = Query(None)):
except HTTPException:
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
return
print("ПОДКЛЮЧЕНИЕ")
await manager.connect(user_id, websocket)
print("ПОДКЛЮЧЕНО")
try:
while True:
data = await websocket.receive_json()
print("ОЖИДАНИЕ СООБЩЕНИЙ")
data = await websocket.receive_text()
message_data = json.loads(data)
print(f"DEBUG: Получены данные: {message_data}")
receiver_id = str(data.get("receiver_id"))
message_to_send = {
"sender_id": user_id,
"text": data.get("text"),
"created_at": datetime.now().isoformat()
}
if message_data.get("type") == "private_message":
receiver_id = message_data.get("receiver_id")
content = message_data.get("content")
new_msg = models.Message(
sender_id=user_id,
receiver_id=receiver_id,
content=content
)
db.add(new_msg)
db.commit()
db.refresh(new_msg)
# Формируем пакет для получателя
outgoing_message = {
"id": new_msg.id,
"type": "private_message",
"sender_id": user_id,
"reciever_id": receiver_id,
"content": message_data.get("content"),
"timestamp": datetime.now().isoformat()
}
await manager.send_personal_message(message_to_send, receiver_id)
await manager.send_personal_message(message_to_send, user_id)
# Пересылаем получателю, если он в сети
await manager.send_personal_message(outgoing_message, str(receiver_id))
except WebSocketDisconnect:
pass
finally:
@ -53,8 +84,11 @@ class ConnectionManager:
del self.active_connections[user_id]
async def send_personal_message(self, message: dict, user_id: str):
if user_id in self.active_connections:
await self.active_connections[user_id].send_json(message)
if str(user_id) in self.active_connections:
await self.active_connections[str(user_id)].send_json(message)
print('Sent to socket')
else:
print('User not active')
async def broadcast(self, message: dict):
# Рассылка вообще всем (например, системное уведомление)

View File

@ -1,5 +1,5 @@
from fastapi import FastAPI
from app.api.endpoints import users, auth
from app.api.endpoints import users, auth, messages
from app.websocket.connection_manager import wsRouter
from fastapi.middleware.cors import CORSMiddleware
@ -7,6 +7,7 @@ app = FastAPI()
app.include_router(auth.authRouter)
app.include_router(users.usersRouter)
app.include_router(messages.messagesRouter)
app.include_router(wsRouter)
app.add_middleware(