diff --git a/src/BaseError.ts b/src/BaseError.ts index 572f552..04997a6 100644 --- a/src/BaseError.ts +++ b/src/BaseError.ts @@ -1,11 +1,17 @@ +export enum BaseErrorCodes { + INVALID_FORM_DATA = "INVALID_FORM_DATA" +} + export default class BaseError extends Error { public readonly errorCode: string; public readonly statusCode: number; + public readonly data: object; - constructor(message: string = "An unexpected error occurred", errorCode: string = "GENERIC_ERROR", statusCode: number = 500) { + constructor(message: string = "An unexpected error occurred", errorCode: string = "GENERIC_ERROR", statusCode: number = 500, data: object = {}) { super(message); // Pass message to the Error parent class this.errorCode = errorCode; this.statusCode = statusCode; + this.data = data; Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain } } diff --git a/src/app/(auth)/register/page.tsx b/src/app/(auth)/register/page.tsx index 5578d3b..2424684 100644 --- a/src/app/(auth)/register/page.tsx +++ b/src/app/(auth)/register/page.tsx @@ -1,5 +1,6 @@ "use client"; +import createUser from "@/features/auth/actions/createUser"; import { Paper, PasswordInput, @@ -11,7 +12,7 @@ import { Button, } from "@mantine/core"; import { useForm } from "@mantine/form"; -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; export interface RegisterFormSchema { email: string, @@ -22,6 +23,8 @@ export interface RegisterFormSchema { export default function RegisterPage() { + const [errorMessage, setErrorMessage] = useState("") + const form = useForm({ initialValues: { email: "", @@ -31,19 +34,46 @@ export default function RegisterPage() { }, validate: { email: (value: string) => (/^\S+@\S+$/.test(value) ? null : 'Invalid email'), - password: (value: string) => (value.length > 6 ? null : 'Password should be at least 6 characters'), + password: (value: string) => (value.length >= 6 ? null : 'Password should be at least 6 characters'), passwordConfirmation: (value: string, values: RegisterFormSchema) => value === values.password ? null : 'Passwords should match', name: (value: string) => (value.length > 0 ? null : 'Name is required'), } }); + const handleSubmit = async (values: RegisterFormSchema) => { + const formData = new FormData(); + Object.entries(values) + .forEach(([key, value]) => { + formData.append(key, value) + }); + + const response = await createUser(formData); + + if (!response.success){ + setErrorMessage(response.error.message); + + if (response.error.errors){ + const errors = Object.entries(response.error.errors) + .reduce((prev, [k,v]) => { + prev[k] = v[0] + return prev; + }, {} as {[k: string]: string}) + + form.setErrors(errors) + console.log(form.errors) + } else { + form.clearErrors() + } + } + } + return (
Register -
{})}> + handleSubmit(values))}> = {}) { + super(message, errorCode, statusCode, data); } } diff --git a/src/features/auth/actions/createUser.ts b/src/features/auth/actions/createUser.ts index 8bd29e6..5912963 100644 --- a/src/features/auth/actions/createUser.ts +++ b/src/features/auth/actions/createUser.ts @@ -1,31 +1,100 @@ -import prisma from "@/db" +"use server" +import { z } from "zod"; +import prisma from "@/db"; import AuthError, { AuthErrorCode } from "../AuthError"; -import { hashPassword } from "../authUtils"; +import BaseError, { BaseErrorCodes } from "@/BaseError"; +import { createJwtToken, hashPassword } from "../authUtils"; +import { cookies } from "next/headers"; +import { redirect } from "next/navigation"; +/** + * Interface for the schema of a new user. + */ interface CreateUserSchema { - name: string, - email: string, - plainPassword: string, + name: string; + email: string; + password: string; } -const register = async (inputData: CreateUserSchema) => { - const existingUser = await prisma.user.findUnique({ - where: { - email: inputData.email +/** + * Validation schema for creating a user. + */ +const createUserSchema = z.object({ + name: z.string(), + email: z.string().email(), + password: z.string().min(6), + passwordConfirmation: z.string().optional(), +}).refine( + (data) => data.password === data.passwordConfirmation, + { + message: "Password confirmation must match the password", + path: ["passwordConfirmation"], + } +); + +/** + * Creates a new user in the system. + * + * @param formData - The form data containing user details. + * @returns An object indicating the result of the operation. + */ +export default async function createUser(formData: FormData){ + //TODO: Add Throttling + //TODO: Add validation check if the user is already logged in + + try { + const parsedData = { + email: formData.get("email")?.toString() ?? '', + name: formData.get("name")?.toString() ?? '', + password: formData.get("password")?.toString() ?? '', + passwordConfirmation: formData.get("passwordConfirmation")?.toString() + }; + const validatedFields = createUserSchema.safeParse(parsedData); + + if (!validatedFields.success) { + return { + success: false, + error: { + message: "", + errors: validatedFields.error.flatten().fieldErrors + } + } } - }); - if (existingUser) throw new AuthError(AuthErrorCode.USER_ALREADY_EXISTS, 419, "Email already exists") + const existingUser = await prisma.user.findUnique({ + where: { email: validatedFields.data.email }, + }); - const user = await prisma.user.create({ - data: { - name: inputData.name, - email: inputData.email, - passwordHash: await hashPassword(inputData.plainPassword) + if (existingUser){ + return { + success: false, + error: { + message: "", + errors: { + email: ["Email already exists"] + } + } + } } - }); - return user; + const user = await prisma.user.create({ + data: { + name: validatedFields.data.name, + email: validatedFields.data.email, + passwordHash: await hashPassword(validatedFields.data.password), + }, + }); + + const token = createJwtToken({ id: user.id }); + cookies().set("token", token); + redirect("/dashboard"); + } catch (e: unknown) { + // Handle unexpected errors + return { + success: false, + error: { + message: "An unexpected error occurred on the server. Please try again or contact the administrator.", + }, + }; + } } - -export default register; diff --git a/src/features/auth/actions/signIn.ts b/src/features/auth/actions/signIn.ts index efb3fb0..3645aa7 100644 --- a/src/features/auth/actions/signIn.ts +++ b/src/features/auth/actions/signIn.ts @@ -5,6 +5,7 @@ import AuthError, { AuthErrorCode } from "../AuthError"; import { comparePassword, createJwtToken } from "../authUtils"; import { cookies } from "next/headers"; import { redirect } from "next/navigation"; +import BaseError from "@/BaseError"; /** * Handles the sign-in process for a user. @@ -57,7 +58,7 @@ export default async function signIn(prevState: any, rawFormData: FormData) { redirect("/dashboard"); } catch (e: unknown) { // Custom error handling for authentication errors - if (e instanceof AuthError) { + if (e instanceof BaseError) { // Specific error handling for known authentication errors switch (e.errorCode) { case AuthErrorCode.EMAIL_NOT_FOUND: