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
|
DEBUG: true
|
||||||
CONTEXT: dev
|
CONTEXT: dev
|
||||||
|
|
||||||
|
HASH_SECRET: ''
|
||||||
|
JWT_SECRET: ''
|
||||||
|
|
||||||
MYSQL_PORT: 3434
|
MYSQL_PORT: 3434
|
||||||
MYSQL_HOST: 'localhost'
|
MYSQL_HOST: 'localhost'
|
||||||
MYSQL_USERNAME: 'apdbqixmwnsesdj'
|
MYSQL_USERNAME: 'apdbqixmwnsesdj'
|
||||||
|
2
.idea/sqldialects.xml
generated
2
.idea/sqldialects.xml
generated
@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="SqlDialectMappings">
|
<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" />
|
<file url="PROJECT" dialect="MariaDB" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
@ -6,7 +6,7 @@ create table follows
|
|||||||
target_id varchar(36) not null comment 'identifier of the followed user',
|
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'
|
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
|
create table users
|
||||||
(
|
(
|
||||||
|
@ -13,13 +13,17 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@node-rs/argon2": "^1.8.3",
|
"@node-rs/argon2": "^1.8.3",
|
||||||
|
"axios": "^1.6.8",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"helmet": "^7.1.0",
|
"helmet": "^7.1.0",
|
||||||
|
"jose": "^5.3.0",
|
||||||
"mongodb": "^6.6.1",
|
"mongodb": "^6.6.1",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"mysql2": "^3.9.7",
|
"mysql2": "^3.9.7",
|
||||||
|
"randomatic": "^3.1.1",
|
||||||
|
"react-hook-form": "^7.51.4",
|
||||||
"tslog": "^4.9.2",
|
"tslog": "^4.9.2",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
},
|
},
|
||||||
@ -29,6 +33,7 @@
|
|||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/node": "^20.12.11",
|
"@types/node": "^20.12.11",
|
||||||
|
"@types/randomatic": "^3.1.5",
|
||||||
"@types/uuid": "^9.0.8",
|
"@types/uuid": "^9.0.8",
|
||||||
"arkit": "^1.6.4"
|
"arkit": "^1.6.4"
|
||||||
}
|
}
|
||||||
|
10
src/app.ts
10
src/app.ts
@ -5,7 +5,7 @@ import compression from "compression";
|
|||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
import express, { type Express } from "express";
|
import express, { type Express } from "express";
|
||||||
import helmet from "helmet";
|
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");
|
console.log("\n\n> Starting...\n\n\n\n");
|
||||||
|
|
||||||
@ -26,10 +26,10 @@ app.use(
|
|||||||
);
|
);
|
||||||
app.use(helmet.xXssProtection());
|
app.use(helmet.xXssProtection());
|
||||||
|
|
||||||
// parse json request body
|
// parse json requests body
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
// parse urlencoded request body
|
// parse urlencoded requests body
|
||||||
app.use(
|
app.use(
|
||||||
express.urlencoded({
|
express.urlencoded({
|
||||||
extended: true,
|
extended: true,
|
||||||
@ -40,7 +40,7 @@ app.use(
|
|||||||
app.use(compression());
|
app.use(compression());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
//app.use("/auth", AuthRouter);
|
app.use("/auth", AuthRouter);
|
||||||
logger.info("Routers loaded !");
|
logger.info("Routers loaded !");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
@ -62,5 +62,3 @@ try {
|
|||||||
logger.error(`Server failed to start: ${error}`);
|
logger.error(`Server failed to start: ${error}`);
|
||||||
process.exit(1);
|
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 = {
|
const IntCodeService = {
|
||||||
generate: () => {
|
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 {MariadbService} from "@services/databases/mariadb.service";
|
||||||
import {MongodbService} from "@services/databases/mongodb.service";
|
import {MongodbService} from "@services/databases/mongodb.service";
|
||||||
import {LogsUtils} from "@utils/logs.util";
|
import {LogsUtils} from "@utils/logs.util";
|
||||||
|
import {UserInDatabase} from "@interfaces/db/mariadb.interface";
|
||||||
|
|
||||||
interface MariaDbStatusResult {
|
interface MariaDbStatusResult {
|
||||||
fieldCount: number;
|
fieldCount: number;
|
||||||
@ -12,7 +13,7 @@ interface MariaDbStatusResult {
|
|||||||
changedRows: number;
|
changedRows: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DatabasesService {
|
export class DatabasesService {
|
||||||
private readonly _appName;
|
private readonly _appName;
|
||||||
private readonly maria: MariadbService;
|
private readonly maria: MariadbService;
|
||||||
private readonly mongo: MongodbService;
|
private readonly mongo: MongodbService;
|
||||||
@ -25,10 +26,49 @@ class DatabasesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getAllUsers() {
|
async getAllUsers() {
|
||||||
const result = await this.maria.query("SELECT * FROM users");
|
const result: Array<object> = await this.maria.query("SELECT * FROM users") as unknown as Array<object>;
|
||||||
this.logs.debug('Fetching users from database...', result)
|
this.logs.debug('Fetching users from database...', `${result?.length} user(s) found.`)
|
||||||
return result;
|
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')
|
export const justForTesting = new DatabasesService('OnlyDevs')
|
@ -56,6 +56,7 @@ export class MariadbService {
|
|||||||
factorize(data: ISqlFactorizeInput): Promise<ISqlFactorizeOutput> {
|
factorize(data: ISqlFactorizeInput): Promise<ISqlFactorizeOutput> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
|
console.log(data);
|
||||||
let _id = "";
|
let _id = "";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (data.values.id) {
|
if (data.values.id) {
|
||||||
@ -67,15 +68,16 @@ export class MariadbService {
|
|||||||
const _sqlQueryKeys = Object.keys(data.values).map(
|
const _sqlQueryKeys = Object.keys(data.values).map(
|
||||||
(key: string) => `${key}`,
|
(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(
|
this.logs.debug(
|
||||||
`Factorized ${_sqlQueryKeys.length} keys for a prepare Query.`, `Action: ${data.actionName}`,
|
`Factorized ${_sqlQueryKeys.length} keys for a prepare Query.`, `Action: ${data.actionName}`,
|
||||||
);
|
);
|
||||||
const sqlQueryKeys = _sqlQueryKeys.join(", ");
|
if (_id.length > 0) {
|
||||||
if (_id && _id.length > 2) {
|
|
||||||
this.logs.trace(`Id post-pushed in factorized data.`, _id);
|
this.logs.trace(`Id post-pushed in factorized data.`, _id);
|
||||||
values.push(_id);
|
values.push(_id);
|
||||||
|
_sqlQueryKeys.push('id')
|
||||||
}
|
}
|
||||||
|
const sqlQueryKeys = _sqlQueryKeys.join(", ");
|
||||||
|
|
||||||
const questionMarksFields = Array(values.length)
|
const questionMarksFields = Array(values.length)
|
||||||
.fill("?")
|
.fill("?")
|
||||||
@ -113,11 +115,9 @@ export class MariadbService {
|
|||||||
values,
|
values,
|
||||||
(err: mysql.QueryError | null, results: mysql.QueryResult) => {
|
(err: mysql.QueryError | null, results: mysql.QueryResult) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logs.error(`Error executing query:`, err);
|
|
||||||
reject(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
|
//displayName
|
||||||
|
|
||||||
isPassword: (value: string) => {
|
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);
|
return passwordRegex.test(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user