diff --git a/routes/__pycache__/auth.cpython-313.pyc b/routes/__pycache__/auth.cpython-313.pyc index cc0be2c..3792ef5 100644 Binary files a/routes/__pycache__/auth.cpython-313.pyc and b/routes/__pycache__/auth.cpython-313.pyc differ diff --git a/routes/__pycache__/contacts.cpython-313.pyc b/routes/__pycache__/contacts.cpython-313.pyc index eefd5f6..82385d2 100644 Binary files a/routes/__pycache__/contacts.cpython-313.pyc and b/routes/__pycache__/contacts.cpython-313.pyc differ diff --git a/routes/__pycache__/conversations.cpython-313.pyc b/routes/__pycache__/conversations.cpython-313.pyc index 1d3995c..7919942 100644 Binary files a/routes/__pycache__/conversations.cpython-313.pyc and b/routes/__pycache__/conversations.cpython-313.pyc differ diff --git a/routes/__pycache__/main.cpython-313.pyc b/routes/__pycache__/main.cpython-313.pyc index ffba016..3d533c6 100644 Binary files a/routes/__pycache__/main.cpython-313.pyc and b/routes/__pycache__/main.cpython-313.pyc differ diff --git a/routes/__pycache__/rooms.cpython-313.pyc b/routes/__pycache__/rooms.cpython-313.pyc index f893e97..8a6e38e 100644 Binary files a/routes/__pycache__/rooms.cpython-313.pyc and b/routes/__pycache__/rooms.cpython-313.pyc differ diff --git a/routes/auth.py b/routes/auth.py index ba06704..15fab4e 100644 --- a/routes/auth.py +++ b/routes/auth.py @@ -1,6 +1,16 @@ from flask import render_template, request, flash, redirect, url_for from flask_login import login_user, logout_user, login_required, current_user from models import db, User +from functools import wraps + +def require_password_change(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if current_user.is_authenticated and current_user.check_password('changeme'): + flash('Please change your password before continuing.', 'warning') + return redirect(url_for('auth.change_password')) + return f(*args, **kwargs) + return decorated_function def init_routes(auth_bp): @auth_bp.route('/login', methods=['GET', 'POST']) @@ -18,8 +28,14 @@ def init_routes(auth_bp): if not user or not user.check_password(password): flash('Please check your login details and try again.', 'danger') return redirect(url_for('auth.login')) - + login_user(user, remember=remember) + + # Check if user is using default password + if password == 'changeme': + flash('Please change your password before continuing.', 'warning') + return redirect(url_for('auth.change_password')) + next_page = request.args.get('next') if next_page: return redirect(next_page) @@ -62,4 +78,27 @@ def init_routes(auth_bp): @login_required def logout(): logout_user() - return redirect(url_for('auth.login')) \ No newline at end of file + return redirect(url_for('auth.login')) + + @auth_bp.route('/change-password', methods=['GET', 'POST']) + @login_required + def change_password(): + if request.method == 'POST': + current_password = request.form.get('current_password') + new_password = request.form.get('new_password') + confirm_password = request.form.get('confirm_password') + + if not current_user.check_password(current_password): + flash('Current password is incorrect.', 'danger') + return redirect(url_for('auth.change_password')) + + if new_password != confirm_password: + flash('New passwords do not match.', 'danger') + return redirect(url_for('auth.change_password')) + + current_user.set_password(new_password) + db.session.commit() + flash('Password changed successfully!', 'success') + return redirect(url_for('main.dashboard')) + + return render_template('auth/change_password.html') \ No newline at end of file diff --git a/routes/contacts.py b/routes/contacts.py index 9fcc849..d48f176 100644 --- a/routes/contacts.py +++ b/routes/contacts.py @@ -4,6 +4,7 @@ from models import db, User from forms import UserForm from flask import abort from sqlalchemy import or_ +from routes.auth import require_password_change import json import os from werkzeug.utils import secure_filename @@ -23,6 +24,7 @@ def admin_required(): @contacts_bp.route('/') @login_required +@require_password_change def contacts_list(): result = admin_required() if result: return result @@ -76,6 +78,7 @@ def contacts_list(): @contacts_bp.route('/new', methods=['GET', 'POST']) @login_required +@require_password_change def new_contact(): result = admin_required() if result: return result @@ -114,19 +117,16 @@ def new_contact(): is_admin=form.is_admin.data, profile_picture=profile_picture ) - if form.new_password.data: - user.set_password(form.new_password.data) - else: - flash('Password is required when creating a new user.', 'error') - return render_template('contacts/form.html', form=form, title='New User', total_admins=total_admins) + user.set_password('changeme') # Set default password db.session.add(user) db.session.commit() - flash('User created successfully!', 'success') + flash('User created successfully! They will need to change their password on first login.', 'success') return redirect(url_for('contacts.contacts_list')) return render_template('contacts/form.html', form=form, title='New User', total_admins=total_admins) @contacts_bp.route('/profile/edit', methods=['GET', 'POST']) @login_required +@require_password_change def edit_profile(): form = UserForm() total_admins = User.query.filter_by(is_admin=True).count() @@ -168,6 +168,7 @@ def edit_profile(): @contacts_bp.route('//edit', methods=['GET', 'POST']) @login_required +@require_password_change def edit_contact(id): result = admin_required() if result: return result @@ -237,6 +238,7 @@ def edit_contact(id): @contacts_bp.route('//delete', methods=['POST']) @login_required +@require_password_change def delete_contact(id): result = admin_required() if result: return result @@ -251,6 +253,7 @@ def delete_contact(id): @contacts_bp.route('//toggle-active', methods=['POST']) @login_required +@require_password_change def toggle_active(id): result = admin_required() if result: return result diff --git a/routes/conversations.py b/routes/conversations.py index 1d482cb..c9c0427 100644 --- a/routes/conversations.py +++ b/routes/conversations.py @@ -3,6 +3,7 @@ from flask_login import login_required, current_user from flask_socketio import emit, join_room, leave_room from models import db, Conversation, User, Message, MessageAttachment from forms import ConversationForm +from routes.auth import require_password_change import os from werkzeug.utils import secure_filename from datetime import datetime @@ -44,6 +45,7 @@ def get_file_extension(filename): @conversations_bp.route('/') @login_required +@require_password_change def conversations(): search = request.args.get('search', '').strip() if current_user.is_admin: @@ -57,6 +59,7 @@ def conversations(): @conversations_bp.route('/create', methods=['GET', 'POST']) @login_required +@require_password_change def create_conversation(): if not current_user.is_admin: flash('Only administrators can create conversations.', 'error') @@ -89,6 +92,7 @@ def create_conversation(): @conversations_bp.route('/') @login_required +@require_password_change def conversation(conversation_id): conversation = Conversation.query.get_or_404(conversation_id) # Check if user is a member @@ -109,6 +113,7 @@ def conversation(conversation_id): @conversations_bp.route('//members') @login_required +@require_password_change def conversation_members(conversation_id): conversation = Conversation.query.get_or_404(conversation_id) if not current_user.is_admin and current_user not in conversation.members: @@ -126,6 +131,7 @@ def conversation_members(conversation_id): @conversations_bp.route('//members/add', methods=['POST']) @login_required +@require_password_change def add_member(conversation_id): conversation = Conversation.query.get_or_404(conversation_id) if not current_user.is_admin: @@ -149,6 +155,7 @@ def add_member(conversation_id): @conversations_bp.route('//members//remove', methods=['POST']) @login_required +@require_password_change def remove_member(conversation_id, user_id): conversation = Conversation.query.get_or_404(conversation_id) if not current_user.is_admin: @@ -170,6 +177,7 @@ def remove_member(conversation_id, user_id): @conversations_bp.route('//edit', methods=['GET', 'POST']) @login_required +@require_password_change def edit_conversation(conversation_id): if not current_user.is_admin: flash('Only administrators can edit conversations.', 'error') @@ -213,6 +221,7 @@ def edit_conversation(conversation_id): @conversations_bp.route('//delete', methods=['POST']) @login_required +@require_password_change def delete_conversation(conversation_id): if not current_user.is_admin: flash('Only administrators can delete conversations.', 'error') @@ -251,6 +260,7 @@ def on_leave(data): @conversations_bp.route('//send_message', methods=['POST']) @login_required +@require_password_change def send_message(conversation_id): conversation = Conversation.query.get_or_404(conversation_id) @@ -331,6 +341,7 @@ def send_message(conversation_id): @conversations_bp.route('/messages//attachment/') @login_required +@require_password_change def download_attachment(message_id, attachment_index): message = Message.query.get_or_404(message_id) conversation = message.conversation @@ -353,6 +364,7 @@ def download_attachment(message_id, attachment_index): @conversations_bp.route('//messages') @login_required +@require_password_change def get_messages(conversation_id): conversation = Conversation.query.get_or_404(conversation_id) diff --git a/routes/main.py b/routes/main.py index 5a78311..1644b7f 100644 --- a/routes/main.py +++ b/routes/main.py @@ -1,6 +1,7 @@ from flask import render_template, Blueprint, redirect, url_for, request, flash, Response from flask_login import current_user, login_required from models import User, db, Room, RoomFile, RoomMemberPermission, SiteSettings +from routes.auth import require_password_change import os from werkzeug.utils import secure_filename from sqlalchemy import func, case, literal_column, text @@ -26,13 +27,14 @@ def init_routes(main_bp): return dict(site_settings=site_settings) @main_bp.route('/') + @login_required + @require_password_change def home(): - if not current_user.is_authenticated: - return redirect(url_for('auth.login')) return redirect(url_for('main.dashboard')) @main_bp.route('/dashboard') @login_required + @require_password_change def dashboard(): logger.info("Loading dashboard...") # Get 3 most recent users @@ -265,6 +267,7 @@ def init_routes(main_bp): @main_bp.route('/profile', methods=['GET', 'POST']) @login_required + @require_password_change def profile(): if request.method == 'POST': # Handle profile picture removal @@ -321,16 +324,19 @@ def init_routes(main_bp): @main_bp.route('/starred') @login_required + @require_password_change def starred(): return render_template('starred/starred.html') @main_bp.route('/conversations') @login_required + @require_password_change def conversations(): return redirect(url_for('conversations.conversations')) @main_bp.route('/trash') @login_required + @require_password_change def trash(): return render_template('trash/trash.html') diff --git a/routes/rooms.py b/routes/rooms.py index a401200..ed8f6ed 100644 --- a/routes/rooms.py +++ b/routes/rooms.py @@ -3,11 +3,13 @@ from flask_login import login_required, current_user from models import db, Room, User, RoomMemberPermission, RoomFile from forms import RoomForm from routes.room_files import user_has_permission +from routes.auth import require_password_change rooms_bp = Blueprint('rooms', __name__, url_prefix='/rooms') @rooms_bp.route('/') @login_required +@require_password_change def rooms(): search = request.args.get('search', '').strip() if current_user.is_admin: @@ -27,6 +29,7 @@ def rooms(): @rooms_bp.route('/create', methods=['GET', 'POST']) @login_required +@require_password_change def create_room(): form = RoomForm() if form.validate_on_submit(): @@ -56,6 +59,7 @@ def create_room(): @rooms_bp.route('/') @login_required +@require_password_change def room(room_id): room = Room.query.get_or_404(room_id) # Admins always have access @@ -74,6 +78,7 @@ def room(room_id): @rooms_bp.route('//members') @login_required +@require_password_change def room_members(room_id): room = Room.query.get_or_404(room_id) # Admins always have access @@ -91,6 +96,7 @@ def room_members(room_id): @rooms_bp.route('//members/add', methods=['POST']) @login_required +@require_password_change def add_member(room_id): room = Room.query.get_or_404(room_id) # Membership check using RoomMemberPermission @@ -121,6 +127,7 @@ def add_member(room_id): @rooms_bp.route('//members//remove', methods=['POST']) @login_required +@require_password_change def remove_member(room_id, user_id): room = Room.query.get_or_404(room_id) # Membership check using RoomMemberPermission diff --git a/templates/auth/change_password.html b/templates/auth/change_password.html new file mode 100644 index 0000000..ee16885 --- /dev/null +++ b/templates/auth/change_password.html @@ -0,0 +1,50 @@ +{% extends "common/base.html" %} + +{% block title %}Change Password{% endblock %} + +{% block content %} +
+
+
+

Change Password

+
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/contacts/form.html b/templates/contacts/form.html index bad06bd..69fbafd 100644 --- a/templates/contacts/form.html +++ b/templates/contacts/form.html @@ -6,7 +6,7 @@

- + {% if title %} {{ title }} {% else %} @@ -71,36 +71,6 @@ {% endif %}

- {% if current_user.is_admin %} -
- {{ form.new_password.label(class="block text-sm font-medium text-gray-700 mb-1") }} - {{ form.new_password(class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 pr-10", autocomplete="new-password", id="new_password") }} - -
-
- {{ form.confirm_password.label(class="block text-sm font-medium text-gray-700 mb-1") }} - {{ form.confirm_password(class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 pr-10", autocomplete="new-password", id="confirm_password") }} - - {% if form.confirm_password.errors %} - {% for error in form.confirm_password.errors %} -

{{ error }}

- {% endfor %} - {% endif %} -
- - {% endif %} -
{{ form.phone.label(class="block text-sm font-medium text-gray-700 mb-1") }} @@ -177,7 +147,10 @@
- {{ form.submit(class="text-white px-6 py-2 rounded-lg transition duration-200", style="background-color: #16767b; border: 1px solid #16767b;", onmouseover="this.style.backgroundColor='#1a8a90'", onmouseout="this.style.backgroundColor='#16767b'") }} + {{ form.submit(class="text-white px-6 py-2 rounded-lg transition duration-200", + style="background-color: var(--primary-color); border: 1px solid var(--primary-color);", + onmouseover="this.style.backgroundColor='var(--primary-light)'", + onmouseout="this.style.backgroundColor='var(--primary-color)'") }}