feat: password fields with masked input, reveal toggle, confirmation

- Password deploy fields now use type=password (masked by default)
- Added eye toggle button to reveal/hide password and confirm fields
- Added confirmation field below each password input
- Generate button fills both password and confirmation fields
- Form validation checks password confirmation matches before deploy
- Confirmation field only shown for new deployments (not already deployed)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 16:33:33 +01:00
parent e3a54f2ff8
commit eb2207fb62
+38 -3
View File
@@ -308,15 +308,25 @@
</select> </select>
{{else if eq .Type "password"}} {{else if eq .Type "password"}}
<div class="input-with-button"> <div class="input-with-button">
<input type="text" id="field-{{.EnvVar}}" name="{{.EnvVar}}" <input type="password" id="field-{{.EnvVar}}" name="{{.EnvVar}}"
class="form-control" value="{{.Default}}" class="form-control" value="{{.Default}}"
placeholder="{{.Placeholder}}" placeholder="{{.Placeholder}}"
data-field-type="password" data-field-type="password"
required required
{{if $.AlreadyDeployed}}disabled{{end}}> {{if $.AlreadyDeployed}}disabled{{end}}>
<button type="button" class="btn btn-sm btn-outline pw-toggle-btn"
onclick="togglePasswordField('field-{{.EnvVar}}', 'field-confirm-{{.EnvVar}}', this)"
title="Megjelenítés">&#128065;</button>
<button type="button" class="btn btn-sm btn-outline" <button type="button" class="btn btn-sm btn-outline"
onclick="generatePassword('field-{{.EnvVar}}')">Generálás</button> onclick="generatePassword('field-{{.EnvVar}}', 'field-confirm-{{.EnvVar}}')">Generálás</button>
</div> </div>
{{if not $.AlreadyDeployed}}
<div class="input-with-button" style="margin-top:.25rem">
<input type="password" id="field-confirm-{{.EnvVar}}"
class="form-control" placeholder="Jelszó megerősítése"
data-confirm-for="field-{{.EnvVar}}">
</div>
{{end}}
{{else if eq .Type "boolean"}} {{else if eq .Type "boolean"}}
<label class="toggle"> <label class="toggle">
<input type="checkbox" id="field-{{.EnvVar}}" name="{{.EnvVar}}" value="true" <input type="checkbox" id="field-{{.EnvVar}}" name="{{.EnvVar}}" value="true"
@@ -549,7 +559,7 @@ function toggleAutoField(fieldId, btn) {
el.type = el.type === 'password' ? 'text' : 'password'; el.type = el.type === 'password' ? 'text' : 'password';
btn.textContent = el.type === 'password' ? 'Megjelenítés' : 'Elrejtés'; btn.textContent = el.type === 'password' ? 'Megjelenítés' : 'Elrejtés';
} }
function generatePassword(fieldId) { function generatePassword(fieldId, confirmFieldId) {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let pass = ''; let pass = '';
const arr = new Uint8Array(16); const arr = new Uint8Array(16);
@@ -558,6 +568,19 @@ function generatePassword(fieldId) {
pass += chars[arr[i] % chars.length]; pass += chars[arr[i] % chars.length];
} }
document.getElementById(fieldId).value = pass; document.getElementById(fieldId).value = pass;
if (confirmFieldId) {
var ce = document.getElementById(confirmFieldId);
if (ce) ce.value = pass;
}
}
function togglePasswordField(fieldId, confirmFieldId, btn) {
var el = document.getElementById(fieldId);
if (!el) return;
var newType = el.type === 'password' ? 'text' : 'password';
el.type = newType;
var ce = document.getElementById(confirmFieldId);
if (ce) ce.type = newType;
btn.title = newType === 'password' ? 'Megjelenítés' : 'Elrejtés';
} }
function deleteStaleData(stackName, stalePath, btn) { function deleteStaleData(stackName, stalePath, btn) {
@@ -620,6 +643,18 @@ document.getElementById('deploy-form').addEventListener('submit', async function
} }
} }
// Client-side validation: check password confirmation matches
const confirmInputs = e.target.querySelectorAll('input[data-confirm-for]');
for (const ci of confirmInputs) {
const mainEl = document.getElementById(ci.getAttribute('data-confirm-for'));
if (mainEl && !mainEl.disabled && ci.value !== mainEl.value) {
const label = mainEl.closest('.form-group').querySelector('label').textContent.trim();
showAlert('A két jelszó nem egyezik: ' + label);
ci.focus();
return;
}
}
// Client-side validation: check subdomain format // Client-side validation: check subdomain format
const subdomainField = e.target.querySelector('.subdomain-input'); const subdomainField = e.target.querySelector('.subdomain-input');
if (subdomainField && !subdomainField.disabled) { if (subdomainField && !subdomainField.disabled) {