782e4c545e
The dashboard pages (index.html, login.html) were fetching tailwindcss
and htmx.org from cdn.tailwindcss.com and unpkg.com at runtime. That
leaks browser request metadata to third parties, makes the dashboard
inoperable on air-gapped deployments, and ties dashboard availability
to two SaaS CDNs the operator doesn't control.
Both files are now embedded in the hbbs binary (include_bytes!) and
served from /admin/assets/{tailwindcss.js,htmx.min.js}. Versions
pinned in source: Tailwind 3.4.16 (Play CDN JIT, the same JS the
<script src="cdn.tailwindcss.com"> tag was previously loading) and
htmx.org 1.9.10. To upgrade either: re-fetch the file at the same
path and rebuild hbbs.
Asset routes are unauthenticated so the login page can load them,
and served with Cache-Control: public, max-age=31536000, immutable
since version bumps roll with binary upgrades anyway.
Bundle size impact: +500 KB in the hbbs binary, fully cached on the
client after first load.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
102 lines
4.2 KiB
HTML
102 lines
4.2 KiB
HTML
<!doctype html>
|
|
<html lang="en" class="h-full">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>Sign in — RustDesk Admin</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<script src="/admin/assets/tailwindcss.js"></script>
|
|
<script src="/admin/assets/htmx.min.js"></script>
|
|
<style>
|
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; }
|
|
</style>
|
|
</head>
|
|
<body class="h-full bg-slate-950 text-slate-100 flex items-center justify-center">
|
|
<main class="w-full max-w-sm px-6">
|
|
<div class="text-center mb-8">
|
|
<h1 class="text-2xl font-semibold">RustDesk Admin</h1>
|
|
<p class="text-slate-400 text-sm mt-1">Sign in to manage the server.</p>
|
|
</div>
|
|
|
|
<form
|
|
class="space-y-4 bg-slate-900 border border-slate-800 rounded-lg p-6 shadow-xl"
|
|
hx-post="/admin/login"
|
|
hx-target="#err"
|
|
hx-swap="innerHTML"
|
|
hx-on::after-request="
|
|
const xhr = event.detail.xhr;
|
|
if (event.detail.successful && (xhr.responseText || '').trim() === '') {
|
|
/* Empty 2xx body = real login. The TOTP-required path returns 2xx
|
|
with an HTML prompt fragment, which we MUST NOT redirect away
|
|
from. */
|
|
window.location.href = '/admin/';
|
|
}
|
|
"
|
|
>
|
|
<div>
|
|
<label class="block text-xs font-medium text-slate-400 mb-1" for="username">Username</label>
|
|
<input
|
|
id="username" name="username" type="text" required autocomplete="username"
|
|
class="w-full bg-slate-800 border border-slate-700 rounded px-3 py-2 text-sm focus:outline-none focus:border-sky-500"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-medium text-slate-400 mb-1" for="password">Password</label>
|
|
<input
|
|
id="password" name="password" type="password" required autocomplete="current-password"
|
|
class="w-full bg-slate-800 border border-slate-700 rounded px-3 py-2 text-sm focus:outline-none focus:border-sky-500"
|
|
/>
|
|
</div>
|
|
|
|
<div id="tfa-section" class="hidden">
|
|
<label class="block text-xs font-medium text-slate-400 mb-1" for="tfaCode">6-digit TOTP code</label>
|
|
<input
|
|
id="tfaCode" name="tfaCode" type="text" inputmode="numeric" pattern="[0-9]{6}" maxlength="6" autocomplete="one-time-code"
|
|
class="w-full bg-slate-800 border border-slate-700 rounded px-3 py-2 text-sm tracking-widest text-center focus:outline-none focus:border-sky-500"
|
|
/>
|
|
<input id="secret" name="secret" type="hidden" />
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
class="w-full bg-sky-600 hover:bg-sky-500 text-white text-sm font-medium rounded px-4 py-2 transition"
|
|
>
|
|
Sign in
|
|
</button>
|
|
|
|
<div id="err" class="text-sm text-rose-400 min-h-[1.25em]"></div>
|
|
</form>
|
|
|
|
<!-- OIDC providers (rendered only when /admin/oidc/providers is non-empty) -->
|
|
<div id="oidc-block" class="mt-6 hidden">
|
|
<div class="flex items-center gap-3 mb-3">
|
|
<div class="flex-1 h-px bg-slate-800"></div>
|
|
<span class="text-xs text-slate-500">or</span>
|
|
<div class="flex-1 h-px bg-slate-800"></div>
|
|
</div>
|
|
<div id="oidc-buttons" class="space-y-2"></div>
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
// Fetch enabled providers and render one button each. The button just
|
|
// navigates to /admin/login/oidc/<name>, which 302s the browser to the
|
|
// IdP. After the IdP redirects to /oidc/callback, the server sets our
|
|
// session cookie and redirects to /admin/.
|
|
fetch('/admin/oidc/providers').then(r => r.json()).then(list => {
|
|
if (!Array.isArray(list) || list.length === 0) return;
|
|
const block = document.getElementById('oidc-block');
|
|
const root = document.getElementById('oidc-buttons');
|
|
list.forEach(p => {
|
|
const a = document.createElement('a');
|
|
a.href = '/admin/login/oidc/' + encodeURIComponent(p.name);
|
|
a.className = 'block w-full text-center bg-slate-800 hover:bg-slate-700 border border-slate-700 text-sm rounded px-4 py-2 transition';
|
|
a.textContent = 'Sign in with ' + (p.display_name || p.name);
|
|
root.appendChild(a);
|
|
});
|
|
block.classList.remove('hidden');
|
|
}).catch(() => { /* silently hide block on any error */ });
|
|
</script>
|
|
</body>
|
|
</html>
|