diff --git a/src/app/routes/admin.py b/src/app/routes/admin.py index 0fd72bb..f2ee0a1 100644 --- a/src/app/routes/admin.py +++ b/src/app/routes/admin.py @@ -478,6 +478,52 @@ def reset_cost_factors(): flash('An error occurred while resetting cost factors.', 'error') return redirect(url_for('admin.cost_factors')) +@bp.route('/user//reset-password', methods=['POST']) +@login_required +@limiter.limit("10 per minute") +def admin_reset_password(user_id): + """Admin function to reset a user's password""" + try: + user = User.query.get_or_404(user_id) + + # Generate a temporary password + import secrets + import string + + # Generate a secure temporary password (12 characters with mix of letters, numbers, symbols) + temp_password = ''.join(secrets.choice(string.ascii_letters + string.digits + '!@#$%^&*') for _ in range(12)) + + # Set the new password + user.set_password(temp_password) + + # Generate a reset token for the user (optional - they can use this to set their own password) + reset_token = user.generate_reset_token() + + db.session.commit() + + # Log the password reset + current_app.logger.info(f'Admin {current_user.email} reset password for user {user.email}') + + # Create reset URL + reset_url = url_for('auth.reset_password', token=reset_token, _external=True) + + return jsonify({ + 'status': 'success', + 'message': 'Password reset successfully', + 'temporary_password': temp_password, + 'reset_url': reset_url, + 'username': user.username, + 'email': user.email + }) + + except Exception as e: + db.session.rollback() + current_app.logger.error(f'Admin password reset error: {str(e)}') + return jsonify({ + 'status': 'error', + 'message': 'An error occurred while resetting the password' + }), 500 + @bp.route('/debug-endpoints') def debug_endpoints(): from flask import current_app diff --git a/src/app/templates/admin/user_detail.html b/src/app/templates/admin/user_detail.html index 88678c0..8c45169 100644 --- a/src/app/templates/admin/user_detail.html +++ b/src/app/templates/admin/user_detail.html @@ -81,6 +81,14 @@
+ +
+ + +
+ + + `; + + // Remove existing modal if present + const existingModal = document.getElementById('passwordResetModal'); + if (existingModal) { + existingModal.remove(); + } + + // Add modal to body + document.body.insertAdjacentHTML('beforeend', modalHtml); + + // Show modal + const modal = new bootstrap.Modal(document.getElementById('passwordResetModal')); + modal.show(); + + // Clean up modal after it's hidden + document.getElementById('passwordResetModal').addEventListener('hidden.bs.modal', function () { + this.remove(); + }); +} + +function togglePasswordVisibility() { + const passwordField = document.getElementById('tempPassword'); + const toggleIcon = document.getElementById('passwordToggleIcon'); + + if (passwordField.type === 'password') { + passwordField.type = 'text'; + toggleIcon.className = 'fas fa-eye-slash'; + } else { + passwordField.type = 'password'; + toggleIcon.className = 'fas fa-eye'; + } +} + +function copyToClipboard(elementId) { + const element = document.getElementById(elementId); + element.select(); + element.setSelectionRange(0, 99999); // For mobile devices + + navigator.clipboard.writeText(element.value).then(function() { + // Show temporary success feedback + const button = event.target.closest('button'); + const originalHtml = button.innerHTML; + button.innerHTML = ' Copied!'; + button.classList.add('btn-success'); + button.classList.remove('btn-outline-primary'); + + setTimeout(() => { + button.innerHTML = originalHtml; + button.classList.remove('btn-success'); + button.classList.add('btn-outline-primary'); + }, 2000); + }).catch(function(err) { + console.error('Could not copy text: ', err); + alert('Could not copy to clipboard'); + }); +} {% endblock %} diff --git a/src/app/templates/admin/users.html b/src/app/templates/admin/users.html index 2cd0f97..7cb7954 100644 --- a/src/app/templates/admin/users.html +++ b/src/app/templates/admin/users.html @@ -88,15 +88,25 @@ title="View User Details" data-bs-toggle="tooltip"> - View + View + {% if user.id != current_user.id %} {% endif %} @@ -176,8 +186,165 @@ document.addEventListener('DOMContentLoaded', function() { var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl); }); + + // Reset password functionality + document.querySelectorAll('.reset-password-btn').forEach(button => { + button.addEventListener('click', function() { + const userId = this.dataset.userId; + const username = this.dataset.username; + const email = this.dataset.email; + + if (!confirm(`Are you sure you want to reset the password for user "${username}"?\n\nThis will generate a new temporary password and invalidate their current password.`)) { + return; + } + + // Show loading state + const originalHtml = this.innerHTML; + this.innerHTML = 'Resetting...'; + this.disabled = true; + + fetch(`/admin/user/${userId}/reset-password`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + } + }) + .then(response => response.json()) + .then(data => { + if (data.status === 'success') { + showPasswordResetModal(data); + } else { + alert('Error: ' + data.message); + } + }) + .catch(error => { + console.error('Error:', error); + alert('An error occurred while resetting the password'); + }) + .finally(() => { + // Restore button state + this.innerHTML = originalHtml; + this.disabled = false; + }); + }); + }); }); +function showPasswordResetModal(data) { + const modalHtml = ` + + `; + + // Remove existing modal if present + const existingModal = document.getElementById('passwordResetModal'); + if (existingModal) { + existingModal.remove(); + } + + // Add modal to body + document.body.insertAdjacentHTML('beforeend', modalHtml); + + // Show modal + const modal = new bootstrap.Modal(document.getElementById('passwordResetModal')); + modal.show(); + + // Clean up modal after it's hidden + document.getElementById('passwordResetModal').addEventListener('hidden.bs.modal', function () { + this.remove(); + }); +} + +function togglePasswordVisibility() { + const passwordField = document.getElementById('tempPassword'); + const toggleIcon = document.getElementById('passwordToggleIcon'); + + if (passwordField.type === 'password') { + passwordField.type = 'text'; + toggleIcon.className = 'fas fa-eye-slash'; + } else { + passwordField.type = 'password'; + toggleIcon.className = 'fas fa-eye'; + } +} + +function copyToClipboard(elementId) { + const element = document.getElementById(elementId); + element.select(); + element.setSelectionRange(0, 99999); // For mobile devices + + navigator.clipboard.writeText(element.value).then(function() { + // Show temporary success feedback + const button = event.target.closest('button'); + const originalHtml = button.innerHTML; + button.innerHTML = ' Copied!'; + button.classList.add('btn-success'); + button.classList.remove('btn-outline-primary'); + + setTimeout(() => { + button.innerHTML = originalHtml; + button.classList.remove('btn-success'); + button.classList.add('btn-outline-primary'); + }, 2000); + }).catch(function(err) { + console.error('Could not copy text: ', err); + alert('Could not copy to clipboard'); + }); +} + function toggleUserStatus(userId, status) { const button = event.target.closest('button'); const isActive = status === 'warning'; @@ -263,5 +430,38 @@ function deleteUser(userId) { .btn-group .btn:last-child { margin-right: 0; } + +/* Action buttons responsiveness */ +@media (max-width: 1199.98px) { + .btn-group .btn span { + display: none !important; + } + + .btn-group .btn { + padding: 0.25rem 0.5rem; + min-width: 32px; + } +} + +/* Reset password button styling */ +.reset-password-btn:hover { + background-color: #6c757d; + border-color: #6c757d; + color: white; +} + +/* Modal improvements */ +.modal-lg { + max-width: 800px; +} + +.input-group .form-control { + font-family: 'Courier New', monospace; + font-size: 0.9rem; +} + +.alert-success { + border-left: 4px solid #28a745; +} {% endblock %} \ No newline at end of file