@@ -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,63 @@ 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(
4875+ f""" <!-- Password Requirements -->
4876+ <div class="bg-blue-50 dark:bg-blue-900 border border-blue-200 dark:border-blue-700 rounded-md p-4">
4877+ <div class="flex items-start">
4878+ <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">
4879+ <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"/>
4880+ </svg>
4881+ <div class="ml-3 flex-1">
4882+ <h3 class="text-sm font-semibold text-blue-900 dark:text-blue-200">Password Requirements</h3>
4883+ <div class="mt-2 text-sm text-blue-800 dark:text-blue-300 space-y-1">
4884+ <div class="flex items-center" id="req-length">
4885+ <span class="inline-flex items-center justify-center w-4 h-4 bg-gray-400 text-white rounded-full text-xs mr-2">✗</span>
4886+ <span>At least {settings.password_min_length} characters long</span>
4887+ </div>
4888+ """
4889+ )
4890+ if settings.password_require_uppercase:
4891+ pr_lines.append(
4892+ """
4893+ <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>
4894+ """
4895+ )
4896+ if settings.password_require_lowercase:
4897+ pr_lines.append(
4898+ """
4899+ <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>
4900+ """
4901+ )
4902+ if settings.password_require_numbers:
4903+ pr_lines.append(
4904+ """
4905+ <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>
4906+ """
4907+ )
4908+ if settings.password_require_special:
4909+ pr_lines.append(
4910+ """
4911+ <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>
4912+ """
4913+ )
4914+ pr_lines.append(
4915+ """
4916+ </div>
4917+ </div>
4918+ </div>
4919+ </div>
4920+ """
4921+ )
4922+ password_requirements_html = "".join(pr_lines)
4923+ else:
4924+ # Intentionally an empty string for HTML insertion when no requirements apply.
4925+ # This is not a password value; suppress Bandit false positive B105.
4926+ password_requirements_html = "" # nosec B105
4927+
48714928 # Create edit form HTML
48724929 edit_form = f"""
48734930 <div class="space-y-4">
@@ -4902,27 +4959,7 @@ async def admin_get_user_edit(
49024959 oninput="validatePasswordMatch()">
49034960 <div id="password-match-message" class="mt-1 text-sm text-red-600 hidden">Passwords do not match</div>
49044961 </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>
4962+ {password_requirements_html}
49264963
49274964 <script>
49284965 // Password policy settings injected from backend
@@ -4934,6 +4971,8 @@ async def admin_get_user_edit(
49344971 requireSpecial: {'true' if settings.password_require_special else 'false'}
49354972 }};
49364973
4974+ // (No debug output) passwordPolicy available in JS for logic below
4975+
49374976 function updateRequirementIcon(elementId, isValid) {{
49384977 const req = document.getElementById(elementId);
49394978 if (req) {{
@@ -4957,19 +4996,19 @@ async def admin_get_user_edit(
49574996
49584997 // Check uppercase requirement (if enabled)
49594998 const uppercaseCheck = !passwordPolicy.requireUppercase || /[A-Z]/.test(password);
4960- updateRequirementIcon('req-uppercase', /[A-Z]/.test(password) );
4999+ updateRequirementIcon('req-uppercase', uppercaseCheck );
49615000
49625001 // Check lowercase requirement (if enabled)
49635002 const lowercaseCheck = !passwordPolicy.requireLowercase || /[a-z]/.test(password);
4964- updateRequirementIcon('req-lowercase', /[a-z]/.test(password) );
5003+ updateRequirementIcon('req-lowercase', lowercaseCheck );
49655004
49665005 // Check numbers requirement (if enabled)
49675006 const numbersCheck = !passwordPolicy.requireNumbers || /[0-9]/.test(password);
4968- updateRequirementIcon('req-numbers', /[0-9]/.test(password) );
5007+ updateRequirementIcon('req-numbers', numbersCheck );
49695008
49705009 // Check special character requirement (if enabled) - matches backend set
4971- const specialCheck = !passwordPolicy.requireSpecial || /[!@#$%^&*(),.?": {{}}|<> ]/.test(password);
4972- updateRequirementIcon('req-special', /[!@#$%^&*(),.?":{{}}|<>]/.test(password) );
5010+ const specialCheck = !passwordPolicy.requireSpecial || /[!@#$%^&*()_+\\-\\=\\[\\] {{}};:'"\\\\|,.<>`~\\/\\? ]/.test(password);
5011+ updateRequirementIcon('req-special', specialCheck );
49735012
49745013 // Enable/disable submit button based on active requirements
49755014 const submitButton = document.querySelector('#user-edit-modal-content button[type="submit"]');
@@ -5000,9 +5039,25 @@ async def admin_get_user_edit(
50005039 }}
50015040 }}
50025041
5003- // Initialize validation on page load
5004- document.addEventListener('DOMContentLoaded', function() {{
5005- validatePasswordRequirements();
5042+ // Initialize validation when the form is present (supports HTMX-injected content)
5043+ (function initPasswordValidation() {{
5044+ if (document.getElementById('password-field')) {{
5045+ validatePasswordRequirements();
5046+ validatePasswordMatch();
5047+ }}
5048+ }})();
5049+
5050+ // Re-run validation after HTMX swaps content into the DOM (modal loaded via HTMX)
5051+ document.addEventListener('htmx:afterSwap', function(event) {{
5052+ try {{
5053+ const target = event.detail && event.detail.target ? event.detail.target : null;
5054+ if (target && (target.querySelector('#password-field') || target.id === 'user-edit-modal-content')) {{
5055+ validatePasswordRequirements();
5056+ validatePasswordMatch();
5057+ }}
5058+ }} catch (e) {{
5059+ // Ignore errors from HTMX event handling
5060+ }}
50065061 }});
50075062 </script>
50085063 <div class="flex justify-end space-x-3">
0 commit comments