Files
UmbraTrans/web/index.html
umbra audio 3419485aa7 Fix a syntax error I introduced, which put a password in a URL
The commit immediately before this one - the password-toggle fix - deleted a closing
brace and did not replace it. app.js did not parse. The browser reported it in a console
nobody was watching, and every script-driven behaviour on the sign-in page stopped.

The consequence was not a cosmetic one. The login form's submit handler, which is the
only thing calling preventDefault(), never ran, so the browser performed a native form
submission. Neither form declared a method, and a form with no method attribute submits
as GET. Both password fields went into the query string:

    GET /?email=umbra%40bdmurphy.me&password=...

The server logged it in plain text, three times, before it was noticed. It would also
have sat in browser history and in any proxy access log between here and the public
internet.

Two fixes, the second of which is the one that matters:

- The toggle fix is reapplied correctly to the last known-good version of app.js, with
  nothing else in the file touched - the diff is four lines against thirteen and two
  against one.
- **Every form on every page now declares method=post.** The script still intercepts
  and submits via fetch; this decides what happens when the script does not run, which is
  precisely the case that failed. Five more forms carried the same latent defect,
  including password reset and email verification.

test_form_safety.py checks both, and was confirmed to fail when app.js is broken the
same way. It uses esprima rather than a hand-written brace counter, which gave two false
readings during this work - one of them 'balanced' for a file that did not parse at all.
A check that can be wrong about a syntax error is worse than no check.

The account password should be treated as exposed and changed. So should any password
entered into that form while the script was broken.
2026-10-03 20:06:05 -07:00

119 lines
7.7 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="UmbraTrans workspace sign in">
<title>Sign in · UmbraTrans</title>
<link rel="icon" type="image/png" href="assets/umbratrans-emblem.png">
<link rel="stylesheet" href="assets/styles.css">
</head>
<body class="auth-page">
<main class="auth-shell">
<section class="auth-brand" aria-labelledby="brand-title">
<a class="brand-lockup" href="index.html" aria-label="UmbraTrans home">
<img class="brand-image" src="assets/umbratrans-emblem.png" alt="" aria-hidden="true">
<span>UmbraTrans</span>
</a>
<div class="brand-copy">
<p class="eyebrow">Translation production workspace</p>
<h1 id="brand-title">One source of truth for every language.</h1>
<p class="lede">Review books, translations, voices, and audio delivery from one project-aware workspace.</p>
</div>
<div class="brand-points" aria-label="Workspace capabilities">
<div><span class="point-dot"></span>Project-scoped access</div>
<div><span class="point-dot"></span>Shared speaker and voice identity</div>
<div><span class="point-dot"></span>Traceable analysis and delivery lineage</div>
</div>
<p class="prototype-note" id="service-note">Checking service status…</p>
</section>
<section class="auth-card" aria-label="Account access">
<div id="login-view">
<div class="auth-card-heading">
<span class="status-pill status-pill--info">Secure workspace</span>
<h2 id="signin-title">Sign in to your workspace</h2>
<p>Use the email address your account was issued with, and your password. Access is determined by your organization membership and role.</p>
</div>
<!-- method="post" is load-bearing, not decoration.
A form with no method attribute defaults to GET, and a GET submission puts every
named field into the query string. If the script fails to load for any reason -
a syntax error, a truncated deploy, a blocked asset - the submit handler that
calls preventDefault() never runs, and the browser performs a native GET. That
happened: a syntax error I introduced left both password fields going into
`/?email=...&password=...`, which the server logged in plain text.
With method="post" the fallback is a POST body, which does not appear in the
URL, in the request line, or in referrer headers. The JavaScript still intercepts
and submits via fetch; this is what happens when the JavaScript does not. -->
<form id="login-form" class="stack-form" method="post" action="/api/auth/login" novalidate>
<label for="login-email">Email address</label>
<input id="login-email" name="email" type="email" inputmode="email" autocomplete="username" autocapitalize="none" autocorrect="off" spellcheck="false" placeholder="you@example.com" required>
<label for="login-password">Password</label>
<div class="password-field">
<input id="login-password" name="password" type="password" autocomplete="current-password" placeholder="Enter your password" required>
<button id="toggle-login-password" class="password-toggle" type="button" aria-pressed="false" aria-controls="login-password">Show</button>
</div>
<div id="login-message" class="form-message" role="status" aria-live="polite"></div>
<button id="login-submit" class="button button--primary button--wide" type="submit">Sign in <span aria-hidden="true">→</span></button>
</form>
<div id="verify-panel" class="verify-panel" hidden>
<h3>Confirm your email address</h3>
<p id="verify-panel-text">Your password is correct, but this address has not been confirmed yet. Check your inbox for the confirmation link, or send a new one.</p>
<button id="resend-verification" class="button button--ghost" type="button">Send a new link</button>
<div id="verify-panel-message" class="form-message" role="status" aria-live="polite"></div>
</div>
<div id="setup-entry" class="auth-divider" hidden><span>First run</span></div>
<a id="setup-link" class="admin-link" href="setup.html" hidden><span>Set up this deployment</span><span aria-hidden="true">↗</span></a>
<div id="admin-entry" class="auth-divider"><span>Administrator?</span></div>
<a id="admin-link" class="admin-link" href="uadmin/"><span>Open control panel</span><span aria-hidden="true">↗</span></a>
<p class="auth-footnote">An account issued a temporary password must replace it at first sign-in before any workspace access is granted.</p>
</div>
<div id="password-view" hidden>
<div class="auth-card-heading">
<span class="status-pill status-pill--warning">Action required</span>
<h2 id="password-title">Choose a new password</h2>
<p id="password-lede">Your temporary password must be replaced before you can continue. This step is mandatory.</p>
</div>
<div class="policy-list" aria-labelledby="policy-title">
<p class="policy-title" id="policy-title">Password policy</p>
<ul>
<li id="policy-length">At least 14 characters.</li>
<li>Must not begin or end with whitespace.</li>
<li id="policy-username">Must not contain your account name.</li>
<li>Must differ from the temporary password you are replacing.</li>
</ul>
</div>
<!-- method="post" for the same reason as the login form: a native fallback
submission of a password change must never reach the query string. The action is
the real endpoint so that even the fallback is the right request. -->
<form id="password-form" class="stack-form" method="post" action="/api/auth/change-password" novalidate>
<label for="current-password">Current temporary password</label>
<div class="password-field">
<input id="current-password" name="current_password" type="password" autocomplete="current-password" required>
<button id="toggle-current-password" class="password-toggle" type="button" aria-pressed="false" aria-controls="current-password">Show</button>
</div>
<label for="new-password">New password</label>
<div class="password-field">
<input id="new-password" name="new_password" type="password" autocomplete="new-password" required>
<button id="toggle-new-password" class="password-toggle" type="button" aria-pressed="false" aria-controls="new-password">Show</button>
</div>
<label for="confirm-password">Confirm new password</label>
<input id="confirm-password" name="confirm_password" type="password" autocomplete="new-password" required>
<div id="password-message" class="form-message" role="status" aria-live="polite"></div>
<button id="password-submit" class="button button--primary button--wide" type="submit">Set new password <span aria-hidden="true">→</span></button>
<a id="password-settings" class="admin-link" href="settings.html" hidden><span>Your account settings</span><span aria-hidden="true">↗</span></a>
<button id="password-signout" class="button button--ghost button--wide" type="button">Sign out instead</button>
</form>
<p class="auth-footnote">This page cannot be left until the password has been changed. Your session stays open while you do.</p>
</div>
</section>
</main>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="assets/api.js" defer></script>
<script src="assets/app.js" defer></script>
</body>
</html>