🏗️ reconstruct all system in user module

This commit is contained in:
Rafi Arrafif
2025-07-16 23:42:13 +07:00
parent 558891ade7
commit 29b76fb91a
25 changed files with 85 additions and 548 deletions

View File

@ -1,5 +1,5 @@
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import { findUserByEmailOrUsernameService } from "../../user/services/findUserByEmailOrUsername.service"; import { findUserByEmailOrUsernameService } from "../../user/services/getUserData.service";
import { LoginWithPasswordRequest } from "../auth.types"; import { LoginWithPasswordRequest } from "../auth.types";
import { AppError } from "../../../helpers/error/instances/app"; import { AppError } from "../../../helpers/error/instances/app";
import { UserHeaderInformation } from "../../../helpers/http/userHeader/getUserHeaderInformation/types"; import { UserHeaderInformation } from "../../../helpers/http/userHeader/getUserHeaderInformation/types";

View File

@ -1,32 +0,0 @@
import { Context } from "elysia";
import { checkUserPasswordService } from "../services/checkUserPassword.service";
import { jwtDecode } from "../../../helpers/http/jwt/decode";
import { getCookie } from "../../../helpers/http/userHeader/cookies/getCookies";
import { mainErrorHandler } from "../../../helpers/error/handler";
import { returnWriteResponse } from "../../../helpers/callback/httpResponse";
export const checkUserPasswordController = async (
ctx: Context & { body: { password: string } }
) => {
try {
// Get the credentials information from cookies
const cookie = getCookie(ctx);
const jwtPayload = jwtDecode(cookie.auth_token!);
// Execute the check user password service
const checkUserPassword = await checkUserPasswordService(
jwtPayload.user.username,
ctx.body.password
);
// If the password is valid, return a success response
return returnWriteResponse(
ctx.set,
204,
"Password is valid",
checkUserPassword
);
} catch (error) {
return mainErrorHandler(ctx.set, error);
}
};

View File

@ -1,50 +0,0 @@
import {
returnErrorResponse,
returnWriteResponse,
} from "../../../helpers/callback/httpResponse";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
import { createUserService } from "../services/createUser.service";
import { mainErrorHandler } from "../../../helpers/error/handler";
import { createUserSchema } from "../schemas/createUser.schema";
/**
* @function createUser
* @description Creates a new user in the database.
*
* @param {Context & { body: Prisma.UserCreateInput }} ctx - The context object containing the request body.
* @param {Prisma.UserCreateInput} ctx.body - The user data to be created.
*
* @returns {Promise<Object>} A response object indicating success or failure.
* @throws {Object} An error response object if validation fails or an error occurs during user creation.
*
* @example
* Request route: POST /users
* Request body:
* {
* "username": "john_doe",
* "email": "john@example.com",
* "password": "password123"
* }
*/
export const createUserController = async (
ctx: Context & { body: Prisma.UserCreateInput }
) => {
// Validate the user input using a validation schema
const { error } = createUserSchema.validate(ctx.body);
if (error)
return returnErrorResponse(ctx.set, 400, "Invalid user input", error);
// Create the user in the database using the service
try {
const newUser = await createUserService(ctx.body);
return returnWriteResponse(
ctx.set,
201,
"User created successfully",
newUser
);
} catch (error) {
return mainErrorHandler(ctx.set, error);
}
};

View File

@ -0,0 +1,21 @@
import { Context } from "elysia";
import { createUserSchema } from "../schemas/createUser.schema";
import { mainErrorHandler } from "../../../helpers/error/handler";
import { createUserViaRegisterService } from "../services/createUserViaRegister.service";
import { returnWriteResponse } from "../../../helpers/callback/httpResponse";
export const createUserViaRegisterController = async (ctx: Context) => {
try {
const validate = createUserSchema.parse(ctx.body);
const createUser = await createUserViaRegisterService(validate);
return returnWriteResponse(
ctx.set,
201,
"User Successfully Created",
createUser
);
} catch (error) {
return mainErrorHandler(ctx.set, error);
}
};

View File

@ -1,93 +0,0 @@
import { Context } from "elysia";
import {
returnErrorResponse,
returnWriteResponse,
} from "../../../helpers/callback/httpResponse";
import { mainErrorHandler } from "../../../helpers/error/handler";
import { Prisma } from "@prisma/client";
import { updateUserService } from "../services/updateUser.service";
import { getCookie } from "../../../helpers/http/userHeader/cookies/getCookies";
import { getUserHeaderInformation } from "../../../helpers/http/userHeader/getUserHeaderInformation";
import { setCookie } from "../../../helpers/http/userHeader/cookies/setCookies";
import { COOKIE_KEYS } from "../../../constants/cookie.keys";
import { jwtEncode } from "../../../helpers/http/jwt/encode";
import { editUserSchema } from "../schemas/editUser.schema";
/**
* @function editUserController
* @description Updates user profile information. Requires valid JWT authentication token in cookies.
* On success, returns updated user data and sets a new JWT token in cookies.
* In development environment, the new JWT token is also returned in the response.
*
* @param {Context & { body: Prisma.UserUncheckedCreateInput }} ctx - The context object containing request information.
* @param {Object} ctx.body - The updated user data.
*
* @returns {Promise<Object>} A response object indicating success or failure.
* @throws {Object} An error response if authentication fails or validation errors occur.
*
* @example
* Request route: PUT /users
* Request headers:
* {
* "Cookie": "auth_token=<JWT_TOKEN>"
* }
* Request body:
* {
* "username": "new_username",
* "name": "Updated Name",
* "birthDate": "1990-01-01T00:00:00.000Z",
* "gender": "male",
* "phoneCC": 62,
* "phoneNumber": 81234567890,
* "bioProfile": "Updated bio",
* "profilePicture": JPG/PNG/JPEG File,
* "commentPicture": JPG/PNG/JPEG File,
* "deletedAt": null
* }
*
* Success Response:
* Status: 201 Created
* {
* "message": "User data updated",
* "token": "<NEW_JWT_TOKEN>" // Only in development environment
* }
*
* Failure Responses:
* - 401 Unauthorized: Missing or invalid authentication token
* - 400 Bad Request: Invalid user data
* - 500 Internal Server Error: Database operation failed
*/
export const editUserController = async (
ctx: Context & {
body: Prisma.UserUncheckedCreateInput;
}
) => {
// Validate the request body against the edit user schema
const { error } = editUserSchema.validate(ctx.body);
if (error)
return returnErrorResponse(ctx.set, 422, "Invalid form input", error);
try {
// Get the user JWT token from cookies, if the token is not found, return an error response
const userCookie = getCookie(ctx);
const auth_token = userCookie.auth_token!;
// Get user browser header information from the context
const userHeaderInfo = getUserHeaderInformation(ctx);
// Excecute the edit user data service
const newUserData = await updateUserService(
auth_token,
userHeaderInfo,
ctx.body
);
// create a new JWT token with the updated user data, and set it in the cookies
const newJwtToken = await jwtEncode(newUserData);
setCookie(ctx.set, COOKIE_KEYS.AUTH, newJwtToken);
return returnWriteResponse(ctx.set, 201, "User data updated", newJwtToken);
} catch (error) {
return mainErrorHandler(ctx.set, error);
}
};

View File

@ -1,12 +0,0 @@
import { Context } from "elysia";
import { findUserByEmailService } from "../services/findUserByEmail.service";
import { mainErrorHandler } from "../../../helpers/error/handler";
export const findUserByEmailController = async (ctx: Context) => {
try {
const findUserByEmail = await findUserByEmailService(ctx.params.email);
return findUserByEmail;
} catch (error) {
return mainErrorHandler(ctx.set, error);
}
};

View File

@ -1,18 +0,0 @@
import { returnReadResponse } from "../../../helpers/callback/httpResponse";
import { Context } from "elysia";
import { getAllUsersService } from "../services/getAllUser.service";
import { mainErrorHandler } from "../../../helpers/error/handler";
export const getAllUserController = async (ctx: Context) => {
try {
const allUser = await getAllUsersService();
return returnReadResponse(
ctx.set,
200,
"All user ranks successfully",
allUser
);
} catch (error) {
return mainErrorHandler(ctx.set, error);
}
};

View File

@ -1,7 +0,0 @@
import { Context } from "elysia";
import { returnWriteResponse } from "../../../helpers/callback/httpResponse";
export const softDeleteUserController = async (ctx: Context) => {
const data = ctx.params.username;
return returnWriteResponse(ctx.set, 201, "Okay", data);
};

View File

@ -1,30 +1,7 @@
import Elysia from "elysia"; import Elysia from "elysia";
import { getAllUserController } from "./controller/getAllUser.controller"; import { createUserViaRegisterController } from "./controller/createUserViaRegister.controller";
import { createUserController } from "./controller/createUser.controller";
import { editUserController } from "./controller/editUser.controller";
import { unautenticatedMiddleware } from "../../middleware/auth/unauthenticated.middleware";
import { authenticatedMiddleware } from "../../middleware/auth/authenticated.middleware";
import { checkUserPasswordController } from "./controller/checkUserPassword.controller";
import { isOwnerOrAdminMiddleware } from "../../middleware/userRoles/isOwnerOrAdmin.middleware";
import { softDeleteUserController } from "./controller/softDeleteUser.controller";
import { findUserByEmailController } from "./controller/findUserByEmail.controller";
export const userModule = new Elysia({ prefix: "/users" }) export const userModule = new Elysia({ prefix: "/users" }).post(
.get("/", getAllUserController) "/",
.get("/e/:email", findUserByEmailController) createUserViaRegisterController
.group("", (app) => );
app
.onBeforeHandle(unautenticatedMiddleware) // middleware to ensure the user is not authenticated
.post("/", createUserController)
)
.group("", (app) =>
app
.onBeforeHandle(authenticatedMiddleware) // middleware to ensure the user is authenticated
.put("/", editUserController)
.post("/check-password", checkUserPasswordController)
)
.group("", (app) =>
app
.onBeforeHandle(isOwnerOrAdminMiddleware)
.delete(":username", softDeleteUserController)
);

View File

@ -1,17 +0,0 @@
import { userModel } from "../user.model";
export const checkUserEmailAndUsernameAvailabillityRepo = async (
id: string,
username: string,
email: string
) => {
const checkUsernameAndEmailAvailabillity = await userModel.findFirst({
where: {
OR: [{ username: username ?? undefined }, { email: email ?? undefined }],
NOT: {
id: id,
},
},
});
return checkUsernameAndEmailAvailabillity;
};

View File

@ -0,0 +1,10 @@
import { userModel } from "../../user.model";
import { createUserViaRegisterInput } from "../../user.types";
export const createUserViaRegisterRepository = async (
payload: createUserViaRegisterInput
) => {
return await userModel.create({
data: payload,
});
};

View File

@ -1,13 +0,0 @@
import { Prisma } from "@prisma/client";
import { userModel } from "../user.model";
export const createUserRepo = async (data: Prisma.UserCreateInput) => {
const userData = await userModel.create({
data,
omit: {
password: true,
},
});
return userData;
};

View File

@ -1,28 +0,0 @@
import { userModel } from "../user.model";
import { FindUserByEmailOrUsernameOptions } from "../user.types";
export const findUserByEmailOrUsernameRepository = async (
identifier: string,
options: FindUserByEmailOrUsernameOptions
) => {
const userData = await userModel.findUnique({
where: { email: identifier },
include: {
assignedRoles: {
select: {
role: {
omit: {
createdBy: true,
updatedAt: true,
createdAt: true,
deletedAt: true,
},
},
},
},
},
});
if (!userData) return false;
return userData;
};

View File

@ -1,11 +0,0 @@
import { userModel } from "../user.model";
export const getAllUserRepo = async () => {
const data = await userModel.findMany({
where: {
deletedAt: null,
},
});
return data;
};

View File

@ -1,16 +0,0 @@
import { Prisma } from "@prisma/client";
import { userModel } from "../user.model";
export const updateUserRepository = async (
username: string,
payload: Prisma.UserUpdateInput
) => {
const userData = await userModel.update({
where: {
username,
},
data: payload,
});
return userData;
};

View File

@ -1,19 +1,17 @@
import Joi from "joi"; import z from "zod";
export const createUserSchema = Joi.object({ export const createUserSchema = z.object({
name: Joi.string() name: z.string(),
.min(4) username: z
.max(255) .string()
.required(), .min(4) //Total all username character must over 4 character
username: Joi.string() .regex(/^[a-zA-Z0-9_-]+$/), //Prohibiting the use of spaces and symbols other than - and _
.min(4) email: z.email(),
.max(255) password: z
.required(), .string()
email: Joi.string() .min(8) //Total all password chaacter must over 8 character
.email() .regex(/[A-Z]/) //Min has 1 uppercase letter
.required(), .regex(/[a-z]/) //Min has 1 lowercase letter
password: Joi.string() .regex(/[0-9]/) //Min has 1 number
.min(8) .regex(/[^A-Za-z0-9"]/), //Min has 1 symbol character
.max(255)
.required(),
}); });

View File

@ -1,20 +0,0 @@
import { Prisma } from "@prisma/client";
import { ErrorForwarder } from "../../../helpers/error/instances/forwarder";
import { checkUserEmailAndUsernameAvailabillityRepo } from "../repositories/checkUserEmailAndUsernameAvailabillity.repository";
export const checkUserEmailAndUsernameAvailabillityService = async (
payload: Prisma.UserUpdateInput,
idException: string
) => {
try {
const usernameAndEmailAvailabillity =
checkUserEmailAndUsernameAvailabillityRepo(
idException!,
payload.username as string,
payload.email as string
);
return usernameAndEmailAvailabillity;
} catch (error) {
ErrorForwarder(error);
}
};

View File

@ -1,28 +0,0 @@
import { AppError } from "../../../helpers/error/instances/app";
import { ErrorForwarder } from "../../../helpers/error/instances/forwarder";
import { comparePassword } from "../../../helpers/security/password/compare";
import { findUserByEmailOrUsernameService } from "./findUserByEmailOrUsername.service";
import { User } from "@prisma/client";
export const checkUserPasswordService = async (
username: string,
password: string
) => {
try {
// find user by username and get the password
const userData = (await findUserByEmailOrUsernameService(username, {
verbose: true,
})) as User;
const StoredPassword = userData.password;
// compare the provided password with the stored password
const matchingPassword = await comparePassword(password, StoredPassword);
if (!matchingPassword) {
throw new AppError(401, "Invalid Credential");
}
return true;
} catch (error) {
ErrorForwarder(error);
}
};

View File

@ -1,19 +0,0 @@
import { Prisma } from "@prisma/client";
import { hashPassword } from "../../../helpers/security/password/hash";
import { createUserRepo } from "../repositories/createUser.repository";
import { ErrorForwarder } from "../../../helpers/error/instances/forwarder";
export const createUserService = async (userData: Prisma.UserCreateInput) => {
try {
const { password, ...rest } = userData; // Destructure the password and the rest of the user data
const hashedPassword = await hashPassword(password); // Hash the password before saving to the database
const newUser = await createUserRepo({
...rest,
password: hashedPassword,
});
return newUser;
} catch (error) {
ErrorForwarder(error, 500, "Internal server error");
}
};

View File

@ -0,0 +1,19 @@
import { ErrorForwarder } from "../../../helpers/error/instances/forwarder";
import { hashPassword } from "../../../helpers/security/password/hash";
import { createUserViaRegisterRepository } from "../repositories/create/createUserViaRegister.repository";
import { createUserViaRegisterInput } from "../user.types";
export const createUserViaRegisterService = async (
payload: createUserViaRegisterInput
) => {
try {
const hashedPassword = await hashPassword(payload.password);
return createUserViaRegisterRepository({
...payload,
password: hashedPassword,
});
} catch (error) {
ErrorForwarder(error);
}
};

View File

@ -1,13 +0,0 @@
import { ErrorForwarder } from "../../../helpers/error/instances/forwarder";
import { findUserByEmailOrUsernameRepository } from "../repositories/findUserByEmailOrUsername.repository";
export const findUserByEmailService = async (email: string) => {
try {
const findUserByEmail = findUserByEmailOrUsernameRepository(email, {
queryTarget: "email",
});
return findUserByEmail;
} catch (error) {
ErrorForwarder(error);
}
};

View File

@ -1,21 +0,0 @@
import { AppError } from "../../../helpers/error/instances/app";
import { ErrorForwarder } from "../../../helpers/error/instances/forwarder";
import { findUserByEmailOrUsernameRepository } from "../repositories/findUserByEmailOrUsername.repository";
import { FindUserByEmailOrUsernameOptions } from "../user.types";
export const findUserByEmailOrUsernameService = async (
identifier: string,
options: FindUserByEmailOrUsernameOptions
) => {
try {
const userData = await findUserByEmailOrUsernameRepository(
identifier,
options
);
if (!userData) throw new AppError(404, "User not found");
return userData;
} catch (error) {
ErrorForwarder(error);
}
};

View File

@ -1,11 +0,0 @@
import { ErrorForwarder } from "../../../helpers/error/instances/forwarder";
import { getAllUserRepo } from "../repositories/getAllUser.repository";
export const getAllUsersService = async () => {
try {
const allUser = await getAllUserRepo();
return allUser;
} catch (error) {
ErrorForwarder(error);
}
};

View File

@ -1,85 +0,0 @@
import { Prisma } from "@prisma/client";
import { jwtDecode } from "../../../helpers/http/jwt/decode";
import { AppError } from "../../../helpers/error/instances/app";
import { ErrorForwarder } from "../../../helpers/error/instances/forwarder";
import { updateUserRepository } from "../repositories/updateUser.repository";
import { checkUserEmailAndUsernameAvailabillityService } from "./checkUserEmailAndUsernameAvailabillity.service";
import { logoutService } from "../../auth/services/logout.service";
import { loginFromSystemService } from "../../auth/services/loginFromSystem.service";
import { UserHeaderInformation } from "../../../helpers/http/userHeader/getUserHeaderInformation/types";
import { saveAvatar } from "../../../helpers/files/saveFile/modules/saveAvatar";
import { saveCommentBackground } from "../../../helpers/files/saveFile/modules/saveCommentBackgorund";
export const updateUserService = async (
cookie: string,
userHeaderInfo: UserHeaderInformation,
payload: Prisma.UserUpdateInput
) => {
try {
// Decode the JWT token and verify the user, if the user is not the same as the identifier, throw an error
const jwtSession = jwtDecode(cookie);
// Check if the username or email is being taken by another user, if so, throw an error
const isUsernameOrEmailIsBeingTaken = await checkUserEmailAndUsernameAvailabillityService(
payload,
jwtSession.userId
);
if (isUsernameOrEmailIsBeingTaken)
throw new AppError(
409,
"The username or email has already taken by another user."
);
// Store the avatar to the file system if provided in the payload
let storeAvatar: string | undefined = undefined;
if (payload.avatar) storeAvatar = await saveAvatar(payload.avatar as File);
// Store the comment background to the file system if provided in the payload
let storeCommentBackground: string | undefined = undefined;
if (payload.commentBackground)
storeCommentBackground = await saveCommentBackground(
payload.commentBackground as File
);
// Prepare the fields to update, only include fields that are provided in the payload
const fieldsToUpdate: Partial<Prisma.UserUpdateInput> = {
...(payload.username && payload.username !== jwtSession.user.username
? { username: payload.username }
: {}),
...(payload.name !== undefined ? { name: payload.name } : {}),
...(payload.birthDate !== undefined
? { birthDate: payload.birthDate }
: {}),
...(payload.gender !== undefined ? { gender: payload.gender } : {}),
...(payload.phoneCC !== undefined ? { phoneCC: payload.phoneCC } : {}),
...(payload.phoneNumber !== undefined
? { phoneNumber: payload.phoneNumber }
: {}),
...(payload.bioProfile !== undefined
? { bioProfile: payload.bioProfile }
: {}),
...(storeAvatar !== undefined ? { avatar: storeAvatar } : {}),
...(storeCommentBackground !== undefined
? { commentBackground: storeCommentBackground }
: {}),
...(payload.deletedAt !== undefined
? { deletedAt: payload.deletedAt }
: {}),
};
// Update the user in the database, use username from the JWT session to find the user
await updateUserRepository(jwtSession.user.username, fieldsToUpdate);
// Clear the session and re-login the user to get a new JWT token
await logoutService(cookie);
const newUserSession = await loginFromSystemService(
jwtSession.userId,
userHeaderInfo
);
return newUserSession;
} catch (error) {
ErrorForwarder(error, 500, "Internal server error");
}
};

View File

@ -1,10 +1,16 @@
export interface FindUserByEmailOrUsernameOptions { export interface getUserDataService {
queryTarget: "email" | "username" | "both"; identifier: string;
verbosity?: FindUserByEmailOrUsernameVerbosity; // If true, returns the user with all details including sensitive information queryTarget: "id" | "email" | "username" | "email_username";
options?: getUserDataOptions;
} }
enum FindUserByEmailOrUsernameVerbosity { export interface getUserDataOptions {
"exists", verbosity?: "exists" | "basic" | "full";
"basic", include?: ("preference" | "role")[];
"extended", }
"full",
export interface createUserViaRegisterInput {
name: string;
username: string;
email: string;
password: string;
} }