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:
@@ -85,6 +85,20 @@
|
||||
onerror="this.style.display='none'">
|
||||
</div>
|
||||
|
||||
{{- if .SetupGateClosed}}
|
||||
<div class="app-info-card" style="margin-top:1rem" id="setup-gate-card">
|
||||
<h3>{{T "app_info.setup_gate_title"}}</h3>
|
||||
<p>{{T "app_info.setup_gate_closed"}}</p>
|
||||
{{- if .SetupGateHasProbe}}
|
||||
<p class="form-hint">{{T "app_info.setup_gate_probe"}}</p>
|
||||
{{- else}}
|
||||
<p class="form-hint">{{T "app_info.setup_gate_button_hint"}}</p>
|
||||
<button type="button" class="btn btn-sm btn-primary" onclick="appPress(this, '/apps/{{.Meta.Slug}}/setup-gate/open')">{{T "app_info.setup_gate_done_btn"}}</button>
|
||||
<span class="form-hint app-press-err" style="display:none;color:var(--red)"></span>
|
||||
{{- end}}
|
||||
</div>
|
||||
{{- end}}
|
||||
|
||||
{{if .DataPathCards}}
|
||||
<div class="app-info-card" style="margin-top:1rem">
|
||||
<h3>{{T "app_info.hova_tegyem_a_fajlokat"}}</h3>
|
||||
@@ -222,6 +236,10 @@ function appMigrate(btn,app,label){
|
||||
<h3>{{T "app_info.alapertelmezett_belepes"}}</h3>
|
||||
<p class="app-info-creds">{{.AppInfo.DefaultCreds}}</p>
|
||||
{{if .KnownLoginLine}}<p class="app-info-creds-warn" id="known-login-line">{{.KnownLoginLine}}</p>{{else}}<p class="app-info-creds-warn">{{T "app_info.az_elso_bejelentkezes_utan_azonnal_2"}}</p>{{end}}
|
||||
{{- if and .Stack.Deployed .KnownLoginLine}}
|
||||
<button type="button" class="btn btn-sm btn-outline" onclick="appPress(this, '/apps/{{.Meta.Slug}}/default-login/changed')">{{T "app_info.default_login_changed_btn"}}</button>
|
||||
<span class="form-hint app-press-err" style="display:none;color:var(--red)"></span>
|
||||
{{- end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -288,4 +306,21 @@ function icCopyPw(btn) {
|
||||
{{end}}
|
||||
|
||||
{{template "layout_end" .}}
|
||||
{{- if or .SetupGateClosed (and .Stack.Deployed .KnownLoginLine)}}
|
||||
<script>
|
||||
// v0.280.0: the household's presses on this page — "Done, I set it up" (decision 46), "I changed it" (R-710).
|
||||
function appPress(btn, url) {
|
||||
var err = btn.parentNode.querySelector('.app-press-err');
|
||||
if (err) { err.style.display = 'none'; }
|
||||
btn.disabled = true;
|
||||
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (j) {
|
||||
if (!j.ok) { throw new Error(j.error || ''); }
|
||||
window.location.reload();
|
||||
})
|
||||
.catch(function (e) { btn.disabled = false; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
|
||||
}
|
||||
</script>
|
||||
{{- end}}
|
||||
{{end}}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{{define "setupgate"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{T "layout.html_lang"}}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex">
|
||||
<title>{{if .AppName}}{{.AppName}} — {{end}}{{T "setup_gate.page_title"}}</title>
|
||||
<link rel="stylesheet" href="/static/style.css?v={{.Version}}">
|
||||
</head>
|
||||
<body class="login-body">
|
||||
<div class="login-card">
|
||||
<img src="/static/felhom-logo.svg?v={{.Version}}" alt="Felhom.eu" class="login-logo">
|
||||
{{- if .AppName}}
|
||||
<h1 class="login-title">{{.AppName}}</h1>
|
||||
{{- end}}
|
||||
<p id="setup-gate-text">{{T "setup_gate.page_body"}}</p>
|
||||
<a class="btn btn-primary btn-full" href="{{.LoginURL}}">{{T "setup_gate.sign_in"}}</a>
|
||||
<p class="login-footer">{{.Host}}</p>
|
||||
<div class="shell-lang">{{template "lang_globe" .}}</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user