Compare commits

...

11 Commits

Author SHA1 Message Date
1723a8588a
feat(controllers): update registerUser function in auth controller
This commit includes changes to improve the `registerUser` function in auth.controller.ts.
- Switched registration from displayName to email and added email validation
- Adjusted types and logging for better consistency and readability

Signed-off-by: Mathis <yidhra@tuta.io>
2024-05-02 12:23:20 +02:00
3fe6453b0c
style(utils): improve code readability in email validator
Improved code readability by formatting the regular expression in the `isEmail` function in `utils/validators/email.ts` and also ensured a newline at the end of the file.

Issue: #18
Signed-off-by: Mathis <yidhra@tuta.io>
2024-05-02 12:23:05 +02:00
98477c5f27
feat(interfaces): update user and request interfaces
- Renamed `is_mail_verified` to `is_email_verified` in `IDbUser`
- Replaced `username` with `email` in `IReqLogin`
- Commented out `dob` in `IReqRegister`
- Added new field `_questionMarksFields` in `IDbFactorize`

Issue: #18
Signed-off-by: Mathis <yidhra@tuta.io>
2024-05-02 12:22:43 +02:00
3472c59ac2
feat(services): refactor MysqlHandler to improve factorize method
Update the MysqlHandler class in the 'mysql.service.ts':
- Enhance the `factorize` method to handle 'id' in a special manner and create a string of '?' for prepared SQL queries.
- Refactor the `add` method to utilize the updated `factorize` method for constructing SQL queries.
- Update the return types of `getById` and `getByEmail` methods to return an array of IDbUser instead of a single IDbUser instance.
- Rename a private attribute 'Logger' to 'logger'.

Issue: #18
Signed-off-by: Mathis <yidhra@tuta.io>
2024-05-02 12:22:17 +02:00
3b6726113d
feat(services): enhance user service functions
- Modified the logic in `getUserByEmail` to handle multiple user instances.
- Altered the dob field to save as current date in inserted user records.
- Richened `getByEmailService` function to check email existence in the database.
- Adjusted `is_mail_verified` flag to `is_email_verified` within user insertion function.

Issue: #18
Signed-off-by: Mathis <yidhra@tuta.io>
2024-05-02 12:21:37 +02:00
d78b0aec4c
feat(utils): add email validator function
A new function `isEmail` has been added to the utils folder that validates if a given string input is a valid email address. This should help in reducing erroneous entries and improve data validation.

Signed-off-by: Mathis <yidhra@tuta.io>
2024-05-02 09:59:51 +02:00
ae6b25fbd6
style(services): improve code readability in user.service file
The user.service has been reformatted for better readability. Changes mainly include adding new lines and spaces to make the code more structured. No change in logic involved, purely cosmetic.

Signed-off-by: Mathis <yidhra@tuta.io>
2024-04-30 16:52:23 +02:00
d7f9cb0b37
feat(services): enhance readability and update factorize function for various elements in mysql.service
- The readability within mysql.service.ts file is improved by formatting multi-line functions.
- The factorize function for 'users', 'brands', 'vehicles', and 'categories' has been updated to enhance code quality.
- Import statements were restructured for better readability.

Signed-off-by: Mathis <yidhra@tuta.io>
2024-04-30 16:51:55 +02:00
cb1c2ee87c
feat(interfaces): add newline at end of IDbFactorize file
A newline has been added at the end of the `IDbFactorize.ts` file to conform to coding style conventions.

Signed-off-by: Mathis <yidhra@tuta.io>
2024-04-30 16:51:13 +02:00
23ce32cb6f
feat(services): refactor update methods in mysql service
This commit:
- Standardizes the data handling in all update methods, using `handler.factorize()`
- Moves gdpr date validation to the top of 'update' method to reject it earliest
- Removes the unused `_values` and `_template` variables.

Signed-off-by: Mathis <yidhra@tuta.io>
2024-04-30 16:50:40 +02:00
f6d18fc58d
feat(services): simplify SQL query keys generation in mysql service
Remove the condition to check 'id' while generating SQL query keys in the `mysql.service.ts`. Now, a map function is used directly resulting in cleaner and leaner code.

Issue: #18
Signed-off-by: Mathis <yidhra@tuta.io>
2024-04-30 16:39:43 +02:00
8 changed files with 221 additions and 174 deletions

View File

@ -1,7 +1,9 @@
import JwtService from "@services/jwt.service";
import type { IReqEditUserData } from "@interfaces/IReqEditUserData";
import type { IReqRegister } from "@interfaces/requests/IReqRegister";
import UserService from "@services/user.service";
import { isEmail } from "@utils/validators/email";
import type { Request, Response } from "express";
import { Logger } from "tslog";
@ -9,21 +11,8 @@ const logger = new Logger({
name: "AuthController",
});
//FIX Better return object interface
/**
* Registers a user with the given request data.
*
* @param {Request} req - The request object containing user data.
* @param {Response} res - The response object to send the registration result.
*
* @return {Promise} A promise that resolves to the registration result.
* It can have the following properties:
* - error: "gdprNotApproved" if GDPR is not approved
* - error: "exist" if the user already exists
* - Otherwise, the registered user data
*/
async function registerUser(req: Request, res: Response): Promise<unknown> {
const body = req.body;
const body: IReqRegister = req.body;
if (!body) {
logger.warn(`Invalid input data (${req.ip})`);
return res.type("application/json").status(400).json({
@ -35,7 +24,7 @@ async function registerUser(req: Request, res: Response): Promise<unknown> {
!body.username ||
!body.firstName ||
!body.lastName ||
!body.displayName
!body.email
) {
logger.warn(`Field(s) missing (${req.ip})`);
return res.type("application/json").status(400).json({
@ -43,13 +32,20 @@ async function registerUser(req: Request, res: Response): Promise<unknown> {
});
}
if (!isEmail(body.email)) {
logger.warn(`Invalid email format (${req.ip})`);
return res.type("application/json").status(400).json({
error: "Invalid email format",
});
}
let gdpr = false;
if (body.gdpr === true) {
gdpr = true;
}
const sanitizeData = {
const sanitizeData: IReqRegister = {
username: `${body.username}`,
displayName: `${body.displayName}`,
email: `${body.email.toLowerCase()}`,
gdpr: gdpr,
password: `${body.password}`,
firstName: `${body.firstName}`,
@ -74,7 +70,7 @@ async function registerUser(req: Request, res: Response): Promise<unknown> {
}
// SUCCESS
logger.info(`User registered successfully (${req.ip})`);
logger.info(`User registered successfully (${sanitizeData.username})`);
return res.type("application/json").status(201).json(RegisterServiceResult);
}

View File

@ -14,6 +14,12 @@ export interface IDbFactorizeOutput {
* @type {string}
*/
_keysTemplate: string;
/**
* The list of ? for the "VALUE" section
*/
_questionMarksFields: string;
/**
* The total number of fields.
*

View File

@ -5,7 +5,7 @@ export interface IDbUser {
lastname: string;
dob: Date;
email: string;
is_mail_verified: boolean;
is_email_verified: boolean;
is_admin: boolean;
gdpr: Date;
hash: string;

View File

@ -1,4 +1,4 @@
export interface IReqLogin {
username: string;
email: string;
password: string;
}

View File

@ -2,7 +2,7 @@ export interface IReqRegister {
username: string;
firstName: string;
lastName: string;
dob: Date;
/*dob: Date;*/
email: string;
gdpr?: boolean;
password: string;

View File

@ -1,14 +1,18 @@
import process from "node:process";
import type { IDbBrand } from "@interfaces/database/IDbBrand";
import type { IDbCategory } from "@interfaces/database/IDbCategory";
import type {
IDbFactorizeInput,
IDbFactorizeOutput,
} from "@interfaces/database/IDbFactorize";
import type { IDbModel } from "@interfaces/database/IDbModel";
import type { IDbStatusResult } from "@interfaces/database/IDbStatusResult";
import type { IDbUser } from "@interfaces/database/IDbUser";
import type { IDbVehicle } from "@interfaces/database/IDbVehicle";
import type { IUserUpdate } from "@interfaces/services/IUserUpdate";
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";
import { v4 } from "uuid";
const access: ConnectionOptions = {
host: `${process.env["MYSQL_HOST"]}`,
@ -20,7 +24,7 @@ const access: ConnectionOptions = {
class MysqlHandler {
private readonly handlerName: string;
private Logger: Logger<unknown>;
Logger: Logger<unknown>;
private Connection: Connection;
constructor(handlerName?: string) {
@ -34,7 +38,9 @@ class MysqlHandler {
this.Logger.error(`Error connecting to MySQL: ${err}`);
process.exit(1);
}
this.Logger.info(`\n\n> Connected to MySQL database (${access.database})\n`);
this.Logger.info(
`\n\n> Connected to MySQL database (${access.database})\n`,
);
});
}
closeConnection() {
@ -64,30 +70,45 @@ class MysqlHandler {
factorize(data: IDbFactorizeInput): Promise<IDbFactorizeOutput> {
return new Promise((resolve, reject) => {
try {
let _id = "";
// @ts-ignore
data.values.id ? delete data.values.id : null;
const _sqlQueryKeys = Object.keys(data.values).map((key: string) => {
if (key !== 'id') {
return `\'${key}\' = ?`
if (data.values.id) {
// @ts-ignore
_id = data.values.id;
// @ts-ignore
delete data.values.id;
}
return '';
})
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 _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(", ");
if (_id && _id.length > 2) {
this.Logger.trace(`Id post-pushed in factorized data`);
values.push(_id);
}
const questionMarksFields = Array(values.length)
.fill("?")
.join(", ")
.toString();
const factorizedOutput: IDbFactorizeOutput = {
_keysTemplate: sqlQueryKeys,
_questionMarksFields: questionMarksFields,
totalFields: _sqlQueryKeys.length,
_valuesArray: values
}
_valuesArray: values,
};
resolve(factorizedOutput);
} catch (err) {
if (data.throwOnError) throw new Error(`${err}`)
this.Logger.error(`\n|\n${err}\n|`)
reject(`${err}`)
if (data.throwOnError) throw new Error(`${err}`);
this.Logger.error(`\n|\n${err}\n|`);
reject(`${err}`);
}
})
});
}
/**
@ -155,28 +176,42 @@ const MySqlService = {
if (!data.id) return reject("Id is undefined");
if (data.id.length !== 36) return reject("Id invalid");
const _sql =
"INSERT INTO `users`(`id`,`username`, `firstname`, `lastname`, `dob`, `email`, `is_mail_verified`, `is_admin`, `gdpr`, `hash`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
const _values = [
data.id,
data.username,
data.firstname,
data.lastname,
data.dob,
data.email,
data.is_mail_verified,
data.is_admin,
data.gdpr,
data.hash,
];
// const _values = [
// data.id,
// data.username,
// data.firstname,
// data.lastname,
// data.dob,
// data.email,
// data.is_mail_verified,
// data.is_admin,
// data.gdpr,
// data.hash,
// ];
handler
.factorize({
values: data,
actionName: "Inserting new user",
})
.then((result) => {
const valuesArray = result._valuesArray;
const template = result._keysTemplate;
const _sql = `INSERT INTO users (${template + `, id`}) VALUES(${
result._questionMarksFields
})`;
try {
handler.execute(_sql, _values).then((result) => {
handler.Logger.trace(_sql);
handler.Logger.trace(valuesArray);
handler.execute(_sql, valuesArray).then((result) => {
return resolve(result as unknown as IDbStatusResult);
});
} catch (err: unknown) {
reject(err as Error);
}
});
});
},
/**
@ -188,38 +223,37 @@ const MySqlService = {
*/
update(handler: MysqlHandler, data: IUserUpdate): Promise<IDbStatusResult> {
return new Promise((resolve, reject) => {
if (data.gdpr && typeof data.gdpr !== typeof Date) {
return reject("Invalid gdpr date.");
}
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` = ?," && _values.push(data.username) as unknown as void : null}
${data.firstname ? "`firstname` = ?," : null}
${data.lastname ? "`lastname` = ?," : null}
${data.dob ? "`dob` = ?," : null}
${data.gdpr ? "`gdpr` = ?," : null}`
const _sql = `UPDATE "users" SET ${_template} WHERE 'id' = ?`;
handler.execute(_sql, _values).then((result) => {
handler
.factorize({
values: data,
actionName: `Update user ID::${data.id}`,
})
.then((result) => {
const _sql = `UPDATE "users" SET ${result._keysTemplate} WHERE 'id' = '${data.id}'`;
handler.execute(_sql, result._valuesArray).then((result) => {
return resolve(result as unknown as IDbStatusResult);
});
});
} catch (err: unknown) {
reject(err as Error);
}
});
},
getById(handler: MysqlHandler, userId: string): Promise<IDbUser> {
getById(handler: MysqlHandler, userId: string): Promise<Array<IDbUser>> {
return new Promise((resolve, reject) => {
if (userId.length !== 36) return reject("Id invalid");
const _sql = "SELECT * FROM `users` WHERE `id` = ?";
const _values = [userId];
try {
handler.execute(_sql, _values).then((result) => {
return resolve(result as unknown as IDbUser);
return resolve(result as unknown as Array<IDbUser>);
});
} catch (err: unknown) {
reject(err as Error);
@ -252,17 +286,17 @@ const MySqlService = {
*
* @param {MysqlHandler} handler - The MySQL database handler instance.
* @param {string} email - The email of the user to retrieve.
* @return {Promise<IDbUser>} - A promise that resolves to the retrieved user object.
* @return {Promise<Array<IDbUser>>} - A promise that resolves to the retrieved user object.
* @throws {Error} - If an error occurs while retrieving the user.
*/
getByEmail(handler: MysqlHandler, email: string): Promise<IDbUser> {
getByEmail(handler: MysqlHandler, email: string): Promise<Array<IDbUser>> {
return new Promise((resolve, reject) => {
if (!email) return reject("email is undefined");
const _sql = "SELECT * FROM `users` WHERE `email` = ?";
const _values = [email];
try {
handler.execute(_sql, _values).then((result) => {
return resolve(result as unknown as IDbUser);
return resolve(result as unknown as Array<IDbUser>);
});
} catch (err: unknown) {
reject(err as Error);
@ -364,16 +398,18 @@ const MySqlService = {
return new Promise((resolve, reject) => {
if (!data.id) return reject("Id is undefined");
if (data.id.length !== 36) return reject("Id invalid");
try {
handler.factorize({
handler
.factorize({
values: data,
actionName: `Update user ID::${data.id}`
actionName: `Update brand ID::${data.id}`,
})
const _sql = `UPDATE "brands" SET ${_template} WHERE 'id' = ?`;
handler.execute(_sql, _values).then((result) => {
.then((result) => {
const _sql = `UPDATE "brands" SET ${result._keysTemplate} WHERE 'id' = '${data.id}'`;
handler.execute(_sql, result._valuesArray).then((result) => {
return resolve(result as unknown as IDbStatusResult);
});
});
} catch (err: unknown) {
reject(err as Error);
}
@ -586,31 +622,18 @@ const MySqlService = {
return new Promise((resolve, reject) => {
if (!data.id) return reject("Id is undefined");
if (data.id.length !== 36) return reject("Id invalid");
try {
const _template = `
${data.slug_name ? "`slug_name` = ?," : null}
${data.display_name ? "`display_name` = ?," : null}
${data.brand_id ? "`brand_id` = ?," : null}
${data.category_id ? "`category_id` = ?," : null}
${data.image_blob ? "`image_blob` = ?," : null}
${data.is_trending ? "`is_trending` = ?," : null}
${data.base_price ? "`base_price` = ?," : null}`;
const _values = [
data.slug_name,
data.display_name,
data.brand_id,
data.category_id,
data.image_blob,
data.is_trending,
data.base_price,
data.id,
];
const _sql = `UPDATE "models" SET ${_template} WHERE 'id' = ?`;
handler.execute(_sql, _values).then((result) => {
handler
.factorize({
values: data,
actionName: `Update users ID::${data.id}`,
})
.then((result) => {
const _sql = `UPDATE "users" SET ${result._keysTemplate} WHERE 'id' = '${data.id}'`;
handler.execute(_sql, result._valuesArray).then((result) => {
return resolve(result as unknown as IDbStatusResult);
});
});
} catch (err: unknown) {
reject(err as Error);
}
@ -686,25 +709,18 @@ const MySqlService = {
return new Promise((resolve, reject) => {
if (!data.id) return reject("Id is undefined");
if (data.id.length !== 36) return reject("Id invalid");
try {
const _template = `
${data.model_id ? "`model_id` = ?," : null}
${data.plate_number ? "`plate_number` = ?," : null}
${data.odometer ? "`odometer` = ?," : null}
${data.health_state ? "`health_state` = ?," : null}`;
const _values = [
data.model_id,
data.plate_number,
data.odometer,
data.health_state,
data.id,
];
const _sql = `UPDATE "vehicles" SET ${_template} WHERE 'id' = ?`;
handler.execute(_sql, _values).then((result) => {
handler
.factorize({
values: data,
actionName: `Update vehicle ID::${data.id}`,
})
.then((result) => {
const _sql = `UPDATE "vehicles" SET ${result._keysTemplate} WHERE 'id' = '${data.id}'`;
handler.execute(_sql, result._valuesArray).then((result) => {
return resolve(result as unknown as IDbStatusResult);
});
});
} catch (err: unknown) {
reject(err as Error);
}
@ -813,17 +829,18 @@ const MySqlService = {
return new Promise((resolve, reject) => {
if (!data.id) return reject("Id is undefined");
if (data.id.length !== 36) return reject("Id invalid");
try {
const _template = `
${data.slug_name ? "`slug_name` = ?," : null}
${data.display_name ? "`display_name` = ?," : null}`;
const _values = [data.slug_name, data.display_name, data.id];
const _sql = `UPDATE "categories" SET ${_template} WHERE 'id' = ?`;
handler.execute(_sql, _values).then((result) => {
handler
.factorize({
values: data,
actionName: `Update category ID::${data.id}`,
})
.then((result) => {
const _sql = `UPDATE "categories" SET ${result._keysTemplate} WHERE 'id' = '${data.id}'`;
handler.execute(_sql, result._valuesArray).then((result) => {
return resolve(result as unknown as IDbStatusResult);
});
});
} catch (err: unknown) {
reject(err as Error);
}

View File

@ -26,14 +26,18 @@ const DbHandler = new MySqlService.Handler("UserService");
async function getUserByEmail(targetEmail: string): Promise<IDbUser | ISError> {
try {
const dbUser = await MySqlService.User.getByEmail(DbHandler, targetEmail);
if (dbUser === undefined) {
if (dbUser.length === 0) {
logger.info(`\n\n> User not found (${targetEmail})\n`);
return {
error: ErrorType.NotFound,
message: "The user was not fund.",
};
}
return dbUser;
if (dbUser.length === 1 && dbUser[0]) return dbUser[0];
return {
error: ErrorType.ServiceError,
message: "To many user found, suspicious.",
};
} catch (err) {
logger.error(err);
return {
@ -135,17 +139,18 @@ async function register(inputData: IReqRegister): Promise<ISError | string> {
};
}
const currentId = v4();
logger.info(`\n\n> Trying to insert a new user... (${currentId})\n`);
const NewUser = await MySqlService.User.insert(DbHandler, {
id: currentId,
email: inputData.email,
username: inputData.username,
firstname: inputData.firstName,
lastname: inputData.lastName,
dob: inputData.dob,
dob: new Date(),
hash: passwordHash,
gdpr: currentDate,
is_admin: false,
is_mail_verified: false,
is_email_verified: false,
});
if ("error" in NewUser || NewUser.affectedRows === 0) {
return {
@ -153,7 +158,9 @@ async function register(inputData: IReqRegister): Promise<ISError | string> {
message: "Error when inserting user in database.",
};
}
logger.info(`\n\n> New user created ! (${inputData.username}::${currentId})\n`);
logger.info(
`\n\n> New user created ! (${inputData.username}::${currentId})\n`,
);
// JWT
const token = await JwtService.sign(
@ -204,7 +211,7 @@ async function login(inputData: IReqLogin): Promise<ISError | string> {
}
const isPasswordValid = await CredentialService.compare(
inputData.password,
dbUser.hash
dbUser.hash,
);
if (!isPasswordValid) {
return {
@ -215,16 +222,18 @@ async function login(inputData: IReqLogin): Promise<ISError | string> {
const token = await JwtService.sign(
{
sub: dbUser.id,
p: [{
p: [
{
isAdmin: dbUser.is_admin,
gdpr: dbUser.gdpr
}]
gdpr: dbUser.gdpr,
},
],
},
{
alg: "HS512",
},
"1d",
"user"
"user",
);
return token;
} catch (err) {
@ -236,6 +245,16 @@ async function login(inputData: IReqLogin): Promise<ISError | string> {
}
}
async function getByEmailService(email: string): Promise<IDbUser | false> {
const dbUser = await MySqlService.User.getByEmail(DbHandler, email);
if (dbUser === undefined) {
logger.trace(`\n\n> User not found in DB (${email})\n`);
return false;
}
logger.trace(dbUser);
return dbUser;
}
//TOTest
/**
* Retrieves all users from the database.
@ -262,7 +281,10 @@ async function getAllUsersService(): Promise<Array<IDbUser> | ISError> {
}
}
async function editUserService(targetId, inputData: IDbUser): Promise<ISError | boolean> {
async function editUserService(
targetId,
inputData: IDbUser,
): Promise<ISError | boolean> {
if (!targetId || targetId.length !== 36) {
logger.info(`\n\n> Invalid ID (${targetId})\n`);
return {
@ -270,7 +292,7 @@ async function editUserService(targetId, inputData: IDbUser): Promise<ISError |
message: "Invalid ID length.",
};
}
const dbUser = await MySqlService.User.getById(DbHandler, targetId)
const dbUser = await MySqlService.User.getById(DbHandler, targetId);
if (!dbUser.id) {
return {
error: ErrorType.NotFound,
@ -282,7 +304,7 @@ async function editUserService(targetId, inputData: IDbUser): Promise<ISError |
firstname: inputData.firstname,
lastname: inputData.lastname,
dob: inputData.dob,
})
});
}
/**
@ -311,6 +333,7 @@ const UserService = {
login: login,
getAll: getAllUsersService,
getFromId: getUserFromIdService,
getByEmail: getByEmailService,
edit: editUserService,
delete: deleteUserService,
};

View File

@ -0,0 +1,5 @@
export function isEmail(email: string) {
const re =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(String(email).toLowerCase());
}