|
|
@@ -33,44 +33,39 @@ import {
|
|
|
/**
|
|
|
* LoginForm (RHL-020)
|
|
|
*
|
|
|
- * Responsibilities:
|
|
|
- * - Render login UI using shadcn/ui primitives.
|
|
|
- * - Show a reason banner (session expired / logged out) if present.
|
|
|
- * - Submit credentials via apiClient.login().
|
|
|
- * - On success: redirect to `nextPath` (if safe) or "/" (default).
|
|
|
- * - On failure: show a safe, user-friendly error message.
|
|
|
+ * Username casing policy:
|
|
|
+ * - Our backend stores usernames in lowercase and performs lowercase normalization.
|
|
|
+ * - To make UX consistent (and avoid "it works with BranchUser" confusion),
|
|
|
+ * we normalize the username input to lowercase in the UI as well.
|
|
|
*
|
|
|
- * Clean code notes:
|
|
|
- * - "reason" and "nextPath" parsing/sanitization is centralized in helpers.
|
|
|
- * - Error message mapping is centralized in lib/frontend/authMessages.js.
|
|
|
+ * NOTE:
|
|
|
+ * - Password is NOT normalized. It remains case-sensitive.
|
|
|
*
|
|
|
* @param {{ reason: string|null, nextPath: string|null }} props
|
|
|
*/
|
|
|
export default function LoginForm({ reason, nextPath }) {
|
|
|
const router = useRouter();
|
|
|
|
|
|
- // Controlled inputs: keep state explicit and predictable.
|
|
|
+ // Controlled inputs.
|
|
|
const [username, setUsername] = React.useState("");
|
|
|
const [password, setPassword] = React.useState("");
|
|
|
|
|
|
- // UX state: disable submit while in-flight.
|
|
|
+ // UX state.
|
|
|
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
|
|
-
|
|
|
- // UI-safe error message shown above the form.
|
|
|
const [errorMessage, setErrorMessage] = React.useState("");
|
|
|
|
|
|
- // Informational banner (optional).
|
|
|
+ // Optional informational banner (session expired / logged-out).
|
|
|
const reasonAlert = getLoginReasonAlert(reason);
|
|
|
|
|
|
- // Defensive: sanitize again on the client (even though server parsing already does it).
|
|
|
+ // Defensive: sanitize nextPath again on the client.
|
|
|
const safeNext = sanitizeNext(nextPath) || homePath();
|
|
|
|
|
|
async function onSubmit(e) {
|
|
|
e.preventDefault();
|
|
|
|
|
|
- // Minimal validation to avoid unnecessary network calls.
|
|
|
- const u = username.trim();
|
|
|
- const p = password;
|
|
|
+ // Enforce our username policy at submit time as well (defense-in-depth).
|
|
|
+ const u = username.trim().toLowerCase();
|
|
|
+ const p = password; // do NOT normalize password
|
|
|
|
|
|
if (!u || !p) {
|
|
|
setErrorMessage("Please enter username and password.");
|
|
|
@@ -81,10 +76,7 @@ export default function LoginForm({ reason, nextPath }) {
|
|
|
setErrorMessage("");
|
|
|
|
|
|
try {
|
|
|
- // Backend sets an HTTP-only cookie on success.
|
|
|
await login({ username: u, password: p });
|
|
|
-
|
|
|
- // Replace history entry so "Back" does not return to login.
|
|
|
router.replace(safeNext);
|
|
|
} catch (err) {
|
|
|
setErrorMessage(getLoginErrorMessage(err));
|
|
|
@@ -123,8 +115,15 @@ export default function LoginForm({ reason, nextPath }) {
|
|
|
id="username"
|
|
|
name="username"
|
|
|
autoComplete="username"
|
|
|
+ // Prevent mobile keyboards from auto-capitalizing the first character.
|
|
|
+ autoCapitalize="none"
|
|
|
+ autoCorrect="off"
|
|
|
+ spellCheck={false}
|
|
|
value={username}
|
|
|
- onChange={(e) => setUsername(e.target.value)}
|
|
|
+ onChange={(e) => {
|
|
|
+ // Normalize to lowercase as the user types (consistent UX).
|
|
|
+ setUsername(e.target.value.toLowerCase());
|
|
|
+ }}
|
|
|
disabled={isSubmitting}
|
|
|
placeholder="e.g. branchuser"
|
|
|
/>
|