Fix Change Passwords button: add API endpoint, system password modal, fix security banner link

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/bf43bea9-9f93-4f7b-b6fd-c76714e7f25b

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-04-07 16:44:57 +00:00
committed by GitHub
parent 06bdf999a6
commit ff1632dcda
3 changed files with 132 additions and 1 deletions

View File

@@ -2952,6 +2952,69 @@ async def api_security_status():
return {"status": status, "warning": warning} return {"status": status, "warning": warning}
# ── System password change ────────────────────────────────────────
FREE_PASSWORD_FILE = "/var/lib/secrets/free-password"
class ChangePasswordRequest(BaseModel):
new_password: str
confirm_password: str
@app.post("/api/change-password")
async def api_change_password(req: ChangePasswordRequest):
"""Change the system 'free' user password.
Updates /etc/shadow via chpasswd and writes the new password to
/var/lib/secrets/free-password so the Hub credentials view stays in sync.
Also clears the legacy security-status and security-warning files so the
security banner disappears after a successful change.
"""
if not req.new_password:
raise HTTPException(status_code=400, detail="New password must not be empty.")
if req.new_password != req.confirm_password:
raise HTTPException(status_code=400, detail="Passwords do not match.")
if len(req.new_password) < 8:
raise HTTPException(status_code=400, detail="Password must be at least 8 characters long.")
# Update /etc/shadow via chpasswd
try:
result = subprocess.run(
["chpasswd"],
input=f"free:{req.new_password}",
capture_output=True,
text=True,
)
if result.returncode != 0:
detail = (result.stderr or result.stdout).strip() or "chpasswd failed."
raise HTTPException(status_code=500, detail=detail)
except HTTPException:
raise
except Exception as exc:
raise HTTPException(status_code=500, detail=f"Failed to update system password: {exc}")
# Write new password to secrets file so Hub credentials stay in sync
try:
os.makedirs(os.path.dirname(FREE_PASSWORD_FILE), exist_ok=True)
with open(FREE_PASSWORD_FILE, "w") as f:
f.write(req.new_password)
os.chmod(FREE_PASSWORD_FILE, 0o600)
except Exception as exc:
raise HTTPException(status_code=500, detail=f"Failed to write secrets file: {exc}")
# Clear legacy security status so the warning banner is removed
for path in (SECURITY_STATUS_FILE, SECURITY_WARNING_FILE):
try:
os.remove(path)
except FileNotFoundError:
pass
except Exception:
pass # Non-fatal; don't block a successful password change
return {"ok": True}
# ── Matrix user management ──────────────────────────────────────── # ── Matrix user management ────────────────────────────────────────
MATRIX_USERS_FILE = "/var/lib/secrets/matrix-users" MATRIX_USERS_FILE = "/var/lib/secrets/matrix-users"

View File

@@ -606,7 +606,7 @@ function renderAutolaunchToggle(enabled) {
'<div class="security-inline-banner">' + '<div class="security-inline-banner">' +
'<span class="security-inline-icon">⚠</span>' + '<span class="security-inline-icon">⚠</span>' +
'<span class="security-inline-text">' + msg + '</span>' + '<span class="security-inline-text">' + msg + '</span>' +
'<a class="security-inline-link" href="/onboarding?step=passwords">Change Passwords</a>' + '<a class="security-inline-link" href="#" onclick="openServiceDetailModal(\'root-password-setup.service\', \'System Passwords\', \'passwords\'); return false;">Change Passwords</a>' +
'</div>'; '</div>';
} }

View File

@@ -280,6 +280,10 @@ async function openServiceDetailModal(unit, name, icon) {
'<button class="matrix-action-btn" id="matrix-add-user-btn"> Add New User</button>' + '<button class="matrix-action-btn" id="matrix-add-user-btn"> Add New User</button>' +
'<button class="matrix-action-btn" id="matrix-change-pw-btn">🔑 Change Password</button>' + '<button class="matrix-action-btn" id="matrix-change-pw-btn">🔑 Change Password</button>' +
'</div>' : "") + '</div>' : "") +
(unit === "root-password-setup.service" ?
'<hr class="matrix-actions-divider"><div class="matrix-actions-row">' +
'<button class="matrix-action-btn" id="sys-change-pw-btn">🔑 Change Password</button>' +
'</div>' : "") +
'</div>'; '</div>';
} else if (!data.enabled && !data.feature) { } else if (!data.enabled && !data.feature) {
html += '<div class="svc-detail-section">' + html += '<div class="svc-detail-section">' +
@@ -366,6 +370,11 @@ async function openServiceDetailModal(unit, name, icon) {
if (changePwBtn) changePwBtn.addEventListener("click", function() { openMatrixChangePasswordModal(unit, name, icon); }); if (changePwBtn) changePwBtn.addEventListener("click", function() { openMatrixChangePasswordModal(unit, name, icon); });
} }
if (unit === "root-password-setup.service") {
var sysPwBtn = document.getElementById("sys-change-pw-btn");
if (sysPwBtn) sysPwBtn.addEventListener("click", function() { openSystemChangePasswordModal(unit, name, icon); });
}
if (data.feature) { if (data.feature) {
var addonBtn = document.getElementById("svc-detail-addon-btn"); var addonBtn = document.getElementById("svc-detail-addon-btn");
if (addonBtn) { if (addonBtn) {
@@ -535,4 +544,63 @@ function openMatrixChangePasswordModal(unit, name, icon) {
}); });
} }
function openSystemChangePasswordModal(unit, name, icon) {
if (!$credsBody) return;
$credsBody.innerHTML =
'<div class="matrix-form-group"><label class="matrix-form-label" for="sys-chpw-new">New Password</label>' +
'<input class="matrix-form-input" type="password" id="sys-chpw-new" placeholder="New strong password" autocomplete="new-password"></div>' +
'<div class="matrix-form-group"><label class="matrix-form-label" for="sys-chpw-confirm">Confirm Password</label>' +
'<input class="matrix-form-input" type="password" id="sys-chpw-confirm" placeholder="Confirm new password" autocomplete="new-password"></div>' +
'<div class="matrix-form-actions">' +
'<button class="matrix-form-back" id="sys-chpw-back-btn">← Back</button>' +
'<button class="matrix-form-submit" id="sys-chpw-submit-btn">Change Password</button>' +
'</div>' +
'<div class="matrix-form-result" id="sys-chpw-result"></div>';
document.getElementById("sys-chpw-back-btn").addEventListener("click", function() {
openServiceDetailModal(unit, name, icon);
});
document.getElementById("sys-chpw-submit-btn").addEventListener("click", async function() {
var submitBtn = document.getElementById("sys-chpw-submit-btn");
var resultEl = document.getElementById("sys-chpw-result");
var newPassword = document.getElementById("sys-chpw-new").value || "";
var confirmPassword = document.getElementById("sys-chpw-confirm").value || "";
if (!newPassword || !confirmPassword) {
resultEl.className = "matrix-form-result error";
resultEl.textContent = "Both password fields are required.";
return;
}
submitBtn.disabled = true;
submitBtn.textContent = "Changing…";
resultEl.className = "matrix-form-result";
resultEl.textContent = "";
try {
await apiFetch("/api/change-password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ new_password: newPassword, confirm_password: confirmPassword })
});
resultEl.className = "matrix-form-result success";
resultEl.textContent = "✅ System password changed successfully.";
submitBtn.textContent = "Change Password";
submitBtn.disabled = false;
// Hide the legacy security banner if it's visible
if (typeof _securityIsLegacy !== "undefined" && _securityIsLegacy) {
_securityIsLegacy = false;
var banner = document.querySelector(".security-inline-banner");
if (banner) banner.remove();
}
} catch (err) {
resultEl.className = "matrix-form-result error";
resultEl.textContent = "❌ " + (err.message || "Failed to change password.");
submitBtn.textContent = "Change Password";
submitBtn.disabled = false;
}
});
}
function closeCredsModal() { if ($credsModal) $credsModal.classList.remove("open"); } function closeCredsModal() { if ($credsModal) $credsModal.classList.remove("open"); }