Compare commits

...

6 Commits

Author SHA1 Message Date
2a33f45257
feat(services): 🚀 UserService 2024-04-23 15:13:23 +02:00
742330d6fe
refactor(services): 🏗️ var name change
#9
2024-04-23 15:12:41 +02:00
71c20c8a06
fix(services): 🐛 types on return value 2024-04-23 15:11:52 +02:00
3b41cf1c5a
feat(routes): 🚀 add controllers call on routes 2024-04-23 15:11:14 +02:00
b48b34e4e5
feat(interfaces): IReqEditUserData 2024-04-23 15:10:37 +02:00
750e36e363
refactor: AuthController
#9
2024-04-23 15:09:26 +02:00
6 changed files with 273 additions and 26 deletions

View File

@ -4,6 +4,7 @@ import JwtService from "@services/jwt.service";
import {Logger} from "tslog";
import type {Request, Response} from "express";
import UserService from "@services/user.service";
import {IReqEditUserData} from "@interfaces/IReqEditUserData";
const logger = new Logger({ name: "AuthController" });
@ -233,7 +234,7 @@ async function getUser(req: Request, res: Response) {
//TODO - Implement re-auth by current password in case of password change
async function editUser(req: Request, res: Response) {
const body = req.body;
const body: IReqEditUserData | null = req.body;
if (!body) {
return res
.type('application/json')
@ -278,12 +279,16 @@ async function editUser(req: Request, res: Response) {
}
//TODO Interface
const modifiedData= {}
const modifiedData = {
}
//@ts-ignore
if (body.firstName) modifiedData.firstName = `${body.firstName}`;
//@ts-ignore
if (body.lastName) modifiedData.lastName = `${body.lastName}`;
//@ts-ignore
if (body.displayName) modifiedData.displayName = `${body.displayName}`;
// Case handled with hashing by the service.
if (body.password) modifiedData.password = `${body.password}`;
//TODO Case handled with hashing by the service.
//if (body.password) modifiedData.password = `${body.password}`;
//Call service
const EditUserServiceResult = await UserService.edit(`${targetUserId}`, modifiedData);

View File

@ -0,0 +1,6 @@
export interface IReqEditUserData {
firstName?: string;
lastName?: string;
displayName?: string;
password?: string;
}

View File

@ -1,36 +1,38 @@
import express, {type Router} from "express";
import UserGuard from "@validators/UserGuard";
import AdminGuard from "@validators/AdminGuard";
import AuthController from "@controllers/AuthController";
const router: Router = express.Router();
router.route('/login').post()
router.route('/register').post()
router.route('/login').post(AuthController.login)
router.route('/register').post(AuthController.register)
// PATCH
//TODO - To test
router.route('/me')
.patch(UserGuard)
.patch(UserGuard, AuthController.editUser)
// GET
router.route('/me')
.get(UserGuard)
.get(UserGuard, AuthController.getSelf)
// DELETE
router.route('/me')
.delete(UserGuard)
.delete(UserGuard, AuthController.deleteSelf)
// GET
router.route('/all')
.get(AdminGuard)
.get(AdminGuard, AuthController.getAllUsers)
// GET
router.route('/user/:targetId')
.get(AdminGuard)
.patch(AdminGuard)
.delete(AdminGuard)
.get(AdminGuard, AuthController.getUser)
.patch(AdminGuard, AuthController.editUser)
.delete(AdminGuard, AuthController.deleteUser)
export default router

View File

@ -8,10 +8,10 @@ const logger = new Logger({ name: "JwtService" });
*
* @param {string | Uint8Array} jwt
* - The JWT token to verify.
* @returns {Promise<null | object>}
* @returns {Promise<null | JWTPayload>}
* - The payload of the verified JWT token or null if verification fails.
*/
async function JwtVerifyService(jwt: string | Uint8Array): Promise<null | object> {
async function JwtVerifyService(jwt: string | Uint8Array): Promise<null | JWTPayload> {
try {
const result = await Jose.jwtVerify(
jwt,

View File

@ -13,7 +13,7 @@ const access: ConnectionOptions = {
};
class MySqlHandler {
class MysqlHandler {
private readonly handlerName: string;
private Logger: Logger<unknown>
private Connection: Connection;
@ -81,9 +81,9 @@ class MySqlHandler {
}
const MySqlService = {
Handler : MySqlHandler,
Handler : MysqlHandler,
User: {
insert(handler: MySqlHandler, userData: DbUserData) {
insert(handler: MysqlHandler, userData: DbUserData) {
return new Promise((resolve, reject) => {
const _now = new Date()
const _sql = "INSERT INTO `users`(`username`, `displayName`, `firstName`, `lastName`, `email`, `passwordHash`, `isAdmin`, `isDisabled`, `dob`, `gdpr`, `iat`, `uat`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
@ -109,7 +109,7 @@ const MySqlService = {
})
},
update(handler: MySqlHandler, userData: DbUserData) {
update(handler: MysqlHandler, userData: DbUserData) {
return new Promise((resolve, reject) => {
//@ts-ignore
@ -142,7 +142,7 @@ const MySqlService = {
});
},
getById(handler: MySqlHandler, userId: string): Promise<DbUserData> {
getById(handler: MysqlHandler, userId: string): Promise<DbUserData> {
return new Promise((resolve, reject) => {
const _sql = "SELECT * FROM `users` WHERE `id` = ?";
const _values = [userId];
@ -154,7 +154,7 @@ const MySqlService = {
});
},
getAll(handler: MySqlHandler): Promise<Array<DbUserData>> {
getAll(handler: MysqlHandler): Promise<Array<DbUserData>> {
return new Promise((resolve, reject) => {
const _sql = "SELECT * FROM `users`";
try {
@ -165,7 +165,7 @@ const MySqlService = {
});
},
getByUsername(handler: MySqlHandler, username: string) {
getByUsername(handler: MysqlHandler, username: string) {
return new Promise((resolve, reject) => {
const _sql = "SELECT * FROM `users` WHERE `username` = ?";
const _values = [username];
@ -177,7 +177,7 @@ const MySqlService = {
});
},
getByEmail(handler: MySqlHandler, email: string) {
getByEmail(handler: MysqlHandler, email: string) {
return new Promise((resolve, reject) => {
const _sql = "SELECT * FROM `users` WHERE `email` = ?";
const _values = [email];
@ -189,7 +189,7 @@ const MySqlService = {
});
},
getByDisplayName(handler: MySqlHandler, displayName: string) {
getByDisplayName(handler: MysqlHandler, displayName: string) {
return new Promise((resolve, reject) => {
const _sql = "SELECT * FROM `users` WHERE `displayName` = ?";
const _values = [displayName];
@ -201,7 +201,7 @@ const MySqlService = {
});
},
getAdminStateForId(handler: MySqlHandler, userId: string) : Promise<boolean> {
getAdminStateForId(handler: MysqlHandler, userId: string) : Promise<boolean> {
return new Promise((resolve, reject) => {
const _sql = "SELECT `isAdmin` FROM `users` WHERE `id` = ?";
const _values = [userId];
@ -217,7 +217,7 @@ const MySqlService = {
});
},
delete(handler: MySqlHandler, userId: string) {
delete(handler: MysqlHandler, userId: string) {
return new Promise((resolve, reject) => {
const _sql = "DELETE FROM `users` WHERE `id` = ?";
const _values = [userId];

View File

@ -0,0 +1,234 @@
import {Logger} from "tslog";
import Argon2id from "@node-rs/argon2";
import MySqlService from "@services/mysql.service";
const logger = new Logger({ name: "UserService" });
const DbHandler = new MySqlService.Handler('UserService')
/**
* Retrieves a user object from the database based on the given username.
*
* @param {string} username - The username of the user to retrieve.
* @returns {Promise<Object | null>} - The user object if found, or null if not found.
*/
async function getUserFromUsername(username: string): Promise<object | null> {
const dbUser = await MySqlService.User.getByUsername(DbHandler, username)
if (dbUser === undefined) return null;
return dbUser;
}
async function getUserFromIdService(id: string | undefined) {
const dbUser = await MySqlService.User.getById(DbHandler, id);
if (dbUser === undefined) return null;
return dbUser;
}
/**
* Registers a new user by creating a UserService object, generating a JWT token, and inserting the user into the database.
*
* @param {Object} sanitizedData - The sanitized user data.
* @param {string} sanitizedData.username - The username of the new user.
* @param {string} sanitizedData.displayName - The display namcoe of the new user.
* @param {string} sanitizedData.firstName
* @param {string} sanitizedData.lastName
* @param {string} sanitizedData.password - The password of the new user.
* @param {boolean} sanitizedData.gdpr - Indicates whether the new user has accepted GDPR.
*
* @returns {Object} - An object containing the registered user's data and JWT token.
* @returns {string} error - The error name, if any. "none" if registration was successful.
* @returns {string|null} jwt - The JWT token for the registered user. Null if registration was not successful.
* @returns {Object|null} user - The registered user's data. Null if registration was not successful.
* @returns {string|null} user.id - The ID of the registered user. Null if registration was not successful.
* @returns {string|null} user.username - The username of the registered user. Null if registration was not successful.
* @returns {string|null} user.displayName - The display name of the registered user. Null if registration was not successful.
*/
async function RegisterService(sanitizedData) {
if (sanitizedData.password.length < 6) {
logger.info(`REGISTER :> Invalid password (${sanitizedData.username})`)
return { error: "invalidPassword" };
}
const passwordHash = await getHashFromPassword(sanitizedData.password)
// Does the new user has accepted GDPR ?
if (sanitizedData.gdpr !== true) {
logger.info(`REGISTER :> GDPR not validated (${sanitizedData.username})`)
return { error: "gdprNotApproved" }
}
// Check if exist and return
const dbUserIfExist = await getUserFromUsername(sanitizedData.username)
if (dbUserIfExist) {
logger.info(`REGISTER :> User exist (${dbUserIfExist.username})\n ID:${dbUserIfExist.id}`)
return { error: "exist" }
}
const currentDate = new Date();
// New UserService (class)
const NewUser = new User(sanitizedData.username, sanitizedData.displayName, passwordHash, currentDate);
NewUser.setFirstName(sanitizedData.firstName);
NewUser.setLastName(sanitizedData.lastName);
// JWT
const alg = 'HS512'
const token = await JwtSign({
sub: NewUser.id
}, alg,
'1d',
'user')
const userData = {
error: "none",
jwt: token,
user: {
id: NewUser.id,
username: NewUser.username,
displayName: NewUser.displayName,
firstName: NewUser.firstName,
lastName: NewUser.lastName
}};
logger.info(userData)
await Db.collection("users").insertOne(NewUser);
logger.info(`REGISTER :> Inserted new user (${NewUser.username})`)
return userData
}
/**
* Performs the login process by verifying the provided credentials.
* @param {Object} sanitizedData - The sanitized user login data.
* @param {string} sanitizedData.username - The username provided by the user.
* @param {string} sanitizedData.password - The password provided by the user.
* @returns {Object} - The login result object.
* @returns {string} result.error - The error code if there is an error during the login process.
* @returns {string} result.jwt - The JSON Web Token (JWT) generated upon successful login.
* @returns {Object} result.user - The user information.
* @returns {number} result.user.id - The ID of the user.
* @returns {string} result.user.username - The username of the user.
* @returns {string} result.user.displayName - The display name of the user.
*/
async function LoginService(sanitizedData) {
//const passwordHash = await getHashFromPassword(sanitizedData.password);
const dbUser = await getUserFromUsername(sanitizedData.username);
if (!dbUser) {
console.log(`LoginService :> User does not exist (${sanitizedData.username})`);
return { error: "userNotFound" };
}
if (sanitizedData.password.length < 6) {
console.log('X')
console.log(`LoginService :> Invalid password (${sanitizedData.username})`);
return { error: "invalidPassword" };
}
const isPasswordValid = await Argon2id.verify(
Buffer.from(dbUser.passwordHash),
Buffer.from(sanitizedData.password),
{
secret: Buffer.from(`${process.env.HASH_SECRET}`),
algorithm: 2
});
if (!isPasswordValid) {
console.log(isPasswordValid)
console.log(`LoginService :> Invalid password (${sanitizedData.username})`);
return { error: "invalidPassword" };
}
// biome-ignore lint/style/useConst: <explanation>
let userData = {
error: "none",
jwt: null,
user: {
id: dbUser.id,
username: dbUser.username,
displayName: dbUser.displayName,
}
};
const alg = 'HS512';
userData.jwt = await JwtSign({sub: dbUser.id}, alg, '1d', 'user')
console.log("USERDATA :>");
console.log(userData);
return userData;
}
/**
* 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.
*/
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
}
}
/**
* 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})`);
return { error: "userNotFound" };
}
logger.info(`EDIT :> User updated (${targetId})`);
return { error: "none" };
}
/**
* Delete a user from the database.
*
* @param {string} targetId - The ID of the user to be deleted.
* @return {Promise<boolean>} - A promise that resolves to true if the user is successfully deleted, or false if an error occurs.
*/
async function deleteUserService(targetId) {
logger.info(`Deleting user ${targetId}`)
try {
await Db.collection("users").deleteOne({id: targetId});
return true
} catch (e) {
logger.warn(e)
return false
}
}
const UserService = {
register: RegisterService,
login: LoginService,
getAll: getAllUsersService,
getFromId: getUserFromIdService,
edit: editUserService,
delete: deleteUserService
}
export default UserService;