feat: add password creation step to onboarding wizard (#2)

- Add GET /api/security/password-is-default endpoint in server.py
- Add Step 2 (Create Your Password) to onboarding wizard HTML
- Renumber old steps: Domains→3, Ports→4, Complete→5
- Add 5th step dot indicator
- Update onboarding.js: TOTAL_STEPS=5, ROLE_SKIP_STEPS=[3,4] for desktop/node
- Add loadStep2/saveStep2 for password step with smart default detection
- Rename old step functions to loadStep3/saveStep3/loadStep4
- Add password form CSS styles in onboarding.css

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/74a30916-fb2d-4f1d-9763-e380b1aa5540

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-04-07 17:36:59 +00:00
committed by GitHub
parent f2a808ed13
commit d28f224ad5
4 changed files with 319 additions and 39 deletions

View File

@@ -2953,6 +2953,17 @@ async def api_security_status():
return {"status": status, "warning": warning} return {"status": status, "warning": warning}
@app.get("/api/security/password-is-default")
async def api_password_is_default():
"""Check if the free account password is still the factory default."""
try:
with open("/var/lib/secrets/free-password", "r") as f:
current = f.read().strip()
return {"is_default": current == "free"}
except FileNotFoundError:
return {"is_default": True}
# ── System password change ──────────────────────────────────────── # ── System password change ────────────────────────────────────────
FREE_PASSWORD_FILE = "/var/lib/secrets/free-password" FREE_PASSWORD_FILE = "/var/lib/secrets/free-password"

View File

@@ -575,6 +575,110 @@
color: var(--text-secondary); color: var(--text-secondary);
} }
/* ── Password step (Step 2) ─────────────────────────────────────── */
.onboarding-password-group {
display: flex;
flex-direction: column;
gap: 6px;
padding: 10px 0;
}
.onboarding-password-input-wrap {
display: flex;
align-items: center;
gap: 6px;
}
.onboarding-password-input {
flex: 1;
padding: 9px 12px;
border: 1px solid var(--border-color);
border-radius: var(--radius-btn);
background-color: var(--card-color);
color: var(--text-primary);
font-size: 0.88rem;
font-family: 'Cantarell', 'Inter', 'Segoe UI', sans-serif;
transition: border-color 0.15s;
}
.onboarding-password-input:focus {
outline: none;
border-color: var(--accent-color);
}
.onboarding-password-toggle {
padding: 6px 10px;
background-color: var(--card-color);
border: 1px solid var(--border-color);
border-radius: var(--radius-btn);
color: var(--text-secondary);
cursor: pointer;
font-size: 1rem;
line-height: 1;
transition: background-color 0.15s, border-color 0.15s;
flex-shrink: 0;
}
.onboarding-password-toggle:hover {
background-color: rgba(137, 180, 250, 0.12);
border-color: var(--accent-color);
}
.onboarding-password-hint {
font-size: 0.78rem;
color: var(--text-dim);
line-height: 1.4;
}
.onboarding-password-warning {
padding: 10px 14px;
background-color: rgba(229, 165, 10, 0.1);
border: 1px solid rgba(229, 165, 10, 0.35);
border-radius: 8px;
font-size: 0.85rem;
color: var(--yellow);
line-height: 1.5;
margin-top: 6px;
}
.onboarding-password-success {
padding: 12px 16px;
background-color: rgba(166, 227, 161, 0.1);
border: 1px solid rgba(166, 227, 161, 0.35);
border-radius: 8px;
font-size: 0.92rem;
color: var(--green);
line-height: 1.5;
}
.onboarding-password-optional {
margin-top: 12px;
font-size: 0.88rem;
}
.onboarding-password-optional > summary {
cursor: pointer;
font-size: 0.85rem;
font-weight: 600;
color: var(--accent-color);
list-style: none;
user-select: none;
}
.onboarding-password-optional > summary::-webkit-details-marker {
display: none;
}
.onboarding-password-optional > summary::before {
content: '▶ ';
font-size: 0.65em;
}
.onboarding-password-optional[open] > summary::before {
content: '▼ ';
}
/* ── Reboot overlay ─────────────────────────────────────────────── */ /* ── Reboot overlay ─────────────────────────────────────────────── */
.reboot-overlay { .reboot-overlay {

View File

@@ -1,21 +1,25 @@
/* Sovran_SystemsOS Hub — First-Boot Onboarding Wizard /* Sovran_SystemsOS Hub — First-Boot Onboarding Wizard
Drives the 4-step post-install setup flow. */ Drives the 5-step post-install setup flow. */
"use strict"; "use strict";
// ── Constants ───────────────────────────────────────────────────── // ── Constants ─────────────────────────────────────────────────────
const TOTAL_STEPS = 4; const TOTAL_STEPS = 5;
// Steps to skip per role (steps 2 and 3 involve domain/port setup) // Steps to skip per role (steps 3 and 4 involve domain/port setup)
// Step 2 (password) is NEVER skipped — all roles need it.
const ROLE_SKIP_STEPS = { const ROLE_SKIP_STEPS = {
"desktop": [2, 3], "desktop": [3, 4],
"node": [2, 3], "node": [3, 4],
}; };
// ── Role state (loaded at init) ─────────────────────────────────── // ── Role state (loaded at init) ───────────────────────────────────
var _onboardingRole = "server_plus_desktop"; var _onboardingRole = "server_plus_desktop";
// Password default state (loaded at step 2)
var _passwordIsDefault = true;
// Domains that may need configuration, with service unit mapping for enabled check // Domains that may need configuration, with service unit mapping for enabled check
const DOMAIN_DEFS = [ const DOMAIN_DEFS = [
{ name: "matrix", label: "Matrix (Synapse)", unit: "matrix-synapse.service", needsDdns: true }, { name: "matrix", label: "Matrix (Synapse)", unit: "matrix-synapse.service", needsDdns: true },
@@ -91,6 +95,8 @@ function showStep(step) {
// Lazy-load step content // Lazy-load step content
if (step === 2) loadStep2(); if (step === 2) loadStep2();
if (step === 3) loadStep3(); if (step === 3) loadStep3();
if (step === 4) loadStep4();
// Step 5 (Complete) is static — no lazy-load needed
} }
// Return the next step number, skipping over role-excluded steps // Return the next step number, skipping over role-excluded steps
@@ -119,12 +125,135 @@ async function loadStep1() {
} catch (_) {} } catch (_) {}
} }
// ── Step 2: Domain Configuration ───────────────────────────────── // ── Step 2: Create Your Password ─────────────────────────────────
async function loadStep2() { async function loadStep2() {
var body = document.getElementById("step-2-body"); var body = document.getElementById("step-2-body");
if (!body) return; if (!body) return;
var nextBtn = document.getElementById("step-2-next");
try {
var result = await apiFetch("/api/security/password-is-default");
_passwordIsDefault = result.is_default !== false;
} catch (_) {
_passwordIsDefault = true;
}
if (_passwordIsDefault) {
// Factory-sealed scenario: password must be set before continuing
if (nextBtn) nextBtn.textContent = "Set Password & Continue \u2192";
body.innerHTML =
'<div class="onboarding-password-group">' +
'<label class="onboarding-domain-label" for="pw-new">New Password</label>' +
'<div class="onboarding-password-input-wrap">' +
'<input class="onboarding-password-input" type="password" id="pw-new" autocomplete="new-password" placeholder="At least 8 characters" />' +
'<button type="button" class="onboarding-password-toggle" data-target="pw-new" aria-label="Show password">👁</button>' +
'</div>' +
'<p class="onboarding-password-hint">Minimum 8 characters</p>' +
'</div>' +
'<div class="onboarding-password-group">' +
'<label class="onboarding-domain-label" for="pw-confirm">Confirm Password</label>' +
'<div class="onboarding-password-input-wrap">' +
'<input class="onboarding-password-input" type="password" id="pw-confirm" autocomplete="new-password" placeholder="Re-enter your password" />' +
'<button type="button" class="onboarding-password-toggle" data-target="pw-confirm" aria-label="Show password">👁</button>' +
'</div>' +
'</div>' +
'<div class="onboarding-password-warning">⚠️ Write this password down — it cannot be recovered.</div>';
// Wire show/hide toggles
body.querySelectorAll(".onboarding-password-toggle").forEach(function(btn) {
btn.addEventListener("click", function() {
var inp = document.getElementById(btn.dataset.target);
if (inp) inp.type = (inp.type === "password") ? "text" : "password";
});
});
} else {
// DIY install scenario: password already set by installer
if (nextBtn) nextBtn.textContent = "Continue \u2192";
body.innerHTML =
'<div class="onboarding-password-success">✅ Your password was already set during installation.</div>' +
'<details class="onboarding-password-optional">' +
'<summary>Change it anyway</summary>' +
'<div style="margin-top:14px;">' +
'<div class="onboarding-password-group">' +
'<label class="onboarding-domain-label" for="pw-new">New Password</label>' +
'<div class="onboarding-password-input-wrap">' +
'<input class="onboarding-password-input" type="password" id="pw-new" autocomplete="new-password" placeholder="At least 8 characters" />' +
'<button type="button" class="onboarding-password-toggle" data-target="pw-new" aria-label="Show password">👁</button>' +
'</div>' +
'<p class="onboarding-password-hint">Minimum 8 characters</p>' +
'</div>' +
'<div class="onboarding-password-group">' +
'<label class="onboarding-domain-label" for="pw-confirm">Confirm Password</label>' +
'<div class="onboarding-password-input-wrap">' +
'<input class="onboarding-password-input" type="password" id="pw-confirm" autocomplete="new-password" placeholder="Re-enter your password" />' +
'<button type="button" class="onboarding-password-toggle" data-target="pw-confirm" aria-label="Show password">👁</button>' +
'</div>' +
'</div>' +
'<div class="onboarding-password-warning">⚠️ Write this password down — it cannot be recovered.</div>' +
'</div>' +
'</details>';
// Wire show/hide toggles
body.querySelectorAll(".onboarding-password-toggle").forEach(function(btn) {
btn.addEventListener("click", function() {
var inp = document.getElementById(btn.dataset.target);
if (inp) inp.type = (inp.type === "password") ? "text" : "password";
});
});
}
}
async function saveStep2() {
var newPw = document.getElementById("pw-new");
var confirmPw = document.getElementById("pw-confirm");
// If no fields visible or both empty and password already set → skip
if (!newPw || !newPw.value.trim()) {
if (!_passwordIsDefault) return true; // already set, no change requested
setStatus("step-2-status", "⚠ Please enter a password.", "error");
return false;
}
var pw = newPw.value;
var cpw = confirmPw ? confirmPw.value : "";
if (pw.length < 8) {
setStatus("step-2-status", "⚠ Password must be at least 8 characters.", "error");
return false;
}
if (pw !== cpw) {
setStatus("step-2-status", "⚠ Passwords do not match.", "error");
return false;
}
setStatus("step-2-status", "Saving password…", "info");
try {
await apiFetch("/api/change-password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ new_password: pw, confirm_password: cpw }),
});
} catch (err) {
setStatus("step-2-status", "⚠ " + err.message, "error");
return false;
}
setStatus("step-2-status", "✓ Password saved", "ok");
_passwordIsDefault = false;
return true;
}
// ── Step 3: Domain Configuration ─────────────────────────────────
async function loadStep3() {
var body = document.getElementById("step-3-body");
if (!body) return;
try { try {
// Fetch services, domains, and network info in parallel // Fetch services, domains, and network info in parallel
var results = await Promise.all([ var results = await Promise.all([
@@ -194,8 +323,8 @@ async function loadStep2() {
body.innerHTML = html; body.innerHTML = html;
} }
async function saveStep2() { async function saveStep3() {
setStatus("step-2-status", "Saving domains…", "info"); setStatus("step-3-status", "Saving domains…", "info");
var errors = []; var errors = [];
// Save each domain input // Save each domain input
@@ -235,18 +364,18 @@ async function saveStep2() {
} }
if (errors.length > 0) { if (errors.length > 0) {
setStatus("step-2-status", "⚠ Some errors: " + errors.join("; "), "error"); setStatus("step-3-status", "⚠ Some errors: " + errors.join("; "), "error");
return false; return false;
} }
setStatus("step-2-status", "✓ Saved", "ok"); setStatus("step-3-status", "✓ Saved", "ok");
return true; return true;
} }
// ── Step 3: Port Forwarding ─────────────────────────────────────── // ── Step 4: Port Forwarding ───────────────────────────────────────
async function loadStep3() { async function loadStep4() {
var body = document.getElementById("step-3-body"); var body = document.getElementById("step-4-body");
if (!body) return; if (!body) return;
body.innerHTML = '<p class="onboarding-loading">Checking ports…</p>'; body.innerHTML = '<p class="onboarding-loading">Checking ports…</p>';
@@ -327,10 +456,10 @@ async function loadStep3() {
body.innerHTML = html; body.innerHTML = html;
} }
// ── Step 4: Complete ────────────────────────────────────────────── // ── Step 5: Complete ──────────────────────────────────────────────
async function completeOnboarding() { async function completeOnboarding() {
var btn = document.getElementById("step-4-finish"); var btn = document.getElementById("step-5-finish");
if (btn) { btn.disabled = true; btn.textContent = "Finishing…"; } if (btn) { btn.disabled = true; btn.textContent = "Finishing…"; }
try { try {
@@ -345,28 +474,40 @@ async function completeOnboarding() {
// ── Event wiring ────────────────────────────────────────────────── // ── Event wiring ──────────────────────────────────────────────────
function wireNavButtons() { function wireNavButtons() {
// Step 1 → next (may skip 2+3 for desktop/node) // Step 1 → next
var s1next = document.getElementById("step-1-next"); var s1next = document.getElementById("step-1-next");
if (s1next) s1next.addEventListener("click", function() { showStep(nextStep(1)); }); if (s1next) s1next.addEventListener("click", function() { showStep(nextStep(1)); });
// Step 2 → 3 (save first) // Step 2 → 3 (save password first)
var s2next = document.getElementById("step-2-next"); var s2next = document.getElementById("step-2-next");
if (s2next) s2next.addEventListener("click", async function() { if (s2next) s2next.addEventListener("click", async function() {
s2next.disabled = true; s2next.disabled = true;
var origText = s2next.textContent;
s2next.textContent = "Saving…"; s2next.textContent = "Saving…";
await saveStep2(); var ok = await saveStep2();
s2next.disabled = false; s2next.disabled = false;
s2next.textContent = "Save & Continue →"; s2next.textContent = origText;
showStep(nextStep(2)); if (ok) showStep(nextStep(2));
}); });
// Step 3 → 4 (Complete) // Step 3 → 4 (save domains first)
var s3next = document.getElementById("step-3-next"); var s3next = document.getElementById("step-3-next");
if (s3next) s3next.addEventListener("click", function() { showStep(nextStep(3)); }); if (s3next) s3next.addEventListener("click", async function() {
s3next.disabled = true;
s3next.textContent = "Saving…";
await saveStep3();
s3next.disabled = false;
s3next.textContent = "Save & Continue →";
showStep(nextStep(3));
});
// Step 4: finish // Step 4 → 5 (Complete)
var s4finish = document.getElementById("step-4-finish"); var s4next = document.getElementById("step-4-next");
if (s4finish) s4finish.addEventListener("click", completeOnboarding); if (s4next) s4next.addEventListener("click", function() { showStep(nextStep(4)); });
// Step 5: finish
var s5finish = document.getElementById("step-5-finish");
if (s5finish) s5finish.addEventListener("click", completeOnboarding);
// Back buttons // Back buttons
document.querySelectorAll(".onboarding-btn-back").forEach(function(btn) { document.querySelectorAll(".onboarding-btn-back").forEach(function(btn) {

View File

@@ -34,6 +34,8 @@
<span class="onboarding-step-dot" data-step="3">3</span> <span class="onboarding-step-dot" data-step="3">3</span>
<span class="onboarding-step-connector"></span> <span class="onboarding-step-connector"></span>
<span class="onboarding-step-dot" data-step="4">4</span> <span class="onboarding-step-dot" data-step="4">4</span>
<span class="onboarding-step-connector"></span>
<span class="onboarding-step-dot" data-step="5">5</span>
</div> </div>
<!-- Step panels --> <!-- Step panels -->
@@ -70,8 +72,29 @@
</div> </div>
</div> </div>
<!-- ── Step 2: Domain Configuration ── --> <!-- ── Step 2: Create Your Password ── -->
<div class="onboarding-panel" id="step-2" style="display:none"> <div class="onboarding-panel" id="step-2" style="display:none">
<div class="onboarding-step-header">
<span class="onboarding-step-icon">🔒</span>
<h2 class="onboarding-step-title">Create Your Password</h2>
<p class="onboarding-step-desc">
Choose a strong password for your <strong>'free'</strong> user account. This will be your login password for the desktop, SSH, and the Hub.
</p>
</div>
<div class="onboarding-card" id="step-2-body">
<p class="onboarding-loading">Checking password status…</p>
</div>
<div id="step-2-status" class="onboarding-save-status"></div>
<div class="onboarding-footer">
<button class="btn btn-close-modal onboarding-btn-back" data-prev="1">← Back</button>
<button class="btn btn-primary onboarding-btn-next" id="step-2-next">
Set Password &amp; Continue →
</button>
</div>
</div>
<!-- ── Step 3: Domain Configuration ── -->
<div class="onboarding-panel" id="step-3" style="display:none">
<div class="onboarding-step-header"> <div class="onboarding-step-header">
<span class="onboarding-step-icon">🌐</span> <span class="onboarding-step-icon">🌐</span>
<h2 class="onboarding-step-title">Domain Configuration</h2> <h2 class="onboarding-step-title">Domain Configuration</h2>
@@ -82,20 +105,20 @@
Finally, paste the DDNS curl command from your Njal.la dashboard for each service below. Finally, paste the DDNS curl command from your Njal.la dashboard for each service below.
</p> </p>
</div> </div>
<div class="onboarding-card onboarding-card--scroll" id="step-2-body"> <div class="onboarding-card onboarding-card--scroll" id="step-3-body">
<p class="onboarding-loading">Loading service information…</p> <p class="onboarding-loading">Loading service information…</p>
</div> </div>
<div id="step-2-status" class="onboarding-save-status"></div> <div id="step-3-status" class="onboarding-save-status"></div>
<div class="onboarding-footer"> <div class="onboarding-footer">
<button class="btn btn-close-modal onboarding-btn-back" data-prev="1">← Back</button> <button class="btn btn-close-modal onboarding-btn-back" data-prev="2">← Back</button>
<button class="btn btn-primary onboarding-btn-next" id="step-2-next"> <button class="btn btn-primary onboarding-btn-next" id="step-3-next">
Save &amp; Continue → Save &amp; Continue →
</button> </button>
</div> </div>
</div> </div>
<!-- ── Step 3: Port Forwarding ── --> <!-- ── Step 4: Port Forwarding ── -->
<div class="onboarding-panel" id="step-3" style="display:none"> <div class="onboarding-panel" id="step-4" style="display:none">
<div class="onboarding-step-header"> <div class="onboarding-step-header">
<span class="onboarding-step-icon">🔌</span> <span class="onboarding-step-icon">🔌</span>
<h2 class="onboarding-step-title">Port Forwarding Check</h2> <h2 class="onboarding-step-title">Port Forwarding Check</h2>
@@ -104,19 +127,19 @@
<strong>Ports 80 and 443 must be open for SSL certificates to work.</strong> <strong>Ports 80 and 443 must be open for SSL certificates to work.</strong>
</p> </p>
</div> </div>
<div class="onboarding-card onboarding-card--ports" id="step-3-body"> <div class="onboarding-card onboarding-card--ports" id="step-4-body">
<p class="onboarding-loading">Checking ports…</p> <p class="onboarding-loading">Checking ports…</p>
</div> </div>
<div class="onboarding-footer"> <div class="onboarding-footer">
<button class="btn btn-close-modal onboarding-btn-back" data-prev="2">← Back</button> <button class="btn btn-close-modal onboarding-btn-back" data-prev="3">← Back</button>
<button class="btn btn-primary onboarding-btn-next" id="step-3-next"> <button class="btn btn-primary onboarding-btn-next" id="step-4-next">
Continue → Continue →
</button> </button>
</div> </div>
</div> </div>
<!-- ── Step 4: Complete ── --> <!-- ── Step 5: Complete ── -->
<div class="onboarding-panel" id="step-4" style="display:none"> <div class="onboarding-panel" id="step-5" style="display:none">
<div class="onboarding-hero"> <div class="onboarding-hero">
<div class="onboarding-logo"></div> <div class="onboarding-logo"></div>
<h1 class="onboarding-title">Your Sovran_SystemsOS is Ready!</h1> <h1 class="onboarding-title">Your Sovran_SystemsOS is Ready!</h1>
@@ -128,13 +151,14 @@
monitor your services, manage credentials, and make changes at any time. monitor your services, manage credentials, and make changes at any time.
</p> </p>
<ul class="onboarding-checklist" id="onboarding-checklist"> <ul class="onboarding-checklist" id="onboarding-checklist">
<li>✅ Password configured</li>
<li>✅ Domain configuration saved</li> <li>✅ Domain configuration saved</li>
<li>✅ Port forwarding reviewed</li> <li>✅ Port forwarding reviewed</li>
</ul> </ul>
</div> </div>
<div class="onboarding-footer"> <div class="onboarding-footer">
<button class="btn btn-close-modal onboarding-btn-back" data-prev="3">← Back</button> <button class="btn btn-close-modal onboarding-btn-back" data-prev="4">← Back</button>
<button class="btn btn-primary" id="step-4-finish"> <button class="btn btn-primary" id="step-5-finish">
Go to Dashboard → Go to Dashboard →
</button> </button>
</div> </div>