LoginForm.jsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. // ---------------------------------------------------------------------------
  2. // Folder: components/auth
  3. // File: LoginForm.jsx
  4. // Relative Path: components/auth/LoginForm.jsx
  5. // ---------------------------------------------------------------------------
  6. "use client";
  7. import React from "react";
  8. import { useRouter } from "next/navigation";
  9. import { login } from "@/lib/frontend/apiClient";
  10. import { sanitizeNext } from "@/lib/frontend/authRedirect";
  11. import {
  12. getLoginErrorMessage,
  13. getLoginReasonAlert,
  14. } from "@/lib/frontend/authMessages";
  15. import { homePath } from "@/lib/frontend/routes";
  16. import { Button } from "@/components/ui/button";
  17. import { Input } from "@/components/ui/input";
  18. import { Label } from "@/components/ui/label";
  19. import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
  20. import {
  21. Card,
  22. CardHeader,
  23. CardTitle,
  24. CardDescription,
  25. CardContent,
  26. CardFooter,
  27. } from "@/components/ui/card";
  28. /**
  29. * LoginForm (RHL-020)
  30. *
  31. * Username casing policy:
  32. * - Our backend stores usernames in lowercase and performs lowercase normalization.
  33. * - To make UX consistent (and avoid "it works with BranchUser" confusion),
  34. * we normalize the username input to lowercase in the UI as well.
  35. *
  36. * NOTE:
  37. * - Password is NOT normalized. It remains case-sensitive.
  38. *
  39. * @param {{ reason: string|null, nextPath: string|null }} props
  40. */
  41. export default function LoginForm({ reason, nextPath }) {
  42. const router = useRouter();
  43. // Controlled inputs.
  44. const [username, setUsername] = React.useState("");
  45. const [password, setPassword] = React.useState("");
  46. // UX state.
  47. const [isSubmitting, setIsSubmitting] = React.useState(false);
  48. const [errorMessage, setErrorMessage] = React.useState("");
  49. // Optional informational banner (session expired / logged-out).
  50. const reasonAlert = getLoginReasonAlert(reason);
  51. // Defensive: sanitize nextPath again on the client.
  52. const safeNext = sanitizeNext(nextPath) || homePath();
  53. async function onSubmit(e) {
  54. e.preventDefault();
  55. // Enforce our username policy at submit time as well (defense-in-depth).
  56. const u = username.trim().toLowerCase();
  57. const p = password; // do NOT normalize password
  58. if (!u || !p) {
  59. setErrorMessage("Please enter username and password.");
  60. return;
  61. }
  62. setIsSubmitting(true);
  63. setErrorMessage("");
  64. try {
  65. await login({ username: u, password: p });
  66. router.replace(safeNext);
  67. } catch (err) {
  68. setErrorMessage(getLoginErrorMessage(err));
  69. setIsSubmitting(false);
  70. }
  71. }
  72. return (
  73. <Card>
  74. <CardHeader>
  75. <CardTitle>Sign in</CardTitle>
  76. <CardDescription>
  77. Enter your credentials to access the delivery note browser.
  78. </CardDescription>
  79. </CardHeader>
  80. <CardContent className="space-y-4">
  81. {reasonAlert ? (
  82. <Alert>
  83. <AlertTitle>{reasonAlert.title}</AlertTitle>
  84. <AlertDescription>{reasonAlert.description}</AlertDescription>
  85. </Alert>
  86. ) : null}
  87. {errorMessage ? (
  88. <Alert variant="destructive">
  89. <AlertTitle>Login error</AlertTitle>
  90. <AlertDescription>{errorMessage}</AlertDescription>
  91. </Alert>
  92. ) : null}
  93. <form onSubmit={onSubmit} className="space-y-4">
  94. <div className="grid gap-2">
  95. <Label htmlFor="username">Username</Label>
  96. <Input
  97. id="username"
  98. name="username"
  99. autoComplete="username"
  100. // Prevent mobile keyboards from auto-capitalizing the first character.
  101. autoCapitalize="none"
  102. autoCorrect="off"
  103. spellCheck={false}
  104. value={username}
  105. onChange={(e) => {
  106. // Normalize to lowercase as the user types (consistent UX).
  107. setUsername(e.target.value.toLowerCase());
  108. }}
  109. disabled={isSubmitting}
  110. placeholder="e.g. branchuser"
  111. />
  112. </div>
  113. <div className="grid gap-2">
  114. <Label htmlFor="password">Password</Label>
  115. <Input
  116. id="password"
  117. name="password"
  118. type="password"
  119. autoComplete="current-password"
  120. value={password}
  121. onChange={(e) => setPassword(e.target.value)}
  122. disabled={isSubmitting}
  123. placeholder="••••••••"
  124. />
  125. </div>
  126. <CardFooter className="p-0">
  127. <Button type="submit" className="w-full" disabled={isSubmitting}>
  128. {isSubmitting ? "Signing in..." : "Sign in"}
  129. </Button>
  130. </CardFooter>
  131. </form>
  132. </CardContent>
  133. </Card>
  134. );
  135. }