Compare commits
107 Commits
26b89dae76
...
staging-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8413093d43 | ||
|
|
1a8a1736bf | ||
|
|
51c7d172b3 | ||
|
|
6999ae5680 | ||
|
|
0c3f74e7de | ||
|
|
d2703ff84b | ||
|
|
1a9e0825fc | ||
|
|
284a861927 | ||
|
|
02b4e6b5b4 | ||
|
|
60084c292e | ||
|
|
fa22a080b9 | ||
|
|
70f0af98f6 | ||
|
|
cd4df316ae | ||
|
|
ff55dce746 | ||
|
|
5a86c03f74 | ||
| 1c2df46ac4 | |||
| 8839620e63 | |||
| c03126e8f8 | |||
|
|
10ef36859d | ||
|
|
4acb75f2bd | ||
|
|
77e2fb2537 | ||
|
|
c7bbb97a68 | ||
|
|
6d1c360c02 | ||
|
|
3b73eb3bd1 | ||
|
|
6ffcc056ad | ||
|
|
742f680d0d | ||
|
|
c872f1c6b0 | ||
|
|
bc5a40f143 | ||
|
|
c2bd3f6273 | ||
|
|
343dee3576 | ||
|
|
ebcafd3c6d | ||
|
|
5231b5ca4b | ||
|
|
1195456bee | ||
|
|
48de6b9821 | ||
|
|
cd4a17fe31 | ||
|
|
d3a5b3e6ef | ||
|
|
3c4c6c7389 | ||
|
|
876f728aa2 | ||
|
|
950a6dabd8 | ||
|
|
1d9589a186 | ||
|
|
b13fa7dc05 | ||
|
|
069f6c3ec7 | ||
|
|
5a27b79b51 | ||
|
|
72453c80bf | ||
| 14800ffb1e | |||
| e2f36d01bc | |||
| 55b231b456 | |||
|
|
b4b2607df1 | ||
|
|
ac9ba4776c | ||
|
|
85aca0d022 | ||
|
|
80c74b2d1a | ||
|
|
d28f224ad5 | ||
|
|
f2a808ed13 | ||
|
|
4ef420651d | ||
|
|
65ce66a541 | ||
|
|
deae53b721 | ||
|
|
f459e83861 | ||
|
|
badab99242 | ||
|
|
84124ba1b1 | ||
|
|
2ad0d2072d | ||
|
|
ff1632dcda | ||
|
|
531b8c1d09 | ||
|
|
a8128cef8d | ||
|
|
3baffb2a69 | ||
|
|
06bdf999a6 | ||
|
|
76ff1f4d4f | ||
|
|
2360b4147c | ||
|
|
37874ff58e | ||
|
|
aef13155fc | ||
|
|
1d4f104524 | ||
|
|
11ec4b4816 | ||
|
|
2bd899848d | ||
|
|
18a6e8d24c | ||
|
|
13c686a8a1 | ||
|
|
7a172c0306 | ||
|
|
7fc04fcf20 | ||
|
|
a40ea61415 | ||
| eba517d34d | |||
|
|
38257492bd | ||
| 93592c984d | |||
|
|
7a08bc0b2b | ||
|
|
25e8cac613 | ||
|
|
02eaea85d8 | ||
|
|
6c433d642d | ||
|
|
7aed3e09e8 | ||
|
|
e0e6ab0de6 | ||
|
|
7a1cd8a6f6 | ||
|
|
9407d500c8 | ||
| 9f1dd7def1 | |||
|
|
480f188d86 | ||
| e2bd366bb3 | |||
|
|
f80c8a0481 | ||
| 7e996fffa1 | |||
|
|
d14e25c29f | ||
|
|
1ed7ab9776 | ||
|
|
dd8867b52f | ||
|
|
3668eb2829 | ||
|
|
e751dfc1b2 | ||
|
|
6c3bbbf72b | ||
|
|
9dcb45a017 | ||
|
|
b9069433b1 | ||
|
|
739f6a08da | ||
|
|
2fc8b64964 | ||
|
|
6e133b6b59 | ||
|
|
01e3e02a62 | ||
|
|
85af70e2ee | ||
| b21d9bef87 |
@@ -9,6 +9,7 @@ import json
|
||||
import os
|
||||
import pwd
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
@@ -59,6 +60,11 @@ REBOOT_COMMAND = ["reboot"]
|
||||
ONBOARDING_FLAG = "/var/lib/sovran/onboarding-complete"
|
||||
AUTOLAUNCH_DISABLE_FLAG = "/var/lib/sovran/hub-autolaunch-disabled"
|
||||
|
||||
# ── Legacy security check constants ──────────────────────────────
|
||||
|
||||
SECURITY_STATUS_FILE = "/var/lib/sovran/security-status"
|
||||
SECURITY_WARNING_FILE = "/var/lib/sovran/security-warning"
|
||||
|
||||
# ── Tech Support constants ────────────────────────────────────────
|
||||
|
||||
SUPPORT_KEY_FILE = "/root/.ssh/sovran_support_authorized"
|
||||
@@ -398,6 +404,27 @@ SERVICE_DESCRIPTIONS: dict[str, str] = {
|
||||
"Your system account credentials. These are the keys to your Sovran_SystemsOS machine — "
|
||||
"root access, user accounts, and SSH passphrases. Keep them safe."
|
||||
),
|
||||
"sparrow-autoconnect.service": (
|
||||
"Sparrow Wallet is a privacy-focused Bitcoin desktop wallet for sending, receiving, "
|
||||
"and managing your Bitcoin. Sovran_SystemsOS automatically connects it to your local "
|
||||
"Electrs server on first boot — your address lookups, balances, and transactions "
|
||||
"never touch a third-party server. Full privacy, zero configuration."
|
||||
),
|
||||
"bisq-autoconnect.service": (
|
||||
"Bisq is a decentralized, peer-to-peer Bitcoin exchange — buy and sell Bitcoin "
|
||||
"with no KYC and no middleman. Sovran_SystemsOS automatically connects it to your "
|
||||
"local Bitcoin node on first boot, routing all traffic through Tor. Your trades are "
|
||||
"verified by your own node, keeping you fully sovereign."
|
||||
),
|
||||
}
|
||||
|
||||
SERVICE_DESKTOP_LINKS: dict[str, list[dict[str, str]]] = {
|
||||
"sparrow-autoconnect.service": [
|
||||
{"label": "Open Sparrow Wallet", "desktop_file": "sparrow-desktop.desktop"},
|
||||
],
|
||||
"bisq-autoconnect.service": [
|
||||
{"label": "Open Bisq", "desktop_file": "Bisq.desktop"},
|
||||
],
|
||||
}
|
||||
|
||||
# ── App setup ────────────────────────────────────────────────────
|
||||
@@ -664,7 +691,10 @@ def _check_port_status(
|
||||
for pt in ports_set
|
||||
)
|
||||
|
||||
if is_listening and is_allowed:
|
||||
# A process bound to the port is the authoritative signal; firewall
|
||||
# detection (nft/iptables) is only used as a secondary hint when nothing
|
||||
# is listening yet.
|
||||
if is_listening:
|
||||
return "listening"
|
||||
if is_allowed:
|
||||
return "firewall_open"
|
||||
@@ -675,7 +705,7 @@ def _check_port_status(
|
||||
|
||||
def _generate_qr_base64(data: str) -> str | None:
|
||||
"""Generate a QR code PNG and return it as a base64-encoded data URI.
|
||||
Uses qrencode CLI (available on the system via credentials-pdf.nix)."""
|
||||
Uses qrencode CLI (available on the system via credentials.nix)."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["qrencode", "-o", "-", "-t", "PNG", "-s", "6", "-m", "2", "-l", "H", data],
|
||||
@@ -2132,6 +2162,7 @@ async def api_service_detail(unit: str, icon: str | None = None):
|
||||
"credentials": resolved_creds,
|
||||
"needs_domain": needs_domain,
|
||||
"domain": domain,
|
||||
"domain_name": domain_key,
|
||||
"domain_status": domain_status,
|
||||
"port_requirements": port_requirements,
|
||||
"port_statuses": port_statuses,
|
||||
@@ -2151,9 +2182,36 @@ async def api_service_detail(unit: str, icon: str | None = None):
|
||||
btc_ver = _format_bitcoin_version(raw_ver, icon=icon)
|
||||
service_detail["bitcoin_version"] = btc_ver # backwards compat
|
||||
service_detail["version"] = btc_ver
|
||||
desktop_links = SERVICE_DESKTOP_LINKS.get(unit, [])
|
||||
if desktop_links:
|
||||
service_detail["desktop_links"] = desktop_links
|
||||
return service_detail
|
||||
|
||||
|
||||
@app.post("/api/desktop/launch/{desktop_file}")
|
||||
async def api_desktop_launch(desktop_file: str):
|
||||
"""Launch a desktop application via gtk-launch on the local GNOME session."""
|
||||
import re as _re
|
||||
if not _re.match(r'^[a-zA-Z0-9_.-]+\.desktop$', desktop_file):
|
||||
raise HTTPException(status_code=400, detail="Invalid desktop file name")
|
||||
|
||||
try:
|
||||
env = dict(os.environ)
|
||||
env["DISPLAY"] = ":0"
|
||||
result = subprocess.run(
|
||||
["gtk-launch", desktop_file],
|
||||
capture_output=True, text=True, timeout=10, env=env,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to launch: {result.stderr.strip()}")
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=500, detail="gtk-launch not found on this system")
|
||||
except subprocess.TimeoutExpired:
|
||||
raise HTTPException(status_code=500, detail="Launch command timed out")
|
||||
|
||||
return {"ok": True, "launched": desktop_file}
|
||||
|
||||
|
||||
@app.get("/api/network")
|
||||
async def api_network():
|
||||
loop = asyncio.get_event_loop()
|
||||
@@ -2865,6 +2923,196 @@ async def api_domains_check(req: DomainCheckRequest):
|
||||
return {"domains": list(check_results)}
|
||||
|
||||
|
||||
# ── Legacy security check ─────────────────────────────────────────
|
||||
|
||||
@app.get("/api/security/status")
|
||||
async def api_security_status():
|
||||
"""Return the legacy security status and warning message, if present.
|
||||
|
||||
Reads /var/lib/sovran/security-status and /var/lib/sovran/security-warning.
|
||||
Returns {"status": "legacy", "warning": "<message>"} for legacy machines,
|
||||
or {"status": "ok", "warning": ""} when the files are absent.
|
||||
"""
|
||||
try:
|
||||
with open(SECURITY_STATUS_FILE, "r") as f:
|
||||
status = f.read().strip()
|
||||
except FileNotFoundError:
|
||||
status = "ok"
|
||||
|
||||
warning = ""
|
||||
if status == "legacy":
|
||||
try:
|
||||
with open(SECURITY_WARNING_FILE, "r") as f:
|
||||
warning = f.read().strip()
|
||||
except FileNotFoundError:
|
||||
warning = (
|
||||
"This machine was manufactured before the factory-seal process. "
|
||||
"The default system password may be known to the factory. "
|
||||
"Please change your system and application passwords immediately."
|
||||
)
|
||||
elif status == "unsealed":
|
||||
try:
|
||||
with open(SECURITY_WARNING_FILE, "r") as f:
|
||||
warning = f.read().strip()
|
||||
except FileNotFoundError:
|
||||
warning = (
|
||||
"This machine was set up without the factory seal process. "
|
||||
"Factory test data — including SSH keys, database contents, and wallet information — "
|
||||
"may still be present on this system."
|
||||
)
|
||||
|
||||
return {"status": status, "warning": warning}
|
||||
|
||||
|
||||
def _is_free_password_default() -> bool:
|
||||
"""Check /etc/shadow directly to see if 'free' still has a factory default password.
|
||||
|
||||
Hashes each known factory default against the current shadow hash so that
|
||||
password changes made via GNOME, passwd, or any method other than the Hub
|
||||
are detected correctly.
|
||||
"""
|
||||
import subprocess
|
||||
import re as _re
|
||||
|
||||
FACTORY_DEFAULTS = ["free", "gosovransystems"]
|
||||
# Map shadow algorithm IDs to openssl passwd flags (SHA-512 and SHA-256 only,
|
||||
# matching the shell-script counterpart in factory-seal.nix)
|
||||
ALGO_FLAGS = {"6": "-6", "5": "-5"}
|
||||
try:
|
||||
with open("/etc/shadow", "r") as f:
|
||||
for line in f:
|
||||
parts = line.strip().split(":")
|
||||
if parts[0] == "free" and len(parts) > 1:
|
||||
current_hash = parts[1]
|
||||
if not current_hash or current_hash in ("!", "*", "!!"):
|
||||
return True # locked/no password — treat as default
|
||||
# Parse hash: $id$[rounds=N$]salt$hash
|
||||
hash_fields = current_hash.split("$")
|
||||
# hash_fields: ["", id, salt_or_rounds, ...]
|
||||
if len(hash_fields) < 4:
|
||||
return True # unrecognized format — assume default for safety
|
||||
algo_id = hash_fields[1]
|
||||
salt_field = hash_fields[2]
|
||||
if algo_id not in ALGO_FLAGS:
|
||||
return True # unrecognized algorithm — assume default for safety
|
||||
if salt_field.startswith("rounds="):
|
||||
return True # can't extract real salt simply — assume default for safety
|
||||
# Validate salt contains only safe characters (alphanumeric, '.', '/', '-', '_')
|
||||
# to guard against unexpected shadow file content before passing to subprocess
|
||||
if not _re.fullmatch(r"[A-Za-z0-9./\-_]+", salt_field):
|
||||
return True # unexpected salt format — assume default for safety
|
||||
openssl_flag = ALGO_FLAGS[algo_id]
|
||||
for default_pw in FACTORY_DEFAULTS:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["openssl", "passwd", openssl_flag, "-salt", salt_field, default_pw],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip() == current_hash:
|
||||
return True
|
||||
except Exception:
|
||||
return True # if openssl fails, assume default for safety
|
||||
return False
|
||||
except (FileNotFoundError, PermissionError):
|
||||
pass
|
||||
return True # if /etc/shadow is unreadable, assume default for safety
|
||||
|
||||
|
||||
@app.get("/api/security/password-is-default")
|
||||
async def api_password_is_default():
|
||||
"""Check if the free account password is still the factory default.
|
||||
|
||||
Uses /etc/shadow as the authoritative source so that password changes made
|
||||
via GNOME Settings, the passwd command, or any other method are detected
|
||||
correctly — not just changes made through the Hub or change-free-password.
|
||||
"""
|
||||
return {"is_default": _is_free_password_default()}
|
||||
|
||||
|
||||
# ── 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.")
|
||||
|
||||
# Locate chpasswd binary (NixOS puts it in the Nix store, not /usr/bin)
|
||||
chpasswd_bin = (
|
||||
shutil.which("chpasswd")
|
||||
or ("/run/current-system/sw/bin/chpasswd"
|
||||
if os.path.isfile("/run/current-system/sw/bin/chpasswd") else None)
|
||||
)
|
||||
if chpasswd_bin is None:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="chpasswd binary not found. Cannot update system password.",
|
||||
)
|
||||
|
||||
# Update /etc/shadow via chpasswd
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[chpasswd_bin],
|
||||
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 — but only
|
||||
# for "legacy" machines (pre-seal era). For "unsealed" machines, changing
|
||||
# passwords is not enough; the factory residue (SSH keys, wallet data,
|
||||
# databases) remains until a proper re-seal or re-install is performed.
|
||||
try:
|
||||
with open(SECURITY_STATUS_FILE, "r") as f:
|
||||
current_status = f.read().strip()
|
||||
if current_status == "legacy":
|
||||
os.remove(SECURITY_STATUS_FILE)
|
||||
try:
|
||||
os.remove(SECURITY_WARNING_FILE)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except (FileNotFoundError, OSError):
|
||||
pass
|
||||
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ── Matrix user management ────────────────────────────────────────
|
||||
|
||||
MATRIX_USERS_FILE = "/var/lib/secrets/matrix-users"
|
||||
@@ -3052,3 +3300,55 @@ async def _startup_save_ip():
|
||||
loop = asyncio.get_event_loop()
|
||||
ip = await loop.run_in_executor(None, _get_internal_ip)
|
||||
_save_internal_ip(ip)
|
||||
|
||||
|
||||
# ── Startup: recover stale RUNNING status files ──────────────────
|
||||
|
||||
_SAFE_UNIT_RE = re.compile(r'^[a-zA-Z0-9@._\-]+\.service$')
|
||||
|
||||
|
||||
def _recover_stale_status(status_file: str, log_file: str, unit_name: str):
|
||||
"""If status_file says RUNNING but the systemd unit is not active, reset to FAILED."""
|
||||
if not _SAFE_UNIT_RE.match(unit_name):
|
||||
return
|
||||
|
||||
try:
|
||||
with open(status_file, "r") as f:
|
||||
status = f.read().strip()
|
||||
except FileNotFoundError:
|
||||
return
|
||||
|
||||
if status != "RUNNING":
|
||||
return
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["systemctl", "is-active", unit_name],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
active = result.stdout.strip() == "active"
|
||||
except Exception:
|
||||
active = False
|
||||
|
||||
if not active:
|
||||
try:
|
||||
with open(status_file, "w") as f:
|
||||
f.write("FAILED")
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
with open(log_file, "a") as f:
|
||||
f.write(
|
||||
"\n[Hub] Process was interrupted (stale RUNNING status detected"
|
||||
" on startup). Marking as failed.\n"
|
||||
)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def _startup_recover_stale_status():
|
||||
"""Reset stale RUNNING status files left by interrupted update/rebuild jobs."""
|
||||
loop = asyncio.get_event_loop()
|
||||
await loop.run_in_executor(None, _recover_stale_status, UPDATE_STATUS, UPDATE_LOG, UPDATE_UNIT)
|
||||
await loop.run_in_executor(None, _recover_stale_status, REBUILD_STATUS, REBUILD_LOG, REBUILD_UNIT)
|
||||
|
||||
@@ -147,6 +147,17 @@ button.btn-reboot:hover:not(:disabled) {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.creds-title-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
vertical-align: middle;
|
||||
border-radius: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.creds-close-btn {
|
||||
|
||||
@@ -146,17 +146,6 @@
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.onboarding-card--scroll {
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--border-color) transparent;
|
||||
}
|
||||
|
||||
.onboarding-card--ports {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* Body text */
|
||||
|
||||
.onboarding-body-text {
|
||||
@@ -228,7 +217,9 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 4px;
|
||||
padding-top: 24px;
|
||||
padding-bottom: 24px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.onboarding-btn-next {
|
||||
@@ -575,6 +566,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 {
|
||||
|
||||
107
app/sovran_systemsos_web/static/css/security.css
Normal file
107
app/sovran_systemsos_web/static/css/security.css
Normal file
@@ -0,0 +1,107 @@
|
||||
/* ── Legacy security inline warning banner ───────────────────────── */
|
||||
|
||||
.security-inline-banner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 12px;
|
||||
background-color: rgba(180, 100, 0, 0.12);
|
||||
border-left: 3px solid #c97a00;
|
||||
border-radius: 6px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.security-inline-icon {
|
||||
font-size: 1rem;
|
||||
color: #e69000;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.security-inline-text {
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.5;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.security-inline-link {
|
||||
display: inline-block;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
color: #e69000;
|
||||
text-decoration: none;
|
||||
border: 1px solid #c97a00;
|
||||
border-radius: 4px;
|
||||
padding: 4px 10px;
|
||||
align-self: flex-start;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.security-inline-link:hover {
|
||||
background-color: rgba(180, 100, 0, 0.22);
|
||||
}
|
||||
|
||||
/* ── System change-password form extras ──────────────────────────── */
|
||||
|
||||
.sys-chpw-header {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.sys-chpw-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.sys-chpw-desc {
|
||||
font-size: 0.82rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.pw-input-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pw-input-wrap .matrix-form-input {
|
||||
padding-right: 2.4rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pw-toggle-btn {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
padding: 2px 4px;
|
||||
line-height: 1;
|
||||
color: var(--text-secondary);
|
||||
opacity: 0.75;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.pw-toggle-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.pw-hint {
|
||||
font-size: 0.76rem;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.pw-credentials-note {
|
||||
font-size: 0.78rem;
|
||||
color: #c97a00;
|
||||
background-color: rgba(180, 100, 0, 0.10);
|
||||
border-left: 2px solid #c97a00;
|
||||
border-radius: 4px;
|
||||
padding: 7px 10px;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
@@ -310,6 +310,12 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ── Service detail: Domain configure button ─────────────────────── */
|
||||
|
||||
.svc-detail-domain-btn {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* ── Service detail: Addon feature toggle ────────────────────────── */
|
||||
|
||||
.svc-detail-addon-row {
|
||||
@@ -343,3 +349,18 @@
|
||||
color: var(--yellow);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
||||
/* ── Desktop launch buttons ──────────────────────────────────────── */
|
||||
|
||||
.svc-detail-launch-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.svc-detail-launch-btn {
|
||||
font-size: 0.85rem;
|
||||
padding: 8px 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -84,6 +84,9 @@ async function init() {
|
||||
// If we can't reach the endpoint, continue to normal dashboard
|
||||
}
|
||||
|
||||
// Check for legacy machine security warning
|
||||
await checkLegacySecurity();
|
||||
|
||||
try {
|
||||
var cfg = await apiFetch("/api/config");
|
||||
_currentRole = cfg.role || "server_plus_desktop";
|
||||
|
||||
@@ -599,9 +599,29 @@ function renderAutolaunchToggle(enabled) {
|
||||
var section = document.createElement("div");
|
||||
section.className = "category-section autolaunch-section";
|
||||
|
||||
var securityBanner = "";
|
||||
if (_securityIsLegacy) {
|
||||
var msg = _securityWarningMessage || "Your system may have factory default passwords. Please change your passwords to secure your system.";
|
||||
var linkText, linkAction;
|
||||
if (_securityStatus === "unsealed") {
|
||||
linkText = "Contact Support";
|
||||
linkAction = "openSupportModal(); return false;";
|
||||
} else {
|
||||
linkText = "Change Passwords";
|
||||
linkAction = "openServiceDetailModal('root-password-setup.service', 'System Passwords', 'passwords'); return false;";
|
||||
}
|
||||
securityBanner =
|
||||
'<div class="security-inline-banner">' +
|
||||
'<span class="security-inline-icon">⚠</span>' +
|
||||
'<span class="security-inline-text">' + msg + '</span>' +
|
||||
'<a class="security-inline-link" href="#" onclick="' + linkAction + '">' + linkText + '</a>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
section.innerHTML =
|
||||
'<div class="section-header">Preferences</div>' +
|
||||
'<hr class="section-divider" />' +
|
||||
securityBanner +
|
||||
'<div class="feature-card">' +
|
||||
'<div class="feature-card-top">' +
|
||||
'<div class="feature-card-info">' +
|
||||
|
||||
16
app/sovran_systemsos_web/static/js/security.js
Normal file
16
app/sovran_systemsos_web/static/js/security.js
Normal file
@@ -0,0 +1,16 @@
|
||||
"use strict";
|
||||
|
||||
// ── Legacy security warning ───────────────────────────────────────
|
||||
|
||||
async function checkLegacySecurity() {
|
||||
try {
|
||||
var data = await apiFetch("/api/security/status");
|
||||
if (data && (data.status === "legacy" || data.status === "unsealed")) {
|
||||
_securityIsLegacy = true;
|
||||
_securityStatus = data.status;
|
||||
_securityWarningMessage = data.warning || "This machine may have a security issue. Please review your system security.";
|
||||
}
|
||||
} catch (_) {
|
||||
// Non-fatal — silently ignore if the endpoint is unreachable
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,20 @@ function _attachCopyHandlers(container) {
|
||||
|
||||
async function openServiceDetailModal(unit, name, icon) {
|
||||
if (!$credsModal) return;
|
||||
if ($credsTitle) $credsTitle.textContent = name;
|
||||
if ($credsTitle) {
|
||||
$credsTitle.innerHTML = '';
|
||||
if (icon) {
|
||||
var iconImg = document.createElement("img");
|
||||
iconImg.className = "creds-title-icon";
|
||||
iconImg.src = "/static/icons/" + escHtml(icon) + ".svg";
|
||||
iconImg.alt = name;
|
||||
iconImg.onerror = function() { this.style.display = "none"; };
|
||||
$credsTitle.appendChild(iconImg);
|
||||
}
|
||||
var nameSpan = document.createElement("span");
|
||||
nameSpan.textContent = name;
|
||||
$credsTitle.appendChild(nameSpan);
|
||||
}
|
||||
if ($credsBody) $credsBody.innerHTML = '<p class="creds-loading">Loading…</p>';
|
||||
$credsModal.classList.add("open");
|
||||
|
||||
@@ -72,6 +85,21 @@ async function openServiceDetailModal(unit, name, icon) {
|
||||
'</div>';
|
||||
}
|
||||
|
||||
// Section: Desktop Launch (only for services with desktop apps)
|
||||
if (data.desktop_links && data.desktop_links.length > 0) {
|
||||
var launchBtns = '';
|
||||
data.desktop_links.forEach(function(link) {
|
||||
launchBtns += '<button class="btn btn-primary svc-detail-launch-btn" data-desktop="' + escHtml(link.desktop_file) + '">' +
|
||||
'🖥️ ' + escHtml(link.label) +
|
||||
'</button>';
|
||||
});
|
||||
html += '<div class="svc-detail-section">' +
|
||||
'<div class="svc-detail-section-title">Open on Desktop</div>' +
|
||||
'<p class="svc-detail-desc" style="margin-bottom:12px">If you are accessing this machine locally on the GNOME desktop, click below to launch the app directly.</p>' +
|
||||
'<div class="svc-detail-launch-row">' + launchBtns + '</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
// Section B: Status
|
||||
// When a feature override is present, use the feature's enabled state so the
|
||||
// modal matches what the dashboard tile shows (feature toggle is authoritative).
|
||||
@@ -216,8 +244,8 @@ async function openServiceDetailModal(unit, name, icon) {
|
||||
'<li>Find the domain you purchased for this service</li>' +
|
||||
'<li>Create a Dynamic DNS record pointing to your external IP: <code>' + escHtml(ds.expected_ip || "—") + '</code></li>' +
|
||||
'<li>Copy the DDNS curl command from Njal.la\'s dashboard</li>' +
|
||||
'<li>You can re-enter it in the Feature Manager to update your configuration</li>' +
|
||||
'</ol>' +
|
||||
'<button class="btn btn-primary svc-detail-domain-btn" id="svc-detail-reconfig-domain-btn">🔄 Reconfigure Domain</button>' +
|
||||
'</div>';
|
||||
} else {
|
||||
domainBadge = '<span class="svc-detail-domain-value">' + escHtml(data.domain) + '</span>';
|
||||
@@ -229,9 +257,9 @@ async function openServiceDetailModal(unit, name, icon) {
|
||||
'<p style="margin-top:8px">To get this service working:</p>' +
|
||||
'<ol>' +
|
||||
'<li>Purchase a subdomain at <a href="https://njal.la" target="_blank">njal.la</a> (if you haven\'t already)</li>' +
|
||||
'<li>Go to the <strong>Feature Manager</strong> in the sidebar</li>' +
|
||||
'<li>Find this service and configure your domain through the setup wizard</li>' +
|
||||
'<li>Use the button below to configure your domain through the setup wizard</li>' +
|
||||
'</ol>' +
|
||||
'<button class="btn btn-primary svc-detail-domain-btn" id="svc-detail-config-domain-btn">🌐 Configure Domain</button>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
@@ -252,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-change-pw-btn">🔑 Change Password</button>' +
|
||||
'</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>';
|
||||
} else if (!data.enabled && !data.feature) {
|
||||
html += '<div class="svc-detail-section">' +
|
||||
@@ -312,6 +344,25 @@ async function openServiceDetailModal(unit, name, icon) {
|
||||
$credsBody.innerHTML = html;
|
||||
_attachCopyHandlers($credsBody);
|
||||
|
||||
// Desktop launch button handlers
|
||||
$credsBody.querySelectorAll(".svc-detail-launch-btn").forEach(function(btn) {
|
||||
btn.addEventListener("click", async function() {
|
||||
var desktopFile = btn.dataset.desktop;
|
||||
btn.disabled = true;
|
||||
btn.textContent = "Launching…";
|
||||
try {
|
||||
await apiFetch("/api/desktop/launch/" + encodeURIComponent(desktopFile), {
|
||||
method: "POST"
|
||||
});
|
||||
btn.textContent = "✓ Launched!";
|
||||
setTimeout(function() { btn.textContent = "🖥️ " + btn.textContent; btn.disabled = false; }, 2000);
|
||||
} catch (err) {
|
||||
btn.textContent = "❌ Failed";
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (unit === "matrix-synapse.service") {
|
||||
var addBtn = document.getElementById("matrix-add-user-btn");
|
||||
var changePwBtn = document.getElementById("matrix-change-pw-btn");
|
||||
@@ -319,6 +370,11 @@ async function openServiceDetailModal(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) {
|
||||
var addonBtn = document.getElementById("svc-detail-addon-btn");
|
||||
if (addonBtn) {
|
||||
@@ -329,6 +385,26 @@ async function openServiceDetailModal(unit, name, icon) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Configure Domain button (for non-feature services that need a domain)
|
||||
var configDomainBtn = document.getElementById("svc-detail-config-domain-btn");
|
||||
var reconfigDomainBtn = document.getElementById("svc-detail-reconfig-domain-btn");
|
||||
var domainBtn = configDomainBtn || reconfigDomainBtn;
|
||||
if (domainBtn && data.needs_domain && data.domain_name) {
|
||||
var pseudoFeat = {
|
||||
id: data.domain_name,
|
||||
name: name,
|
||||
domain_name: data.domain_name,
|
||||
needs_ddns: true,
|
||||
extra_fields: []
|
||||
};
|
||||
domainBtn.addEventListener("click", function() {
|
||||
closeCredsModal();
|
||||
openDomainSetupModal(pseudoFeat, function() {
|
||||
openServiceDetailModal(unit, name, icon);
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
if ($credsBody) $credsBody.innerHTML = '<p class="creds-empty">Could not load service details.</p>';
|
||||
}
|
||||
@@ -336,9 +412,22 @@ async function openServiceDetailModal(unit, name, icon) {
|
||||
|
||||
// ── Credentials info modal ────────────────────────────────────────
|
||||
|
||||
async function openCredsModal(unit, name) {
|
||||
async function openCredsModal(unit, name, icon) {
|
||||
if (!$credsModal) return;
|
||||
if ($credsTitle) $credsTitle.textContent = name + " — Connection Info";
|
||||
if ($credsTitle) {
|
||||
$credsTitle.innerHTML = '';
|
||||
if (icon) {
|
||||
var iconImg = document.createElement("img");
|
||||
iconImg.className = "creds-title-icon";
|
||||
iconImg.src = "/static/icons/" + escHtml(icon) + ".svg";
|
||||
iconImg.alt = name;
|
||||
iconImg.onerror = function() { this.style.display = "none"; };
|
||||
$credsTitle.appendChild(iconImg);
|
||||
}
|
||||
var nameSpan = document.createElement("span");
|
||||
nameSpan.textContent = name + " — Connection Info";
|
||||
$credsTitle.appendChild(nameSpan);
|
||||
}
|
||||
if ($credsBody) $credsBody.innerHTML = '<p class="creds-loading">Loading…</p>';
|
||||
$credsModal.classList.add("open");
|
||||
try {
|
||||
@@ -475,4 +564,104 @@ function openMatrixChangePasswordModal(unit, name, icon) {
|
||||
});
|
||||
}
|
||||
|
||||
function openSystemChangePasswordModal(unit, name, icon) {
|
||||
if (!$credsBody) return;
|
||||
$credsBody.innerHTML =
|
||||
'<div class="sys-chpw-header">' +
|
||||
'<div class="sys-chpw-title">🔑 Change \'free\' Account Password</div>' +
|
||||
'<div class="sys-chpw-desc">This updates the system login password for the <strong>free</strong> user account on this device.</div>' +
|
||||
'</div>' +
|
||||
'<div class="matrix-form-group"><label class="matrix-form-label" for="sys-chpw-new">New Password</label>' +
|
||||
'<div class="pw-input-wrap">' +
|
||||
'<input class="matrix-form-input" type="password" id="sys-chpw-new" placeholder="New strong password" autocomplete="new-password">' +
|
||||
'<button type="button" class="pw-toggle-btn" id="sys-chpw-new-toggle" aria-label="Toggle password visibility">👁</button>' +
|
||||
'</div>' +
|
||||
'<div class="pw-hint">Password must be at least 8 characters.</div></div>' +
|
||||
'<div class="matrix-form-group"><label class="matrix-form-label" for="sys-chpw-confirm">Confirm Password</label>' +
|
||||
'<div class="pw-input-wrap">' +
|
||||
'<input class="matrix-form-input" type="password" id="sys-chpw-confirm" placeholder="Confirm new password" autocomplete="new-password">' +
|
||||
'<button type="button" class="pw-toggle-btn" id="sys-chpw-confirm-toggle" aria-label="Toggle password visibility">👁</button>' +
|
||||
'</div></div>' +
|
||||
'<div class="pw-credentials-note">⚠ After changing, your updated password will appear in the System Passwords credentials tile. Make sure to remember it.</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-new-toggle").addEventListener("click", function() {
|
||||
var inp = document.getElementById("sys-chpw-new");
|
||||
var isHidden = inp.type === "password";
|
||||
inp.type = isHidden ? "text" : "password";
|
||||
this.textContent = isHidden ? "👁🗨" : "👁";
|
||||
});
|
||||
|
||||
document.getElementById("sys-chpw-confirm-toggle").addEventListener("click", function() {
|
||||
var inp = document.getElementById("sys-chpw-confirm");
|
||||
var isHidden = inp.type === "password";
|
||||
inp.type = isHidden ? "text" : "password";
|
||||
this.textContent = isHidden ? "👁🗨" : "👁";
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (newPassword.length < 8) {
|
||||
resultEl.className = "matrix-form-result error";
|
||||
resultEl.textContent = "Password must be at least 8 characters.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
resultEl.className = "matrix-form-result error";
|
||||
resultEl.textContent = "Passwords do not match.";
|
||||
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 — but only for
|
||||
// "legacy" status machines. For "unsealed" machines, changing passwords
|
||||
// is not enough; the factory residue remains until a proper re-seal or re-install.
|
||||
if (typeof _securityIsLegacy !== "undefined" && _securityIsLegacy &&
|
||||
(typeof _securityStatus === "undefined" || _securityStatus !== "unsealed")) {
|
||||
_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"); }
|
||||
|
||||
@@ -99,5 +99,10 @@ const $upgradeConfirmBtn = document.getElementById("upgrade-confirm-btn");
|
||||
const $upgradeCancelBtn = document.getElementById("upgrade-cancel-btn");
|
||||
const $upgradeCloseBtn = document.getElementById("upgrade-close-btn");
|
||||
|
||||
// Legacy security warning state (populated by checkLegacySecurity in security.js)
|
||||
var _securityIsLegacy = false;
|
||||
var _securityStatus = "ok"; // "ok", "legacy", or "unsealed"
|
||||
var _securityWarningMessage = "";
|
||||
|
||||
// System status banner
|
||||
// (removed — health is now shown per-tile via the composite health field)
|
||||
@@ -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 =
|
||||
'<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 {
|
||||
// Fetch services, domains, and network info in parallel
|
||||
var results = await Promise.all([
|
||||
@@ -179,6 +308,8 @@ async function loadStep2() {
|
||||
html += '<label class="onboarding-domain-label onboarding-domain-label--sub">Njal.la DDNS Curl Command</label>';
|
||||
html += '<input class="onboarding-domain-input domain-field-input" type="text" id="ddns-input-' + escHtml(d.name) + '" data-ddns="' + escHtml(d.name) + '" placeholder="curl "https://njal.la/update/?h=' + escHtml(d.name) + '.yourdomain.com&k=abc123&auto"" />';
|
||||
html += '<p class="onboarding-hint" style="margin-top:4px;">ℹ Paste the curl URL from your Njal.la dashboard\'s Dynamic record</p>';
|
||||
html += '<button type="button" class="btn btn-primary onboarding-domain-save-btn" data-save-domain="' + escHtml(d.name) + '" style="align-self:flex-start;margin-top:8px;font-size:0.82rem;padding:6px 16px;">Save</button>';
|
||||
html += '<span class="onboarding-domain-save-status" id="domain-save-status-' + escHtml(d.name) + '" style="font-size:0.82rem;min-height:1.2em;"></span>';
|
||||
html += '</div>';
|
||||
});
|
||||
}
|
||||
@@ -189,13 +320,82 @@ async function loadStep2() {
|
||||
html += '<label class="onboarding-domain-label">📧 SSL Certificate Email</label>';
|
||||
html += '<p class="onboarding-hint onboarding-hint--inline">Let\'s Encrypt uses this for certificate expiry notifications.</p>';
|
||||
html += '<input class="onboarding-domain-input domain-field-input" type="email" id="ssl-email-input" placeholder="you@example.com" value="' + escHtml(emailVal) + '" />';
|
||||
html += '<button type="button" class="btn btn-primary onboarding-domain-save-btn" data-save-email="true" style="align-self:flex-start;margin-top:8px;font-size:0.82rem;padding:6px 16px;">Save</button>';
|
||||
html += '<span class="onboarding-domain-save-status" id="domain-save-status-email" style="font-size:0.82rem;min-height:1.2em;"></span>';
|
||||
html += '</div>';
|
||||
|
||||
body.innerHTML = html;
|
||||
|
||||
// Wire per-field save buttons for domains
|
||||
body.querySelectorAll('[data-save-domain]').forEach(function(btn) {
|
||||
btn.addEventListener('click', async function() {
|
||||
var domainName = btn.dataset.saveDomain;
|
||||
var domainInput = document.getElementById('domain-input-' + domainName);
|
||||
var ddnsInput = document.getElementById('ddns-input-' + domainName);
|
||||
var statusEl = document.getElementById('domain-save-status-' + domainName);
|
||||
var domainVal = domainInput ? domainInput.value.trim() : '';
|
||||
var ddnsVal = ddnsInput ? ddnsInput.value.trim() : '';
|
||||
|
||||
if (!domainVal) {
|
||||
if (statusEl) { statusEl.textContent = '⚠ Enter a domain first'; statusEl.style.color = 'var(--red)'; }
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Saving…';
|
||||
if (statusEl) { statusEl.textContent = ''; }
|
||||
|
||||
try {
|
||||
await apiFetch('/api/domains/set', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ domain_name: domainName, domain: domainVal, ddns_url: ddnsVal }),
|
||||
});
|
||||
if (statusEl) { statusEl.textContent = '✓ Saved'; statusEl.style.color = 'var(--green)'; }
|
||||
} catch (err) {
|
||||
if (statusEl) { statusEl.textContent = '⚠ ' + err.message; statusEl.style.color = 'var(--red)'; }
|
||||
}
|
||||
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Save';
|
||||
});
|
||||
});
|
||||
|
||||
// Wire save button for SSL email
|
||||
body.querySelectorAll('[data-save-email]').forEach(function(btn) {
|
||||
btn.addEventListener('click', async function() {
|
||||
var emailInput = document.getElementById('ssl-email-input');
|
||||
var statusEl = document.getElementById('domain-save-status-email');
|
||||
var emailVal = emailInput ? emailInput.value.trim() : '';
|
||||
|
||||
if (!emailVal) {
|
||||
if (statusEl) { statusEl.textContent = '⚠ Enter an email first'; statusEl.style.color = 'var(--red)'; }
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Saving…';
|
||||
if (statusEl) { statusEl.textContent = ''; }
|
||||
|
||||
try {
|
||||
await apiFetch('/api/domains/set-email', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: emailVal }),
|
||||
});
|
||||
if (statusEl) { statusEl.textContent = '✓ Saved'; statusEl.style.color = 'var(--green)'; }
|
||||
} catch (err) {
|
||||
if (statusEl) { statusEl.textContent = '⚠ ' + err.message; statusEl.style.color = 'var(--red)'; }
|
||||
}
|
||||
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Save';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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 +435,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 = '<p class="onboarding-loading">Checking ports…</p>';
|
||||
|
||||
@@ -327,10 +527,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 +545,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) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 973 B After Width: | Height: | Size: 22 KiB |
@@ -14,6 +14,7 @@
|
||||
<link rel="stylesheet" href="/static/css/onboarding.css" />
|
||||
<link rel="stylesheet" href="/static/css/support.css" />
|
||||
<link rel="stylesheet" href="/static/css/domain-setup.css" />
|
||||
<link rel="stylesheet" href="/static/css/security.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -236,6 +237,7 @@
|
||||
<script src="/static/js/update.js"></script>
|
||||
<script src="/static/js/rebuild.js"></script>
|
||||
<script src="/static/js/features.js"></script>
|
||||
<script src="/static/js/security.js"></script>
|
||||
<script src="/static/js/events.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -34,6 +34,8 @@
|
||||
<span class="onboarding-step-dot" data-step="3">3</span>
|
||||
<span class="onboarding-step-connector"></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>
|
||||
|
||||
<!-- Step panels -->
|
||||
@@ -70,8 +72,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Step 2: Domain Configuration ── -->
|
||||
<!-- ── Step 2: Create Your Password ── -->
|
||||
<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.
|
||||
</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 & Continue →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Step 3: Domain Configuration ── -->
|
||||
<div class="onboarding-panel" id="step-3" style="display:none">
|
||||
<div class="onboarding-step-header">
|
||||
<span class="onboarding-step-icon">🌐</span>
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
<div class="onboarding-card onboarding-card--scroll" id="step-2-body">
|
||||
<div class="onboarding-card" id="step-3-body">
|
||||
<p class="onboarding-loading">Loading service information…</p>
|
||||
</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">
|
||||
<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">
|
||||
<button class="btn btn-close-modal onboarding-btn-back" data-prev="2">← Back</button>
|
||||
<button class="btn btn-primary onboarding-btn-next" id="step-3-next">
|
||||
Save & Continue →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Step 3: Port Forwarding ── -->
|
||||
<div class="onboarding-panel" id="step-3" style="display:none">
|
||||
<!-- ── Step 4: Port Forwarding ── -->
|
||||
<div class="onboarding-panel" id="step-4" style="display:none">
|
||||
<div class="onboarding-step-header">
|
||||
<span class="onboarding-step-icon">🔌</span>
|
||||
<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>
|
||||
</p>
|
||||
</div>
|
||||
<div class="onboarding-card onboarding-card--ports" id="step-3-body">
|
||||
<div class="onboarding-card" id="step-4-body">
|
||||
<p class="onboarding-loading">Checking ports…</p>
|
||||
</div>
|
||||
<div class="onboarding-footer">
|
||||
<button class="btn btn-close-modal onboarding-btn-back" data-prev="2">← Back</button>
|
||||
<button class="btn btn-primary onboarding-btn-next" id="step-3-next">
|
||||
<button class="btn btn-close-modal onboarding-btn-back" data-prev="3">← Back</button>
|
||||
<button class="btn btn-primary onboarding-btn-next" id="step-4-next">
|
||||
Continue →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Step 4: Complete ── -->
|
||||
<div class="onboarding-panel" id="step-4" style="display:none">
|
||||
<!-- ── Step 5: Complete ── -->
|
||||
<div class="onboarding-panel" id="step-5" style="display:none">
|
||||
<div class="onboarding-hero">
|
||||
<div class="onboarding-logo">✅</div>
|
||||
<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.
|
||||
</p>
|
||||
<ul class="onboarding-checklist" id="onboarding-checklist">
|
||||
<li>✅ Password configured</li>
|
||||
<li>✅ Domain configuration saved</li>
|
||||
<li>✅ Port forwarding reviewed</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="onboarding-footer">
|
||||
<button class="btn btn-close-modal onboarding-btn-back" data-prev="3">← Back</button>
|
||||
<button class="btn btn-primary" id="step-4-finish">
|
||||
<button class="btn btn-close-modal onboarding-btn-back" data-prev="4">← Back</button>
|
||||
<button class="btn btn-primary" id="step-5-finish">
|
||||
Go to Dashboard →
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
nixpkgs.config.permittedInsecurePackages = [ "jitsi-meet-1.0.8043" ];
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
nftables
|
||||
git wget fish htop btop
|
||||
gnomeExtensions.transparent-top-bar-adjustable-transparency
|
||||
gnomeExtensions.dash-to-dock
|
||||
|
||||
@@ -22,4 +22,20 @@
|
||||
|
||||
# ─── Add your custom NixOS configuration below ───────────
|
||||
|
||||
# ─── Custom Caddy virtual hosts ──────────────────────────
|
||||
# Uncomment and edit below to add your own Caddy sites:
|
||||
#
|
||||
# sovran_systemsOS.caddy.extraVirtualHosts = ''
|
||||
# mysite.example.com {
|
||||
# encode gzip zstd
|
||||
# root * /var/lib/www/mysite
|
||||
# php_fastcgi unix//run/phpfpm/mypool.sock
|
||||
# file_server browse
|
||||
# }
|
||||
#
|
||||
# anotherdomain.com {
|
||||
# reverse_proxy localhost:9090
|
||||
# }
|
||||
# '';
|
||||
|
||||
}
|
||||
36
flake.lock
generated
36
flake.lock
generated
@@ -5,11 +5,11 @@
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1773169138,
|
||||
"narHash": "sha256-6X41z8o2z8KjF4gMzLTPD41WjvCDGXTc0muPGmwcOMk=",
|
||||
"lastModified": 1775155316,
|
||||
"narHash": "sha256-4H8aEChZ6rra9jd8OcVHgHs3IuzKzpDt4PPtsPJrkyM=",
|
||||
"owner": "emmanuelrosa",
|
||||
"repo": "bitcoin-knots-bip-110-nix",
|
||||
"rev": "b9d018b71e20ce8c1567cbc2401b6edc2c1c7793",
|
||||
"rev": "663ea34f6f846f48c385a73d4581ba599bb5bbc0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -24,11 +24,11 @@
|
||||
"oldNixpkgs": "oldNixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1774797058,
|
||||
"narHash": "sha256-URUOiKNjG3s7vDkTj554+3yzQ0qqNQoQwHdc7vs63X0=",
|
||||
"lastModified": 1775155241,
|
||||
"narHash": "sha256-J8dGfHLXlpJgDSgbBuQtll9chM9Hsv5hhVWglSRLgIE=",
|
||||
"owner": "emmanuelrosa",
|
||||
"repo": "btc-clients-nix",
|
||||
"rev": "a10dae067da04b7b170eed73efc665d27fc0e0c5",
|
||||
"rev": "e76e32fef6d0b8a8e9a32318835ba09915232a5c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -127,11 +127,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1772380631,
|
||||
"narHash": "sha256-FhW0uxeXjefINP0vUD4yRBB52Us7fXZPk9RiPAopfiY=",
|
||||
"lastModified": 1775054576,
|
||||
"narHash": "sha256-iiIr1hlTMu2LLARsUYtiqlE90tqocqIMVLK2fIzB/UY=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "6d3b61b190a899042ce82a5355111976ba76d698",
|
||||
"rev": "fc4b9b74d4b0bdbf3c97fef4bd34c05225172912",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -191,11 +191,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1772380631,
|
||||
"narHash": "sha256-FhW0uxeXjefINP0vUD4yRBB52Us7fXZPk9RiPAopfiY=",
|
||||
"lastModified": 1775054576,
|
||||
"narHash": "sha256-iiIr1hlTMu2LLARsUYtiqlE90tqocqIMVLK2fIzB/UY=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "6d3b61b190a899042ce82a5355111976ba76d698",
|
||||
"rev": "fc4b9b74d4b0bdbf3c97fef4bd34c05225172912",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -222,11 +222,11 @@
|
||||
},
|
||||
"nixpkgs_4": {
|
||||
"locked": {
|
||||
"lastModified": 1775036866,
|
||||
"narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=",
|
||||
"lastModified": 1775423009,
|
||||
"narHash": "sha256-vPKLpjhIVWdDrfiUM8atW6YkIggCEKdSAlJPzzhkQlw=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "6201e203d09599479a3b3450ed24fa81537ebc4e",
|
||||
"rev": "68d8aa3d661f0e6bd5862291b5bb263b2a6595c9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -259,11 +259,11 @@
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1774802402,
|
||||
"narHash": "sha256-L1UJ/zxKTyyaGGmytH6OYlgQ0HGSMhvPkvU+iz4Mkb8=",
|
||||
"lastModified": 1775307257,
|
||||
"narHash": "sha256-y9hEecHH4ennFwIcw1n480YCGh73DkEmizmQnyXuvgg=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixvim",
|
||||
"rev": "cbd8536a05d1aae2593cb5c9ace1010c8c5845cb",
|
||||
"rev": "2e008bb941f72379d5b935d5bfe70ed8b7c793ff",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
20
flake.nix
20
flake.nix
@@ -25,27 +25,17 @@
|
||||
modules = [
|
||||
{ nixpkgs.hostPlatform = "x86_64-linux"; }
|
||||
self.nixosModules.Sovran_SystemsOS
|
||||
/etc/nixos/hardware-configuration.nix
|
||||
/etc/nixos/role-state.nix
|
||||
/etc/nixos/custom.nix
|
||||
./hardware-configuration.nix
|
||||
./role-state.nix
|
||||
./custom.nix
|
||||
];
|
||||
};
|
||||
|
||||
nixosConfigurations.sovran-iso-desktop = nixpkgs.lib.nixosSystem {
|
||||
nixosConfigurations.sovran_systemsos-iso = nixpkgs.lib.nixosSystem {
|
||||
modules = [
|
||||
{ nixpkgs.hostPlatform = "x86_64-linux"; }
|
||||
({ config, pkgs, ... }: { nixpkgs.overlays = [ overlay-stable ]; })
|
||||
./iso/desktop.nix
|
||||
nix-bitcoin.nixosModules.default
|
||||
nixvim.nixosModules.nixvim
|
||||
];
|
||||
};
|
||||
|
||||
nixosConfigurations.sovran-iso-server = nixpkgs.lib.nixosSystem {
|
||||
modules = [
|
||||
{ nixpkgs.hostPlatform = "x86_64-linux"; }
|
||||
({ config, pkgs, ... }: { nixpkgs.overlays = [ overlay-stable ]; })
|
||||
./iso/server.nix
|
||||
./iso/common.nix
|
||||
nix-bitcoin.nixosModules.default
|
||||
nixvim.nixosModules.nixvim
|
||||
];
|
||||
|
||||
@@ -14,6 +14,28 @@ LOGO = "/etc/sovran/logo.png"
|
||||
LOG = "/tmp/sovran-install.log"
|
||||
FLAKE = "/etc/sovran/flake"
|
||||
|
||||
DEPLOYED_FLAKE = """\
|
||||
{
|
||||
description = "Sovran_SystemsOS for the Sovran Pro from Sovran Systems";
|
||||
|
||||
inputs = {
|
||||
Sovran_Systems.url = "git+https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS?ref=staging-dev";
|
||||
};
|
||||
|
||||
outputs = { self, Sovran_Systems, ... }@inputs: {
|
||||
nixosConfigurations."nixos" = Sovran_Systems.inputs.nixpkgs.lib.nixosSystem {
|
||||
modules = [
|
||||
{ nixpkgs.hostPlatform = "x86_64-linux"; }
|
||||
./hardware-configuration.nix
|
||||
./role-state.nix
|
||||
./custom.nix
|
||||
Sovran_Systems.nixosModules.Sovran_SystemsOS
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
"""
|
||||
|
||||
try:
|
||||
logfile = open(LOG, "a")
|
||||
atexit.register(logfile.close)
|
||||
@@ -87,7 +109,7 @@ def symbolic_icon(name):
|
||||
return icon
|
||||
|
||||
|
||||
# ── Application ────────────────────────────────────────────────────────────────
|
||||
# ── Application ──────────────────────────────────────────────────────────
|
||||
|
||||
class InstallerApp(Adw.Application):
|
||||
def __init__(self):
|
||||
@@ -99,7 +121,7 @@ class InstallerApp(Adw.Application):
|
||||
self.win.present()
|
||||
|
||||
|
||||
# ── Main Window ────────────────────────────────────────────────────────────────
|
||||
# ── Main Window ──────────────────────────────────────────────────────────
|
||||
|
||||
class InstallerWindow(Adw.ApplicationWindow):
|
||||
def __init__(self, **kwargs):
|
||||
@@ -146,7 +168,7 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
break
|
||||
self.push_page(title, child)
|
||||
|
||||
# ── Shared widgets ───────────────<EFBFBD><EFBFBD>─────────────────────────────────────
|
||||
# ── Shared widgets ────────────────────────────────────────────────────
|
||||
|
||||
def make_scrolled_log(self):
|
||||
sw = Gtk.ScrolledWindow()
|
||||
@@ -793,6 +815,9 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
data_p1 = f"{data_path}p1" if "nvme" in data_path else f"{data_path}1"
|
||||
run_stream(["sudo", "mkdir", "-p", "/mnt/run/media/Second_Drive"], buf)
|
||||
run_stream(["sudo", "mount", data_p1, "/mnt/run/media/Second_Drive"], buf)
|
||||
run_stream(["sudo", "mkdir", "-p", "/mnt/run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node"], buf)
|
||||
run_stream(["sudo", "mkdir", "-p", "/mnt/run/media/Second_Drive/BTCEcoandBackup/Electrs_Data"], buf)
|
||||
run_stream(["sudo", "mkdir", "-p", "/mnt/run/media/Second_Drive/BTCEcoandBackup/NixOS_Snapshot_Backup"], buf)
|
||||
|
||||
GLib.idle_add(append_text, buf, "\n=== Generating hardware config ===\n")
|
||||
run_stream(["sudo", "nixos-generate-config", "--root", "/mnt"], buf)
|
||||
@@ -831,7 +856,7 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
raise RuntimeError(f"Failed to write role-state.nix: {proc.stderr}")
|
||||
run(["sudo", "cp", "/mnt/etc/nixos/custom.template.nix", "/mnt/etc/nixos/custom.nix"])
|
||||
|
||||
# ── Step 4: Ready to install ────────<EFBFBD><EFBFBD><EFBFBD>──────────────────────────────────
|
||||
# ── Step 4: Ready to install ──────────────────────────────────────────
|
||||
|
||||
def push_ready(self):
|
||||
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||
@@ -930,9 +955,24 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
path = os.path.join(nixos_dir, entry)
|
||||
run(["sudo", "rm", "-rf", path])
|
||||
|
||||
GLib.idle_add(append_text, buf, "Writing deployed flake.nix...\n")
|
||||
proc = subprocess.run(
|
||||
["sudo", "tee", "/mnt/etc/nixos/flake.nix"],
|
||||
input=DEPLOYED_FLAKE,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
log(proc.stdout)
|
||||
if proc.returncode != 0:
|
||||
log(proc.stderr)
|
||||
raise RuntimeError(proc.stderr.strip() or "Failed to write deployed flake.nix")
|
||||
GLib.idle_add(append_text, buf, "Locking flake to staging-dev...\n")
|
||||
run_stream(["sudo", "nix", "--extra-experimental-features", "nix-command flakes",
|
||||
"flake", "lock", "/mnt/etc/nixos"], buf)
|
||||
|
||||
GLib.idle_add(self.push_complete)
|
||||
|
||||
# ── Step 6: Complete ───────────────────────────────────────────────────
|
||||
# ── Complete ───────────────────────────────────────────────────────────
|
||||
|
||||
def push_complete(self):
|
||||
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||
@@ -943,7 +983,7 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
status.set_vexpand(True)
|
||||
|
||||
creds_group = Adw.PreferencesGroup()
|
||||
creds_group.set_title("⚠ Write down your login details before rebooting")
|
||||
creds_group.set_title("⚠ Important — read before rebooting")
|
||||
creds_group.set_margin_start(40)
|
||||
creds_group.set_margin_end(40)
|
||||
|
||||
@@ -953,15 +993,15 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
creds_group.add(user_row)
|
||||
|
||||
pass_row = Adw.ActionRow()
|
||||
pass_row.set_title("Password")
|
||||
pass_row.set_subtitle("free")
|
||||
pass_row.set_title("Default Password")
|
||||
pass_row.set_subtitle("free — you will be prompted to change it on first boot")
|
||||
creds_group.add(pass_row)
|
||||
|
||||
note_row = Adw.ActionRow()
|
||||
note_row.set_title("App Passwords")
|
||||
note_row.set_title("First Boot Setup")
|
||||
note_row.set_subtitle(
|
||||
"After rebooting, all app passwords (Nextcloud, Bitcoin, Matrix, etc.) "
|
||||
"will be available in the Sovran Hub on your dashboard."
|
||||
"After rebooting, the Sovran Hub will guide you through setting "
|
||||
"your password, domains, and all app credentials."
|
||||
)
|
||||
creds_group.add(note_row)
|
||||
|
||||
@@ -1009,4 +1049,4 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = InstallerApp()
|
||||
app.run(None)
|
||||
app.run(None)
|
||||
@@ -69,7 +69,36 @@ lib.mkIf config.sovran_systemsOS.services.bitcoin {
|
||||
};
|
||||
|
||||
nix-bitcoin.useVersionLockedPkgs = false;
|
||||
|
||||
|
||||
systemd.services.bitcoind = {
|
||||
requires = [ "run-media-Second_Drive.mount" ];
|
||||
after = [ "run-media-Second_Drive.mount" ];
|
||||
};
|
||||
|
||||
systemd.services.electrs = {
|
||||
requires = [ "run-media-Second_Drive.mount" ];
|
||||
after = [ "run-media-Second_Drive.mount" ];
|
||||
};
|
||||
|
||||
systemd.services.sovran-btc-permissions = {
|
||||
description = "Fix Bitcoin/Electrs data directory ownership on second drive";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "run-media-Second_Drive.mount" ];
|
||||
before = [ "bitcoind.service" "electrs.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
script = ''
|
||||
if [ -d /run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node ]; then
|
||||
chown -R bitcoin:bitcoin /run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node
|
||||
fi
|
||||
if [ -d /run/media/Second_Drive/BTCEcoandBackup/Electrs_Data ]; then
|
||||
chown -R electrs:electrs /run/media/Second_Drive/BTCEcoandBackup/Electrs_Data
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
sovran_systemsOS.domainRequirements = [
|
||||
{ name = "btcpayserver"; label = "BTCPay Server"; example = "pay.yourdomain.com"; }
|
||||
];
|
||||
|
||||
@@ -2,13 +2,25 @@
|
||||
|
||||
let
|
||||
exposeBtcpay = config.sovran_systemsOS.web.btcpayserver;
|
||||
extraVhosts = config.sovran_systemsOS.caddy.extraVirtualHosts;
|
||||
in
|
||||
{
|
||||
services.caddy = {
|
||||
enable = true;
|
||||
user = "caddy";
|
||||
group = "root";
|
||||
configFile = "/run/caddy/Caddyfile";
|
||||
};
|
||||
|
||||
# Override ExecStart + ExecReload to point at the runtime-generated Caddyfile
|
||||
systemd.services.caddy.serviceConfig = {
|
||||
ExecStart = lib.mkForce [
|
||||
""
|
||||
"${pkgs.caddy}/bin/caddy run --config /run/caddy/Caddyfile --adapter caddyfile"
|
||||
];
|
||||
ExecReload = lib.mkForce [
|
||||
""
|
||||
"${pkgs.caddy}/bin/caddy reload --config /run/caddy/Caddyfile --adapter caddyfile --force"
|
||||
];
|
||||
};
|
||||
|
||||
systemd.services.caddy-generate-config = {
|
||||
@@ -170,6 +182,11 @@ EOF
|
||||
encode gzip zstd
|
||||
}
|
||||
EOF
|
||||
|
||||
# ── Custom vhosts from custom.nix ──────────────
|
||||
cat >> /run/caddy/Caddyfile <<'CUSTOM_VHOSTS_EOF'
|
||||
${extraVhosts}
|
||||
CUSTOM_VHOSTS_EOF
|
||||
'';
|
||||
};
|
||||
}
|
||||
}
|
||||
292
modules/core/factory-seal.nix
Normal file
292
modules/core/factory-seal.nix
Normal file
@@ -0,0 +1,292 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
sovran-factory-seal = pkgs.writeShellScriptBin "sovran-factory-seal" ''
|
||||
set -euo pipefail
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Error: must be run as root." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||
echo "║ ⚠ SOVRAN FACTORY SEAL — WARNING ⚠ ║"
|
||||
echo "╠══════════════════════════════════════════════════════════════╣"
|
||||
echo "║ This command will PERMANENTLY DELETE: ║"
|
||||
echo "║ • All generated passwords and secrets ║"
|
||||
echo "║ • LND wallet data (seed words, channels, macaroons) ║"
|
||||
echo "║ • SSH factory login key ║"
|
||||
echo "║ • Application databases (Matrix, Nextcloud, WordPress) ║"
|
||||
echo "║ • Vaultwarden database ║"
|
||||
echo "║ ║"
|
||||
echo "║ After sealing, all credentials will be regenerated fresh ║"
|
||||
echo "║ when the customer boots the device for the first time. ║"
|
||||
echo "║ ║"
|
||||
echo "║ DO NOT run this on a customer's live system. ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo -n "Type SEAL to confirm: "
|
||||
read -r CONFIRM
|
||||
if [ "$CONFIRM" != "SEAL" ]; then
|
||||
echo "Aborted." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Sealing system..."
|
||||
|
||||
# ── 1. Delete all generated secrets ──────────────────────────────
|
||||
echo " Wiping secrets..."
|
||||
[ -d /var/lib/secrets ] && find /var/lib/secrets -mindepth 1 -delete || true
|
||||
rm -rf /var/lib/matrix-synapse/registration-secret
|
||||
rm -rf /var/lib/matrix-synapse/db-password
|
||||
rm -rf /var/lib/gnome-remote-desktop/rdp-password
|
||||
rm -rf /var/lib/gnome-remote-desktop/rdp-username
|
||||
rm -rf /var/lib/gnome-remote-desktop/rdp-credentials
|
||||
rm -rf /var/lib/livekit/livekit_keyFile
|
||||
rm -rf /etc/nix-bitcoin-secrets/*
|
||||
|
||||
# ── 2. Wipe LND wallet (seed words, wallet DB, macaroons) ────────
|
||||
echo " Wiping LND wallet data..."
|
||||
rm -rf /var/lib/lnd/*
|
||||
|
||||
# ── 3. Wipe SSH factory key so it regenerates with new passphrase ─
|
||||
echo " Removing SSH factory key..."
|
||||
rm -f /home/free/.ssh/factory_login /home/free/.ssh/factory_login.pub
|
||||
if [ -f /root/.ssh/authorized_keys ]; then
|
||||
sed -i '/factory_login/d' /root/.ssh/authorized_keys
|
||||
fi
|
||||
|
||||
# ── 4. Drop application databases ────────────────────────────────
|
||||
echo " Dropping application databases..."
|
||||
sudo -u postgres psql -c "DROP DATABASE IF EXISTS \"matrix-synapse\";" 2>/dev/null || true
|
||||
sudo -u postgres psql -c "DROP DATABASE IF EXISTS nextclouddb;" 2>/dev/null || true
|
||||
mysql -u root -e "DROP DATABASE IF EXISTS wordpressdb;" 2>/dev/null || true
|
||||
|
||||
# ── 5. Remove application config files (so init services re-run) ─
|
||||
echo " Removing application config files..."
|
||||
rm -rf /var/lib/www/wordpress/wp-config.php
|
||||
rm -rf /var/lib/www/nextcloud/config/config.php
|
||||
|
||||
# ── 6. Wipe Vaultwarden database ──────────────────────────────────
|
||||
echo " Wiping Vaultwarden data..."
|
||||
rm -rf /var/lib/bitwarden_rs/*
|
||||
rm -rf /var/lib/vaultwarden/*
|
||||
|
||||
# ── 7. Set sealed flag and remove onboarded flag ─────────────────
|
||||
echo " Setting sealed flag..."
|
||||
touch /var/lib/sovran-factory-sealed
|
||||
rm -f /var/lib/sovran-customer-onboarded
|
||||
|
||||
echo ""
|
||||
echo "System sealed. Power off now or the system will shut down in 10 seconds."
|
||||
sleep 10
|
||||
poweroff
|
||||
'';
|
||||
|
||||
in
|
||||
{
|
||||
environment.systemPackages = [ sovran-factory-seal ];
|
||||
|
||||
# ── Auto-seal on first customer boot ───────────────────────────────
|
||||
systemd.services.sovran-auto-seal = {
|
||||
description = "Auto-seal Sovran system on first customer boot";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
before = [ "sovran-hub.service" "sovran-legacy-security-check.service" ];
|
||||
after = [ "local-fs.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
path = [ pkgs.coreutils pkgs.e2fsprogs pkgs.openssl pkgs.postgresql pkgs.mariadb pkgs.shadow ];
|
||||
script = ''
|
||||
# ── Idempotency check ─────────────────────────────────────────
|
||||
if [ -f /var/lib/sovran-factory-sealed ]; then
|
||||
echo "sovran-auto-seal: already sealed, nothing to do."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "sovran-auto-seal: seal flag missing — checking system state..."
|
||||
|
||||
# ── Safety guard 1: customer has already onboarded ────────────
|
||||
if [ -f /var/lib/sovran-customer-onboarded ]; then
|
||||
echo "sovran-auto-seal: /var/lib/sovran-customer-onboarded exists — live system detected. Restoring flag and exiting."
|
||||
touch /var/lib/sovran-factory-sealed
|
||||
chattr +i /var/lib/sovran-factory-sealed 2>/dev/null || true
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Safety guard 2: onboarding was completed ──────────────────
|
||||
if [ -f /var/lib/sovran/onboarding-complete ]; then
|
||||
echo "sovran-auto-seal: /var/lib/sovran/onboarding-complete exists — live system detected. Restoring flag and exiting."
|
||||
touch /var/lib/sovran-factory-sealed
|
||||
chattr +i /var/lib/sovran-factory-sealed 2>/dev/null || true
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Safety guard 3: password has been changed from factory defaults ──
|
||||
if [ -f /etc/shadow ]; then
|
||||
FREE_HASH=$(grep '^free:' /etc/shadow | cut -d: -f2)
|
||||
if [ -n "$FREE_HASH" ] && [ "$FREE_HASH" != "!" ] && [ "$FREE_HASH" != "*" ]; then
|
||||
ALGO_ID=$(printf '%s' "$FREE_HASH" | cut -d'$' -f2)
|
||||
SALT=$(printf '%s' "$FREE_HASH" | cut -d'$' -f3)
|
||||
STILL_DEFAULT=false
|
||||
# If the salt field starts with "rounds=", we cannot extract the real salt
|
||||
# with a simple cut — treat as still-default for safety
|
||||
if printf '%s' "$SALT" | grep -q '^rounds='; then
|
||||
STILL_DEFAULT=true
|
||||
else
|
||||
for DEFAULT_PW in "free" "gosovransystems"; do
|
||||
case "$ALGO_ID" in
|
||||
6) EXPECTED=$(openssl passwd -6 -salt "$SALT" "$DEFAULT_PW" 2>/dev/null) ;;
|
||||
5) EXPECTED=$(openssl passwd -5 -salt "$SALT" "$DEFAULT_PW" 2>/dev/null) ;;
|
||||
*)
|
||||
# Unknown hash algorithm — treat as still-default for safety
|
||||
STILL_DEFAULT=true
|
||||
break
|
||||
;;
|
||||
esac
|
||||
if [ -n "$EXPECTED" ] && [ "$EXPECTED" = "$FREE_HASH" ]; then
|
||||
STILL_DEFAULT=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if [ "$STILL_DEFAULT" = "false" ]; then
|
||||
echo "sovran-auto-seal: password has been changed from factory defaults — live system detected. Restoring flag and exiting."
|
||||
touch /var/lib/sovran-factory-sealed
|
||||
chattr +i /var/lib/sovran-factory-sealed 2>/dev/null || true
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── All safety guards passed: this is a fresh/unsealed system ─
|
||||
echo "sovran-auto-seal: fresh system confirmed — performing auto-seal..."
|
||||
|
||||
# ── 1. Wipe generated secrets ─────────────────────────────────
|
||||
echo "sovran-auto-seal: wiping secrets..."
|
||||
[ -d /var/lib/secrets ] && find /var/lib/secrets -mindepth 1 -delete || true
|
||||
rm -rf /var/lib/matrix-synapse/registration-secret
|
||||
rm -rf /var/lib/matrix-synapse/db-password
|
||||
rm -rf /var/lib/gnome-remote-desktop/rdp-password
|
||||
rm -rf /var/lib/gnome-remote-desktop/rdp-username
|
||||
rm -rf /var/lib/gnome-remote-desktop/rdp-credentials
|
||||
rm -rf /var/lib/livekit/livekit_keyFile
|
||||
rm -rf /etc/nix-bitcoin-secrets/*
|
||||
|
||||
# ── 2. Wipe LND wallet data ───────────────────────────────────
|
||||
echo "sovran-auto-seal: wiping LND wallet data..."
|
||||
rm -rf /var/lib/lnd/*
|
||||
|
||||
# ── 3. Remove SSH factory key ─────────────────────────────────
|
||||
echo "sovran-auto-seal: removing SSH factory key..."
|
||||
rm -f /home/free/.ssh/factory_login /home/free/.ssh/factory_login.pub
|
||||
if [ -f /root/.ssh/authorized_keys ]; then
|
||||
sed -i '/factory_login/d' /root/.ssh/authorized_keys
|
||||
fi
|
||||
|
||||
# ── 4. Drop application databases ────────────────────────────
|
||||
echo "sovran-auto-seal: dropping application databases..."
|
||||
sudo -u postgres psql -c "DROP DATABASE IF EXISTS \"matrix-synapse\";" 2>/dev/null || true
|
||||
sudo -u postgres psql -c "DROP DATABASE IF EXISTS nextclouddb;" 2>/dev/null || true
|
||||
mysql -u root -e "DROP DATABASE IF EXISTS wordpressdb;" 2>/dev/null || true
|
||||
|
||||
# ── 5. Remove application config files ───────────────────────
|
||||
echo "sovran-auto-seal: removing application config files..."
|
||||
rm -rf /var/lib/www/wordpress/wp-config.php
|
||||
rm -rf /var/lib/www/nextcloud/config/config.php
|
||||
|
||||
# ── 6. Wipe Vaultwarden data ──────────────────────────────────
|
||||
echo "sovran-auto-seal: wiping Vaultwarden data..."
|
||||
rm -rf /var/lib/bitwarden_rs/*
|
||||
rm -rf /var/lib/vaultwarden/*
|
||||
|
||||
# ── 7. Set sealed flag and make it immutable ──────────────────
|
||||
echo "sovran-auto-seal: setting sealed flag..."
|
||||
touch /var/lib/sovran-factory-sealed
|
||||
chattr +i /var/lib/sovran-factory-sealed 2>/dev/null || true
|
||||
|
||||
# ── 8. Remove onboarded flag so onboarding runs fresh ─────────
|
||||
rm -f /var/lib/sovran-customer-onboarded
|
||||
|
||||
echo "sovran-auto-seal: auto-seal complete. Continuing boot into onboarding."
|
||||
'';
|
||||
};
|
||||
|
||||
# ── Legacy security check: warn existing (pre-seal) machines ───────
|
||||
systemd.services.sovran-legacy-security-check = {
|
||||
description = "Check for legacy (pre-factory-seal) security status";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "local-fs.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
path = [ pkgs.coreutils pkgs.openssl ];
|
||||
script = ''
|
||||
# If sealed AND onboarded — fully clean, nothing to do
|
||||
[ -f /var/lib/sovran-factory-sealed ] && [ -f /var/lib/sovran-customer-onboarded ] && exit 0
|
||||
|
||||
# If sealed but not yet onboarded — seal was run, customer hasn't finished setup yet, that's fine
|
||||
[ -f /var/lib/sovran-factory-sealed ] && exit 0
|
||||
|
||||
# If onboarded but NOT sealed — installer ran without factory seal!
|
||||
if [ -f /var/lib/sovran-customer-onboarded ] && [ ! -f /var/lib/sovran-factory-sealed ]; then
|
||||
mkdir -p /var/lib/sovran
|
||||
echo "unsealed" > /var/lib/sovran/security-status
|
||||
cat > /var/lib/sovran/security-warning << 'EOF'
|
||||
This machine was set up without the factory seal process. Factory test data — including SSH keys, database contents, and wallet information — may still be present on this system. It is strongly recommended to back up any important data and re-install using a fresh ISO, or contact Sovran Systems support for assistance.
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# If the user completed Hub onboarding, they've addressed security
|
||||
[ -f /var/lib/sovran/onboarding-complete ] && exit 0
|
||||
|
||||
# If the free password has been changed from ALL known factory defaults, no warning needed
|
||||
if [ -f /etc/shadow ]; then
|
||||
FREE_HASH=$(grep '^free:' /etc/shadow | cut -d: -f2)
|
||||
if [ -n "$FREE_HASH" ] && [ "$FREE_HASH" != "!" ] && [ "$FREE_HASH" != "*" ]; then
|
||||
ALGO_ID=$(printf '%s' "$FREE_HASH" | cut -d'$' -f2)
|
||||
SALT=$(printf '%s' "$FREE_HASH" | cut -d'$' -f3)
|
||||
STILL_DEFAULT=false
|
||||
# If the salt field starts with "rounds=", we cannot extract the real salt
|
||||
# with a simple cut — treat as still-default for safety
|
||||
if printf '%s' "$SALT" | grep -q '^rounds='; then
|
||||
STILL_DEFAULT=true
|
||||
else
|
||||
for DEFAULT_PW in "free" "gosovransystems"; do
|
||||
case "$ALGO_ID" in
|
||||
6) EXPECTED=$(openssl passwd -6 -salt "$SALT" "$DEFAULT_PW" 2>/dev/null) ;;
|
||||
5) EXPECTED=$(openssl passwd -5 -salt "$SALT" "$DEFAULT_PW" 2>/dev/null) ;;
|
||||
*)
|
||||
# Unknown hash algorithm — treat as still-default for safety
|
||||
STILL_DEFAULT=true
|
||||
break
|
||||
;;
|
||||
esac
|
||||
if [ -n "$EXPECTED" ] && [ "$EXPECTED" = "$FREE_HASH" ]; then
|
||||
STILL_DEFAULT=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if [ "$STILL_DEFAULT" = "false" ]; then
|
||||
# Password was changed — clear any legacy warning and exit
|
||||
rm -f /var/lib/sovran/security-status /var/lib/sovran/security-warning
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# No flags at all + secrets exist = legacy (pre-seal era) machine
|
||||
if [ -f /var/lib/secrets/root-password ]; then
|
||||
mkdir -p /var/lib/sovran
|
||||
echo "legacy" > /var/lib/sovran/security-status
|
||||
echo "This system was deployed before the factory seal feature. Your passwords may be known to the factory. Please change your passwords through the Sovran Hub." > /var/lib/sovran/security-warning
|
||||
fi
|
||||
'';
|
||||
};
|
||||
}
|
||||
@@ -60,6 +60,15 @@
|
||||
};
|
||||
};
|
||||
|
||||
# ── Caddy customisation ───────────────────────────────────
|
||||
caddy = {
|
||||
extraVirtualHosts = lib.mkOption {
|
||||
type = lib.types.lines;
|
||||
default = "";
|
||||
description = "Additional raw Caddyfile blocks appended to the generated Caddy config. Use this in custom.nix to add custom domains and reverse proxies.";
|
||||
};
|
||||
};
|
||||
|
||||
# ── Domain setup registry ─────────────────────────────────
|
||||
domainRequirements = lib.mkOption {
|
||||
type = lib.types.listOf (lib.types.submodule {
|
||||
|
||||
@@ -10,7 +10,7 @@ let
|
||||
{ label = "Free Account — Username"; value = "free"; }
|
||||
{ label = "Free Account — Password"; file = "/var/lib/secrets/free-password"; }
|
||||
{ label = "Root Password"; file = "/var/lib/secrets/root-password"; }
|
||||
{ label = "SSH Local Access"; value = "ssh root@localhost / Passphrase: gosovransystems"; }
|
||||
{ label = "SSH Passphrase"; file = "/var/lib/secrets/ssh-passphrase"; }
|
||||
]; }
|
||||
]
|
||||
# ── Infrastructure — Caddy + Tor (NOT desktop-only) ────────
|
||||
|
||||
@@ -12,9 +12,29 @@ lib.mkIf userExists {
|
||||
"d /home/${userName}/.ssh 0700 ${userName} users -"
|
||||
];
|
||||
|
||||
systemd.services.ssh-passphrase-setup = {
|
||||
description = "Generate per-device SSH key passphrase";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
before = [ "factory-ssh-keygen.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
path = [ pkgs.pwgen pkgs.coreutils ];
|
||||
script = ''
|
||||
if [ ! -f "/var/lib/secrets/ssh-passphrase" ]; then
|
||||
mkdir -p /var/lib/secrets
|
||||
pwgen -s 20 1 > /var/lib/secrets/ssh-passphrase
|
||||
chmod 600 /var/lib/secrets/ssh-passphrase
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.services.factory-ssh-keygen = {
|
||||
description = "Generate factory SSH key for ${userName} if missing";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "ssh-passphrase-setup.service" ];
|
||||
requires = [ "ssh-passphrase-setup.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
@@ -22,7 +42,8 @@ lib.mkIf userExists {
|
||||
path = [ pkgs.openssh pkgs.coreutils ];
|
||||
script = ''
|
||||
if [ ! -f "${keyPath}" ]; then
|
||||
ssh-keygen -q -N "gosovransystems" -t ed25519 -f "${keyPath}"
|
||||
PASSPHRASE=$(cat /var/lib/secrets/ssh-passphrase)
|
||||
ssh-keygen -q -N "$PASSPHRASE" -t ed25519 -f "${keyPath}"
|
||||
chown ${userName}:users "${keyPath}" "${keyPath}.pub"
|
||||
chmod 600 "${keyPath}"
|
||||
chmod 644 "${keyPath}.pub"
|
||||
|
||||
@@ -1,444 +0,0 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
fonts = pkgs.liberation_ttf;
|
||||
|
||||
# ── Helper: change 'free' password and save it ─────────────
|
||||
change-free-password = pkgs.writeShellScriptBin "change-free-password" ''
|
||||
set -euo pipefail
|
||||
SECRET_FILE="/var/lib/secrets/free-password"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Error: must be run as root (use sudo)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -n "New password for free: "
|
||||
read -rs NEW_PASS
|
||||
echo
|
||||
echo -n "Confirm password: "
|
||||
read -rs CONFIRM
|
||||
echo
|
||||
|
||||
if [ "$NEW_PASS" != "$CONFIRM" ]; then
|
||||
echo "Passwords do not match." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$NEW_PASS" ]; then
|
||||
echo "Password cannot be empty." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "free:$NEW_PASS" | ${pkgs.shadow}/bin/chpasswd
|
||||
mkdir -p /var/lib/secrets
|
||||
echo "$NEW_PASS" > "$SECRET_FILE"
|
||||
chmod 600 "$SECRET_FILE"
|
||||
echo "Password for 'free' updated and saved."
|
||||
'';
|
||||
in
|
||||
{
|
||||
# ── Make helper available system-wide ───────────────────────
|
||||
environment.systemPackages = [ change-free-password ];
|
||||
|
||||
# ── Shell aliases: intercept 'passwd free' ─────────────────
|
||||
programs.bash.interactiveShellInit = ''
|
||||
passwd() {
|
||||
if [ "$1" = "free" ]; then
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════╗"
|
||||
echo "║ ⚠ Use 'sudo change-free-password' instead. ║"
|
||||
echo "║ ║"
|
||||
echo "║ 'passwd free' only updates /etc/shadow. ║"
|
||||
echo "║ The Hub and Magic Keys PDF will NOT be updated. ║"
|
||||
echo "╚══════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
return 1
|
||||
fi
|
||||
command passwd "$@"
|
||||
}
|
||||
'';
|
||||
|
||||
programs.fish.interactiveShellInit = ''
|
||||
function passwd --wraps passwd
|
||||
if test "$argv[1]" = "free"
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════╗"
|
||||
echo "║ ⚠ Use 'sudo change-free-password' instead. ║"
|
||||
echo "║ ║"
|
||||
echo "║ 'passwd free' only updates /etc/shadow. ║"
|
||||
echo "║ The Hub and Magic Keys PDF will NOT be updated. ║"
|
||||
echo "╚════════════════════════════<EFBFBD><EFBFBD>═════════════════════════╝"
|
||||
echo ""
|
||||
return 1
|
||||
end
|
||||
command passwd $argv
|
||||
end
|
||||
'';
|
||||
|
||||
# ── 1. Auto-Generate Root Password (Runs once) ─────────────
|
||||
systemd.services.root-password-setup = {
|
||||
description = "Generate and set a random root password";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
path = [ pkgs.pwgen pkgs.shadow pkgs.coreutils ];
|
||||
script = ''
|
||||
SECRET_FILE="/var/lib/secrets/root-password"
|
||||
if [ ! -f "$SECRET_FILE" ]; then
|
||||
mkdir -p /var/lib/secrets
|
||||
ROOT_PASS=$(pwgen -s 20 1)
|
||||
echo "root:$ROOT_PASS" | chpasswd
|
||||
echo "$ROOT_PASS" > "$SECRET_FILE"
|
||||
chmod 600 "$SECRET_FILE"
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
# ── 1b. Save 'free' password on first boot ─────────────────
|
||||
systemd.services.free-password-setup = {
|
||||
description = "Save the initial 'free' user password";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
path = [ pkgs.coreutils ];
|
||||
script = ''
|
||||
SECRET_FILE="/var/lib/secrets/free-password"
|
||||
if [ ! -f "$SECRET_FILE" ]; then
|
||||
mkdir -p /var/lib/secrets
|
||||
echo "free" > "$SECRET_FILE"
|
||||
chmod 600 "$SECRET_FILE"
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
# ── 1c. Save Zeus/lndconnect URL for hub credentials ────────
|
||||
systemd.services.zeus-connect-setup = {
|
||||
description = "Save Zeus lndconnect URL";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "lnd.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
path = [ pkgs.coreutils "/run/current-system/sw" ];
|
||||
script = ''
|
||||
SECRET_FILE="/var/lib/secrets/zeus-connect-url"
|
||||
mkdir -p /var/lib/secrets
|
||||
|
||||
URL=""
|
||||
if command -v lndconnect >/dev/null 2>&1; then
|
||||
URL=$(lndconnect --url 2>/dev/null || true)
|
||||
elif command -v lnconnect-clnrest >/dev/null 2>&1; then
|
||||
URL=$(lnconnect-clnrest --url 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
if [ -n "$URL" ]; then
|
||||
echo "$URL" > "$SECRET_FILE"
|
||||
chmod 600 "$SECRET_FILE"
|
||||
echo "Zeus connect URL saved."
|
||||
else
|
||||
echo "No lndconnect URL available yet."
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
# ── Refresh Zeus URL periodically (certs/macaroons may rotate)
|
||||
systemd.timers.zeus-connect-setup = {
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnBootSec = "2min";
|
||||
OnUnitActiveSec = "30min";
|
||||
Unit = "zeus-connect-setup.service";
|
||||
};
|
||||
};
|
||||
|
||||
# ── 2. Timer: Check every 5 minutes ────────────────────────
|
||||
systemd.timers.generate-credentials-pdf = {
|
||||
description = "Periodically check if Magic Keys PDF needs regenerating";
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnBootSec = "30s";
|
||||
OnUnitActiveSec = "5min";
|
||||
Unit = "generate-credentials-pdf.service";
|
||||
};
|
||||
};
|
||||
|
||||
# ── 3. Generate the Magic Keys PDF ─────────────────────────
|
||||
systemd.services.generate-credentials-pdf = {
|
||||
description = "Generate Magic Keys PDF for Sovran_SystemsOS";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
};
|
||||
|
||||
path = [
|
||||
pkgs.pandoc
|
||||
pkgs.typst
|
||||
pkgs.coreutils
|
||||
pkgs.qrencode
|
||||
pkgs.gnugrep
|
||||
fonts
|
||||
"/run/current-system/sw"
|
||||
];
|
||||
|
||||
environment = {
|
||||
TYPST_FONT_PATHS = "${fonts}/share/fonts";
|
||||
};
|
||||
|
||||
script = ''
|
||||
DOC_DIR="/home/free/Documents"
|
||||
OUTPUT="$DOC_DIR/Sovran_SystemsOS_Magic_Keys.pdf"
|
||||
WORK_DIR="/tmp/magic_keys_build"
|
||||
FILE="$WORK_DIR/magic_keys.md"
|
||||
HASH_FILE="/var/lib/secrets/.magic-keys-hash"
|
||||
|
||||
FENCE='```'
|
||||
|
||||
# ── Collect all secret sources into a single hash ──
|
||||
SECRET_SOURCES=""
|
||||
for f in \
|
||||
/var/lib/secrets/root-password \
|
||||
/var/lib/secrets/free-password \
|
||||
/etc/nix-bitcoin-secrets/rtl-password \
|
||||
/var/lib/tor/onion/rtl/hostname \
|
||||
/var/lib/tor/onion/electrs/hostname \
|
||||
/var/lib/tor/onion/bitcoind/hostname \
|
||||
/var/lib/secrets/matrix-users \
|
||||
/var/lib/gnome-remote-desktop/rdp-credentials \
|
||||
/var/lib/secrets/nextcloud-admin \
|
||||
/var/lib/secrets/wordpress-admin \
|
||||
/var/lib/secrets/vaultwarden/vaultwarden.env \
|
||||
/var/lib/domains/vaultwarden \
|
||||
/var/lib/domains/btcpayserver \
|
||||
/var/lib/secrets/zeus-connect-url; do
|
||||
if [ -f "$f" ]; then
|
||||
SECRET_SOURCES="$SECRET_SOURCES$(cat "$f")"
|
||||
fi
|
||||
done
|
||||
|
||||
# Add lndconnect URL to hash sources (changes if certs/macaroons rotate)
|
||||
if command -v lndconnect >/dev/null 2>&1; then
|
||||
SECRET_SOURCES="$SECRET_SOURCES$(lndconnect --url 2>/dev/null || true)"
|
||||
elif command -v lnconnect-clnrest >/dev/null 2>&1; then
|
||||
SECRET_SOURCES="$SECRET_SOURCES$(lnconnect-clnrest --url 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
CURRENT_HASH=$(echo -n "$SECRET_SOURCES" | sha256sum | cut -d' ' -f1)
|
||||
OLD_HASH=""
|
||||
if [ -f "$HASH_FILE" ]; then
|
||||
OLD_HASH=$(cat "$HASH_FILE")
|
||||
fi
|
||||
|
||||
# ── Skip if PDF exists and nothing changed ──
|
||||
if [ -f "$OUTPUT" ] && [ "$CURRENT_HASH" = "$OLD_HASH" ]; then
|
||||
echo "No changes detected, skipping PDF regeneration."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Changes detected (or PDF missing), regenerating..."
|
||||
mkdir -p "$DOC_DIR" "$WORK_DIR"
|
||||
|
||||
# ── Read secrets (default to placeholder if missing) ──
|
||||
read_secret() { if [ -f "$1" ]; then cat "$1"; else echo "$2"; fi; }
|
||||
|
||||
ROOT_PASS=$(read_secret /var/lib/secrets/root-password "Generating...")
|
||||
FREE_PASS=$(read_secret /var/lib/secrets/free-password "free")
|
||||
RTL_PASS=$(read_secret /etc/nix-bitcoin-secrets/rtl-password "Not found")
|
||||
RTL_ONION=$(read_secret /var/lib/tor/onion/rtl/hostname "Not generated yet")
|
||||
ELECTRS_ONION=$(read_secret /var/lib/tor/onion/electrs/hostname "Not generated yet")
|
||||
BITCOIN_ONION=$(read_secret /var/lib/tor/onion/bitcoind/hostname "Not generated yet")
|
||||
|
||||
# ── Generate Zeus QR code PNG if lndconnect URL is available ──
|
||||
ZEUS_URL=""
|
||||
HAS_ZEUS_QR=""
|
||||
if command -v lndconnect >/dev/null 2>&1; then
|
||||
ZEUS_URL=$(lndconnect --url 2>/dev/null || true)
|
||||
elif command -v lnconnect-clnrest >/dev/null 2>&1; then
|
||||
ZEUS_URL=$(lnconnect-clnrest --url 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
if [ -n "$ZEUS_URL" ]; then
|
||||
qrencode -o "$WORK_DIR/zeus-qr.png" -s 4 -m 1 -l H "$ZEUS_URL" 2>/dev/null && HAS_ZEUS_QR="1"
|
||||
fi
|
||||
|
||||
# ── Build the Markdown document ──
|
||||
cat > "$FILE" << ENDOFFILE
|
||||
---
|
||||
title: "Sovran SystemsOS Magic Keys"
|
||||
---
|
||||
|
||||
# Your Sovran SystemsOS Magic Keys! 🗝️
|
||||
|
||||
Welcome to your new computer! We have built a lot of cool secret forts (services) for you. To get into your forts, you need your magic keys (passwords).
|
||||
|
||||
Here are all of your keys in one place. **Keep this document safe and do not share it with strangers!**
|
||||
|
||||
> **How this document works:** This PDF is automatically generated by your computer. If any of your passwords, services, or connection details change, this document will automatically update itself within a few minutes. You can always find the latest version right here in your Documents folder. If you accidentally delete it, don't worry — your computer will recreate it for you!
|
||||
|
||||
## 🖥️ Your Computer
|
||||
These are the master keys to the actual machine.
|
||||
|
||||
### 1. Main Screen Unlock (The 'free' account)
|
||||
When you turn the computer on, it usually logs you in automatically. However, if the screen goes to sleep, or **if you enable Remote Desktop (RDP)**, you will need this to log in:
|
||||
- **Username:** \`free\`
|
||||
- **Password:** \`$FREE_PASS\`
|
||||
|
||||
🚨 **VERY IMPORTANT:** You MUST write this password down and keep it safe! If you lose it, you will be locked out of your computer!
|
||||
|
||||
### 2. The Big Boss (Root)
|
||||
Sometimes a pop-up box might ask for an Administrator (Root) password to change a setting. We created a super-secret password just for this!
|
||||
- **Root Password:** \`$ROOT_PASS\`
|
||||
|
||||
### 3. The Hacker Terminal (\`ssh root@localhost\`)
|
||||
Because your main account is so safe, you cannot just type normal commands to become the boss. If you open a black terminal box and want to make big changes, you must use your special factory key!
|
||||
|
||||
Type this exact command into the terminal:
|
||||
\`ssh root@localhost\`
|
||||
|
||||
When it asks for a passphrase, type:
|
||||
- **Terminal Password:** \`gosovransystems\`
|
||||
ENDOFFILE
|
||||
|
||||
# --- BITCOIN ECOSYSTEM ---
|
||||
if [ -f "/etc/nix-bitcoin-secrets/rtl-password" ] || [ -f "/var/lib/tor/onion/rtl/hostname" ]; then
|
||||
cat >> "$FILE" << BITCOIN
|
||||
|
||||
## ⚡ Your Bitcoin & Lightning Node
|
||||
Your computer is a real Bitcoin node! It talks to the network secretly using Tor. Here is how to connect your wallet apps to it:
|
||||
|
||||
### 1. Ride The Lightning (RTL)
|
||||
*This is the control panel for your Lightning Node.*
|
||||
Open the **Tor Browser** and go to this website. Use this password to log in:
|
||||
- **Website:** \`http://$RTL_ONION\`
|
||||
- **Password:** \`$RTL_PASS\`
|
||||
|
||||
### 2. Electrs (Your Private Bank Teller)
|
||||
*If you use a wallet app on your phone or computer (like Sparrow or BlueWallet), tell it to connect here so nobody can spy on your money!*
|
||||
- **Tor Address:** \`$ELECTRS_ONION\`
|
||||
- **Port:** \`50001\`
|
||||
|
||||
### 3. Bitcoin Core
|
||||
*This is the heartbeat of your node. It uses this address to talk to other Bitcoiners securely.*
|
||||
- **Tor Address:** \`$BITCOIN_ONION\`
|
||||
BITCOIN
|
||||
fi
|
||||
|
||||
# --- ZEUS MOBILE WALLET QR CODE ---
|
||||
if [ "$HAS_ZEUS_QR" = "1" ]; then
|
||||
echo "" >> "$FILE"
|
||||
echo "## 📱 Connect Zeus Mobile Wallet" >> "$FILE"
|
||||
echo "" >> "$FILE"
|
||||
echo "Take your Bitcoin Lightning node anywhere in the world! Scan this QR code with the **Zeus** app on your phone to instantly connect your mobile wallet to your Lightning node." >> "$FILE"
|
||||
echo "" >> "$FILE"
|
||||
echo "1. Download **Zeus** from the App Store or Google Play" >> "$FILE"
|
||||
echo "2. Open Zeus and tap **\"Scan Node Config\"**" >> "$FILE"
|
||||
echo "3. Point your phone's camera at this QR code:" >> "$FILE"
|
||||
echo "" >> "$FILE"
|
||||
echo "{ width=200px }" >> "$FILE"
|
||||
echo "" >> "$FILE"
|
||||
echo "That's it! You're now mobile. Send and receive Bitcoin anywhere in the world, powered by your very own node! ⚡" >> "$FILE"
|
||||
elif [ -n "$ZEUS_URL" ]; then
|
||||
echo "" >> "$FILE"
|
||||
echo "## 📱 Connect Zeus Mobile Wallet" >> "$FILE"
|
||||
echo "" >> "$FILE"
|
||||
echo "Take your Bitcoin Lightning node anywhere in the world! Paste this connection URL into the **Zeus** app on your phone:" >> "$FILE"
|
||||
echo "" >> "$FILE"
|
||||
echo "1. Download **Zeus** from the App Store or Google Play" >> "$FILE"
|
||||
echo "2. Open Zeus and tap **\"Scan Node Config\"** then **\"Paste Node Config\"**" >> "$FILE"
|
||||
echo "3. Paste this URL:" >> "$FILE"
|
||||
echo "" >> "$FILE"
|
||||
echo "$FENCE" >> "$FILE"
|
||||
echo "$ZEUS_URL" >> "$FILE"
|
||||
echo "$FENCE" >> "$FILE"
|
||||
echo "" >> "$FILE"
|
||||
echo "That's it! You're now mobile. Send and receive Bitcoin anywhere in the world, powered by your very own node! ⚡" >> "$FILE"
|
||||
fi
|
||||
|
||||
# --- MATRIX / ELEMENT ---
|
||||
if [ -f "/var/lib/secrets/matrix-users" ]; then
|
||||
echo "" >> "$FILE"
|
||||
echo "## 💬 Your Private Chat (Matrix / Element)" >> "$FILE"
|
||||
echo "This is your very own private messaging app! Log in using an app like Element with these details:" >> "$FILE"
|
||||
echo "$FENCE" >> "$FILE"
|
||||
cat /var/lib/secrets/matrix-users >> "$FILE"
|
||||
echo "$FENCE" >> "$FILE"
|
||||
fi
|
||||
|
||||
# --- GNOME RDP ---
|
||||
if [ -f "/var/lib/gnome-remote-desktop/rdp-credentials" ]; then
|
||||
echo "" >> "$FILE"
|
||||
echo "## 🌎 Connect from Far Away (Remote Desktop)" >> "$FILE"
|
||||
echo "This lets you control your computer screen from another device!" >> "$FILE"
|
||||
echo "$FENCE" >> "$FILE"
|
||||
cat /var/lib/gnome-remote-desktop/rdp-credentials >> "$FILE"
|
||||
echo "$FENCE" >> "$FILE"
|
||||
fi
|
||||
|
||||
# --- NEXTCLOUD ---
|
||||
if [ -f "/var/lib/secrets/nextcloud-admin" ]; then
|
||||
echo "" >> "$FILE"
|
||||
echo "## ☁️ Your Personal Cloud (Nextcloud)" >> "$FILE"
|
||||
echo "This is like your own private Google Drive!" >> "$FILE"
|
||||
echo "$FENCE" >> "$FILE"
|
||||
cat /var/lib/secrets/nextcloud-admin >> "$FILE"
|
||||
echo "$FENCE" >> "$FILE"
|
||||
fi
|
||||
|
||||
# --- WORDPRESS ---
|
||||
if [ -f "/var/lib/secrets/wordpress-admin" ]; then
|
||||
echo "" >> "$FILE"
|
||||
echo "## 📝 Your Website (WordPress)" >> "$FILE"
|
||||
echo "This is your very own website where you can write blogs or make pages." >> "$FILE"
|
||||
echo "$FENCE" >> "$FILE"
|
||||
cat /var/lib/secrets/wordpress-admin >> "$FILE"
|
||||
echo "$FENCE" >> "$FILE"
|
||||
fi
|
||||
|
||||
# --- VAULTWARDEN ---
|
||||
if [ -f "/var/lib/domains/vaultwarden" ]; then
|
||||
DOMAIN=$(cat /var/lib/domains/vaultwarden)
|
||||
VW_ADMIN_TOKEN="Not found"
|
||||
if [ -f "/var/lib/secrets/vaultwarden/vaultwarden.env" ]; then
|
||||
VW_ADMIN_TOKEN=$(grep -oP 'ADMIN_TOKEN=\K.*' /var/lib/secrets/vaultwarden/vaultwarden.env || echo "Not found")
|
||||
fi
|
||||
echo "" >> "$FILE"
|
||||
echo "## 🔐 Your Password Manager (Vaultwarden)" >> "$FILE"
|
||||
echo "This keeps all your other passwords safe! Go to this website to use it:" >> "$FILE"
|
||||
echo "- **Website:** https://$DOMAIN" >> "$FILE"
|
||||
echo "- **Admin Panel:** https://$DOMAIN/admin" >> "$FILE"
|
||||
echo "- **Admin Token:** \`$VW_ADMIN_TOKEN\`" >> "$FILE"
|
||||
echo "" >> "$FILE"
|
||||
echo "*(Create your own account on the main page. Use the Admin Token to access the admin panel and manage your server.)*" >> "$FILE"
|
||||
fi
|
||||
|
||||
# --- BTCPAY SERVER ---
|
||||
if [ -f "/var/lib/domains/btcpayserver" ]; then
|
||||
DOMAIN=$(cat /var/lib/domains/btcpayserver)
|
||||
echo "" >> "$FILE"
|
||||
echo "## ₿ Your Bitcoin Store (BTCPay Server)" >> "$FILE"
|
||||
echo "This lets you accept Bitcoin like a real shop!" >> "$FILE"
|
||||
echo "- **Website:** https://$DOMAIN" >> "$FILE"
|
||||
echo "*(You make up your own Admin Password the first time you visit!)*" >> "$FILE"
|
||||
fi
|
||||
|
||||
# ── Generate PDF (cd into work dir so Typst finds images) ──
|
||||
cd "$WORK_DIR"
|
||||
pandoc magic_keys.md -o "$OUTPUT" --pdf-engine=typst \
|
||||
-V mainfont="Liberation Sans" \
|
||||
-V monofont="Liberation Mono"
|
||||
|
||||
chown free:users "$OUTPUT"
|
||||
|
||||
# ── Save hash so we skip next time if nothing changed ──
|
||||
mkdir -p "$(dirname "$HASH_FILE")"
|
||||
echo "$CURRENT_HASH" > "$HASH_FILE"
|
||||
|
||||
rm -rf "$WORK_DIR"
|
||||
echo "PDF generated successfully."
|
||||
'';
|
||||
};
|
||||
}
|
||||
117
modules/credentials.nix
Normal file
117
modules/credentials.nix
Normal file
@@ -0,0 +1,117 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
# ── Helper: change 'free' password and save it ─────────────
|
||||
change-free-password = pkgs.writeShellScriptBin "change-free-password" ''
|
||||
set -euo pipefail
|
||||
SECRET_FILE="/var/lib/secrets/free-password"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Error: must be run as root (use sudo)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -n "New password for free: "
|
||||
read -rs NEW_PASS
|
||||
echo
|
||||
echo -n "Confirm password: "
|
||||
read -rs CONFIRM
|
||||
echo
|
||||
|
||||
if [ "$NEW_PASS" != "$CONFIRM" ]; then
|
||||
echo "Passwords do not match." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$NEW_PASS" ]; then
|
||||
echo "Password cannot be empty." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "free:$NEW_PASS" | ${pkgs.shadow}/bin/chpasswd
|
||||
mkdir -p /var/lib/secrets
|
||||
echo "$NEW_PASS" > "$SECRET_FILE"
|
||||
chmod 600 "$SECRET_FILE"
|
||||
echo "Password for 'free' updated and saved."
|
||||
'';
|
||||
in
|
||||
{
|
||||
# ── Make helper available system-wide ───────────────────────
|
||||
environment.systemPackages = [ change-free-password ];
|
||||
|
||||
# ── Shell aliases: intercept 'passwd free' ─────────────────
|
||||
programs.bash.interactiveShellInit = ''
|
||||
passwd() {
|
||||
if [ "$1" = "free" ]; then
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════╗"
|
||||
echo "║ ⚠ Use 'sudo change-free-password' instead. ║"
|
||||
echo "║ ║"
|
||||
echo "║ 'passwd free' only updates /etc/shadow. ║"
|
||||
echo "║ The Hub credentials view will NOT be updated. ║"
|
||||
echo "╚══════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
return 1
|
||||
fi
|
||||
command passwd "$@"
|
||||
}
|
||||
'';
|
||||
|
||||
programs.fish.interactiveShellInit = ''
|
||||
function passwd --wraps passwd
|
||||
if test "$argv[1]" = "free"
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════╗"
|
||||
echo "║ ⚠ Use 'sudo change-free-password' instead. ║"
|
||||
echo "║ ║"
|
||||
echo "║ 'passwd free' only updates /etc/shadow. ║"
|
||||
echo "║ The Hub credentials view will NOT be updated. ║"
|
||||
echo "╚════════════════════════════<EFBFBD><EFBFBD>═════════════════════════╝"
|
||||
echo ""
|
||||
return 1
|
||||
end
|
||||
command passwd $argv
|
||||
end
|
||||
'';
|
||||
|
||||
# ── 1. Auto-Generate Root Password (Runs once) ─────────────
|
||||
systemd.services.root-password-setup = {
|
||||
description = "Generate and set a random root password";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
path = [ pkgs.pwgen pkgs.shadow pkgs.coreutils ];
|
||||
script = ''
|
||||
SECRET_FILE="/var/lib/secrets/root-password"
|
||||
if [ ! -f "$SECRET_FILE" ]; then
|
||||
mkdir -p /var/lib/secrets
|
||||
ROOT_PASS=$(pwgen -s 20 1)
|
||||
echo "root:$ROOT_PASS" | chpasswd
|
||||
echo "$ROOT_PASS" > "$SECRET_FILE"
|
||||
chmod 600 "$SECRET_FILE"
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
# ── 1b. Save 'free' password on first boot ─────────────────
|
||||
systemd.services.free-password-setup = {
|
||||
description = "Save the initial 'free' user password";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
path = [ pkgs.coreutils ];
|
||||
script = ''
|
||||
SECRET_FILE="/var/lib/secrets/free-password"
|
||||
if [ ! -f "$SECRET_FILE" ]; then
|
||||
mkdir -p /var/lib/secrets
|
||||
echo "free" > "$SECRET_FILE"
|
||||
chmod 600 "$SECRET_FILE"
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
}
|
||||
@@ -12,11 +12,12 @@
|
||||
./core/sovran_systemsos-desktop.nix
|
||||
./core/sshd-localhost.nix
|
||||
./core/sovran-hub.nix
|
||||
./core/factory-seal.nix
|
||||
|
||||
# ── Always on (no flag) ───────────────────────────────────
|
||||
./php.nix
|
||||
./Sovran_SystemsOS_File_Fixes_And_New_Services.nix
|
||||
./credentials-pdf.nix
|
||||
./credentials.nix
|
||||
|
||||
# ── Services (default ON — disable in custom.nix) ─────────
|
||||
./synapse.nix
|
||||
|
||||
@@ -67,7 +67,7 @@ lib.mkIf config.sovran_systemsOS.services.nextcloud {
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
|
||||
path = with pkgs; [ curl unzip php pwgen coreutils ];
|
||||
path = with pkgs; [ curl unzip php pwgen coreutils shadow util-linux ];
|
||||
|
||||
script = ''
|
||||
set -euo pipefail
|
||||
@@ -109,7 +109,7 @@ lib.mkIf config.sovran_systemsOS.services.nextcloud {
|
||||
|
||||
echo "Waiting for PostgreSQL..."
|
||||
for i in $(seq 1 30); do
|
||||
if su -s /bin/sh caddy -c "php -r \"new PDO('pgsql:host=$DB_HOST;dbname=$DB_NAME', '$DB_USER', '$DB_PASS');\"" 2>/dev/null; then
|
||||
if /run/wrappers/bin/su -s /bin/sh caddy -c "php -r \"new PDO('pgsql:host=$DB_HOST;dbname=$DB_NAME', '$DB_USER', '$DB_PASS');\"" 2>/dev/null; then
|
||||
echo "Database ready."
|
||||
break
|
||||
fi
|
||||
@@ -117,7 +117,7 @@ lib.mkIf config.sovran_systemsOS.services.nextcloud {
|
||||
done
|
||||
|
||||
echo "Running Nextcloud installation..."
|
||||
su -s /bin/sh caddy -c "
|
||||
/run/wrappers/bin/su -s /bin/sh caddy -c "
|
||||
php $INSTALL_DIR/occ maintenance:install \
|
||||
--database 'pgsql' \
|
||||
--database-name '$DB_NAME' \
|
||||
@@ -129,19 +129,19 @@ lib.mkIf config.sovran_systemsOS.services.nextcloud {
|
||||
--data-dir '$DATA_DIR'
|
||||
"
|
||||
|
||||
su -s /bin/sh caddy -c "
|
||||
/run/wrappers/bin/su -s /bin/sh caddy -c "
|
||||
php $INSTALL_DIR/occ config:system:set trusted_domains 0 --value='$DOMAIN'
|
||||
php $INSTALL_DIR/occ config:system:set overwrite.cli.url --value='https://$DOMAIN'
|
||||
php $INSTALL_DIR/occ config:system:set overwriteprotocol --value='https'
|
||||
"
|
||||
|
||||
su -s /bin/sh caddy -c "
|
||||
/run/wrappers/bin/su -s /bin/sh caddy -c "
|
||||
php $INSTALL_DIR/occ config:system:set default_phone_region --value='US'
|
||||
php $INSTALL_DIR/occ config:system:set memcache.local --value='\OC\Memcache\APCu'
|
||||
php $INSTALL_DIR/occ background:cron
|
||||
"
|
||||
|
||||
su -s /bin/sh caddy -c "
|
||||
/run/wrappers/bin/su -s /bin/sh caddy -c "
|
||||
php $INSTALL_DIR/occ app:install calendar || true
|
||||
php $INSTALL_DIR/occ app:install contacts || true
|
||||
php $INSTALL_DIR/occ app:install tasks || true
|
||||
@@ -187,4 +187,4 @@ CREDS
|
||||
sovran_systemsOS.domainRequirements = [
|
||||
{ name = "nextcloud"; label = "Nextcloud"; example = "cloud.yourdomain.com"; }
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -80,4 +80,45 @@ EOF
|
||||
'';
|
||||
};
|
||||
|
||||
# ── Zeus Connect (lndconnect URL for mobile wallet) ──────────
|
||||
systemd.services.zeus-connect-setup = {
|
||||
description = "Save Zeus lndconnect URL";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "lnd.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
path = [ pkgs.coreutils "/run/current-system/sw" ];
|
||||
script = ''
|
||||
SECRET_FILE="/var/lib/secrets/zeus-connect-url"
|
||||
mkdir -p /var/lib/secrets
|
||||
|
||||
URL=""
|
||||
if command -v lndconnect >/dev/null 2>&1; then
|
||||
URL=$(lndconnect --url 2>/dev/null || true)
|
||||
elif command -v lnconnect-clnrest >/dev/null 2>&1; then
|
||||
URL=$(lnconnect-clnrest --url 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
if [ -n "$URL" ]; then
|
||||
echo "$URL" > "$SECRET_FILE"
|
||||
chmod 600 "$SECRET_FILE"
|
||||
echo "Zeus connect URL saved."
|
||||
else
|
||||
echo "No lndconnect URL available yet."
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
# ── Refresh Zeus URL periodically (certs/macaroons may rotate)
|
||||
systemd.timers.zeus-connect-setup = {
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnBootSec = "2min";
|
||||
OnUnitActiveSec = "30min";
|
||||
Unit = "zeus-connect-setup.service";
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ lib.mkIf config.sovran_systemsOS.services.wordpress {
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
|
||||
path = with pkgs; [ curl unzip wp-cli pwgen php coreutils ];
|
||||
path = with pkgs; [ curl unzip wp-cli pwgen php coreutils shadow util-linux ];
|
||||
|
||||
script = ''
|
||||
set -euo pipefail
|
||||
@@ -97,7 +97,7 @@ lib.mkIf config.sovran_systemsOS.services.wordpress {
|
||||
|
||||
echo "Generating wp-config.php..."
|
||||
cd "$INSTALL_DIR"
|
||||
su -s /bin/sh caddy -c "
|
||||
/run/wrappers/bin/su -s /bin/sh caddy -c "
|
||||
wp config create \
|
||||
--dbname='$DB_NAME' \
|
||||
--dbuser='$DB_USER' \
|
||||
@@ -108,14 +108,14 @@ lib.mkIf config.sovran_systemsOS.services.wordpress {
|
||||
|
||||
echo "Waiting for database..."
|
||||
for i in $(seq 1 30); do
|
||||
if su -s /bin/sh caddy -c "wp db check" 2>/dev/null; then
|
||||
if /run/wrappers/bin/su -s /bin/sh caddy -c "wp db check" 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "Running WordPress core install..."
|
||||
su -s /bin/sh caddy -c "
|
||||
/run/wrappers/bin/su -s /bin/sh caddy -c "
|
||||
wp core install \
|
||||
--url='https://$DOMAIN' \
|
||||
--title='Sovran_SystemsOS' \
|
||||
@@ -125,7 +125,7 @@ lib.mkIf config.sovran_systemsOS.services.wordpress {
|
||||
--skip-email
|
||||
"
|
||||
|
||||
su -s /bin/sh caddy -c "
|
||||
/run/wrappers/bin/su -s /bin/sh caddy -c "
|
||||
wp option update blogdescription 'Powered by Sovran_SystemsOS'
|
||||
wp option update permalink_structure '/%postname%/'
|
||||
wp option update default_ping_status 'closed'
|
||||
@@ -133,7 +133,7 @@ lib.mkIf config.sovran_systemsOS.services.wordpress {
|
||||
wp rewrite flush
|
||||
"
|
||||
|
||||
su -s /bin/sh caddy -c "
|
||||
/run/wrappers/bin/su -s /bin/sh caddy -c "
|
||||
wp config set DISALLOW_FILE_EDIT true --raw
|
||||
wp config set WP_AUTO_UPDATE_CORE true --raw
|
||||
wp config set FORCE_SSL_ADMIN true --raw
|
||||
|
||||
139
onboarding.html
139
onboarding.html
@@ -1,139 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Sovran_SystemsOS — First-Boot Setup</title>
|
||||
<link rel="stylesheet" href="/static/style.css?v={{ style_css_hash }}" />
|
||||
</head>
|
||||
<body class="onboarding-body">
|
||||
|
||||
<!-- Onboarding wizard container -->
|
||||
<div class="onboarding-shell">
|
||||
|
||||
<!-- Progress bar -->
|
||||
<div class="onboarding-progress-bar">
|
||||
<div class="onboarding-progress-fill" id="onboarding-progress-fill"></div>
|
||||
</div>
|
||||
|
||||
<!-- Step indicators -->
|
||||
<div class="onboarding-steps-nav" id="onboarding-steps-nav">
|
||||
<span class="onboarding-step-dot" data-step="1">1</span>
|
||||
<span class="onboarding-step-connector"></span>
|
||||
<span class="onboarding-step-dot" data-step="2">2</span>
|
||||
<span class="onboarding-step-connector"></span>
|
||||
<span class="onboarding-step-dot" data-step="3">3</span>
|
||||
<span class="onboarding-step-connector"></span>
|
||||
<span class="onboarding-step-dot" data-step="4">4</span>
|
||||
</div>
|
||||
|
||||
<!-- Step panels -->
|
||||
<div class="onboarding-panel-wrap">
|
||||
|
||||
<!-- ── Step 1: Welcome ── -->
|
||||
<div class="onboarding-panel" id="step-1">
|
||||
<div class="onboarding-hero">
|
||||
<div class="onboarding-logo">
|
||||
<img src="/static/logo-light.svg" alt="Sovran Systems" class="onboarding-logo-img" />
|
||||
</div>
|
||||
<h1 class="onboarding-title">Welcome to Sovran_SystemsOS!</h1>
|
||||
<p class="onboarding-subtitle">Be Digitally Sovereign</p>
|
||||
</div>
|
||||
<div class="onboarding-card">
|
||||
<p class="onboarding-body-text">
|
||||
Your system is installed and ready to configure. This wizard will guide
|
||||
you through the final setup steps so everything works perfectly.
|
||||
</p>
|
||||
<div class="onboarding-role-row" id="onboarding-role-row">
|
||||
<span class="onboarding-role-label">Your Role:</span>
|
||||
<span class="onboarding-role-badge" id="onboarding-role-badge">Loading…</span>
|
||||
</div>
|
||||
<p class="onboarding-body-text onboarding-body-text--dim">
|
||||
This setup only takes a few minutes. You can always revisit these
|
||||
settings from the main Hub dashboard.
|
||||
</p>
|
||||
</div>
|
||||
<div class="onboarding-footer">
|
||||
<div></div>
|
||||
<button class="btn btn-primary onboarding-btn-next" id="step-1-next">
|
||||
Let's Go →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Step 2: Domain Configuration ── -->
|
||||
<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">Domain Configuration</h2>
|
||||
<p class="onboarding-step-desc">
|
||||
Sovran_SystemsOS uses <strong><a href="https://njal.la" target="_blank" style="color: var(--accent-color);">Njal.la</a></strong> for domains and Dynamic DNS.
|
||||
First, create an account at <strong>Njal.la</strong> and purchase your domain.
|
||||
Then, in the Njal.la web interface, create a <strong>Dynamic</strong> record pointing to this machine's external IP address (shown below).
|
||||
Finally, paste the DDNS curl command from your Njal.la dashboard for each service below.
|
||||
</p>
|
||||
</div>
|
||||
<div class="onboarding-card onboarding-card--scroll" id="step-2-body">
|
||||
<p class="onboarding-loading">Loading service information…</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">
|
||||
Save & Continue →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Step 3: Port Forwarding ── -->
|
||||
<div class="onboarding-panel" id="step-3" style="display:none">
|
||||
<div class="onboarding-step-header">
|
||||
<span class="onboarding-step-icon">🔌</span>
|
||||
<h2 class="onboarding-step-title">Port Forwarding Check</h2>
|
||||
<p class="onboarding-step-desc">
|
||||
Forward these ports on your router to this machine. Each port only needs to be opened once — they are shared across all your services.
|
||||
<strong>Ports 80 and 443 must be open for SSL certificates to work.</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="onboarding-card onboarding-card--ports" id="step-3-body">
|
||||
<p class="onboarding-loading">Checking ports…</p>
|
||||
</div>
|
||||
<div class="onboarding-footer">
|
||||
<button class="btn btn-close-modal onboarding-btn-back" data-prev="2">← Back</button>
|
||||
<button class="btn btn-primary onboarding-btn-next" id="step-3-next">
|
||||
Continue →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Step 4: Complete ── -->
|
||||
<div class="onboarding-panel" id="step-4" style="display:none">
|
||||
<div class="onboarding-hero">
|
||||
<div class="onboarding-logo">✅</div>
|
||||
<h1 class="onboarding-title">Your Sovran_SystemsOS is Ready!</h1>
|
||||
<p class="onboarding-subtitle">Setup complete</p>
|
||||
</div>
|
||||
<div class="onboarding-card">
|
||||
<p class="onboarding-body-text">
|
||||
All configuration steps are done. Head to the main Hub dashboard to
|
||||
monitor your services, manage credentials, and make changes at any time.
|
||||
</p>
|
||||
<ul class="onboarding-checklist" id="onboarding-checklist">
|
||||
<li>✅ Domain configuration saved</li>
|
||||
<li>✅ Port forwarding reviewed</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="onboarding-footer">
|
||||
<button class="btn btn-close-modal onboarding-btn-back" data-prev="3">← Back</button>
|
||||
<button class="btn btn-primary" id="step-4-finish">
|
||||
Go to Dashboard →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /panel-wrap -->
|
||||
</div><!-- /shell -->
|
||||
|
||||
<script src="/static/onboarding.js?v={{ onboarding_js_hash }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user