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:
parent
18c20c52d5
commit
1ad136ea60
@ -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
2
.idea/sqldialects.xml
generated
@ -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>
|
@ -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
|
||||
(
|
||||
|
@ -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"
|
||||
}
|
||||
|
12
src/app.ts
12
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(()=>{})
|
||||
}
|
66
src/controllers/auth.controller.ts
Normal file
66
src/controllers/auth.controller.ts
Normal 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;
|
13
src/interfaces/services/register.types.ts
Normal file
13
src/interfaces/services/register.types.ts
Normal 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
|
||||
}
|
10
src/routers/auth.router.ts
Normal file
10
src/routers/auth.router.ts
Normal 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;
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
46
src/services/authentication/register.service.ts
Normal file
46
src/services/authentication/register.service.ts
Normal 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;
|
@ -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')
|
@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
28
src/services/users.service.ts
Normal file
28
src/services/users.service.ts
Normal 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;
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user