feat: Implement registration feature and update database service

This commit creates new methods for the authentication system, especially the user registration feature. The update also validates input data and checks if a user already exists in the database. It modifies the application entry point to include the updated user registration route and provides updates to the database service to include user-related functions. The MariaDB collation was also updated in the database schema script to support unicode.
This commit is contained in:
Mathis H (Avnyr) 2024-05-21 16:14:54 +02:00
parent 18c20c52d5
commit 1ad136ea60
Signed by: Mathis
GPG Key ID: DD9E0666A747D126
14 changed files with 233 additions and 23 deletions

View File

@ -2,6 +2,9 @@ APP_PORT: 3000
DEBUG: true
CONTEXT: dev
HASH_SECRET: ''
JWT_SECRET: ''
MYSQL_PORT: 3434
MYSQL_HOST: 'localhost'
MYSQL_USERNAME: 'apdbqixmwnsesdj'

2
.idea/sqldialects.xml generated
View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/src/services/databases/databases.service.ts" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/maria.sql" dialect="MariaDB" />
<file url="PROJECT" dialect="MariaDB" />
</component>
</project>

View File

@ -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
(

View File

@ -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"
}

View File

@ -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(()=>{})
}

View File

@ -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;

View File

@ -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
}

View File

@ -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;

View File

@ -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)
}
}

View File

@ -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<IRegisterOutput> {
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;

View File

@ -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<object> = await this.maria.query("SELECT * FROM users") as unknown as Array<object>;
this.logs.debug('Fetching users from database...', `${result?.length} user(s) found.`)
return result;
}
async getUserById(id: number) {
const result: Array<object> = await this.maria.execute(`SELECT * FROM users WHERE id = ?`, [id]) as unknown as Array<object>;
this.logs.debug(`Fetching user with id ${id} from database...`, `${result?.length} user(s) found.`);
return result;
}
async getUserByUsername(username: string) {
const result: Array<object> = await this.maria.execute(`SELECT * FROM users WHERE username = ?`, [username]) as unknown as Array<object>;
this.logs.debug(`Fetching user with username "${username}" from database...`, `${result?.length} user(s) found.`);
return result;
}
async getUserByEmail(email: string) {
const result: Array<object> = await this.maria.execute(`SELECT * FROM users WHERE email = ?`, [email]) as unknown as Array<object>;
this.logs.debug(`Fetching user with email "${email}" from database...`, `${result?.length} user(s) found.`);
return result;
}
async insertUser(user: UserInDatabase): Promise<boolean> {
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')

View File

@ -56,6 +56,7 @@ export class MariadbService {
factorize(data: ISqlFactorizeInput): Promise<ISqlFactorizeOutput> {
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);
},
);
});

View File

@ -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;

View File

@ -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);
}
}