fixed some stuff mainly chat interface and made some stuff work

This commit is contained in:
2025-08-23 22:31:05 +02:00
parent 1c5768aab6
commit e2aad6903f
6 changed files with 1523 additions and 1829 deletions

28
dockerfile Normal file
View File

@@ -0,0 +1,28 @@
# Use an official lightweight Python image
FROM python:3.11-slim
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Create a working directory
WORKDIR /app
# Install system dependencies (if needed)
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy the application code
COPY . .
# Expose port
EXPOSE 3248
# Run with Gunicorn WSGI server
# Assumes your Flask app is named "app" inside "app.py"
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:3248", "app:app"]

BIN
src/static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -0,0 +1,925 @@
// Content Security Policy enforcement
if (!window.crypto || !window.crypto.subtle) {
alert('This browser does not support required cryptographic features. Please use a modern browser.');
throw new Error('WebCrypto API not available');
}
// Global variables
let socket = null;
let currentRoom = null;
let currentUserId = null;
let currentDisplayName = null;
let keyPair = null;
let sessionKey = null;
let roomUsers = {};
let keysReady = false;
let lastMessageTime = 0;
let messageQueue = [];
let isConnected = false;
let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 5;
const MESSAGE_RATE_LIMIT = 1000;
const MAX_MESSAGE_LENGTH = 4000;
const MAX_ROOM_ID_LENGTH = 32;
const MAX_MESSAGES = 256;
// Security utilities
const SecurityUtils = {
sanitizeInput: function(input) {
if (typeof input !== 'string') return '';
return input.trim().substring(0, MAX_MESSAGE_LENGTH);
},
isValidRoomId: function(roomId) {
if (!roomId || typeof roomId !== 'string') return false;
if (roomId.length > MAX_ROOM_ID_LENGTH) return false;
return /^[a-zA-Z0-9\-_]+$/.test(roomId);
},
canSendMessage: function() {
const now = Date.now();
if (now - lastMessageTime < MESSAGE_RATE_LIMIT) {
return false;
}
lastMessageTime = now;
return true;
},
generateSecureId: function(length = 8) {
const array = new Uint8Array(length);
crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('').toUpperCase().substring(0, length);
},
isValidBase64: function(str) {
try {
return btoa(atob(str)) === str;
} catch (err) {
return false;
}
}
};
class SecureChatError extends Error {
constructor(message, code = 'UNKNOWN') {
super(message);
this.name = 'SecureChatError';
this.code = code;
}
}
// Initialize the app
async function initializeApp() {
try {
console.log("Initializing bytechat");
// Initialize socket connection
socket = io({
transports: ['websocket', 'polling'],
reconnection: true,
reconnectionAttempts: MAX_RECONNECT_ATTEMPTS,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
timeout: 10000
});
await generateKeyPair();
setupSocketListeners();
setupSecurityFeatures();
} catch (error) {
console.error('Failed to initialize app:', error);
updateStatus('Failed to initialize application', true);
}
}
function setupSecurityFeatures() {
document.addEventListener('visibilitychange', function() {
if (document.hidden) {
const messageInput = document.getElementById('messageInput');
if (messageInput) {
messageInput.value = '';
}
}
});
window.addEventListener('beforeunload', function() {
clearSensitiveData();
});
const securityIndicator = document.getElementById('securityIndicator');
securityIndicator.style.display = 'block';
}
function clearSensitiveData() {
sessionKey = null;
keyPair = null;
roomUsers = {};
const passwordInput = document.getElementById('roomPasswordInput');
if (passwordInput) {
passwordInput.value = '';
}
}
async function generateKeyPair() {
try {
updateStatus('Generating encryption keys...', false);
keyPair = await window.crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 2048, // Reduced from 4096 for better performance
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256"
},
false,
["encrypt", "decrypt"]
);
keysReady = true;
updateStatus("Ready to join a room", false);
console.log("Key pair generated successfully");
} catch (error) {
console.error("Failed to generate key pair:", error);
updateStatus("Error: Failed to generate encryption keys", true);
throw new SecureChatError("Key generation failed", "KEY_GEN_ERROR");
}
}
async function generateSessionKey() {
try {
sessionKey = await window.crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
console.log("Session key generated");
return sessionKey;
} catch (error) {
console.error("Failed to generate session key:", error);
throw new SecureChatError("Session key generation failed", "SESSION_KEY_ERROR");
}
}
async function exportPublicKey(key) {
try {
const exported = await window.crypto.subtle.exportKey("spki", key);
const base64Key = btoa(String.fromCharCode(...new Uint8Array(exported)));
if (!SecurityUtils.isValidBase64(base64Key)) {
throw new Error('Invalid key export');
}
return base64Key;
} catch (error) {
console.error("Failed to export public key:", error);
throw new SecureChatError("Public key export failed", "KEY_EXPORT_ERROR");
}
}
async function importPublicKey(keyString) {
try {
if (!SecurityUtils.isValidBase64(keyString)) {
throw new Error('Invalid base64 key format');
}
const keyData = Uint8Array.from(atob(keyString), c => c.charCodeAt(0));
return await window.crypto.subtle.importKey(
"spki",
keyData,
{ name: "RSA-OAEP", hash: "SHA-256" },
false,
["encrypt"]
);
} catch (error) {
console.error("Failed to import public key:", error);
throw new SecureChatError("Public key import failed", "KEY_IMPORT_ERROR");
}
}
async function encryptSessionKey(sessionKey, publicKey) {
try {
const keyData = await window.crypto.subtle.exportKey("raw", sessionKey);
const encrypted = await window.crypto.subtle.encrypt(
{ name: "RSA-OAEP" },
publicKey,
keyData
);
return btoa(String.fromCharCode(...new Uint8Array(encrypted)));
} catch (error) {
console.error("Failed to encrypt session key:", error);
throw new SecureChatError("Session key encryption failed", "SESSION_KEY_ENCRYPT_ERROR");
}
}
async function decryptSessionKey(encryptedKey) {
try {
if (!SecurityUtils.isValidBase64(encryptedKey)) {
throw new Error('Invalid encrypted key format');
}
const keyData = Uint8Array.from(atob(encryptedKey), c => c.charCodeAt(0));
const decrypted = await window.crypto.subtle.decrypt(
{ name: "RSA-OAEP" },
keyPair.privateKey,
keyData
);
return await window.crypto.subtle.importKey(
"raw",
decrypted,
{ name: "AES-GCM" },
false,
["encrypt", "decrypt"]
);
} catch (error) {
console.error("Failed to decrypt session key:", error);
throw new SecureChatError("Session key decryption failed", "SESSION_KEY_DECRYPT_ERROR");
}
}
async function encryptMessage(message) {
if (!sessionKey) {
throw new SecureChatError("No session key available", "NO_SESSION_KEY");
}
try {
const sanitizedMessage = SecurityUtils.sanitizeInput(message);
const encoder = new TextEncoder();
const data = encoder.encode(sanitizedMessage);
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const encrypted = await window.crypto.subtle.encrypt(
{ name: "AES-GCM", iv: iv },
sessionKey,
data
);
return {
encrypted: btoa(String.fromCharCode(...new Uint8Array(encrypted))),
iv: btoa(String.fromCharCode(...iv))
};
} catch (error) {
console.error("Failed to encrypt message:", error);
throw new SecureChatError("Message encryption failed", "MESSAGE_ENCRYPT_ERROR");
}
}
async function decryptMessage(encryptedData, ivString) {
if (!sessionKey) {
return "[Encrypted message - no session key]";
}
try {
if (!SecurityUtils.isValidBase64(encryptedData) || !SecurityUtils.isValidBase64(ivString)) {
return "[Invalid encrypted message format]";
}
const encrypted = Uint8Array.from(atob(encryptedData), c => c.charCodeAt(0));
const iv = Uint8Array.from(atob(ivString), c => c.charCodeAt(0));
const decrypted = await window.crypto.subtle.decrypt(
{ name: "AES-GCM", iv: iv },
sessionKey,
encrypted
);
const decoder = new TextDecoder();
const decryptedMessage = decoder.decode(decrypted);
return SecurityUtils.sanitizeInput(decryptedMessage);
} catch (error) {
console.error("Failed to decrypt message:", error);
return "[Failed to decrypt message]";
}
}
// Enhanced socket listeners with fixed key exchange
function setupSocketListeners() {
socket.on('connect', () => {
isConnected = true;
reconnectAttempts = 0;
console.log("Socket connected securely");
updateConnectionStatus(true);
updateInputState();
// If user is already in a room, re-emit join_room after reconnect
if (currentRoom && keyPair && keysReady) {
exportPublicKey(keyPair.publicKey).then((publicKeyString) => {
socket.emit('join_room', {
room_id: currentRoom,
public_key: publicKeyString,
password: ''
});
});
}
});
socket.on('disconnect', (reason) => {
isConnected = false;
updateStatus("Connection lost. Attempting to reconnect...", true);
updateConnectionStatus(false);
updateInputState();
console.log('Socket disconnected:', reason);
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
reconnectAttempts++;
setTimeout(() => {
if (!isConnected && reconnectAttempts <= MAX_RECONNECT_ATTEMPTS) {
socket.connect();
}
}, 2000 * Math.pow(2, reconnectAttempts));
} else {
updateStatus("Connection failed. Please refresh the page.", true);
updateInputState();
}
});
socket.on('connect_error', (error) => {
console.error('Connection error:', error);
updateStatus("Connection error. Please check your internet connection.", true);
});
socket.on('user_connected', (data) => {
if (data && data.user_id) {
currentUserId = data.user_id;
console.log("Connected as user:", currentUserId);
}
});
socket.on('room_joined', async (data) => {
try {
if (data.error) {
updateStatus(data.error, true);
updateInputState();
return;
}
if (!data.room_id || !data.user_id || !SecurityUtils.isValidRoomId(data.room_id)) {
throw new SecureChatError("Invalid room data received", "INVALID_ROOM_DATA");
}
currentRoom = data.room_id;
currentUserId = data.user_id;
currentDisplayName = data.display_name;
roomUsers = data.user_keys || {};
// Switch to chat screen
document.getElementById('welcomeScreen').style.display = 'none';
document.getElementById('chatScreen').style.display = 'flex';
document.getElementById('roomInfo').style.display = 'flex';
document.getElementById('inputSection').style.display = 'block';
// Update room info
document.getElementById('currentRoomId').textContent = currentRoom;
document.getElementById('userCounter').textContent = `Users: ${data.users?.length || 0}`;
document.getElementById('messageCounter').textContent = `Messages: ${data.message_count || 0}/256`;
// Handle session key based on user status
if (data.is_first_user) {
console.log("First user - generating session key");
await generateSessionKey();
updateStatus("You're the first user! Session key generated. Others will receive it when they join.", false);
updateInputState();
} else if (sessionKey) {
console.log("Rejoining with existing session key");
updateStatus("Session key present. You can chat.", false);
updateInputState();
} else {
console.log("Waiting for session key from existing users");
updateStatus("Waiting for session key from other users...", false);
updateInputState();
}
// Load existing messages
if (data.messages && Array.isArray(data.messages)) {
for (const msg of data.messages) {
if (msg && typeof msg === 'object') {
await displayMessage(msg);
}
}
}
console.log("[room_joined] sessionKey:", !!sessionKey, "isFirstUser:", data.is_first_user);
} catch (error) {
console.error("Error processing room_joined:", error);
updateStatus("Error joining room", true);
updateInputState();
}
});
socket.on('user_joined', async (data) => {
try {
if (!data || !data.user_id || !data.public_key) return;
roomUsers[data.user_id] = data.public_key;
updateUsersCount();
addSystemMessage(`${data.display_name || 'User'} joined the room`);
// If we have a session key, share it with the new user
if (sessionKey) {
console.log("Sharing session key with new user:", data.user_id);
try {
const publicKey = await importPublicKey(data.public_key);
const encryptedKey = await encryptSessionKey(sessionKey, publicKey);
socket.emit('share_session_key', {
room_id: currentRoom,
target_user_id: data.user_id,
encrypted_key: encryptedKey
});
console.log("Session key shared with new user successfully");
} catch (error) {
console.error("Failed to share session key with new user:", error);
}
}
} catch (error) {
console.error("Error processing user_joined:", error);
}
});
socket.on('request_session_key', async (data) => {
try {
console.log("Session key requested for new user:", data.new_user_id);
if (!sessionKey || !data.new_user_id || !data.public_key) {
console.log("Cannot fulfill session key request - missing data");
return;
}
try {
const publicKey = await importPublicKey(data.public_key);
const encryptedKey = await encryptSessionKey(sessionKey, publicKey);
socket.emit('share_session_key', {
room_id: currentRoom,
target_user_id: data.new_user_id,
encrypted_key: encryptedKey
});
console.log("Session key shared in response to request");
} catch (error) {
console.error("Failed to share session key on request:", error);
}
} catch (error) {
console.error("Error processing request_session_key:", error);
}
});
socket.on('session_key_received', async (data) => {
try {
console.log("Session key received from:", data.from_user_id);
if (!data || !data.encrypted_key) {
console.log("Invalid session key data received");
return;
}
if (!sessionKey) {
try {
sessionKey = await decryptSessionKey(data.encrypted_key);
console.log("Session key decrypted successfully");
updateStatus("Session key received! You can now chat securely.", false);
updateInputState();
addSystemMessage("🔑 Session key received - you can now chat!");
} catch (error) {
console.error("Failed to decrypt received session key:", error);
updateStatus("Failed to decrypt session key", true);
}
} else {
console.log("Session key already exists, ignoring duplicate");
}
} catch (error) {
console.error("Failed to process session key:", error);
updateStatus("Error: Failed to decrypt session key", true);
}
});
socket.on('new_message', async (data) => {
try {
if (!data || typeof data !== 'object') return;
await displayMessage(data);
if (data.message_count) {
document.getElementById('messageCounter').textContent = `Messages: ${data.message_count}/256`;
}
} catch (error) {
console.error("Error processing new message:", error);
}
});
socket.on('user_left', (data) => {
try {
if (data && data.user_id) {
delete roomUsers[data.user_id];
updateUsersCount();
addSystemMessage(`User left the room`);
}
} catch (error) {
console.error("Error processing user_left:", error);
}
});
socket.on('error', (error) => {
console.error('Socket error:', error);
updateStatus("Connection error occurred", true);
});
socket.on('ping', () => {
socket.emit('pong');
});
}
async function createRoom() {
try {
const roomId = SecurityUtils.generateSecureId(6);
const password = document.getElementById('roomPasswordInput').value;
await joinSpecificRoom(roomId, password);
} catch (error) {
console.error("Failed to create room:", error);
updateStatus("Error creating room", true);
}
}
async function joinRoom() {
try {
const roomInput = document.getElementById('roomInput');
const roomId = SecurityUtils.sanitizeInput(roomInput.value);
const password = document.getElementById('roomPasswordInput').value;
if (!roomId) {
await createRoom();
return;
}
if (!SecurityUtils.isValidRoomId(roomId)) {
updateStatus("Invalid room ID format. Use only letters, numbers, hyphens and underscores.", true);
return;
}
await joinSpecificRoom(roomId, password);
} catch (error) {
console.error("Failed to join room:", error);
updateStatus("Error joining room", true);
}
}
async function joinSpecificRoom(roomId, password = "") {
if (!keysReady) {
updateStatus("Error: Encryption keys not ready", true);
return;
}
if (!SecurityUtils.isValidRoomId(roomId)) {
updateStatus("Invalid room ID format", true);
return;
}
try {
updateStatus("Joining room...", false);
const publicKeyString = await exportPublicKey(keyPair.publicKey);
socket.emit('join_room', {
room_id: roomId,
public_key: publicKeyString,
password: password
});
} catch (error) {
console.error("Failed to join room:", error);
updateStatus("Error: Failed to join room", true);
}
}
async function sendMessage() {
try {
const input = document.getElementById('messageInput');
let message = SecurityUtils.sanitizeInput(input.value);
if (!message || !sessionKey) return;
if (!SecurityUtils.canSendMessage()) {
updateStatus("Please wait before sending another message", true);
return;
}
if (message.length > MAX_MESSAGE_LENGTH) {
updateStatus("Message too long", true);
return;
}
const encryptedData = await encryptMessage(message);
socket.emit('send_message', {
encrypted_content: encryptedData.encrypted,
iv: encryptedData.iv
});
input.value = '';
autoResize(input);
} catch (error) {
console.error("Failed to send message:", error);
updateStatus("Error: Failed to send message", true);
}
}
async function displayMessage(messageData) {
try {
if (!messageData || typeof messageData !== 'object') return;
const container = document.getElementById('messagesContainer');
const isOwnMessage = messageData.sender_id === currentUserId;
let displayText;
if (messageData.iv && sessionKey && messageData.encrypted_content) {
displayText = await decryptMessage(messageData.encrypted_content, messageData.iv);
} else {
displayText = "[Encrypted message]";
}
const escapeHtml = (text) => {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
};
const timestamp = messageData.timestamp ?
new Date(messageData.timestamp * 1000).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
}) :
new Date().toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
});
const displayName = messageData.display_name || `User-${messageData.sender_id?.substring(0, 8) || 'Unknown'}`;
const avatarText = displayName.substring(0, 2).toUpperCase();
const messageGroup = document.createElement('div');
messageGroup.className = 'message-group';
messageGroup.classList.add(isOwnMessage ? 'sent' : 'received');
messageGroup.innerHTML = `
<div class="message-header">
<div class="user-avatar ${isOwnMessage ? 'own' : ''}">${escapeHtml(avatarText)}</div>
<span class="user-name">${escapeHtml(displayName)}</span>
<span class="message-time">${escapeHtml(timestamp)}</span>
</div>
<div class="message-content ${isOwnMessage ? 'own' : ''}" data-message-id="${messageData.id || ''}"></div>
`;
const messageContentEl = messageGroup.querySelector('.message-content');
messageContentEl.textContent = displayText;
container.appendChild(messageGroup);
container.scrollTop = container.scrollHeight;
const messages = container.children;
if (messages.length > MAX_MESSAGES + 50) {
for (let i = 0; i < 50; i++) {
if (messages[0]) {
container.removeChild(messages[0]);
}
}
}
} catch (error) {
console.error("Error displaying message:", error);
}
}
function addSystemMessage(text) {
try {
const container = document.getElementById('messagesContainer');
const systemMessage = document.createElement('div');
systemMessage.className = 'message-group system'; // ✅ add system class
const timestamp = new Date().toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'});
systemMessage.innerHTML = `
<div class="message-header">
<div class="user-avatar"></div>
<span class="user-name">System</span>
<span class="message-time">${timestamp}</span>
</div>
<div class="message-content"></div>
`;
const messageContentEl = systemMessage.querySelector('.message-content');
messageContentEl.textContent = text;
container.appendChild(systemMessage);
container.scrollTop = container.scrollHeight;
} catch (error) {
console.error("Error adding system message:", error);
}
}
function updateStatus(message, isError = false) {
try {
const statusEl = document.getElementById('statusText');
const statusDiv = document.createElement('div');
if (isError) {
statusDiv.className = 'error-message';
} else {
statusDiv.className = 'success-message';
}
statusDiv.textContent = message;
statusEl.innerHTML = '';
statusEl.appendChild(statusDiv);
} catch (error) {
console.error("Error updating status:", error);
}
}
function updateUsersCount() {
try {
const count = Object.keys(roomUsers).length;
const counterEl = document.getElementById('userCounter');
if (counterEl) {
counterEl.textContent = `Users: ${count}`;
}
} catch (error) {
console.error("Error updating users count:", error);
}
}
function updateInputState() {
try {
const input = document.getElementById('messageInput');
const btn = document.getElementById('sendButton');
if (!input || !btn) return;
if (sessionKey && isConnected) {
input.disabled = false;
btn.disabled = false;
input.placeholder = "Message ByteChat…";
} else {
input.disabled = true;
btn.disabled = true;
input.placeholder = isConnected ? "Waiting for encryption key..." : "Disconnected...";
}
} catch (error) {
console.error("Error updating input state:", error);
}
}
function autoResize(textarea) {
try {
if (!textarea) return;
textarea.style.height = 'auto';
const newHeight = Math.min(Math.max(textarea.scrollHeight, 44), 200);
textarea.style.height = newHeight + 'px';
} catch (error) {
console.error("Error in autoResize:", error);
}
}
function updateConnectionStatus(connected) {
const encryptionBadge = document.getElementById('encryptionBadge');
const securityIndicator = document.getElementById('securityIndicator');
if (connected) {
if (encryptionBadge) encryptionBadge.textContent = 'E2E Encrypted';
if (securityIndicator) securityIndicator.textContent = '🔒 End-to-End Encrypted';
} else {
if (encryptionBadge) encryptionBadge.textContent = 'Disconnected';
if (securityIndicator) securityIndicator.textContent = '⚠️ Disconnected';
}
}
// Event listeners setup
document.addEventListener('DOMContentLoaded', () => {
try {
const messageInput = document.getElementById('messageInput');
const roomInput = document.getElementById('roomInput');
const roomPasswordInput = document.getElementById('roomPasswordInput');
if (messageInput) {
messageInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
messageInput.addEventListener('input', (e) => {
autoResize(e.target);
if (e.target.value.length > MAX_MESSAGE_LENGTH) {
e.target.value = e.target.value.substring(0, MAX_MESSAGE_LENGTH);
updateStatus(`Message truncated to ${MAX_MESSAGE_LENGTH} characters`, true);
}
});
messageInput.addEventListener('paste', (e) => {
setTimeout(() => {
if (messageInput.value.length > MAX_MESSAGE_LENGTH) {
messageInput.value = messageInput.value.substring(0, MAX_MESSAGE_LENGTH);
updateStatus(`Pasted content truncated to ${MAX_MESSAGE_LENGTH} characters`, true);
}
autoResize(messageInput);
}, 0);
});
}
if (roomInput) {
roomInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
joinRoom();
}
});
roomInput.addEventListener('input', (e) => {
const value = e.target.value;
if (value && !SecurityUtils.isValidRoomId(value)) {
e.target.setCustomValidity('Only letters, numbers, hyphens and underscores allowed');
} else {
e.target.setCustomValidity('');
}
});
}
if (roomPasswordInput) {
roomPasswordInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
joinRoom();
}
});
}
const joinRoomBtn = document.getElementById('joinRoomBtn');
if (joinRoomBtn) joinRoomBtn.addEventListener('click', joinRoom);
const createRoomBtn = document.getElementById('createRoomBtn');
if (createRoomBtn) createRoomBtn.addEventListener('click', createRoom);
const sendButton = document.getElementById('sendButton');
if (sendButton) sendButton.addEventListener('click', sendMessage);
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter' && messageInput && !messageInput.disabled) {
e.preventDefault();
sendMessage();
}
if (e.key === 'Escape' && messageInput) {
messageInput.value = '';
autoResize(messageInput);
}
});
initializeApp();
} catch (error) {
console.error("Error setting up event listeners:", error);
updateStatus("Error initializing interface", true);
}
});
// Periodic connection health check
setInterval(() => {
if (socket && isConnected) {
socket.volatile.emit('ping');
}
}, 30000);
// Prevent clickjacking
if (top !== self) {
top.location = self.location;
}
// CSP violation reporting
document.addEventListener('securitypolicyviolation', (e) => {
console.error('CSP Violation:', {
violatedDirective: e.violatedDirective,
blockedURI: e.blockedURI,
lineNumber: e.lineNumber,
columnNumber: e.columnNumber
});
});
// Expose necessary functions
window.SecureChat = {
joinRoom,
createRoom,
sendMessage
};
// Prevent console access in production
if (location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') {
console.log = console.warn = console.error = () => {};
}

View File

@@ -1,437 +0,0 @@
* {
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;
}
}

548
src/static/stylesheet.css Normal file
View File

@@ -0,0 +1,548 @@
/* Reset & base */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
overflow: hidden; /* prevents double scrollbars */
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 100%);
color: #ffffff;
display: flex;
flex-direction: column;
}
/* Main container */
.chat-container {
flex: 1;
display: flex;
flex-direction: column;
height: 100vh;
max-width: 100vw;
}
/* Header */
.chat-header {
background: rgba(20, 20, 20, 0.95);
border-bottom: 1px solid #333;
padding: 1rem 1.5rem;
backdrop-filter: blur(10px);
flex-shrink: 0;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 1.5rem;
font-weight: 700;
color: #00ff88;
}
.encryption-badge {
background: linear-gradient(45deg, #00ff88, #00cc6a);
color: #000;
padding: 0.5rem 1rem;
border-radius: 20px;
font-weight: 600;
font-size: 0.85rem;
}
/* Welcome screen */
.welcome-screen {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 2rem;
text-align: center;
}
.welcome-title {
font-size: 3rem;
font-weight: 700;
margin-bottom: 1rem;
background: linear-gradient(45deg, #00ff88, #ffffff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.welcome-subtitle {
font-size: 1.1rem;
color: #aaa;
margin-bottom: 3rem;
max-width: 600px;
line-height: 1.6;
}
.room-section {
margin-bottom: 3rem;
}
.room-input-container {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
flex-wrap: wrap;
justify-content: center;
}
.room-input {
padding: 0.75rem 1rem;
border: 2px solid #333;
border-radius: 8px;
background: #1a1a1a;
color: #fff;
font-size: 1rem;
min-width: 200px;
}
.room-input:focus {
outline: none;
border-color: #00ff88;
box-shadow: 0 0 0 3px rgba(0, 255, 136, 0.1);
}
/* Buttons */
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
background: linear-gradient(45deg, #00ff88, #00cc6a);
color: #000;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
font-size: 1rem;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 255, 136, 0.3);
}
.btn-secondary {
background: linear-gradient(45deg, #333, #555);
color: #fff;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* Status text */
.status-text {
color: #aaa;
margin-top: 1rem;
min-height: 24px;
}
.loading-message {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.loading-spinner {
width: 16px;
height: 16px;
border: 2px solid #333;
border-top: 2px solid #00ff88;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.error-message {
color: #ff4444;
font-weight: 600;
}
.success-message {
color: #00ff88;
font-weight: 600;
}
/* Features */
.feature-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
max-width: 1000px;
margin-top: 2rem;
}
.feature-item {
background: rgba(255, 255, 255, 0.03);
border: 1px solid #333;
border-radius: 12px;
padding: 1.5rem;
text-align: center;
}
.feature-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.feature-title {
font-weight: 600;
margin-bottom: 0.5rem;
color: #fff;
}
.feature-desc {
color: #aaa;
font-size: 0.9rem;
line-height: 1.4;
}
/* Room info */
.room-info {
background: rgba(20, 20, 20, 0.95);
border-bottom: 1px solid #333;
padding: 0.75rem 1rem;
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
gap: 0.75rem;
position: sticky;
top: 0;
z-index: 10;
}
.room-details {
display: flex;
flex-direction: column;
gap: 0.5rem;
align-items: flex-start;
flex: 0 0 auto;
min-width: auto;
}
.room-details span {
color: #aaa;
font-size: 0.85rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.room-details strong {
color: #00ff88;
}
.room-actions {
display: flex;
gap: 0.5rem;
align-items: center;
}
/* Chat screen & messages */
.chat-screen {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.messages-wrapper {
flex: 1;
overflow-y: auto;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
scroll-behavior: smooth;
padding-bottom: 80px; /* leave space for input */
}
.message-group {
display: flex;
flex-direction: column;
animation: fadeIn 0.3s ease-out;
}
/* Align left vs right */
.message-group.sent {
align-items: flex-end;
margin-left: auto;
}
.message-group.received {
align-items: flex-start;
margin-right: auto;
}
.message-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.25rem;
font-size: 0.8rem;
color: #888;
}
.message-group.sent .message-header {
flex-direction: row-reverse;
}
/* System messages */
.message-group.system {
align-items: center;
justify-content: center;
margin: 0.5rem auto;
text-align: center;
max-width: 100%;
}
.message-group.system .message-content {
background: rgba(255, 255, 255, 0.08);
border: 1px solid #444;
color: #ccc;
font-size: 0.85rem;
font-style: italic;
border-radius: 12px;
padding: 0.35rem 0.75rem;
width: fit-content; /* 👈 shrink to text */
max-width: 90%; /* still wrap if very long */
text-align: center;
}
.user-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
background: linear-gradient(45deg, #00ff88, #00cc6a);
color: #000;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
font-weight: 700;
}
.user-avatar.own {
background: linear-gradient(45deg, #0088ff, #0066cc);
color: #fff;
}
.user-name {
font-weight: 600;
}
.message-time {
font-size: 0.75rem;
opacity: 0.7;
}
/* The bubble */
.message-content {
display: inline-block;
background: #2d2d2d;
border: 1px solid #404040;
border-radius: 12px;
padding: 0.5rem 0.75rem;
word-wrap: break-word;
max-width: 75%; /* prevents overly long messages */
width: auto; /* shrink to fit content */
position: relative;
text-align: left;
}
.message-group.sent .message-content {
background: #1a4d3a;
border-color: #00ff88;
text-align: left; /* keep text natural */
}
/* Input section */
.input-section {
padding: 0.75rem 1rem;
background: rgba(20, 20, 20, 0.95);
border-top: 1px solid #333;
position: sticky;
bottom: 0;
left: 0;
width: 100%;
flex-shrink: 0;
}
.input-container {
display: flex;
gap: 0.5rem;
align-items: flex-end;
}
.message-input {
flex: 1;
border: 2px solid #333;
border-radius: 12px;
background: #1a1a1a;
color: #fff;
padding: 0.75rem 1rem;
font-size: 1rem;
font-family: inherit;
resize: none;
max-height: 120px;
min-height: 44px;
}
.message-input:focus {
outline: none;
border-color: #00ff88;
box-shadow: 0 0 0 3px rgba(0, 255, 136, 0.1);
}
.message-input:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.send-button {
width: 44px;
height: 44px;
border: none;
border-radius: 50%;
background: linear-gradient(45deg, #00ff88, #00cc6a);
color: #000;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
flex-shrink: 0;
}
.send-button:hover:not(:disabled) {
transform: scale(1.1);
box-shadow: 0 4px 15px rgba(0, 255, 136, 0.3);
}
.send-button:disabled {
opacity: 0.3;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* Security indicator */
.security-indicator {
position: fixed;
bottom: calc(1rem + 70px);
right: 20px;
background: rgba(0, 0, 0, 0.8);
color: #00ff88;
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.8rem;
border: 1px solid #00ff88;
z-index: 1000;
}
/* Animations */
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* Responsive */
@media (max-width: 768px) {
.welcome-title {
font-size: 2rem;
}
.room-input-container {
flex-direction: column;
align-items: stretch;
}
.room-input {
min-width: 100%;
}
.feature-list {
grid-template-columns: 1fr;
}
.room-info {
flex-direction: column;
align-items: flex-start;
gap: 0.75rem;
padding: 0.75rem;
}
.room-details {
width: 100%;
}
.room-actions {
width: 100%;
justify-content: flex-end;
}
.message-content {
max-width: 85%;
}
.input-section {
padding: 0.75rem;
}
}
@media (max-width: 480px) {
.room-details span {
font-size: 0.8rem;
}
.btn {
padding: 0.6rem 1rem;
font-size: 0.9rem;
}
.message-content {
max-width: 90%;
}
}
/* Accessibility */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.btn:focus,
.room-input:focus,
.message-input:focus,
.send-button:focus {
outline: 2px solid #00ff88;
outline-offset: 2px;
}

File diff suppressed because it is too large Load Diff