From d28f224ad538fe819dc7a4f71d3a37786830cb8f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:36:59 +0000 Subject: [PATCH] feat: add password creation step to onboarding wizard (#2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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> --- app/sovran_systemsos_web/server.py | 11 + .../static/css/onboarding.css | 104 ++++++++++ app/sovran_systemsos_web/static/onboarding.js | 191 +++++++++++++++--- .../templates/onboarding.html | 52 +++-- 4 files changed, 319 insertions(+), 39 deletions(-) diff --git a/app/sovran_systemsos_web/server.py b/app/sovran_systemsos_web/server.py index b6b620b..165aedc 100644 --- a/app/sovran_systemsos_web/server.py +++ b/app/sovran_systemsos_web/server.py @@ -2953,6 +2953,17 @@ async def api_security_status(): 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 ──────────────────────────────────────── FREE_PASSWORD_FILE = "/var/lib/secrets/free-password" diff --git a/app/sovran_systemsos_web/static/css/onboarding.css b/app/sovran_systemsos_web/static/css/onboarding.css index 42343bb..c250f39 100644 --- a/app/sovran_systemsos_web/static/css/onboarding.css +++ b/app/sovran_systemsos_web/static/css/onboarding.css @@ -575,6 +575,110 @@ 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 { diff --git a/app/sovran_systemsos_web/static/onboarding.js b/app/sovran_systemsos_web/static/onboarding.js index 55a7499..4821262 100644 --- a/app/sovran_systemsos_web/static/onboarding.js +++ b/app/sovran_systemsos_web/static/onboarding.js @@ -1,21 +1,25 @@ /* 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"; // ── 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 = { - "desktop": [2, 3], - "node": [2, 3], + "desktop": [3, 4], + "node": [3, 4], }; // ── Role state (loaded at init) ─────────────────────────────────── 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 const DOMAIN_DEFS = [ { name: "matrix", label: "Matrix (Synapse)", unit: "matrix-synapse.service", needsDdns: true }, @@ -91,6 +95,8 @@ function showStep(step) { // Lazy-load step content if (step === 2) loadStep2(); 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 @@ -119,12 +125,135 @@ async function loadStep1() { } catch (_) {} } -// ── Step 2: Domain Configuration ───────────────────────────────── +// ── Step 2: Create Your Password ───────────────────────────────── async function loadStep2() { var body = document.getElementById("step-2-body"); 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 = + '
' + + '' + + '
' + + '' + + '' + + '
' + + '

Minimum 8 characters

' + + '
' + + '
' + + '' + + '
' + + '' + + '' + + '
' + + '
' + + '
⚠️ Write this password down — it cannot be recovered.
'; + + // 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 = + '
✅ Your password was already set during installation.
' + + '
' + + 'Change it anyway' + + '
' + + '
' + + '' + + '
' + + '' + + '' + + '
' + + '

Minimum 8 characters

' + + '
' + + '
' + + '' + + '
' + + '' + + '' + + '
' + + '
' + + '
⚠️ Write this password down — it cannot be recovered.
' + + '
' + + '
'; + + // 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 { // Fetch services, domains, and network info in parallel var results = await Promise.all([ @@ -194,8 +323,8 @@ async function loadStep2() { body.innerHTML = html; } -async function saveStep2() { - setStatus("step-2-status", "Saving domains…", "info"); +async function saveStep3() { + setStatus("step-3-status", "Saving domains…", "info"); var errors = []; // Save each domain input @@ -235,18 +364,18 @@ async function saveStep2() { } if (errors.length > 0) { - setStatus("step-2-status", "⚠ Some errors: " + errors.join("; "), "error"); + setStatus("step-3-status", "⚠ Some errors: " + errors.join("; "), "error"); return false; } - setStatus("step-2-status", "✓ Saved", "ok"); + setStatus("step-3-status", "✓ Saved", "ok"); return true; } -// ── Step 3: Port Forwarding ─────────────────────────────────────── +// ── Step 4: Port Forwarding ─────────────────────────────────────── -async function loadStep3() { - var body = document.getElementById("step-3-body"); +async function loadStep4() { + var body = document.getElementById("step-4-body"); if (!body) return; body.innerHTML = '

Checking ports…

'; @@ -327,10 +456,10 @@ async function loadStep3() { body.innerHTML = html; } -// ── Step 4: Complete ────────────────────────────────────────────── +// ── Step 5: Complete ────────────────────────────────────────────── 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…"; } try { @@ -345,28 +474,40 @@ async function completeOnboarding() { // ── Event wiring ────────────────────────────────────────────────── function wireNavButtons() { - // Step 1 → next (may skip 2+3 for desktop/node) + // Step 1 → next var s1next = document.getElementById("step-1-next"); 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"); if (s2next) s2next.addEventListener("click", async function() { s2next.disabled = true; + var origText = s2next.textContent; s2next.textContent = "Saving…"; - await saveStep2(); + var ok = await saveStep2(); s2next.disabled = false; - s2next.textContent = "Save & Continue →"; - showStep(nextStep(2)); + s2next.textContent = origText; + if (ok) showStep(nextStep(2)); }); - // Step 3 → 4 (Complete) + // Step 3 → 4 (save domains first) 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 - var s4finish = document.getElementById("step-4-finish"); - if (s4finish) s4finish.addEventListener("click", completeOnboarding); + // Step 4 → 5 (Complete) + var s4next = document.getElementById("step-4-next"); + 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 document.querySelectorAll(".onboarding-btn-back").forEach(function(btn) { diff --git a/app/sovran_systemsos_web/templates/onboarding.html b/app/sovran_systemsos_web/templates/onboarding.html index cd701a8..edc15d8 100644 --- a/app/sovran_systemsos_web/templates/onboarding.html +++ b/app/sovran_systemsos_web/templates/onboarding.html @@ -34,6 +34,8 @@ 3 4 + + 5 @@ -70,8 +72,29 @@ - + + + +