Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Large red/green indicator below confirm field shows match status. Submit button is visually gray when disabled, turns green with shadow when all conditions met. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
535 lines
17 KiB
HTML
Executable File
535 lines
17 KiB
HTML
Executable File
{% extends "base.html" %}
|
||
|
||
{% block title %}Nowe haslo - Norda Biznes Partner{% endblock %}
|
||
|
||
{% block container_class %}container-narrow{% endblock %}
|
||
|
||
{% block extra_css %}
|
||
<style>
|
||
.auth-container {
|
||
max-width: 480px;
|
||
margin: 0 auto;
|
||
padding: var(--spacing-2xl) 0;
|
||
}
|
||
|
||
.auth-card {
|
||
background-color: var(--surface);
|
||
padding: var(--spacing-2xl);
|
||
border-radius: var(--radius-xl);
|
||
box-shadow: var(--shadow-lg);
|
||
}
|
||
|
||
.auth-header {
|
||
text-align: center;
|
||
margin-bottom: var(--spacing-xl);
|
||
}
|
||
|
||
.auth-header h1 {
|
||
font-size: var(--font-size-3xl);
|
||
color: var(--text-primary);
|
||
margin-bottom: var(--spacing-sm);
|
||
}
|
||
|
||
.auth-header p {
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.auth-icon {
|
||
width: 64px;
|
||
height: 64px;
|
||
background: linear-gradient(135deg, var(--success), #059669);
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin: 0 auto var(--spacing-lg);
|
||
}
|
||
|
||
.auth-icon svg {
|
||
width: 32px;
|
||
height: 32px;
|
||
color: white;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: var(--spacing-lg);
|
||
}
|
||
|
||
.form-label {
|
||
display: block;
|
||
font-weight: 500;
|
||
margin-bottom: var(--spacing-sm);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.form-label .required {
|
||
color: var(--error);
|
||
}
|
||
|
||
.form-input {
|
||
width: 100%;
|
||
padding: var(--spacing-md);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
font-size: var(--font-size-base);
|
||
font-family: var(--font-family);
|
||
transition: var(--transition);
|
||
}
|
||
|
||
.form-input:focus {
|
||
outline: none;
|
||
border-color: var(--primary);
|
||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||
}
|
||
|
||
.form-input.error {
|
||
border-color: var(--error);
|
||
}
|
||
|
||
.password-strength {
|
||
margin-top: var(--spacing-sm);
|
||
height: 4px;
|
||
background-color: var(--border);
|
||
border-radius: var(--radius-sm);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.password-strength-bar {
|
||
height: 100%;
|
||
width: 0;
|
||
transition: var(--transition);
|
||
}
|
||
|
||
.password-strength-bar.weak {
|
||
background-color: var(--error);
|
||
width: 33%;
|
||
}
|
||
|
||
.password-strength-bar.medium {
|
||
background-color: var(--warning);
|
||
width: 66%;
|
||
}
|
||
|
||
.password-strength-bar.strong {
|
||
background-color: var(--success);
|
||
width: 100%;
|
||
}
|
||
|
||
.password-requirements {
|
||
font-size: var(--font-size-sm);
|
||
color: var(--text-secondary);
|
||
margin-top: var(--spacing-sm);
|
||
}
|
||
|
||
.password-requirements ul {
|
||
list-style: none;
|
||
padding: 0;
|
||
margin: var(--spacing-sm) 0 0 0;
|
||
}
|
||
|
||
.password-requirements li {
|
||
padding: var(--spacing-xs) 0;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--spacing-sm);
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.password-requirements li .checkbox-icon {
|
||
width: 20px;
|
||
height: 20px;
|
||
border: 2px solid var(--border);
|
||
border-radius: 4px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: var(--surface);
|
||
transition: all 0.2s ease;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.password-requirements li.valid .checkbox-icon {
|
||
background: var(--success);
|
||
border-color: var(--success);
|
||
}
|
||
|
||
.password-requirements li.valid .checkbox-icon::after {
|
||
content: "\2713";
|
||
color: white;
|
||
font-weight: bold;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.password-requirements li.valid {
|
||
color: var(--success);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.form-actions {
|
||
margin-top: var(--spacing-xl);
|
||
}
|
||
|
||
.btn-full {
|
||
width: 100%;
|
||
}
|
||
|
||
/* Match indicator under confirm field */
|
||
.match-indicator {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-top: 10px;
|
||
padding: 10px 14px;
|
||
border-radius: var(--radius);
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.match-indicator.mismatch {
|
||
background: #fef2f2;
|
||
border: 2px solid #ef4444;
|
||
color: #dc2626;
|
||
}
|
||
|
||
.match-indicator.match {
|
||
background: #f0fdf4;
|
||
border: 2px solid #22c55e;
|
||
color: #16a34a;
|
||
}
|
||
|
||
.match-icon {
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: white;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.match-indicator.mismatch .match-icon {
|
||
background: #ef4444;
|
||
}
|
||
|
||
.match-indicator.match .match-icon {
|
||
background: #22c55e;
|
||
}
|
||
|
||
/* Submit button states */
|
||
#submitBtn {
|
||
background-color: #d1d5db;
|
||
color: #9ca3af;
|
||
cursor: not-allowed;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
#submitBtn:not(:disabled) {
|
||
background-color: var(--success);
|
||
color: white;
|
||
cursor: pointer;
|
||
box-shadow: 0 4px 12px rgba(22, 163, 74, 0.4);
|
||
}
|
||
|
||
#submitBtn:not(:disabled):hover {
|
||
background-color: #059669;
|
||
box-shadow: 0 6px 16px rgba(22, 163, 74, 0.5);
|
||
}
|
||
|
||
.auth-footer {
|
||
text-align: center;
|
||
margin-top: var(--spacing-lg);
|
||
padding-top: var(--spacing-lg);
|
||
border-top: 1px solid var(--border);
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.auth-footer a {
|
||
color: var(--primary);
|
||
text-decoration: none;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.auth-footer a:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
/* Password toggle */
|
||
.password-wrapper {
|
||
position: relative;
|
||
}
|
||
|
||
.password-wrapper .form-input {
|
||
padding-right: 48px;
|
||
}
|
||
|
||
.password-toggle {
|
||
position: absolute;
|
||
right: 12px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
background: none;
|
||
border: none;
|
||
cursor: pointer;
|
||
padding: 4px;
|
||
color: var(--text-secondary);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.password-toggle:hover {
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.password-toggle svg {
|
||
width: 20px;
|
||
height: 20px;
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="auth-container">
|
||
<div class="auth-card">
|
||
<div class="auth-header">
|
||
<div class="auth-icon">
|
||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<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>
|
||
</div>
|
||
<h1>Ustaw nowe haslo</h1>
|
||
<p>Wprowadz nowe haslo do swojego konta</p>
|
||
</div>
|
||
|
||
<form method="POST" action="{{ url_for('reset_password', token=token) }}" novalidate>
|
||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||
|
||
<div class="form-group">
|
||
<label for="password" class="form-label">
|
||
Nowe haslo <span class="required">*</span>
|
||
</label>
|
||
<div class="password-wrapper">
|
||
<input
|
||
type="password"
|
||
id="password"
|
||
name="password"
|
||
class="form-input"
|
||
placeholder="********"
|
||
required
|
||
autocomplete="new-password"
|
||
autofocus
|
||
>
|
||
<button type="button" class="password-toggle" onclick="togglePassword('password', this)" title="Pokaż/ukryj hasło">
|
||
<svg class="eye-open" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
||
<circle cx="12" cy="12" r="3"/>
|
||
</svg>
|
||
<svg class="eye-closed" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="display:none;">
|
||
<path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19m-6.72-1.07a3 3 0 11-4.24-4.24"/>
|
||
<line x1="1" y1="1" x2="23" y2="23"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<div class="password-strength">
|
||
<div class="password-strength-bar" id="strengthBar"></div>
|
||
</div>
|
||
<div class="password-requirements">
|
||
<ul id="requirements">
|
||
<li id="req-length">
|
||
<span class="checkbox-icon"></span>
|
||
<span class="requirement-text">Minimum 8 znakow</span>
|
||
</li>
|
||
<li id="req-upper">
|
||
<span class="checkbox-icon"></span>
|
||
<span class="requirement-text">Wielka litera</span>
|
||
</li>
|
||
<li id="req-lower">
|
||
<span class="checkbox-icon"></span>
|
||
<span class="requirement-text">Mala litera</span>
|
||
</li>
|
||
<li id="req-digit">
|
||
<span class="checkbox-icon"></span>
|
||
<span class="requirement-text">Cyfra</span>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="password_confirm" class="form-label">
|
||
Potwierdz haslo <span class="required">*</span>
|
||
</label>
|
||
<div class="password-wrapper">
|
||
<input
|
||
type="password"
|
||
id="password_confirm"
|
||
name="password_confirm"
|
||
class="form-input"
|
||
placeholder="********"
|
||
required
|
||
autocomplete="new-password"
|
||
>
|
||
<button type="button" class="password-toggle" onclick="togglePassword('password_confirm', this)" title="Pokaż/ukryj hasło">
|
||
<svg class="eye-open" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
||
<circle cx="12" cy="12" r="3"/>
|
||
</svg>
|
||
<svg class="eye-closed" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="display:none;">
|
||
<path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19m-6.72-1.07a3 3 0 11-4.24-4.24"/>
|
||
<line x1="1" y1="1" x2="23" y2="23"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<div id="matchIndicator" class="match-indicator" style="display:none;">
|
||
<span class="match-icon" id="matchIcon"></span>
|
||
<span class="match-text" id="matchText"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-actions">
|
||
<button type="submit" class="btn btn-lg btn-full" id="submitBtn" disabled>
|
||
Zmien haslo
|
||
</button>
|
||
</div>
|
||
</form>
|
||
|
||
<div class="auth-footer">
|
||
<p><a href="{{ url_for('login') }}">Powrot do logowania</a></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="toastContainer" style="position: fixed; top: 80px; right: 20px; z-index: 1100; display: flex; flex-direction: column; gap: 10px;"></div>
|
||
<style>
|
||
.toast { padding: 12px 20px; border-radius: var(--radius); background: var(--surface); border-left: 4px solid var(--primary); box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: flex; align-items: center; gap: 10px; animation: toastIn 0.3s ease; }
|
||
.toast.error { border-left-color: var(--error); }
|
||
@keyframes toastIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
|
||
@keyframes toastOut { from { opacity: 1; } to { opacity: 0; } }
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
// Password toggle function
|
||
function togglePassword(inputId, button) {
|
||
const input = document.getElementById(inputId);
|
||
const eyeOpen = button.querySelector('.eye-open');
|
||
const eyeClosed = button.querySelector('.eye-closed');
|
||
|
||
if (input.type === 'password') {
|
||
input.type = 'text';
|
||
eyeOpen.style.display = 'none';
|
||
eyeClosed.style.display = 'block';
|
||
} else {
|
||
input.type = 'password';
|
||
eyeOpen.style.display = 'block';
|
||
eyeClosed.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
function showToast(message, type = 'info', duration = 4000) {
|
||
const container = document.getElementById('toastContainer');
|
||
const icons = { error: '✕', info: 'ℹ' };
|
||
const toast = document.createElement('div');
|
||
toast.className = `toast ${type}`;
|
||
toast.innerHTML = `<span style="font-size:1.2em">${icons[type]||'ℹ'}</span><span>${message}</span>`;
|
||
container.appendChild(toast);
|
||
setTimeout(() => { toast.style.animation = 'toastOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, duration);
|
||
}
|
||
|
||
const passwordInput = document.getElementById('password');
|
||
const passwordConfirm = document.getElementById('password_confirm');
|
||
const strengthBar = document.getElementById('strengthBar');
|
||
const submitBtn = document.getElementById('submitBtn');
|
||
|
||
passwordInput.addEventListener('input', function() {
|
||
const password = this.value;
|
||
let strength = 0;
|
||
let validCount = 0;
|
||
|
||
const hasLength = password.length >= 8;
|
||
const hasUpper = /[A-Z]/.test(password);
|
||
const hasLower = /[a-z]/.test(password);
|
||
const hasDigit = /\d/.test(password);
|
||
|
||
updateRequirement('req-length', hasLength);
|
||
updateRequirement('req-upper', hasUpper);
|
||
updateRequirement('req-lower', hasLower);
|
||
updateRequirement('req-digit', hasDigit);
|
||
|
||
if (hasLength) { strength++; validCount++; }
|
||
if (hasUpper) { strength++; validCount++; }
|
||
if (hasLower) { strength++; validCount++; }
|
||
if (hasDigit) { strength++; validCount++; }
|
||
|
||
strengthBar.className = 'password-strength-bar';
|
||
if (strength === 1 || strength === 2) {
|
||
strengthBar.classList.add('weak');
|
||
} else if (strength === 3) {
|
||
strengthBar.classList.add('medium');
|
||
} else if (strength === 4) {
|
||
strengthBar.classList.add('strong');
|
||
}
|
||
|
||
checkFormValidity();
|
||
});
|
||
|
||
passwordConfirm.addEventListener('input', checkFormValidity);
|
||
|
||
function updateRequirement(id, valid) {
|
||
const el = document.getElementById(id);
|
||
if (valid) {
|
||
el.classList.add('valid');
|
||
} else {
|
||
el.classList.remove('valid');
|
||
}
|
||
}
|
||
|
||
const matchIndicator = document.getElementById('matchIndicator');
|
||
const matchIcon = document.getElementById('matchIcon');
|
||
const matchText = document.getElementById('matchText');
|
||
|
||
function checkFormValidity() {
|
||
const password = passwordInput.value;
|
||
const confirm = passwordConfirm.value;
|
||
|
||
const hasLength = password.length >= 8;
|
||
const hasUpper = /[A-Z]/.test(password);
|
||
const hasLower = /[a-z]/.test(password);
|
||
const hasDigit = /\d/.test(password);
|
||
const passwordsMatch = password === confirm && confirm.length > 0;
|
||
|
||
// Show match indicator only when user started typing confirmation
|
||
if (confirm.length > 0) {
|
||
matchIndicator.style.display = 'flex';
|
||
if (passwordsMatch) {
|
||
matchIndicator.className = 'match-indicator match';
|
||
matchIcon.textContent = '\u2713';
|
||
matchText.textContent = 'Hasła są identyczne';
|
||
} else {
|
||
matchIndicator.className = 'match-indicator mismatch';
|
||
matchIcon.textContent = '\u2717';
|
||
matchText.textContent = 'Hasła nie są identyczne';
|
||
}
|
||
} else {
|
||
matchIndicator.style.display = 'none';
|
||
}
|
||
|
||
submitBtn.disabled = !(hasLength && hasUpper && hasLower && hasDigit && passwordsMatch);
|
||
}
|
||
|
||
document.querySelector('form').addEventListener('submit', function(e) {
|
||
const password = passwordInput.value;
|
||
const confirm = passwordConfirm.value;
|
||
|
||
if (password !== confirm) {
|
||
passwordConfirm.classList.add('error');
|
||
e.preventDefault();
|
||
showToast('Hasła nie są identyczne', 'error');
|
||
}
|
||
});
|
||
{% endblock %}
|