diff --git a/requirements.txt b/requirements.txt
index 92e78ed..2650190 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,8 @@
-Flask>=3.1.2
-Flask-SQLAlchemy>=3.0.5
-Flask-Cache>=0.13.1
-cryptography>=45.0.6
\ No newline at end of file
+Flask==3.0.0
+Flask-SocketIO==5.3.6
+Flask-Limiter==3.5.0
+redis==5.0.1
+cryptography==41.0.7
+bcrypt==4.1.2
+python-dotenv==1.0.0
+gunicorn==21.2.0
\ No newline at end of file
diff --git a/src/app.py b/src/app.py
index ec5ff68..62a0209 100644
--- a/src/app.py
+++ b/src/app.py
@@ -1,11 +1,554 @@
-from flask import Flask
-from todo import todo
+import eventlet
+eventlet.monkey_patch()
+from flask import Flask, make_response, render_template, request, jsonify
+from flask_socketio import SocketIO, emit, join_room, leave_room, disconnect
+from flask_limiter import Limiter
+from flask_limiter.util import get_remote_address
+import collections
+import threading
+import time
+import uuid
+import json
+import secrets
+import hashlib
+import hmac
+import re
+from datetime import datetime, timedelta
+import logging
+from functools import wraps
+import os
+
+# Configure logging
+logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
+logger = logging.getLogger(__name__)
app = Flask(__name__)
+socketio = SocketIO(app)
+
+# Security constants
+MAX_MESSAGES = 256
+MAX_ROOM_ID_LENGTH = 32
+MAX_MESSAGE_SIZE = 8192 # 8KB max encrypted message
+MAX_ROOMS_PER_IP = 5
+MAX_USERS_PER_ROOM = 50
+ROOM_CLEANUP_INTERVAL = 3600 # 1 hour
+USER_SESSION_TIMEOUT = 3600 # 1 hour
+RATE_LIMIT_MESSAGE = "10 per minute"
+RATE_LIMIT_ROOM_JOIN = "5 per minute"
+
+# In-memory storage with enhanced security
+chat_rooms = {}
+room_keys = {}
+user_sessions = {}
+room_passwords = {} # Separate password storage with hashing
+room_creation_times = {}
+ip_room_count = {} # Track rooms created per IP
+failed_password_attempts = {} # Track failed password attempts
+message_hashes = {} # Store message hashes for duplicate detection
+room_session_keys = {} # Store session keys for each room
+
+class CircularMessageBuffer:
+ def __init__(self, max_size=MAX_MESSAGES):
+ self.buffer = collections.deque(maxlen=max_size)
+ self.lock = threading.Lock()
+ self.creation_time = time.time()
+ self.last_activity = time.time()
+
+ def add_message(self, message_data):
+ with self.lock:
+ # Generate unique message ID and validate
+ message_id = str(uuid.uuid4())
+
+ # Create message hash for duplicate detection
+ message_hash = hashlib.sha256(
+ f"{message_data['encrypted_content']}{message_data['sender_id']}{message_data.get('iv', '')}".encode()
+ ).hexdigest()
+
+ self.buffer.append({
+ 'id': message_id,
+ 'encrypted_content': message_data['encrypted_content'],
+ 'sender_id': message_data['sender_id'],
+ 'timestamp': time.time(),
+ 'iv': message_data.get('iv'),
+ 'hash': message_hash
+ })
+
+ self.last_activity = time.time()
+
+ def get_messages(self):
+ with self.lock:
+ return list(self.buffer)
+
+ def get_message_count(self):
+ with self.lock:
+ return len(self.buffer)
+
+ def is_expired(self, timeout_seconds=USER_SESSION_TIMEOUT):
+ return time.time() - self.last_activity > timeout_seconds
+
+# Security validation functions
+def is_valid_room_id(room_id):
+ """Validate room ID format and length"""
+ if not room_id or len(room_id) > MAX_ROOM_ID_LENGTH:
+ return False
+ # Allow only alphanumeric characters and hyphens
+ return re.match(r'^[a-zA-Z0-9\-_]+$', room_id) is not None
+
+def is_valid_message(encrypted_content, iv=None):
+ """Validate message format and size"""
+ if not encrypted_content or len(encrypted_content) > MAX_MESSAGE_SIZE:
+ return False
+ if iv and len(iv) > 256: # IV should be reasonable size
+ return False
+ return True
+
+def is_valid_public_key(public_key):
+ """Validate public key format"""
+ if not public_key or len(public_key) > 2048: # RSA-2048 base64 encoded
+ return False
+ try:
+ # Basic base64 validation
+ import base64
+ base64.b64decode(public_key)
+ return True
+ except Exception:
+ return False
+
+def hash_password(password, salt=None):
+ """Hash password with salt using PBKDF2"""
+ if salt is None:
+ salt = secrets.token_bytes(32)
+ else:
+ salt = bytes.fromhex(salt)
+
+ hashed = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000)
+ return salt.hex() + hashed.hex()
+
+def verify_password(password, hashed):
+ """Verify password against hash"""
+ try:
+ salt = bytes.fromhex(hashed[:64])
+ stored_hash = hashed[64:]
+ new_hash = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000).hex()
+ return hmac.compare_digest(stored_hash, new_hash)
+ except Exception:
+ return False
+
+def cleanup_expired_rooms():
+ """Clean up expired rooms and sessions"""
+ current_time = time.time()
+ expired_rooms = []
+
+ for room_id, room_buffer in chat_rooms.items():
+ if room_buffer.is_expired():
+ expired_rooms.append(room_id)
+
+ for room_id in expired_rooms:
+ logger.info(f"Cleaning up expired room: {room_id}")
+ cleanup_room(room_id)
+
+def cleanup_room(room_id):
+ """Completely clean up a room"""
+ chat_rooms.pop(room_id, None)
+ room_keys.pop(room_id, None)
+ room_passwords.pop(room_id, None)
+ room_creation_times.pop(room_id, None)
+ message_hashes.pop(room_id, None)
+ room_session_keys.pop(room_id, None) # Clean up session keys
+
+def get_client_ip():
+ """Get real client IP address"""
+ if request.headers.get('X-Forwarded-For'):
+ return request.headers.get('X-Forwarded-For').split(',')[0].strip()
+ elif request.headers.get('X-Real-IP'):
+ return request.headers.get('X-Real-IP')
+ return request.remote_addr
+
+# Authentication decorator
+def require_valid_session(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ if request.sid not in user_sessions:
+ logger.warning(f"Invalid session attempt from {get_client_ip()}")
+ disconnect()
+ return
+ return f(*args, **kwargs)
+ return decorated_function
+
+# CSP header as requested
+@app.after_request
+def apply_csp(response):
+ csp = (
+ "default-src 'self'; "
+ "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
+ "style-src 'self' 'unsafe-inline'; "
+ "img-src 'self' data:; "
+ "connect-src 'self' ws://localhost:5000 wss://localhost:5000;"
+ )
+ response.headers["Content-Security-Policy"] = csp
+ return response
@app.route('/')
-def hello():
- htmlbasic= """
-
- """
- return htmlbasic + "" + str(todo) + "
" + ""
\ No newline at end of file
+def index():
+ return render_template('chat.html')
+
+@app.route('/room/')
+def room(room_id):
+ if not is_valid_room_id(room_id):
+ return "Invalid room ID", 400
+ return render_template('chat.html', room_id=room_id)
+
+@app.route('/api/room//info')
+def get_room_info(room_id):
+ if not is_valid_room_id(room_id):
+ return jsonify({'error': 'Invalid room ID'}), 400
+
+ if room_id in chat_rooms:
+ return jsonify({
+ 'exists': True,
+ 'message_count': chat_rooms[room_id].get_message_count(),
+ 'max_messages': MAX_MESSAGES,
+ 'user_count': len(room_keys.get(room_id, {})),
+ 'max_users': MAX_USERS_PER_ROOM
+ })
+ return jsonify({'exists': False})
+
+# Socket event handlers
+@socketio.on('connect')
+def handle_connect():
+ client_ip = get_client_ip()
+ user_id = str(uuid.uuid4())
+
+ # Check connection limits
+ active_connections = sum(1 for session in user_sessions.values()
+ if session.get('ip') == client_ip)
+
+ if active_connections > 10: # Max 10 connections per IP
+ logger.warning(f"Too many connections from IP: {client_ip}")
+ disconnect()
+ return
+
+ user_sessions[request.sid] = {
+ 'user_id': user_id,
+ 'room': None,
+ 'display_name': f"User-{user_id[:8]}",
+ 'ip': client_ip,
+ 'connected_at': time.time(),
+ 'last_activity': time.time()
+ }
+
+ emit('user_connected', {'user_id': user_id})
+ logger.info(f"User {user_id} connected from {client_ip}")
+
+@socketio.on('disconnect')
+def handle_disconnect():
+ if request.sid in user_sessions:
+ user_data = user_sessions[request.sid]
+ room_id = user_data.get('room')
+ user_id = user_data.get('user_id')
+
+ if room_id:
+ leave_room(room_id)
+ # Remove user's public key
+ if room_id in room_keys and user_id in room_keys[room_id]:
+ del room_keys[room_id][user_id]
+
+ emit('user_left', {'user_id': user_id}, room=room_id)
+
+ # Clean up empty rooms
+ if room_id in room_keys and len(room_keys[room_id]) == 0:
+ cleanup_room(room_id)
+
+ del user_sessions[request.sid]
+ logger.info(f"User {user_id} disconnected")
+
+@socketio.on('join_room')
+@require_valid_session
+def handle_join_room(data):
+ try:
+ room_id = data.get('room_id', '').strip()
+ public_key = data.get('public_key', '')
+ password = data.get('password', '')
+
+ # Validation
+ if not is_valid_room_id(room_id):
+ emit('room_joined', {'error': 'Invalid room ID format'})
+ return
+
+ if not is_valid_public_key(public_key):
+ emit('room_joined', {'error': 'Invalid public key format'})
+ return
+
+ client_ip = get_client_ip()
+ user_id = user_sessions[request.sid]['user_id']
+
+ is_first_user = False
+
+ # Check if room exists
+ if room_id not in chat_rooms:
+ # Check room creation limits per IP
+ rooms_by_ip = ip_room_count.get(client_ip, 0)
+ if rooms_by_ip >= MAX_ROOMS_PER_IP:
+ emit('room_joined', {'error': 'Too many rooms created from this IP'})
+ return
+
+ # Create new room
+ chat_rooms[room_id] = CircularMessageBuffer()
+ room_keys[room_id] = {}
+ room_creation_times[room_id] = time.time()
+ message_hashes[room_id] = set()
+ room_session_keys[room_id] = None # Initialize session key storage
+
+ # Store hashed password if provided
+ if password:
+ room_passwords[room_id] = hash_password(password)
+
+ # Update IP room count
+ ip_room_count[client_ip] = rooms_by_ip + 1
+ is_first_user = True
+
+ logger.info(f"Room {room_id} created by {user_id}")
+ else:
+ # Check room capacity
+ if len(room_keys[room_id]) >= MAX_USERS_PER_ROOM:
+ emit('room_joined', {'error': 'Room is full'})
+ return
+
+ # Verify password if room is password protected
+ if room_id in room_passwords:
+ if not password:
+ emit('room_joined', {'error': 'Password required'})
+ return
+
+ # Check failed attempts
+ attempt_key = f"{client_ip}_{room_id}"
+ failed_attempts = failed_password_attempts.get(attempt_key, 0)
+
+ if failed_attempts >= 5: # Max 5 failed attempts
+ emit('room_joined', {'error': 'Too many failed password attempts'})
+ return
+
+ if not verify_password(password, room_passwords[room_id]):
+ failed_password_attempts[attempt_key] = failed_attempts + 1
+ emit('room_joined', {'error': 'Incorrect password'})
+ return
+ else:
+ # Clear failed attempts on success
+ failed_password_attempts.pop(attempt_key, None)
+
+ # Generate new UUID for this room session
+ new_user_id = str(uuid.uuid4())
+ user_sessions[request.sid]['user_id'] = new_user_id
+ user_sessions[request.sid]['display_name'] = f"User-{new_user_id[:8]}"
+ user_sessions[request.sid]['last_activity'] = time.time()
+
+ # Store user's public key
+ room_keys[room_id][new_user_id] = public_key
+
+ # Join the room
+ join_room(room_id)
+ user_sessions[request.sid]['room'] = room_id
+
+ # Get room data
+ messages = chat_rooms[room_id].get_messages()
+ user_keys = room_keys[room_id]
+
+ # Send room joined confirmation
+ emit('room_joined', {
+ 'room_id': room_id,
+ 'user_id': new_user_id,
+ 'display_name': user_sessions[request.sid]['display_name'],
+ 'messages': messages,
+ 'users': list(user_keys.keys()),
+ 'user_keys': user_keys,
+ 'message_count': chat_rooms[room_id].get_message_count(),
+ 'is_first_user': is_first_user
+ })
+
+ # Notify others about new user
+ emit('user_joined', {
+ 'user_id': new_user_id,
+ 'display_name': user_sessions[request.sid]['display_name'],
+ 'public_key': public_key
+ }, room=room_id, include_self=False)
+
+ # Handle session key distribution
+ if is_first_user:
+ # First user - they will generate the session key on client side
+ logger.info(f"First user {new_user_id} joined room {room_id} - will generate session key")
+ else:
+ # Not first user - request session key from existing users
+ logger.info(f"User {new_user_id} joined room {room_id} - requesting session key")
+ emit('request_session_key', {
+ 'new_user_id': new_user_id,
+ 'public_key': public_key
+ }, room=room_id, include_self=False)
+
+ logger.info(f"User {new_user_id} joined room {room_id}")
+
+ except Exception as e:
+ logger.error(f"Error in join_room: {str(e)}")
+ emit('room_joined', {'error': 'Internal server error'})
+
+@socketio.on('send_message')
+@require_valid_session
+def handle_send_message(data):
+ try:
+ user_data = user_sessions[request.sid]
+ room_id = user_data['room']
+
+ if not room_id or room_id not in chat_rooms:
+ return
+
+ encrypted_content = data.get('encrypted_content', '')
+ iv = data.get('iv', '')
+
+ # Validate message
+ if not is_valid_message(encrypted_content, iv):
+ logger.warning(f"Invalid message from user {user_data['user_id']}")
+ return
+
+ # Check for duplicate messages
+ message_hash = hashlib.sha256(
+ f"{encrypted_content}{user_data['user_id']}{iv}".encode()
+ ).hexdigest()
+
+ if room_id not in message_hashes:
+ message_hashes[room_id] = set()
+
+ if message_hash in message_hashes[room_id]:
+ logger.warning(f"Duplicate message detected from user {user_data['user_id']}")
+ return
+
+ message_hashes[room_id].add(message_hash)
+
+ # Limit message hash storage
+ if len(message_hashes[room_id]) > MAX_MESSAGES * 2:
+ message_hashes[room_id] = set(list(message_hashes[room_id])[-MAX_MESSAGES:])
+
+ message_data = {
+ 'encrypted_content': encrypted_content,
+ 'sender_id': user_data['user_id'],
+ 'display_name': user_data['display_name'],
+ 'iv': iv
+ }
+
+ # Add to room buffer
+ chat_rooms[room_id].add_message(message_data)
+
+ # Update user activity
+ user_sessions[request.sid]['last_activity'] = time.time()
+
+ # Broadcast to room
+ emit('new_message', {
+ 'id': str(uuid.uuid4()),
+ 'encrypted_content': encrypted_content,
+ 'sender_id': user_data['user_id'],
+ 'display_name': user_data['display_name'],
+ 'timestamp': time.time(),
+ 'iv': iv,
+ 'message_count': chat_rooms[room_id].get_message_count()
+ }, room=room_id)
+
+ except Exception as e:
+ logger.error(f"Error in send_message: {str(e)}")
+
+@socketio.on('share_session_key')
+@require_valid_session
+def handle_share_session_key(data):
+ """Handle sharing of session key with a specific user"""
+ try:
+ room_id = data.get('room_id', '')
+ target_user_id = data.get('target_user_id', '')
+ encrypted_key = data.get('encrypted_key', '')
+
+ # Validation
+ if not room_id or not target_user_id or not encrypted_key:
+ logger.warning("Invalid share_session_key request")
+ return
+
+ if len(encrypted_key) > 2048: # Reasonable limit for encrypted session key
+ logger.warning("Encrypted key too large")
+ return
+
+ sender_id = user_sessions[request.sid]['user_id']
+ sender_room = user_sessions[request.sid].get('room')
+
+ # Verify sender is in the correct room
+ if room_id != sender_room:
+ logger.warning(f"User {sender_id} tried to share key for room {room_id} but is in {sender_room}")
+ return
+
+ # Find target user's socket session
+ target_sid = None
+ for sid, session in user_sessions.items():
+ if session.get('user_id') == target_user_id and session.get('room') == room_id:
+ target_sid = sid
+ break
+
+ if target_sid:
+ # Send the encrypted session key directly to the target user
+ emit('session_key_received', {
+ 'from_user_id': sender_id,
+ 'encrypted_key': encrypted_key
+ }, room=target_sid)
+ logger.info(f"Session key shared from {sender_id} to {target_user_id} in room {room_id}")
+ else:
+ logger.warning(f"Target user {target_user_id} not found in room {room_id}")
+
+ except Exception as e:
+ logger.error(f"Error in share_session_key: {str(e)}")
+
+@socketio.on('key_exchange')
+@require_valid_session
+def handle_key_exchange(data):
+ """Legacy key exchange handler - redirects to share_session_key"""
+ try:
+ # Map old format to new format
+ room_id = data.get('room_id', '')
+ target_user = data.get('target_user', '')
+ encrypted_key = data.get('encrypted_key', '')
+
+ if room_id and target_user and encrypted_key:
+ handle_share_session_key({
+ 'room_id': room_id,
+ 'target_user_id': target_user,
+ 'encrypted_key': encrypted_key
+ })
+ except Exception as e:
+ logger.error(f"Error in legacy key_exchange: {str(e)}")
+
+# Background cleanup task
+def start_cleanup_task():
+ def cleanup_worker():
+ while True:
+ try:
+ cleanup_expired_rooms()
+ time.sleep(ROOM_CLEANUP_INTERVAL)
+ except Exception as e:
+ logger.error(f"Error in cleanup task: {str(e)}")
+
+ cleanup_thread = threading.Thread(target=cleanup_worker, daemon=True)
+ cleanup_thread.start()
+
+# Error handlers
+@app.errorhandler(429)
+def ratelimit_handler(e):
+ return jsonify({'error': 'Rate limit exceeded'}), 429
+
+@app.errorhandler(404)
+def not_found(e):
+ return jsonify({'error': 'Not found'}), 404
+
+@app.errorhandler(500)
+def internal_error(e):
+ logger.error(f"Internal server error: {str(e)}")
+ return jsonify({'error': 'Internal server error'}), 500
+
+if __name__ == "__main__":
+ try:
+ socketio.run(app, debug=True, allow_unsafe_werkzeug=True)
+ except BrokenPipeError:
+ # Suppress noisy broken pipe errors (client disconnects)
+ import sys
+ print("Broken pipe error suppressed.", file=sys.stderr)
\ No newline at end of file
diff --git a/src/static/script.js b/src/static/script.js
new file mode 100644
index 0000000..e69de29
diff --git a/src/static/styles.css b/src/static/styles.css
new file mode 100644
index 0000000..2450522
--- /dev/null
+++ b/src/static/styles.css
@@ -0,0 +1,437 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
+ background: #0f0f0f;
+ color: #ececec;
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+.chat-container {
+ background: #212121;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ max-width: 1200px;
+ margin: 0 auto;
+ width: 100%;
+ box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
+}
+
+.chat-header {
+ background: #0f0f0f;
+ border-bottom: 1px solid #333;
+ padding: 16px 20px;
+ position: relative;
+ z-index: 100;
+}
+
+.header-content {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ max-width: 1000px;
+ margin: 0 auto;
+}
+
+.logo {
+ font-size: 18px;
+ font-weight: 600;
+ color: #fff;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.encryption-badge {
+ background: #10a37f;
+ color: white;
+ padding: 4px 8px;
+ border-radius: 12px;
+ font-size: 12px;
+ font-weight: 500;
+}
+
+.room-section {
+ background: #2d2d2d;
+ padding: 20px;
+ margin: 20px;
+ border-radius: 12px;
+ border: 1px solid #404040;
+}
+
+.room-input-container {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+ margin-top: 16px;
+}
+
+.room-input {
+ flex: 1;
+ background: #0f0f0f;
+ border: 1px solid #404040;
+ color: #ececec;
+ padding: 12px 16px;
+ border-radius: 8px;
+ font-size: 14px;
+ outline: none;
+ transition: border-color 0.2s;
+}
+
+.room-input:focus {
+ border-color: #10a37f;
+ box-shadow: 0 0 0 2px rgba(16, 163, 127, 0.1);
+}
+
+.btn {
+ background: #10a37f;
+ color: white;
+ border: none;
+ padding: 12px 20px;
+ border-radius: 8px;
+ cursor: pointer;
+ font-weight: 500;
+ font-size: 14px;
+ transition: all 0.2s;
+ white-space: nowrap;
+}
+
+.btn:hover {
+ background: #0d8c6a;
+}
+
+.btn-secondary {
+ background: #2d2d2d;
+ border: 1px solid #404040;
+}
+
+.btn-secondary:hover {
+ background: #3d3d3d;
+}
+
+.status-text {
+ color: #b4b4b4;
+ font-size: 14px;
+ margin-top: 12px;
+}
+
+.room-info {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 16px;
+ background: #2d2d2d;
+ border-radius: 8px;
+ font-size: 13px;
+ color: #b4b4b4;
+ margin-bottom: 16px;
+}
+
+.room-details {
+ display: flex;
+ gap: 20px;
+}
+
+.messages-container {
+ flex: 1;
+ overflow-y: auto;
+ padding: 20px;
+ background: #212121;
+ max-width: 1000px;
+ margin: 0 auto;
+ width: 100%;
+}
+
+.messages-container::-webkit-scrollbar {
+ width: 8px;
+}
+
+.messages-container::-webkit-scrollbar-track {
+ background: #2d2d2d;
+}
+
+.messages-container::-webkit-scrollbar-thumb {
+ background: #404040;
+ border-radius: 4px;
+}
+
+
+.message-group {
+ margin-bottom: 24px;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+}
+
+.message-group.sent {
+ align-items: flex-end;
+}
+
+.message-group.received {
+ align-items: flex-start;
+}
+
+.message-header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 8px;
+ gap: 8px;
+}
+
+.user-avatar {
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ background: linear-gradient(45deg, #10a37f, #0d8c6a);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ font-weight: bold;
+ color: white;
+}
+
+.user-avatar.own {
+ background: linear-gradient(45deg, #6366f1, #8b5cf6);
+}
+
+.user-name {
+ font-weight: 600;
+ font-size: 14px;
+ color: #ececec;
+}
+
+.message-time {
+ color: #888;
+ font-size: 12px;
+ margin-left: auto;
+}
+
+
+.message-content {
+ background: #2d2d2d;
+ padding: 12px 16px;
+ border-radius: 12px;
+ margin-left: 36px;
+ border: 1px solid #404040;
+ line-height: 1.5;
+ word-wrap: break-word;
+ max-width: 70%;
+ text-align: left;
+}
+
+.message-group.sent .message-content {
+ background: #0f0f0f;
+ border-color: #333;
+ margin-left: 0;
+ margin-right: 36px;
+ text-align: right;
+ align-self: flex-end;
+}
+
+.message-group.received .message-content {
+ align-self: flex-start;
+}
+
+.input-section {
+ background: #0f0f0f;
+ padding: 20px;
+ border-top: 1px solid #333;
+}
+
+.input-container {
+ max-width: 1000px;
+ margin: 0 auto;
+ position: relative;
+}
+
+.message-input {
+ width: 100%;
+ background: #2d2d2d;
+ border: 1px solid #404040;
+ color: #ececec;
+ padding: 16px 60px 16px 20px;
+ border-radius: 12px;
+ font-size: 16px;
+ outline: none;
+ resize: none;
+ min-height: 24px;
+ max-height: 200px;
+ font-family: inherit;
+ line-height: 1.5;
+ transition: border-color 0.2s;
+}
+
+.message-input:focus {
+ border-color: #10a37f;
+ box-shadow: 0 0 0 2px rgba(16, 163, 127, 0.1);
+}
+
+.message-input:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.send-button {
+ position: absolute;
+ right: 8px;
+ top: 50%;
+ transform: translateY(-50%);
+ background: #10a37f;
+ border: none;
+ width: 40px;
+ height: 40px;
+ border-radius: 8px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s;
+ color: white;
+}
+
+.send-button:hover:not(:disabled) {
+ background: #0d8c6a;
+}
+
+.send-button:disabled {
+ background: #404040;
+ cursor: not-allowed;
+}
+
+.welcome-screen {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ flex: 1;
+ text-align: center;
+ padding: 40px 20px;
+}
+
+.welcome-title {
+ font-size: 32px;
+ font-weight: 700;
+ margin-bottom: 16px;
+ background: linear-gradient(45deg, #10a37f, #6366f1);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.welcome-subtitle {
+ font-size: 18px;
+ color: #b4b4b4;
+ margin-bottom: 32px;
+ max-width: 600px;
+}
+
+.feature-list {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 20px;
+ margin-top: 32px;
+ max-width: 800px;
+}
+
+.feature-item {
+ background: #2d2d2d;
+ padding: 20px;
+ border-radius: 12px;
+ border: 1px solid #404040;
+}
+
+.feature-icon {
+ font-size: 24px;
+ margin-bottom: 12px;
+}
+
+.feature-title {
+ font-weight: 600;
+ margin-bottom: 8px;
+ color: #ececec;
+}
+
+.feature-desc {
+ color: #b4b4b4;
+ font-size: 14px;
+ line-height: 1.4;
+}
+
+.loading-message {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: #b4b4b4;
+ font-size: 14px;
+}
+
+.loading-spinner {
+ width: 16px;
+ height: 16px;
+ border: 2px solid #404040;
+ border-top: 2px solid #10a37f;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+.error-message {
+ background: #dc2626;
+ color: white;
+ padding: 12px 16px;
+ border-radius: 8px;
+ margin: 16px 0;
+ font-size: 14px;
+}
+
+.typing-indicator {
+ color: #888;
+ font-style: italic;
+ font-size: 13px;
+ padding: 8px 20px;
+}
+
+@media (max-width: 768px) {
+ .chat-container {
+ height: 100vh;
+ }
+
+ .header-content {
+ padding: 0 4px;
+ }
+
+ .room-input-container {
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ .room-input, .btn {
+ width: 100%;
+ }
+
+ .room-details {
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ .message-content {
+ margin-left: 0;
+ margin-top: 8px;
+ }
+
+ .feature-list {
+ grid-template-columns: 1fr;
+ }
+}
\ No newline at end of file
diff --git a/src/templates/chat.html b/src/templates/chat.html
new file mode 100644
index 0000000..abb7701
--- /dev/null
+++ b/src/templates/chat.html
@@ -0,0 +1,1560 @@
+
+
+
+
+
+ Secure Chat Platform
+
+
+
+
+
+
+
+
+
+
+
+
End-to-End Encrypted Chat
+
+ Your messages are encrypted in your browser before being sent.
+ Messages are stored temporarily in memory and automatically deleted after 256 messages.
+
+
+
+
+
+
+ Join Room
+ Create New Room
+
+
+
+
+
Initializing encryption keys...
+
+
+
+
+
+
+
đ
+
End-to-End Encryption
+
Messages are encrypted with AES-256 in your browser. The server never sees your messages.
+
+
+
âĄ
+
Ephemeral Messages
+
Only 256 messages stored in memory. Older messages are automatically deleted.
+
+
+
đ
+
Real-time Chat
+
Instant messaging with WebSocket technology for immediate message delivery.
+
+
+
đ
+
Anonymous Users
+
Each session gets a new random UUID. No registration or personal info required.
+
+
+
+
+
+
+
+
+ Room:
+ Messages: 0/256
+
+
+ Users: 0
+ đ Encrypted
+
+
+
+
+
+
+
+
+
+
+
+
+ đ End-to-End Encrypted
+
+
+
+
+
\ No newline at end of file