Merge branch 'development'

This commit is contained in:
sianida26 2024-02-28 11:35:38 +07:00
commit 48d079254e
21 changed files with 469 additions and 349 deletions

View File

@ -5,6 +5,7 @@
"project": "./tsconfig.json" "project": "./tsconfig.json"
}, },
"rules": { "rules": {
"@typescript-eslint/no-floating-promises": "warn" "@typescript-eslint/no-floating-promises": "warn",
"@typescript-eslint/no-unused-vars": "warn"
} }
} }

View File

@ -94,7 +94,7 @@ export default async function permissionSeed(prisma: PrismaClient) {
isActive: true isActive: true
} }
]; ];
await Promise.all( await Promise.all(
permissionData.map(async (permission) => { permissionData.map(async (permission) => {
await prisma.permission.upsert({ await prisma.permission.upsert({

View File

@ -1,5 +1,5 @@
"use client"; "use client";
import signIn from "@/modules/auth/actions/signIn"; import signIn from "@/modules/auth/actions/signInAction";
import { import {
Paper, Paper,
PasswordInput, PasswordInput,
@ -11,9 +11,7 @@ import {
Button, Button,
Alert, Alert,
} from "@mantine/core"; } from "@mantine/core";
import { redirect } from "next/navigation"; import React from "react";
import { useRouter } from "next/navigation";
import React, { useEffect, useState } from "react";
import { useFormState } from "react-dom"; import { useFormState } from "react-dom";
const initialState = { const initialState = {

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import logout from "@/modules/auth/actions/logout"; import logoutAction from "@/modules/auth/actions/logoutAction";
import { useEffect } from "react"; import { useEffect } from "react";
/** /**
@ -9,9 +9,9 @@ import { useEffect } from "react";
*/ */
export default function LogoutPage() { export default function LogoutPage() {
useEffect(() => { useEffect(() => {
const logoutAction = async () => await logout(); (async () => await logoutAction())()
.then(() => {})
logoutAction(); .catch(() => {});
}, []); }, []);
return <div></div>; return <div></div>;

View File

@ -1,6 +1,10 @@
"use client"; "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 { import {
Paper, Paper,
PasswordInput, PasswordInput,
@ -12,65 +16,53 @@ import {
Button, Button,
} from "@mantine/core"; } from "@mantine/core";
import { useForm } from "@mantine/form"; import { useForm } from "@mantine/form";
import { showNotification } from "@mantine/notifications";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
export interface RegisterFormSchema {
email: string;
password: string;
passwordConfirmation: string;
name: string;
}
export default function RegisterPage() { export default function RegisterPage() {
const [errorMessage, setErrorMessage] = useState(""); const [errorMessage, setErrorMessage] = useState("");
const form = useForm<RegisterFormSchema>({ const form = useForm<CreateUserSchema>({
initialValues: { initialValues: {
email: "", email: "",
password: "", plainPassword: "",
passwordConfirmation: "", plainPasswordConfirmation: "",
name: "", name: "",
}, },
validate: { validate: {
email: (value: string) => email: (value: string) =>
/^\S+@\S+$/.test(value) ? null : "Invalid email", /^\S+@\S+$/.test(value) ? null : "Invalid email",
password: (value: string) => plainPassword: (value: string) =>
value.length >= 6 value.length >= 6
? null ? null
: "Password should be at least 6 characters", : "Password should be at least 6 characters",
passwordConfirmation: (value: string, values: RegisterFormSchema) => plainPasswordConfirmation: (value: string, values: CreateUserSchema) =>
value === values.password ? null : "Passwords should match", value === values.plainPassword ? null : "Passwords should match",
name: (value: string) => name: (value: string) =>
value.length > 0 ? null : "Name is required", value.length > 0 ? null : "Name is required",
}, },
}); });
const handleSubmit = async (values: RegisterFormSchema) => { const handleSubmit = async (values: CreateUserSchema) => {
const formData = new FormData(); withServerAction(createUserAction, form.values)
Object.entries(values).forEach(([key, value]) => { .then((response) => {
formData.append(key, value); showNotification({message: "Register Success", color: "green"})
}); })
.catch((e) => {
const response = await createUser(formData); if (e instanceof DashboardError) {
if (e.errorCode === "INVALID_FORM_DATA") {
if (!response.success) { form.setErrors(e.formErrors ?? {});
setErrorMessage(response.error.message); } else {
setErrorMessage(`ERROR: ${e.message} (${e.errorCode})`);
if (response.error.errors) { }
const errors = Object.entries(response.error.errors).reduce( } else if (e instanceof Error) {
(prev, [k, v]) => { setErrorMessage(`ERROR: ${e.message}`);
prev[k] = v[0]; } else {
return prev; setErrorMessage(
}, `Unkown error is occured. Please contact administrator`
{} as { [k: string]: string } );
); }
})
form.setErrors(errors);
console.log(form.errors);
} else {
form.clearErrors();
}
}
}; };
return ( return (
@ -102,14 +94,14 @@ export default function RegisterPage() {
placeholder="Your password" placeholder="Your password"
name="password" name="password"
autoComplete="new-password" autoComplete="new-password"
{...form.getInputProps("password")} {...form.getInputProps("plainPassword")}
/> />
<PasswordInput <PasswordInput
label="Repeat Password" label="Repeat Password"
placeholder="Repeat yout password" placeholder="Repeat yout password"
name="passwordConfirmation" name="passwordConfirmation"
autoComplete="new-password" autoComplete="new-password"
{...form.getInputProps("passwordConfirmation")} {...form.getInputProps("plainPasswordConfirmation")}
/> />
</Stack> </Stack>

View File

@ -4,7 +4,7 @@ import Image from "next/image";
import React from "react"; import React from "react";
import logo from "@/assets/logos/logo.png"; import logo from "@/assets/logos/logo.png";
import DashboardLayout from "@/modules/dashboard/components/DashboardLayout"; 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 { redirect } from "next/navigation";
import { Notifications } from "@mantine/notifications"; import { Notifications } from "@mantine/notifications";

View File

@ -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");
}

View File

@ -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<ServerResponseAction> {
//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)
}
}

View File

@ -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<ServerResponseAction<Awaited<ReturnType<typeof getMyDetail>>>> {
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);
}
}

View File

@ -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;
}
}

View File

@ -1,12 +1,25 @@
"use server"; "use server";
import { redirect } from "next/navigation"; 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<void> {
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) { if (user) {
redirect("dashboard"); redirect("/dashboard");
} }
} }

View File

@ -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");
}

View File

@ -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.",
},
};
}
}

View File

@ -1,68 +1,90 @@
// Directive to enforce client-side operation in a Next.js application.
"use client"; "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 { interface UserData {
name: string; name: string;
email: string; email: string;
photoUrl: string | null; photoUrl: string | null;
// Add additional user fields as needed // Additional user fields can be added here.
} }
// State structure for the authentication context.
interface AuthContextState { interface AuthContextState {
user: UserData | null; user: UserData | null;
fetchUserData: () => void; fetchUserData: () => void;
logout: () => void; logout: () => void;
} }
// Props type definition for the AuthContextProvider component.
interface Props { interface Props {
children: ReactNode; children: ReactNode;
} }
// Creating the authentication context with an undefined initial value.
const AuthContext = createContext<AuthContextState | undefined>(undefined); const AuthContext = createContext<AuthContextState | undefined>(undefined);
export const AuthContextProvider = ({ children }: Props) => { /**
const [user, setUser] = useState<UserData | null>(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<UserData | null>(null);
const fetchUserData = useCallback(() => { // Function to fetch user data and update state accordingly.
const getUserData = async () => { const fetchUserData = useCallback(() => {
const user = await getUser(); withServerAction(getMyDetailAction)
setUser(user); .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() // Fetch user data on component mount.
.then(() => {}) useEffect(() => {
.catch(() => {}); fetchUserData();
}, []); }, [fetchUserData]);
useEffect(() => { // Function to clear user data, effectively logging the user out.
fetchUserData(); const logout = () => {
}, [fetchUserData]); setUser(null);
};
const logout = () => { // Providing authentication state and functions to the context consumers.
setUser(null); return (
}; <AuthContext.Provider value={{ user, fetchUserData, logout }}>
{children}
return ( </AuthContext.Provider>
<AuthContext.Provider value={{ user, fetchUserData, logout }}> );
{children}
</AuthContext.Provider>
);
}; };
export const useAuth = () => { /**
const context = useContext(AuthContext); * Custom hook to consume the authentication context. This hook ensures the context is used within a provider.
if (!context) { *
throw new Error("useAuth must be used within an AuthContextProvider"); * @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.
return context; */
export const useAuth = (): AuthContextState => {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthContextProvider");
}
return context;
}; };

View File

@ -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"],
});

View File

@ -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,
};
}

View File

@ -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,
};
}

View File

@ -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 };
}

View File

@ -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;
}

View File

@ -1,5 +1,4 @@
"use client"; "use client";
import getUser from "@/modules/auth/actions/getUser";
import CrudPermissions from "@/modules/dashboard/types/CrudPermissions"; import CrudPermissions from "@/modules/dashboard/types/CrudPermissions";
import { Table, Text, Flex, Button, Center } from "@mantine/core"; import { Table, Text, Flex, Button, Center } from "@mantine/core";
import { import {