LoginForm.jsx 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  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. * Responsibilities:
  32. * - Render login UI using shadcn/ui primitives.
  33. * - Show a reason banner (session expired / logged out) if present.
  34. * - Submit credentials via apiClient.login().
  35. * - On success: redirect to `nextPath` (if safe) or "/" (default).
  36. * - On failure: show a safe, user-friendly error message.
  37. *
  38. * Clean code notes:
  39. * - "reason" and "nextPath" parsing/sanitization is centralized in helpers.
  40. * - Error message mapping is centralized in lib/frontend/authMessages.js.
  41. *
  42. * @param {{ reason: string|null, nextPath: string|null }} props
  43. */
  44. export default function LoginForm({ reason, nextPath }) {
  45. const router = useRouter();
  46. // Controlled inputs: keep state explicit and predictable.
  47. const [username, setUsername] = React.useState("");
  48. const [password, setPassword] = React.useState("");
  49. // UX state: disable submit while in-flight.
  50. const [isSubmitting, setIsSubmitting] = React.useState(false);
  51. // UI-safe error message shown above the form.
  52. const [errorMessage, setErrorMessage] = React.useState("");
  53. // Informational banner (optional).
  54. const reasonAlert = getLoginReasonAlert(reason);
  55. // Defensive: sanitize again on the client (even though server parsing already does it).
  56. const safeNext = sanitizeNext(nextPath) || homePath();
  57. async function onSubmit(e) {
  58. e.preventDefault();
  59. // Minimal validation to avoid unnecessary network calls.
  60. const u = username.trim();
  61. const p = password;
  62. if (!u || !p) {
  63. setErrorMessage("Please enter username and password.");
  64. return;
  65. }
  66. setIsSubmitting(true);
  67. setErrorMessage("");
  68. try {
  69. // Backend sets an HTTP-only cookie on success.
  70. await login({ username: u, password: p });
  71. // Replace history entry so "Back" does not return to login.
  72. router.replace(safeNext);
  73. } catch (err) {
  74. setErrorMessage(getLoginErrorMessage(err));
  75. setIsSubmitting(false);
  76. }
  77. }
  78. return (
  79. <Card>
  80. <CardHeader>
  81. <CardTitle>Sign in</CardTitle>
  82. <CardDescription>
  83. Enter your credentials to access the delivery note browser.
  84. </CardDescription>
  85. </CardHeader>
  86. <CardContent className="space-y-4">
  87. {reasonAlert ? (
  88. <Alert>
  89. <AlertTitle>{reasonAlert.title}</AlertTitle>
  90. <AlertDescription>{reasonAlert.description}</AlertDescription>
  91. </Alert>
  92. ) : null}
  93. {errorMessage ? (
  94. <Alert variant="destructive">
  95. <AlertTitle>Login error</AlertTitle>
  96. <AlertDescription>{errorMessage}</AlertDescription>
  97. </Alert>
  98. ) : null}
  99. <form onSubmit={onSubmit} className="space-y-4">
  100. <div className="grid gap-2">
  101. <Label htmlFor="username">Username</Label>
  102. <Input
  103. id="username"
  104. name="username"
  105. autoComplete="username"
  106. value={username}
  107. onChange={(e) => setUsername(e.target.value)}
  108. disabled={isSubmitting}
  109. placeholder="e.g. branchuser"
  110. />
  111. </div>
  112. <div className="grid gap-2">
  113. <Label htmlFor="password">Password</Label>
  114. <Input
  115. id="password"
  116. name="password"
  117. type="password"
  118. autoComplete="current-password"
  119. value={password}
  120. onChange={(e) => setPassword(e.target.value)}
  121. disabled={isSubmitting}
  122. placeholder="••••••••"
  123. />
  124. </div>
  125. <CardFooter className="p-0">
  126. <Button type="submit" className="w-full" disabled={isSubmitting}>
  127. {isSubmitting ? "Signing in..." : "Sign in"}
  128. </Button>
  129. </CardFooter>
  130. </form>
  131. </CardContent>
  132. </Card>
  133. );
  134. }