feat(auth): add autocomplete attributes, password validation, and error resetting
All checks were successful
Deploy Waveform to VPS / deploy (push) Successful in 4m25s

This commit is contained in:
Chneemann 2026-10-12 10:34:48 +02:00
parent e150c819ca
commit 5a387609b4
No known key found for this signature in database
2 changed files with 51 additions and 40 deletions

View file

@ -1,6 +1,6 @@
/** /**
* @file app/(auth)/login/page.tsx * @file app/(auth)/login/page.tsx
* @description Client component rendering the login page, handling form submission, authentication requests, error messaging, and redirection. * @description Client component rendering the login page, handling form submission, authentication requests, error messaging, autocomplete, and redirection.
*/ */
"use client"; "use client";
@ -11,11 +11,7 @@ import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { loginAsGuest, loginUser } from "@/lib/services/auth.service"; import { loginAsGuest, loginUser } from "@/lib/services/auth.service";
/** /** Renders the user login interface and manages authentication form state. */
* Renders the user login interface and manages authentication form state.
*
* @returns {JSX.Element} The rendered login page component.
*/
export default function LoginPage() { export default function LoginPage() {
const router = useRouter(); const router = useRouter();
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -23,12 +19,15 @@ export default function LoginPage() {
"credentials" | "guest" | null "credentials" | "guest" | null
>(null); >(null);
/** /** Clears active error state when user modifies input fields. */
* Form submit handler extracting credentials from FormData and delegating to handleSignIn. const handleInputChange = () => {
* if (error) {
* @param {React.SubmitEvent<HTMLFormElement>} e - The form submission event. setError(null);
*/ }
const handleSubmit = async (e: React.SubmitEvent<HTMLFormElement>) => { };
/** Form submit handler extracting credentials from FormData and delegating to handleSignIn. */
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
const formData = new FormData(e.currentTarget); const formData = new FormData(e.currentTarget);
const email = formData.get("email") as string; const email = formData.get("email") as string;
@ -36,10 +35,7 @@ export default function LoginPage() {
await handleSignIn(email, password, "credentials"); await handleSignIn(email, password, "credentials");
}; };
/** /** Authenticates the user with provided credentials and redirects upon success. */
* Authenticates the user with provided credentials and redirects upon success.
*
*/
const handleSignIn = async ( const handleSignIn = async (
email: string, email: string,
password: string, password: string,
@ -58,11 +54,7 @@ export default function LoginPage() {
} }
}; };
/** /** Triggers secure guest authentication via the backend service. */
* Triggers secure guest authentication via the backend service.
*
* @async
*/
const handleGuestLogin = async () => { const handleGuestLogin = async () => {
setError(null); setError(null);
setLoadingType("guest"); setLoadingType("guest");
@ -106,7 +98,11 @@ export default function LoginPage() {
)} )}
{/* Form */} {/* Form */}
<form onSubmit={handleSubmit} className="space-y-4"> <form
onSubmit={handleSubmit}
onChange={handleInputChange}
className="space-y-4"
>
<div> <div>
<label <label
htmlFor="email" htmlFor="email"
@ -121,6 +117,7 @@ export default function LoginPage() {
required required
placeholder="name@example.com" placeholder="name@example.com"
maxLength={255} maxLength={255}
autoComplete="email"
className="w-full bg-neutral-900 border border-neutral-800 rounded-lg px-3 py-2 text-foreground placeholder-neutral-500 outline-none focus:border-accent transition-colors text-sm" className="w-full bg-neutral-900 border border-neutral-800 rounded-lg px-3 py-2 text-foreground placeholder-neutral-500 outline-none focus:border-accent transition-colors text-sm"
/> />
</div> </div>
@ -139,6 +136,7 @@ export default function LoginPage() {
required required
placeholder="•••••••••••••" placeholder="•••••••••••••"
maxLength={72} maxLength={72}
autoComplete="current-password"
className="w-full bg-neutral-900 border border-neutral-800 rounded-lg px-3 py-2 text-foreground placeholder-neutral-500 outline-none focus:border-accent transition-colors text-sm" className="w-full bg-neutral-900 border border-neutral-800 rounded-lg px-3 py-2 text-foreground placeholder-neutral-500 outline-none focus:border-accent transition-colors text-sm"
/> />
</div> </div>
@ -148,7 +146,7 @@ export default function LoginPage() {
disabled={loadingType !== null} disabled={loadingType !== null}
className="w-full bg-accent hover:opacity-90 transition-opacity text-white font-medium py-2.5 rounded-lg text-sm disabled:opacity-50 mt-2 cursor-pointer" className="w-full bg-accent hover:opacity-90 transition-opacity text-white font-medium py-2.5 rounded-lg text-sm disabled:opacity-50 mt-2 cursor-pointer"
> >
{loadingType === "credentials" ? "Signing in..." : "Sign In"}{" "} {loadingType === "credentials" ? "Signing in..." : "Sign In"}
</button> </button>
</form> </form>

View file

@ -1,6 +1,6 @@
/** /**
* @file app/(auth)/register/page.tsx * @file app/(auth)/register/page.tsx
* @description Client component rendering the registration page, handling form submissions, account creation logic, error handling, and navigation. * @description Client component rendering the registration page, handling form submissions, client-side validation, error handling, autocomplete, and navigation.
*/ */
"use client"; "use client";
@ -11,31 +11,35 @@ import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { registerUser } from "@/lib/services/auth.service"; import { registerUser } from "@/lib/services/auth.service";
/** /** Renders the user registration interface and manages sign-up form state. */
* Renders the user registration interface and manages sign-up form state.
*
* @returns {JSX.Element} The rendered registration page component.
*/
export default function RegisterPage() { export default function RegisterPage() {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const router = useRouter(); const router = useRouter();
/** /** Clears active error state when user modifies input fields. */
* Handles user registration form submission, validates form data via auth service, const handleInputChange = () => {
* and navigates to summary page on successful registration. if (error) {
* setError(null);
* @param {React.SubmitEvent<HTMLFormElement>} event - The form submission event. }
* @returns {Promise<void>} };
*/
async function handleSubmit(event: React.SubmitEvent<HTMLFormElement>) { /** Handles user registration form submission, validates form data via auth service, and navigates to summary page on successful registration. */
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault(); event.preventDefault();
setError(null); setError(null);
setLoading(true);
const formData = new FormData(event.currentTarget); const formData = new FormData(event.currentTarget);
const payload = Object.fromEntries(formData.entries()); const payload = Object.fromEntries(formData.entries());
// Client-side password matching validation
if (payload.password !== payload.confirmPassword) {
setError("Passwords do not match.");
return;
}
setLoading(true);
const result = await registerUser(payload); const result = await registerUser(payload);
if (result.error) { if (result.error) {
@ -75,7 +79,11 @@ export default function RegisterPage() {
)} )}
{/* Form */} {/* Form */}
<form onSubmit={handleSubmit} className="space-y-4"> <form
onSubmit={handleSubmit}
onChange={handleInputChange}
className="space-y-4"
>
<div> <div>
<label <label
htmlFor="username" htmlFor="username"
@ -90,6 +98,7 @@ export default function RegisterPage() {
required required
placeholder="username" placeholder="username"
maxLength={50} maxLength={50}
autoComplete="username"
className="w-full bg-neutral-900 border border-neutral-800 rounded-lg px-3 py-2 text-foreground placeholder-neutral-500 outline-none focus:border-accent transition-colors text-sm" className="w-full bg-neutral-900 border border-neutral-800 rounded-lg px-3 py-2 text-foreground placeholder-neutral-500 outline-none focus:border-accent transition-colors text-sm"
/> />
</div> </div>
@ -108,6 +117,7 @@ export default function RegisterPage() {
required required
placeholder="name@example.com" placeholder="name@example.com"
maxLength={255} maxLength={255}
autoComplete="email"
className="w-full bg-neutral-900 border border-neutral-800 rounded-lg px-3 py-2 text-foreground placeholder-neutral-500 outline-none focus:border-accent transition-colors text-sm" className="w-full bg-neutral-900 border border-neutral-800 rounded-lg px-3 py-2 text-foreground placeholder-neutral-500 outline-none focus:border-accent transition-colors text-sm"
/> />
</div> </div>
@ -126,12 +136,14 @@ export default function RegisterPage() {
required required
placeholder="•••••••••••••" placeholder="•••••••••••••"
maxLength={72} maxLength={72}
autoComplete="new-password"
className="w-full bg-neutral-900 border border-neutral-800 rounded-lg px-3 py-2 text-foreground placeholder-neutral-500 outline-none focus:border-accent transition-colors text-sm" className="w-full bg-neutral-900 border border-neutral-800 rounded-lg px-3 py-2 text-foreground placeholder-neutral-500 outline-none focus:border-accent transition-colors text-sm"
/> />
</div> </div>
<div> <div>
<label <label
htmlFor="password" htmlFor="confirmPassword"
className="block text-xs font-semibold uppercase tracking-wider text-neutral-400 mb-2" className="block text-xs font-semibold uppercase tracking-wider text-neutral-400 mb-2"
> >
Confirm Password Confirm Password
@ -143,6 +155,7 @@ export default function RegisterPage() {
required required
placeholder="•••••••••••••" placeholder="•••••••••••••"
maxLength={72} maxLength={72}
autoComplete="new-password"
className="w-full bg-neutral-900 border border-neutral-800 rounded-lg px-3 py-2 text-foreground placeholder-neutral-500 outline-none focus:border-accent transition-colors text-sm" className="w-full bg-neutral-900 border border-neutral-800 rounded-lg px-3 py-2 text-foreground placeholder-neutral-500 outline-none focus:border-accent transition-colors text-sm"
/> />
</div> </div>