Files
msgs/web/index.html
T
2025-11-23 17:03:48 +07:00

868 lines
29 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Secure Chat App</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.container {
width: 90%;
max-width: 1200px;
height: 90vh;
background: white;
border-radius: 15px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
display: flex;
overflow: hidden;
}
.auth-container {
width: 400px;
margin: 0 auto;
background: white;
padding: 40px;
border-radius: 15px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
}
.auth-tabs {
display: flex;
margin-bottom: 30px;
border-bottom: 1px solid #e9ecef;
}
.auth-tab {
flex: 1;
padding: 15px;
text-align: center;
cursor: pointer;
border-bottom: 3px solid transparent;
transition: all 0.3s ease;
}
.auth-tab.active {
border-bottom-color: #3498db;
color: #3498db;
font-weight: 600;
}
.auth-form {
display: none;
}
.auth-form.active {
display: block;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #2c3e50;
}
.form-control {
width: 100%;
padding: 15px 20px;
border: 2px solid #e9ecef;
border-radius: 10px;
font-size: 16px;
outline: none;
transition: border-color 0.3s ease;
}
.form-control:focus {
border-color: #3498db;
}
.btn {
width: 100%;
padding: 15px;
background: linear-gradient(135deg, #3498db, #2980b9);
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(52, 152, 219, 0.4);
}
.alert {
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 20px;
display: none;
}
.alert-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
/* Chat styles remain the same as before */
.chat-container {
width: 100%;
height: 100%;
display: none;
}
.users-panel {
width: 300px;
background: #2c3e50;
color: white;
padding: 20px;
display: flex;
flex-direction: column;
}
.users-panel h2 {
margin-bottom: 20px;
text-align: center;
font-weight: 300;
border-bottom: 1px solid #34495e;
padding-bottom: 10px;
}
.user-list {
list-style: none;
flex: 1;
overflow-y: auto;
}
.user-item {
padding: 15px;
margin: 8px 0;
background: #34495e;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
border-left: 4px solid transparent;
}
.user-item:hover {
background: #3d566e;
transform: translateX(5px);
}
.user-item.active {
background: #3498db;
border-left-color: #2980b9;
}
.chat-panel {
flex: 1;
display: flex;
flex-direction: column;
background: #f8f9fa;
}
.chat-header {
padding: 20px;
background: white;
border-bottom: 1px solid #e9ecef;
text-align: center;
font-weight: 600;
color: #2c3e50;
display: flex;
justify-content: space-between;
align-items: center;
}
.logout-btn {
padding: 8px 16px;
background: #e74c3c;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
}
.logout-btn:hover {
background: #c0392b;
}
.chat-messages {
flex: 1;
padding: 20px;
overflow-y: auto;
background: #f8f9fa;
}
.message {
max-width: 70%;
margin: 15px 0;
padding: 15px 20px;
border-radius: 20px;
position: relative;
word-wrap: break-word;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.incoming {
background: white;
border: 1px solid #e9ecef;
align-self: flex-start;
border-bottom-left-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
}
.outgoing {
background: linear-gradient(135deg, #3498db, #2980b9);
color: white;
align-self: flex-end;
margin-left: auto;
border-bottom-right-radius: 5px;
box-shadow: 0 2px 5px rgba(52, 152, 219, 0.3);
}
.message-sender {
font-size: 12px;
font-weight: 600;
margin-bottom: 5px;
opacity: 0.8;
}
.message-content {
margin: 8px 0;
line-height: 1.4;
}
.message-time {
font-size: 11px;
text-align: right;
opacity: 0.7;
margin-top: 5px;
}
.chat-input-area {
padding: 20px;
background: white;
border-top: 1px solid #e9ecef;
display: flex;
gap: 12px;
align-items: center;
}
.message-input {
flex: 1;
padding: 15px 20px;
border: 2px solid #e9ecef;
border-radius: 25px;
outline: none;
font-size: 14px;
transition: border-color 0.3s ease;
}
.message-input:focus {
border-color: #3498db;
}
.send-button {
padding: 15px 25px;
background: linear-gradient(135deg, #3498db, #2980b9);
color: white;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s ease;
}
.send-button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(52, 152, 219, 0.4);
}
.send-button:disabled {
background: #bdc3c7;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.system-message {
text-align: center;
color: #7f8c8d;
font-style: italic;
margin: 15px 0;
font-size: 13px;
padding: 10px;
background: #f8f9fa;
border-radius: 10px;
}
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
</style>
</head>
<body>
<!-- Authentication Container -->
<div id="authContainer" class="auth-container">
<div class="auth-tabs">
<div class="auth-tab active" onclick="showTab('login')">Login</div>
<div class="auth-tab" onclick="showTab('register')">Register</div>
</div>
<div id="alert" class="alert" style="display: none;"></div>
<!-- Login Form -->
<div id="loginForm" class="auth-form active">
<div class="form-group">
<label for="loginPhone">Phone Number</label>
<input type="text" id="loginPhone" class="form-control" placeholder="+1234567890">
</div>
<div class="form-group">
<label for="loginPassword">Password</label>
<input type="password" id="loginPassword" class="form-control" placeholder="Enter your password">
</div>
<button class="btn" onclick="login()">Login</button>
</div>
<!-- Register Form -->
<div id="registerForm" class="auth-form">
<div class="form-group">
<label for="registerPhone">Phone Number</label>
<input type="text" id="registerPhone" class="form-control" placeholder="+1234567890">
</div>
<div class="form-group">
<label for="registerUsername">Username</label>
<input type="text" id="registerUsername" class="form-control" placeholder="Choose a username">
</div>
<div class="form-group">
<label for="registerPassword">Password</label>
<input type="password" id="registerPassword" class="form-control" placeholder="Choose a password (min 6 characters)">
</div>
<button class="btn" onclick="register()">Register</button>
</div>
</div>
<!-- Chat Container -->
<div class="container" style="display: none;" id="chatContainer">
<div class="users-panel">
<h2>Online Users</h2>
<ul class="user-list" id="userList"></ul>
</div>
<div class="chat-panel">
<div class="chat-header">
<span id="chatHeader">Select a user to start chatting</span>
<button class="logout-btn" onclick="logout()">Logout</button>
</div>
<div class="chat-messages" id="chatMessages"></div>
<div class="chat-input-area">
<input type="text" id="messageInput" class="message-input" placeholder="Type a message..." disabled>
<button onclick="sendMessage()" class="send-button" id="sendButton" disabled>Send</button>
</div>
</div>
</div>
<script>
let ws = null;
let currentUser = '';
let currentToken = '';
let currentRecipient = '';
let userList = [];
let messageHistory = {};
// Check for saved session on page load
window.addEventListener('DOMContentLoaded', function() {
const savedToken = localStorage.getItem('chatToken');
const savedUsername = localStorage.getItem('chatUsername');
if (savedToken && savedUsername) {
validateToken(savedToken, savedUsername);
}
});
function showTab(tabName) {
// Update tabs
document.querySelectorAll('.auth-tab').forEach(tab => {
tab.classList.remove('active');
});
document.querySelectorAll('.auth-form').forEach(form => {
form.classList.remove('active');
});
// Show selected tab
if (tabName === 'login') {
document.querySelector('.auth-tab:nth-child(1)').classList.add('active');
document.getElementById('loginForm').classList.add('active');
} else {
document.querySelector('.auth-tab:nth-child(2)').classList.add('active');
document.getElementById('registerForm').classList.add('active');
}
// Clear alerts
hideAlert();
}
function showAlert(message, type) {
const alert = document.getElementById('alert');
alert.textContent = message;
alert.className = `alert alert-${type}`;
alert.style.display = 'block';
}
function hideAlert() {
document.getElementById('alert').style.display = 'none';
}
async function validateToken(token, username) {
try {
const response = await fetch('/api/validate-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ token }),
});
const data = await response.json();
if (data.success) {
currentUser = data.username;
currentToken = token;
// Save to localStorage
localStorage.setItem('chatToken', token);
localStorage.setItem('chatUsername', data.username);
// Hide auth, show chat
document.getElementById('authContainer').style.display = 'none';
document.getElementById('chatContainer').style.display = 'flex';
document.getElementById('chatHeader').textContent = `Welcome, ${currentUser}`;
// Connect to WebSocket
connectWebSocket();
} else {
// Clear invalid token
localStorage.removeItem('chatToken');
localStorage.removeItem('chatUsername');
showAlert('Session expired. Please login again.', 'error');
}
} catch (error) {
console.error('Token validation failed:', error);
localStorage.removeItem('chatToken');
localStorage.removeItem('chatUsername');
showAlert('Session validation failed. Please login again.', 'error');
}
}
async function register() {
const phone = document.getElementById('registerPhone').value.trim();
const username = document.getElementById('registerUsername').value.trim();
const password = document.getElementById('registerPassword').value;
if (!phone || !username || !password) {
showAlert('Please fill all fields', 'error');
return;
}
if (password.length < 6) {
showAlert('Password must be at least 6 characters', 'error');
return;
}
try {
const response = await fetch('/api/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ phone, username, password }),
});
const data = await response.json();
if (data.success) {
// Save token and username to localStorage
localStorage.setItem('chatToken', data.token);
localStorage.setItem('chatUsername', data.username);
currentUser = data.username;
currentToken = data.token;
// Hide auth, show chat
document.getElementById('authContainer').style.display = 'none';
document.getElementById('chatContainer').style.display = 'flex';
document.getElementById('chatHeader').textContent = `Welcome, ${currentUser}`;
// Connect to WebSocket
connectWebSocket();
showAlert('Registration successful!', 'success');
} else {
showAlert(data.message, 'error');
}
} catch (error) {
showAlert('Registration failed. Please try again.', 'error');
}
}
async function login() {
const phone = document.getElementById('loginPhone').value.trim();
const password = document.getElementById('loginPassword').value;
if (!phone || !password) {
showAlert('Please fill all fields', 'error');
return;
}
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ phone, password }),
});
const data = await response.json();
if (data.success) {
// Save token and username to localStorage
localStorage.setItem('chatToken', data.token);
localStorage.setItem('chatUsername', data.username);
currentUser = data.username;
currentToken = data.token;
// Hide auth, show chat
document.getElementById('authContainer').style.display = 'none';
document.getElementById('chatContainer').style.display = 'flex';
document.getElementById('chatHeader').textContent = `Welcome, ${currentUser}`;
// Connect to WebSocket
connectWebSocket();
} else {
showAlert(data.message, 'error');
}
} catch (error) {
showAlert('Login failed. Please try again.', 'error');
}
}
function logout() {
// Clear localStorage
localStorage.removeItem('chatToken');
localStorage.removeItem('chatUsername');
// Close WebSocket connection
if (ws) {
ws.close();
}
// Reset state
currentUser = '';
currentToken = '';
currentRecipient = '';
userList = [];
messageHistory = {};
// Show auth, hide chat
document.getElementById('authContainer').style.display = 'block';
document.getElementById('chatContainer').style.display = 'none';
// Clear forms
document.getElementById('loginPhone').value = '';
document.getElementById('loginPassword').value = '';
document.getElementById('registerPhone').value = '';
document.getElementById('registerUsername').value = '';
document.getElementById('registerPassword').value = '';
showTab('login');
}
function connectWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws`;
ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log('WebSocket connected');
// Send authentication
ws.send(JSON.stringify({
type: 'auth',
token: currentToken
}));
addSystemMessage('Connected to chat server');
};
ws.onmessage = (event) => {
console.log('Received message:', event.data);
try {
const data = JSON.parse(event.data);
if (data.type === 'user_list') {
updateUserList(data.users);
} else if (data.type === 'error') {
showAlert(data.content, 'error');
// If authentication error, logout
if (data.content.includes('Invalid') || data.content.includes('expired') || data.content.includes('auth')) {
logout();
}
} else if (data.type === 'system') {
addSystemMessage(data.content);
} else {
// Regular chat message
handleChatMessage(data);
}
} catch (e) {
console.error('Error parsing message:', e, 'Raw data:', event.data);
}
};
ws.onclose = () => {
console.log('WebSocket disconnected');
addSystemMessage('Disconnected from server');
document.getElementById('sendButton').disabled = true;
document.getElementById('messageInput').disabled = true;
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
addSystemMessage('Connection error');
};
}
function updateUserList(users) {
userList = users.filter(user => user !== currentUser);
const userListElement = document.getElementById('userList');
userListElement.innerHTML = '';
userList.forEach(user => {
const li = document.createElement('li');
li.className = 'user-item';
li.textContent = user;
li.onclick = () => selectUser(user);
userListElement.appendChild(li);
});
// Auto-select first user if none selected
if (!currentRecipient && userList.length > 0) {
selectUser(userList[0]);
}
}
function selectUser(user) {
currentRecipient = user;
// Update active state in user list
document.querySelectorAll('.user-item').forEach(item => {
item.classList.remove('active');
if (item.textContent === user) {
item.classList.add('active');
}
});
// Update chat header
document.getElementById('chatHeader').textContent = `Chat with ${user}`;
// Enable input
document.getElementById('sendButton').disabled = false;
document.getElementById('messageInput').disabled = false;
document.getElementById('messageInput').focus();
// Display chat history with this user
displayChatHistory(user);
}
function displayChatHistory(user) {
const chatMessages = document.getElementById('chatMessages');
chatMessages.innerHTML = '';
if (messageHistory[user]) {
messageHistory[user].forEach(msg => {
displayMessage(msg, false);
});
}
chatMessages.scrollTop = chatMessages.scrollHeight;
}
function handleChatMessage(msg) {
console.log('Handling chat message:', msg);
const otherUser = msg.from_user === currentUser ? msg.to_user : msg.from_user;
if (!messageHistory[otherUser]) {
messageHistory[otherUser] = [];
}
// Check if message already exists to avoid duplicates
const messageExists = messageHistory[otherUser].some(
existingMsg => existingMsg.id === msg.id
);
if (!messageExists) {
messageHistory[otherUser].push(msg);
}
if (otherUser === currentRecipient) {
displayMessage(msg, true);
}
}
function displayMessage(msg, scrollToBottom) {
const chatMessages = document.getElementById('chatMessages');
const messageDiv = document.createElement('div');
if (msg.message_type === 'system') {
messageDiv.className = 'system-message';
messageDiv.textContent = msg.content;
} else {
const isOutgoing = msg.from_user === currentUser;
messageDiv.className = `message ${isOutgoing ? 'outgoing' : 'incoming'}`;
if (!isOutgoing) {
const senderDiv = document.createElement('div');
senderDiv.className = 'message-sender';
senderDiv.textContent = msg.from_user;
messageDiv.appendChild(senderDiv);
}
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
contentDiv.textContent = msg.content;
messageDiv.appendChild(contentDiv);
const timeDiv = document.createElement('div');
timeDiv.className = 'message-time';
const date = new Date(msg.timestamp);
timeDiv.textContent = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
messageDiv.appendChild(timeDiv);
}
chatMessages.appendChild(messageDiv);
if (scrollToBottom) {
chatMessages.scrollTop = chatMessages.scrollHeight;
}
}
function addSystemMessage(content) {
const chatMessages = document.getElementById('chatMessages');
const systemDiv = document.createElement('div');
systemDiv.className = 'system-message';
systemDiv.textContent = content;
chatMessages.appendChild(systemDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
function sendMessage() {
if (!ws || ws.readyState !== WebSocket.OPEN) {
showAlert('Please connect to the server first', 'error');
return;
}
if (!currentRecipient) {
showAlert('Please select a user to chat with', 'error');
return;
}
const input = document.getElementById('messageInput');
const content = input.value.trim();
if (!content) {
return;
}
const message = {
id: generateId(),
from_user: currentUser, // This will be overwritten by server, but needed for structure
to_user: currentRecipient,
content: content,
timestamp: Date.now(),
message_type: 'text'
};
console.log('Sending message:', message);
ws.send(JSON.stringify(message));
input.value = '';
}
function generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
// Event listeners for Enter key
document.getElementById('messageInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
sendMessage();
}
});
document.getElementById('loginPassword').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
login();
}
});
document.getElementById('registerPassword').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
register();
}
});
// Focus phone input on load
document.getElementById('loginPhone').focus();
</script>
</body>
</html>