Jelajahi Sumber

RHL-020 feat(auth): implement LoginForm component with user authentication and error handling

Code_Uwe 1 bulan lalu
induk
melakukan
2f46f6fe21
1 mengubah file dengan 156 tambahan dan 0 penghapusan
  1. 156 0
      components/auth/LoginForm.jsx

+ 156 - 0
components/auth/LoginForm.jsx

@@ -0,0 +1,156 @@
+// ---------------------------------------------------------------------------
+// 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>
+	);
+}