Files
remote-control-support-webapp/public/enroll.html
T
rmancinasandClaude Opus 5 cfda6b19b7 feat(install): one-command device registration for macOS, Windows and Linux
Registering a machine meant reading a multi-step page, installing Node, and
running three commands in the right order. Now it is one line per platform.

The hub serves scripts/install.sh and scripts/install.ps1 with its own address
and the enrolment token substituted in, so the published command carries
everything and there is nothing to fill in:

  curl -fsSL https://support.freakma.com/install.sh?token=TOKEN | sh
  irm https://support.freakma.com/install.ps1?token=TOKEN | iex

That output is piped straight into a shell, so the token — the only untrusted
value in either file — is refused unless it matches the base64url shape that
randomToken produces.

Each script checks for a usable runtime and stops with instructions rather than
guessing, warns when nothing is serving RFB on the loopback, installs per-user
with no root or administrator, and registers a login-scoped service: launchd on
macOS, a lingering systemd user service on Linux, a logon task on Windows. The
Windows script uses Node 22 when it is present and falls back to the bundled
executable otherwise, which is what lets one command cover both Windows 11 and
Server 2008 R2.

It registers a logon task rather than a service on purpose: services run in
session 0 and cannot draw on the interactive desktop, so the "ask first" consent
prompt would never appear.

Also adds /docs — a per-OS setup guide with service management and a
troubleshooting table — and reworks the enrolment page into OS tabs that open on
whichever platform the reader is sitting at.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 11:37:12 -07:00

195 lines
7.6 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark">
<title>Set up remote support</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<div class="bg-field" id="bg-field" aria-hidden="true"></div>
<div class="center-shell">
<div class="center-card">
<div class="card-header">
<div class="brand-mark">RC</div>
<div>
<strong style="font-size:14px">Remote Support</strong>
<span class="brand-sub">Machine enrollment</span>
</div>
</div>
<div class="card-body">
<span class="card-kicker">One-time setup</span>
<h1>Set up remote support</h1>
<div id="state-loading" class="faint">Checking this link…</div>
<div id="state-invalid" class="notice error hidden"></div>
<div id="state-ok" class="hidden">
<p class="faint">
This registers this computer so it can be supported remotely. It stays connected
until you stop the agent.
</p>
<nav class="tabs" id="os-tabs" style="margin:16px 0 4px">
<button data-os="macos" class="active">macOS</button>
<button data-os="windows">Windows</button>
<button data-os="linux">Linux</button>
</nav>
<ol class="steps os-panel" data-os="macos">
<li>
<strong>Turn on Screen Sharing</strong> — System Settings → General → Sharing.
<div class="faint" style="margin-top:6px">
Under Computer Settings, allow VNC viewers and set a password.
</div>
</li>
<li>
<strong>Install <a href="https://nodejs.org" target="_blank" rel="noopener">Node.js 22+</a></strong> if it is not already there.
</li>
<li>
<strong>Paste this into Terminal</strong>:
<pre class="code" id="cmd-macos"></pre>
<div class="row wrap" style="margin-top:10px">
<button class="small" data-copy="cmd-macos">Copy</button>
</div>
</li>
</ol>
<ol class="steps os-panel" data-os="windows" hidden>
<li>
<strong>Install TightVNC or UltraVNC</strong>, set a password, and let it register as a
service so it survives a reboot.
</li>
<li>
<strong>Paste this into PowerShell</strong>:
<pre class="code" id="cmd-windows"></pre>
<div class="row wrap" style="margin-top:10px">
<button class="small" data-copy="cmd-windows">Copy</button>
</div>
<div class="faint" style="margin-top:8px">
Nothing else to install — if Node.js is missing the script fetches a self-contained
agent instead.
</div>
</li>
</ol>
<ol class="steps os-panel" data-os="linux" hidden>
<li>
<strong>Start a VNC server on the loopback</strong>:
<pre class="code" id="cmd-linux-x11vnc"></pre>
</li>
<li>
<strong>Install <a href="https://nodejs.org" target="_blank" rel="noopener">Node.js 22+</a></strong> if it is not already there.
</li>
<li>
<strong>Paste this into a terminal</strong>:
<pre class="code" id="cmd-linux"></pre>
<div class="row wrap" style="margin-top:10px">
<button class="small" data-copy="cmd-linux">Copy</button>
</div>
</li>
</ol>
<div class="notice hidden" id="legacy-windows" style="margin-top:14px">
<strong>Windows 7 or Server 2008 R2?</strong>
<div class="faint" style="margin-top:6px">
Those have PowerShell 2.0 and no TLS 1.2, so the command above fails. Run this from
<span class="mono">cmd.exe</span> instead:
</div>
<pre class="code" id="cmd-windows-legacy"></pre>
<div class="row wrap" style="margin-top:10px">
<button class="small" data-copy="cmd-windows-legacy">Copy</button>
<a class="btn small" href="/download/agent.exe" download="rcs-agent.exe">Download the agent instead</a>
</div>
</div>
<p class="login-foot">
Link expires <span id="expiry"></span> · <a href="/docs" target="_blank" rel="noopener">full setup guide</a>
</p>
</div>
</div>
</div>
</div>
<script>
const token = location.pathname.split('/').filter(Boolean).pop();
const origin = location.origin;
const link = `${origin}/enroll/${token}`;
// The hub stamps its own address and this token into the install scripts, so
// each of these really is the whole command — nothing to fill in by hand.
const commands = {
'cmd-macos': `curl -fsSL ${origin}/install.sh?token=${token} | sh`,
'cmd-linux': `curl -fsSL ${origin}/install.sh?token=${token} | sh`,
'cmd-windows': `irm ${origin}/install.ps1?token=${token} | iex`,
'cmd-windows-legacy': `powershell -c "[Net.ServicePointManager]::SecurityProtocol=3072; (New-Object Net.WebClient).DownloadString('${origin}/install.ps1?token=${token}') | iex"`,
'cmd-linux-x11vnc': 'x11vnc -localhost -rfbport 5900 -forever -shared -passwd YOURPASSWORD',
};
for (const [id, text] of Object.entries(commands)) {
const el = document.getElementById(id);
if (el) el.textContent = text;
}
function copy(text, button) {
navigator.clipboard.writeText(text).then(() => {
const old = button.textContent;
button.textContent = 'Copied';
setTimeout(() => { button.textContent = old; }, 1500);
});
}
document.querySelectorAll('[data-copy]').forEach((button) => {
button.onclick = () => copy(commands[button.dataset.copy], button);
});
const tabs = document.getElementById('os-tabs');
const legacyNotice = document.getElementById('legacy-windows');
tabs.onclick = (event) => {
const button = event.target.closest('button[data-os]');
if (!button) return;
tabs.querySelectorAll('button').forEach((b) => b.classList.toggle('active', b === button));
document.querySelectorAll('.os-panel').forEach((panel) => {
panel.hidden = panel.dataset.os !== button.dataset.os;
});
legacyNotice.classList.toggle('hidden', button.dataset.os !== 'windows');
};
// Open on the tab for whatever this machine is — it is the machine being set up.
const guess = /Mac/i.test(navigator.platform) ? 'macos'
: /Win/i.test(navigator.platform) ? 'windows'
: /Linux/i.test(navigator.platform) ? 'linux' : null;
if (guess) tabs.querySelector(`button[data-os="${guess}"]`).click();
fetch(`/api/public/invite/${encodeURIComponent(token)}`)
.then((r) => r.json().then((body) => ({ ok: r.ok, body })))
.then(({ ok, body }) => {
document.getElementById('state-loading').classList.add('hidden');
if (!ok || !body.usable || body.kind !== 'enroll') {
const box = document.getElementById('state-invalid');
box.textContent = body.error || `This enrollment link is ${body.reason || 'not valid'}.`;
box.classList.remove('hidden');
return;
}
const expiry = document.getElementById('expiry');
expiry.textContent = body.expiresAt ? new Date(body.expiresAt).toLocaleString() : 'never';
document.getElementById('state-ok').classList.remove('hidden');
})
.catch(() => {
document.getElementById('state-loading').textContent = 'Could not reach the server.';
});
</script>
<script type="module">
import { mountParticles } from '/particles.js';
mountParticles(document.getElementById('bg-field'));
</script>
</body>
</html>