@@ -3757,7 +3757,7 @@ async def admin_get_team_edit(
37573757 if not team:
37583758 return HTMLResponse(content='<div class="text-red-500">Team not found</div>', status_code=404)
37593759
3760- edit_form = f """
3760+ edit_form = rf """
37613761 <div class="space-y-4">
37623762 <h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Edit Team</h3>
37633763 <form method="post" action="{root_path}/admin/teams/{team_id}/update" hx-post="{root_path}/admin/teams/{team_id}/update" hx-target="#team-edit-modal-content" class="space-y-4">
@@ -4868,6 +4868,51 @@ async def admin_get_user_edit(
48684868 if not user_obj:
48694869 return HTMLResponse(content='<div class="text-red-500">User not found</div>', status_code=404)
48704870
4871+ # Build Password Requirements HTML separately to avoid backslash issues inside f-strings
4872+ if settings.password_require_uppercase or settings.password_require_lowercase or settings.password_require_numbers or settings.password_require_special:
4873+ pr_lines = []
4874+ pr_lines.append(f""" <!-- Password Requirements -->
4875+ <div class="bg-blue-50 dark:bg-blue-900 border border-blue-200 dark:border-blue-700 rounded-md p-4">
4876+ <div class="flex items-start">
4877+ <svg class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" viewBox="0 0 20 20" fill="currentColor">
4878+ <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
4879+ </svg>
4880+ <div class="ml-3 flex-1">
4881+ <h3 class="text-sm font-semibold text-blue-900 dark:text-blue-200">Password Requirements</h3>
4882+ <div class="mt-2 text-sm text-blue-800 dark:text-blue-300 space-y-1">
4883+ <div class="flex items-center" id="req-length">
4884+ <span class="inline-flex items-center justify-center w-4 h-4 bg-gray-400 text-white rounded-full text-xs mr-2">✗</span>
4885+ <span>At least {settings.password_min_length} characters long</span>
4886+ </div>
4887+ """)
4888+ if settings.password_require_uppercase:
4889+ pr_lines.append("""
4890+ <div class="flex items-center" id="req-uppercase"><span class="inline-flex items-center justify-center w-4 h-4 bg-gray-400 text-white rounded-full text-xs mr-2">✗</span><span>Contains uppercase letters (A-Z)</span></div>
4891+ """)
4892+ if settings.password_require_lowercase:
4893+ pr_lines.append("""
4894+ <div class="flex items-center" id="req-lowercase"><span class="inline-flex items-center justify-center w-4 h-4 bg-gray-400 text-white rounded-full text-xs mr-2">✗</span><span>Contains lowercase letters (a-z)</span></div>
4895+ """)
4896+ if settings.password_require_numbers:
4897+ pr_lines.append("""
4898+ <div class="flex items-center" id="req-numbers"><span class="inline-flex items-center justify-center w-4 h-4 bg-gray-400 text-white rounded-full text-xs mr-2">✗</span><span>Contains numbers (0-9)</span></div>
4899+ """)
4900+ if settings.password_require_special:
4901+ pr_lines.append("""
4902+ <div class="flex items-center" id="req-special"><span class="inline-flex items-center justify-center w-4 h-4 bg-gray-400 text-white rounded-full text-xs mr-2">✗</span><span>Contains special characters (!@#$%^&*(),.?":{{}}|<>)</span></div>
4903+ """)
4904+ pr_lines.append("""
4905+ </div>
4906+ </div>
4907+ </div>
4908+ </div>
4909+ """)
4910+ password_requirements_html = "".join(pr_lines)
4911+ else:
4912+ # Intentionally an empty string for HTML insertion when no requirements apply.
4913+ # This is not a password value; suppress Bandit false positive B105.
4914+ password_requirements_html = "" # nosec B105
4915+
48714916 # Create edit form HTML
48724917 edit_form = f"""
48734918 <div class="space-y-4">
@@ -4902,27 +4947,7 @@ async def admin_get_user_edit(
49024947 oninput="validatePasswordMatch()">
49034948 <div id="password-match-message" class="mt-1 text-sm text-red-600 hidden">Passwords do not match</div>
49044949 </div>
4905- <!-- Password Requirements -->
4906- <div class="bg-blue-50 dark:bg-blue-900 border border-blue-200 dark:border-blue-700 rounded-md p-4">
4907- <div class="flex items-start">
4908- <svg class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" viewBox="0 0 20 20" fill="currentColor">
4909- <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
4910- </svg>
4911- <div class="ml-3 flex-1">
4912- <h3 class="text-sm font-semibold text-blue-900 dark:text-blue-200">Password Requirements</h3>
4913- <div class="mt-2 text-sm text-blue-800 dark:text-blue-300 space-y-1">
4914- <div class="flex items-center" id="req-length">
4915- <span class="inline-flex items-center justify-center w-4 h-4 bg-gray-400 text-white rounded-full text-xs mr-2">✗</span>
4916- <span>At least {settings.password_min_length} characters long</span>
4917- </div>
4918- {'<div class="flex items-center" id="req-uppercase"><span class="inline-flex items-center justify-center w-4 h-4 bg-gray-400 text-white rounded-full text-xs mr-2">✗</span><span>Contains uppercase letters (A-Z)</span></div>' if settings.password_require_uppercase else ''}
4919- {'<div class="flex items-center" id="req-lowercase"><span class="inline-flex items-center justify-center w-4 h-4 bg-gray-400 text-white rounded-full text-xs mr-2">✗</span><span>Contains lowercase letters (a-z)</span></div>' if settings.password_require_lowercase else ''}
4920- {'<div class="flex items-center" id="req-numbers"><span class="inline-flex items-center justify-center w-4 h-4 bg-gray-400 text-white rounded-full text-xs mr-2">✗</span><span>Contains numbers (0-9)</span></div>' if settings.password_require_numbers else ''}
4921- {'<div class="flex items-center" id="req-special"><span class="inline-flex items-center justify-center w-4 h-4 bg-gray-400 text-white rounded-full text-xs mr-2">✗</span><span>Contains special characters (!@#$%^&*(),.?":{{}}|<>)</span></div>' if settings.password_require_special else ''}
4922- </div>
4923- </div>
4924- </div>
4925- </div>
4950+ {password_requirements_html}
49264951
49274952 <script>
49284953 // Password policy settings injected from backend
@@ -4934,6 +4959,8 @@ async def admin_get_user_edit(
49344959 requireSpecial: {'true' if settings.password_require_special else 'false'}
49354960 }};
49364961
4962+ // (No debug output) passwordPolicy available in JS for logic below
4963+
49374964 function updateRequirementIcon(elementId, isValid) {{
49384965 const req = document.getElementById(elementId);
49394966 if (req) {{
@@ -4957,19 +4984,19 @@ async def admin_get_user_edit(
49574984
49584985 // Check uppercase requirement (if enabled)
49594986 const uppercaseCheck = !passwordPolicy.requireUppercase || /[A-Z]/.test(password);
4960- updateRequirementIcon('req-uppercase', /[A-Z]/.test(password) );
4987+ updateRequirementIcon('req-uppercase', uppercaseCheck );
49614988
49624989 // Check lowercase requirement (if enabled)
49634990 const lowercaseCheck = !passwordPolicy.requireLowercase || /[a-z]/.test(password);
4964- updateRequirementIcon('req-lowercase', /[a-z]/.test(password) );
4991+ updateRequirementIcon('req-lowercase', lowercaseCheck );
49654992
49664993 // Check numbers requirement (if enabled)
49674994 const numbersCheck = !passwordPolicy.requireNumbers || /[0-9]/.test(password);
4968- updateRequirementIcon('req-numbers', /[0-9]/.test(password) );
4995+ updateRequirementIcon('req-numbers', numbersCheck );
49694996
49704997 // Check special character requirement (if enabled) - matches backend set
4971- const specialCheck = !passwordPolicy.requireSpecial || /[!@#$%^&*(),.?": {{}}|<> ]/.test(password);
4972- updateRequirementIcon('req-special', /[!@#$%^&*(),.?":{{}}|<>]/.test(password) );
4998+ const specialCheck = !passwordPolicy.requireSpecial || /[!@#$%^&*()_+\\-\\=\\[\\] {{}};:'"\\\\|,.<>`~\\/\\? ]/.test(password);
4999+ updateRequirementIcon('req-special', specialCheck );
49735000
49745001 // Enable/disable submit button based on active requirements
49755002 const submitButton = document.querySelector('#user-edit-modal-content button[type="submit"]');
@@ -5000,9 +5027,25 @@ async def admin_get_user_edit(
50005027 }}
50015028 }}
50025029
5003- // Initialize validation on page load
5004- document.addEventListener('DOMContentLoaded', function() {{
5005- validatePasswordRequirements();
5030+ // Initialize validation when the form is present (supports HTMX-injected content)
5031+ (function initPasswordValidation() {{
5032+ if (document.getElementById('password-field')) {{
5033+ validatePasswordRequirements();
5034+ validatePasswordMatch();
5035+ }}
5036+ }})();
5037+
5038+ // Re-run validation after HTMX swaps content into the DOM (modal loaded via HTMX)
5039+ document.addEventListener('htmx:afterSwap', function(event) {{
5040+ try {{
5041+ const target = event.detail && event.detail.target ? event.detail.target : null;
5042+ if (target && (target.querySelector('#password-field') || target.id === 'user-edit-modal-content')) {{
5043+ validatePasswordRequirements();
5044+ validatePasswordMatch();
5045+ }}
5046+ }} catch (e) {{
5047+ // Ignore errors from HTMX event handling
5048+ }}
50065049 }});
50075050 </script>
50085051 <div class="flex justify-end space-x-3">
0 commit comments