🔐 RESTORE: Admin password reset functionality
✅ Added admin password reset capability with:
- Generate secure temporary passwords (12 chars with symbols)
- Create password reset links with 15-min expiry
- Beautiful modal UI with copy-to-clipboard functionality
- Toggle password visibility for security
- Reset buttons in both users list and user detail pages
- Proper error handling and loading states
- Responsive design for mobile/desktop
FEATURES:
- Admins can reset any user's password instantly
- Users get both temporary password and reset link options
- Secure password generation with proper entropy
- Audit logging of all password reset actions
- Professional UI with Bootstrap modals and tooltips
SAFE: Only affects admin functionality, calculator unchanged
This commit is contained in:
@@ -478,6 +478,52 @@ def reset_cost_factors():
|
|||||||
flash('An error occurred while resetting cost factors.', 'error')
|
flash('An error occurred while resetting cost factors.', 'error')
|
||||||
return redirect(url_for('admin.cost_factors'))
|
return redirect(url_for('admin.cost_factors'))
|
||||||
|
|
||||||
|
@bp.route('/user/<int:user_id>/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')
|
@bp.route('/debug-endpoints')
|
||||||
def debug_endpoints():
|
def debug_endpoints():
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|||||||
@@ -81,6 +81,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-outline-secondary reset-password-btn"
|
||||||
|
data-user-id="{{ user.id }}"
|
||||||
|
data-username="{{ user.username }}"
|
||||||
|
data-email="{{ user.email }}"
|
||||||
|
title="Reset User Password">
|
||||||
|
<i class="fas fa-key me-2"></i>Reset Password
|
||||||
|
</button>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-{{ 'warning' if user.is_active else 'success' }} toggle-user-btn"
|
class="btn btn-{{ 'warning' if user.is_active else 'success' }} toggle-user-btn"
|
||||||
data-user-id="{{ user.id }}"
|
data-user-id="{{ user.id }}"
|
||||||
@@ -241,7 +249,164 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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 = '<i class="fas fa-spinner fa-spin me-2"></i>Resetting Password...';
|
||||||
|
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 = `
|
||||||
|
<div class="modal fade" id="passwordResetModal" tabindex="-1" aria-labelledby="passwordResetModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-success text-white">
|
||||||
|
<h5 class="modal-title" id="passwordResetModalLabel">
|
||||||
|
<i class="fas fa-check-circle me-2"></i>Password Reset Successful
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<i class="fas fa-info-circle me-2"></i>
|
||||||
|
Password has been reset for user <strong>${data.username}</strong> (${data.email})
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0"><i class="fas fa-key me-2"></i>Temporary Password</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="password" class="form-control" id="tempPassword" value="${data.temporary_password}" readonly>
|
||||||
|
<button class="btn btn-outline-secondary" type="button" onclick="togglePasswordVisibility()">
|
||||||
|
<i class="fas fa-eye" id="passwordToggleIcon"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-primary" type="button" onclick="copyToClipboard('tempPassword')">
|
||||||
|
<i class="fas fa-copy"></i> Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">The user can log in with this temporary password and should change it immediately.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0"><i class="fas fa-link me-2"></i>Password Reset Link</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" id="resetUrl" value="${data.reset_url}" readonly>
|
||||||
|
<button class="btn btn-outline-primary" type="button" onclick="copyToClipboard('resetUrl')">
|
||||||
|
<i class="fas fa-copy"></i> Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">Send this link to the user so they can set their own password. Link expires in 15 minutes.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 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 = '<i class="fas fa-check"></i> 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');
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -88,15 +88,25 @@
|
|||||||
title="View User Details"
|
title="View User Details"
|
||||||
data-bs-toggle="tooltip">
|
data-bs-toggle="tooltip">
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-eye"></i>
|
||||||
<span class="d-none d-md-inline ms-1">View</span>
|
<span class="d-none d-lg-inline ms-1">View</span>
|
||||||
</a>
|
</a>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-outline-secondary btn-sm reset-password-btn"
|
||||||
|
data-user-id="{{ user.id }}"
|
||||||
|
data-username="{{ user.username }}"
|
||||||
|
data-email="{{ user.email }}"
|
||||||
|
title="Reset User Password"
|
||||||
|
data-bs-toggle="tooltip">
|
||||||
|
<i class="fas fa-key"></i>
|
||||||
|
<span class="d-none d-lg-inline ms-1">Reset</span>
|
||||||
|
</button>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-{{ 'warning' if user.is_active else 'success' }} btn-sm"
|
class="btn btn-{{ 'warning' if user.is_active else 'success' }} btn-sm"
|
||||||
onclick="toggleUserStatus({{ user.id }}, '{{ 'warning' if user.is_active else 'success' }}')"
|
onclick="toggleUserStatus({{ user.id }}, '{{ 'warning' if user.is_active else 'success' }}')"
|
||||||
title="{{ 'Deactivate User' if user.is_active else 'Activate User' }}"
|
title="{{ 'Deactivate User' if user.is_active else 'Activate User' }}"
|
||||||
data-bs-toggle="tooltip">
|
data-bs-toggle="tooltip">
|
||||||
<i class="fas fa-{{ 'ban' if user.is_active else 'check' }}"></i>
|
<i class="fas fa-{{ 'ban' if user.is_active else 'check' }}"></i>
|
||||||
<span class="d-none d-md-inline ms-1">{{ 'Deactivate' if user.is_active else 'Activate' }}</span>
|
<span class="d-none d-lg-inline ms-1">{{ 'Deactivate' if user.is_active else 'Activate' }}</span>
|
||||||
</button>
|
</button>
|
||||||
{% if user.id != current_user.id %}
|
{% if user.id != current_user.id %}
|
||||||
<button type="button"
|
<button type="button"
|
||||||
@@ -105,7 +115,7 @@
|
|||||||
title="Delete User Permanently"
|
title="Delete User Permanently"
|
||||||
data-bs-toggle="tooltip">
|
data-bs-toggle="tooltip">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
<span class="d-none d-md-inline ms-1">Delete</span>
|
<span class="d-none d-lg-inline ms-1">Delete</span>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -176,8 +186,165 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||||
return new bootstrap.Tooltip(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 = '<i class="fas fa-spinner fa-spin"></i><span class="d-none d-lg-inline ms-1">Resetting...</span>';
|
||||||
|
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 = `
|
||||||
|
<div class="modal fade" id="passwordResetModal" tabindex="-1" aria-labelledby="passwordResetModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-success text-white">
|
||||||
|
<h5 class="modal-title" id="passwordResetModalLabel">
|
||||||
|
<i class="fas fa-check-circle me-2"></i>Password Reset Successful
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<i class="fas fa-info-circle me-2"></i>
|
||||||
|
Password has been reset for user <strong>${data.username}</strong> (${data.email})
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0"><i class="fas fa-key me-2"></i>Temporary Password</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="password" class="form-control" id="tempPassword" value="${data.temporary_password}" readonly>
|
||||||
|
<button class="btn btn-outline-secondary" type="button" onclick="togglePasswordVisibility()">
|
||||||
|
<i class="fas fa-eye" id="passwordToggleIcon"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-primary" type="button" onclick="copyToClipboard('tempPassword')">
|
||||||
|
<i class="fas fa-copy"></i> Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">The user can log in with this temporary password and should change it immediately.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0"><i class="fas fa-link me-2"></i>Password Reset Link</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" id="resetUrl" value="${data.reset_url}" readonly>
|
||||||
|
<button class="btn btn-outline-primary" type="button" onclick="copyToClipboard('resetUrl')">
|
||||||
|
<i class="fas fa-copy"></i> Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">Send this link to the user so they can set their own password. Link expires in 15 minutes.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 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 = '<i class="fas fa-check"></i> 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) {
|
function toggleUserStatus(userId, status) {
|
||||||
const button = event.target.closest('button');
|
const button = event.target.closest('button');
|
||||||
const isActive = status === 'warning';
|
const isActive = status === 'warning';
|
||||||
@@ -263,5 +430,38 @@ function deleteUser(userId) {
|
|||||||
.btn-group .btn:last-child {
|
.btn-group .btn:last-child {
|
||||||
margin-right: 0;
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user