diff --git a/.eslintrc.json b/.eslintrc.json
index b2bcd8c..3e8ce47 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -5,6 +5,7 @@
"project": "./tsconfig.json"
},
"rules": {
- "@typescript-eslint/no-floating-promises": "warn"
+ "@typescript-eslint/no-floating-promises": "warn",
+ "@typescript-eslint/no-unused-vars": "warn"
}
}
diff --git a/prisma/seeds/permissionSeed.ts b/prisma/seeds/permissionSeed.ts
index 2050aeb..167add4 100644
--- a/prisma/seeds/permissionSeed.ts
+++ b/prisma/seeds/permissionSeed.ts
@@ -94,7 +94,7 @@ export default async function permissionSeed(prisma: PrismaClient) {
isActive: true
}
];
-
+
await Promise.all(
permissionData.map(async (permission) => {
await prisma.permission.upsert({
diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx
index 5941cd0..ed42d9d 100644
--- a/src/app/(auth)/login/page.tsx
+++ b/src/app/(auth)/login/page.tsx
@@ -1,5 +1,5 @@
"use client";
-import signIn from "@/modules/auth/actions/signIn";
+import signIn from "@/modules/auth/actions/signInAction";
import {
Paper,
PasswordInput,
@@ -11,9 +11,7 @@ import {
Button,
Alert,
} from "@mantine/core";
-import { redirect } from "next/navigation";
-import { useRouter } from "next/navigation";
-import React, { useEffect, useState } from "react";
+import React from "react";
import { useFormState } from "react-dom";
const initialState = {
diff --git a/src/app/(auth)/logout/page.tsx b/src/app/(auth)/logout/page.tsx
index 6971ebb..99b7a27 100644
--- a/src/app/(auth)/logout/page.tsx
+++ b/src/app/(auth)/logout/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import logout from "@/modules/auth/actions/logout";
+import logoutAction from "@/modules/auth/actions/logoutAction";
import { useEffect } from "react";
/**
@@ -9,9 +9,9 @@ import { useEffect } from "react";
*/
export default function LogoutPage() {
useEffect(() => {
- const logoutAction = async () => await logout();
-
- logoutAction();
+ (async () => await logoutAction())()
+ .then(() => {})
+ .catch(() => {});
}, []);
return
;
diff --git a/src/app/(auth)/register/page.tsx b/src/app/(auth)/register/page.tsx
index f0544db..82450fb 100644
--- a/src/app/(auth)/register/page.tsx
+++ b/src/app/(auth)/register/page.tsx
@@ -1,6 +1,10 @@
"use client";
-import createUser from "@/modules/auth/actions/createUser";
+import createUserAction from "@/modules/auth/actions/createUserAction";
+import createUser from "@/modules/auth/actions/createUserAction";
+import { CreateUserSchema } from "@/modules/auth/formSchemas/CreateUserFormSchema";
+import DashboardError from "@/modules/dashboard/errors/DashboardError";
+import withServerAction from "@/modules/dashboard/utils/withServerAction";
import {
Paper,
PasswordInput,
@@ -12,65 +16,53 @@ import {
Button,
} from "@mantine/core";
import { useForm } from "@mantine/form";
+import { showNotification } from "@mantine/notifications";
import React, { useEffect, useState } from "react";
-export interface RegisterFormSchema {
- email: string;
- password: string;
- passwordConfirmation: string;
- name: string;
-}
-
export default function RegisterPage() {
const [errorMessage, setErrorMessage] = useState("");
- const form = useForm({
+ const form = useForm({
initialValues: {
email: "",
- password: "",
- passwordConfirmation: "",
+ plainPassword: "",
+ plainPasswordConfirmation: "",
name: "",
},
validate: {
email: (value: string) =>
/^\S+@\S+$/.test(value) ? null : "Invalid email",
- password: (value: string) =>
+ plainPassword: (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",
+ plainPasswordConfirmation: (value: string, values: CreateUserSchema) =>
+ value === values.plainPassword ? 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();
- }
- }
+ const handleSubmit = async (values: CreateUserSchema) => {
+ withServerAction(createUserAction, form.values)
+ .then((response) => {
+ showNotification({message: "Register Success", color: "green"})
+ })
+ .catch((e) => {
+ if (e instanceof DashboardError) {
+ if (e.errorCode === "INVALID_FORM_DATA") {
+ form.setErrors(e.formErrors ?? {});
+ } else {
+ setErrorMessage(`ERROR: ${e.message} (${e.errorCode})`);
+ }
+ } else if (e instanceof Error) {
+ setErrorMessage(`ERROR: ${e.message}`);
+ } else {
+ setErrorMessage(
+ `Unkown error is occured. Please contact administrator`
+ );
+ }
+ })
};
return (
@@ -102,14 +94,14 @@ export default function RegisterPage() {
placeholder="Your password"
name="password"
autoComplete="new-password"
- {...form.getInputProps("password")}
+ {...form.getInputProps("plainPassword")}
/>
diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx
index 5ca4783..e8f1116 100644
--- a/src/app/dashboard/layout.tsx
+++ b/src/app/dashboard/layout.tsx
@@ -4,7 +4,7 @@ import Image from "next/image";
import React from "react";
import logo from "@/assets/logos/logo.png";
import DashboardLayout from "@/modules/dashboard/components/DashboardLayout";
-import getUser from "@/modules/auth/actions/getUser";
+import getUser from "@/modules/auth/actions/getMyDetailAction";
import { redirect } from "next/navigation";
import { Notifications } from "@mantine/notifications";
diff --git a/src/modules/auth/actions/createUser.ts b/src/modules/auth/actions/createUser.ts
deleted file mode 100644
index 2df4d5d..0000000
--- a/src/modules/auth/actions/createUser.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-"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/createUserAction.ts b/src/modules/auth/actions/createUserAction.ts
new file mode 100644
index 0000000..6e92ab3
--- /dev/null
+++ b/src/modules/auth/actions/createUserAction.ts
@@ -0,0 +1,30 @@
+"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";
+import createUser from "../services/createUser";
+import ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction";
+import handleCatch from "@/modules/dashboard/utils/handleCatch";
+import { CreateUserSchema } from "../formSchemas/CreateUserFormSchema";
+
+/**
+ * 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 createUserAction(formData: CreateUserSchema): Promise {
+ //TODO: Add Throttling
+ //TODO: Add validation check if the user is already logged in
+
+ try {
+ const result = await createUser(formData);
+ cookies().set("token", result.token);
+ redirect("/dashboard");
+ } catch (e) {
+ return handleCatch(e)
+ }
+}
diff --git a/src/modules/auth/actions/getMyDetailAction.ts b/src/modules/auth/actions/getMyDetailAction.ts
new file mode 100644
index 0000000..66ef024
--- /dev/null
+++ b/src/modules/auth/actions/getMyDetailAction.ts
@@ -0,0 +1,47 @@
+"use server";
+
+import getMyDetail from "../services/getMyDetail";
+import AuthError from "../error/AuthError";
+import BaseError from "@/core/error/BaseError";
+import ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction";
+import handleCatch from "@/modules/dashboard/utils/handleCatch";
+import "server-only";
+import { cookies } from "next/headers";
+import getUserFromToken from "../utils/getUserFromToken";
+
+/**
+ * Asynchronously retrieves the authenticated user's details from a server-side context in a Next.js application.
+ * This function uses a JWT token obtained from cookies to authenticate the user and fetch their details.
+ * If the authentication fails due to an invalid JWT token, or if any other error occurs, the function handles these errors gracefully.
+ *
+ * @returns A promise that resolves to a `ServerResponseAction` object. This object includes a `success` flag indicating the operation's outcome, the user's details in the `data` field if successful, or an error object in the `error` field if an error occurs.
+ * @throws an unhandled error if an unexpected error occurs during the function execution.
+ */
+export default async function getMyDetailAction(): Promise>>> {
+ try {
+ const token = cookies().get("token");
+
+ // Return null if token is not present
+ if (!token) throw new AuthError({errorCode: "INVALID_JWT_TOKEN"});
+
+ // Attempt to fetch and return the user's details.
+ const userDetails = await getMyDetail(token.value);
+ return {
+ success: true,
+ data: userDetails,
+ };
+ } catch (e: unknown) {
+ // Check if the error is an instance of AuthError and handle it.
+ if (e instanceof AuthError && e.errorCode === "INVALID_JWT_TOKEN") {
+ return {
+ success: false,
+ error: new BaseError({
+ errorCode: e.errorCode,
+ message: "You are not authenticated",
+ }),
+ };
+ }
+ // Handle other types of errors.
+ return handleCatch(e);
+ }
+}
diff --git a/src/modules/auth/actions/getUser.ts b/src/modules/auth/actions/getUser.ts
deleted file mode 100644
index 7af6a47..0000000
--- a/src/modules/auth/actions/getUser.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-"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 ?? 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
index 86d5011..ad1535f 100644
--- a/src/modules/auth/actions/guestOnly.ts
+++ b/src/modules/auth/actions/guestOnly.ts
@@ -1,12 +1,25 @@
"use server";
import { redirect } from "next/navigation";
-import getUser from "./getUser";
+import getMyDetail from "../services/getMyDetail";
+import { cookies } from "next/headers";
-export default async function guestOnly() {
- const user = await getUser();
+/**
+ * Enforces a guest-only access policy by redirecting authenticated users to the dashboard.
+ * This function asynchronously checks if the user is authenticated by attempting to retrieve user details.
+ * If the user is authenticated, they are redirected to the dashboard page.
+ *
+ * @returns A promise that resolves when the operation completes. The function itself does not return a value.
+ */
+export default async function guestOnly(): Promise {
+ const token = cookies().get("token");
+ if (!token) return;
+
+ const user = await getMyDetail(token.value);
+
+ // If an authenticated user is detected, redirect them to the dashboard.
if (user) {
- redirect("dashboard");
+ redirect("/dashboard");
}
}
diff --git a/src/modules/auth/actions/logout.ts b/src/modules/auth/actions/logoutAction.ts
similarity index 100%
rename from src/modules/auth/actions/logout.ts
rename to src/modules/auth/actions/logoutAction.ts
diff --git a/src/modules/auth/actions/signIn.ts b/src/modules/auth/actions/signIn.ts
deleted file mode 100644
index 0b85a4c..0000000
--- a/src/modules/auth/actions/signIn.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-"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/actions/signInAction.ts b/src/modules/auth/actions/signInAction.ts
new file mode 100644
index 0000000..3a11733
--- /dev/null
+++ b/src/modules/auth/actions/signInAction.ts
@@ -0,0 +1,70 @@
+"use server";
+import { cookies } from "next/headers";
+import { redirect } from "next/navigation";
+import AuthError from "../error/AuthError";
+import signIn from "../services/signIn";
+
+/**
+ * Asynchronously handles the sign-in process for a user by validating their credentials against the database.
+ * Upon successful validation, the user is redirected to the dashboard, and a JWT token is set as a cookie.
+ * If validation fails, a custom `AuthError` is thrown, and detailed error information is provided to the caller.
+ *
+ * Note: Future enhancements should include throttling to prevent brute force attacks and a check to prevent
+ * sign-in attempts if the user is already logged in.
+ *
+ * @param prevState - The previous state of the application. Currently not utilized but may be used for future enhancements.
+ * @param rawFormData - The raw form data obtained from the sign-in form, containing the user's email and password.
+ * @returns A promise that, upon successful authentication, resolves to a redirection to the dashboard. If authentication fails,
+ * it resolves to an object containing error details.
+ * @throws {AuthError} - Throws a custom `AuthError` with specific error codes for different stages of the authentication failure.
+ */
+export default async function signInAction(prevState: any, rawFormData: FormData) {
+ //TODO: Add Throttling
+ //TODO: Add validation check if the user is already logged in
+ try {
+ // Extract email and password from the raw form data.
+ const formData = {
+ email: rawFormData.get("email") as string,
+ password: rawFormData.get("password") as string,
+ };
+
+ // Attempt to sign in with the provided credentials.
+ const result = await signIn(formData)
+
+ // Set the JWT token in cookies upon successful sign-in.
+ cookies().set("token", result.token);
+
+ // Redirect to the dashboard after successful sign-in.
+ redirect("/dashboard");
+ } 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.",
+ },
+ };
+ }
+}
diff --git a/src/modules/auth/contexts/AuthContext.tsx b/src/modules/auth/contexts/AuthContext.tsx
index 0875164..ada908b 100644
--- a/src/modules/auth/contexts/AuthContext.tsx
+++ b/src/modules/auth/contexts/AuthContext.tsx
@@ -1,68 +1,90 @@
+// Directive to enforce client-side operation in a Next.js application.
"use client";
-import React, {
- ReactElement,
- ReactNode,
- createContext,
- useCallback,
- useContext,
- useEffect,
- useMemo,
- useState,
-} from "react";
-import getUser from "../actions/getUser";
+// Importing React functionalities and required components.
+import React, { ReactElement, ReactNode, createContext, useCallback, useContext, useEffect, useState } from "react";
+import { notifications } from "@mantine/notifications";
+import getMyDetailAction from "../actions/getMyDetailAction";
+import withServerAction from "@/modules/dashboard/utils/withServerAction";
+
+// Defining the structure for user data within the authentication context.
interface UserData {
- name: string;
- email: string;
- photoUrl: string | null;
- // Add additional user fields as needed
+ name: string;
+ email: string;
+ photoUrl: string | null;
+ // Additional user fields can be added here.
}
+// State structure for the authentication context.
interface AuthContextState {
- user: UserData | null;
- fetchUserData: () => void;
- logout: () => void;
+ user: UserData | null;
+ fetchUserData: () => void;
+ logout: () => void;
}
+// Props type definition for the AuthContextProvider component.
interface Props {
- children: ReactNode;
+ children: ReactNode;
}
+// Creating the authentication context with an undefined initial value.
const AuthContext = createContext(undefined);
-export const AuthContextProvider = ({ children }: Props) => {
- const [user, setUser] = useState(null);
+/**
+ * Provides an authentication context to wrap around components that require authentication data.
+ * This component initializes user data state, fetches user data upon component mount, and provides
+ * a logout function to clear the user data.
+ *
+ * @param {Props} props - Component props containing children to be rendered within the provider.
+ * @returns {ReactElement} A provider component wrapping children with access to authentication context.
+ */
+export const AuthContextProvider = ({ children }: Props): ReactElement => {
+ const [user, setUser] = useState(null);
- const fetchUserData = useCallback(() => {
- const getUserData = async () => {
- const user = await getUser();
- setUser(user);
- };
+ // Function to fetch user data and update state accordingly.
+ const fetchUserData = useCallback(() => {
+ withServerAction(getMyDetailAction)
+ .then((response) => {
+ setUser(response.data);
+ })
+ .catch((error) => {
+ notifications.show({
+ title: 'Error',
+ message: 'Error while retrieving user data',
+ color: 'red',
+ });
+ console.error("Error while retrieving user data", error);
+ });
+ }, []);
- getUserData()
- .then(() => {})
- .catch(() => {});
- }, []);
+ // Fetch user data on component mount.
+ useEffect(() => {
+ fetchUserData();
+ }, [fetchUserData]);
- useEffect(() => {
- fetchUserData();
- }, [fetchUserData]);
+ // Function to clear user data, effectively logging the user out.
+ const logout = () => {
+ setUser(null);
+ };
- const logout = () => {
- setUser(null);
- };
-
- return (
-
- {children}
-
- );
+ // Providing authentication state and functions to the context consumers.
+ return (
+
+ {children}
+
+ );
};
-export const useAuth = () => {
- const context = useContext(AuthContext);
- if (!context) {
- throw new Error("useAuth must be used within an AuthContextProvider");
- }
- return context;
+/**
+ * Custom hook to consume the authentication context. This hook ensures the context is used within a provider.
+ *
+ * @returns {AuthContextState} The authentication context state including user data and auth functions.
+ * @throws {Error} Throws an error if the hook is used outside of an AuthContextProvider.
+ */
+export const useAuth = (): AuthContextState => {
+ const context = useContext(AuthContext);
+ if (!context) {
+ throw new Error("useAuth must be used within an AuthContextProvider");
+ }
+ return context;
};
diff --git a/src/modules/auth/formSchemas/CreateUserFormSchema.ts b/src/modules/auth/formSchemas/CreateUserFormSchema.ts
new file mode 100644
index 0000000..0885c2c
--- /dev/null
+++ b/src/modules/auth/formSchemas/CreateUserFormSchema.ts
@@ -0,0 +1,23 @@
+import {z} from "zod"
+
+/**
+ * Interface for the schema of a new user.
+ */
+export interface CreateUserSchema {
+ name: string;
+ email: string;
+ plainPassword: string;
+ plainPasswordConfirmation: string;
+}
+
+export 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"],
+ });
\ No newline at end of file
diff --git a/src/modules/auth/services/createUser.ts b/src/modules/auth/services/createUser.ts
new file mode 100644
index 0000000..e777464
--- /dev/null
+++ b/src/modules/auth/services/createUser.ts
@@ -0,0 +1,63 @@
+import DashboardError from "@/modules/dashboard/errors/DashboardError";
+import {
+ CreateUserSchema,
+ createUserSchema,
+} from "../formSchemas/CreateUserFormSchema";
+import mapObjectToFirstValue from "@/utils/mapObjectToFirstValue";
+import db from "@/core/db";
+import AuthError from "../error/AuthError";
+import hashPassword from "../utils/hashPassword";
+import { createJwtToken } from "../utils/createJwtToken";
+import { cookies } from "next/headers";
+
+/**
+ * Creates a new user in the database after validating the input data.
+ * It throws errors if the input data is invalid or if the user already exists.
+ * On successful creation, it returns a token for the created user.
+ *
+ * @param userData - The user data to create a new user. Must conform to CreateUserSchema.
+ * @returns An object containing the JWT token for the newly created user.
+ * @throws If the input validation fails.
+ * @throws If the user already exists in the database.
+ */
+export default async function createUser(userData: CreateUserSchema) {
+ const validatedFields = createUserSchema.safeParse(userData);
+
+ //Validate form input
+ if (!validatedFields.success) {
+ throw new DashboardError({
+ errorCode: "INVALID_FORM_DATA",
+ formErrors: mapObjectToFirstValue(
+ validatedFields.error.flatten().fieldErrors
+ ),
+ });
+ }
+
+ //Check email exists
+ if (
+ await db.user.findFirst({
+ where: { email: validatedFields.data.email },
+ })
+ ) {
+ throw new AuthError({
+ errorCode: "USER_ALREADY_EXISTS",
+ message: "This email already exists",
+ });
+ }
+
+ //Create user
+ const user = await db.user.create({
+ data: {
+ name: validatedFields.data.name,
+ email: validatedFields.data.email,
+ passwordHash: await hashPassword(validatedFields.data.password),
+ },
+ });
+
+ //Set token
+ const token = createJwtToken({ id: user.id });
+
+ return {
+ token,
+ };
+}
diff --git a/src/modules/auth/services/getMyDetail.ts b/src/modules/auth/services/getMyDetail.ts
new file mode 100644
index 0000000..4b3b9d0
--- /dev/null
+++ b/src/modules/auth/services/getMyDetail.ts
@@ -0,0 +1,30 @@
+import { cookies } from "next/headers";
+import getUserFromToken from "../utils/getUserFromToken";
+import AuthError from "../error/AuthError";
+
+/**
+ * Retrieves the details of the currently authenticated user based on the JWT token.
+ * If the token is not present or the user cannot be found, it returns null.
+ * Otherwise, it returns the user's name, email, and photo URL.
+ *
+ * @returns An object containing the user's name, email, and photo URL, or null if the user cannot be authenticated.
+ */
+export default async function getMyDetail(token?: string) {
+ if (!token)
+ throw new AuthError({
+ errorCode: "INVALID_JWT_TOKEN",
+ message: "You are not authenticated",
+ });
+
+ const user = await getUserFromToken(token);
+
+ // Return null if user is not found
+ if (!user) return null;
+
+ // Return user details
+ return {
+ name: user.name ?? "",
+ email: user.email ?? "",
+ photoUrl: user.photoProfile ?? null,
+ };
+}
diff --git a/src/modules/auth/services/signIn.ts b/src/modules/auth/services/signIn.ts
new file mode 100644
index 0000000..e62eca9
--- /dev/null
+++ b/src/modules/auth/services/signIn.ts
@@ -0,0 +1,54 @@
+import "server-only";
+import SignInFormData from "../types/SignInFormData";
+import db from "@/core/db";
+import AuthError from "../error/AuthError";
+import comparePassword from "../utils/comparePassword";
+import { createJwtToken } from "../utils/createJwtToken";
+
+/**
+ * Authenticates a user with email and password credentials.
+ *
+ * This function looks up the user in the database by email. If the user exists and the password matches
+ * the hashed password in the database, a JWT token is created and returned. If any step of this process fails,
+ * an `AuthError` with a specific error code and message is thrown.
+ *
+ * @param rawCredential - Contains the email and password provided by the user.
+ * @returns An object containing a JWT token if authentication is successful.
+ * @throws {AuthError} - Throws an `AuthError` with an appropriate error code and message for various failure scenarios.
+ */
+export default async function signIn(rawCredential: SignInFormData) {
+ const user = await db.user.findUnique({
+ where: { email: rawCredential.email },
+ });
+
+ if (!user)
+ throw new AuthError({
+ errorCode: "EMAIL_NOT_FOUND",
+ message: "Email or Password does not match",
+ });
+
+ //TODO: Add handle for empty password hash
+ // Ensure there is a password hash to compare against.
+ if (!user.passwordHash)
+ throw new AuthError({
+ errorCode: "EMPTY_USER_HASH",
+ message: "Something wrong. Please contact your administrator",
+ });
+
+ // Compare the provided password with the stored hash.
+ const isMatch = await comparePassword(
+ rawCredential.password,
+ user.passwordHash
+ );
+
+ // Create a JWT token upon successful authentication.
+ if (!isMatch)
+ throw new AuthError({
+ errorCode: "INVALID_CREDENTIALS",
+ message: "Email or Password does not match",
+ });
+
+ const token = createJwtToken({ id: user.id });
+
+ return { token };
+}
diff --git a/src/modules/auth/types/SignInFormData.d.ts b/src/modules/auth/types/SignInFormData.d.ts
new file mode 100644
index 0000000..95ae8a2
--- /dev/null
+++ b/src/modules/auth/types/SignInFormData.d.ts
@@ -0,0 +1,21 @@
+/**
+ * Defines the structure for sign-in form data.
+ *
+ * This interface is utilized to type-check the data received from the sign-in form, ensuring that it contains
+ * both an `email` and a `password` field of type `string`. The `email` field represents the user's email address,
+ * and the `password` field represents the user's password. Both fields are required for the sign-in process.
+ */
+export default interface SignInFormData {
+ /**
+ * The user's email address.
+ * Must be a valid email format.
+ */
+ email: string;
+
+ /**
+ * The user's password.
+ * There are no specific constraints defined here for the password's format or strength, but it is expected
+ * to comply with the application's password policy.
+ */
+ password: string;
+}
diff --git a/src/modules/userManagement/tables/UsersTable/UsersTable.tsx b/src/modules/userManagement/tables/UsersTable/UsersTable.tsx
index 7f44662..b947cbf 100644
--- a/src/modules/userManagement/tables/UsersTable/UsersTable.tsx
+++ b/src/modules/userManagement/tables/UsersTable/UsersTable.tsx
@@ -1,5 +1,4 @@
"use client";
-import getUser from "@/modules/auth/actions/getUser";
import CrudPermissions from "@/modules/dashboard/types/CrudPermissions";
import { Table, Text, Flex, Button, Center } from "@mantine/core";
import {