Compare commits

...

5 Commits

Author SHA1 Message Date
bc12f94e41
feat(services): refactor user service and improve error handling
- Introduced error handling in `register`, `login`, `getAllUsersService`, and `editUserService` methods.
- Improved logging by adding breaks for better visibility.
- Refactored code for better readability and maintainability.
- Eliminated unnecessary and duplicate imports.
- Transitioned from MongoDB to MySQL service.
- Added comment flags for testing purposes.

Issue: #18
Signed-off-by: Mathis <yidhra@tuta.io>
2024-04-30 16:19:12 +02:00
df28d3aa52
feat(services): add factorize method and refactor update method in mysql.service
- The `factorize` method was added to `mysql.service.ts` to convert input data into a database query.
- The `update` method was refactored; unnecessary data fields were removed and a check for the `gdpr` field was added. A corresponding interface, `IUserUpdate`, was also imported at the beginning of the file.

Issue: #18
Signed-off-by: Mathis <yidhra@tuta.io>
2024-04-30 16:15:19 +02:00
3d5ea6ac30
feat(interfaces): add IUserUpdate interface
This commit introduces a new interface named IUserUpdate. This interface consists of optional properties like id, username, firstname, lastname, dob and gdpr. These properties are used for validating user's data during update operation.

Issue: #18
Signed-off-by: Mathis <yidhra@tuta.io>
2024-04-30 16:14:47 +02:00
2fb6cd6e83
feat(interfaces): add IDbFactorize interfaces
Added IDbFactorizeOutput and IDbFactorizeInput interfaces to handle inputs and outputs of the SQL query factorization function. Both interfaces contain various properties for customizing the SQL query and its error handling behavior.

Issue: #18
Signed-off-by: Mathis <yidhra@tuta.io>
2024-04-30 16:14:29 +02:00
34f028ef9f
feat(app): updated log messages in multiple files
Changes applied to several services and controllers files:
- Enhanced logging messages with line breaks in `brand.controller.ts`, `auth.controller.ts`, `model.controller.ts`, and `category.controller.ts`.
- Adjusted logging in `mysql.service.ts`, `brand.service.ts`, `user.service.ts`, `category.service.ts`, `model.service.ts` and `jwt.service.ts` to commence on a new line for better readability.

Signed-off-by: Mathis <yidhra@tuta.io>
2024-04-30 14:18:47 +02:00
12 changed files with 289 additions and 195 deletions

View File

@ -397,6 +397,8 @@ async function getSelf(req: Request, res: Response) {
});
}
logger.debug("\nController loaded.");
const AuthController = {
register: registerUser,
login: loginUser,

View File

@ -171,6 +171,8 @@ async function deleteBrand(req: Request, res: Response): Promise<Response> {
//TODO get models of the brand
logger.debug("\nController loaded.");
const BrandController = {
create: createBrand,
update: updateBrand,

View File

@ -177,6 +177,8 @@ async function getBySlugCategory(
});
}
logger.debug("\nController loaded.");
const CategoryController = {
create: createCategory,
update: updateCategory,

View File

@ -131,6 +131,8 @@ async function deleteModel(req: Request, res: Response): Promise<Response> {
//TODO get model with vehicle available.
logger.debug("\nController loaded.");
const ModelController = {
create: createModel,
update: updateModel,

View File

@ -0,0 +1,48 @@
/**
* Represents the output of the factorization function.
*/
export interface IDbFactorizeOutput {
/**
* Description: The variable `_valuesArray` is an array that can contain values of type `string`, `boolean`, `number`, or `Date`.
* (The value associated with the keys of `_keysTemplate`)
*
* @type {Array<string | boolean | number | Date>}
*/
_valuesArray: Array<string | boolean | number | Date>;
/**
* Represents the SQL Query template for the keys.
* @type {string}
*/
_keysTemplate: string;
/**
* The total number of fields.
*
* @type {number}
*/
totalFields: number;
}
/**
* Interface IDbFactorizeInput represents the input required to factorize a SQL query.
*/
export interface IDbFactorizeInput {
/**
* An object containing values that will be in a SQL Query.
*
* @type {Array<string | boolean | number | Date>}
*/
values: object;
/**
* Represents the name of the action that will result of the prepared SQL Query.
*
* @type {string}
*/
actionName: string;
/**
* Indicates whether an error should be thrown when encountering an error.
* If set to true, an error will be thrown. If set to false or not provided, the error will not be thrown.
*
* @type {boolean}
*/
throwOnError?: true;
}

View File

@ -0,0 +1,8 @@
export interface IUserUpdate {
id?: string;
username?: string;
firstname?: string;
lastname?: string;
dob?: Date;
gdpr?: Date;
}

View File

@ -168,7 +168,7 @@ async function deleteBrand(brandId: string): Promise<boolean> {
return true;
}
logger.debug("Service loaded.");
logger.debug("\nService loaded.");
const BrandService = {
create: createBrand,

View File

@ -127,7 +127,7 @@ async function deleteCategory(id: string): Promise<unknown> {
}
}
logger.debug("Service loaded.");
logger.debug("\nService loaded.");
const CategoryService = {
create: createCategory,

View File

@ -64,7 +64,7 @@ async function JwtSignService(
.sign(new TextEncoder().encode(`${process.env["JWT_SECRET"]}`));
}
logger.debug("Service loaded.");
logger.debug("\nService loaded.");
const JwtService = {
verify: JwtVerifyService,

View File

@ -135,7 +135,7 @@ async function getAllModels(): Promise<IDbModel[] | null> {
}
}
logger.debug("Service loaded.");
logger.debug("\nService loaded.");
/**
* ModelService is responsible for managing models.

View File

@ -7,6 +7,8 @@ import type { IDbUser } from "@interfaces/database/IDbUser";
import type { IDbVehicle } from "@interfaces/database/IDbVehicle";
import mysql, { type Connection, type ConnectionOptions } from "mysql2";
import { Logger } from "tslog";
import {IUserUpdate} from "@interfaces/services/IUserUpdate";
import {IDbFactorizeInput, IDbFactorizeOutput} from "@interfaces/database/IDbFactorize";
const access: ConnectionOptions = {
host: `${process.env["MYSQL_HOST"]}`,
@ -32,7 +34,7 @@ class MysqlHandler {
this.Logger.error(`Error connecting to MySQL: ${err}`);
process.exit(1);
}
this.Logger.info(`Connected to MySQL database (${access.database})`);
this.Logger.info(`\n\n> Connected to MySQL database (${access.database})\n`);
});
}
closeConnection() {
@ -52,6 +54,40 @@ class MysqlHandler {
});
}
/**
* Factorize the input data values into a database query.
*
* @param {IDbFactorizeInput} data - The input data containing values to factorize.
* @return {Promise<IDbFactorizeOutput>} - A promise resolving to the factorized output.
*/
factorize(data: IDbFactorizeInput): Promise<IDbFactorizeOutput> {
return new Promise((resolve, reject)=>{
try {
const _sqlQueryKeys = Object.keys(data.values).map((key: string) => `\'${key}\' = ?`)
const values = Object.values(data.values).map((val)=>val)
this.Logger.debug(`\n\n>-> Factorized ${_sqlQueryKeys.length} keys for a prepare Query.\n>-> Action: ${data.actionName}\n`)
const sqlQueryKeys = _sqlQueryKeys.join(', ')
const factorizedOutput: IDbFactorizeOutput = {
_keysTemplate: sqlQueryKeys,
totalFields: _sqlQueryKeys.length,
_valuesArray: values
}
resolve(factorizedOutput);
} catch (err) {
if (data.throwOnError) throw new Error(`${err}`)
this.Logger.error(`\n|\n${err}\n|`)
reject(`${err}`)
}
})
}
/**
* Executes a query using the provided queryString and values.
* @param {string} queryString - The SQL query string to execute.
* @param {Array<string | boolean | Date | number>} values - The values to be inserted into the query.
* @returns {Promise<unknown>} - A promise that resolves with the query results or rejects with an error.
*/
execute(
queryString: string,
values: Array<string | boolean | Date | number>,
@ -138,38 +174,25 @@ const MySqlService = {
/**
* Updates a user in the database.
* @param {MysqlHandler} handler - The MySQL handler object.
* @param {IDbUser} data - The updated user data.
* @param {IUserUpdate} data - The updated user data.
* @returns {Promise<IDbStatusResult>} - A promise that resolves to the result of the update operation.
* @throws {Error} If an error occurs during the update operation.
*/
update(handler: MysqlHandler, data: IDbUser): Promise<IDbStatusResult> {
update(handler: MysqlHandler, data: IUserUpdate): Promise<IDbStatusResult> {
return new Promise((resolve, reject) => {
if (!data.id) return reject("Id is undefined");
if (data.id.length !== 36) return reject("Id invalid");
if (data.gdpr && typeof data.gdpr !== typeof Date) {
return reject("Invalid gdpr date.")
}
try {
const _values = [];
const _template = `
${data.username ? "`username` = ?," : null}
${data.username ? "`username` = ?," && _values.push(data.username) as unknown as void : null}
${data.firstname ? "`firstname` = ?," : null}
${data.lastname ? "`lastname` = ?," : null}
${data.dob ? "`dob` = ?," : null}
${data.email ? "`email` = ?," : null}
${data.is_mail_verified ? "`is_mail_verified` = ?," : null}
${data.is_admin ? "`is_admin` = ?," : null}
${data.gdpr ? "`gdpr` = ?," : null}
${data.hash ? "`hash` = ?" : null}`;
const _values = [
data.username,
data.firstname,
data.lastname,
data.dob,
data.email,
data.is_mail_verified,
data.is_admin,
data.gdpr,
data.hash,
data.id,
];
${data.gdpr ? "`gdpr` = ?," : null}`
const _sql = `UPDATE "users" SET ${_template} WHERE 'id' = ?`;
handler.execute(_sql, _values).then((result) => {

View File

@ -5,7 +5,6 @@ import { ErrorType, type ISError } from "@interfaces/services/ISError";
import CredentialService from "@services/credential.service";
import JwtService from "@services/jwt.service";
import MySqlService from "@services/mysql.service";
import MysqlService from "@services/mysql.service";
import { Logger } from "tslog";
import { v4 } from "uuid";
@ -28,7 +27,7 @@ async function getUserByEmail(targetEmail: string): Promise<IDbUser | ISError> {
try {
const dbUser = await MySqlService.User.getByEmail(DbHandler, targetEmail);
if (dbUser === undefined) {
logger.info(`User not found (${targetEmail})`);
logger.info(`\n\n> User not found (${targetEmail})\n`);
return {
error: ErrorType.NotFound,
message: "The user was not fund.",
@ -53,7 +52,7 @@ async function getUserByEmail(targetEmail: string): Promise<IDbUser | ISError> {
async function getUserFromIdService(id: string): Promise<IDbUser | ISError> {
try {
if (!id || id.length !== 36) {
logger.info(`Invalid ID (${id})`);
logger.info(`\n\n> Invalid ID (${id})\n`);
return {
error: ErrorType.InvalidData,
message: "Invalid ID length.",
@ -76,194 +75,201 @@ async function getUserFromIdService(id: string): Promise<IDbUser | ISError> {
}
}
//ToTest
/**
* Registers a new user.
*
* @param {IReqRegister} inputData - The input data for registration.
* @return {Promise<ISError | string>} - A Promise that resolves to either an error or a token.
*/
async function register(inputData: IReqRegister): Promise<ISError | string> {
if (inputData.password.length < 6) {
return {
error: ErrorType.InvalidData,
message: "Password must be at least 6 characters long.",
};
}
//TODO check Object content keys
const passwordHash = await CredentialService.hash(`${inputData.password}`);
try {
if (inputData.password.length < 6) {
return {
error: ErrorType.InvalidData,
message: "Password must be at least 6 characters long.",
};
}
// Does the new user has accepted GDPR ?
if (inputData.gdpr !== true) {
return {
error: ErrorType.InvalidData,
message: "GDPR acceptance is required.",
};
}
const currentDate = new Date();
const passwordHash = await CredentialService.hash(`${inputData.password}`);
// Check if exist and return
const dbUserIfExist: IDbUser | ISError = await getUserByEmail(
inputData.email,
);
if ("error" in dbUserIfExist) {
return {
error: dbUserIfExist.error,
message: dbUserIfExist.message,
};
}
if (dbUserIfExist.id) {
logger.info(
`User already exist for email "${inputData.email}".\n(${dbUserIfExist.username}::${dbUserIfExist.id})\n`,
// Does the new user has accepted GDPR ?
if (inputData.gdpr !== true) {
return {
error: ErrorType.InvalidData,
message: "GDPR acceptance is required.",
};
}
const currentDate = new Date();
// Check if exist and return
const dbUserIfExist: IDbUser | ISError = await getUserByEmail(
inputData.email,
);
return {
error: ErrorType.UnAuthorized,
message: "User already exists.",
};
}
const currentId = v4();
const NewUser = await MySqlService.User.insert(DbHandler, {
id: currentId,
email: inputData.email,
username: inputData.username,
firstname: inputData.firstName,
lastname: inputData.lastName,
dob: inputData.dob,
hash: passwordHash,
gdpr: currentDate,
is_admin: false,
is_mail_verified: false,
});
if ("error" in NewUser || NewUser.affectedRows === 0) {
if ("error" in dbUserIfExist) {
return {
error: dbUserIfExist.error,
message: dbUserIfExist.message,
};
}
if (dbUserIfExist.id) {
logger.info(
`\n\n> User already exist for email "${inputData.email}".\n(${dbUserIfExist.username}::${dbUserIfExist.id})\n`,
);
return {
error: ErrorType.UnAuthorized,
message: "User already exists.",
};
}
const currentId = v4();
const NewUser = await MySqlService.User.insert(DbHandler, {
id: currentId,
email: inputData.email,
username: inputData.username,
firstname: inputData.firstName,
lastname: inputData.lastName,
dob: inputData.dob,
hash: passwordHash,
gdpr: currentDate,
is_admin: false,
is_mail_verified: false,
});
if ("error" in NewUser || NewUser.affectedRows === 0) {
return {
error: ErrorType.DatabaseError,
message: "Error when inserting user in database.",
};
}
logger.info(`\n\n> New user created ! (${inputData.username}::${currentId})\n`);
// JWT
const token = await JwtService.sign(
{
sub: currentId,
},
{
alg: "HS512",
},
"1d",
"user",
);
return token;
} catch (err) {
logger.error(`\n\n${err}\n`);
return {
error: ErrorType.DatabaseError,
message: "Error when inserting user in database.",
message: "An unknown error occurred.",
};
}
logger.info(`New user created ! (${inputData.username}::${currentId})`);
// JWT
const token = await JwtService.sign(
{
sub: NewUser.id,
},
{
alg: "HS512",
},
"1d",
"user",
);
return token;
}
async function login(ReqData: IReqLogin) {
//const passwordHash = await getHashFromPassword(sanitizedData.password);
const dbUser = await MysqlService.User.getByUsername(
DbHandler,
ReqData.username,
);
if (!dbUser) {
console.log(`LoginService :> User does not exist (${ReqData.username})`);
//ToTest
/**
* Logs in a user with the provided input data.
*
* @param inputData - The input data for the login operation.
* @property email - The email of the user.
* @property password - The password of the user.
* @returns A promise that resolves to either an error or a token string.
* @throws {ISError} - If an error occurs in the login process.
* @throws {string} - If the login was successful, returns a token string.
*/
async function login(inputData: IReqLogin): Promise<ISError | string> {
try {
const dbUser = await getUserByEmail(inputData.email);
if ("error" in dbUser) {
return {
error: dbUser.error,
message: dbUser.message,
};
}
if (!dbUser.id) {
return {
error: ErrorType.NotFound,
message: "User not found.",
};
}
const isPasswordValid = await CredentialService.compare(
inputData.password,
dbUser.hash
);
if (!isPasswordValid) {
return {
error: ErrorType.UnAuthorized,
message: "Invalid password.",
};
}
const token = await JwtService.sign(
{
sub: dbUser.id,
p: [{
isAdmin: dbUser.is_admin,
gdpr: dbUser.gdpr
}]
},
{
alg: "HS512",
},
"1d",
"user"
);
return token;
} catch (err) {
logger.error(`\n\n${err}\n`);
return {
error: "userNotFound",
error: ErrorType.DatabaseError,
message: "An unknown error occurred.",
};
}
if (ReqData.password.length < 6) {
console.log("X");
console.log(`LoginService :> Invalid password (${ReqData.username})`);
return {
error: "invalidPassword",
};
}
const isPasswordValid = await CredentialService.compare(
ReqData.password,
dbUser.hash,
);
if (!isPasswordValid) {
console.log(isPasswordValid);
console.log(`LoginService :> Invalid password (${ReqData.username})`);
return {
error: "invalidPassword",
};
}
// biome-ignore lint/style/useConst: <explanation>
let userData = {
error: "none",
jwt: "",
user: {
id: dbUser.id,
username: dbUser.username,
displayName: dbUser.displayName,
},
};
userData.jwt = await JwtService.sign(
{
sub: dbUser.id,
},
{
alg: "HS512",
},
"7d",
"user",
);
console.log("USERDATA :>");
console.log(userData);
return userData;
}
//TOTest
/**
* Retrieves all users from the database.
*
* @async
* @function getAllUsersService
* @returns {Promise<{iat: number, users: Array<user>, length: number}>} - The response object containing the users array and its length.
* @returns {Promise<Array<IDbUser> | ISError>} The list of users, or an error object if an error occurred.
*/
async function getAllUsersService() {
const users = await Db.collection("users").find().toArray();
// biome-ignore lint/complexity/noForEach: <explanation>
users.forEach((user) => {
delete user.passwordHash;
delete user._id;
delete user.gdpr;
});
logger.info(`Query ${users.length} user(s)`);
return {
iat: Date.now(),
users: users,
length: users.length,
};
async function getAllUsersService(): Promise<Array<IDbUser> | ISError> {
try {
const allUsers = await MySqlService.User.getAll(DbHandler);
if (allUsers === undefined) {
logger.error(`Error retrieving all users.`);
return {
error: ErrorType.DatabaseError,
message: "An unknown error occurred.",
};
}
return allUsers;
} catch (err) {
logger.error(`\n\n${err}\n`);
return {
error: ErrorType.DatabaseError,
message: "An unknown error occurred.",
};
}
}
/**
* Edits a user in the database.
*
* @param {string} targetId - The ID of the user to be edited.
* @param {object} sanitizedData - The sanitized data to update the user with.
* @returns {object} - An object indicating the result of the operation.
* If the user is not found, the error property will be a string "userNotFound".
* Otherwise, the error property will be a string "none".
*/
async function editUserService(targetId, sanitizedData) {
if (sanitizedData.password) {
const passwordHash = await getHashFromPassword(sanitizedData.password);
delete sanitizedData.password;
logger.info(`Changing password for user "${targetId}"`);
sanitizedData.passwordHash = passwordHash;
}
const updatedUserResult = await Db.collection("users").updateOne(
{
id: targetId,
},
{
$set: sanitizedData,
},
);
if (updatedUserResult.modifiedCount === 0) {
logger.info(`EDIT :> User not found (${targetId})`);
async function editUserService(targetId, inputData: IDbUser): Promise<ISError | boolean> {
if (!targetId || targetId.length !== 36) {
logger.info(`\n\n> Invalid ID (${targetId})\n`);
return {
error: ErrorType.InvalidData,
message: "Invalid ID length.",
};
}
const dbUser = await MySqlService.User.getById(DbHandler, targetId)
if (!dbUser.id) {
return {
error: "userNotFound",
};
error: ErrorType.NotFound,
message: "User not found.",
};
}
logger.info(`EDIT :> User updated (${targetId})`);
return {
error: "none",
};
const result = await MySqlService.User.update(DbHandler, {
username: inputData.username,
firstname: inputData.firstname,
lastname: inputData.lastname,
dob: inputData.dob,
})
}
/**
@ -285,7 +291,8 @@ async function deleteUserService(targetId) {
}
}
logger.debug("Service loaded.");
logger.debug("\nService loaded.");
const UserService = {
register: register,
login: login,