optimize auth
This commit is contained in:
parent
65131f9653
commit
c6034de16a
12
package.json
12
package.json
|
|
@ -6,17 +6,18 @@
|
|||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"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.0",
|
||||
"@mantine/form": "^7.5.0",
|
||||
"@mantine/hooks": "^7.5.0",
|
||||
"@mantine/notifications": "^7.5.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",
|
||||
|
|
@ -44,6 +45,7 @@
|
|||
"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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -17,8 +14,8 @@ model User {
|
|||
emailVerified DateTime?
|
||||
passwordHash String?
|
||||
photoProfile UserPhotoProfiles?
|
||||
roles Role[]
|
||||
directPermissions Permission[]
|
||||
directPermissions Permission[] @relation("PermissionToUser")
|
||||
roles Role[] @relation("RoleToUser")
|
||||
}
|
||||
|
||||
model UserPhotoProfiles {
|
||||
|
|
@ -34,8 +31,8 @@ model Role {
|
|||
name String
|
||||
description String @default("")
|
||||
isActive Boolean @default(false)
|
||||
users User[]
|
||||
permissions Permission[]
|
||||
permissions Permission[] @relation("PermissionToRole")
|
||||
users User[] @relation("RoleToUser")
|
||||
}
|
||||
|
||||
model Permission {
|
||||
|
|
@ -44,6 +41,6 @@ model Permission {
|
|||
name String
|
||||
description String @default("")
|
||||
isActive Boolean @default(false)
|
||||
roles Role[]
|
||||
directUsers User[]
|
||||
roles Role[] @relation("PermissionToRole")
|
||||
directUsers User[] @relation("PermissionToUser")
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
const logoutAction = async () => await logout()
|
||||
|
||||
logoutAction()
|
||||
}, [])
|
||||
logoutAction();
|
||||
}, []);
|
||||
|
||||
return <div></div>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}</>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import createUser from "@/features/auth/actions/createUser";
|
||||
import createUser from "@/modules/auth/actions/createUser";
|
||||
import {
|
||||
Paper,
|
||||
PasswordInput,
|
||||
|
|
@ -15,36 +15,40 @@ 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<RegisterFormSchema>({
|
||||
initialValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
passwordConfirmation: "",
|
||||
name: ""
|
||||
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'),
|
||||
}
|
||||
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);
|
||||
|
|
@ -53,19 +57,21 @@ export default function RegisterPage() {
|
|||
setErrorMessage(response.error.message);
|
||||
|
||||
if (response.error.errors) {
|
||||
const errors = Object.entries(response.error.errors)
|
||||
.reduce((prev, [k,v]) => {
|
||||
prev[k] = v[0]
|
||||
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 (
|
||||
<div className="w-screen h-screen flex items-center justify-center">
|
||||
|
|
@ -73,7 +79,9 @@ export default function RegisterPage() {
|
|||
<Text size="lg" fw={500} mb={30}>
|
||||
Register
|
||||
</Text>
|
||||
<form onSubmit={form.onSubmit((values) => handleSubmit(values))}>
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) => handleSubmit(values))}
|
||||
>
|
||||
<Stack>
|
||||
<TextInput
|
||||
label="Name"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
@layer tailwind{
|
||||
@tailwind base;
|
||||
}
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
15
src/core/db/index.ts
Normal file
15
src/core/db/index.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const prismaClientSingleton = () => {
|
||||
return new PrismaClient();
|
||||
};
|
||||
|
||||
declare global {
|
||||
var prisma: undefined | ReturnType<typeof prismaClientSingleton>;
|
||||
}
|
||||
|
||||
const prisma = globalThis.prisma ?? prismaClientSingleton();
|
||||
|
||||
export default prisma;
|
||||
|
||||
if (process.env.NODE_ENV !== "production") globalThis.prisma = prisma;
|
||||
31
src/core/error/BaseError.ts
Normal file
31
src/core/error/BaseError.ts
Normal file
|
|
@ -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;
|
||||
105
src/modules/auth/actions/createUser.ts
Normal file
105
src/modules/auth/actions/createUser.ts
Normal file
|
|
@ -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");
|
||||
}
|
||||
39
src/modules/auth/actions/getUser.ts
Normal file
39
src/modules/auth/actions/getUser.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
12
src/modules/auth/actions/guestOnly.ts
Normal file
12
src/modules/auth/actions/guestOnly.ts
Normal file
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
16
src/modules/auth/actions/logout.ts
Normal file
16
src/modules/auth/actions/logout.ts
Normal file
|
|
@ -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");
|
||||
}
|
||||
99
src/modules/auth/actions/signIn.ts
Normal file
99
src/modules/auth/actions/signIn.ts
Normal file
|
|
@ -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");
|
||||
}
|
||||
5
src/modules/auth/authConfig.ts
Normal file
5
src/modules/auth/authConfig.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
const authConfig = {
|
||||
saltRounds: 10,
|
||||
};
|
||||
|
||||
export default authConfig;
|
||||
28
src/modules/auth/error/AuthError.ts
Normal file
28
src/modules/auth/error/AuthError.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
7
src/modules/auth/types/UserClaims.d.ts
vendored
Normal file
7
src/modules/auth/types/UserClaims.d.ts
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { User } from "@prisma/client";
|
||||
|
||||
type UserClaims = {
|
||||
id: User["id"];
|
||||
};
|
||||
|
||||
export default UserClaims;
|
||||
17
src/modules/auth/utils/comparePassword.ts
Normal file
17
src/modules/auth/utils/comparePassword.ts
Normal file
|
|
@ -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<boolean> {
|
||||
return bcrypt.compare(password, hash);
|
||||
}
|
||||
|
||||
export default comparePassword;
|
||||
20
src/modules/auth/utils/createJwtToken.ts
Normal file
20
src/modules/auth/utils/createJwtToken.ts
Normal file
|
|
@ -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);
|
||||
}
|
||||
21
src/modules/auth/utils/decodeJwtToken.ts
Normal file
21
src/modules/auth/utils/decodeJwtToken.ts
Normal file
|
|
@ -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;
|
||||
34
src/modules/auth/utils/getUserFromToken.ts
Normal file
34
src/modules/auth/utils/getUserFromToken.ts
Normal file
|
|
@ -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;
|
||||
14
src/modules/auth/utils/hashPassword.ts
Normal file
14
src/modules/auth/utils/hashPassword.ts
Normal file
|
|
@ -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<string> {
|
||||
return bcrypt.hash(password, authConfig.saltRounds);
|
||||
}
|
||||
|
||||
export default hashPassword;
|
||||
Loading…
Reference in New Issue
Block a user