| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156 |
- // ---------------------------------------------------------------------------
- // Folder: components/auth
- // File: LoginForm.jsx
- // Relative Path: components/auth/LoginForm.jsx
- // ---------------------------------------------------------------------------
- "use client";
- import React from "react";
- import { useRouter } from "next/navigation";
- import { login } from "@/lib/frontend/apiClient";
- import { sanitizeNext } from "@/lib/frontend/authRedirect";
- import {
- getLoginErrorMessage,
- getLoginReasonAlert,
- } from "@/lib/frontend/authMessages";
- import { homePath } from "@/lib/frontend/routes";
- import { Button } from "@/components/ui/button";
- import { Input } from "@/components/ui/input";
- import { Label } from "@/components/ui/label";
- import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
- import {
- Card,
- CardHeader,
- CardTitle,
- CardDescription,
- CardContent,
- CardFooter,
- } from "@/components/ui/card";
- /**
- * 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.
- *
- * Clean code notes:
- * - "reason" and "nextPath" parsing/sanitization is centralized in helpers.
- * - Error message mapping is centralized in lib/frontend/authMessages.js.
- *
- * @param {{ reason: string|null, nextPath: string|null }} props
- */
- export default function LoginForm({ reason, nextPath }) {
- const router = useRouter();
- // Controlled inputs: keep state explicit and predictable.
- const [username, setUsername] = React.useState("");
- const [password, setPassword] = React.useState("");
- // UX state: disable submit while in-flight.
- const [isSubmitting, setIsSubmitting] = React.useState(false);
- // UI-safe error message shown above the form.
- const [errorMessage, setErrorMessage] = React.useState("");
- // Informational banner (optional).
- const reasonAlert = getLoginReasonAlert(reason);
- // Defensive: sanitize again on the client (even though server parsing already does it).
- 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;
- if (!u || !p) {
- setErrorMessage("Please enter username and password.");
- return;
- }
- setIsSubmitting(true);
- 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));
- setIsSubmitting(false);
- }
- }
- return (
- <Card>
- <CardHeader>
- <CardTitle>Sign in</CardTitle>
- <CardDescription>
- Enter your credentials to access the delivery note browser.
- </CardDescription>
- </CardHeader>
- <CardContent className="space-y-4">
- {reasonAlert ? (
- <Alert>
- <AlertTitle>{reasonAlert.title}</AlertTitle>
- <AlertDescription>{reasonAlert.description}</AlertDescription>
- </Alert>
- ) : null}
- {errorMessage ? (
- <Alert variant="destructive">
- <AlertTitle>Login error</AlertTitle>
- <AlertDescription>{errorMessage}</AlertDescription>
- </Alert>
- ) : null}
- <form onSubmit={onSubmit} className="space-y-4">
- <div className="grid gap-2">
- <Label htmlFor="username">Username</Label>
- <Input
- id="username"
- name="username"
- autoComplete="username"
- value={username}
- onChange={(e) => setUsername(e.target.value)}
- disabled={isSubmitting}
- placeholder="e.g. branchuser"
- />
- </div>
- <div className="grid gap-2">
- <Label htmlFor="password">Password</Label>
- <Input
- id="password"
- name="password"
- type="password"
- autoComplete="current-password"
- value={password}
- onChange={(e) => setPassword(e.target.value)}
- disabled={isSubmitting}
- placeholder="••••••••"
- />
- </div>
- <CardFooter className="p-0">
- <Button type="submit" className="w-full" disabled={isSubmitting}>
- {isSubmitting ? "Signing in..." : "Sign in"}
- </Button>
- </CardFooter>
- </form>
- </CardContent>
- </Card>
- );
- }
|