password.html
506 lignes · 22918 octets
{% extends "base.html" %} {% block title %}{{ page_title }}{% endblock %} {% block navbar %} {% if !must_change %} {% include "components/navbar.html" %} {% endif %} {% endblock %} {% block content %} {% if must_change %} <div class="min-h-screen flex items-center justify-center bg-base-200 px-4 py-12"> <div class="w-full max-w-lg"> <div class="text-center mb-8"> <div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-warning/20 mb-4"> <svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-warning" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg> </div> <h1 class="text-3xl font-bold mb-2">{{ title }}</h1> <p class="text-base-content/70 text-lg">{{ must_change_message }}</p> </div> {% else %} <div class="container mx-auto px-4 py-8"> <div class="text-sm breadcrumbs mb-6"> <ul> <li><a href="/">{{ breadcrumb_home }}</a></li> <li><a href="/profile">{{ breadcrumb_settings }}</a></li> <li>{{ breadcrumb_password }}</li> </ul> </div> <div class="mb-8"> <h1 class="text-3xl font-bold mb-2">{{ title }}</h1> {% if password_change_require_email %} <p class="text-base-content/70">{{ subtitle }}</p> {% else %} <p class="text-base-content/70">{{ subtitle_no_email }}</p> {% endif %} </div> {% endif %} <!-- Feature 026: Pending password change request alert --> {% if has_pending_request %} <div class="alert alert-info mb-6 max-w-2xl"> <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> <div class="flex-1"> <h3 class="font-bold">{{ pending_request_message }}</h3> <div class="text-sm">{{ pending_request_info }}</div> </div> <div> <button id="cancelPendingRequestBtn" class="btn btn-sm btn-ghost" onclick="cancelPendingRequest()" > {{ pending_request_cancel }} </button> </div> </div> {% endif %} <!-- Password Change Form Card --> <div class="card bg-base-100 shadow-xl max-w-2xl"> <div class="card-body"> <h2 class="card-title mb-4"> <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg> {{ new_password_label }} </h2> <form id="passwordForm" method="POST" action="/settings/password" class="space-y-6"> <!-- CSRF Token --> <input type="hidden" name="csrf_token" value="{{ csrf_token }}"> <!-- Old Password --> <div class="form-control"> <label class="label"> <span class="label-text font-semibold">{{ current_password_label }}</span> <span class="label-text-alt text-error">{{ required }}</span> </label> <input type="password" name="old_password" id="oldPassword" placeholder="{{ current_password_placeholder }}" class="input input-bordered w-full" required autocomplete="current-password" > </div> <!-- New Password --> <div class="form-control"> <label class="label"> <span class="label-text font-semibold">{{ new_password_label }}</span> <span class="label-text-alt text-error">{{ required }}</span> </label> <input type="password" name="new_password" id="newPassword" placeholder="{{ new_password_placeholder }}" class="input input-bordered w-full" required minlength="{{ pw_min_length }}" autocomplete="new-password" > <label class="label"> <span class="label-text-alt text-base-content/70"> {{ requirements }} </span> </label> <!-- Password Strength Indicator --> <div class="mt-2"> <div id="strengthBar" class="hidden"> <label class="label"> <span class="label-text-alt">{{ strength_label }}</span> <span id="strengthText" class="label-text-alt font-semibold"></span> </label> <progress id="strengthProgress" class="progress w-full" value="0" max="100"></progress> </div> </div> </div> <!-- Confirm Password --> <div class="form-control"> <label class="label"> <span class="label-text font-semibold">{{ confirm_password_label }}</span> <span class="label-text-alt text-error">{{ required }}</span> </label> <input type="password" name="confirm_password" id="confirmPassword" placeholder="{{ confirm_password_placeholder }}" class="input input-bordered w-full" required minlength="{{ pw_min_length }}" autocomplete="new-password" > <label class="label"> <span id="matchIndicator" class="label-text-alt text-base-content/70"></span> </label> </div> <!-- Security Alert --> <div class="alert alert-warning"> <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg> <div> <h3 class="font-bold">{{ logout_warning_title }}</h3> <div class="text-xs">{{ logout_warning_desc }}</div> </div> </div> <!-- Form Actions --> <div class="card-actions justify-end"> {% if must_change %} <a href="/api/v1/auth/logout" class="btn btn-ghost" onclick="event.preventDefault(); localStorage.removeItem('refresh_token'); localStorage.removeItem('remember_me'); localStorage.removeItem('jwt_expires_in'); fetch('/api/v1/auth/logout', {method:'POST', credentials:'include', headers:{'Accept':'text/html'}}).then(()=>window.location.href='/login');"> {{ logout_label }} </a> {% else %} <a href="/profile" class="btn btn-ghost">{{ cancel }}</a> {% endif %} <button type="submit" class="btn btn-error" id="submitBtn" disabled> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg> {{ change_button }} </button> </div> </form> </div> </div> {% if must_change %} </div> </div> {% endif %} <!-- Security Tips Card --> {% if !must_change %} <div class="card bg-base-200 shadow-xl max-w-2xl mt-6"> <div class="card-body"> <h3 class="card-title text-base"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg> {{ security_tips_title }} </h3> <ul class="list-disc list-inside text-sm space-y-2 text-base-content/80"> <li>{{ security_tip_unique }}</li> <li>{{ security_tip_share }}</li> <li>{{ security_tip_regular }}</li> <li>{{ security_tip_2fa }}</li> <li>{{ security_tip_manager }}</li> </ul> </div> </div> {% endif %} {% if !must_change %} </div> {% endif %} <script> const newPasswordInput = document.getElementById('newPassword'); const confirmPasswordInput = document.getElementById('confirmPassword'); const submitBtn = document.getElementById('submitBtn'); const strengthBar = document.getElementById('strengthBar'); const strengthProgress = document.getElementById('strengthProgress'); const strengthText = document.getElementById('strengthText'); const matchIndicator = document.getElementById('matchIndicator'); // Password rules from app_settings (injected by server) const PW_MIN_LENGTH = {{ pw_min_length }}; const PW_REQ_UPPER = {{ pw_require_uppercase }}; const PW_REQ_LOWER = {{ pw_require_lowercase }}; const PW_REQ_DIGITS = {{ pw_require_digits }}; const PW_REQ_SPECIAL= {{ pw_require_special }}; function checkPasswordStrength(password) { let strength = 0; let feedback = []; const totalChecks = 1 + (PW_REQ_UPPER ? 1 : 0) + (PW_REQ_LOWER ? 1 : 0) + (PW_REQ_DIGITS ? 1 : 0) + (PW_REQ_SPECIAL? 1 : 0); const pointsPerCheck = Math.floor(100 / (totalChecks + 1)); if (password.length >= PW_MIN_LENGTH) strength += pointsPerCheck; if (password.length >= PW_MIN_LENGTH + 4) strength += pointsPerCheck; if (password.length < PW_MIN_LENGTH) feedback.push('{{ strength_too_short }}'); if (PW_REQ_LOWER) { if (/[a-z]/.test(password)) strength += pointsPerCheck; else feedback.push('{{ strength_missing_lowercase }}'); } if (PW_REQ_UPPER) { if (/[A-Z]/.test(password)) strength += pointsPerCheck; else feedback.push('{{ strength_missing_uppercase }}'); } if (PW_REQ_DIGITS) { if (/[0-9]/.test(password)) strength += pointsPerCheck; else feedback.push('{{ strength_missing_digit }}'); } if (PW_REQ_SPECIAL) { if (/[^a-zA-Z0-9]/.test(password)) strength += pointsPerCheck; else feedback.push('{{ strength_missing_special }}'); } if (!PW_REQ_LOWER && /[a-z]/.test(password)) strength += 5; if (!PW_REQ_UPPER && /[A-Z]/.test(password)) strength += 5; if (!PW_REQ_DIGITS && /[0-9]/.test(password)) strength += 5; if (!PW_REQ_SPECIAL&& /[^a-zA-Z0-9]/.test(password)) strength += 5; strength = Math.min(strength, 100); return { strength, feedback }; } // Update password strength indicator newPasswordInput.addEventListener('input', function() { const password = this.value; if (password.length > 0) { strengthBar.classList.remove('hidden'); const { strength, feedback } = checkPasswordStrength(password); strengthProgress.value = strength; if (strength < 40) { strengthProgress.className = 'progress progress-error w-full'; strengthText.textContent = '{{ strength_weak }} (' + feedback.join(', ') + ')'; strengthText.className = 'label-text-alt font-semibold text-error'; } else if (strength < 70) { strengthProgress.className = 'progress progress-warning w-full'; strengthText.textContent = '{{ strength_medium }}' + (feedback.length ? ' (' + feedback.join(', ') + ')' : ''); strengthText.className = 'label-text-alt font-semibold text-warning'; } else { strengthProgress.className = 'progress progress-success w-full'; strengthText.textContent = '{{ strength_strong }}'; strengthText.className = 'label-text-alt font-semibold text-success'; } } else { strengthBar.classList.add('hidden'); } checkFormValidity(); }); // Check password match confirmPasswordInput.addEventListener('input', function() { if (this.value.length > 0) { if (this.value === newPasswordInput.value) { matchIndicator.textContent = '{{ passwords_match }}'; matchIndicator.className = 'label-text-alt text-success font-semibold'; } else { matchIndicator.textContent = '{{ passwords_not_match }}'; matchIndicator.className = 'label-text-alt text-error font-semibold'; } } else { matchIndicator.textContent = ''; } checkFormValidity(); }); // Enable submit button only if form is valid function checkFormValidity() { const oldPassword = document.getElementById('oldPassword').value; const newPassword = newPasswordInput.value; const confirmPassword = confirmPasswordInput.value; const { strength, feedback } = checkPasswordStrength(newPassword); const isValid = oldPassword.length > 0 && newPassword.length >= PW_MIN_LENGTH && confirmPassword === newPassword && feedback.length === 0; submitBtn.disabled = !isValid; } // Form submission document.getElementById('passwordForm').addEventListener('submit', async function(e) { e.preventDefault(); const formData = new FormData(this); submitBtn.disabled = true; submitBtn.innerHTML = '<span class="loading loading-spinner"></span> {{ changing }}'; try { // Convert FormData to JSON object const oldPassword = formData.get('old_password'); const newPassword = formData.get('new_password'); const confirmPassword = formData.get('confirm_password'); // Validate that all fields are present if (!oldPassword || !newPassword || !confirmPassword) { alert('❌ {{ error_all_fields_required }}'); submitBtn.disabled = false; submitBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg> {{ change_button }}'; return; } const jsonData = { old_password: oldPassword.toString(), new_password: newPassword.toString(), confirm_password: confirmPassword.toString() }; const jsonBody = JSON.stringify(jsonData); // Validate JSON is not empty if (!jsonBody || jsonBody === '{}' || jsonBody.length === 0) { alert('❌ {{ error_empty_data }}'); submitBtn.disabled = false; submitBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg> {{ change_button }}'; return; } console.log('Sending password change request:', { method: 'PUT', url: '/settings/password', headers: { 'Content-Type': 'application/json' }, body: jsonBody, bodyLength: jsonBody.length }); const response = await fetch('/settings/password', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRF-Token': '{{ csrf_token }}', }, credentials: 'include', body: jsonBody }); console.log('Response status:', response.status); console.log('Response headers:', Object.fromEntries(response.headers.entries())); // Clone response to read it multiple times if needed const responseClone = response.clone(); let result = {}; let errorText = ''; // Try to parse as JSON first try { const contentType = response.headers.get('content-type') || ''; if (contentType.includes('application/json')) { result = await response.json(); } else { // Not JSON, read as text errorText = await responseClone.text(); console.error('Non-JSON response (status ' + response.status + '):', errorText); } } catch (e) { // Failed to parse JSON, try text try { errorText = await responseClone.text(); console.error('Failed to parse response (status ' + response.status + '):', errorText); } catch (e2) { console.error('Failed to read response:', e2); errorText = 'Erreur lors de la lecture de la réponse du serveur'; } } const savedEmailMsg = "{{ saved }}"; const savedDirectMsg = "{{ saved_direct }}"; const errorAllFields = "{{ error_all_fields_required }}"; const errorEmpty = "{{ error_empty_data }}"; const errorInvalid = "{{ error_invalid_data }}"; const errorIncorrect = "{{ error_incorrect_current }}"; const error415 = "{{ error_415 }}"; const networkErr = "{{ network_error }}"; const changeBtnText = "{{ change_button }}"; if (response.ok) { const msgKey = result && result.message; if (msgKey === 'email_confirmation_sent') { alert('✅ ' + savedEmailMsg); } else { alert('✅ ' + savedDirectMsg); } window.location.href = '/settings'; } else { let errorMessage = 'Échec du changement de mot de passe'; if (response.status === 415) { errorMessage = error415; console.error('415 Error details:', errorText || 'No error text available'); } else if (result && result.error) { errorMessage = result.error; } else if (result && result.message) { errorMessage = result.message; } else if (errorText) { errorMessage = errorText; } else if (response.status === 400) { errorMessage = errorInvalid; } else if (response.status === 401) { errorMessage = errorIncorrect; } alert('❌ ' + errorMessage); submitBtn.disabled = false; submitBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg> ' + changeBtnText; } } catch (error) { console.error('Error:', error); alert('❌ ' + networkErr); submitBtn.disabled = false; submitBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg> ' + changeBtnText; } }); // Initialize form check document.getElementById('oldPassword').addEventListener('input', checkFormValidity); // Feature 026: Cancel pending password change request async function cancelPendingRequest() { const cancelBtn = document.getElementById('cancelPendingRequestBtn'); if (!cancelBtn) return; if (!confirm('{{ pending_request_cancel }} ?')) { return; } cancelBtn.disabled = true; cancelBtn.innerHTML = '<span class="loading loading-spinner"></span>'; try { const response = await fetch('/settings/password/pending', { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': '{{ csrf_token }}', }, credentials: 'same-origin' }); if (response.ok) { // Reload page to update UI window.location.reload(); } else { const result = await response.json().catch(() => ({})); const errorMessage = result.message || result.error || '{{ network_error }}'; alert('❌ ' + errorMessage); cancelBtn.disabled = false; cancelBtn.innerHTML = '{{ pending_request_cancel }}'; } } catch (error) { console.error('Error canceling pending request:', error); alert('❌ {{ network_error }}'); cancelBtn.disabled = false; cancelBtn.innerHTML = '{{ pending_request_cancel }}'; } } </script> {% endblock %} {% block footer %} {% if !must_change %} <footer class="bg-base-200 border-t border-base-300 py-4 mt-8"> <div class="container mx-auto px-4 text-center text-base-content text-sm"> <p>{{ app_name }} {{ app_version }}{% if app_domain != "" %} - {{ app_domain }}{% endif %}</p> {% if app_description != "" %} <p class="text-base-content/70 mt-1">{{ app_description }}</p> {% endif %} </div> </footer> {% endif %} {% endblock %}
GitRust