Updated online detection and fixed bugs

Updated online detection and fixed bugs
This commit is contained in:
Olai Vike Bøe 2024-06-25 23:27:34 +02:00
parent 0ac1f2795b
commit bc9fcda6aa
17 changed files with 342 additions and 169 deletions

167
app.py
View file

@ -1,13 +1,13 @@
from PIL import Image # Import the Image module from Pillow
from PIL import Image
from flask import Flask, render_template, request, redirect, url_for, session, jsonify, send_from_directory, make_response
from flask_sqlalchemy import SQLAlchemy
from cryptography.fernet import Fernet, InvalidToken
import hashlib
import os
import random
import string
from datetime import datetime, timedelta, timezone
from werkzeug.utils import secure_filename # Add this line
from werkzeug.security import generate_password_hash, check_password_hash
from werkzeug.utils import secure_filename
from flask_socketio import SocketIO, emit, join_room, leave_room
import logging
import jwt
@ -67,6 +67,7 @@ class User(db.Model):
is_admin = db.Column(db.Boolean, nullable=False, default=False)
disabled = db.Column(db.Boolean, nullable=False, default=False)
profile_picture = db.Column(db.String(150), nullable=True)
custom_css = db.Column(db.Text, nullable=True)
class Message(db.Model):
id = db.Column(db.Integer, primary_key=True)
@ -124,17 +125,21 @@ class GroupMessage(db.Model):
group = db.relationship('Group', foreign_keys=[group_id])
sender = db.relationship('User', foreign_keys=[sender_id])
def generate_salt():
return os.urandom(16).hex()
def sha256_password_hash(password, salt=None):
if salt is None:
salt = generate_salt()
hash = hashlib.sha256((salt + password).encode()).hexdigest()
return f"{salt}${hash}"
@app.route('/')
def index():
token = request.cookies.get('token')
if token:
user_id = decode_token(token)
if user_id:
if 'user_id' in session:
user_id = session['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'))
@ -143,7 +148,7 @@ def register():
if request.method == 'POST':
username = request.form['username'].lower()
password = request.form['password']
hashed_password = generate_password_hash(password) # Default method
hashed_password = sha256_password_hash(password)
new_user = User(username=username, password=hashed_password)
try:
db.session.add(new_user)
@ -165,10 +170,11 @@ def login():
password = request.form['password']
user = User.query.filter_by(username=username).first()
if user:
salt, stored_hash = user.password.split('$')
if user.disabled:
logger.warning(f"Disabled account login attempt: {username}")
return jsonify({'status': 'error', 'message': 'Account Disabled. Contact Support'})
if check_password_hash(user.password, password):
if stored_hash == hashlib.sha256((salt + password).encode()).hexdigest():
session['username'] = user.username
session['user_id'] = user.id
session['is_admin'] = user.is_admin
@ -182,28 +188,25 @@ def login():
return jsonify({'status': 'error', 'message': 'Invalid credentials. Please try again.'})
return render_template('login.html')
@app.route('/change_password', methods=['GET', 'POST'])
def change_password():
if request.method == 'POST':
username = request.form['username'].lower()
current_password = request.form['current_password']
new_password = request.form['new_password']
user = User.query.filter_by(username=username).first()
if user and check_password_hash(user.password, current_password):
hashed_new_password = generate_password_hash(new_password)
user.password = hashed_new_password
if user:
current_salt, current_hash = user.password.split('$')
if current_hash == hashlib.sha256((current_salt + current_password).encode()).hexdigest():
new_hashed_password = sha256_password_hash(new_password)
user.password = new_hashed_password
db.session.commit()
logger.info(f"Password changed for user: {username}")
return jsonify({'status': 'Password changed successfully'})
logger.warning(f"Password change failed for user: {username}")
return jsonify({'status': 'error'})
return render_template('change_password.html')
# Update your admin check accordingly
@app.route('/admin', methods=['GET'])
def admin_panel():
if 'username' in session:
@ -217,22 +220,20 @@ def admin_panel():
def admin_change_password():
if 'username' in session:
admin_user = User.query.filter_by(username=session['username']).first()
logger.debug(f"Admin User: {admin_user.username}, Is Admin: {admin_user.is_admin}")
if admin_user and admin_user.is_admin: # Check if the user is an admin
if admin_user and admin_user.is_admin:
try:
data = request.get_json() # Parse the JSON data from the request
data = request.get_json()
user_id = data['user_id']
new_password = data['new_password']
user = User.query.get(user_id)
if user:
user.password = generate_password_hash(new_password)
user.password = sha256_password_hash(new_password)
db.session.commit()
return jsonify({'status': 'success'})
return jsonify({'status': 'user not found'}), 404
except Exception as e:
logger.error(f"Error changing password: {e}")
return jsonify({'status': 'error', 'message': str(e)}), 500
logger.warning(f"Unauthorized attempt to access admin change password by user {session.get('username', 'unknown')}")
return jsonify({'status': 'error'}), 401
@app.route('/admin/disable_account', methods=['POST'])
@ -250,7 +251,6 @@ def admin_disable_account():
except Exception as e:
logger.error(f"Error disabling account: {e}")
return jsonify({'status': 'error', 'message': str(e)}), 500
logger.warning(f"Unauthorized attempt to disable account by user {session.get('username', 'unknown')}")
return jsonify({'status': 'error'}), 401
@app.route('/admin/enable_account', methods=['POST'])
@ -268,7 +268,6 @@ def admin_enable_account():
except Exception as e:
logger.error(f"Error enabling account: {e}")
return jsonify({'status': 'error', 'message': str(e)}), 500
logger.warning(f"Unauthorized attempt to enable account by user {session.get('username', 'unknown')}")
return jsonify({'status': 'error'}), 401
@app.route('/dashboard')
@ -281,9 +280,8 @@ def dashboard():
friend_requests = FriendRequest.query.filter_by(receiver_id=user.id, status='pending').all()
pending_messages = PendingMessage.query.filter_by(receiver=session['username']).all()
messages = Message.query.filter_by(receiver_id=user.id).all() # Updated line
messages = Message.query.filter_by(receiver_id=user.id).all()
# Fetch groups for the user
group_memberships = GroupMember.query.filter_by(user_id=user.id).all()
group_ids = [membership.group_id for membership in group_memberships]
groups = Group.query.filter(Group.id.in_(group_ids)).all()
@ -315,13 +313,12 @@ def chat(friend_id):
friend_user = User.query.filter_by(id=friend_id).first()
user = User.query.filter_by(id=current_user_id).first()
friends = Friend.query.filter_by(user_id=user.id).all()
profile_picture = user.profile_picture # Assuming this is the path to the profile picture
profile_picture = user.profile_picture
friend_ids = [f.friend_id for f in friends]
friend_users = User.query.filter(User.id.in_(friend_ids)).all()
friend_requests = FriendRequest.query.filter_by(receiver_id=user.id, status='pending').all()
# Fetch groups for the user
group_memberships = GroupMember.query.filter_by(user_id=user.id).all()
group_ids = [membership.group_id for membership in group_memberships]
groups = Group.query.filter(Group.id.in_(group_ids)).all()
@ -366,9 +363,9 @@ def send_message(receiver):
decrypted_content = cipher.decrypt(encrypted_content.encode()).decode() if content_type == 'text' else encrypted_content
socketio.emit('new_message', {
'sender': session['username'],
'receiver': receiver_user.username, # Include the receiver information
'receiver': receiver_user.username,
'content': decrypted_content,
'sender_profile_picture': sender_user.profile_picture, # Ensure this is correct
'sender_profile_picture': sender_user.profile_picture,
'content_type': content_type,
'timestamp': timestamp,
'id': new_message.id
@ -380,7 +377,6 @@ def send_message(receiver):
db.session.add(pending_message)
db.session.commit()
logger.info(f"Pending message from {session['username']} to {receiver_user.username}")
return jsonify({'status': 'Pending message sent', 'message_id': pending_message.id, 'sender_profile_picture': sender_user.profile_picture}), 200
return jsonify({'error': 'No content or file provided'}), 400
except Exception as e:
@ -430,8 +426,8 @@ def compress_image(image_path, quality=15):
if img.format == 'PNG':
img.save(image_path, "PNG", optimize=True)
else:
img = img.convert("RGB") # Ensure the image is in RGB mode for non-PNG files
img.save(image_path, "JPEG", quality=quality) # Save with JPEG format and compression
img = img.convert("RGB")
img.save(image_path, "JPEG", quality=quality)
@app.route('/upload_profile_picture', methods=['POST'])
def upload_profile_picture():
@ -446,15 +442,12 @@ def upload_profile_picture():
filename = f"{filename_parts[0]}-{random_string}.{filename_parts[1]}"
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(file_path)
# Compress the image
compress_image(file_path, quality=85)
user.profile_picture = filename
db.session.commit()
logger.info(f"User {session['username']} uploaded a new profile picture: {filename}")
return redirect(url_for('dashboard')) # Redirect to a valid endpoint
return redirect(url_for('dashboard')) # Redirect to a valid endpoint if not logged in
return redirect(url_for('dashboard'))
return redirect(url_for('dashboard'))
@app.route('/cdn/<filename>')
def uploaded_file(filename):
@ -463,23 +456,19 @@ def uploaded_file(filename):
@app.route('/add_friend', methods=['POST'])
def add_friend():
if 'username' in session:
friend_username = request.form['friend_username'].lower() # Convert to lowercase
friend_username = request.form['friend_username'].lower()
user = User.query.filter_by(username=session['username']).first()
friend = User.query.filter_by(username=friend_username).first()
if friend and user != friend:
# Check if friend request already exists
existing_request = FriendRequest.query.filter_by(sender_id=user.id, receiver_id=friend.id).first()
if existing_request:
return jsonify({'status': 'Friend request already sent'})
# Check if already friends
existing_friend = Friend.query.filter(
((Friend.user_id == user.id) & (Friend.friend_id == friend.id)) |
((Friend.user_id == friend.id) & (Friend.friend_id == user.id))
).first()
if existing_friend:
return jsonify({'status': 'Already friends'})
friend_request = FriendRequest(sender_id=user.id, receiver_id=friend.id)
db.session.add(friend_request)
db.session.commit()
@ -493,7 +482,6 @@ def add_friend():
return jsonify({'status': 'Friend not found or cannot add yourself as a friend'})
return jsonify({'status': 'Unauthorized'}), 401
@app.route('/accept_friend/<int:request_id>', methods=['POST'])
def accept_friend(request_id):
if 'username' in session:
@ -502,7 +490,6 @@ def accept_friend(request_id):
friend_request.status = 'accepted'
db.session.commit()
# Create friendships both ways
user_id = friend_request.receiver_id
friend_id = friend_request.sender_id
new_friend_1 = Friend(user_id=user_id, friend_id=friend_id)
@ -511,7 +498,6 @@ def accept_friend(request_id):
db.session.add(new_friend_2)
db.session.commit()
# Move pending messages to messages
pending_messages = PendingMessage.query.filter_by(sender=friend_request.sender.username, receiver=friend_request.receiver.username).all()
for pm in pending_messages:
new_message = Message(sender=pm.sender, receiver=pm.receiver, content=pm.content, content_type=pm.content_type, timestamp=pm.timestamp)
@ -524,7 +510,6 @@ def accept_friend(request_id):
'receiver': friend_request.receiver.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'))
logger.warning(f"Friend request accept failed: Friend request not found or unauthorized access by {session['username']}")
return 'Friend request not found'
@ -537,13 +522,11 @@ def reject_friend(request_id):
if friend_request and friend_request.receiver.username == session['username']:
db.session.delete(friend_request)
db.session.commit()
socketio.emit('friend_request_rejected', {
'sender': friend_request.sender.username,
'receiver': friend_request.receiver.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'))
logger.warning(f"Friend request reject failed: Friend request not found or unauthorized access by {session['username']}")
return 'Friend request not found'
@ -560,7 +543,6 @@ def remove_friend(friend_id):
if reciprocal_friend:
db.session.delete(reciprocal_friend)
# Remove friend requests associated with the friend
sent_request = FriendRequest.query.filter_by(sender_id=user.id, receiver_id=friend_id).first()
if sent_request:
db.session.delete(sent_request)
@ -569,13 +551,11 @@ def remove_friend(friend_id):
db.session.delete(received_request)
db.session.commit()
socketio.emit('friend_removed', {
'sender': session['username'],
'receiver': friend.friend.username
}, room=friend.friend.username)
logger.info(f"Friend {friend.friend.username} removed by {session['username']}")
return jsonify({'status': 'success'}), 200
logger.warning(f"Friend removal failed: Friend not found or unauthorized access by {session['username']}")
return jsonify({'status': 'error', 'message': 'Friend not found or unauthorized access'}), 404
@ -584,20 +564,33 @@ def remove_friend(friend_id):
@app.route('/create_group', methods=['POST'])
def create_group():
if 'username' in session:
try:
user = User.query.filter_by(username=session['username']).first()
group_name = request.form['group_name']
member_usernames = [username.strip() for username in request.form['members'].split(',')]
# Create a new group
existing_group = Group.query.filter_by(name=group_name).first()
if existing_group:
return jsonify({'status': 'Error', 'message': 'A group with this name already exists.'}), 400
for username in member_usernames:
member = User.query.filter_by(username=username).first()
if not member:
return jsonify({'status': 'Error', 'message': f'User {username} does not exist.'}), 400
friendship = Friend.query.filter(
((Friend.user_id == user.id) & (Friend.friend_id == member.id)) |
((Friend.user_id == member.id) & (Friend.friend_id == user.id))
).first()
if not friendship:
return jsonify({'status': 'Error', 'message': f'User {username} is not your friend.'}), 400
new_group = Group(name=group_name, admin_id=user.id)
db.session.add(new_group)
db.session.commit()
db.session.flush()
# Add the group creator as a member
new_group_member = GroupMember(group_id=new_group.id, user_id=user.id)
db.session.add(new_group_member)
# Add other members to the group
for username in member_usernames:
member = User.query.filter_by(username=username).first()
if member:
@ -605,9 +598,11 @@ def create_group():
db.session.add(new_group_member)
db.session.commit()
return redirect(url_for('dashboard'))
return redirect(url_for('login'))
return jsonify({'status': 'Group created'})
except Exception as e:
db.session.rollback()
return jsonify({'status': 'Error', 'message': str(e)}), 500
return jsonify({'status': 'Unauthorized'}), 401
@app.route('/invite_to_group', methods=['POST'])
def invite_to_group():
@ -672,12 +667,10 @@ def send_group_message(group_id):
encrypted_content = cipher.encrypt(content.encode()).decode() if content_type == 'text' else content
timestamp_dt = datetime.strptime(timestamp, '%Y-%m-%d %H:%M:%S').replace(tzinfo=timezone.utc)
# Fetch the sender user object
sender_user = User.query.filter_by(username=session['username']).first()
if not sender_user:
return jsonify({'error': 'User not found'}), 404
# Create new group message
new_message = GroupMessage(group_id=group_id, sender_id=sender_user.id, content=encrypted_content, content_type=content_type, timestamp=timestamp_dt)
db.session.add(new_message)
db.session.commit()
@ -687,7 +680,7 @@ def send_group_message(group_id):
'group_id': group_id,
'sender': session['username'],
'content': decrypted_content,
'sender_profile_picture': sender_user.profile_picture, # Add profile picture
'sender_profile_picture': sender_user.profile_picture,
'content_type': content_type,
'timestamp': timestamp,
'id': new_message.id
@ -710,8 +703,8 @@ def get_group_messages(group_id):
{
'id': msg.GroupMessage.id,
'group_id': msg.GroupMessage.group_id,
'sender': msg.User.username, # Use the username from the User table
'sender_profile_picture': msg.User.profile_picture, # Add profile picture
'sender': msg.User.username,
'sender_profile_picture': msg.User.profile_picture,
'content': cipher.decrypt(msg.GroupMessage.content.encode()).decode() if msg.GroupMessage.content_type == 'text' else msg.GroupMessage.content,
'content_type': msg.GroupMessage.content_type,
'timestamp': msg.GroupMessage.timestamp.strftime("%Y-%m-%d %H:%M:%S")
@ -750,6 +743,26 @@ def settings():
return render_template('settings.html', current_theme=current_theme)
return redirect(url_for('login'))
@app.route('/custom_css', methods=['GET', 'POST'])
def custom_css():
if 'username' in session:
user = User.query.filter_by(username=session['username']).first()
if request.method == 'POST':
custom_css = request.form.get('custom_css')
user.custom_css = custom_css
db.session.commit()
return redirect(url_for('dashboard'))
return render_template('custom_css.html', custom_css=user.custom_css if user.custom_css else "")
return redirect(url_for('login'))
@app.context_processor
def inject_user():
if 'username' in session:
user = User.query.filter_by(username=session['username']).first()
return dict(current_user=user)
return dict(current_user=None)
@app.route('/logout')
def logout():
session.pop('username', None)
@ -759,12 +772,17 @@ def logout():
logger.info(f"User logged out")
return response
@socketio.on('ping')
def handle_ping():
emit('pong')
@socketio.on('connect')
def handle_connect():
if 'username' in session:
user = User.query.filter_by(username=session['username']).first()
if user:
user.online = True
user.last_activity = datetime.utcnow()
db.session.commit()
emit('user_online', {'username': user.username}, broadcast=True)
logger.info(f"User {session['username']} connected")
@ -805,6 +823,23 @@ def handle_leave_group(data):
leave_room(f"group_{group_id}")
logger.info(f"User {username} left group room {group_id}")
@socketio.on('user_activity')
def handle_user_activity():
if 'username' in session:
user = User.query.filter_by(username=session['username']).first()
if user:
user.last_activity = datetime.utcnow()
db.session.commit()
def set_users_to_idle():
with app.app_context():
idle_threshold = datetime.utcnow() - timedelta(minutes=5)
users = User.query.filter(User.online == True, User.last_activity < idle_threshold).all()
for user in users:
user.online = False
db.session.commit()
emit('user_idle', {'username': user.username}, broadcast=True)
if __name__ == '__main__':
with app.app_context():
db.create_all()

View file

@ -122,3 +122,11 @@ hr{
border: none;
@apply !bg-[#f1f1f1] dark:!bg-[#363636]
}
.button-admin{
@apply bg-transparent border-solid border-1 border-black dark:border-white p-2 shadow-lg
}
.button-admin:disabled{
@apply bg-neutral-400 text-neutral-200 dark:bg-neutral-700 dark:text-neutral-500
}

View file

@ -18,7 +18,7 @@ document.addEventListener('DOMContentLoaded', (event) => {
socket.emit('join', { username: username });
document.title = `Chatting with ${friendUsername}`;
document.title = `${friendUsername}`;
const messagesList = document.getElementById('messages');
const imageOverlay = document.getElementById('image-overlay');
@ -147,7 +147,7 @@ document.addEventListener('DOMContentLoaded', (event) => {
// Include the profile picture if available
const profilePicture = data.sender_profile_picture ? `<img src="/cdn/${data.sender_profile_picture}" alt="${data.sender}" class="profile-picture enhanceable-image">` : '';
usernameElement.innerHTML = `${profilePicture} <strong>${data.sender}</strong>: <div class="message-sender-timestamp">${timestampFormatted}</div>`;
usernameElement.innerHTML = `${profilePicture} <strong>${data.sender}</strong><div class="message-sender-timestamp">${timestampFormatted}</div>`;
messagesList.appendChild(usernameElement);
}
@ -179,7 +179,7 @@ document.addEventListener('DOMContentLoaded', (event) => {
.then(response => response.json())
.then(data => {
messagesList.innerHTML = '';
document.getElementById('chat-with').textContent = `Chatting with ${friendUsername}`;
document.getElementById('chat-with').textContent = `${friendUsername}`;
document.getElementById('send-message-form').dataset.receiver = friendId;
data.messages.forEach((msg, index, messages) => {
@ -261,7 +261,7 @@ document.addEventListener('DOMContentLoaded', (event) => {
usernameElement.classList.add('message-sender');
const timestampFormatted = formatLocalTime(timestamp);
const profilePictureUrl = `/cdn/${profilePicture}`; // Correct reference
usernameElement.innerHTML = `<img src="${profilePictureUrl}" alt="${username}" class="profile-picture enhanceable-image"> <strong>${username}</strong>: <div class="message-sender-timestamp">${timestampFormatted}</div>`;
usernameElement.innerHTML = `<img src="${profilePictureUrl}" alt="${username}" class="profile-picture enhanceable-image"> <strong>${username}</strong><div class="message-sender-timestamp">${timestampFormatted}</div>`;
messagesList.appendChild(usernameElement);
}
@ -436,21 +436,4 @@ document.addEventListener('DOMContentLoaded', (event) => {
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');
}
});
});

View file

@ -0,0 +1,33 @@
document.addEventListener('DOMContentLoaded', (event) => {
const socket = io();
let isConnected = true;
function checkConnection() {
socket.emit('ping');
}
socket.on('pong', function () {
isConnected = true;
});
setInterval(() => {
if (!isConnected) {
if (confirm('Something went wrong, please reload the website')) {
window.location.reload();
}
}
isConnected = false;
checkConnection();
}, 3000);
socket.on('connect', () => {
isConnected = true;
});
socket.on('disconnect', () => {
isConnected = false;
if (confirm('Something went wrong, please reload the website')) {
window.location.reload();
}
});
});

View file

@ -1,24 +1,6 @@
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');

View file

@ -3,7 +3,7 @@ document.addEventListener('DOMContentLoaded', (event) => {
const md = window.markdownit();
socket.emit('join_group', { group_id: groupId });
document.title = `Group Chat: ${groupName}`;
document.title = `${groupName}`;
const messagesList = document.getElementById('messages');
const imageOverlay = document.getElementById('image-overlay');
@ -76,7 +76,7 @@ document.addEventListener('DOMContentLoaded', (event) => {
const usernameElement = document.createElement('div');
usernameElement.classList.add('message-sender');
const profilePictureUrl = `/cdn/${data.sender_profile_picture}`;
usernameElement.innerHTML = `<img src="${profilePictureUrl}" alt="${data.sender}" class="profile-picture"> <strong>${data.sender}</strong>:<div class="message-sender-timestamp">${formatLocalTime(data.timestamp)}</div>`;
usernameElement.innerHTML = `<img src="${profilePictureUrl}" alt="${data.sender}" class="profile-picture enhanceable-image"> <strong>${data.sender}</strong><div class="message-sender-timestamp">${formatLocalTime(data.timestamp)}</div>`;
messagesList.appendChild(usernameElement);
}
@ -112,7 +112,7 @@ document.addEventListener('DOMContentLoaded', (event) => {
})
.then(data => {
messagesList.innerHTML = '';
document.getElementById('chat-with').textContent = `Group Chat: ${groupName}`;
document.getElementById('chat-with').textContent = `${groupName}`;
document.getElementById('send-message-form').dataset.receiver = groupId;
data.messages.forEach((msg, index, messages) => {
@ -128,7 +128,7 @@ document.addEventListener('DOMContentLoaded', (event) => {
usernameElement.classList.add('message-myself-sender');
}
const profilePictureUrl = `/cdn/${msg.sender_profile_picture}`;
usernameElement.innerHTML = `<img src="${profilePictureUrl}" alt="${msg.sender}" class="profile-picture"> <strong>${msg.sender}</strong><div class="message-sender-timestamp">${formatLocalTime(msg.timestamp)}</div>`;
usernameElement.innerHTML = `<img src="${profilePictureUrl}" alt="${msg.sender}" class="profile-picture enhanceable-image"> <strong>${msg.sender}</strong><div class="message-sender-timestamp">${formatLocalTime(msg.timestamp)}</div>`;
messagesList.appendChild(usernameElement);
}
@ -345,21 +345,4 @@ document.addEventListener('DOMContentLoaded', (event) => {
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');
}
});
});

View file

@ -0,0 +1,20 @@
document.addEventListener('DOMContentLoaded', (event) => {
const socket = io();
// Handle user online/offline status
socket.on('user_online', function(data) {
const statusIndicator = document.querySelector(`.status-indicator[data-username="${data.username}"]`);
if (statusIndicator) {
statusIndicator.classList.remove('offline');
statusIndicator.classList.add('online');
}
});
socket.on('user_offline', function(data) {
const statusIndicator = document.querySelector(`.status-indicator[data-username="${data.username}"]`);
if (statusIndicator) {
statusIndicator.classList.remove('online');
statusIndicator.classList.add('offline');
}
});
});

View file

@ -616,6 +616,10 @@ video {
margin-bottom: 1.25rem;
}
.ml-10 {
margin-left: 2.5rem;
}
.mr-5 {
margin-right: 1.25rem;
}
@ -640,6 +644,10 @@ video {
height: 400px !important;
}
.\!h-\[460px\] {
height: 460px !important;
}
.\!h-\[465px\] {
height: 465px !important;
}
@ -686,6 +694,10 @@ video {
width: 100%;
}
.min-w-\[280px\] {
min-width: 280px;
}
.flex-1 {
flex: 1 1 0%;
}
@ -1203,6 +1215,37 @@ hr:is(.dark *) {
background-color: rgb(54 54 54 / var(--tw-bg-opacity)) !important;
}
.button-admin {
border-style: solid;
--tw-border-opacity: 1;
border-color: rgb(0 0 0 / var(--tw-border-opacity));
background-color: transparent;
padding: 0.5rem;
--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
border-width: 1px;
}
.button-admin:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(255 255 255 / var(--tw-border-opacity));
}
.button-admin:disabled {
--tw-bg-opacity: 1;
background-color: rgb(163 163 163 / var(--tw-bg-opacity));
--tw-text-opacity: 1;
color: rgb(229 229 229 / var(--tw-text-opacity));
}
.button-admin:disabled:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(64 64 64 / var(--tw-bg-opacity));
--tw-text-opacity: 1;
color: rgb(115 115 115 / var(--tw-text-opacity));
}
.hover\:\!text-slate-400:hover {
--tw-text-opacity: 1 !important;
color: rgb(148 163 184 / var(--tw-text-opacity)) !important;

View file

@ -358,6 +358,10 @@ ul#friends-list {
background-color: #80848e;
}
.idle {
background-color: #f0b232;
}
/* Custom Scroll Bar */
::-webkit-scrollbar {

View file

@ -69,19 +69,28 @@
<table class="table dark:!text-white text-black">
<thead>
<tr>
<th>ID</th>
<th>Username</th>
<th>Actions</th>
<th>Change Password</th>
<th>Disable Account</th>
<th>Online?</th>
<th>Admin?</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.id }}</td>
<td>{{ user.username }}</td>
<td>
<button class="btn btn-warning" onclick="changePassword({{ user.id }})">Change Password</button>
<button onclick="disableAccount({{ user.id }})" {% if user.disabled %}disabled{% endif %}>Disable</button>
<button onclick="enableAccount({{ user.id }})" {% if not user.disabled %}disabled{% endif %}>Enable</button>
</td>
<td>
<button class="button-admin" onclick="disableAccount({{ user.id }})" {% if user.disabled %}disabled{% endif %}>Disable</button>
<button class="button-admin" onclick="enableAccount({{ user.id }})" {% if not user.disabled %}disabled{% endif %}>Enable</button>
</td>
<td>{{ user.online }}</td>
<td>{{ user.is_admin }}</td>
</tr>
{% endfor %}
</tbody>

View file

@ -1,3 +1,3 @@
<div class="bg-white h-16 border-solid border-1 border-[#cad6d8] dark:!border-[#595959] flex items-center dark:bg-[#242424]">
<i class="fa-solid fa-hashtag dark:!text-neutral-200"></i><h3 id="chat-with" class="dark:!text-neutral-200"> Chatting with {{ friend_username }}</h3>
<div class="bg-white h-16 border-solid border-l-1 border-r-1 border-[#cad6d8] dark:!border-[#595959] flex items-center dark:bg-[#242424]">
<i class="fa-solid fa-hashtag dark:!text-neutral-200"></i><h3 id="chat-with" class="dark:!text-neutral-200">{{ friend_username }}</h3>
</div>

View file

@ -1,3 +1,3 @@
<div class="bg-white h-16 border-solid border-1 border-[#cad6d8] dark:!border-[#595959] flex items-center dark:bg-[#242424]">
<i class="fa-solid fa-hashtag dark:!text-neutral-200"></i><h3 id="chat-with" class="dark:!text-neutral-200"> Group Chat: {{ group.name }}</h3>
<div class="bg-white h-16 border-solid border-l-1 border-r-1 border-[#cad6d8] dark:!border-[#595959] flex items-center dark:bg-[#242424]">
<i class="fa-solid fa-hashtag dark:!text-neutral-200"></i><h3 id="chat-with" class="dark:!text-neutral-200">{{ group.name }}</h3>
</div>

26
templates/custom_css.html Normal file
View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Custom CSS</title>
{% include 'head.html' %}
</head>
<body class="login-body">
<div class="login-div !h-[460px]">
<form class="login-form" id="custom-css-form" method="POST">
<h2 class="login-h2">Custom CSS</h2>
<div class="login-input-div-first">
<label for="custom_css" class="login-label">Enter your custom CSS:</label>
<textarea class="login-input" id="custom_css" name="custom_css" rows="10">{{ custom_css }}</textarea>
</div>
<div class="login-input-div-first !flex-row">
<a class="no-account no-account-button" href="{{ url_for('dashboard') }}"> Go Back</a>
</div>
<div class="login-input-div-first items-end">
<button class="ms-button-signin" type="submit" class="btn btn-primary">Save CSS</button>
</div>
</form>
<div id="message" class="mt-3"></div>
</div>
</body>
</html>

View file

@ -43,15 +43,50 @@
</form>
</div>
<!-- <div>
<h4 class="text-black dark:!text-white"X>Create Group</h4>
<form id="create-group-form" method="POST">
<input type="text" name="group_name" placeholder="Group Name" required autocomplete="off">
<input type="text" name="members" placeholder="Member Usernames (comma separated)" required autocomplete="off">
{% if is_admin %}
<div class="dark:!bg-[#1f1f1f] shadow-lg bg-[#f1f1f1] w-fit flex ml-10 flex-col rounded-md items-center">
<h4 class="text-black dark:!text-white relative top-2">Create Group</h4>
<form id="create-group-form" method="POST" class="w-fit flex flex-col items-center justify-center">
<input class="dark:!bg-[#1f1f1f] bg-[#f1f1f1] pt-5 rounded-t-md w-3/4" type="text" name="group_name" placeholder="Group Name" required autocomplete="off">
<input class="dark:!bg-[#1f1f1f] bg-[#f1f1f1] pt-5 rounded-t-md w-full min-w-[280px]" type="text" name="members" placeholder="Member Usernames (comma separated)" required autocomplete="off">
<button class="ms-button" type="submit">Create Group</button>
<div id="create-group-message" style="color: green;"></div>
</form>
</div> -->
</div>
<script>
document.addEventListener('DOMContentLoaded', (event) => {
const createGroupForm = document.getElementById('create-group-form');
if (createGroupForm) {
createGroupForm.addEventListener('submit', function(event) {
event.preventDefault();
const formData = new FormData(createGroupForm);
const members = formData.get('members').split(',').map(member => member.trim());
formData.set('members', members.join(','));
fetch('/create_group', {
method: 'POST',
body: formData
}).then(response => response.json())
.then(data => {
if (data.status === 'Group created') {
alert('Group created successfully');
location.reload();
} else {
alert('Error creating group: ' + data.message);
}
})
.catch(error => {
alert('Error creating group: ' + error.message);
});
});
}
});
</script>
{% endif %}
</div>
</div>
</div>

View file

@ -1,5 +1,6 @@
<!-- <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"> -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='output.css') }}">
@ -9,6 +10,7 @@
<script src="https://vjs.zencdn.net/7.11.4/video.min.js"></script>
<script src="https://kit.fontawesome.com/47dbf3c43e.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="{{ url_for('static', filename='js/connection_check.js') }}"></script>
<script>
// Function to apply the theme based on user selection or system preference
@ -34,3 +36,8 @@
}
});
</script>
{% if 'username' in session %}
<style>
{{ current_user.custom_css | safe }}
</style>
{% endif %}

View file

@ -1,3 +1,4 @@
<script src="{{ url_for('static', filename='js/online_detection.js') }}"></script>
<div class="sidebar w-fit bg-transparent text-white flex flex-col items-center">
<div class="sidebar-content h-full w-60 p-5 rounded-t-lg">
<h4 class="flex flex-row items-center">
@ -11,7 +12,7 @@
<ul id="friends-list">
{% for friend in friends %}
<li data-id="{{ friend.id }}" data-username="{{ friend.username }}">
<span class="status-indicator {{ 'online' if friend.online else 'offline' }}"></span>
<span class="status-indicator {{ 'online' if friend.online else 'offline' }}" data-username="{{ friend.username }}"></span>
<a class="dark:!text-slate-400 text-slate-900 hover:!text-slate-400 hover:dark:!text-slate-200" href="/chat/{{ friend.id }}">{{ friend.username }}</a>
</li>
{% endfor %}

View file

@ -8,10 +8,14 @@
<p>{{ username }}</p>
<a href="/settings"><i class="fa-solid fa-gear"></i> Settings</a>
<a href="/change_password"><i class="fa-solid fa-lock"></i> Change Password</a>
<a href="{{ url_for('custom_css') }}"><i class="fa-brands fa-css3-alt"></i> Custom CSS</a>
{% if is_admin %}
<a href="/admin"><i class="fa-solid fa-shield-alt"></i> Admin Panel</a>
{% endif %}
<a href="/logout"><i class="fa-solid fa-right-from-bracket"></i> Logout</a>
</div>
</div>