Beginning all routes for login/password

This commit is contained in:
Maxime Lalo 2023-11-23 15:54:04 +01:00
parent a93b2616ec
commit 220a77e063
9 changed files with 351 additions and 69 deletions

View File

@ -46,6 +46,7 @@
"@pinata/sdk": "^2.1.0", "@pinata/sdk": "^2.1.0",
"@prisma/client": "^4.11.0", "@prisma/client": "^4.11.0",
"adm-zip": "^0.5.10", "adm-zip": "^0.5.10",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.0", "class-validator": "^0.14.0",
"classnames": "^2.3.2", "classnames": "^2.3.2",
@ -55,7 +56,7 @@
"file-type-checker": "^1.0.8", "file-type-checker": "^1.0.8",
"fp-ts": "^2.16.1", "fp-ts": "^2.16.1",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.94", "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.95",
"module-alias": "^2.2.2", "module-alias": "^2.2.2",
"monocle-ts": "^2.3.13", "monocle-ts": "^2.3.13",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
@ -74,6 +75,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/adm-zip": "^0.5.3", "@types/adm-zip": "^0.5.3",
"@types/bcrypt": "^5.0.2",
"@types/cors": "^2.8.13", "@types/cors": "^2.8.13",
"@types/cron": "^2.0.1", "@types/cron": "^2.0.1",
"@types/express": "^4.17.16", "@types/express": "^4.17.16",

View File

@ -0,0 +1,232 @@
import { Response, Request } from "express";
import { Controller, Post } from "@ControllerPattern/index";
import ApiController from "@Common/system/controller-pattern/ApiController";
import { Service } from "typedi";
import { EnrollmentResponse } from "@Services/common/Id360Service/Id360Service";
import CustomersService from "@Services/customer/CustomersService/CustomersService";
import AuthService, { ICustomerJwtPayload } from "@Services/common/AuthService/AuthService";
import { Customer } from "le-coffre-resources/dist/SuperAdmin";
@Controller()
@Service()
export default class AuthController extends ApiController {
constructor(private customerService: CustomersService, private authService: AuthService) {
super();
}
@Post("/api/v1/customer/pre-login")
protected async preLogin(req: Request, response: Response) {
const email = req.body["email"];
if (!email) {
this.httpBadRequest(response, "Email is required");
return;
}
let customer = await this.customerService.getOne({
where: {
contact: {
email,
},
},
include: {
contact: true,
},
});
if (!customer) {
this.httpNotFoundRequest(response, "Customer not found");
return;
}
// if code has more than 5mn, regenerate it
if (
!customer.smsCodeExpire ||
(customer.smsCodeExpire && new Date().getTime() - customer.smsCodeExpire.getTime() > 5 * 60 * 1000)
) {
customer = await this.customerService.generateSmsCode(customer);
}
if (!customer.password) {
try {
this.httpSuccess(response, { info: "Sending a sms for first connection" });
} catch (error) {
console.log(error);
this.httpInternalError(response);
}
return;
}
try {
this.httpSuccess(response, { email, customer });
} catch (error) {
console.log(error);
this.httpInternalError(response);
return;
}
}
@Post("/api/v1/customer/login")
protected async login(req: Request, response: Response) {
const email = req.body["email"];
const password = req.body["password"];
if (!email) {
this.httpBadRequest(response, "Email is required");
return;
}
if (!password) {
this.httpBadRequest(response, "Password is required");
return;
}
let customer = await this.customerService.getOne({
where: {
contact: {
email,
},
},
include: {
contact: true,
},
});
if (!customer) {
this.httpNotFoundRequest(response, "Customer not found");
return;
}
if (!customer.password) {
this.httpBadRequest(response, "Customer not registered");
return;
}
// compare password to the hash
const isPasswordValid = await this.authService.comparePassword(password, customer.password);
if (!isPasswordValid) {
this.httpBadRequest(response, "Invalid password");
return;
}
try {
this.httpSuccess(response, { customer });
} catch (error) {
console.log(error);
this.httpInternalError(response);
return;
}
}
@Post("/api/v1/customer/set-password")
protected async setPassword(req: Request, response: Response) {
const email = req.body["email"];
const smsCode = req.body["smsCode"];
const password = req.body["password"];
if (!email) {
this.httpBadRequest(response, "Email is required");
return;
}
if (!smsCode) {
this.httpBadRequest(response, "Sms code is required");
return;
}
if (!password) {
this.httpBadRequest(response, "Password is required");
return;
}
const customer = await this.customerService.getOne({
where: {
contact: {
email,
},
},
include: {
contact: true,
},
});
if (!customer) {
this.httpNotFoundRequest(response, "Customer not found");
return;
}
if (!customer.smsCode) {
this.httpBadRequest(response, "No sms code found");
return;
}
if (customer.smsCode !== smsCode) {
this.httpBadRequest(response, "Invalid sms code");
return;
}
if (customer.password) {
this.httpBadRequest(response, "Password already set");
return;
}
const hashedPassword = await this.authService.hashPassword(password);
await this.customerService.setPassword(customer, hashedPassword);
try {
this.httpSuccess(response, { email });
} catch (error) {
console.log(error);
this.httpInternalError(response);
return;
}
}
@Post("/api/v1/customer/check-sms-code")
protected async checkSmsCode(req: Request, response: Response) {
const email = req.body["email"];
const smsCode = req.body["smsCode"];
if (!email) {
this.httpBadRequest(response, "Email is required");
return;
}
if (!smsCode) {
this.httpBadRequest(response, "Sms code is required");
return;
}
const customer = await this.customerService.getOne({
where: {
contact: {
email,
},
},
include: {
contact: true,
},
});
if (!customer) {
this.httpNotFoundRequest(response, "Customer not found");
return;
}
if (!customer.smsCode) {
this.httpBadRequest(response, "No sms code found");
return;
}
if (customer.smsCode !== smsCode) {
this.httpBadRequest(response, "Invalid sms code");
return;
}
try {
this.httpSuccess(response, { success: "success" });
} catch (error) {
console.log(error);
this.httpInternalError(response);
return;
}
}
}

View File

@ -45,7 +45,7 @@ import LiveVoteController from "./api/super-admin/LiveVoteController";
import DocumentControllerId360 from "./api/id360/DocumentController"; import DocumentControllerId360 from "./api/id360/DocumentController";
import CustomerControllerId360 from "./api/id360/CustomerController"; import CustomerControllerId360 from "./api/id360/CustomerController";
import UserNotificationController from "./api/notary/UserNotificationController"; import UserNotificationController from "./api/notary/UserNotificationController";
import AuthController from "./api/customer/AuthController";
/** /**
* @description This allow to declare all controllers used in the application * @description This allow to declare all controllers used in the application
@ -99,5 +99,6 @@ export default {
Container.get(UserNotificationController); Container.get(UserNotificationController);
Container.get(DocumentControllerId360); Container.get(DocumentControllerId360);
Container.get(CustomerControllerId360); Container.get(CustomerControllerId360);
Container.get(AuthController);
}, },
}; };

View File

@ -0,0 +1,6 @@
-- AlterTable
ALTER TABLE "customers" ADD COLUMN "password" VARCHAR(255),
ADD COLUMN "passwordCode" VARCHAR(255),
ADD COLUMN "passwordcodeExpire" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "smsCode" VARCHAR(255),
ADD COLUMN "smsCodeExpire" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP;

View File

@ -101,6 +101,12 @@ model Customers {
updated_at DateTime? @updatedAt updated_at DateTime? @updatedAt
office_folders OfficeFolders[] @relation("OfficeFolderHasCustomers") office_folders OfficeFolders[] @relation("OfficeFolderHasCustomers")
documents Documents[] documents Documents[]
password String? @db.VarChar(255)
smsCode String? @db.VarChar(255)
smsCodeExpire DateTime? @default(now())
passwordCode String? @db.VarChar(255)
passwordcodeExpire DateTime? @default(now())
@@map("customers") @@map("customers")
} }

View File

@ -25,6 +25,15 @@ export default class CustomersRepository extends BaseRepository {
return this.model.findMany({ ...query, include: { contact: { include: { address: true } } } }); return this.model.findMany({ ...query, include: { contact: { include: { address: true } } } });
} }
/**
* @description : Find one customers
*/
public async findOne(query: Prisma.CustomersFindFirstArgs) {
query.take = Math.min(query.take || this.defaultFetchRows, this.maxFetchRows);
if (!query.include) return this.model.findFirst({ ...query, include: { contact: true } });
return this.model.findFirst({ ...query, include: { contact: { include: { address: true } } } });
}
/** /**
* @description : Create a customer * @description : Create a customer
*/ */
@ -79,6 +88,11 @@ export default class CustomersRepository extends BaseRepository {
address: {}, address: {},
}, },
}, },
smsCode: customer.smsCode,
smsCodeExpire: customer.smsCodeExpire,
passwordCode: customer.passwordCode,
passwordcodeExpire: customer.passwordCodeExpire,
password: customer.password,
}, },
}; };
if (customer.contact!.address) { if (customer.contact!.address) {
@ -88,6 +102,7 @@ export default class CustomersRepository extends BaseRepository {
city: customer.contact!.address!.city, city: customer.contact!.address!.city,
}; };
} }
return this.model.update({ ...updateArgs, include: { contact: true } }); return this.model.update({ ...updateArgs, include: { contact: true } });
} }

View File

@ -6,6 +6,7 @@ import UsersService from "@Services/super-admin/UsersService/UsersService";
import CustomersService from "@Services/super-admin/CustomersService/CustomersService"; import CustomersService from "@Services/super-admin/CustomersService/CustomersService";
import { ECustomerStatus } from "@prisma/client"; import { ECustomerStatus } from "@prisma/client";
import { Customer } from "le-coffre-resources/dist/Notary"; import { Customer } from "le-coffre-resources/dist/Notary";
import bcrypt from "bcrypt";
enum PROVIDER_OPENID { enum PROVIDER_OPENID {
idNot = "idNot", idNot = "idNot",
@ -19,9 +20,9 @@ export interface ICustomerJwtPayload {
} }
export interface IdNotJwtPayload { export interface IdNotJwtPayload {
sub: string, sub: string;
profile_idn: string, profile_idn: string;
entity_idn: string, entity_idn: string;
} }
export interface IUserJwtPayload { export interface IUserJwtPayload {
@ -44,7 +45,7 @@ export default class AuthService extends BaseService {
} }
public async getCustomerJwtPayload(customers: Customer[]): Promise<ICustomerJwtPayload | null> { public async getCustomerJwtPayload(customers: Customer[]): Promise<ICustomerJwtPayload | null> {
for (const customer of customers){ for (const customer of customers) {
if (customer.status === ECustomerStatus["PENDING"]) { if (customer.status === ECustomerStatus["PENDING"]) {
customer.status = ECustomerStatus["VALIDATED"]; customer.status = ECustomerStatus["VALIDATED"];
await this.customerService.update(customer.uid!, customer); await this.customerService.update(customer.uid!, customer);
@ -52,7 +53,7 @@ export default class AuthService extends BaseService {
} }
return { return {
customerId: customers[0]!.uid!, customerId: customers[0]!.uid!,
email: customers[0]!.contact!.email, email: customers[0]!.contact!.email,
}; };
} }
@ -69,7 +70,7 @@ export default class AuthService extends BaseService {
if (user.office_role) { if (user.office_role) {
user.office_role.rules.forEach((rule) => { user.office_role.rules.forEach((rule) => {
if(!rules.includes(rule.name)) { if (!rules.includes(rule.name)) {
rules.push(rule.name); rules.push(rule.name);
} }
}); });
@ -84,11 +85,11 @@ export default class AuthService extends BaseService {
}; };
} }
public generateAccessToken(user: any): string { public generateAccessToken(user: any): string {
return jwt.sign({ ...user}, this.variables.ACCESS_TOKEN_SECRET, { expiresIn: "15m" }); return jwt.sign({ ...user }, this.variables.ACCESS_TOKEN_SECRET, { expiresIn: "15m" });
} }
public generateRefreshToken(user: any): string { public generateRefreshToken(user: any): string {
return jwt.sign({ ...user}, this.variables.REFRESH_TOKEN_SECRET, { expiresIn: "1h" }); return jwt.sign({ ...user }, this.variables.REFRESH_TOKEN_SECRET, { expiresIn: "1h" });
} }
public verifyAccessToken(token: string, callback?: VerifyCallback) { public verifyAccessToken(token: string, callback?: VerifyCallback) {
@ -98,4 +99,12 @@ export default class AuthService extends BaseService {
public verifyRefreshToken(token: string, callback?: VerifyCallback) { public verifyRefreshToken(token: string, callback?: VerifyCallback) {
return jwt.verify(token, this.variables.REFRESH_TOKEN_SECRET, callback); return jwt.verify(token, this.variables.REFRESH_TOKEN_SECRET, callback);
} }
public comparePassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
public hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, 10);
}
} }

View File

@ -1,6 +1,7 @@
import { Customers, Prisma } from "@prisma/client"; import { Customers, Prisma } from "@prisma/client";
import CustomersRepository from "@Repositories/CustomersRepository"; import CustomersRepository from "@Repositories/CustomersRepository";
import BaseService from "@Services/BaseService"; import BaseService from "@Services/BaseService";
import { Customer } from "le-coffre-resources/dist/Notary";
import { Service } from "typedi"; import { Service } from "typedi";
@Service() @Service()
@ -16,4 +17,44 @@ export default class CustomersService extends BaseService {
public async get(query: Prisma.CustomersFindManyArgs): Promise<Customers[]> { public async get(query: Prisma.CustomersFindManyArgs): Promise<Customers[]> {
return this.customerRepository.findMany(query); return this.customerRepository.findMany(query);
} }
}
/**
* @description : Get all Customers
* @throws {Error} If Customers cannot be get
*/
public async getOne(query: Prisma.CustomersFindFirstArgs): Promise<Customers | null> {
return this.customerRepository.findOne(query);
}
/**
* @description : Generate a SMS code for a customer
* @throws {Error}
*/
public async generateSmsCode(customer: Customer) {
const smsCode = Math.floor(100000 + Math.random() * 900000);
return await this.customerRepository.update(
customer.uid as string,
Customer.hydrate<Customer>({
...customer,
smsCode: smsCode.toString(),
smsCodeExpire: new Date(),
}),
);
}
/**
* @description : Set password for a customer
* @throws {Error} If customer cannot be updated
*/
public async setPassword(customer: Customer, password: string) {
return await this.customerRepository.update(
customer.uid as string,
Customer.hydrate<Customer>({
...customer,
password,
smsCode: null,
smsCodeExpire: null,
}),
);
}
}

View File

@ -4,11 +4,7 @@
// "module": "es2022", // "module": "es2022",
"target": "es2017", "target": "es2017",
"module": "commonjs", "module": "commonjs",
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom",
"dom.iterable",
"esnext"
],
"jsx": "preserve", "jsx": "preserve",
"sourceMap": true, "sourceMap": true,
"outDir": "./dist", "outDir": "./dist",
@ -18,7 +14,7 @@
"resolveJsonModule": true, "resolveJsonModule": true,
/* Strict Type-Checking Options */ /* Strict Type-Checking Options */
"allowUnreachableCode": false, "allowUnreachableCode": false,
"allowUnusedLabels": false, "allowUnusedLabels": true,
"exactOptionalPropertyTypes": false, "exactOptionalPropertyTypes": false,
"noImplicitOverride": true, "noImplicitOverride": true,
"strict": true, "strict": true,
@ -31,7 +27,8 @@
"alwaysStrict": true, "alwaysStrict": true,
"noPropertyAccessFromIndexSignature": true, "noPropertyAccessFromIndexSignature": true,
/* Additional Checks */ /* Additional Checks */
"noUnusedLocals": true, "noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitReturns": true, "noImplicitReturns": true,
"noUncheckedIndexedAccess": true, "noUncheckedIndexedAccess": true,
"useUnknownInCatchVariables": true, "useUnknownInCatchVariables": true,
@ -39,64 +36,37 @@
"moduleResolution": "node", "moduleResolution": "node",
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@App/*": [ "@App/*": ["src/app/*"],
"src/app/*" "@Api/*": ["src/app/api/*"],
], "@Services/*": ["src/services/*"],
"@Api/*": [ "@Repositories/*": ["src/common/repositories/*"],
"src/app/api/*" "@Entries/*": ["src/entries/*"],
], "@Common/*": ["src/common/*"],
"@Services/*": [ "@Config/*": ["src/common/config/*"],
"src/services/*" "@Entities/*": ["src/common/ressources/*"],
], "@System/*": ["src/common/system/*"],
"@Repositories/*": [ "@ControllerPattern/*": ["src/common/system/controller-pattern/*"],
"src/common/repositories/*" "@Test/*": ["src/test/*"]
],
"@Entries/*": [
"src/entries/*"
],
"@Common/*": [
"src/common/*"
],
"@Config/*": [
"src/common/config/*"
],
"@Entities/*": [
"src/common/ressources/*"
],
"@System/*": [
"src/common/system/*"
],
"@ControllerPattern/*": [
"src/common/system/controller-pattern/*"
],
"@Test/*": [
"src/test/*"
],
}, },
// "rootDirs": [], // "rootDirs": [],
// "typeRoots": [], // "typeRoots": [],
// "types": [], // "types": [],
// "allowSyntheticDefaultImports": true, // "allowSyntheticDefaultImports": true,
"esModuleInterop": true, "esModuleInterop": true,
// "allowUmdGlobalAccess": true, // "allowUmdGlobalAccess": true,
/* Source Map Options */ /* Source Map Options */
//"sourceRoot": "./src", //"sourceRoot": "./src",
//"mapRoot": "./dist", //"mapRoot": "./dist",
//"inlineSourceMap": false, //"inlineSourceMap": false,
//"inlineSources": false, //"inlineSources": false,
/* Experimental Options */ /* Experimental Options */
"experimentalDecorators": true, "experimentalDecorators": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"skipLibCheck": true, "skipLibCheck": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"allowJs": true, "allowJs": true,
"isolatedModules": true, "isolatedModules": true
}, },
"include": [ "include": ["**/*.ts", "**/*.tsx", "src/services/common/TestService"],
"**/*.ts", "exclude": ["node_modules"]
"**/*.tsx", "src/services/common/TestService", }
],
"exclude": [
"node_modules"
]
}