LoginForm.jsx 4.1 KB

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