Added new login screen and MD

This commit is contained in:
Olai Vike Bøe 2024-06-14 11:35:53 +02:00
parent 918d6a9800
commit b5937c03e6
9 changed files with 697 additions and 327 deletions

98
app.py
View file

@ -1,10 +1,17 @@
from flask import Flask, render_template, request, redirect, url_for, session, jsonify, send_from_directory from flask import Flask, render_template, request, redirect, url_for, session, jsonify, send_from_directory, make_response
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from cryptography.fernet import Fernet, InvalidToken from cryptography.fernet import Fernet, InvalidToken
import os import os
from datetime import datetime from datetime import datetime, timedelta, timezone
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
from flask_socketio import SocketIO, emit, join_room, leave_room from flask_socketio import SocketIO, emit, join_room, leave_room
import logging
import jwt
# Set up logging
logging.basicConfig(filename='app.log', level=logging.DEBUG,
format='%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]', filemode='w')
logger = logging.getLogger(__name__)
app = Flask(__name__) app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(24) app.config['SECRET_KEY'] = os.urandom(24)
@ -28,6 +35,24 @@ if not os.path.exists("secret.key"):
key = load_key() key = load_key()
cipher = Fernet(key) cipher = Fernet(key)
# JWT Utility Functions
def generate_token(user_id):
payload = {
'user_id': user_id,
'exp': datetime.now(timezone.utc) + timedelta(days=7)
}
token = jwt.encode(payload, app.config['SECRET_KEY'], algorithm='HS256')
return token
def decode_token(token):
try:
payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
return payload['user_id']
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
class User(db.Model): class User(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(150), unique=True, nullable=False) username = db.Column(db.String(150), unique=True, nullable=False)
@ -40,7 +65,7 @@ class Message(db.Model):
receiver = db.Column(db.String(150), nullable=False) receiver = db.Column(db.String(150), nullable=False)
content = db.Column(db.Text, nullable=False) content = db.Column(db.Text, nullable=False)
content_type = db.Column(db.String(20), nullable=False, default='text') content_type = db.Column(db.String(20), nullable=False, default='text')
timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) timestamp = db.Column(db.DateTime, nullable=False, default=datetime.now(timezone.utc))
class PendingMessage(db.Model): class PendingMessage(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
@ -48,7 +73,7 @@ class PendingMessage(db.Model):
receiver = db.Column(db.String(150), nullable=False) receiver = db.Column(db.String(150), nullable=False)
content = db.Column(db.Text, nullable=False) content = db.Column(db.Text, nullable=False)
content_type = db.Column(db.String(20), nullable=False, default='text') content_type = db.Column(db.String(20), nullable=False, default='text')
timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) timestamp = db.Column(db.DateTime, nullable=False, default=datetime.now(timezone.utc))
class Friend(db.Model): class Friend(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
@ -67,8 +92,15 @@ class FriendRequest(db.Model):
@app.route('/') @app.route('/')
def index(): def index():
if 'username' in session: token = request.cookies.get('token')
return redirect(url_for('dashboard')) if token:
user_id = decode_token(token)
if user_id:
user = User.query.get(user_id)
if user:
session['username'] = user.username
session['user_id'] = user.id
return redirect(url_for('dashboard'))
return redirect(url_for('login')) return redirect(url_for('login'))
@app.route('/register', methods=['GET', 'POST']) @app.route('/register', methods=['GET', 'POST'])
@ -81,8 +113,10 @@ def register():
try: try:
db.session.add(new_user) db.session.add(new_user)
db.session.commit() db.session.commit()
logger.info(f"New user registered: {username}")
return redirect(url_for('login')) return redirect(url_for('login'))
except Exception as e: except Exception as e:
logger.error(f"Error registering user {username}: {e}")
return 'Username already exists!' return 'Username already exists!'
return render_template('register.html') return render_template('register.html')
@ -95,8 +129,14 @@ def login():
if user and check_password_hash(user.password, password): if user and check_password_hash(user.password, password):
session['username'] = user.username session['username'] = user.username
session['user_id'] = user.id session['user_id'] = user.id
return redirect(url_for('dashboard')) token = generate_token(user.id)
return 'Invalid credentials' response = make_response(jsonify({'status': 'success'}))
expires = datetime.now(timezone.utc) + timedelta(days=7)
response.set_cookie('token', token, httponly=True, expires=expires)
logger.info(f"User logged in: {username}")
return response
logger.warning(f"Invalid login attempt for user: {username}")
return jsonify({'status': 'error'})
return render_template('login.html') return render_template('login.html')
@app.route('/dashboard') @app.route('/dashboard')
@ -125,6 +165,7 @@ def dashboard():
except InvalidToken: except InvalidToken:
decrypted_messages.append((msg.sender, "Invalid encrypted message", msg.timestamp)) decrypted_messages.append((msg.sender, "Invalid encrypted message", msg.timestamp))
logger.info(f"User {session['username']} accessed dashboard")
return render_template('dashboard.html', username=session['username'], user_id=user.id, friends=friend_users, friend_requests=friend_requests, pending_messages=decrypted_pending_messages, messages=decrypted_messages) return render_template('dashboard.html', username=session['username'], user_id=user.id, friends=friend_users, friend_requests=friend_requests, pending_messages=decrypted_pending_messages, messages=decrypted_messages)
return redirect(url_for('login')) return redirect(url_for('login'))
@ -141,8 +182,10 @@ def chat(friend_id):
friend_users = User.query.filter(User.id.in_(friend_ids)).all() friend_users = User.query.filter(User.id.in_(friend_ids)).all()
friend_requests = FriendRequest.query.filter_by(receiver_id=user.id, status='pending').all() friend_requests = FriendRequest.query.filter_by(receiver_id=user.id, status='pending').all()
logger.info(f"User {session['username']} opened chat with {friend_user.username}")
return render_template('chat.html', username=session['username'], friend_id=friend_id, friend_username=friend_user.username, friends=friend_users, friend_requests=friend_requests) return render_template('chat.html', username=session['username'], friend_id=friend_id, friend_username=friend_user.username, friends=friend_users, friend_requests=friend_requests)
else: else:
logger.warning(f"User {session['username']} attempted to access chat with non-friend user_id: {friend_id}")
return redirect(url_for('dashboard')) return redirect(url_for('dashboard'))
return redirect(url_for('login')) return redirect(url_for('login'))
@ -152,24 +195,25 @@ def send_message(receiver):
try: try:
content = request.form.get('content') content = request.form.get('content')
timestamp = request.form.get('timestamp') timestamp = request.form.get('timestamp')
file = request.files.get('image') or request.files.get('video') file = request.files.get('file')
content_type = 'text' content_type = 'text'
if file: if file:
filename = f"{datetime.utcnow().timestamp()}_{file.filename}" filename = f"{datetime.now(timezone.utc).timestamp()}_{file.filename}"
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
content = filename content = filename
content_type = 'file' content_type = 'file'
if content or file: if content:
# Encrypt text content only, do not encrypt file names # Encrypt text content only, do not encrypt file names
encrypted_content = cipher.encrypt(content.encode()).decode() if content_type == 'text' else content encrypted_content = cipher.encrypt(content.encode()).decode() if content_type == 'text' else content
timestamp_dt = datetime.strptime(timestamp, '%Y-%m-%d %H:%M:%S') timestamp_dt = datetime.strptime(timestamp, '%Y-%m-%d %H:%M:%S').replace(tzinfo=timezone.utc)
# Check if they are friends # Check if they are friends
user = User.query.filter_by(username=session['username']).first() user = User.query.filter_by(username=session['username']).first()
receiver_user = User.query.filter_by(id=receiver).first() receiver_user = User.query.filter_by(id=receiver).first()
if not receiver_user: if not receiver_user:
logger.error(f"Message send failed: User not found {receiver}")
return jsonify({'error': 'User not found'}), 404 return jsonify({'error': 'User not found'}), 404
friend = Friend.query.filter_by(user_id=user.id, friend_id=receiver_user.id).first() friend = Friend.query.filter_by(user_id=user.id, friend_id=receiver_user.id).first()
@ -182,16 +226,21 @@ def send_message(receiver):
'sender': session['username'], 'sender': session['username'],
'content': decrypted_content, 'content': decrypted_content,
'content_type': content_type, 'content_type': content_type,
'timestamp': timestamp 'timestamp': timestamp,
'id': new_message.id
}, room=receiver_user.username) }, room=receiver_user.username)
logger.info(f"Message sent from {session['username']} to {receiver_user.username}")
return jsonify({'status': 'Message sent', 'message_id': new_message.id}), 200
else: else:
pending_message = PendingMessage(sender=session['username'], receiver=receiver_user.username, content=encrypted_content, content_type=content_type, timestamp=timestamp_dt) pending_message = PendingMessage(sender=session['username'], receiver=receiver_user.username, content=encrypted_content, content_type=content_type, timestamp=timestamp_dt)
db.session.add(pending_message) db.session.add(pending_message)
db.session.commit() db.session.commit()
logger.info(f"Pending message from {session['username']} to {receiver_user.username}")
return jsonify({'status': 'Message sent'}), 200 return jsonify({'status': 'Pending message sent', 'message_id': pending_message.id}), 200
return jsonify({'error': 'No content or file provided'}), 400 return jsonify({'error': 'No content or file provided'}), 400
except Exception as e: except Exception as e:
logger.error(f"Error sending message from {session['username']} to {receiver}: {e}")
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
return jsonify({'error': 'Unauthorized'}), 401 return jsonify({'error': 'Unauthorized'}), 401
@ -212,7 +261,9 @@ def get_messages(friend_id):
{'sender': msg.sender, 'content': cipher.decrypt(msg.content.encode()).decode() if msg.content_type == 'text' else msg.content, 'content_type': msg.content_type, 'timestamp': msg.timestamp.strftime("%Y-%m-%d %H:%M:%S")} {'sender': msg.sender, 'content': cipher.decrypt(msg.content.encode()).decode() if msg.content_type == 'text' else msg.content, 'content_type': msg.content_type, 'timestamp': msg.timestamp.strftime("%Y-%m-%d %H:%M:%S")}
for msg in messages for msg in messages
] ]
logger.info(f"Messages retrieved for chat between {current_user.username} and {friend_user.username}")
return jsonify({'messages': decrypted_messages}) return jsonify({'messages': decrypted_messages})
logger.warning(f"Unauthorized message retrieval attempt by user {session.get('username', 'unknown')}")
return jsonify({'error': 'Unauthorized'}), 401 return jsonify({'error': 'Unauthorized'}), 401
@app.route('/cdn/<filename>') @app.route('/cdn/<filename>')
@ -233,7 +284,9 @@ def add_friend():
'sender': session['username'], 'sender': session['username'],
'receiver': friend_username 'receiver': friend_username
}, room=friend_username) }, room=friend_username)
logger.info(f"Friend request sent from {session['username']} to {friend_username}")
return redirect(url_for('dashboard')) return redirect(url_for('dashboard'))
logger.warning(f"Friend request failed from {session['username']} to {friend_username}: Friend not found or cannot add yourself as a friend")
return 'Friend not found or cannot add yourself as a friend' return 'Friend not found or cannot add yourself as a friend'
return redirect(url_for('login')) return redirect(url_for('login'))
@ -266,8 +319,10 @@ def accept_friend(request_id):
'sender': friend_request.sender.username, 'sender': friend_request.sender.username,
'receiver': friend_request.receiver.username 'receiver': friend_request.receiver.username
}, room=friend_request.sender.username) }, room=friend_request.sender.username)
logger.info(f"Friend request accepted by {session['username']} from {friend_request.sender.username}")
return redirect(url_for('dashboard')) return redirect(url_for('dashboard'))
logger.warning(f"Friend request accept failed: Friend request not found or unauthorized access by {session['username']}")
return 'Friend request not found' return 'Friend request not found'
return redirect(url_for('login')) return redirect(url_for('login'))
@ -283,8 +338,10 @@ def reject_friend(request_id):
'sender': friend_request.sender.username, 'sender': friend_request.sender.username,
'receiver': friend_request.receiver.username 'receiver': friend_request.receiver.username
}, room=friend_request.sender.username) }, room=friend_request.sender.username)
logger.info(f"Friend request rejected by {session['username']} from {friend_request.sender.username}")
return redirect(url_for('dashboard')) return redirect(url_for('dashboard'))
logger.warning(f"Friend request reject failed: Friend request not found or unauthorized access by {session['username']}")
return 'Friend request not found' return 'Friend request not found'
return redirect(url_for('login')) return redirect(url_for('login'))
@ -304,8 +361,10 @@ def remove_friend(friend_id):
'sender': session['username'], 'sender': session['username'],
'receiver': friend.friend.username 'receiver': friend.friend.username
}, room=friend.friend.username) }, room=friend.friend.username)
logger.info(f"Friend {friend.friend.username} removed by {session['username']}")
return redirect(url_for('dashboard')) return redirect(url_for('dashboard'))
logger.warning(f"Friend removal failed: Friend not found or unauthorized access by {session['username']}")
return 'Friend not found' return 'Friend not found'
return redirect(url_for('login')) return redirect(url_for('login'))
@ -313,7 +372,10 @@ def remove_friend(friend_id):
def logout(): def logout():
session.pop('username', None) session.pop('username', None)
session.pop('user_id', None) session.pop('user_id', None)
return redirect(url_for('login')) response = make_response(redirect(url_for('login')))
response.set_cookie('token', '', expires=0)
logger.info(f"User logged out")
return response
@socketio.on('connect') @socketio.on('connect')
def handle_connect(): def handle_connect():
@ -323,6 +385,7 @@ def handle_connect():
user.online = True user.online = True
db.session.commit() db.session.commit()
emit('user_online', {'username': user.username}, broadcast=True) emit('user_online', {'username': user.username}, broadcast=True)
logger.info(f"User {session['username']} connected")
@socketio.on('disconnect') @socketio.on('disconnect')
def handle_disconnect(): def handle_disconnect():
@ -332,18 +395,21 @@ def handle_disconnect():
user.online = False user.online = False
db.session.commit() db.session.commit()
emit('user_offline', {'username': user.username}, broadcast=True) emit('user_offline', {'username': user.username}, broadcast=True)
logger.info(f"User {session['username']} disconnected")
@socketio.on('join') @socketio.on('join')
def handle_join(data): def handle_join(data):
username = data['username'] username = data['username']
join_room(username) join_room(username)
logger.info(f"User {username} joined room {username}")
@socketio.on('leave') @socketio.on('leave')
def handle_leave(data): def handle_leave(data):
username = data['username'] username = data['username']
leave_room(username) leave_room(username)
logger.info(f"User {username} left room {username}")
if __name__ == '__main__': if __name__ == '__main__':
with app.app_context(): with app.app_context():
db.create_all() db.create_all()
socketio.run(app, host='0.0.0.0', port=8086, debug=True) socketio.run(app, debug=True) # host='0.0.0.0', port=8086,

View file

@ -5,3 +5,4 @@ cryptography==42.0.8
Werkzeug==3.0.3 Werkzeug==3.0.3
python-socketio==5.11.2 python-socketio==5.11.2
python-engineio==4.9.1 python-engineio==4.9.1
PyJWT==2.8.0

332
static/js/chat.js Normal file
View file

@ -0,0 +1,332 @@
document.addEventListener('DOMContentLoaded', (event) => {
const socket = io();
const md = window.markdownit();
socket.emit('join', { username: username });
document.title = `Chatting with ${friendUsername}`;
const messagesList = document.getElementById('messages');
const imageOverlay = document.getElementById('image-overlay');
const overlayImage = document.getElementById('overlay-image');
const contextMenu = document.getElementById('context-menu');
const toast = document.getElementById('toast');
const toastMessage = document.getElementById('toast-message');
let currentMessageId = null;
let currentMessageText = null;
function showToast(message) {
toastMessage.textContent = message;
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
function scrollToBottom() {
messagesList.scrollTop = messagesList.scrollHeight;
}
function shouldShowUsername(previousTimestamp, currentTimestamp) {
const tenMinutes = 10 * 60 * 1000;
return (currentTimestamp - previousTimestamp) > tenMinutes;
}
let previousSender = null;
let previousTimestamp = 0;
socket.on('new_message', function(data) {
const currentTimestamp = new Date(data.timestamp).getTime();
const showUsername = previousSender !== data.sender || shouldShowUsername(previousTimestamp, currentTimestamp);
previousSender = data.sender;
previousTimestamp = currentTimestamp;
const newMessage = document.createElement('div');
newMessage.classList.add('message');
newMessage.dataset.messageId = data.id; // Ensure this is correct
newMessage.dataset.messageText = data.content;
if (showUsername) {
const usernameElement = document.createElement('div');
usernameElement.classList.add('message-sender');
usernameElement.innerHTML = `<strong>${data.sender}</strong>:`;
messagesList.appendChild(usernameElement);
}
if (data.content_type === 'text') {
newMessage.innerHTML = `${md.render(data.content)}<div class="timestamp">${data.timestamp}</div>`;
} else if (data.content_type === 'file') {
const isImage = /\.(jpg|jpeg|png|gif|bmp)$/i.test(data.content);
const isVideo = /\.(mp4|webm|ogg)$/i.test(data.content);
if (isImage) {
newMessage.innerHTML = `<img src="/cdn/${data.content}" alt="Image" class="enhanceable-image" style="max-width: 200px; max-height: 200px;" /><div class="timestamp">${data.timestamp}</div>`;
} else if (isVideo) {
newMessage.innerHTML = `<video controls style="max-width: 200px; max-height: 200px;"><source src="/cdn/${data.content}" type="video/mp4"></video><div class="timestamp">${data.timestamp}</div>`;
} else {
newMessage.innerHTML = `<a href="/cdn/${data.content}" target="_blank">Download File</a><div class="timestamp">${data.timestamp}</div>`;
}
}
messagesList.appendChild(newMessage);
scrollToBottom();
});
if (friendId) {
fetch(`/get_messages/${friendId}`)
.then(response => response.json())
.then(data => {
messagesList.innerHTML = '';
document.getElementById('chat-with').textContent = `Chatting with ${friendUsername}`;
document.getElementById('send-message-form').dataset.receiver = friendId;
data.messages.forEach((msg, index, messages) => {
const currentTimestamp = new Date(msg.timestamp).getTime();
const previousMessage = messages[index - 1];
const previousTimestamp = previousMessage ? new Date(previousMessage.timestamp).getTime() : 0;
const showUsername = index === 0 || msg.sender !== previousMessage.sender || shouldShowUsername(previousTimestamp, currentTimestamp);
if (showUsername) {
const usernameElement = document.createElement('div');
usernameElement.classList.add('message-sender');
usernameElement.innerHTML = `<strong>${msg.sender}</strong>:`;
messagesList.appendChild(usernameElement);
}
const messageElement = document.createElement('div');
messageElement.classList.add('message');
messageElement.dataset.messageId = msg.id;
messageElement.dataset.messageText = msg.content;
if (msg.content_type === 'text') {
messageElement.innerHTML = `${md.render(msg.content)}<div class="timestamp">${msg.timestamp}</div>`;
} else if (msg.content_type === 'file') {
const isImage = /\.(jpg|jpeg|png|gif|bmp)$/i.test(msg.content);
const isVideo = /\.(mp4|webm|ogg)$/i.test(msg.content);
if (isImage) {
messageElement.innerHTML = `<img src="/cdn/${msg.content}" alt="Image" class="enhanceable-image" style="max-width: 200px; max-height: 200px;" /><div class="timestamp">${msg.timestamp}</div>`;
} else if (isVideo) {
messageElement.innerHTML = `<video controls style="max-width: 200px; max-height: 200px;"><source src="/cdn/${msg.content}" type="video/mp4"></video><div class="timestamp">${msg.timestamp}</div>`;
} else {
messageElement.innerHTML = `<a href="/cdn/${msg.content}" target="_blank">Download File</a><div class="timestamp">${msg.timestamp}</div>`;
}
}
messagesList.appendChild(messageElement);
});
scrollToBottom();
})
.catch(error => console.error('Error fetching messages:', error));
}
const sendMessageForm = document.querySelector('.send-message-form');
if (sendMessageForm) {
sendMessageForm.addEventListener('submit', function(event) {
event.preventDefault();
const receiver = this.dataset.receiver;
const contentInput = this.querySelector('input[name="content"]');
const fileInput = this.querySelector('input[name="file"]');
const content = contentInput.value;
const timestamp = new Date().toISOString().slice(0, 19).replace('T', ' ');
const imagePreview = document.querySelector('.file-preview-image');
const videoPreview = document.querySelector('.file-preview-video');
const filenamePreview = document.querySelector('.file-preview-filename');
if (content || fileInput.files.length > 0) {
// Append the message locally
const newMessage = document.createElement('div');
newMessage.classList.add('message');
newMessage.dataset.messageId = 'temp-id'; // Temporary ID for the new message
const currentTimestamp = new Date(timestamp).getTime();
const showUsername = previousSender !== username || shouldShowUsername(previousTimestamp, currentTimestamp);
previousSender = username;
previousTimestamp = currentTimestamp;
if (showUsername) {
const usernameElement = document.createElement('div');
usernameElement.classList.add('message-sender');
usernameElement.innerHTML = `<strong>${username}</strong>:`;
messagesList.appendChild(usernameElement);
}
if (content) {
newMessage.dataset.messageText = content;
newMessage.innerHTML = `${md.render(content)}<div class="timestamp">${timestamp}</div>`;
}
if (fileInput.files.length > 0) {
const file = fileInput.files[0];
const isImage = /\.(jpg|jpeg|png|gif|bmp)$/i.test(file.name);
const isVideo = /\.(mp4|webm|ogg)$/i.test(file.name);
if (isImage) {
newMessage.innerHTML = `<img src="${imagePreview.src}" alt="Image" class="enhanceable-image" style="max-width: 200px; max-height: 200px;" /><div class="timestamp">${timestamp}</div>`;
} else if (isVideo) {
newMessage.innerHTML = `<video controls style="max-width: 200px; max-height: 200px;"><source src="${videoPreview.src}" type="video/mp4"></video><div class="timestamp">${timestamp}</div>`;
} else {
newMessage.innerHTML = `<a href="${URL.createObjectURL(file)}" target="_blank">Download File</a><div class="timestamp">${timestamp}</div>`;
}
}
messagesList.appendChild(newMessage);
scrollToBottom();
const formData = new FormData();
formData.append('content', content);
formData.append('timestamp', timestamp);
if (fileInput.files[0]) {
formData.append('file', fileInput.files[0]);
}
fetch(`/send_message/${receiver}`, {
method: 'POST',
body: formData
}).then(response => response.json())
.then(data => {
if (data.status === 'Message sent') {
contentInput.value = '';
fileInput.value = '';
document.querySelector('.file-preview').style.display = 'none';
document.querySelector('.file-preview img').src = '';
document.querySelector('.file-preview video').style.display = 'none';
document.querySelector('.file-preview video').src = '';
document.querySelector('.file-preview .file-preview-filename').style.display = 'none';
document.querySelector('.file-preview .file-preview-filename').textContent = '';
// Update the message ID with the real one from the server
newMessage.dataset.messageId = data.message_id;
} else {
showToast('Message sending failed');
}
}).catch(error => {
showToast('Message sending failed');
console.error('Error sending message:', error);
});
}
});
document.querySelector('input[name="file"]').addEventListener('change', function(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
const imagePreview = document.querySelector('.file-preview-image');
const videoPreview = document.querySelector('.file-preview-video');
const filenamePreview = document.querySelector('.file-preview-filename');
reader.onload = function(e) {
document.querySelector('.file-preview').style.display = 'block';
if (file.type.startsWith('image/')) {
imagePreview.style.display = 'block';
videoPreview.style.display = 'none';
filenamePreview.style.display = 'none';
imagePreview.src = e.target.result;
} else if (file.type.startsWith('video/')) {
videoPreview.style.display = 'block';
imagePreview.style.display = 'none';
filenamePreview.style.display = 'none';
videoPreview.querySelector('source').src = e.target.result;
videoPreview.load();
} else {
filenamePreview.style.display = 'block';
imagePreview.style.display = 'none';
videoPreview.style.display = 'none';
filenamePreview.textContent = file.name;
}
};
reader.readAsDataURL(file);
}
});
document.querySelector('.remove-file').addEventListener('click', function() {
const fileInput = document.querySelector('input[name="file"]');
fileInput.value = '';
const filePreview = document.querySelector('.file-preview');
filePreview.style.display = 'none';
filePreview.querySelector('img').style.display = 'none';
filePreview.querySelector('img').src = '';
filePreview.querySelector('video').style.display = 'none';
filePreview.querySelector('video').src = '';
filePreview.querySelector('.file-preview-filename').style.display = 'none';
filePreview.querySelector('.file-preview-filename').textContent = '';
});
}
function checkFriendRequests() {
const friendRequestsList = document.getElementById('friend-requests');
const friendRequestsSection = document.getElementById('friend-requests-section');
if (friendRequestsList && friendRequestsSection) {
if (friendRequestsList.children.length === 0) {
friendRequestsSection.style.display = 'none';
} else {
friendRequestsSection.style.display = 'block';
}
}
}
checkFriendRequests();
// Image enhancer functionality
document.addEventListener('click', function(event) {
if (event.target.classList.contains('enhanceable-image')) {
overlayImage.src = event.target.src;
imageOverlay.style.display = 'flex';
}
});
imageOverlay.addEventListener('click', function() {
imageOverlay.style.display = 'none';
});
// Custom Context Menu Functionality
document.addEventListener('contextmenu', function(event) {
const messageElement = event.target.closest('.message');
if (messageElement) {
event.preventDefault();
currentMessageId = messageElement.dataset.messageId;
currentMessageText = messageElement.dataset.messageText;
contextMenu.style.top = `${event.clientY}px`;
contextMenu.style.left = `${event.clientX}px`;
contextMenu.style.display = 'block';
} else {
contextMenu.style.display = 'none';
}
});
document.getElementById('copy-text').addEventListener('click', function() {
if (currentMessageText) {
navigator.clipboard.writeText(currentMessageText).then(() => {
showToast('Text copied to clipboard');
});
}
contextMenu.style.display = 'none';
});
document.getElementById('copy-id').addEventListener('click', function() {
if (currentMessageId) {
navigator.clipboard.writeText(currentMessageId).then(() => {
showToast('Message ID copied to clipboard');
});
}
contextMenu.style.display = 'none';
});
contextMenu.addEventListener('contextmenu', function(event) {
event.preventDefault();
});
document.addEventListener('click', function() {
contextMenu.style.display = 'none';
});
// Handle user online/offline status
socket.on('user_online', function(data) {
const statusIndicator = document.getElementById(`status-${data.username}`);
if (statusIndicator) {
statusIndicator.classList.remove('offline');
statusIndicator.classList.add('online');
}
});
socket.on('user_offline', function(data) {
const statusIndicator = document.getElementById(`status-${data.username}`);
if (statusIndicator) {
statusIndicator.classList.remove('online');
statusIndicator.classList.add('offline');
}
});
});

85
static/js/dashboard.js Normal file
View file

@ -0,0 +1,85 @@
document.addEventListener('DOMContentLoaded', (event) => {
const socket = io();
socket.on('user_online', function(data) {
const username = data.username;
const friendElement = document.querySelector(`[data-username="${username}"]`);
if (friendElement) {
friendElement.querySelector('.status-indicator').classList.remove('offline');
friendElement.querySelector('.status-indicator').classList.add('online');
}
});
socket.on('user_offline', function(data) {
const username = data.username;
const friendElement = document.querySelector(`[data-username="${username}"]`);
if (friendElement) {
friendElement.querySelector('.status-indicator').classList.remove('online');
friendElement.querySelector('.status-indicator').classList.add('offline');
}
});
function checkFriendRequests() {
const friendRequestsList = document.getElementById('friend-requests');
const friendRequestsSection = document.getElementById('friend-requests-section');
if (friendRequestsList && friendRequestsSection) {
if (friendRequestsList.children.length === 0) {
friendRequestsSection.style.display = 'none';
} else {
friendRequestsSection.style.display = 'block';
}
}
}
checkFriendRequests();
const acceptButtons = document.querySelectorAll('.accept-friend-request');
const rejectButtons = document.querySelectorAll('.reject-friend-request');
acceptButtons.forEach(button => {
button.addEventListener('click', function() {
const requestId = this.dataset.requestId;
fetch(`/accept_friend/${requestId}`, { method: 'POST' })
.then(response => response.json())
.then(data => {
if (data.status === 'Friend request accepted') {
this.closest('.friend-request').remove();
checkFriendRequests();
}
});
});
});
rejectButtons.forEach(button => {
button.addEventListener('click', function() {
const requestId = this.dataset.requestId;
fetch(`/reject_friend/${requestId}`, { method: 'POST' })
.then(response => response.json())
.then(data => {
if (data.status === 'Friend request rejected') {
this.closest('.friend-request').remove();
checkFriendRequests();
}
});
});
});
const addFriendForm = document.getElementById('add-friend-form');
if (addFriendForm) {
addFriendForm.addEventListener('submit', function(event) {
event.preventDefault();
const formData = new FormData(addFriendForm);
fetch('/add_friend', {
method: 'POST',
body: formData
}).then(response => response.json())
.then(data => {
if (data.status === 'Friend request sent') {
alert('Friend request sent');
} else {
alert('Failed to send friend request');
}
});
});
}
});

View file

@ -94,10 +94,16 @@ body {
.send-message-form { .send-message-form {
display: flex; display: flex;
align-items: center; flex-direction: column;
} }
.send-message-form input[type="text"] { .message-input-container {
display: flex;
align-items: center;
margin-top: 10px;
}
.message-input-container input[type="text"] {
flex: 1; flex: 1;
padding: 10px; padding: 10px;
margin-right: 10px; margin-right: 10px;
@ -105,7 +111,7 @@ body {
border-radius: 5px; border-radius: 5px;
} }
.send-message-form button { .message-input-container button {
padding: 10px; padding: 10px;
border: none; border: none;
background-color: #7289da; background-color: #7289da;
@ -114,18 +120,27 @@ body {
cursor: pointer; cursor: pointer;
} }
.image-preview { .add-file-btn {
margin-right: 10px;
}
.file-preview {
position: relative; position: relative;
display: inline-block; display: inline-block;
margin-bottom: 10px;
max-width: 100%;
} }
.image-preview img { .file-preview img,
max-width: 100px; .file-preview video,
.file-preview .file-preview-filename {
max-width: 100%;
max-height: 100px; max-height: 100px;
display: block; display: block;
margin-bottom: 5px;
} }
.image-preview .remove-image { .file-preview .remove-file {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
@ -158,17 +173,100 @@ body {
cursor: pointer; cursor: pointer;
} }
.online{ .sidebar-tabs {
width: 100%;
height: 34px;
display: flex;
padding-left: 10px;
align-items: center;
border-radius: 5px;
}
.sidebar-selected {
background: white;
}
.sidebar-tabs-content {
font-size: 1.5rem;
}
.sidebar-tabs-content > a {
color: black;
}
/* Toast */
.toast {
display: none;
position: fixed;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
background-color: #333;
color: #fff;
padding: 10px;
border-radius: 5px;
z-index: 1000;
}
.toast.show {
display: block;
animation: fadeInOut 3s;
}
@keyframes fadeInOut {
0% { opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { opacity: 0; }
}
/* Right click menu */
.custom-context-menu {
display: none;
position: absolute;
z-index: 1000;
width: 150px;
background-color: #fff;
border: 1px solid #ccc;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
}
.custom-context-menu ul {
list-style-type: none;
margin: 0;
padding: 0;
}
.custom-context-menu ul li {
padding: 10px;
cursor: pointer;
}
.custom-context-menu ul li:hover {
background-color: #eee;
}
/* Status indicator */
/* li {
list-style-type: none;
} */
ul#friends-list {
padding: 0;
}
.status-indicator {
margin-right: 10px; margin-right: 10px;
display: inline-block;
height: 10px; height: 10px;
width: 10px; width: 10px;
background-color: #23a55a;
border-radius: 50%; border-radius: 50%;
} }
.offline{
margin-right: 10px; .online {
height: 10px; background-color: #23a55a;
width: 10px; }
.offline {
background-color: #80848e; background-color: #80848e;
border-radius: 50%; }
}

View file

@ -4,205 +4,20 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Chat</title> <title>Chat</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<link rel="stylesheet" href="https://vjs.zencdn.net/7.11.4/video-js.css">
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
<script src="https://cdn.socket.io/4.0.1/socket.io.min.js"></script> <script src="https://cdn.socket.io/4.0.1/socket.io.min.js"></script>
<script type="text/javascript"> <script src="https://cdn.jsdelivr.net/npm/markdown-it/dist/markdown-it.min.js"></script>
document.addEventListener('DOMContentLoaded', (event) => { <script src="https://vjs.zencdn.net/7.11.4/video.min.js"></script>
const socket = io(); <script>
socket.emit('join', { username: "{{ username }}" }); var friendId = "{{ friend_id }}";
var friendUsername = "{{ friend_username }}";
const messagesList = document.getElementById('messages'); var username = "{{ username }}";
const imageOverlay = document.getElementById('image-overlay');
const overlayImage = document.getElementById('overlay-image');
function scrollToBottom() {
messagesList.scrollTop = messagesList.scrollHeight;
}
socket.on('new_message', function(data) {
const newMessage = document.createElement('div');
newMessage.classList.add('message');
if (data.content_type === 'text') {
newMessage.innerHTML = `<strong>${data.sender}</strong>: ${data.content}<div class="timestamp">${data.timestamp}</div>`;
} else if (data.content_type === 'file') {
const isImage = /\.(jpg|jpeg|png|gif|bmp)$/i.test(data.content);
const isVideo = /\.(mp4|webm|ogg)$/i.test(data.content);
if (isImage) {
newMessage.innerHTML = `<strong>${data.sender}</strong>: <img src="/cdn/${data.content}" alt="Image" class="enhanceable-image" style="max-width: 200px; max-height: 200px;" /><div class="timestamp">${data.timestamp}</div>`;
} else if (isVideo) {
newMessage.innerHTML = `<strong>${data.sender}</strong>: <video controls style="max-width: 200px; max-height: 200px;"><source src="/cdn/${data.content}" type="video/mp4"></video><div class="timestamp">${data.timestamp}</div>`;
} else {
newMessage.innerHTML = `<strong>${data.sender}</strong>: <a href="/cdn/${data.content}" target="_blank">Download File</a><div class="timestamp">${data.timestamp}</div>`;
}
}
messagesList.appendChild(newMessage);
scrollToBottom();
});
const friendId = "{{ friend_id }}"; // Ensure this is treated as a string
if (friendId) {
fetch(`/get_messages/${friendId}`)
.then(response => response.json())
.then(data => {
messagesList.innerHTML = '';
document.getElementById('chat-with').textContent = `Chatting with {{ friend_username }}`;
document.getElementById('send-message-form').dataset.receiver = friendId;
data.messages.forEach(msg => {
const messageElement = document.createElement('div');
messageElement.classList.add('message');
if (msg.content_type === 'text') {
messageElement.innerHTML = `<strong>${msg.sender}</strong>: ${msg.content}<div class="timestamp">${msg.timestamp}</div>`;
} else if (msg.content_type === 'file') {
const isImage = /\.(jpg|jpeg|png|gif|bmp)$/i.test(msg.content);
const isVideo = /\.(mp4|webm|ogg)$/i.test(msg.content);
if (isImage) {
messageElement.innerHTML = `<strong>${msg.sender}</strong>: <img src="/cdn/${msg.content}" alt="Image" class="enhanceable-image" style="max-width: 200px; max-height: 200px;" /><div class="timestamp">${msg.timestamp}</div>`;
} else if (isVideo) {
messageElement.innerHTML = `<strong>${msg.sender}</strong>: <video controls style="max-width: 200px; max-height: 200px;"><source src="/cdn/${msg.content}" type="video/mp4"></video><div class="timestamp">${msg.timestamp}</div>`;
} else {
messageElement.innerHTML = `<strong>${msg.sender}</strong>: <a href="/cdn/${msg.content}" target="_blank">Download File</a><div class="timestamp">${msg.timestamp}</div>`;
}
}
messagesList.appendChild(messageElement);
});
scrollToBottom();
})
.catch(error => console.error('Error fetching messages:', error));
}
const sendMessageForm = document.querySelector('.send-message-form');
if (sendMessageForm) {
sendMessageForm.addEventListener('submit', function(event) {
event.preventDefault();
const receiver = this.dataset.receiver;
const contentInput = this.querySelector('input[name="content"]');
const fileInput = this.querySelector('input[name="image"]') || this.querySelector('input[name="video"]');
const content = contentInput.value;
const timestamp = new Date().toISOString().slice(0, 19).replace('T', ' ');
const imagePreview = document.querySelector('.image-preview img');
const videoPreview = document.querySelector('.video-preview video');
if (content || fileInput.files.length > 0) {
// Append the message locally
const newMessage = document.createElement('div');
newMessage.classList.add('message');
if (content) {
newMessage.innerHTML = `<strong>{{ username }}</strong>: ${content}<div class="timestamp">${timestamp}</div>`;
}
if (fileInput.files.length > 0) {
const isImage = /\.(jpg|jpeg|png|gif|bmp)$/i.test(fileInput.files[0].name);
const isVideo = /\.(mp4|webm|ogg)$/i.test(fileInput.files[0].name);
if (isImage) {
newMessage.innerHTML = `<strong>{{ username }}</strong>: <img src="${imagePreview.src}" alt="Image" class="enhanceable-image" style="max-width: 200px; max-height: 200px;" /><div class="timestamp">${timestamp}</div>`;
} else if (isVideo) {
newMessage.innerHTML = `<strong>{{ username }}</strong>: <video controls style="max-width: 200px; max-height: 200px;"><source src="${videoPreview.src}" type="video/mp4"></video><div class="timestamp">${timestamp}</div>`;
} else {
newMessage.innerHTML = `<strong>{{ username }}</strong>: <a href="${fileInput.src}" target="_blank">Download File</a><div class="timestamp">${timestamp}</div>`;
}
}
messagesList.appendChild(newMessage);
scrollToBottom();
const formData = new FormData();
formData.append('content', content);
formData.append('timestamp', timestamp);
if (fileInput.files[0]) {
formData.append('image', fileInput.files[0]);
}
fetch(`/send_message/${receiver}`, {
method: 'POST',
body: formData
}).then(response => response.json())
.then(data => {
if (data.status === 'Message sent') {
contentInput.value = '';
fileInput.value = '';
document.querySelector('.image-preview').style.display = 'none';
document.querySelector('.image-preview img').src = '';
document.querySelector('.video-preview').style.display = 'none';
document.querySelector('.video-preview video').src = '';
} else {
alert('Message sending failed');
}
});
}
});
document.querySelector('input[name="image"]').addEventListener('change', function(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
const imagePreview = document.querySelector('.image-preview');
imagePreview.style.display = 'block';
imagePreview.querySelector('img').src = e.target.result;
};
reader.readAsDataURL(file);
}
});
document.querySelector('input[name="video"]').addEventListener('change', function(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
const videoPreview = document.querySelector('.video-preview');
videoPreview.style.display = 'block';
videoPreview.querySelector('video').src = e.target.result;
};
reader.readAsDataURL(file);
}
});
document.querySelector('.remove-image').addEventListener('click', function() {
const fileInput = document.querySelector('input[name="image"]');
fileInput.value = '';
const imagePreview = document.querySelector('.image-preview');
imagePreview.style.display = 'none';
imagePreview.querySelector('img').src = '';
});
document.querySelector('.remove-video').addEventListener('click', function() {
const fileInput = document.querySelector('input[name="video"]');
fileInput.value = '';
const videoPreview = document.querySelector('.video-preview');
videoPreview.style.display = 'none';
videoPreview.querySelector('video').src = '';
});
}
function checkFriendRequests() {
const friendRequestsList = document.getElementById('friend-requests');
const friendRequestsSection = document.getElementById('friend-requests-section');
if (friendRequestsList && friendRequestsSection) {
if (friendRequestsList.children.length === 0) {
friendRequestsSection.style.display = 'none';
} else {
friendRequestsSection.style.display = 'block';
}
}
}
checkFriendRequests();
// Image enhancer functionality
document.addEventListener('click', function(event) {
if (event.target.classList.contains('enhanceable-image')) {
overlayImage.src = event.target.src;
imageOverlay.style.display = 'flex';
}
});
imageOverlay.addEventListener('click', function() {
imageOverlay.style.display = 'none';
});
});
</script> </script>
<script src="{{ url_for('static', filename='js/chat.js') }}"></script>
</head> </head>
<body> <body>
{% include 'sidebar.html' %} {% include 'sidebar.html' %}
<div class="chat-container"> <div class="chat-container">
<div class="chat-header"> <div class="chat-header">
<h3 id="chat-with">Chatting with {{ friend_username }}</h3> <h3 id="chat-with">Chatting with {{ friend_username }}</h3>
@ -211,27 +26,38 @@
<!-- Messages will be displayed here --> <!-- Messages will be displayed here -->
</div> </div>
<form class="send-message-form" id="send-message-form" data-receiver="{{ friend_id }}" method="POST"> <form class="send-message-form" id="send-message-form" data-receiver="{{ friend_id }}" method="POST">
<input type="text" name="content" placeholder="Type a message"> <div class="file-preview" style="display: none;">
<input type="file" name="image" style="display: none;"> <img src="" alt="Image Preview" class="file-preview-image" style="display: none;">
<input type="file" name="video" style="display: none;"> <video controls class="file-preview-video video-js vjs-default-skin" style="display: none;">
<button type="button" onclick="document.querySelector('input[name=\'image\']').click()">+</button>
<button type="button" onclick="document.querySelector('input[name=\'video\']').click()">+</button>
<div class="image-preview" style="display: none;">
<img src="" alt="Image Preview">
<button type="button" class="remove-image">X</button>
</div>
<div class="video-preview" style="display: none;">
<video controls>
<source src="" type="video/mp4"> <source src="" type="video/mp4">
</video> </video>
<button type="button" class="remove-video">X</button> <div class="file-preview-filename" style="display: none;"></div>
<button type="button" class="remove-file">X</button>
</div>
<div class="message-input-container">
<input type="text" name="content" placeholder="Type a message">
<input type="file" name="file" style="display: none;" accept="image/*,video/*,application/pdf,application/msword">
<button type="button" class="add-file-btn" onclick="document.querySelector('input[name=\'file\']').click()">+</button>
<button type="submit" class="send-btn">Send</button>
</div> </div>
<button type="submit">Send</button>
</form> </form>
<div id="image-overlay" class="image-overlay"> <div id="image-overlay" class="image-overlay">
<img id="overlay-image" src="" alt="Enhanced Image"> <img id="overlay-image" src="" alt="Enhanced Image">
</div> </div>
<!-- Custom Context Menu -->
<div id="context-menu" class="custom-context-menu">
<ul>
<li id="copy-text">Copy Text</li>
<li id="copy-id">Copy Message ID</li>
</ul>
</div>
<!-- Toast Notification -->
<div id="toast" class="toast">
<div id="toast-message"></div>
</div>
</div> </div>
</body> </body>
</html> </html>

View file

@ -5,89 +5,12 @@
<title>Dashboard</title> <title>Dashboard</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
<script> <script src="https://cdn.socket.io/4.0.1/socket.io.min.js"></script>
document.addEventListener('DOMContentLoaded', (event) => { <script src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
function checkFriendRequests() {
const friendRequestsList = document.getElementById('friend-requests');
const friendRequestsSection = document.getElementById('friend-requests-section');
if (friendRequestsList && friendRequestsSection) {
if (friendRequestsList.children.length === 0) {
friendRequestsSection.style.display = 'none';
} else {
friendRequestsSection.style.display = 'block';
}
}
}
checkFriendRequests();
// Add event listeners if elements exist
const acceptButtons = document.querySelectorAll('.accept-friend-request');
const rejectButtons = document.querySelectorAll('.reject-friend-request');
acceptButtons.forEach(button => {
button.addEventListener('click', function() {
const requestId = this.dataset.requestId;
fetch(`/accept_friend/${requestId}`, { method: 'POST' })
.then(response => response.json())
.then(data => {
if (data.status === 'Friend request accepted') {
// Handle UI updates if necessary
this.closest('.friend-request').remove();
checkFriendRequests();
}
});
});
});
rejectButtons.forEach(button => {
button.addEventListener('click', function() {
const requestId = this.dataset.requestId;
fetch(`/reject_friend/${requestId}`, { method: 'POST' })
.then(response => response.json())
.then(data => {
if (data.status === 'Friend request rejected') {
// Handle UI updates if necessary
this.closest('.friend-request').remove();
checkFriendRequests();
}
});
});
});
const addFriendForm = document.getElementById('add-friend-form');
if (addFriendForm) {
addFriendForm.addEventListener('submit', function(event) {
event.preventDefault();
const formData = new FormData(addFriendForm);
fetch('/add_friend', {
method: 'POST',
body: formData
}).then(response => response.json())
.then(data => {
if (data.status === 'Friend request sent') {
alert('Friend request sent');
} else {
alert('Failed to send friend request');
}
});
});
}
});
</script>
</head> </head>
<body> <body>
{% include 'sidebar.html' %} {% include 'sidebar.html' %}
<div class="content"> <div class="content">
<h4>Friends</h4>
<ul id="friends-list">
{% for friend in friends %}
<li>
<a href="/chat/{{ friend.id }}">{{ friend.username }}</a>
</li>
{% endfor %}
</ul>
<div id="friend-requests-section"> <div id="friend-requests-section">
<h4>Friend Requests</h4> <h4>Friend Requests</h4>
<ul id="friend-requests"> <ul id="friend-requests">

View file

@ -3,16 +3,44 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Login</title> <title>Login</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head> </head>
<body> <body>
<h2>Login</h2> <div class="container">
<form method="POST"> <h2>Login</h2>
<label for="username">Username:</label> <form id="login-form" method="POST">
<input type="text" id="username" name="username" required><br> <div class="form-group">
<label for="password">Password:</label> <label for="username">Username</label>
<input type="password" id="password" name="password" required><br> <input type="text" class="form-control" id="username" name="username" required>
<button type="submit">Login</button> </div>
</form> <div class="form-group">
<a href="{{ url_for('register') }}">Register</a> <label for="password">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
</div>
<script>
document.getElementById('login-form').addEventListener('submit', function(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
axios.post('/login', formData)
.then(response => {
if (response.data.status === 'success') {
window.location.href = '/dashboard';
} else {
alert('Invalid credentials. Please try again.');
}
})
.catch(error => {
console.error('Error logging in:', error);
alert('An error occurred. Please try again.');
});
});
</script>
</body> </body>
</html> </html>

View file

@ -3,13 +3,24 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Sidebar</title> <title>Sidebar</title>
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head> </head>
<body> <body>
<div class="sidebar"> <div class="sidebar">
<h3>Welcome, {{ username }}</h3> <h3>Welcome, {{ username }}</h3>
<ul> <ul>
<li><a href="/dashboard">Dashboard</a></li> <div class="sidebar-tabs sidebar-selected">
<li><a href="/logout">Logout</a></li> <div class="sidebar-tabs-content"><a href="/dashboard">Friends</a></div>
</div>
</ul>
<h4>Direct Messages</h4>
<ul id="friends-list">
{% for friend in friends %}
<li data-username="{{ friend.username }}">
<span class="status-indicator {{ 'online' if friend.online else 'offline' }}" id="status-{{ friend.username }}"></span>
<a href="/chat/{{ friend.id }}">{{ friend.username }}</a>
</li>
{% endfor %}
</ul> </ul>
</div> </div>
</body> </body>