v0.280.0: the setup gate (decision 46); R-710 'I changed it' + absent-record window; R-709 password fields off the page; password:N:special generator
gates / gates (push) Successful in 25s
gates / gates (push) Successful in 25s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -563,19 +563,26 @@
|
||||
</select>
|
||||
{{else if eq .Type "password"}}
|
||||
<div class="input-with-button">
|
||||
{{- if $.AlreadyDeployed}}
|
||||
{{- /* R-709 (v0.280.0): an installed app's password is never in this page; the eye fetches it. */}}
|
||||
<input type="password" id="field-{{.EnvVar}}" class="form-control" value="" placeholder="••••••••••••" disabled>
|
||||
{{- if not (and $.RestoredLogins (index $.RestoredLogins .EnvVar))}}
|
||||
<button type="button" class="btn btn-sm btn-outline pw-toggle-btn"
|
||||
onclick="revealPasswordField('{{$.Stack.Name}}', '{{.EnvVar}}', this)"
|
||||
title="{{T "deploy.megjelenites"}}">👁</button>
|
||||
{{- end}}
|
||||
{{- else}}
|
||||
<input type="password" id="field-{{.EnvVar}}" name="{{.EnvVar}}"
|
||||
class="form-control" value="{{if and $.AlreadyDeployed $.DeployedFieldValues}}{{index $.DeployedFieldValues .EnvVar}}{{else}}{{.Default}}{{end}}"
|
||||
class="form-control" value="{{.Default}}"
|
||||
placeholder="{{.Placeholder}}"
|
||||
data-field-type="password"
|
||||
required
|
||||
{{if $.AlreadyDeployed}}disabled{{end}}>
|
||||
required>
|
||||
<button type="button" class="btn btn-sm btn-outline pw-toggle-btn"
|
||||
onclick="togglePasswordField('field-{{.EnvVar}}', 'field-confirm-{{.EnvVar}}', this)"
|
||||
title="{{T "deploy.megjelenites"}}">👁</button>
|
||||
{{if not $.AlreadyDeployed}}
|
||||
<button type="button" class="btn btn-sm btn-outline"
|
||||
onclick="generatePassword('field-{{.EnvVar}}', 'field-confirm-{{.EnvVar}}')">{{T "deploy.generalas"}}</button>
|
||||
{{end}}
|
||||
onclick="generatePassword('field-{{.EnvVar}}', 'field-confirm-{{.EnvVar}}', '{{.Generate}}')">{{T "deploy.generalas"}}</button>
|
||||
{{- end}}
|
||||
</div>
|
||||
{{if $.AlreadyDeployed}}
|
||||
<span class="form-hint">{{if and $.RestoredLogins (index $.RestoredLogins .EnvVar)}}{{T "deploy.login_from_backup"}}{{else}}{{T "deploy.telepiteskor_beallitott_kezdeti_jelszo_h"}}{{end}}</span>
|
||||
@@ -808,6 +815,30 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// R-254 site two: on an already-deployed app the value is NOT in this page — it is fetched on
|
||||
// demand, and the server records the act. Nothing caches it between presses.
|
||||
// R-709 (v0.280.0): an installed app's `type: password` value, fetched on demand like a secret.
|
||||
function revealPasswordField(stackName, envVar, btn) {
|
||||
var el = document.getElementById('field-' + envVar);
|
||||
if (!el) return;
|
||||
if (btn.dataset.shown === '1') {
|
||||
el.value = '';
|
||||
el.type = 'password';
|
||||
btn.dataset.shown = '';
|
||||
return;
|
||||
}
|
||||
btn.disabled = true;
|
||||
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
|
||||
credentials: 'same-origin',
|
||||
body: 'env_var=' + encodeURIComponent(envVar)
|
||||
}).then(function (r) { return r.json(); }).then(function (j) {
|
||||
btn.disabled = false;
|
||||
if (!j.ok) { showAlert(j.error || '{{T "deploy.a_lekeres_nem_sikerult"}}'); return; }
|
||||
el.value = j.data.value;
|
||||
el.type = 'text';
|
||||
btn.dataset.shown = '1';
|
||||
}).catch(function () { btn.disabled = false; showAlert('{{T "deploy.a_lekeres_nem_sikerult"}}'); });
|
||||
}
|
||||
function revealAutoField(stackName, envVar, btn) {
|
||||
var el = document.getElementById('auto-field-' + envVar);
|
||||
if (!el) return;
|
||||
@@ -839,13 +870,28 @@ function toggleAutoField(fieldId, btn) {
|
||||
el.type = el.type === 'password' ? 'text' : 'password';
|
||||
btn.textContent = el.type === 'password' ? '{{T "deploy.megjelenites"}}' : '{{T "deploy.elrejtes"}}';
|
||||
}
|
||||
function generatePassword(fieldId, confirmFieldId) {
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
// spec is the field's `generate:` (e.g. "password:24:special"); empty = 16 letters and digits. With
|
||||
// ":special" (v0.280.0) the password carries a lower, an upper, a digit and one of SPECIAL — for an app whose
|
||||
// own policy demands it (calibre-web). SPECIAL matches internal/stacks generateValue.
|
||||
function generatePassword(fieldId, confirmFieldId, spec) {
|
||||
const letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
const SPECIAL = '-_.!@#%+=';
|
||||
var parts = (spec || '').split(':');
|
||||
var n = parseInt(parts[1], 10);
|
||||
if (parts[0] !== 'password' || !(n >= 12 && n <= 64)) { n = 16; }
|
||||
var special = parts[2] === 'special';
|
||||
var chars = special ? letters + SPECIAL : letters;
|
||||
let pass = '';
|
||||
const arr = new Uint8Array(16);
|
||||
crypto.getRandomValues(arr);
|
||||
for (let i = 0; i < 16; i++) {
|
||||
pass += chars[arr[i] % chars.length];
|
||||
for (;;) {
|
||||
pass = '';
|
||||
const arr = new Uint32Array(n);
|
||||
crypto.getRandomValues(arr);
|
||||
for (let i = 0; i < n; i++) {
|
||||
pass += chars[arr[i] % chars.length];
|
||||
}
|
||||
if (!special || (/[a-z]/.test(pass) && /[A-Z]/.test(pass) && /[0-9]/.test(pass) && /[-_.!@#%+=]/.test(pass) && /^[A-Za-z0-9]/.test(pass))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
document.getElementById(fieldId).value = pass;
|
||||
if (confirmFieldId) {
|
||||
|
||||
Reference in New Issue
Block a user