e183b386a1
The login form's hx-on::after-request redirected to /admin/ on any 2xx response. The TOTP-required path also returns 2xx — with an HTML fragment that unhides the TFA section — so the redirect fired before the user ever saw the code input, locking out anyone who had enrolled TOTP. Only redirect when the 2xx body is empty (the real-login signal). When the body is non-empty it's the prompt fragment, which htmx swaps into #err and whose inline <script> reveals #tfa-section. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
72 lines
2.8 KiB
HTML
72 lines
2.8 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="https://cdn.tailwindcss.com"></script>
|
|
<script src="https://unpkg.com/htmx.org@1.9.10"></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>
|
|
</main>
|
|
</body>
|
|
</html>
|