From c6034de16afc6920c38cc2837d71d90b1299303b Mon Sep 17 00:00:00 2001 From: sianida26 Date: Wed, 14 Feb 2024 09:56:37 +0700 Subject: [PATCH] optimize auth --- bun.lockb | Bin 199949 -> 201003 bytes package.json | 118 +++++++++++---------- prisma/schema.prisma | 49 ++++----- prisma/seeds/userSeed.ts | 2 +- src/app/(auth)/login/layout.tsx | 5 +- src/app/(auth)/login/page.tsx | 6 +- src/app/(auth)/logout/page.tsx | 19 ++-- src/app/(auth)/register/layout.tsx | 6 +- src/app/(auth)/register/page.tsx | 82 +++++++------- src/app/globals.css | 4 +- src/core/db/index.ts | 15 +++ src/core/error/BaseError.ts | 31 ++++++ src/modules/auth/actions/createUser.ts | 105 ++++++++++++++++++ src/modules/auth/actions/getUser.ts | 39 +++++++ src/modules/auth/actions/guestOnly.ts | 12 +++ src/modules/auth/actions/logout.ts | 16 +++ src/modules/auth/actions/signIn.ts | 99 +++++++++++++++++ src/modules/auth/authConfig.ts | 5 + src/modules/auth/error/AuthError.ts | 28 +++++ src/modules/auth/types/UserClaims.d.ts | 7 ++ src/modules/auth/utils/comparePassword.ts | 17 +++ src/modules/auth/utils/createJwtToken.ts | 20 ++++ src/modules/auth/utils/decodeJwtToken.ts | 21 ++++ src/modules/auth/utils/getUserFromToken.ts | 34 ++++++ src/modules/auth/utils/hashPassword.ts | 14 +++ 25 files changed, 609 insertions(+), 145 deletions(-) create mode 100644 src/core/db/index.ts create mode 100644 src/core/error/BaseError.ts create mode 100644 src/modules/auth/actions/createUser.ts create mode 100644 src/modules/auth/actions/getUser.ts create mode 100644 src/modules/auth/actions/guestOnly.ts create mode 100644 src/modules/auth/actions/logout.ts create mode 100644 src/modules/auth/actions/signIn.ts create mode 100644 src/modules/auth/authConfig.ts create mode 100644 src/modules/auth/error/AuthError.ts create mode 100644 src/modules/auth/types/UserClaims.d.ts create mode 100644 src/modules/auth/utils/comparePassword.ts create mode 100644 src/modules/auth/utils/createJwtToken.ts create mode 100644 src/modules/auth/utils/decodeJwtToken.ts create mode 100644 src/modules/auth/utils/getUserFromToken.ts create mode 100644 src/modules/auth/utils/hashPassword.ts diff --git a/bun.lockb b/bun.lockb index ac23f82fff0b48bb7373ee4ea8a81d5fcb43c716..5122c5db0d8dadd45239159d7ca96727e2c9110d 100755 GIT binary patch delta 33063 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 J=~HUe{{fC@1yBG0 diff --git a/package.json b/package.json index 3be7249..737d1ea 100644 --- a/package.json +++ b/package.json @@ -1,60 +1,62 @@ { - "name": "dashboard-template", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint" - }, - "prisma": { - "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts" - }, - "dependencies": { - "@auth/prisma-adapter": "^1.1.0", - "@mantine/core": "^7.5.0", - "@mantine/form": "^7.5.0", - "@mantine/hooks": "^7.5.0", - "@mantine/notifications": "^7.5.0", - "@prisma/client": "5.8.1", - "@tanstack/react-query": "^4.36.1", - "@tanstack/react-query-devtools": "^4.36.1", - "@tanstack/react-table": "^8.11.7", - "@trpc/client": "^10.45.0", - "@trpc/next": "^10.45.0", - "@trpc/react-query": "^10.45.0", - "@trpc/server": "^10.45.0", - "@types/bcrypt": "^5.0.2", - "@types/jsonwebtoken": "^9.0.5", - "@typescript-eslint/eslint-plugin": "^6.19.1", - "bcrypt": "^5.1.1", - "client-only": "^0.0.1", - "clsx": "^2.1.0", - "jsonwebtoken": "^9.0.2", - "mantine-form-zod-resolver": "^1.1.0", - "next": "14.1.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-icons": "^5.0.1", - "sass": "^1.70.0", - "server-only": "^0.0.1", - "superjson": "^2.2.1", - "ts-node": "^10.9.2", - "zod": "^3.22.4" - }, - "devDependencies": { - "@types/node": "^20.11.7", - "@types/react": "^18.2.48", - "@types/react-dom": "^18.2.18", - "autoprefixer": "^10.4.17", - "eslint": "^8.56.0", - "eslint-config-next": "14.0.4", - "postcss": "^8.4.33", - "postcss-preset-mantine": "^1.12.3", - "postcss-simple-vars": "^7.0.1", - "prisma": "^5.8.1", - "tailwindcss": "^3.4.1", - "typescript": "^5.3.3" - } + "name": "dashboard-template", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "key:generate": "bun run src/core/utils/generateJwtSecret.ts" + }, + "prisma": { + "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts" + }, + "dependencies": { + "@auth/prisma-adapter": "^1.1.0", + "@mantine/core": "^7.5.2", + "@mantine/form": "^7.5.2", + "@mantine/hooks": "^7.5.2", + "@mantine/notifications": "^7.5.2", + "@prisma/client": "5.8.1", + "@tanstack/react-query": "^4.36.1", + "@tanstack/react-query-devtools": "^4.36.1", + "@tanstack/react-table": "^8.11.7", + "@trpc/client": "^10.45.0", + "@trpc/next": "^10.45.0", + "@trpc/react-query": "^10.45.0", + "@trpc/server": "^10.45.0", + "@types/bcrypt": "^5.0.2", + "@types/jsonwebtoken": "^9.0.5", + "@typescript-eslint/eslint-plugin": "^6.19.1", + "bcrypt": "^5.1.1", + "client-only": "^0.0.1", + "clsx": "^2.1.0", + "jsonwebtoken": "^9.0.2", + "mantine-form-zod-resolver": "^1.1.0", + "next": "14.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-icons": "^5.0.1", + "sass": "^1.70.0", + "server-only": "^0.0.1", + "superjson": "^2.2.1", + "ts-node": "^10.9.2", + "zod": "^3.22.4" + }, + "devDependencies": { + "@types/bun": "^1.0.5", + "@types/node": "^20.11.7", + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "autoprefixer": "^10.4.17", + "eslint": "^8.56.0", + "eslint-config-next": "14.0.4", + "postcss": "^8.4.33", + "postcss-preset-mantine": "^1.12.3", + "postcss-simple-vars": "^7.0.1", + "prisma": "^5.8.1", + "tailwindcss": "^3.4.1", + "typescript": "^5.3.3" + } } \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index fc0f2a2..5a80f1a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,6 +1,3 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - generator client { provider = "prisma-client-js" } @@ -11,39 +8,39 @@ datasource db { } model User { - id String @id @default(cuid()) + id String @id @default(cuid()) name String? - email String? @unique + email String? @unique emailVerified DateTime? passwordHash String? photoProfile UserPhotoProfiles? - roles Role[] - directPermissions Permission[] + directPermissions Permission[] @relation("PermissionToUser") + roles Role[] @relation("RoleToUser") } model UserPhotoProfiles { - id String @id @default(cuid()) - userId String @unique - path String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + userId String @unique + path String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) } model Role { - id String @id @default(cuid()) - code String @unique - name String - description String @default("") - isActive Boolean @default(false) - users User[] - permissions Permission[] + id String @id @default(cuid()) + code String @unique + name String + description String @default("") + isActive Boolean @default(false) + permissions Permission[] @relation("PermissionToRole") + users User[] @relation("RoleToUser") } model Permission { - id String @id @default(cuid()) - code String @unique - name String - description String @default("") - isActive Boolean @default(false) - roles Role[] - directUsers User[] -} \ No newline at end of file + id String @id @default(cuid()) + code String @unique + name String + description String @default("") + isActive Boolean @default(false) + roles Role[] @relation("PermissionToRole") + directUsers User[] @relation("PermissionToUser") +} diff --git a/prisma/seeds/userSeed.ts b/prisma/seeds/userSeed.ts index b18b605..4a4fbb0 100644 --- a/prisma/seeds/userSeed.ts +++ b/prisma/seeds/userSeed.ts @@ -1,4 +1,4 @@ -import hashPassword from "../../src/features/auth/tools/hashPassword"; +import hashPassword from "../../src/modules/auth/utils/hashPassword"; import { User, PrismaClient, Prisma } from "@prisma/client"; import { DefaultArgs } from "@prisma/client/runtime/library"; import { log } from "console"; diff --git a/src/app/(auth)/login/layout.tsx b/src/app/(auth)/login/layout.tsx index 1c704d6..c7b4606 100644 --- a/src/app/(auth)/login/layout.tsx +++ b/src/app/(auth)/login/layout.tsx @@ -1,7 +1,6 @@ +import guestOnly from "@/modules/auth/actions/guestOnly"; import React from "react"; -import guestOnly from "@/features/auth/actions/guestOnly"; - interface Props { children: React.ReactNode; } @@ -11,4 +10,4 @@ export default async function LoginLayout({ children }: Props) { await guestOnly() return <>{children}; -} +} \ No newline at end of file diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index 197026f..5941cd0 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -1,7 +1,5 @@ "use client"; - -import getUser from "@/features/auth/actions/getUser"; -import signIn from "@/features/auth/actions/signIn"; +import signIn from "@/modules/auth/actions/signIn"; import { Paper, PasswordInput, @@ -40,7 +38,7 @@ export default function LoginPage() { variant="filled" color="pink" title="" - // icon={icon} + // icon={icon} > {state.errors.message} diff --git a/src/app/(auth)/logout/page.tsx b/src/app/(auth)/logout/page.tsx index a918218..6971ebb 100644 --- a/src/app/(auth)/logout/page.tsx +++ b/src/app/(auth)/logout/page.tsx @@ -1,21 +1,18 @@ -"use client" -import getUser from "@//features/auth/actions/getUser"; -import logout from "@/features/auth/actions/logout"; -import { redirect } from "next/navigation"; -import React, { useEffect } from "react"; +"use client"; + +import logout from "@/modules/auth/actions/logout"; +import { useEffect } from "react"; /** * LogoutPage component handles the logout process. * It checks if a user is logged in, logs them out, and redirects to the login page. */ export default function LogoutPage() { + useEffect(() => { + const logoutAction = async () => await logout(); - useEffect(() => { - - const logoutAction = async () => await logout() - - logoutAction() - }, []) + logoutAction(); + }, []); return
; } diff --git a/src/app/(auth)/register/layout.tsx b/src/app/(auth)/register/layout.tsx index 5a0925c..c26712e 100644 --- a/src/app/(auth)/register/layout.tsx +++ b/src/app/(auth)/register/layout.tsx @@ -1,14 +1,12 @@ +import guestOnly from "@/modules/auth/actions/guestOnly"; import React from "react"; -import guestOnly from "@/features/auth/actions/guestOnly"; - interface Props { children: React.ReactNode; } export default async function RegisterLayout({ children }: Props) { - - await guestOnly() + await guestOnly(); return <>{children}; } diff --git a/src/app/(auth)/register/page.tsx b/src/app/(auth)/register/page.tsx index 2424684..f0544db 100644 --- a/src/app/(auth)/register/page.tsx +++ b/src/app/(auth)/register/page.tsx @@ -1,6 +1,6 @@ "use client"; -import createUser from "@/features/auth/actions/createUser"; +import createUser from "@/modules/auth/actions/createUser"; import { Paper, PasswordInput, @@ -9,63 +9,69 @@ import { TextInput, Group, Anchor, - Button, + Button, } from "@mantine/core"; import { useForm } from "@mantine/form"; import React, { useEffect, useState } from "react"; export interface RegisterFormSchema { - email: string, - password: string, - passwordConfirmation: string, - name: string, + email: string; + password: string; + passwordConfirmation: string; + name: string; } export default function RegisterPage() { - - const [errorMessage, setErrorMessage] = useState("") + const [errorMessage, setErrorMessage] = useState(""); const form = useForm({ initialValues: { email: "", password: "", - passwordConfirmation: "", - name: "" + passwordConfirmation: "", + name: "", + }, + 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", + passwordConfirmation: (value: string, values: RegisterFormSchema) => + value === values.password ? null : "Passwords should match", + name: (value: string) => + value.length > 0 ? null : "Name is required", }, - 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'), - 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) - }); + Object.entries(values).forEach(([key, value]) => { + formData.append(key, value); + }); const response = await createUser(formData); - if (!response.success){ + 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] + 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}) + }, + {} as { [k: string]: string } + ); - form.setErrors(errors) - console.log(form.errors) + form.setErrors(errors); + console.log(form.errors); } else { - form.clearErrors() + form.clearErrors(); } } - } + }; return (
@@ -73,9 +79,11 @@ export default function RegisterPage() { Register -
handleSubmit(values))}> + handleSubmit(values))} + > - - toggle()} size="xs" - href="/login" + href="/login" > Already have an account? Login - + diff --git a/src/app/globals.css b/src/app/globals.css index bd6213e..7d76737 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,3 +1,5 @@ -@tailwind base; +@layer tailwind{ + @tailwind base; +} @tailwind components; @tailwind utilities; \ No newline at end of file diff --git a/src/core/db/index.ts b/src/core/db/index.ts new file mode 100644 index 0000000..0273c94 --- /dev/null +++ b/src/core/db/index.ts @@ -0,0 +1,15 @@ +import { PrismaClient } from "@prisma/client"; + +const prismaClientSingleton = () => { + return new PrismaClient(); +}; + +declare global { + var prisma: undefined | ReturnType; +} + +const prisma = globalThis.prisma ?? prismaClientSingleton(); + +export default prisma; + +if (process.env.NODE_ENV !== "production") globalThis.prisma = prisma; diff --git a/src/core/error/BaseError.ts b/src/core/error/BaseError.ts new file mode 100644 index 0000000..9c82306 --- /dev/null +++ b/src/core/error/BaseError.ts @@ -0,0 +1,31 @@ +export const BaseErrorCodes = ["UNKOWN_ERROR"] as const; + +interface ErrorOptions { + message?: string; + errorCode: (typeof BaseErrorCodes)[number] | (string & {}); +} + +class BaseError extends Error { + public readonly errorCode: (typeof BaseErrorCodes)[number] | (string & {}); + + constructor(options: ErrorOptions) { + super(options.message ?? "Undetermined Error"); + this.errorCode = options.errorCode ?? "UNKOWN_ERROR"; + + Object.setPrototypeOf(this, new.target.prototype); + + console.error("error:", options) + } + + getActionResponseObject() { + return { + success: false, + error: { + message: this.message, + errorCode: this.errorCode, + }, + } as const; + } +} + +export default BaseError; diff --git a/src/modules/auth/actions/createUser.ts b/src/modules/auth/actions/createUser.ts new file mode 100644 index 0000000..2df4d5d --- /dev/null +++ b/src/modules/auth/actions/createUser.ts @@ -0,0 +1,105 @@ +"use server"; +import { z } from "zod"; +import prisma from "@/core/db"; +import { cookies } from "next/headers"; +import { redirect } from "next/navigation"; +import { hashPassword } from "../utils/hashPassword"; +import { createJwtToken } from "../utils/createJwtToken"; + +/** + * Interface for the schema of a new user. + */ +interface CreateUserSchema { + name: string; + email: string; + password: string; +} + +/** + * 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, + }, + }; + } + + const existingUser = await prisma.user.findUnique({ + where: { email: validatedFields.data.email }, + }); + + if (existingUser) { + return { + success: false, + error: { + message: "", + errors: { + email: ["Email already exists"], + }, + }, + }; + } + + 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); + } catch (e: unknown) { + // Handle unexpected errors + console.error(e); + //@ts-ignore + console.log(e.message); + return { + success: false, + error: { + message: + "An unexpected error occurred on the server. Please try again or contact the administrator.", + }, + }; + } + + redirect("/dashboard"); +} diff --git a/src/modules/auth/actions/getUser.ts b/src/modules/auth/actions/getUser.ts new file mode 100644 index 0000000..6708827 --- /dev/null +++ b/src/modules/auth/actions/getUser.ts @@ -0,0 +1,39 @@ +"use server"; + +import { cookies } from "next/headers"; +import "server-only"; +import getUserFromToken from "../utils/getUserFromToken"; +import AuthError from "../error/AuthError"; + +/** + * Retrieves the user details based on the JWT token from cookies. + * This function is designed to be used in a server-side context within a Next.js application. + * It attempts to parse the user's token, fetch the user's details, and format the response. + * If the token is invalid or the user cannot be found, it gracefully handles these cases. + * + * @returns A promise that resolves to the user's details object or null if the user cannot be authenticated or an error occurs. + * @throws an error if an unexpected error occurs during execution. + */ +export default async function getUser() { + try { + const token = cookies().get("token"); + + if (!token) return null; + + const user = await getUserFromToken(token.value); + + if (!user) return null; + + return { + name: user.name ?? "", + email: user.email ?? "", + photoUrl: user.photoProfile?.path ?? null, + }; + } catch (e: unknown) { + // Handle specific authentication errors gracefully + if (e instanceof AuthError && e.errorCode === "INVALID_JWT_TOKEN") { + return null; + } + throw e; + } +} diff --git a/src/modules/auth/actions/guestOnly.ts b/src/modules/auth/actions/guestOnly.ts new file mode 100644 index 0000000..86d5011 --- /dev/null +++ b/src/modules/auth/actions/guestOnly.ts @@ -0,0 +1,12 @@ +"use server"; + +import { redirect } from "next/navigation"; +import getUser from "./getUser"; + +export default async function guestOnly() { + const user = await getUser(); + + if (user) { + redirect("dashboard"); + } +} diff --git a/src/modules/auth/actions/logout.ts b/src/modules/auth/actions/logout.ts new file mode 100644 index 0000000..96cd9f8 --- /dev/null +++ b/src/modules/auth/actions/logout.ts @@ -0,0 +1,16 @@ +"use server"; + +import { cookies } from "next/headers"; +import { redirect } from "next/navigation"; +import "server-only"; + +/** + * Handles user logout by deleting the authentication token and redirecting to the login page. + * This function is intended to be used on the server side. + * + * @returns A promise that resolves when the logout process is complete. + */ +export default async function logout() { + cookies().delete("token"); + redirect("/login"); +} diff --git a/src/modules/auth/actions/signIn.ts b/src/modules/auth/actions/signIn.ts new file mode 100644 index 0000000..0b85a4c --- /dev/null +++ b/src/modules/auth/actions/signIn.ts @@ -0,0 +1,99 @@ +"use server"; +import prisma from "@/core/db"; +import { User } from "@prisma/client"; +import { cookies } from "next/headers"; +import { redirect } from "next/navigation"; +import { revalidatePath } from "next/cache"; +import AuthError from "../error/AuthError"; +import comparePassword from "../utils/comparePassword"; +import { createJwtToken } from "../utils/createJwtToken"; + +/** + * Handles the sign-in process for a user. + * + * This function validates a user's credentials (email and password), checks against the database, + * and on successful validation, redirects the user to the dashboard and sets a cookie with a JWT token. + * If the validation fails at any stage, it throws a custom AuthError. + * + * @param prevState - The previous state of the application, not currently used. + * @param rawFormData - The raw form data containing the user's email and password. + * @returns A promise that resolves to a redirect to the dashboard on successful authentication, + * or an object containing error details on failure. + * @throws Specific authentication error based on the failure stage. + */ +export default async function signIn(prevState: any, rawFormData: FormData) { + //TODO: Add Throttling + //TODO: Add validation check if the user is already logged in + try { + const formData = { + email: rawFormData.get("email") as string, + password: rawFormData.get("password") as string, + }; + + // Retrieve user from the database by email + const user = await prisma.user.findUnique({ + where: { email: formData.email }, + }); + + // Throw if user not found + if (!user) + throw new AuthError({ + errorCode: "EMAIL_NOT_FOUND", + message: "Email or Password does not match", + }); + + // Throw if user has no password hash + // TODO: Add check if the user uses another provider + if (!user.passwordHash) + throw new AuthError({ errorCode: "EMPTY_USER_HASH" }); + + // Compare the provided password with the user's stored password hash + const isMatch = await comparePassword( + formData.password, + user.passwordHash + ); + if (!isMatch) + throw new AuthError({ + errorCode: "INVALID_CREDENTIALS", + message: "Email or Password does not match", + }); + + //Set cookie + //TODO: Auth: Add expiry + const token = createJwtToken({ id: user.id }); + + cookies().set("token", token); + } catch (e: unknown) { + // Custom error handling for authentication errors + if (e instanceof AuthError) { + // Specific error handling for known authentication errors + switch (e.errorCode) { + case "EMAIL_NOT_FOUND": + case "INVALID_CREDENTIALS": + return { + errors: { + message: + "Email/Password combination is incorrect. Please try again.", + }, + }; + default: + // Handle other types of authentication errors + return { + errors: { + message: e.message, + }, + }; + } + } + + // Generic error handling for unexpected server errors + return { + errors: { + message: + "An unexpected error occurred on the server. Please try again or contact the administrator.", + }, + }; + } + + redirect("/dashboard"); +} diff --git a/src/modules/auth/authConfig.ts b/src/modules/auth/authConfig.ts new file mode 100644 index 0000000..ddc99f0 --- /dev/null +++ b/src/modules/auth/authConfig.ts @@ -0,0 +1,5 @@ +const authConfig = { + saltRounds: 10, +}; + +export default authConfig; diff --git a/src/modules/auth/error/AuthError.ts b/src/modules/auth/error/AuthError.ts new file mode 100644 index 0000000..beee2b2 --- /dev/null +++ b/src/modules/auth/error/AuthError.ts @@ -0,0 +1,28 @@ +import BaseError from "@/core/error/BaseError"; + +export const AuthErrorCodes = [ + "EMAIL_NOT_FOUND", + "EMPTY_USER_HASH", + "INVALID_CREDENTIALS", + "INVALID_JWT_TOKEN", + "JWT_SECRET_EMPTY", + "USER_ALREADY_EXISTS", +] as const; + +interface AuthErrorOptions { + message?: string; + errorCode: (typeof AuthErrorCodes)[number] | (string & {}); +} + +export default class AuthError extends BaseError { + errorCode: (typeof AuthErrorCodes)[number] | (string & {}); + + constructor(options: AuthErrorOptions) { + super({ + errorCode: options.errorCode, + message: options.message, + }); + + this.errorCode = options.errorCode; + } +} diff --git a/src/modules/auth/types/UserClaims.d.ts b/src/modules/auth/types/UserClaims.d.ts new file mode 100644 index 0000000..7d3e21a --- /dev/null +++ b/src/modules/auth/types/UserClaims.d.ts @@ -0,0 +1,7 @@ +import { User } from "@prisma/client"; + +type UserClaims = { + id: User["id"]; +}; + +export default UserClaims; diff --git a/src/modules/auth/utils/comparePassword.ts b/src/modules/auth/utils/comparePassword.ts new file mode 100644 index 0000000..218192e --- /dev/null +++ b/src/modules/auth/utils/comparePassword.ts @@ -0,0 +1,17 @@ +import bcrypt from "bcrypt"; + +/** + * Compares a plain text password with a hashed password. + * + * @param password - The plain text password to compare. + * @param hash - The hashed password to compare against. + * @returns True if the passwords match, false otherwise. + */ +async function comparePassword( + password: string, + hash: string +): Promise { + return bcrypt.compare(password, hash); +} + +export default comparePassword; diff --git a/src/modules/auth/utils/createJwtToken.ts b/src/modules/auth/utils/createJwtToken.ts new file mode 100644 index 0000000..abb1656 --- /dev/null +++ b/src/modules/auth/utils/createJwtToken.ts @@ -0,0 +1,20 @@ +import { SignOptions } from "jsonwebtoken"; +import UserClaims from "../types/UserClaims"; +import AuthError from "../error/AuthError"; +import jwt from "jsonwebtoken"; + +/** + * Creates a JWT token based on user claims. + * + * @param userClaims - The user claims to encode in the JWT. + * @param options - Optional signing options. + * @returns The generated JWT token. + */ +export function createJwtToken( + userClaims: UserClaims, + options?: SignOptions +): string { + const secret = process.env.JWT_SECRET; + if (!secret) throw new AuthError({ errorCode: "JWT_SECRET_EMPTY" }); + return jwt.sign(userClaims, secret, options); +} diff --git a/src/modules/auth/utils/decodeJwtToken.ts b/src/modules/auth/utils/decodeJwtToken.ts new file mode 100644 index 0000000..f2b3883 --- /dev/null +++ b/src/modules/auth/utils/decodeJwtToken.ts @@ -0,0 +1,21 @@ +import jwt, { JwtPayload } from "jsonwebtoken"; +import AuthError from "../error/AuthError"; + +/** + * Decodes a JWT token and retrieves the payload. + * + * @param token - The JWT token to decode. + * @returns The decoded payload. + */ +function decodeJwtToken(token: string): JwtPayload | string { + const secret = process.env.JWT_SECRET; + if (!secret) throw new AuthError({ errorCode: "JWT_SECRET_NOT_EMPTY" }); + + try { + return jwt.verify(token, secret) as JwtPayload; + } catch (error) { + throw new AuthError({ errorCode: "INVALID_JWT_TOKEN" }); + } +} + +export default decodeJwtToken; diff --git a/src/modules/auth/utils/getUserFromToken.ts b/src/modules/auth/utils/getUserFromToken.ts new file mode 100644 index 0000000..a29a8b7 --- /dev/null +++ b/src/modules/auth/utils/getUserFromToken.ts @@ -0,0 +1,34 @@ +import { cache } from "react"; +import decodeJwtToken from "./decodeJwtToken"; +import prisma from "@/core/db"; + +/** + * Retrieves user data from the database based on the provided JWT token. + * + * This function decodes the JWT token to extract the user ID, and then queries the database using Prisma + * to fetch the user's details, including the profile photo, roles, and direct permissions. + * + * @param token - The JWT token containing the user's ID. + * @returns The user's data if the user exists, or null if no user is found. + * Throws an error if the token is invalid or the database query fails. + */ +const getUserFromToken = cache(async (token: string) => { + // Decode the JWT token to extract the user ID + const decodedToken = decodeJwtToken(token) as { id: string; iat: number }; + + // Fetch the user from the database + const user = await prisma.user.findFirst({ + include: { + photoProfile: true, + roles: true, + directPermissions: true, + }, + where: { + id: decodedToken.id, + }, + }); + + return user; +}); + +export default getUserFromToken; diff --git a/src/modules/auth/utils/hashPassword.ts b/src/modules/auth/utils/hashPassword.ts new file mode 100644 index 0000000..3323eb6 --- /dev/null +++ b/src/modules/auth/utils/hashPassword.ts @@ -0,0 +1,14 @@ +import bcrypt from "bcrypt"; +import authConfig from "../authConfig"; + +/** + * Hashes a plain text password using bcrypt. + * + * @param password - The plain text password to hash. + * @returns The hashed password. + */ +export async function hashPassword(password: string): Promise { + return bcrypt.hash(password, authConfig.saltRounds); +} + +export default hashPassword;