diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index 03b91b0..51cdcf7 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -37,8 +37,10 @@ export default function LoginPage() { const formData = new FormData(e.currentTarget); const email = formData.get("email") as string; const password = formData.get("password") as string; + const defaultErrorMessage = "An unexpected error has occurred."; try { + // Attempt to sign in using Auth.js credentials provider const res = await signIn("credentials", { email, password, @@ -46,18 +48,26 @@ export default function LoginPage() { }); if (res?.error) { + // Handle specific authentication errors if (res.error === "CredentialsSignin") { setError("Incorrect email address or password."); } else { - setError("An unexpected error has occurred."); + setError(defaultErrorMessage); } - setLoading(false); } else { + // Redirect user and refresh router cache on success router.push("/"); router.refresh(); + return; } - } catch { - setError("Connection error. Please try again later."); + } catch (err: any) { + // Handle network or connection errors vs unexpected errors + if (err?.message?.includes("fetch") || err?.name === "TypeError") { + setError("Server error: API endpoint not available"); + } else { + setError(defaultErrorMessage); + } + } finally { setLoading(false); } }; diff --git a/app/(auth)/register/page.tsx b/app/(auth)/register/page.tsx new file mode 100644 index 0000000..46584be --- /dev/null +++ b/app/(auth)/register/page.tsx @@ -0,0 +1,131 @@ +/** + * @file RegisterPage.tsx + * @description Client component providing a user registration interface for creating a new account. + */ + +"use client"; + +import Link from "next/link"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; + +/** + * Renders the registration page featuring a sign-up form, error feedback, + * and routing logic to the login page upon successful account creation. + * + * @returns {JSX.Element} The rendered registration page component. + */ +export default function RegisterPage() { + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + const router = useRouter(); + + /** + * Handles the submission of the registration form, sends user data to the API, + * creates the account, auto-logins the user, and redirects to the main view on success. + * + * @async + * @param {React.SubmitEvent} event - The form submission event. + * @returns {Promise} + */ + async function handleSubmit(event: React.SubmitEvent) { + event.preventDefault(); + setError(null); + setLoading(true); + + const formData = new FormData(event.currentTarget); + const email = formData.get("email"); + const password = formData.get("password"); + + try { + // Send registration request to the API + const response = await fetch("/api/auth/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }), + }); + + // Ensure the response is valid JSON before parsing + const contentType = response.headers.get("content-type"); + let data; + if (contentType && contentType.includes("application/json")) { + data = await response.json(); + } else { + throw new Error("Server error: API endpoint not available"); + } + + // Handle non-successful status codes + if (!response.ok) { + throw new Error(data.message || "Something went wrong."); + } + + // Redirect user and refresh router cache on success + router.push("/"); + router.refresh(); + } catch (err: any) { + setError(err.message); + } finally { + setLoading(false); + } + } + + return ( +
+ {/* Login Card Container */} +
+ {/* Header Section */} +
+

+ Create an Account +

+

+ Get started with your Flowstate Workspace. +

+
+ + {/* Error Notification Banner */} + {error && ( +
+ {error} +
+ )} + + {/* Credentials Form */} +
+ + + +
+ + {/* Back Link Footer */} +
+ Already have an account?{" "} + + Sign In + +
+
+
+ ); +} diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts new file mode 100644 index 0000000..1859e0f --- /dev/null +++ b/app/api/auth/register/route.ts @@ -0,0 +1,109 @@ +/** + * @file route.ts + * @description API route handler for user registration, managing email normalization, credential validation, secure password hashing, and auto sign-in. + */ + +import { NextResponse } from "next/server"; +import { db } from "@/db"; +import { usersTable } from "@/db/schema"; +import { eq } from "drizzle-orm"; +import bcrypt from "bcryptjs"; +import { signIn } from "@/auth"; + +/** + * Handles POST requests to register a new user. + * Parses and validates the request body, normalizes the email address, enforces security rules, + * checks for existing records, stores the hashed password in the database, and attempts an automatic sign-in. + * + * @async + * @param {Request} request - The incoming HTTP request containing the registration data. + * @returns {Promise} A JSON response with status details indicating success or failure. + */ +export async function POST(request: Request) { + try { + // Parse incoming JSON body safely + const body = await request.json().catch(() => null); + + if (!body) { + return NextResponse.json( + { message: "Invalid JSON payload" }, + { status: 400 }, + ); + } + + const { email: rawEmail, password } = body; + + // Ensure types and presence of required fields + if ( + !rawEmail || + typeof rawEmail !== "string" || + !password || + typeof password !== "string" + ) { + return NextResponse.json( + { message: "Email and password are required" }, + { status: 400 }, + ); + } + + // Normalize email (lowercase and trim) + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + const email = rawEmail.toLowerCase().trim(); + + if (!emailRegex.test(email)) { + return NextResponse.json( + { message: "Please provide a valid email address" }, + { status: 400 }, + ); + } + + // Enforce minimum password length security constraint + if (password.length < 8) { + return NextResponse.json( + { message: "Password must be at least 8 characters long" }, + { status: 400 }, + ); + } + + // Check if user with this email already exists + const existing = await db.query.usersTable.findFirst({ + where: eq(usersTable.email, email), + }); + + if (existing) { + return NextResponse.json( + { message: "Mail is already in use" }, + { status: 409 }, + ); + } + + // Hash password securely with bcrypt before storing + const hashedPassword = await bcrypt.hash(password, 10); + await db.insert(usersTable).values({ email, password: hashedPassword }); + + // Automatically authenticate the user after successful registration + const signInResult = await signIn("credentials", { + email, + password, + redirect: false, + }); + + if (signInResult?.error) { + return NextResponse.json( + { + message: + "Registration successful, but auto sign-in failed. Please log in manually.", + }, + { status: 500 }, + ); + } + + return NextResponse.json( + { message: "Registration successful" }, + { status: 201 }, + ); + } catch (error) { + console.error("Register Error:", error); + return NextResponse.json({ message: "Server error" }, { status: 500 }); + } +}