Move sign in to service

This commit is contained in:
sianida26 2024-02-28 11:14:18 +07:00
parent d0870419f0
commit ed9a0baa47
4 changed files with 97 additions and 53 deletions

View File

@ -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,68 +1,41 @@
"use server"; "use server";
import prisma from "@/core/db";
import { User } from "@prisma/client";
import { cookies } from "next/headers"; import { cookies } from "next/headers";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
import AuthError from "../error/AuthError"; import AuthError from "../error/AuthError";
import comparePassword from "../utils/comparePassword"; import signIn from "../services/signIn";
import { createJwtToken } from "../utils/createJwtToken";
/** /**
* Handles the sign-in process for a user. * 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.
* This function validates a user's credentials (email and password), checks against the database, * If validation fails, a custom `AuthError` is thrown, and detailed error information is provided to the caller.
* 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. * 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, not currently used. *
* @param rawFormData - The raw form data containing the user's email and password. * @param prevState - The previous state of the application. Currently not utilized but may be used for future enhancements.
* @returns A promise that resolves to a redirect to the dashboard on successful authentication, * @param rawFormData - The raw form data obtained from the sign-in form, containing the user's email and password.
* or an object containing error details on failure. * @returns A promise that, upon successful authentication, resolves to a redirection to the dashboard. If authentication fails,
* @throws Specific authentication error based on the failure stage. * 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 signIn(prevState: any, rawFormData: FormData) { export default async function signInAction(prevState: any, rawFormData: FormData) {
//TODO: Add Throttling //TODO: Add Throttling
//TODO: Add validation check if the user is already logged in //TODO: Add validation check if the user is already logged in
try { try {
// Extract email and password from the raw form data.
const formData = { const formData = {
email: rawFormData.get("email") as string, email: rawFormData.get("email") as string,
password: rawFormData.get("password") as string, password: rawFormData.get("password") as string,
}; };
// Retrieve user from the database by email // Attempt to sign in with the provided credentials.
const user = await prisma.user.findUnique({ const result = await signIn(formData)
where: { email: formData.email },
});
// Throw if user not found // Set the JWT token in cookies upon successful sign-in.
if (!user) cookies().set("token", result.token);
throw new AuthError({
errorCode: "EMAIL_NOT_FOUND",
message: "Email or Password does not match",
});
// Throw if user has no password hash // Redirect to the dashboard after successful sign-in.
// TODO: Add check if the user uses another provider redirect("/dashboard");
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) { } catch (e: unknown) {
// Custom error handling for authentication errors // Custom error handling for authentication errors
if (e instanceof AuthError) { if (e instanceof AuthError) {
@ -94,6 +67,4 @@ export default async function signIn(prevState: any, rawFormData: FormData) {
}, },
}; };
} }
redirect("/dashboard");
} }

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