diff --git a/bun.lockb b/bun.lockb
index ac23f82..5122c5d 100755
Binary files a/bun.lockb and b/bun.lockb differ
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
-
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;