diff --git a/.env.exemple b/.env.exemple index 85966a4..17ad8bc 100644 --- a/.env.exemple +++ b/.env.exemple @@ -2,6 +2,9 @@ APP_PORT: 3000 DEBUG: true CONTEXT: dev +HASH_SECRET: '' +JWT_SECRET: '' + MYSQL_PORT: 3434 MYSQL_HOST: 'localhost' MYSQL_USERNAME: 'apdbqixmwnsesdj' diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml index 34395df..b35b44f 100644 --- a/.idea/sqldialects.xml +++ b/.idea/sqldialects.xml @@ -1,7 +1,7 @@ - + \ No newline at end of file diff --git a/maria.sql b/maria.sql index 31a1058..c3a456b 100644 --- a/maria.sql +++ b/maria.sql @@ -6,7 +6,7 @@ create table follows target_id varchar(36) not null comment 'identifier of the followed user', iat timestamp default current_timestamp() not null comment 'timestamp of the follow action' ) - comment 'follows of users'; + comment 'follows of users' collate = utf8mb4_unicode_ci; create table users ( diff --git a/package.json b/package.json index 20151fe..1497340 100644 --- a/package.json +++ b/package.json @@ -13,13 +13,17 @@ }, "dependencies": { "@node-rs/argon2": "^1.8.3", + "axios": "^1.6.8", "compression": "^1.7.4", "cors": "^2.8.5", "express": "^4.19.2", "helmet": "^7.1.0", + "jose": "^5.3.0", "mongodb": "^6.6.1", "morgan": "^1.10.0", "mysql2": "^3.9.7", + "randomatic": "^3.1.1", + "react-hook-form": "^7.51.4", "tslog": "^4.9.2", "uuid": "^9.0.1" }, @@ -29,6 +33,7 @@ "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/node": "^20.12.11", + "@types/randomatic": "^3.1.5", "@types/uuid": "^9.0.8", "arkit": "^1.6.4" } diff --git a/src/app.ts b/src/app.ts index 4e36e07..5f3213f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -5,7 +5,7 @@ import compression from "compression"; import cors from "cors"; import express, { type Express } from "express"; import helmet from "helmet"; -import {justForTesting} from "@services/databases/databases.service"; +import AuthRouter from "@routers/auth.router"; console.log("\n\n> Starting...\n\n\n\n"); @@ -26,10 +26,10 @@ app.use( ); app.use(helmet.xXssProtection()); -// parse json request body +// parse json requests body app.use(express.json()); -// parse urlencoded request body +// parse urlencoded requests body app.use( express.urlencoded({ extended: true, @@ -40,7 +40,7 @@ app.use( app.use(compression()); try { - //app.use("/auth", AuthRouter); + app.use("/auth", AuthRouter); logger.info("Routers loaded !"); } catch (err) { logger.error(err); @@ -61,6 +61,4 @@ try { } catch (error) { logger.error(`Server failed to start: ${error}`); process.exit(1); -} - -justForTesting.getAllUsers().then(()=>{}) \ No newline at end of file +} \ No newline at end of file diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts new file mode 100644 index 0000000..9d77d6e --- /dev/null +++ b/src/controllers/auth.controller.ts @@ -0,0 +1,66 @@ +import {Request, Response} from "express"; +import {LogsUtils} from "@utils/logs.util"; +import {HttpStatusCode} from "axios"; +import {IRegisterInput} from "@interfaces/services/register.types"; +import ValidatorsUtils from "@utils/validators.util"; +import UsersService from "@services/users.service"; +import RegisterService from "@services/authentication/register.service"; + +const logs = new LogsUtils('AuthController') + +async function registerController(req: Request, res: Response) { + const body = req.body; + if (!body.username || !body.email || !body.password) { + logs.warn('Missing required fields', req.ip); + return res.status(HttpStatusCode.BadRequest).json({ error: 'Missing required fields' }); + } + if (!ValidatorsUtils.isEmail(body.email)) { + logs.warn('Invalid email format', req.ip); + return res.status(HttpStatusCode.BadRequest).json({ error: 'Invalid email format' }); + } + if (!ValidatorsUtils.isUsername(body.username)) { + logs.warn('Invalid username format', req.ip); + return res.status(HttpStatusCode.BadRequest).json({ error: 'Invalid username format' }); + } + if (!ValidatorsUtils.isPassword(body.password)) { + logs.warn('Invalid password format', req.ip); + return res.status(HttpStatusCode.BadRequest).json({ error: 'Invalid password format' }); + } + + const UserIfExist = { + byEmail: await UsersService.get.byEmail(body.email), + byUsername: await UsersService.get.byUsername(body.username) + } + + if (UserIfExist.byEmail.length > 0 || UserIfExist.byUsername.length > 0) { + logs.warn('User already exists', req.ip); + return res.status(HttpStatusCode.Found).json({ error: 'User already exists' }); + } + + const data: IRegisterInput = { + username: body.username, + email: body.email, + password: body.password + } + if (body.displayName) data.displayName = body.displayName; + + const registerResult = await RegisterService(data) + + if (!registerResult.success) { + logs.error(registerResult.message, req.ip); + return res.status(HttpStatusCode.InternalServerError).json({ error: registerResult.message }); + } + logs.info('User registered successfully', req.ip); + return res.status(HttpStatusCode.Created).json({ + message: 'User registered successfully', + token: registerResult.token, + id: registerResult.id + }); +} + + +const AuthController = { + register: registerController +} + +export default AuthController; \ No newline at end of file diff --git a/src/interfaces/services/register.types.ts b/src/interfaces/services/register.types.ts new file mode 100644 index 0000000..e3fe094 --- /dev/null +++ b/src/interfaces/services/register.types.ts @@ -0,0 +1,13 @@ +export interface IRegisterInput { + username: string, + displayName?: string, + email: string, + password: string +} + +export interface IRegisterOutput { + success: boolean, + message: string + id?: string, + token?: string +} \ No newline at end of file diff --git a/src/routers/auth.router.ts b/src/routers/auth.router.ts new file mode 100644 index 0000000..df1e91c --- /dev/null +++ b/src/routers/auth.router.ts @@ -0,0 +1,10 @@ +import AuthController from "@controllers/auth.controller"; +import express, {type Router} from "express"; + + +const AuthRouter: Router = express.Router(); + +//AuthRouter.route("/login").post(AuthController.login); +AuthRouter.route("/register").post(AuthController.register); + +export default AuthRouter; \ No newline at end of file diff --git a/src/services/authentication/intcode.service.ts b/src/services/authentication/intcode.service.ts index 13fcb74..b8742ab 100644 --- a/src/services/authentication/intcode.service.ts +++ b/src/services/authentication/intcode.service.ts @@ -1,9 +1,10 @@ -import randomatic from "randomatic"; - - const IntCodeService = { generate: () => { - return randomatic('0', 6) + const a = Math.floor(Math.random() * Date.now()); + const b = a.toString().replace(/0/g, ''); + const c = b.split('') + const code = c.join('').slice(0, 6).toString() + return Number.parseInt(code) } } diff --git a/src/services/authentication/register.service.ts b/src/services/authentication/register.service.ts new file mode 100644 index 0000000..a24ec55 --- /dev/null +++ b/src/services/authentication/register.service.ts @@ -0,0 +1,46 @@ +import {IRegisterInput, IRegisterOutput} from "@interfaces/services/register.types"; +import {UserInDatabase} from "@interfaces/db/mariadb.interface"; +import CredentialService from "@services/authentication/credentials.service"; +import IntCodeService from "@services/authentication/intcode.service"; +import {v4} from "uuid"; +import {DatabasesService} from "@services/databases/databases.service"; +import JwtService from "@services/authentication/jwt.service"; + +const db = new DatabasesService('OnlyDevs') +//TODO Logs + +async function registerService(data: IRegisterInput): Promise { + const User: UserInDatabase = { + id: v4(), + username: data.username, + display_name: data.displayName || data.username, + hash: await CredentialService.hash(data.password), + email: data.email, + email_activation: IntCodeService.generate() + } + + const dbResult = await db.insertUser(User) + + if (dbResult) { + //await sendActivationEmail(User.email, User.email_activation); + const token = await JwtService.sign({ + sub: User.id, + iat: Date.now(), + }, { + alg: "HS256" + }, '7d', 'Registered user') + return { + success: true, + message: "User registered successfully", + id: User.id, + token: token + }; + } else { + return { + success: false, + message: "Failed to register user", + }; + } +} + +export default registerService; \ No newline at end of file diff --git a/src/services/databases/databases.service.ts b/src/services/databases/databases.service.ts index 77dd59b..357e839 100644 --- a/src/services/databases/databases.service.ts +++ b/src/services/databases/databases.service.ts @@ -1,6 +1,7 @@ import {MariadbService} from "@services/databases/mariadb.service"; import {MongodbService} from "@services/databases/mongodb.service"; import {LogsUtils} from "@utils/logs.util"; +import {UserInDatabase} from "@interfaces/db/mariadb.interface"; interface MariaDbStatusResult { fieldCount: number; @@ -12,7 +13,7 @@ interface MariaDbStatusResult { changedRows: number; } -class DatabasesService { +export class DatabasesService { private readonly _appName; private readonly maria: MariadbService; private readonly mongo: MongodbService; @@ -25,10 +26,49 @@ class DatabasesService { } async getAllUsers() { - const result = await this.maria.query("SELECT * FROM users"); - this.logs.debug('Fetching users from database...', result) + const result: Array = await this.maria.query("SELECT * FROM users") as unknown as Array; + this.logs.debug('Fetching users from database...', `${result?.length} user(s) found.`) return result; } + + async getUserById(id: number) { + const result: Array = await this.maria.execute(`SELECT * FROM users WHERE id = ?`, [id]) as unknown as Array; + this.logs.debug(`Fetching user with id ${id} from database...`, `${result?.length} user(s) found.`); + return result; + } + + async getUserByUsername(username: string) { + const result: Array = await this.maria.execute(`SELECT * FROM users WHERE username = ?`, [username]) as unknown as Array; + this.logs.debug(`Fetching user with username "${username}" from database...`, `${result?.length} user(s) found.`); + return result; + } + + async getUserByEmail(email: string) { + const result: Array = await this.maria.execute(`SELECT * FROM users WHERE email = ?`, [email]) as unknown as Array; + this.logs.debug(`Fetching user with email "${email}" from database...`, `${result?.length} user(s) found.`); + return result; + } + + async insertUser(user: UserInDatabase): Promise { + const factorized = await this.maria.factorize({ + values: user, + actionName: 'Inserting a user', + throwOnError: true + }); + //TODO if no id stop + const valuesArray = factorized._valuesArray; + const questionMarks = factorized._questionMarksFields + const keysArray = factorized._keysTemplate; + const _sql = `INSERT INTO users (${keysArray}) VALUES (${questionMarks})` + try { + const result = await this.maria.execute(_sql, valuesArray) as unknown as MariaDbStatusResult + this.logs.debug(`Inserted new user with id ${user.id}`, `Rows affected: ${result.affectedRows}`); + return true + } catch (err) { + this.logs.softError('An error occurred.', err) + return false + } + } } export const justForTesting = new DatabasesService('OnlyDevs') \ No newline at end of file diff --git a/src/services/databases/mariadb.service.ts b/src/services/databases/mariadb.service.ts index 0fa2fc6..07fe910 100644 --- a/src/services/databases/mariadb.service.ts +++ b/src/services/databases/mariadb.service.ts @@ -56,6 +56,7 @@ export class MariadbService { factorize(data: ISqlFactorizeInput): Promise { return new Promise((resolve, reject) => { try { + console.log(data); let _id = ""; // @ts-ignore if (data.values.id) { @@ -67,15 +68,16 @@ export class MariadbService { const _sqlQueryKeys = Object.keys(data.values).map( (key: string) => `${key}`, ); - const values = Object.values(data.values).map((val) => val); + const values = Object.values(data.values).map((val) => val.toString()); this.logs.debug( `Factorized ${_sqlQueryKeys.length} keys for a prepare Query.`, `Action: ${data.actionName}`, ); - const sqlQueryKeys = _sqlQueryKeys.join(", "); - if (_id && _id.length > 2) { + if (_id.length > 0) { this.logs.trace(`Id post-pushed in factorized data.`, _id); values.push(_id); + _sqlQueryKeys.push('id') } + const sqlQueryKeys = _sqlQueryKeys.join(", "); const questionMarksFields = Array(values.length) .fill("?") @@ -113,11 +115,9 @@ export class MariadbService { values, (err: mysql.QueryError | null, results: mysql.QueryResult) => { if (err) { - this.logs.error(`Error executing query:`, err); reject(err); - } else { - resolve(results); } + resolve(results); }, ); }); diff --git a/src/services/users.service.ts b/src/services/users.service.ts new file mode 100644 index 0000000..3e2df0d --- /dev/null +++ b/src/services/users.service.ts @@ -0,0 +1,28 @@ +import {DatabasesService} from "@services/databases/databases.service"; + + +const db = new DatabasesService('OnlyDevs') + +async function getByEmail(email: string) { + return await db.getUserByEmail(email) +} + +async function getByUsername(username: string) { + return await db.getUserByUsername(username) +} + +async function getAll() { + return db.getAllUsers() +} + +const get = { + byUsername: getByUsername, + byEmail: getByEmail, + all: getAll +} + +const UsersService = { + get +} + +export default UsersService; \ No newline at end of file diff --git a/src/utils/valiators.util.ts b/src/utils/validators.util.ts similarity index 93% rename from src/utils/valiators.util.ts rename to src/utils/validators.util.ts index 8f2f9a8..2180d66 100644 --- a/src/utils/valiators.util.ts +++ b/src/utils/validators.util.ts @@ -10,7 +10,7 @@ const Validators = { //displayName isPassword: (value: string) => { - const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/; + const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,32}$/; return passwordRegex.test(value); } }