From 220a77e06373eb4a75dc4f469ecc1b1e6f4097b2 Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Thu, 23 Nov 2023 15:54:04 +0100 Subject: [PATCH 01/46] :sparkles: Beginning all routes for login/password --- package.json | 4 +- src/app/api/customer/AuthController.ts | 232 ++++++++++++++++++ src/app/index.ts | 3 +- .../migration.sql | 6 + src/common/databases/schema.prisma | 6 + .../repositories/CustomersRepository.ts | 15 ++ .../common/AuthService/AuthService.ts | 25 +- .../CustomersService/CustomersService.ts | 43 +++- tsconfig.json | 86 +++---- 9 files changed, 351 insertions(+), 69 deletions(-) create mode 100644 src/app/api/customer/AuthController.ts create mode 100644 src/common/databases/migrations/20231123104754_customer_login/migration.sql diff --git a/package.json b/package.json index 6b50b10e..b9f73143 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@pinata/sdk": "^2.1.0", "@prisma/client": "^4.11.0", "adm-zip": "^0.5.10", + "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "classnames": "^2.3.2", @@ -55,7 +56,7 @@ "file-type-checker": "^1.0.8", "fp-ts": "^2.16.1", "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", "monocle-ts": "^2.3.13", "multer": "^1.4.5-lts.1", @@ -74,6 +75,7 @@ }, "devDependencies": { "@types/adm-zip": "^0.5.3", + "@types/bcrypt": "^5.0.2", "@types/cors": "^2.8.13", "@types/cron": "^2.0.1", "@types/express": "^4.17.16", diff --git a/src/app/api/customer/AuthController.ts b/src/app/api/customer/AuthController.ts new file mode 100644 index 00000000..ad48db50 --- /dev/null +++ b/src/app/api/customer/AuthController.ts @@ -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; + } + } +} diff --git a/src/app/index.ts b/src/app/index.ts index f5c3f00a..2de03a00 100644 --- a/src/app/index.ts +++ b/src/app/index.ts @@ -45,7 +45,7 @@ import LiveVoteController from "./api/super-admin/LiveVoteController"; import DocumentControllerId360 from "./api/id360/DocumentController"; import CustomerControllerId360 from "./api/id360/CustomerController"; import UserNotificationController from "./api/notary/UserNotificationController"; - +import AuthController from "./api/customer/AuthController"; /** * @description This allow to declare all controllers used in the application @@ -99,5 +99,6 @@ export default { Container.get(UserNotificationController); Container.get(DocumentControllerId360); Container.get(CustomerControllerId360); + Container.get(AuthController); }, }; diff --git a/src/common/databases/migrations/20231123104754_customer_login/migration.sql b/src/common/databases/migrations/20231123104754_customer_login/migration.sql new file mode 100644 index 00000000..0bf90ced --- /dev/null +++ b/src/common/databases/migrations/20231123104754_customer_login/migration.sql @@ -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; diff --git a/src/common/databases/schema.prisma b/src/common/databases/schema.prisma index dcd5a5b0..74b5b9c5 100644 --- a/src/common/databases/schema.prisma +++ b/src/common/databases/schema.prisma @@ -101,6 +101,12 @@ model Customers { updated_at DateTime? @updatedAt office_folders OfficeFolders[] @relation("OfficeFolderHasCustomers") 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") } diff --git a/src/common/repositories/CustomersRepository.ts b/src/common/repositories/CustomersRepository.ts index 8a27293a..d9acaef5 100644 --- a/src/common/repositories/CustomersRepository.ts +++ b/src/common/repositories/CustomersRepository.ts @@ -25,6 +25,15 @@ export default class CustomersRepository extends BaseRepository { 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 */ @@ -79,6 +88,11 @@ export default class CustomersRepository extends BaseRepository { address: {}, }, }, + smsCode: customer.smsCode, + smsCodeExpire: customer.smsCodeExpire, + passwordCode: customer.passwordCode, + passwordcodeExpire: customer.passwordCodeExpire, + password: customer.password, }, }; if (customer.contact!.address) { @@ -88,6 +102,7 @@ export default class CustomersRepository extends BaseRepository { city: customer.contact!.address!.city, }; } + return this.model.update({ ...updateArgs, include: { contact: true } }); } diff --git a/src/services/common/AuthService/AuthService.ts b/src/services/common/AuthService/AuthService.ts index f4f35b53..6be3ae25 100644 --- a/src/services/common/AuthService/AuthService.ts +++ b/src/services/common/AuthService/AuthService.ts @@ -6,6 +6,7 @@ import UsersService from "@Services/super-admin/UsersService/UsersService"; import CustomersService from "@Services/super-admin/CustomersService/CustomersService"; import { ECustomerStatus } from "@prisma/client"; import { Customer } from "le-coffre-resources/dist/Notary"; +import bcrypt from "bcrypt"; enum PROVIDER_OPENID { idNot = "idNot", @@ -19,9 +20,9 @@ export interface ICustomerJwtPayload { } export interface IdNotJwtPayload { - sub: string, - profile_idn: string, - entity_idn: string, + sub: string; + profile_idn: string; + entity_idn: string; } export interface IUserJwtPayload { @@ -44,7 +45,7 @@ export default class AuthService extends BaseService { } public async getCustomerJwtPayload(customers: Customer[]): Promise { - for (const customer of customers){ + for (const customer of customers) { if (customer.status === ECustomerStatus["PENDING"]) { customer.status = ECustomerStatus["VALIDATED"]; await this.customerService.update(customer.uid!, customer); @@ -52,7 +53,7 @@ export default class AuthService extends BaseService { } return { 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) { user.office_role.rules.forEach((rule) => { - if(!rules.includes(rule.name)) { + if (!rules.includes(rule.name)) { rules.push(rule.name); } }); @@ -84,11 +85,11 @@ export default class AuthService extends BaseService { }; } 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 { - 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) { @@ -98,4 +99,12 @@ export default class AuthService extends BaseService { public verifyRefreshToken(token: string, callback?: VerifyCallback) { return jwt.verify(token, this.variables.REFRESH_TOKEN_SECRET, callback); } + + public comparePassword(password: string, hash: string): Promise { + return bcrypt.compare(password, hash); + } + + public hashPassword(password: string): Promise { + return bcrypt.hash(password, 10); + } } diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index 8ec5f672..6461e6e4 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -1,6 +1,7 @@ import { Customers, Prisma } from "@prisma/client"; import CustomersRepository from "@Repositories/CustomersRepository"; import BaseService from "@Services/BaseService"; +import { Customer } from "le-coffre-resources/dist/Notary"; import { Service } from "typedi"; @Service() @@ -16,4 +17,44 @@ export default class CustomersService extends BaseService { public async get(query: Prisma.CustomersFindManyArgs): Promise { return this.customerRepository.findMany(query); } -} \ No newline at end of file + + /** + * @description : Get all Customers + * @throws {Error} If Customers cannot be get + */ + public async getOne(query: Prisma.CustomersFindFirstArgs): Promise { + 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, + 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, + password, + smsCode: null, + smsCodeExpire: null, + }), + ); + } +} diff --git a/tsconfig.json b/tsconfig.json index bd89006e..bbd8ba4d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,11 +4,7 @@ // "module": "es2022", "target": "es2017", "module": "commonjs", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "jsx": "preserve", "sourceMap": true, "outDir": "./dist", @@ -18,7 +14,7 @@ "resolveJsonModule": true, /* Strict Type-Checking Options */ "allowUnreachableCode": false, - "allowUnusedLabels": false, + "allowUnusedLabels": true, "exactOptionalPropertyTypes": false, "noImplicitOverride": true, "strict": true, @@ -31,7 +27,8 @@ "alwaysStrict": true, "noPropertyAccessFromIndexSignature": true, /* Additional Checks */ - "noUnusedLocals": true, + "noUnusedLocals": false, + "noUnusedParameters": false, "noImplicitReturns": true, "noUncheckedIndexedAccess": true, "useUnknownInCatchVariables": true, @@ -39,64 +36,37 @@ "moduleResolution": "node", "baseUrl": ".", "paths": { - "@App/*": [ - "src/app/*" - ], - "@Api/*": [ - "src/app/api/*" - ], - "@Services/*": [ - "src/services/*" - ], - "@Repositories/*": [ - "src/common/repositories/*" - ], - "@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/*" - ], + "@App/*": ["src/app/*"], + "@Api/*": ["src/app/api/*"], + "@Services/*": ["src/services/*"], + "@Repositories/*": ["src/common/repositories/*"], + "@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": [], - // "typeRoots": [], - // "types": [], - // "allowSyntheticDefaultImports": true, + // "rootDirs": [], + // "typeRoots": [], + // "types": [], + // "allowSyntheticDefaultImports": true, "esModuleInterop": true, - // "allowUmdGlobalAccess": true, + // "allowUmdGlobalAccess": true, /* Source Map Options */ - //"sourceRoot": "./src", - //"mapRoot": "./dist", - //"inlineSourceMap": false, - //"inlineSources": false, + //"sourceRoot": "./src", + //"mapRoot": "./dist", + //"inlineSourceMap": false, + //"inlineSources": false, /* Experimental Options */ "experimentalDecorators": true, "emitDecoratorMetadata": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "allowJs": true, - "isolatedModules": true, + "isolatedModules": true }, - "include": [ - "**/*.ts", - "**/*.tsx", "src/services/common/TestService", - ], - "exclude": [ - "node_modules" - ] -} \ No newline at end of file + "include": ["**/*.ts", "**/*.tsx", "src/services/common/TestService"], + "exclude": ["node_modules"] +} From ee97ccbf460a97198d0af9f40629802bb67c40ae Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Fri, 24 Nov 2023 10:21:31 +0100 Subject: [PATCH 02/46] :sparkles: Checking sms code in login & hiding password in ressource --- package.json | 2 +- src/app/api/customer/AuthController.ts | 45 ++++++++++++++----- .../CustomersService/CustomersService.ts | 3 +- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index b9f73143..eaa73a16 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "file-type-checker": "^1.0.8", "fp-ts": "^2.16.1", "jsonwebtoken": "^9.0.0", - "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.95", + "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.96", "module-alias": "^2.2.2", "monocle-ts": "^2.3.13", "multer": "^1.4.5-lts.1", diff --git a/src/app/api/customer/AuthController.ts b/src/app/api/customer/AuthController.ts index ad48db50..5492bd52 100644 --- a/src/app/api/customer/AuthController.ts +++ b/src/app/api/customer/AuthController.ts @@ -38,11 +38,10 @@ export default class AuthController extends ApiController { return; } - // if code has more than 5mn, regenerate it - if ( - !customer.smsCodeExpire || - (customer.smsCodeExpire && new Date().getTime() - customer.smsCodeExpire.getTime() > 5 * 60 * 1000) - ) { + // if no sms code has been generated, generate it + // if code has expired, regenerate it + const now = new Date().getTime(); + if (!customer.smsCodeExpire || now > customer.smsCodeExpire.getTime()) { customer = await this.customerService.generateSmsCode(customer); } @@ -57,7 +56,7 @@ export default class AuthController extends ApiController { } try { - this.httpSuccess(response, { email, customer }); + this.httpSuccess(response, { info: "Sending a sms for a connection" }); } catch (error) { console.log(error); this.httpInternalError(response); @@ -68,12 +67,19 @@ export default class AuthController extends ApiController { @Post("/api/v1/customer/login") protected async login(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; @@ -95,6 +101,21 @@ export default class AuthController extends ApiController { return; } + if (!customer.smsCode) { + this.httpBadRequest(response, "No sms code found"); + return; + } + + if (!customer.smsCodeExpire || new Date().getTime() > customer.smsCodeExpire.getTime()) { + this.httpBadRequest(response, "Sms code expired"); + return; + } + + if (customer.smsCode !== smsCode) { + this.httpBadRequest(response, "Invalid sms code"); + return; + } + if (!customer.password) { this.httpBadRequest(response, "Customer not registered"); return; @@ -108,7 +129,7 @@ export default class AuthController extends ApiController { } try { - this.httpSuccess(response, { customer }); + this.httpSuccess(response, { customer: Customer.hydrate(customer) }); } catch (error) { console.log(error); this.httpInternalError(response); @@ -153,6 +174,11 @@ export default class AuthController extends ApiController { return; } + if (customer.password) { + this.httpBadRequest(response, "Password already set, please login"); + return; + } + if (!customer.smsCode) { this.httpBadRequest(response, "No sms code found"); return; @@ -163,11 +189,6 @@ export default class AuthController extends ApiController { return; } - if (customer.password) { - this.httpBadRequest(response, "Password already set"); - return; - } - const hashedPassword = await this.authService.hashPassword(password); await this.customerService.setPassword(customer, hashedPassword); diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index 6461e6e4..15e561fd 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -32,12 +32,13 @@ export default class CustomersService extends BaseService { */ public async generateSmsCode(customer: Customer) { const smsCode = Math.floor(100000 + Math.random() * 900000); + const now = new Date(); return await this.customerRepository.update( customer.uid as string, Customer.hydrate({ ...customer, smsCode: smsCode.toString(), - smsCodeExpire: new Date(), + smsCodeExpire: new Date(now.getTime() + 5 * 60 * 1000), }), ); } From 65d6e548d113a315fee40259b3e1342aad155e19 Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Fri, 24 Nov 2023 10:49:00 +0100 Subject: [PATCH 03/46] :bug: Can now update excluded vars --- .../repositories/CustomersRepository.ts | 20 +++++++++++++------ .../CustomersService/CustomersService.ts | 12 ++++++----- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/common/repositories/CustomersRepository.ts b/src/common/repositories/CustomersRepository.ts index d9acaef5..57aaa67f 100644 --- a/src/common/repositories/CustomersRepository.ts +++ b/src/common/repositories/CustomersRepository.ts @@ -4,6 +4,13 @@ import { Service } from "typedi"; import { Customers, ECivility, ECustomerStatus, Prisma } from "@prisma/client"; import { Customer } from "le-coffre-resources/dist/SuperAdmin"; +type IExcludedCustomerVars = { + smsCode?: string; + smsCodeExpire?: Date; + passwordCode?: string; + passwordcodeExpire?: Date; + password?: string; +}; @Service() export default class CustomersRepository extends BaseRepository { constructor(private database: Database) { @@ -70,7 +77,7 @@ export default class CustomersRepository extends BaseRepository { /** * @description : Update data from a customer */ - public async update(uid: string, customer: Customer): Promise { + public async update(uid: string, customer: Customer, excludedVars?: IExcludedCustomerVars): Promise { const updateArgs: Prisma.CustomersUpdateArgs = { where: { uid: uid, @@ -88,13 +95,14 @@ export default class CustomersRepository extends BaseRepository { address: {}, }, }, - smsCode: customer.smsCode, - smsCodeExpire: customer.smsCodeExpire, - passwordCode: customer.passwordCode, - passwordcodeExpire: customer.passwordCodeExpire, - password: customer.password, + smsCode: excludedVars && excludedVars.smsCode, + smsCodeExpire: excludedVars && excludedVars.smsCodeExpire, + passwordCode: excludedVars && excludedVars.passwordCode, + passwordcodeExpire: excludedVars && excludedVars.passwordcodeExpire, + password: excludedVars && excludedVars.password, }, }; + if (customer.contact!.address) { updateArgs.data.contact!.update!.address!.update = { address: customer.contact!.address!.address, diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index 15e561fd..82eadc72 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -37,9 +37,11 @@ export default class CustomersService extends BaseService { customer.uid as string, Customer.hydrate({ ...customer, - smsCode: smsCode.toString(), - smsCodeExpire: new Date(now.getTime() + 5 * 60 * 1000), }), + { + smsCode: smsCode.toString(), + smsCodeExpire: new Date(now.getTime() + 5 * 60000), + }, ); } @@ -52,10 +54,10 @@ export default class CustomersService extends BaseService { customer.uid as string, Customer.hydrate({ ...customer, - password, - smsCode: null, - smsCodeExpire: null, }), + { + password, + }, ); } } From 624dc26dcabe1945578ce905e929071a2cd0cfa8 Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Fri, 24 Nov 2023 10:49:18 +0100 Subject: [PATCH 04/46] :sparkles: Login & set password not returns a valid JWT --- src/app/api/customer/AuthController.ts | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/app/api/customer/AuthController.ts b/src/app/api/customer/AuthController.ts index 5492bd52..e57f695c 100644 --- a/src/app/api/customer/AuthController.ts +++ b/src/app/api/customer/AuthController.ts @@ -22,7 +22,7 @@ export default class AuthController extends ApiController { return; } - let customer = await this.customerService.getOne({ + const customer = await this.customerService.getOne({ where: { contact: { email, @@ -41,8 +41,16 @@ export default class AuthController extends ApiController { // if no sms code has been generated, generate it // if code has expired, regenerate it const now = new Date().getTime(); - if (!customer.smsCodeExpire || now > customer.smsCodeExpire.getTime()) { - customer = await this.customerService.generateSmsCode(customer); + if (customer.smsCodeExpire && now < customer.smsCodeExpire.getTime()) { + this.httpBadRequest(response, "Last sms code is still valid"); + return; + } + + try { + await this.customerService.generateSmsCode(customer); + } catch (error) { + console.log(error); + this.httpInternalError(response); } if (!customer.password) { @@ -128,8 +136,12 @@ export default class AuthController extends ApiController { return; } + const customerHydrated = Customer.hydrate(customer); + const payload = await this.authService.getCustomerJwtPayload([customerHydrated]); + const accessToken = this.authService.generateAccessToken(payload); + const refreshToken = this.authService.generateRefreshToken(payload); try { - this.httpSuccess(response, { customer: Customer.hydrate(customer) }); + this.httpSuccess(response, { accessToken, refreshToken }); } catch (error) { console.log(error); this.httpInternalError(response); @@ -192,8 +204,12 @@ export default class AuthController extends ApiController { const hashedPassword = await this.authService.hashPassword(password); await this.customerService.setPassword(customer, hashedPassword); + const customerHydrated = Customer.hydrate(customer); + const payload = await this.authService.getCustomerJwtPayload([customerHydrated]); + const accessToken = this.authService.generateAccessToken(payload); + const refreshToken = this.authService.generateRefreshToken(payload); try { - this.httpSuccess(response, { email }); + this.httpSuccess(response, { accessToken, refreshToken }); } catch (error) { console.log(error); this.httpInternalError(response); From a3deb8dc2320142892e724d86c61ed75689fe2a2 Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Fri, 24 Nov 2023 10:50:22 +0100 Subject: [PATCH 05/46] :sparkles: Updating resources --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eaa73a16..9cbd22f2 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "file-type-checker": "^1.0.8", "fp-ts": "^2.16.1", "jsonwebtoken": "^9.0.0", - "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.96", + "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.97", "module-alias": "^2.2.2", "monocle-ts": "^2.3.13", "multer": "^1.4.5-lts.1", From fc8b86b7ca5d8cad6818d19d72758d5a50b414fe Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Fri, 24 Nov 2023 12:01:22 +0100 Subject: [PATCH 06/46] :sparkles: Refacto auth controller --- src/app/api/customer/AuthController.ts | 57 ++++------------ .../controller-pattern/BaseController.ts | 4 ++ .../system/controller-pattern/HttpCodes.ts | 1 + .../CustomersService/CustomersService.ts | 65 +++++++++++++++++-- 4 files changed, 75 insertions(+), 52 deletions(-) diff --git a/src/app/api/customer/AuthController.ts b/src/app/api/customer/AuthController.ts index e57f695c..bd88164a 100644 --- a/src/app/api/customer/AuthController.ts +++ b/src/app/api/customer/AuthController.ts @@ -3,7 +3,7 @@ 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 CustomersService, { SmsNotExpiredError } from "@Services/customer/CustomersService/CustomersService"; import AuthService, { ICustomerJwtPayload } from "@Services/common/AuthService/AuthService"; import { Customer } from "le-coffre-resources/dist/SuperAdmin"; @@ -14,61 +14,28 @@ export default class AuthController extends ApiController { super(); } - @Post("/api/v1/customer/pre-login") - protected async preLogin(req: Request, response: Response) { + @Post("/api/v1/customer/login/mail/verify-sms") + protected async mailVerifySms(req: Request, response: Response) { const email = req.body["email"]; if (!email) { this.httpBadRequest(response, "Email 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 no sms code has been generated, generate it - // if code has expired, regenerate it - const now = new Date().getTime(); - if (customer.smsCodeExpire && now < customer.smsCodeExpire.getTime()) { - this.httpBadRequest(response, "Last sms code is still valid"); - return; - } - try { - await this.customerService.generateSmsCode(customer); - } catch (error) { - console.log(error); - this.httpInternalError(response); - } - - if (!customer.password) { - try { - this.httpSuccess(response, { info: "Sending a sms for first connection" }); - } catch (error) { - console.log(error); - this.httpInternalError(response); + const customer = await this.customerService.verifyEmail2FASms(email); + if (!customer) { + this.httpNotFoundRequest(response, "Customer not found"); + return; } - return; - } - - try { - this.httpSuccess(response, { info: "Sending a sms for a connection" }); + this.httpSuccess(response, { partialPhoneNumber: customer.contact?.cell_phone_number.slice(-4) }); } catch (error) { + if (error instanceof SmsNotExpiredError) { + this.httpTooEarlyRequest(response, error.message); + return; + } console.log(error); this.httpInternalError(response); - return; } } diff --git a/src/common/system/controller-pattern/BaseController.ts b/src/common/system/controller-pattern/BaseController.ts index 7baa9c02..0e5003cc 100644 --- a/src/common/system/controller-pattern/BaseController.ts +++ b/src/common/system/controller-pattern/BaseController.ts @@ -20,6 +20,10 @@ export default abstract class BaseController { return this.httpResponse(response, HttpCodes.BAD_REQUEST, responseData); } + protected httpTooEarlyRequest(response: Response, responseData: IResponseData = "Http Too Early Request") { + return this.httpResponse(response, HttpCodes.TOO_EARLY, responseData); + } + protected httpValidationError(response: Response, responseData: IResponseData = "Http Validation Error") { return this.httpResponse(response, HttpCodes.VALIDATION_ERROR, responseData); } diff --git a/src/common/system/controller-pattern/HttpCodes.ts b/src/common/system/controller-pattern/HttpCodes.ts index 95c4a67d..bd89e047 100644 --- a/src/common/system/controller-pattern/HttpCodes.ts +++ b/src/common/system/controller-pattern/HttpCodes.ts @@ -9,5 +9,6 @@ enum HttpCodes { NOT_FOUND = 404, UNAUTHORIZED = 401, FORBIDDEN = 403, + TOO_EARLY = 425, } export default HttpCodes; diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index 82eadc72..9b8dae07 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -4,6 +4,11 @@ import BaseService from "@Services/BaseService"; import { Customer } from "le-coffre-resources/dist/Notary"; import { Service } from "typedi"; +export class SmsNotExpiredError extends Error { + constructor() { + super("SMS code not expired"); + } +} @Service() export default class CustomersService extends BaseService { constructor(private customerRepository: CustomersRepository) { @@ -26,25 +31,58 @@ export default class CustomersService extends BaseService { return this.customerRepository.findOne(query); } + public async userExistsByEmail(email: string): Promise { + return !!(await this.getByEmail(email)); + } + /** - * @description : Generate a SMS code for a customer - * @throws {Error} + * @description : Send SMS to verify the email of a customer (2FA) + * 1: Check if the customer exists + * 2: Check if the SMS code is still valid + * 3: Generate a new SMS code + * 4: Save the SMS code in database + * 5: Send the SMS code to the customer */ - public async generateSmsCode(customer: Customer) { - const smsCode = Math.floor(100000 + Math.random() * 900000); - const now = new Date(); + public async verifyEmail2FASms(email: string): Promise { + const customer = await this.getByEmail(email); + if (!customer) return null; + const now = new Date().getTime(); + // Check if the SMS code is still valid + if (customer.smsCodeExpire && now < customer.smsCodeExpire.getTime()) throw new SmsNotExpiredError(); + + const totpPin = this.generateTotp(); + + await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60000)); + + // Send the SMS code to the customer + await this.sendSmsCodeToCustomer(totpPin, customer); + return customer; + } + + /** + * @description : Saves a TotpPin in database + */ + private async saveTotpPin(customer: Customer, totpPin: number, expireAt: Date) { return await this.customerRepository.update( customer.uid as string, Customer.hydrate({ ...customer, }), { - smsCode: smsCode.toString(), - smsCodeExpire: new Date(now.getTime() + 5 * 60000), + smsCode: totpPin.toString(), + smsCodeExpire: expireAt, }, ); } + private generateTotp() { + return Math.floor(100000 + Math.random() * 900000); + } + + private async sendSmsCodeToCustomer(totpPin: number, customer: Customer) { + console.log(totpPin); + } + /** * @description : Set password for a customer * @throws {Error} If customer cannot be updated @@ -60,4 +98,17 @@ export default class CustomersService extends BaseService { }, ); } + + private getByEmail(email: string) { + return this.customerRepository.findOne({ + where: { + contact: { + email, + }, + }, + include: { + contact: true, + }, + }); + } } From d3f9527f85c354a1a6d51bc8fc6463caaaeb947f Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Fri, 24 Nov 2023 14:14:19 +0100 Subject: [PATCH 07/46] :sparkles: Refacto login --- src/app/api/customer/AuthController.ts | 76 +++++++------------ .../CustomersService/CustomersService.ts | 64 +++++++++++++++- 2 files changed, 90 insertions(+), 50 deletions(-) diff --git a/src/app/api/customer/AuthController.ts b/src/app/api/customer/AuthController.ts index bd88164a..4c9f000b 100644 --- a/src/app/api/customer/AuthController.ts +++ b/src/app/api/customer/AuthController.ts @@ -3,7 +3,13 @@ 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, { SmsNotExpiredError } from "@Services/customer/CustomersService/CustomersService"; +import CustomersService, { + InvalidPasswordError, + InvalidTotpCodeError, + NotRegisteredCustomerError, + SmsNotExpiredError, + TotpCodeExpiredError, +} from "@Services/customer/CustomersService/CustomersService"; import AuthService, { ICustomerJwtPayload } from "@Services/common/AuthService/AuthService"; import { Customer } from "le-coffre-resources/dist/SuperAdmin"; @@ -28,7 +34,7 @@ export default class AuthController extends ApiController { this.httpNotFoundRequest(response, "Customer not found"); return; } - this.httpSuccess(response, { partialPhoneNumber: customer.contact?.cell_phone_number.slice(-4) }); + this.httpSuccess(response, { partialPhoneNumber: customer.contact?.cell_phone_number.replace(/\s/g, "").slice(-4) }); } catch (error) { if (error instanceof SmsNotExpiredError) { this.httpTooEarlyRequest(response, error.message); @@ -60,56 +66,28 @@ export default class AuthController extends ApiController { return; } - let 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.smsCodeExpire || new Date().getTime() > customer.smsCodeExpire.getTime()) { - this.httpBadRequest(response, "Sms code expired"); - return; - } - - if (customer.smsCode !== smsCode) { - this.httpBadRequest(response, "Invalid sms code"); - 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; - } - - const customerHydrated = Customer.hydrate(customer); - const payload = await this.authService.getCustomerJwtPayload([customerHydrated]); - const accessToken = this.authService.generateAccessToken(payload); - const refreshToken = this.authService.generateRefreshToken(payload); try { + const customer = await this.customerService.login(email, smsCode, password); + if (!customer) { + this.httpBadRequest(response, "Customer not found"); + return; + } + const customerHydrated = Customer.hydrate(customer); + const payload = await this.authService.getCustomerJwtPayload([customerHydrated]); + const accessToken = this.authService.generateAccessToken(payload); + const refreshToken = this.authService.generateRefreshToken(payload); this.httpSuccess(response, { accessToken, refreshToken }); } catch (error) { + if (error instanceof TotpCodeExpiredError || error instanceof NotRegisteredCustomerError) { + this.httpBadRequest(response, error.message); + return; + } + + if (error instanceof InvalidTotpCodeError || error instanceof InvalidPasswordError) { + this.httpUnauthorized(response, error.message); + return; + } + console.log(error); this.httpInternalError(response); return; diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index 9b8dae07..85aca519 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -1,6 +1,7 @@ import { Customers, Prisma } from "@prisma/client"; import CustomersRepository from "@Repositories/CustomersRepository"; import BaseService from "@Services/BaseService"; +import AuthService from "@Services/common/AuthService/AuthService"; import { Customer } from "le-coffre-resources/dist/Notary"; import { Service } from "typedi"; @@ -9,9 +10,33 @@ export class SmsNotExpiredError extends Error { super("SMS code not expired"); } } + +export class TotpCodeExpiredError extends Error { + constructor() { + super("Totp code not found or expired"); + } +} + +export class InvalidTotpCodeError extends Error { + constructor() { + super("Invalid Totp code"); + } +} + +export class NotRegisteredCustomerError extends Error { + constructor() { + super("Customer not registered"); + } +} + +export class InvalidPasswordError extends Error { + constructor() { + super("Invalid password"); + } +} @Service() export default class CustomersService extends BaseService { - constructor(private customerRepository: CustomersRepository) { + constructor(private customerRepository: CustomersRepository, private authService: AuthService) { super(); } @@ -111,4 +136,41 @@ export default class CustomersService extends BaseService { }, }); } + + /** + * + * @description : Login a customer + * 1: Check if the customer exists + * 2: Check if the SMS code is existing and is not expired + * 3: Check if the SMS code is valid + * 4: Check if the user has a password or it's their first login + * 5: Check if the password is valid + * 6: Return the customer + * @param email + * @param smsCode + * @param password + * @returns Customer | null + */ + public async login(email: string, smsCode: string, password: string): Promise { + // 1: Check if the customer exists + const customer = await this.getByEmail(email); + if (!customer) return null; + + // 2: Check if the SMS code is existing and is not expired + if (!customer.smsCode || !customer.smsCodeExpire || new Date().getTime() > customer.smsCodeExpire.getTime()) + throw new TotpCodeExpiredError(); + + // 3: Check if the SMS code is valid + if (customer.smsCode !== smsCode) throw new InvalidTotpCodeError(); + + // 4: Check if the user has a password or it's their first login + if (!customer.password) throw new NotRegisteredCustomerError(); + + // 5: Check if the password is valid + const isPasswordValid = await this.authService.comparePassword(password, customer.password); + if (!isPasswordValid) throw new InvalidPasswordError(); + + // 6: Return the customer + return customer; + } } From 97fd3f0d86a671d28f8ce56d25c95039dcccee10 Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Fri, 24 Nov 2023 14:23:55 +0100 Subject: [PATCH 08/46] :sparkles: Refacto set first password --- src/app/api/customer/AuthController.ts | 59 +++----- .../CustomersService/CustomersService.ts | 139 ++++++++++++------ 2 files changed, 111 insertions(+), 87 deletions(-) diff --git a/src/app/api/customer/AuthController.ts b/src/app/api/customer/AuthController.ts index 4c9f000b..b2c1e204 100644 --- a/src/app/api/customer/AuthController.ts +++ b/src/app/api/customer/AuthController.ts @@ -7,6 +7,7 @@ import CustomersService, { InvalidPasswordError, InvalidTotpCodeError, NotRegisteredCustomerError, + PasswordAlreadySetError, SmsNotExpiredError, TotpCodeExpiredError, } from "@Services/customer/CustomersService/CustomersService"; @@ -115,47 +116,29 @@ export default class AuthController extends ApiController { return; } - const 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, "Password already set, please login"); - return; - } - - if (!customer.smsCode) { - this.httpBadRequest(response, "No sms code found"); - return; - } - - if (customer.smsCode !== smsCode) { - this.httpBadRequest(response, "Invalid sms code"); - return; - } - - const hashedPassword = await this.authService.hashPassword(password); - await this.customerService.setPassword(customer, hashedPassword); - - const customerHydrated = Customer.hydrate(customer); - const payload = await this.authService.getCustomerJwtPayload([customerHydrated]); - const accessToken = this.authService.generateAccessToken(payload); - const refreshToken = this.authService.generateRefreshToken(payload); try { + const customer = await this.customerService.setFirstPassword(email, smsCode, password); + if (!customer) { + this.httpBadRequest(response, "Customer not found"); + return; + } + + const customerHydrated = Customer.hydrate(customer); + const payload = await this.authService.getCustomerJwtPayload([customerHydrated]); + const accessToken = this.authService.generateAccessToken(payload); + const refreshToken = this.authService.generateRefreshToken(payload); this.httpSuccess(response, { accessToken, refreshToken }); } catch (error) { + if (error instanceof TotpCodeExpiredError || error instanceof PasswordAlreadySetError) { + this.httpBadRequest(response, error.message); + return; + } + + if (error instanceof InvalidTotpCodeError) { + this.httpUnauthorized(response, error.message); + return; + } + console.log(error); this.httpInternalError(response); return; diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index 85aca519..63885261 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -34,6 +34,12 @@ export class InvalidPasswordError extends Error { super("Invalid password"); } } + +export class PasswordAlreadySetError extends Error { + constructor() { + super("Password already set"); + } +} @Service() export default class CustomersService extends BaseService { constructor(private customerRepository: CustomersRepository, private authService: AuthService) { @@ -56,10 +62,6 @@ export default class CustomersService extends BaseService { return this.customerRepository.findOne(query); } - public async userExistsByEmail(email: string): Promise { - return !!(await this.getByEmail(email)); - } - /** * @description : Send SMS to verify the email of a customer (2FA) * 1: Check if the customer exists @@ -85,56 +87,42 @@ export default class CustomersService extends BaseService { } /** - * @description : Saves a TotpPin in database + * @description : Set the password of a customer when it's the first time they connect + * 1: Check if the customer exists + * 2: Check if the password is already set + * 3: Check if the SMS code is existing and is not expired + * 4: Check if the SMS code is valid + * 5: Hash the password + * 6: Set the password in database + * 7: Returns the customer + * @param email + * @param smsCode + * @param password + * @returns */ - private async saveTotpPin(customer: Customer, totpPin: number, expireAt: Date) { - return await this.customerRepository.update( - customer.uid as string, - Customer.hydrate({ - ...customer, - }), - { - smsCode: totpPin.toString(), - smsCodeExpire: expireAt, - }, - ); - } + public async setFirstPassword(email: string, smsCode: string, password: string): Promise { + // 1: Check if the customer exists + const customer = await this.getByEmail(email); + if (!customer) return null; - private generateTotp() { - return Math.floor(100000 + Math.random() * 900000); - } + // 2: Check if the password is already set + if (customer.password) throw new PasswordAlreadySetError(); - private async sendSmsCodeToCustomer(totpPin: number, customer: Customer) { - console.log(totpPin); - } + // 3: Check if the SMS code is existing and is not expired + if (!customer.smsCode || !customer.smsCodeExpire || new Date().getTime() > customer.smsCodeExpire.getTime()) + throw new TotpCodeExpiredError(); - /** - * @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, - }), - { - password, - }, - ); - } + // 4: Check if the SMS code is valid + if (customer.smsCode !== smsCode) throw new InvalidTotpCodeError(); - private getByEmail(email: string) { - return this.customerRepository.findOne({ - where: { - contact: { - email, - }, - }, - include: { - contact: true, - }, - }); + // 5: Hash the password + const hashedPassword = await this.authService.hashPassword(password); + + // 6: Set the password in database + await this.setPassword(customer, hashedPassword); + + // 7: Returns the customer + return customer; } /** @@ -173,4 +161,57 @@ export default class CustomersService extends BaseService { // 6: Return the customer return customer; } + + /** + * @description : Set password for a customer + * @throws {Error} If customer cannot be updated + */ + private async setPassword(customer: Customer, password: string) { + return await this.customerRepository.update( + customer.uid as string, + Customer.hydrate({ + ...customer, + }), + { + password, + }, + ); + } + + private getByEmail(email: string) { + return this.customerRepository.findOne({ + where: { + contact: { + email, + }, + }, + include: { + contact: true, + }, + }); + } + + /** + * @description : Saves a TotpPin in database + */ + private async saveTotpPin(customer: Customer, totpPin: number, expireAt: Date) { + return await this.customerRepository.update( + customer.uid as string, + Customer.hydrate({ + ...customer, + }), + { + smsCode: totpPin.toString(), + smsCodeExpire: expireAt, + }, + ); + } + + private generateTotp() { + return Math.floor(100000 + Math.random() * 900000); + } + + private async sendSmsCodeToCustomer(totpPin: number, customer: Customer) { + console.log(totpPin); + } } From 5a870b77125dc7710762e1af9f14ef90c19eabc5 Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Fri, 24 Nov 2023 14:34:29 +0100 Subject: [PATCH 09/46] :sparkles: Updating ts config --- src/app/api/customer/AuthController.ts | 53 +------------------------- tsconfig.json | 2 +- 2 files changed, 2 insertions(+), 53 deletions(-) diff --git a/src/app/api/customer/AuthController.ts b/src/app/api/customer/AuthController.ts index b2c1e204..2089cb11 100644 --- a/src/app/api/customer/AuthController.ts +++ b/src/app/api/customer/AuthController.ts @@ -2,7 +2,6 @@ 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, { InvalidPasswordError, InvalidTotpCodeError, @@ -11,7 +10,7 @@ import CustomersService, { SmsNotExpiredError, TotpCodeExpiredError, } from "@Services/customer/CustomersService/CustomersService"; -import AuthService, { ICustomerJwtPayload } from "@Services/common/AuthService/AuthService"; +import AuthService from "@Services/common/AuthService/AuthService"; import { Customer } from "le-coffre-resources/dist/SuperAdmin"; @Controller() @@ -144,54 +143,4 @@ export default class AuthController extends ApiController { 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; - } - } } diff --git a/tsconfig.json b/tsconfig.json index bbd8ba4d..bf9fc16e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,7 +27,7 @@ "alwaysStrict": true, "noPropertyAccessFromIndexSignature": true, /* Additional Checks */ - "noUnusedLocals": false, + "noUnusedLocals": true, "noUnusedParameters": false, "noImplicitReturns": true, "noUncheckedIndexedAccess": true, From 8192d93330888cbda03fde52b6b2f0bcadd3c281 Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Fri, 24 Nov 2023 17:28:14 +0100 Subject: [PATCH 10/46] :sparkles: Better API links --- src/app/api/customer/AuthController.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/api/customer/AuthController.ts b/src/app/api/customer/AuthController.ts index 2089cb11..dc5a40ba 100644 --- a/src/app/api/customer/AuthController.ts +++ b/src/app/api/customer/AuthController.ts @@ -20,7 +20,7 @@ export default class AuthController extends ApiController { super(); } - @Post("/api/v1/customer/login/mail/verify-sms") + @Post("/api/v1/customer/auth/mail/verify-sms") protected async mailVerifySms(req: Request, response: Response) { const email = req.body["email"]; if (!email) { @@ -45,7 +45,7 @@ export default class AuthController extends ApiController { } } - @Post("/api/v1/customer/login") + @Post("/api/v1/customer/auth/login") protected async login(req: Request, response: Response) { const email = req.body["email"]; const smsCode = req.body["smsCode"]; @@ -94,7 +94,7 @@ export default class AuthController extends ApiController { } } - @Post("/api/v1/customer/set-password") + @Post("/api/v1/customer/auth/set-password") protected async setPassword(req: Request, response: Response) { const email = req.body["email"]; const smsCode = req.body["smsCode"]; From d32532b5ede2ea92bb2643b2beb144c5cd5c96c8 Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Mon, 27 Nov 2023 09:53:10 +0100 Subject: [PATCH 11/46] :sparkles: Route for checking sms code --- src/app/api/customer/AuthController.ts | 31 +++++++++++++++++++ .../CustomersService/CustomersService.ts | 21 +++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/app/api/customer/AuthController.ts b/src/app/api/customer/AuthController.ts index dc5a40ba..4cbd2578 100644 --- a/src/app/api/customer/AuthController.ts +++ b/src/app/api/customer/AuthController.ts @@ -143,4 +143,35 @@ export default class AuthController extends ApiController { return; } } + + @Post("/api/v1/customer/auth/verify-totp-code") + protected async verifyTotpCode(req: Request, response: Response) { + const totpCode = req.body["totpCode"]; + const email = req.body["email"]; + if (!totpCode) { + this.httpBadRequest(response, "totpCode is required"); + return; + } + + if (!email) { + this.httpBadRequest(response, "email is required"); + return; + } + + try { + const customer = await this.customerService.verifyTotpCode(totpCode, email); + if (!customer) { + this.httpNotFoundRequest(response, "Customer not found"); + return; + } + this.httpSuccess(response, { validCode: true }); + } catch (error) { + if (error instanceof InvalidTotpCodeError || error instanceof TotpCodeExpiredError) { + this.httpUnauthorized(response, error.message); + return; + } + console.log(error); + this.httpInternalError(response); + } + } } diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index 63885261..f72ca4af 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -214,4 +214,25 @@ export default class CustomersService extends BaseService { private async sendSmsCodeToCustomer(totpPin: number, customer: Customer) { console.log(totpPin); } + + public async verifyTotpCode(totpCode: string, email: string): Promise { + // 1: Check if the customer exists + // 2: Check if the SMS code is existing and is not expired + // 3: Check if the SMS code is valid + // 4: Return the customer + + // 1: Check if the customer exists + const customer = await this.getByEmail(email); + if (!customer) return null; + + // 2: Check if the SMS code is existing and is not expired + if (!customer.smsCode || !customer.smsCodeExpire || new Date().getTime() > customer.smsCodeExpire.getTime()) + throw new TotpCodeExpiredError(); + + // 3: Check if the SMS code is valid + if (customer.smsCode !== totpCode) throw new InvalidTotpCodeError(); + + // 4: Return the customer + return customer; + } } From 3835127d634e4a801d1b5068a7004def59f4b1b6 Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Mon, 27 Nov 2023 10:55:38 +0100 Subject: [PATCH 12/46] :sparkles: Updating sms code in totp code --- src/app/api/customer/AuthController.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/app/api/customer/AuthController.ts b/src/app/api/customer/AuthController.ts index 4cbd2578..40b07421 100644 --- a/src/app/api/customer/AuthController.ts +++ b/src/app/api/customer/AuthController.ts @@ -48,26 +48,26 @@ export default class AuthController extends ApiController { @Post("/api/v1/customer/auth/login") protected async login(req: Request, response: Response) { const email = req.body["email"]; - const smsCode = req.body["smsCode"]; + const totpCode = req.body["totpCode"]; const password = req.body["password"]; if (!email) { - this.httpBadRequest(response, "Email is required"); + this.httpBadRequest(response, "email is required"); return; } - if (!smsCode) { - this.httpBadRequest(response, "Sms code is required"); + if (!totpCode) { + this.httpBadRequest(response, "totpCode is required"); return; } if (!password) { - this.httpBadRequest(response, "Password is required"); + this.httpBadRequest(response, "password is required"); return; } try { - const customer = await this.customerService.login(email, smsCode, password); + const customer = await this.customerService.login(email, totpCode, password); if (!customer) { this.httpBadRequest(response, "Customer not found"); return; @@ -97,7 +97,7 @@ export default class AuthController extends ApiController { @Post("/api/v1/customer/auth/set-password") protected async setPassword(req: Request, response: Response) { const email = req.body["email"]; - const smsCode = req.body["smsCode"]; + const totpCode = req.body["totpCode"]; const password = req.body["password"]; if (!email) { @@ -105,7 +105,7 @@ export default class AuthController extends ApiController { return; } - if (!smsCode) { + if (!totpCode) { this.httpBadRequest(response, "Sms code is required"); return; } @@ -116,7 +116,7 @@ export default class AuthController extends ApiController { } try { - const customer = await this.customerService.setFirstPassword(email, smsCode, password); + const customer = await this.customerService.setFirstPassword(email, totpCode, password); if (!customer) { this.httpBadRequest(response, "Customer not found"); return; @@ -164,7 +164,7 @@ export default class AuthController extends ApiController { this.httpNotFoundRequest(response, "Customer not found"); return; } - this.httpSuccess(response, { validCode: true }); + this.httpSuccess(response, { validCode: true, firstConnection: customer.password === null }); } catch (error) { if (error instanceof InvalidTotpCodeError || error instanceof TotpCodeExpiredError) { this.httpUnauthorized(response, error.message); From 6c001625449d2f8a6bc60b19012e78df53cb0c63 Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Mon, 27 Nov 2023 15:32:52 +0100 Subject: [PATCH 13/46] :sparkles: Renaming smsCode into TotpCode --- package.json | 2 +- .../migration.sql | 6 ++--- src/common/databases/schema.prisma | 6 ++--- .../repositories/CustomersRepository.ts | 12 +++------ .../CustomersService/CustomersService.ts | 26 +++++++++---------- 5 files changed, 22 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index 9cbd22f2..645340ef 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "file-type-checker": "^1.0.8", "fp-ts": "^2.16.1", "jsonwebtoken": "^9.0.0", - "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.97", + "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.98", "module-alias": "^2.2.2", "monocle-ts": "^2.3.13", "multer": "^1.4.5-lts.1", diff --git a/src/common/databases/migrations/20231123104754_customer_login/migration.sql b/src/common/databases/migrations/20231123104754_customer_login/migration.sql index 0bf90ced..db9e4228 100644 --- a/src/common/databases/migrations/20231123104754_customer_login/migration.sql +++ b/src/common/databases/migrations/20231123104754_customer_login/migration.sql @@ -1,6 +1,4 @@ -- 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; +ADD COLUMN "totpCode" VARCHAR(255), +ADD COLUMN "totpCodeExpire" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP; diff --git a/src/common/databases/schema.prisma b/src/common/databases/schema.prisma index 74b5b9c5..1f77bd0d 100644 --- a/src/common/databases/schema.prisma +++ b/src/common/databases/schema.prisma @@ -102,10 +102,8 @@ model Customers { office_folders OfficeFolders[] @relation("OfficeFolderHasCustomers") 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()) + totpCode String? @db.VarChar(255) + totpCodeExpire DateTime? @default(now()) @@map("customers") diff --git a/src/common/repositories/CustomersRepository.ts b/src/common/repositories/CustomersRepository.ts index 57aaa67f..6ab46516 100644 --- a/src/common/repositories/CustomersRepository.ts +++ b/src/common/repositories/CustomersRepository.ts @@ -5,10 +5,8 @@ import { Customers, ECivility, ECustomerStatus, Prisma } from "@prisma/client"; import { Customer } from "le-coffre-resources/dist/SuperAdmin"; type IExcludedCustomerVars = { - smsCode?: string; - smsCodeExpire?: Date; - passwordCode?: string; - passwordcodeExpire?: Date; + totpCode?: string; + totpCodeExpire?: Date; password?: string; }; @Service() @@ -95,10 +93,8 @@ export default class CustomersRepository extends BaseRepository { address: {}, }, }, - smsCode: excludedVars && excludedVars.smsCode, - smsCodeExpire: excludedVars && excludedVars.smsCodeExpire, - passwordCode: excludedVars && excludedVars.passwordCode, - passwordcodeExpire: excludedVars && excludedVars.passwordcodeExpire, + totpCode: excludedVars && excludedVars.totpCode, + totpCodeExpire: excludedVars && excludedVars.totpCodeExpire, password: excludedVars && excludedVars.password, }, }; diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index f72ca4af..fe52345b 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -75,7 +75,7 @@ export default class CustomersService extends BaseService { if (!customer) return null; const now = new Date().getTime(); // Check if the SMS code is still valid - if (customer.smsCodeExpire && now < customer.smsCodeExpire.getTime()) throw new SmsNotExpiredError(); + if (customer.totpCodeExpire && now < customer.totpCodeExpire.getTime()) throw new SmsNotExpiredError(); const totpPin = this.generateTotp(); @@ -96,11 +96,11 @@ export default class CustomersService extends BaseService { * 6: Set the password in database * 7: Returns the customer * @param email - * @param smsCode + * @param totpCode * @param password * @returns */ - public async setFirstPassword(email: string, smsCode: string, password: string): Promise { + public async setFirstPassword(email: string, totpCode: string, password: string): Promise { // 1: Check if the customer exists const customer = await this.getByEmail(email); if (!customer) return null; @@ -109,11 +109,11 @@ export default class CustomersService extends BaseService { if (customer.password) throw new PasswordAlreadySetError(); // 3: Check if the SMS code is existing and is not expired - if (!customer.smsCode || !customer.smsCodeExpire || new Date().getTime() > customer.smsCodeExpire.getTime()) + if (!customer.totpCode || !customer.totpCodeExpire || new Date().getTime() > customer.totpCodeExpire.getTime()) throw new TotpCodeExpiredError(); // 4: Check if the SMS code is valid - if (customer.smsCode !== smsCode) throw new InvalidTotpCodeError(); + if (customer.totpCode !== totpCode) throw new InvalidTotpCodeError(); // 5: Hash the password const hashedPassword = await this.authService.hashPassword(password); @@ -135,21 +135,21 @@ export default class CustomersService extends BaseService { * 5: Check if the password is valid * 6: Return the customer * @param email - * @param smsCode + * @param totpCode * @param password * @returns Customer | null */ - public async login(email: string, smsCode: string, password: string): Promise { + public async login(email: string, totpCode: string, password: string): Promise { // 1: Check if the customer exists const customer = await this.getByEmail(email); if (!customer) return null; // 2: Check if the SMS code is existing and is not expired - if (!customer.smsCode || !customer.smsCodeExpire || new Date().getTime() > customer.smsCodeExpire.getTime()) + if (!customer.totpCode || !customer.totpCodeExpire || new Date().getTime() > customer.totpCodeExpire.getTime()) throw new TotpCodeExpiredError(); // 3: Check if the SMS code is valid - if (customer.smsCode !== smsCode) throw new InvalidTotpCodeError(); + if (customer.totpCode !== totpCode) throw new InvalidTotpCodeError(); // 4: Check if the user has a password or it's their first login if (!customer.password) throw new NotRegisteredCustomerError(); @@ -201,8 +201,8 @@ export default class CustomersService extends BaseService { ...customer, }), { - smsCode: totpPin.toString(), - smsCodeExpire: expireAt, + totpCode: totpPin.toString(), + totpCodeExpire: expireAt, }, ); } @@ -226,11 +226,11 @@ export default class CustomersService extends BaseService { if (!customer) return null; // 2: Check if the SMS code is existing and is not expired - if (!customer.smsCode || !customer.smsCodeExpire || new Date().getTime() > customer.smsCodeExpire.getTime()) + if (!customer.totpCode || !customer.totpCodeExpire || new Date().getTime() > customer.totpCodeExpire.getTime()) throw new TotpCodeExpiredError(); // 3: Check if the SMS code is valid - if (customer.smsCode !== totpCode) throw new InvalidTotpCodeError(); + if (customer.totpCode !== totpCode) throw new InvalidTotpCodeError(); // 4: Return the customer return customer; From cdbb3e325769b10fd7650273fc16820d20401651 Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Mon, 27 Nov 2023 15:49:08 +0100 Subject: [PATCH 14/46] :sparkles: Clearing totp code after login/set password --- src/common/repositories/CustomersRepository.ts | 4 ++-- .../customer/CustomersService/CustomersService.ts | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/common/repositories/CustomersRepository.ts b/src/common/repositories/CustomersRepository.ts index 6ab46516..6844fddb 100644 --- a/src/common/repositories/CustomersRepository.ts +++ b/src/common/repositories/CustomersRepository.ts @@ -5,8 +5,8 @@ import { Customers, ECivility, ECustomerStatus, Prisma } from "@prisma/client"; import { Customer } from "le-coffre-resources/dist/SuperAdmin"; type IExcludedCustomerVars = { - totpCode?: string; - totpCodeExpire?: Date; + totpCode?: string | null; + totpCodeExpire?: Date | null; password?: string; }; @Service() diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index fe52345b..ccab039e 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -158,6 +158,16 @@ export default class CustomersService extends BaseService { const isPasswordValid = await this.authService.comparePassword(password, customer.password); if (!isPasswordValid) throw new InvalidPasswordError(); + await this.customerRepository.update( + customer.uid as string, + Customer.hydrate({ + ...customer, + }), + { + totpCode: null, + totpCodeExpire: null, + }, + ); // 6: Return the customer return customer; } @@ -173,6 +183,8 @@ export default class CustomersService extends BaseService { ...customer, }), { + totpCode: null, + totpCodeExpire: null, password, }, ); From b6e1b2ff62ee5d24e7c450dccd0520e0c371eb4d Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Mon, 27 Nov 2023 17:36:46 +0100 Subject: [PATCH 15/46] :sparkles: TotpCodes in tables instead of in customer --- src/app/api/customer/AuthController.ts | 9 +- .../20231127154201_totp_table/migration.sql | 30 +++++ src/common/databases/schema.prisma | 22 +++- .../repositories/CustomersRepository.ts | 5 +- .../repositories/TotpCodesRepository.ts | 61 +++++++++ .../CustomersService/CustomersService.ts | 124 +++++++++++------- 6 files changed, 192 insertions(+), 59 deletions(-) create mode 100644 src/common/databases/migrations/20231127154201_totp_table/migration.sql create mode 100644 src/common/repositories/TotpCodesRepository.ts diff --git a/src/app/api/customer/AuthController.ts b/src/app/api/customer/AuthController.ts index 40b07421..f125cd2f 100644 --- a/src/app/api/customer/AuthController.ts +++ b/src/app/api/customer/AuthController.ts @@ -159,12 +159,15 @@ export default class AuthController extends ApiController { } try { - const customer = await this.customerService.verifyTotpCode(totpCode, email); - if (!customer) { + const code = await this.customerService.verifyTotpCode(totpCode, email); + if (!code) { this.httpNotFoundRequest(response, "Customer not found"); return; } - this.httpSuccess(response, { validCode: true, firstConnection: customer.password === null }); + this.httpSuccess(response, { + validCode: true, + reason: code.reason, + }); } catch (error) { if (error instanceof InvalidTotpCodeError || error instanceof TotpCodeExpiredError) { this.httpUnauthorized(response, error.message); diff --git a/src/common/databases/migrations/20231127154201_totp_table/migration.sql b/src/common/databases/migrations/20231127154201_totp_table/migration.sql new file mode 100644 index 00000000..89a9d24a --- /dev/null +++ b/src/common/databases/migrations/20231127154201_totp_table/migration.sql @@ -0,0 +1,30 @@ +/* + Warnings: + + - You are about to drop the column `totpCode` on the `customers` table. All the data in the column will be lost. + - You are about to drop the column `totpCodeExpire` on the `customers` table. All the data in the column will be lost. + +*/ +-- CreateEnum +CREATE TYPE "TotpCodesReasons" AS ENUM ('LOGIN', 'RESET_PASSWORD', 'FIRST_LOGIN'); + +-- AlterTable +ALTER TABLE "customers" DROP COLUMN "totpCode", +DROP COLUMN "totpCodeExpire"; + +-- CreateTable +CREATE TABLE "totp_codes" ( + "uid" TEXT NOT NULL, + "customer_uid" VARCHAR(255) NOT NULL, + "code" VARCHAR(255) NOT NULL, + "reason" "TotpCodesReasons" NOT NULL DEFAULT 'LOGIN', + "expire_at" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "totp_codes_pkey" PRIMARY KEY ("uid") +); + +-- CreateIndex +CREATE UNIQUE INDEX "totp_codes_uid_key" ON "totp_codes"("uid"); + +-- AddForeignKey +ALTER TABLE "totp_codes" ADD CONSTRAINT "totp_codes_customer_uid_fkey" FOREIGN KEY ("customer_uid") REFERENCES "customers"("uid") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/common/databases/schema.prisma b/src/common/databases/schema.prisma index 1f77bd0d..3c9c0831 100644 --- a/src/common/databases/schema.prisma +++ b/src/common/databases/schema.prisma @@ -102,10 +102,7 @@ model Customers { office_folders OfficeFolders[] @relation("OfficeFolderHasCustomers") documents Documents[] password String? @db.VarChar(255) - totpCode String? @db.VarChar(255) - totpCodeExpire DateTime? @default(now()) - - + totpCodes TotpCodes[] @@map("customers") } @@ -345,6 +342,23 @@ model Votes { @@map("votes") } +model TotpCodes { + uid String @id @unique @default(uuid()) + customer Customers @relation(fields: [customer_uid], references: [uid], onDelete: Cascade) + customer_uid String @db.VarChar(255) + code String @db.VarChar(255) + reason TotpCodesReasons @default(LOGIN) + expire_at DateTime? @default(now()) + + @@map("totp_codes") +} + +enum TotpCodesReasons { + LOGIN + RESET_PASSWORD + FIRST_LOGIN +} + enum ECivility { MALE FEMALE diff --git a/src/common/repositories/CustomersRepository.ts b/src/common/repositories/CustomersRepository.ts index 6844fddb..65876c6f 100644 --- a/src/common/repositories/CustomersRepository.ts +++ b/src/common/repositories/CustomersRepository.ts @@ -5,7 +5,6 @@ import { Customers, ECivility, ECustomerStatus, Prisma } from "@prisma/client"; import { Customer } from "le-coffre-resources/dist/SuperAdmin"; type IExcludedCustomerVars = { - totpCode?: string | null; totpCodeExpire?: Date | null; password?: string; }; @@ -36,7 +35,7 @@ export default class CustomersRepository extends BaseRepository { 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 } } } }); + return this.model.findFirst({ ...query, include: { contact: { include: { address: true } }, ...query.include } }); } /** @@ -93,8 +92,6 @@ export default class CustomersRepository extends BaseRepository { address: {}, }, }, - totpCode: excludedVars && excludedVars.totpCode, - totpCodeExpire: excludedVars && excludedVars.totpCodeExpire, password: excludedVars && excludedVars.password, }, }; diff --git a/src/common/repositories/TotpCodesRepository.ts b/src/common/repositories/TotpCodesRepository.ts new file mode 100644 index 00000000..f70f55ce --- /dev/null +++ b/src/common/repositories/TotpCodesRepository.ts @@ -0,0 +1,61 @@ +import Database from "@Common/databases/database"; +import BaseRepository from "@Repositories/BaseRepository"; +import { Service } from "typedi"; +import { Prisma, TotpCodes } from "@prisma/client"; +import { TotpCodes as TotpCode } from "le-coffre-resources/dist/Customer"; + +type IExcludedTotpCodesVars = { + code?: string; + expire_at?: Date; +}; + +@Service() +export default class TotpCodesRepository extends BaseRepository { + constructor(private database: Database) { + super(); + } + protected get model() { + return this.database.getClient().totpCodes; + } + protected get instanceDb() { + return this.database.getClient(); + } + + /** + * @description : Find many totp codes + */ + public async findMany(query: Prisma.TotpCodesFindManyArgs) { + query.take = Math.min(query.take || this.defaultFetchRows, this.maxFetchRows); + if (!query.include) return this.model.findMany({ ...query }); + return this.model.findMany({ ...query }); + } + + /** + * @description : Find one totp code + */ + public async findOne(query: Prisma.TotpCodesFindFirstArgs) { + query.take = Math.min(query.take || this.defaultFetchRows, this.maxFetchRows); + if (!query.include) return this.model.findFirst({ ...query }); + return this.model.findFirst({ ...query }); + } + + /** + * @description : Create a customer + */ + public async create(totpCode: TotpCode, excludedVars: IExcludedTotpCodesVars): Promise { + const createArgs: Prisma.TotpCodesCreateArgs = { + data: { + code: excludedVars.code!, + reason: totpCode.reason!, + customer: { + connect: { + uid: totpCode.customer_uid!, + }, + }, + expire_at: excludedVars.expire_at!, + }, + }; + + return this.model.create({ ...createArgs }); + } +} diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index ccab039e..496967e1 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -1,7 +1,9 @@ import { Customers, Prisma } from "@prisma/client"; import CustomersRepository from "@Repositories/CustomersRepository"; +import TotpCodesRepository from "@Repositories/TotpCodesRepository"; import BaseService from "@Services/BaseService"; import AuthService from "@Services/common/AuthService/AuthService"; +import TotpCodes, { TotpCodesReasons } from "le-coffre-resources/dist/Customer/TotpCodes"; import { Customer } from "le-coffre-resources/dist/Notary"; import { Service } from "typedi"; @@ -42,7 +44,11 @@ export class PasswordAlreadySetError extends Error { } @Service() export default class CustomersService extends BaseService { - constructor(private customerRepository: CustomersRepository, private authService: AuthService) { + constructor( + private customerRepository: CustomersRepository, + private authService: AuthService, + private totpCodesRepository: TotpCodesRepository, + ) { super(); } @@ -65,23 +71,33 @@ export default class CustomersService extends BaseService { /** * @description : Send SMS to verify the email of a customer (2FA) * 1: Check if the customer exists - * 2: Check if the SMS code is still valid + * 2: Check in the array of totpCodes if one is still valid * 3: Generate a new SMS code * 4: Save the SMS code in database * 5: Send the SMS code to the customer */ public async verifyEmail2FASms(email: string): Promise { + // 1: Check if the customer exists const customer = await this.getByEmail(email); if (!customer) return null; const now = new Date().getTime(); - // Check if the SMS code is still valid - if (customer.totpCodeExpire && now < customer.totpCodeExpire.getTime()) throw new SmsNotExpiredError(); + const customerHydrated = Customer.hydrate(customer); + + // 2: Check in the array of totpCodes if one is still valid + const validTotpCode = customerHydrated.totpCodes?.find((totpCode) => { + return totpCode.expire_at && totpCode.expire_at.getTime() > now; + }); + if (validTotpCode) throw new SmsNotExpiredError(); + + // 3: Generate a new SMS code const totpPin = this.generateTotp(); - await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60000)); + const reason = customer.password ? TotpCodesReasons.LOGIN : TotpCodesReasons.FIRST_LOGIN; + // 4: Save the SMS code in database + await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60000), reason); - // Send the SMS code to the customer + // 5: Send the SMS code to the customer await this.sendSmsCodeToCustomer(totpPin, customer); return customer; } @@ -90,11 +106,10 @@ export default class CustomersService extends BaseService { * @description : Set the password of a customer when it's the first time they connect * 1: Check if the customer exists * 2: Check if the password is already set - * 3: Check if the SMS code is existing and is not expired + * 3: Check if a totp code is existing and is not expired in the array * 4: Check if the SMS code is valid * 5: Hash the password - * 6: Set the password in database - * 7: Returns the customer + * 6: Set the password in database and return the result of the update * @param email * @param totpCode * @param password @@ -108,28 +123,28 @@ export default class CustomersService extends BaseService { // 2: Check if the password is already set if (customer.password) throw new PasswordAlreadySetError(); - // 3: Check if the SMS code is existing and is not expired - if (!customer.totpCode || !customer.totpCodeExpire || new Date().getTime() > customer.totpCodeExpire.getTime()) - throw new TotpCodeExpiredError(); + const customerHydrated = Customer.hydrate(customer); + // 3: Check if a totp code is existing and is not expired in the array + const validTotpCode = customerHydrated.totpCodes?.find((totpCode) => { + return totpCode.expire_at && new Date().getTime() < totpCode.expire_at.getTime(); + }); + if (!validTotpCode) throw new TotpCodeExpiredError(); // 4: Check if the SMS code is valid - if (customer.totpCode !== totpCode) throw new InvalidTotpCodeError(); + if (validTotpCode.code !== totpCode) throw new InvalidTotpCodeError(); // 5: Hash the password const hashedPassword = await this.authService.hashPassword(password); - // 6: Set the password in database - await this.setPassword(customer, hashedPassword); - - // 7: Returns the customer - return customer; + // 6: Set the password in database and return the result of the update + return await this.setPassword(customer, hashedPassword); } /** * * @description : Login a customer * 1: Check if the customer exists - * 2: Check if the SMS code is existing and is not expired + * 2: Check if a totp code is existing and is not expired in the array * 3: Check if the SMS code is valid * 4: Check if the user has a password or it's their first login * 5: Check if the password is valid @@ -143,13 +158,16 @@ export default class CustomersService extends BaseService { // 1: Check if the customer exists const customer = await this.getByEmail(email); if (!customer) return null; + const customerHydrated = Customer.hydrate(customer); - // 2: Check if the SMS code is existing and is not expired - if (!customer.totpCode || !customer.totpCodeExpire || new Date().getTime() > customer.totpCodeExpire.getTime()) - throw new TotpCodeExpiredError(); + // 2: Check if a totp code is existing and is not expired in the array + const validTotpCode = customerHydrated.totpCodes?.find((totpCode) => { + return totpCode.expire_at && new Date().getTime() < totpCode.expire_at.getTime(); + }); + if (!validTotpCode) throw new TotpCodeExpiredError(); // 3: Check if the SMS code is valid - if (customer.totpCode !== totpCode) throw new InvalidTotpCodeError(); + if (validTotpCode.code !== totpCode) throw new InvalidTotpCodeError(); // 4: Check if the user has a password or it's their first login if (!customer.password) throw new NotRegisteredCustomerError(); @@ -158,18 +176,12 @@ export default class CustomersService extends BaseService { const isPasswordValid = await this.authService.comparePassword(password, customer.password); if (!isPasswordValid) throw new InvalidPasswordError(); - await this.customerRepository.update( + return await this.customerRepository.update( customer.uid as string, Customer.hydrate({ ...customer, }), - { - totpCode: null, - totpCodeExpire: null, - }, ); - // 6: Return the customer - return customer; } /** @@ -183,8 +195,6 @@ export default class CustomersService extends BaseService { ...customer, }), { - totpCode: null, - totpCodeExpire: null, password, }, ); @@ -199,6 +209,7 @@ export default class CustomersService extends BaseService { }, include: { contact: true, + totpCodes: true, }, }); } @@ -206,16 +217,24 @@ export default class CustomersService extends BaseService { /** * @description : Saves a TotpPin in database */ - private async saveTotpPin(customer: Customer, totpPin: number, expireAt: Date) { + private async saveTotpPin(customer: Customer, totpPin: number, expireAt: Date, reason: TotpCodesReasons) { + // Create the totpCode in table using repository + await this.totpCodesRepository.create( + TotpCodes.hydrate({ + reason, + customer_uid: customer.uid, + customer: Customer.hydrate(customer), + }), + { + code: totpPin.toString(), + expire_at: expireAt, + }, + ); return await this.customerRepository.update( customer.uid as string, Customer.hydrate({ ...customer, }), - { - totpCode: totpPin.toString(), - totpCodeExpire: expireAt, - }, ); } @@ -227,24 +246,33 @@ export default class CustomersService extends BaseService { console.log(totpPin); } - public async verifyTotpCode(totpCode: string, email: string): Promise { - // 1: Check if the customer exists - // 2: Check if the SMS code is existing and is not expired - // 3: Check if the SMS code is valid - // 4: Return the customer - + /** + * + * 1: Check if the customer exists + * 2: Check if a totp code is existing and is not expired in the array + * 3: Check if the totp code is valid + * 4: Return the customer + * @param totpCode + * @param email + * @returns + */ + public async verifyTotpCode(totpCode: string, email: string): Promise { // 1: Check if the customer exists const customer = await this.getByEmail(email); if (!customer) return null; - // 2: Check if the SMS code is existing and is not expired - if (!customer.totpCode || !customer.totpCodeExpire || new Date().getTime() > customer.totpCodeExpire.getTime()) - throw new TotpCodeExpiredError(); + const customerHydrated = Customer.hydrate(customer); + + // 2: Check if a totp code is existing and is not expired in the array + const validTotpCode = customerHydrated.totpCodes?.find((totpCode) => { + return totpCode.expire_at && new Date().getTime() < totpCode.expire_at.getTime(); + }); + if (!validTotpCode) throw new TotpCodeExpiredError(); // 3: Check if the SMS code is valid - if (customer.totpCode !== totpCode) throw new InvalidTotpCodeError(); + if (validTotpCode.code !== totpCode) throw new InvalidTotpCodeError(); // 4: Return the customer - return customer; + return validTotpCode; } } From 95ced1f33e171d51b17c8a5517c20d159d1cf102 Mon Sep 17 00:00:00 2001 From: Vins Date: Tue, 28 Nov 2023 00:56:33 +0100 Subject: [PATCH 16/46] Building the switch --- src/common/config/variables/Variables.ts | 4 ++++ .../CustomersService/CustomersService.ts | 24 ++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/common/config/variables/Variables.ts b/src/common/config/variables/Variables.ts index 5a0d22dd..f23db83b 100644 --- a/src/common/config/variables/Variables.ts +++ b/src/common/config/variables/Variables.ts @@ -109,6 +109,9 @@ export class BackendVariables { @IsNotEmpty() public readonly DOCAPOST_APP_PASSWORD!: string; + @IsNotEmpty() + public readonly SMS_PROVIDER!: string; + public constructor() { dotenv.config(); this.DATABASE_PORT = process.env["DATABASE_PORT"]!; @@ -146,6 +149,7 @@ export class BackendVariables { this.BACK_API_HOST = process.env["BACK_API_HOST"]!; this.DOCAPOST_APP_ID = process.env["DOCAPOST_APP_ID"]!; this.DOCAPOST_APP_PASSWORD = process.env["DOCAPOST_APP_PASSWORD"]!; + this.SMS_PROVIDER = process.env["SMS_PROVIDER"]!; } public async validate(groups?: string[]) { diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index 496967e1..9c9a5e34 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -1,3 +1,4 @@ +import { BackendVariables } from "@Common/config/variables/Variables"; import { Customers, Prisma } from "@prisma/client"; import CustomersRepository from "@Repositories/CustomersRepository"; import TotpCodesRepository from "@Repositories/TotpCodesRepository"; @@ -48,6 +49,7 @@ export default class CustomersService extends BaseService { private customerRepository: CustomersRepository, private authService: AuthService, private totpCodesRepository: TotpCodesRepository, + private variables: BackendVariables, ) { super(); } @@ -243,7 +245,27 @@ export default class CustomersService extends BaseService { } private async sendSmsCodeToCustomer(totpPin: number, customer: Customer) { - console.log(totpPin); + try { + const selectedProvider = this.variables.SMS_PROVIDER === 'OVH' ? this.smsService1 : this.smsService2; + + let success = await selectedProvider.sendSms(customer.contact?.phone_number, totpPin); + + // Si l'envoi échoue, basculez automatiquement sur le second fournisseur + if (!success) { + const alternateProvider = this.variables.SMS_PROVIDER === 'OVH' ? this.smsService2 : this.smsService1; + success = await alternateProvider.sendSms(customer.contact?.phone_number, totpPin); + } + + return success; + } catch (error) { + console.error(`Erreur lors de l'envoi du SMS : ${error}`); + return false; + } + + + + + } /** From f00eda1714e4be5996126471c61777fe71ff8c83 Mon Sep 17 00:00:00 2001 From: Vins Date: Wed, 29 Nov 2023 09:33:52 +0100 Subject: [PATCH 17/46] fix --- .../CustomersService/CustomersService.ts | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index 9c9a5e34..52d619ec 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -246,26 +246,24 @@ export default class CustomersService extends BaseService { private async sendSmsCodeToCustomer(totpPin: number, customer: Customer) { try { + // Sélectionnez le fournisseur de SMS en fonction de la variable d'environnement const selectedProvider = this.variables.SMS_PROVIDER === 'OVH' ? this.smsService1 : this.smsService2; - let success = await selectedProvider.sendSms(customer.contact?.phone_number, totpPin); + // Envoi du SMS + let success = await selectedProvider.sendSms(customer.contact?.phone_number, totpPin); - // Si l'envoi échoue, basculez automatiquement sur le second fournisseur - if (!success) { - const alternateProvider = this.variables.SMS_PROVIDER === 'OVH' ? this.smsService2 : this.smsService1; - success = await alternateProvider.sendSms(customer.contact?.phone_number, totpPin); - } + // Si l'envoi échoue, basculez automatiquement sur le second fournisseur + if (!success) { + const alternateProvider = this.variables.SMS_PROVIDER === 'OVH' ? this.smsService2 : this.smsService1; + success = await alternateProvider.sendSms(customer.contact?.phone_number, totpPin); + } + + return success; - return success; } catch (error) { console.error(`Erreur lors de l'envoi du SMS : ${error}`); - return false; + return false; } - - - - - } /** From bbf480fbda896cdfb53d9a9284e2953ab20f4980 Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Wed, 29 Nov 2023 14:38:15 +0100 Subject: [PATCH 18/46] :sparkles: Disable the totp code when used --- package.json | 2 +- .../repositories/TotpCodesRepository.ts | 16 +++++++++++++++- .../CustomersService/CustomersService.ts | 19 ++++++++++++++----- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 645340ef..7c3fdd93 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "file-type-checker": "^1.0.8", "fp-ts": "^2.16.1", "jsonwebtoken": "^9.0.0", - "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.98", + "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.99", "module-alias": "^2.2.2", "monocle-ts": "^2.3.13", "multer": "^1.4.5-lts.1", diff --git a/src/common/repositories/TotpCodesRepository.ts b/src/common/repositories/TotpCodesRepository.ts index f70f55ce..6e84eba0 100644 --- a/src/common/repositories/TotpCodesRepository.ts +++ b/src/common/repositories/TotpCodesRepository.ts @@ -40,7 +40,7 @@ export default class TotpCodesRepository extends BaseRepository { } /** - * @description : Create a customer + * @description : Create a totp code */ public async create(totpCode: TotpCode, excludedVars: IExcludedTotpCodesVars): Promise { const createArgs: Prisma.TotpCodesCreateArgs = { @@ -58,4 +58,18 @@ export default class TotpCodesRepository extends BaseRepository { return this.model.create({ ...createArgs }); } + + /** + * Disable a totp code + */ + public async disable(totpCode: TotpCode): Promise { + return this.model.update({ + where: { + uid: totpCode.uid!, + }, + data: { + expire_at: new Date(), + }, + }); + } } diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index 496967e1..062687f6 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -108,8 +108,9 @@ export default class CustomersService extends BaseService { * 2: Check if the password is already set * 3: Check if a totp code is existing and is not expired in the array * 4: Check if the SMS code is valid - * 5: Hash the password - * 6: Set the password in database and return the result of the update + * 5: Disable the totp code used + * 6: Hash the password + * 7: Set the password in database and return the result of the update * @param email * @param totpCode * @param password @@ -133,10 +134,13 @@ export default class CustomersService extends BaseService { // 4: Check if the SMS code is valid if (validTotpCode.code !== totpCode) throw new InvalidTotpCodeError(); - // 5: Hash the password + // 5: Disable the totp code used + await this.totpCodesRepository.disable(validTotpCode); + + // 6: Hash the password const hashedPassword = await this.authService.hashPassword(password); - // 6: Set the password in database and return the result of the update + // 7: Set the password in database and return the result of the update return await this.setPassword(customer, hashedPassword); } @@ -148,7 +152,8 @@ export default class CustomersService extends BaseService { * 3: Check if the SMS code is valid * 4: Check if the user has a password or it's their first login * 5: Check if the password is valid - * 6: Return the customer + * 6: Disable the totp code used + * 7: Return the customer * @param email * @param totpCode * @param password @@ -176,6 +181,10 @@ export default class CustomersService extends BaseService { const isPasswordValid = await this.authService.comparePassword(password, customer.password); if (!isPasswordValid) throw new InvalidPasswordError(); + // 6: Disable the totp code used + await this.totpCodesRepository.disable(validTotpCode); + + // 7: Return the customer return await this.customerRepository.update( customer.uid as string, Customer.hydrate({ From 82d24a71a447b92b5169be0f3123b420ad67d30c Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Wed, 29 Nov 2023 14:44:41 +0100 Subject: [PATCH 19/46] :sparkles: Verify codes reasons and add a route for password forgotten --- src/app/api/customer/AuthController.ts | 25 ++++++++ .../CustomersService/CustomersService.ts | 58 ++++++++++++++++--- 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/src/app/api/customer/AuthController.ts b/src/app/api/customer/AuthController.ts index f125cd2f..218748d7 100644 --- a/src/app/api/customer/AuthController.ts +++ b/src/app/api/customer/AuthController.ts @@ -45,6 +45,31 @@ export default class AuthController extends ApiController { } } + @Post("/api/v1/customer/auth/ask-new-password") + protected async askNewPassword(req: Request, response: Response) { + const email = req.body["email"]; + if (!email) { + this.httpBadRequest(response, "Email is required"); + return; + } + + try { + const customer = await this.customerService.generateCodeForNewPassword(email); + if (!customer) { + this.httpNotFoundRequest(response, "Customer not found"); + return; + } + this.httpSuccess(response, { partialPhoneNumber: customer.contact?.cell_phone_number.replace(/\s/g, "").slice(-4) }); + } catch (error) { + if (error instanceof SmsNotExpiredError) { + this.httpTooEarlyRequest(response, error.message); + return; + } + console.log(error); + this.httpInternalError(response); + } + } + @Post("/api/v1/customer/auth/login") protected async login(req: Request, response: Response) { const email = req.body["email"]; diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index 062687f6..65bd52f8 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -102,12 +102,45 @@ export default class CustomersService extends BaseService { return customer; } + /** + * @description : Send SMS to verify the email of a customer (2FA) + * 1: Check if the customer exists + * 2: Check in the array of totpCodes if one is still valid + * 3: Generate a new SMS code + * 4: Save the SMS code in database + * 5: Send the SMS code to the customer + */ + public async generateCodeForNewPassword(email: string): Promise { + // 1: Check if the customer exists + const customer = await this.getByEmail(email); + if (!customer) return null; + const now = new Date().getTime(); + + const customerHydrated = Customer.hydrate(customer); + + // 2: Check in the array of totpCodes if one is still valid + const validTotpCode = customerHydrated.totpCodes?.find((totpCode) => { + return totpCode.expire_at && totpCode.expire_at.getTime() > now; + }); + if (validTotpCode) throw new SmsNotExpiredError(); + + // 3: Generate a new SMS code + const totpPin = this.generateTotp(); + + // 4: Save the SMS code in database + await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60000), TotpCodesReasons.RESET_PASSWORD); + + // 5: Send the SMS code to the customer + await this.sendSmsCodeToCustomer(totpPin, customer); + return customer; + } + /** * @description : Set the password of a customer when it's the first time they connect * 1: Check if the customer exists - * 2: Check if the password is already set - * 3: Check if a totp code is existing and is not expired in the array - * 4: Check if the SMS code is valid + * 2: Check if a totp code is existing and is not expired in the array + * 3: Check if the SMS code is valid + * 4: Check the totpcode reason is valid * 5: Disable the totp code used * 6: Hash the password * 7: Set the password in database and return the result of the update @@ -121,19 +154,26 @@ export default class CustomersService extends BaseService { const customer = await this.getByEmail(email); if (!customer) return null; - // 2: Check if the password is already set - if (customer.password) throw new PasswordAlreadySetError(); - const customerHydrated = Customer.hydrate(customer); - // 3: Check if a totp code is existing and is not expired in the array + // 2: Check if a totp code is existing and is not expired in the array const validTotpCode = customerHydrated.totpCodes?.find((totpCode) => { return totpCode.expire_at && new Date().getTime() < totpCode.expire_at.getTime(); }); + if (!validTotpCode) throw new TotpCodeExpiredError(); - // 4: Check if the SMS code is valid + // 3: Check if the SMS code is valid if (validTotpCode.code !== totpCode) throw new InvalidTotpCodeError(); + // 4: Check the totpcode reason is valid + // If the customer already has a password, the reason must be RESET_PASSWORD + // If the customer doesn't have a password, the reason must be FIRST_LOGIN + if ( + (customer.password && validTotpCode.reason !== TotpCodesReasons.RESET_PASSWORD) || + (!customer.password && validTotpCode.reason !== TotpCodesReasons.FIRST_LOGIN) + ) + throw new InvalidTotpCodeError(); + // 5: Disable the totp code used await this.totpCodesRepository.disable(validTotpCode); @@ -167,7 +207,7 @@ export default class CustomersService extends BaseService { // 2: Check if a totp code is existing and is not expired in the array const validTotpCode = customerHydrated.totpCodes?.find((totpCode) => { - return totpCode.expire_at && new Date().getTime() < totpCode.expire_at.getTime(); + return totpCode.expire_at && new Date().getTime() < totpCode.expire_at.getTime() && totpCode.reason === TotpCodesReasons.LOGIN; }); if (!validTotpCode) throw new TotpCodeExpiredError(); From 8ef401b4b20e5d3fb87d5182f57cf411a8535415 Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Wed, 29 Nov 2023 16:04:58 +0100 Subject: [PATCH 20/46] :sparkles: Password forgotten working --- .../customer/CustomersService/CustomersService.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index 65bd52f8..1f7b144c 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -120,10 +120,23 @@ export default class CustomersService extends BaseService { // 2: Check in the array of totpCodes if one is still valid const validTotpCode = customerHydrated.totpCodes?.find((totpCode) => { - return totpCode.expire_at && totpCode.expire_at.getTime() > now; + return totpCode.expire_at && totpCode.expire_at.getTime() > now && totpCode.reason === TotpCodesReasons.RESET_PASSWORD; }); if (validTotpCode) throw new SmsNotExpiredError(); + // 3: Archive all active totp codes for this customer + const activeTotpCodes = customerHydrated.totpCodes?.filter((totpCode) => { + return totpCode.expire_at && totpCode.expire_at.getTime() > now; + }); + + if (activeTotpCodes) { + await Promise.all( + activeTotpCodes.map(async (totpCode) => { + await this.totpCodesRepository.disable(totpCode); + }), + ); + } + // 3: Generate a new SMS code const totpPin = this.generateTotp(); From 9dc1049ce717a06cda7804e53018396f2e1aa3f0 Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Wed, 29 Nov 2023 16:47:50 +0100 Subject: [PATCH 21/46] :sparkles: Send another code working --- src/app/api/customer/AuthController.ts | 25 +++++++++++++ .../20231127154201_totp_table/migration.sql | 3 +- src/common/databases/schema.prisma | 13 +++---- .../repositories/TotpCodesRepository.ts | 11 ++---- .../CustomersService/CustomersService.ts | 36 +++++++++++++++++-- 5 files changed, 70 insertions(+), 18 deletions(-) diff --git a/src/app/api/customer/AuthController.ts b/src/app/api/customer/AuthController.ts index 218748d7..7b2b1dd0 100644 --- a/src/app/api/customer/AuthController.ts +++ b/src/app/api/customer/AuthController.ts @@ -202,4 +202,29 @@ export default class AuthController extends ApiController { this.httpInternalError(response); } } + + @Post("/api/v1/customer/auth/send-another-code") + protected async sendAnotherCode(req: Request, response: Response) { + const email = req.body["email"]; + if (!email) { + this.httpBadRequest(response, "email is required"); + return; + } + + try { + const customer = await this.customerService.askAnotherCode(email); + if (!customer) { + this.httpNotFoundRequest(response, "Customer not found"); + return; + } + this.httpSuccess(response, { partialPhoneNumber: customer.contact?.cell_phone_number.replace(/\s/g, "").slice(-4) }); + } catch (error) { + if (error instanceof InvalidTotpCodeError || error instanceof TotpCodeExpiredError) { + this.httpUnauthorized(response, error.message); + return; + } + console.log(error); + this.httpInternalError(response); + } + } } diff --git a/src/common/databases/migrations/20231127154201_totp_table/migration.sql b/src/common/databases/migrations/20231127154201_totp_table/migration.sql index 89a9d24a..d511b72a 100644 --- a/src/common/databases/migrations/20231127154201_totp_table/migration.sql +++ b/src/common/databases/migrations/20231127154201_totp_table/migration.sql @@ -19,7 +19,8 @@ CREATE TABLE "totp_codes" ( "code" VARCHAR(255) NOT NULL, "reason" "TotpCodesReasons" NOT NULL DEFAULT 'LOGIN', "expire_at" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, - + "created_at" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3), CONSTRAINT "totp_codes_pkey" PRIMARY KEY ("uid") ); diff --git a/src/common/databases/schema.prisma b/src/common/databases/schema.prisma index 3c9c0831..be5c663b 100644 --- a/src/common/databases/schema.prisma +++ b/src/common/databases/schema.prisma @@ -343,13 +343,14 @@ model Votes { } model TotpCodes { - uid String @id @unique @default(uuid()) - customer Customers @relation(fields: [customer_uid], references: [uid], onDelete: Cascade) - customer_uid String @db.VarChar(255) - code String @db.VarChar(255) + uid String @id @unique @default(uuid()) + customer Customers @relation(fields: [customer_uid], references: [uid], onDelete: Cascade) + customer_uid String @db.VarChar(255) + code String @db.VarChar(255) reason TotpCodesReasons @default(LOGIN) - expire_at DateTime? @default(now()) - + expire_at DateTime? @default(now()) + created_at DateTime? @default(now()) + updated_at DateTime? @updatedAt @@map("totp_codes") } diff --git a/src/common/repositories/TotpCodesRepository.ts b/src/common/repositories/TotpCodesRepository.ts index 6e84eba0..8d9d9c33 100644 --- a/src/common/repositories/TotpCodesRepository.ts +++ b/src/common/repositories/TotpCodesRepository.ts @@ -4,11 +4,6 @@ import { Service } from "typedi"; import { Prisma, TotpCodes } from "@prisma/client"; import { TotpCodes as TotpCode } from "le-coffre-resources/dist/Customer"; -type IExcludedTotpCodesVars = { - code?: string; - expire_at?: Date; -}; - @Service() export default class TotpCodesRepository extends BaseRepository { constructor(private database: Database) { @@ -42,17 +37,17 @@ export default class TotpCodesRepository extends BaseRepository { /** * @description : Create a totp code */ - public async create(totpCode: TotpCode, excludedVars: IExcludedTotpCodesVars): Promise { + public async create(totpCode: TotpCode): Promise { const createArgs: Prisma.TotpCodesCreateArgs = { data: { - code: excludedVars.code!, + code: totpCode.code!, reason: totpCode.reason!, customer: { connect: { uid: totpCode.customer_uid!, }, }, - expire_at: excludedVars.expire_at!, + expire_at: totpCode.expire_at!, }, }; diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index 1f7b144c..96aeb5ae 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -246,6 +246,37 @@ export default class CustomersService extends BaseService { ); } + public async askAnotherCode(email: string): Promise { + // 1: Check if the customer exists + const customer = await this.getByEmail(email); + if (!customer) return null; + const now = new Date().getTime(); + + const customerHydrated = Customer.hydrate(customer); + + // 2: Get last code sent + const lastCode = customerHydrated.totpCodes?.find((totpCode) => { + return totpCode.expire_at && totpCode.expire_at.getTime() > now; + }); + if (!lastCode) throw new SmsNotExpiredError(); + + // 3: Check if it was created more than 30 seconds ago + if (lastCode.created_at && lastCode.created_at.getTime() > now - 30000) throw new SmsNotExpiredError(); + + // 4: Generate a new SMS code + const totpPin = this.generateTotp(); + + // 5: Disable the old code + await this.totpCodesRepository.disable(lastCode); + + // 6: Save the SMS code in database + await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60000), lastCode.reason!); + + // 7: Send the SMS code to the customer + await this.sendSmsCodeToCustomer(totpPin, customer); + return customer; + } + /** * @description : Set password for a customer * @throws {Error} If customer cannot be updated @@ -286,11 +317,10 @@ export default class CustomersService extends BaseService { reason, customer_uid: customer.uid, customer: Customer.hydrate(customer), - }), - { + created_at: new Date(), code: totpPin.toString(), expire_at: expireAt, - }, + }), ); return await this.customerRepository.update( customer.uid as string, From b931e98c29e78075d579477e642461f478a17915 Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Wed, 29 Nov 2023 16:52:24 +0100 Subject: [PATCH 22/46] :sparkles: Send the right error when asking for a new code --- src/app/api/customer/AuthController.ts | 3 ++- .../customer/CustomersService/CustomersService.ts | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/app/api/customer/AuthController.ts b/src/app/api/customer/AuthController.ts index 7b2b1dd0..631dce4c 100644 --- a/src/app/api/customer/AuthController.ts +++ b/src/app/api/customer/AuthController.ts @@ -8,6 +8,7 @@ import CustomersService, { NotRegisteredCustomerError, PasswordAlreadySetError, SmsNotExpiredError, + TooSoonForNewCode, TotpCodeExpiredError, } from "@Services/customer/CustomersService/CustomersService"; import AuthService from "@Services/common/AuthService/AuthService"; @@ -219,7 +220,7 @@ export default class AuthController extends ApiController { } this.httpSuccess(response, { partialPhoneNumber: customer.contact?.cell_phone_number.replace(/\s/g, "").slice(-4) }); } catch (error) { - if (error instanceof InvalidTotpCodeError || error instanceof TotpCodeExpiredError) { + if (error instanceof TooSoonForNewCode || error instanceof TotpCodeExpiredError) { this.httpUnauthorized(response, error.message); return; } diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index 96aeb5ae..e4dc47a9 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -42,6 +42,12 @@ export class PasswordAlreadySetError extends Error { super("Password already set"); } } + +export class TooSoonForNewCode extends Error { + constructor() { + super("You need to wait at least 30 seconds before asking for a new code"); + } +} @Service() export default class CustomersService extends BaseService { constructor( @@ -258,10 +264,10 @@ export default class CustomersService extends BaseService { const lastCode = customerHydrated.totpCodes?.find((totpCode) => { return totpCode.expire_at && totpCode.expire_at.getTime() > now; }); - if (!lastCode) throw new SmsNotExpiredError(); + if (!lastCode) throw new TotpCodeExpiredError(); // 3: Check if it was created more than 30 seconds ago - if (lastCode.created_at && lastCode.created_at.getTime() > now - 30000) throw new SmsNotExpiredError(); + if (lastCode.created_at && lastCode.created_at.getTime() > now - 30000) throw new TooSoonForNewCode(); // 4: Generate a new SMS code const totpPin = this.generateTotp(); From bb8b49edbe1a89019919dfc67328ca3471b2fe80 Mon Sep 17 00:00:00 2001 From: Vins Date: Thu, 30 Nov 2023 10:14:49 +0100 Subject: [PATCH 23/46] Ovh --- package.json | 1 + src/common/config/variables/Variables.ts | 13 ++++++ src/services/common/OvhService/OvhService.ts | 43 +++++++++++++++++++ .../CustomersService/CustomersService.ts | 12 +++--- 4 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 src/services/common/OvhService/OvhService.ts diff --git a/package.json b/package.json index 645340ef..6cd4aad7 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "next": "^13.1.5", "node-cache": "^5.1.2", "node-schedule": "^2.1.1", + "ovh": "^2.0.3", "prisma-query": "^2.0.0", "puppeteer": "^21.3.4", "reflect-metadata": "^0.1.13", diff --git a/src/common/config/variables/Variables.ts b/src/common/config/variables/Variables.ts index f23db83b..5f4638f1 100644 --- a/src/common/config/variables/Variables.ts +++ b/src/common/config/variables/Variables.ts @@ -112,6 +112,15 @@ export class BackendVariables { @IsNotEmpty() public readonly SMS_PROVIDER!: string; + @IsNotEmpty() + public readonly OVH_APP_KEY!: string; + + @IsNotEmpty() + public readonly OVH_APP_SECRET!: string; + + @IsNotEmpty() + public readonly OVH_CONSUMER_KEY!: string; + public constructor() { dotenv.config(); this.DATABASE_PORT = process.env["DATABASE_PORT"]!; @@ -150,6 +159,10 @@ export class BackendVariables { this.DOCAPOST_APP_ID = process.env["DOCAPOST_APP_ID"]!; this.DOCAPOST_APP_PASSWORD = process.env["DOCAPOST_APP_PASSWORD"]!; this.SMS_PROVIDER = process.env["SMS_PROVIDER"]!; + this.OVH_APP_KEY = process.env["OVH_APP_KEY"]!; + this.OVH_APP_SECRET = process.env["OVH_APP_SECRET"]!; + this.OVH_CONSUMER_KEY = process.env["OVH_CONSUMER_KEY"]!; + } public async validate(groups?: string[]) { diff --git a/src/services/common/OvhService/OvhService.ts b/src/services/common/OvhService/OvhService.ts new file mode 100644 index 00000000..b05b8e03 --- /dev/null +++ b/src/services/common/OvhService/OvhService.ts @@ -0,0 +1,43 @@ +import { BackendVariables } from "@Common/config/variables/Variables"; +import BaseService from "@Services/BaseService"; +import { Service } from "typedi"; + +Service() +export default class OvhService extends BaseService { + + constructor(private variables: BackendVariables) { + super(); + } + + /** + * @description : Get all Customers + * @throws {Error} If Customers cannot be get + */ + public async sendSms(phoneNumber: string, message: string): Promise { + + const ovh = require('ovh')({ + appKey: this.variables.OVH_APP_KEY, + appSecret: this.variables.OVH_APP_SECRET, + consumerKey: this.variables.OVH_CONSUMER_KEY, + }); + + // Get the serviceName (name of your SMS account) + ovh.request('GET', '/sms', function (err: any, serviceName: string) { + if(err) { + console.log(err, serviceName); + } + else { + console.log("My account SMS is " + serviceName); + + // Send a simple SMS with a short number using your serviceName + ovh.request('POST', '/sms/' + serviceName + '/jobs', { + message: message, + senderForResponse: true, + receivers: [phoneNumber] + }, function (errsend: any, result: any) { + console.log(errsend, result); + }); + } + }); + } +} \ No newline at end of file diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index 52d619ec..e4cbe073 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -7,6 +7,7 @@ import AuthService from "@Services/common/AuthService/AuthService"; import TotpCodes, { TotpCodesReasons } from "le-coffre-resources/dist/Customer/TotpCodes"; import { Customer } from "le-coffre-resources/dist/Notary"; import { Service } from "typedi"; +import OvhService from "@Services/common/OvhService/OvhService"; export class SmsNotExpiredError extends Error { constructor() { @@ -50,6 +51,7 @@ export default class CustomersService extends BaseService { private authService: AuthService, private totpCodesRepository: TotpCodesRepository, private variables: BackendVariables, + private ovhService: OvhService, ) { super(); } @@ -247,16 +249,16 @@ export default class CustomersService extends BaseService { private async sendSmsCodeToCustomer(totpPin: number, customer: Customer) { try { // Sélectionnez le fournisseur de SMS en fonction de la variable d'environnement - const selectedProvider = this.variables.SMS_PROVIDER === 'OVH' ? this.smsService1 : this.smsService2; + const selectedProvider = this.variables.SMS_PROVIDER === 'OVH' ? this.ovhService : null; // Envoi du SMS let success = await selectedProvider.sendSms(customer.contact?.phone_number, totpPin); // Si l'envoi échoue, basculez automatiquement sur le second fournisseur - if (!success) { - const alternateProvider = this.variables.SMS_PROVIDER === 'OVH' ? this.smsService2 : this.smsService1; - success = await alternateProvider.sendSms(customer.contact?.phone_number, totpPin); - } + // if (!success) { + // const alternateProvider = this.variables.SMS_PROVIDER === 'OVH' ? this.smsService2 : this.ovhService; + // success = await alternateProvider.sendSms(customer.contact?.phone_number, totpPin); + // } return success; From 13162a34fcab32eb8ac1803dd7d54d9def68c9fe Mon Sep 17 00:00:00 2001 From: Vins Date: Thu, 30 Nov 2023 10:34:21 +0100 Subject: [PATCH 24/46] ovh ended --- src/services/customer/CustomersService/CustomersService.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index 4aab0783..5e0d0b47 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -347,10 +347,11 @@ export default class CustomersService extends BaseService { private async sendSmsCodeToCustomer(totpPin: number, customer: Customer) { try { // Sélectionnez le fournisseur de SMS en fonction de la variable d'environnement - const selectedProvider = this.variables.SMS_PROVIDER === 'OVH' ? this.ovhService : null; + const selectedProvider = this.variables.SMS_PROVIDER === 'OVH' ? this.ovhService : this.ovhService; // Envoi du SMS - let success = await selectedProvider.sendSms(customer.contact?.phone_number, totpPin); + if(!customer.contact?.phone_number) return false; + let success = await selectedProvider.sendSms(customer.contact?.phone_number, totpPin.toString()); // Si l'envoi échoue, basculez automatiquement sur le second fournisseur // if (!success) { From aa85dab2879126219523e792b98ee571aa4b8535 Mon Sep 17 00:00:00 2001 From: Vins Date: Thu, 30 Nov 2023 11:07:40 +0100 Subject: [PATCH 25/46] @Service on OvhService --- src/services/common/OvhService/OvhService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/common/OvhService/OvhService.ts b/src/services/common/OvhService/OvhService.ts index b05b8e03..7e70d413 100644 --- a/src/services/common/OvhService/OvhService.ts +++ b/src/services/common/OvhService/OvhService.ts @@ -2,7 +2,7 @@ import { BackendVariables } from "@Common/config/variables/Variables"; import BaseService from "@Services/BaseService"; import { Service } from "typedi"; -Service() +@Service() export default class OvhService extends BaseService { constructor(private variables: BackendVariables) { From c0cfb8aec7e55ce1ca75839895f9fe9323a8a462 Mon Sep 17 00:00:00 2001 From: Vins Date: Thu, 30 Nov 2023 11:40:04 +0100 Subject: [PATCH 26/46] Sms factor --- package.json | 1 + src/common/config/variables/Variables.ts | 4 ++ src/services/common/OvhService/OvhService.ts | 66 +++++++++---------- .../SmsFactorService/SmsFactorService.ts | 29 ++++++++ .../CustomersService/CustomersService.ts | 28 ++++---- 5 files changed, 78 insertions(+), 50 deletions(-) create mode 100644 src/services/common/SmsFactorService/SmsFactorService.ts diff --git a/package.json b/package.json index 9689e3ff..33f4337d 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@pinata/sdk": "^2.1.0", "@prisma/client": "^4.11.0", "adm-zip": "^0.5.10", + "axios": "^1.6.2", "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", diff --git a/src/common/config/variables/Variables.ts b/src/common/config/variables/Variables.ts index 5f4638f1..5a46fd26 100644 --- a/src/common/config/variables/Variables.ts +++ b/src/common/config/variables/Variables.ts @@ -121,6 +121,9 @@ export class BackendVariables { @IsNotEmpty() public readonly OVH_CONSUMER_KEY!: string; + @IsNotEmpty() + public readonly SMS_FACTOR_TOKEN!: string; + public constructor() { dotenv.config(); this.DATABASE_PORT = process.env["DATABASE_PORT"]!; @@ -162,6 +165,7 @@ export class BackendVariables { this.OVH_APP_KEY = process.env["OVH_APP_KEY"]!; this.OVH_APP_SECRET = process.env["OVH_APP_SECRET"]!; this.OVH_CONSUMER_KEY = process.env["OVH_CONSUMER_KEY"]!; + this.SMS_FACTOR_TOKEN = process.env["SMS_FACTOR_TOKEN"]!; } diff --git a/src/services/common/OvhService/OvhService.ts b/src/services/common/OvhService/OvhService.ts index 7e70d413..f70b59d2 100644 --- a/src/services/common/OvhService/OvhService.ts +++ b/src/services/common/OvhService/OvhService.ts @@ -4,40 +4,40 @@ import { Service } from "typedi"; @Service() export default class OvhService extends BaseService { - - constructor(private variables: BackendVariables) { + constructor(private variables: BackendVariables) { super(); } - /** - * @description : Get all Customers - * @throws {Error} If Customers cannot be get - */ - public async sendSms(phoneNumber: string, message: string): Promise { + public async sendSms(phoneNumber: string, message: string): Promise { + const ovh = require("ovh")({ + appKey: this.variables.OVH_APP_KEY, + appSecret: this.variables.OVH_APP_SECRET, + consumerKey: this.variables.OVH_CONSUMER_KEY, + }); - const ovh = require('ovh')({ - appKey: this.variables.OVH_APP_KEY, - appSecret: this.variables.OVH_APP_SECRET, - consumerKey: this.variables.OVH_CONSUMER_KEY, - }); - - // Get the serviceName (name of your SMS account) - ovh.request('GET', '/sms', function (err: any, serviceName: string) { - if(err) { - console.log(err, serviceName); - } - else { - console.log("My account SMS is " + serviceName); - - // Send a simple SMS with a short number using your serviceName - ovh.request('POST', '/sms/' + serviceName + '/jobs', { - message: message, - senderForResponse: true, - receivers: [phoneNumber] - }, function (errsend: any, result: any) { - console.log(errsend, result); - }); - } - }); - } -} \ No newline at end of file + ovh.request("GET", "/sms", function (err: any, serviceName: string) { + if (err) { + console.log(err, serviceName); + return false; + } else { + console.log("My account SMS is " + serviceName); + + // Send a simple SMS with a short number using your serviceName + ovh.request( + "POST", + "/sms/" + serviceName + "/jobs", + { + message: message, + senderForResponse: true, + receivers: [phoneNumber], + }, + function (errsend: any, result: any) { + console.log(errsend, result); + }, + ); + return true; + } + }); + return false; + } +} diff --git a/src/services/common/SmsFactorService/SmsFactorService.ts b/src/services/common/SmsFactorService/SmsFactorService.ts new file mode 100644 index 00000000..a736b627 --- /dev/null +++ b/src/services/common/SmsFactorService/SmsFactorService.ts @@ -0,0 +1,29 @@ +import { BackendVariables } from "@Common/config/variables/Variables"; +import BaseService from "@Services/BaseService"; +import { Service } from "typedi"; +import axios from "axios"; + +@Service() +export default class SmsFactorService extends BaseService { + + constructor(private variables: BackendVariables) { + super(); + } + + public async sendSms(phoneNumber: string, message: string): Promise { + axios.post('https://api.smsfactor.com/send/', { + token: this.variables.SMS_FACTOR_TOKEN, + sender: "LeCoffre", + to: phoneNumber, + text: message, + }).then(response => { + console.log('SMS sent successfully:', response.data); + return true; + }) + .catch(error => { + console.error('Error sending SMS:', error.response.data); + return false; + }); + return false; + } +} \ No newline at end of file diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index 5e0d0b47..0a81ee35 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -8,6 +8,7 @@ import TotpCodes, { TotpCodesReasons } from "le-coffre-resources/dist/Customer/T import { Customer } from "le-coffre-resources/dist/Notary"; import { Service } from "typedi"; import OvhService from "@Services/common/OvhService/OvhService"; +import SmsFactorService from "@Services/common/SmsFactorService/SmsFactorService"; export class SmsNotExpiredError extends Error { constructor() { @@ -58,6 +59,7 @@ export default class CustomersService extends BaseService { private totpCodesRepository: TotpCodesRepository, private variables: BackendVariables, private ovhService: OvhService, + private smsFactorService: SmsFactorService, ) { super(); } @@ -345,25 +347,17 @@ export default class CustomersService extends BaseService { } private async sendSmsCodeToCustomer(totpPin: number, customer: Customer) { - try { - // Sélectionnez le fournisseur de SMS en fonction de la variable d'environnement - const selectedProvider = this.variables.SMS_PROVIDER === 'OVH' ? this.ovhService : this.ovhService; + // Sélectionnez le fournisseur de SMS en fonction de la variable d'environnement + const selectedProvider = this.variables.SMS_PROVIDER === "OVH" ? this.ovhService : this.smsFactorService; - // Envoi du SMS - if(!customer.contact?.phone_number) return false; - let success = await selectedProvider.sendSms(customer.contact?.phone_number, totpPin.toString()); + // Envoi du SMS + if (!customer.contact?.phone_number) return; + let success = await selectedProvider.sendSms(customer.contact?.phone_number, totpPin.toString()); - // Si l'envoi échoue, basculez automatiquement sur le second fournisseur - // if (!success) { - // const alternateProvider = this.variables.SMS_PROVIDER === 'OVH' ? this.smsService2 : this.ovhService; - // success = await alternateProvider.sendSms(customer.contact?.phone_number, totpPin); - // } - - return success; - - } catch (error) { - console.error(`Erreur lors de l'envoi du SMS : ${error}`); - return false; + // Si l'envoi échoue, basculez automatiquement sur le second fournisseur + if (!success) { + const alternateProvider = this.variables.SMS_PROVIDER === "OVH" ? this.smsFactorService : this.ovhService; + success = await alternateProvider.sendSms(customer.contact?.phone_number, totpPin.toString()); } } From 033e0b5c7693af56200d655917c2b9e4f1564c32 Mon Sep 17 00:00:00 2001 From: Vins Date: Thu, 30 Nov 2023 12:55:14 +0100 Subject: [PATCH 27/46] relaunch migration --- src/services/common/SmsFactorService/SmsFactorService.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/common/SmsFactorService/SmsFactorService.ts b/src/services/common/SmsFactorService/SmsFactorService.ts index a736b627..851152ed 100644 --- a/src/services/common/SmsFactorService/SmsFactorService.ts +++ b/src/services/common/SmsFactorService/SmsFactorService.ts @@ -19,6 +19,7 @@ export default class SmsFactorService extends BaseService { }).then(response => { console.log('SMS sent successfully:', response.data); return true; + }) .catch(error => { console.error('Error sending SMS:', error.response.data); From 6e2d45c8cc97f8366c7571749138c67c8f60cdf6 Mon Sep 17 00:00:00 2001 From: Vins Date: Thu, 30 Nov 2023 14:14:18 +0100 Subject: [PATCH 28/46] auto run migrations --- devops/stg.values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devops/stg.values.yaml b/devops/stg.values.yaml index e2ce7a4b..74a13fa8 100644 --- a/devops/stg.values.yaml +++ b/devops/stg.values.yaml @@ -5,7 +5,7 @@ scwSecretKey: AgChoEnPitXp4Ny/rVMEcevaWKNVpyj2cJYAcq+yFqKwVwnLB+ffDvwqz9XBHu+6d4 lecoffreBack: serviceAccountName: lecoffre-back-sa envSecrets: stg-env - command: "'sh', '-c', 'export $(xargs Date: Fri, 1 Dec 2023 09:49:13 +0100 Subject: [PATCH 29/46] :sparkles: Totpcode resend by uid --- src/app/api/customer/AuthController.ts | 28 ++++++++++++------- .../CustomersService/CustomersService.ts | 24 ++++++++-------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/app/api/customer/AuthController.ts b/src/app/api/customer/AuthController.ts index 631dce4c..f4067dd9 100644 --- a/src/app/api/customer/AuthController.ts +++ b/src/app/api/customer/AuthController.ts @@ -30,17 +30,16 @@ export default class AuthController extends ApiController { } try { - const customer = await this.customerService.verifyEmail2FASms(email); - if (!customer) { + const res = await this.customerService.verifyEmail2FASms(email); + if (!res) { this.httpNotFoundRequest(response, "Customer not found"); return; } - this.httpSuccess(response, { partialPhoneNumber: customer.contact?.cell_phone_number.replace(/\s/g, "").slice(-4) }); + this.httpSuccess(response, { + partialPhoneNumber: res.customer.contact?.cell_phone_number.replace(/\s/g, "").slice(-4), + totpCodeUid: res.totpCode.uid, + }); } catch (error) { - if (error instanceof SmsNotExpiredError) { - this.httpTooEarlyRequest(response, error.message); - return; - } console.log(error); this.httpInternalError(response); } @@ -207,18 +206,27 @@ export default class AuthController extends ApiController { @Post("/api/v1/customer/auth/send-another-code") protected async sendAnotherCode(req: Request, response: Response) { const email = req.body["email"]; + const totpCodeUid = req.body["totpCodeUid"]; if (!email) { this.httpBadRequest(response, "email is required"); return; } + if (!totpCodeUid) { + this.httpBadRequest(response, "totpCodeUid is required"); + return; + } + try { - const customer = await this.customerService.askAnotherCode(email); - if (!customer) { + const res = await this.customerService.askAnotherCode(email, totpCodeUid); + if (!res) { this.httpNotFoundRequest(response, "Customer not found"); return; } - this.httpSuccess(response, { partialPhoneNumber: customer.contact?.cell_phone_number.replace(/\s/g, "").slice(-4) }); + this.httpSuccess(response, { + partialPhoneNumber: res.customer.contact?.cell_phone_number.replace(/\s/g, "").slice(-4), + totpCodeUid: res.totpCode.uid, + }); } catch (error) { if (error instanceof TooSoonForNewCode || error instanceof TotpCodeExpiredError) { this.httpUnauthorized(response, error.message); diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index 0a81ee35..03c087df 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -88,7 +88,7 @@ export default class CustomersService extends BaseService { * 4: Save the SMS code in database * 5: Send the SMS code to the customer */ - public async verifyEmail2FASms(email: string): Promise { + public async verifyEmail2FASms(email: string): Promise<{ customer: Customer; totpCode: TotpCodes } | null> { // 1: Check if the customer exists const customer = await this.getByEmail(email); if (!customer) return null; @@ -100,18 +100,18 @@ export default class CustomersService extends BaseService { const validTotpCode = customerHydrated.totpCodes?.find((totpCode) => { return totpCode.expire_at && totpCode.expire_at.getTime() > now; }); - if (validTotpCode) throw new SmsNotExpiredError(); + if (validTotpCode) return { customer, totpCode: validTotpCode }; // 3: Generate a new SMS code const totpPin = this.generateTotp(); const reason = customer.password ? TotpCodesReasons.LOGIN : TotpCodesReasons.FIRST_LOGIN; // 4: Save the SMS code in database - await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60000), reason); + const totpCode = await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60000), reason); // 5: Send the SMS code to the customer await this.sendSmsCodeToCustomer(totpPin, customer); - return customer; + return { customer, totpCode }; } /** @@ -258,7 +258,7 @@ export default class CustomersService extends BaseService { ); } - public async askAnotherCode(email: string): Promise { + public async askAnotherCode(email: string, totpCodeUid: string): Promise<{ customer: Customer; totpCode: TotpCodes } | null> { // 1: Check if the customer exists const customer = await this.getByEmail(email); if (!customer) return null; @@ -267,26 +267,26 @@ export default class CustomersService extends BaseService { const customerHydrated = Customer.hydrate(customer); // 2: Get last code sent - const lastCode = customerHydrated.totpCodes?.find((totpCode) => { - return totpCode.expire_at && totpCode.expire_at.getTime() > now; + const totpCodeToResend = customerHydrated.totpCodes?.find((totpCode) => { + return totpCode.uid && totpCodeUid; }); - if (!lastCode) throw new TotpCodeExpiredError(); + if (!totpCodeToResend) throw new TotpCodeExpiredError(); // 3: Check if it was created more than 30 seconds ago - if (lastCode.created_at && lastCode.created_at.getTime() > now - 30000) throw new TooSoonForNewCode(); + if (totpCodeToResend.created_at && totpCodeToResend.created_at.getTime() > now - 30000) throw new TooSoonForNewCode(); // 4: Generate a new SMS code const totpPin = this.generateTotp(); // 5: Disable the old code - await this.totpCodesRepository.disable(lastCode); + await this.totpCodesRepository.disable(totpCodeToResend); // 6: Save the SMS code in database - await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60000), lastCode.reason!); + const totpCode = await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60000), totpCodeToResend.reason!); // 7: Send the SMS code to the customer await this.sendSmsCodeToCustomer(totpPin, customer); - return customer; + return { customer, totpCode }; } /** From 5a51fc63d08bbbc4e394a28a372eb729f8c99695 Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Fri, 1 Dec 2023 10:00:53 +0100 Subject: [PATCH 30/46] :bug: Forgot condition on resend code --- src/services/customer/CustomersService/CustomersService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index 03c087df..cf03f0c8 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -268,7 +268,7 @@ export default class CustomersService extends BaseService { // 2: Get last code sent const totpCodeToResend = customerHydrated.totpCodes?.find((totpCode) => { - return totpCode.uid && totpCodeUid; + return totpCode.uid === totpCodeUid && totpCode.expire_at && totpCode.expire_at.getTime() > now; }); if (!totpCodeToResend) throw new TotpCodeExpiredError(); From a3e88cdbd2edceca1f142b3e4c439a93ed6ac15d Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Fri, 1 Dec 2023 11:09:56 +0100 Subject: [PATCH 31/46] :sparkles: Cron that deletes codes expired for more than 30 seconds --- .../repositories/TotpCodesRepository.ts | 7 +++ .../common/CronService/CronService.ts | 23 +++++++-- .../customer/TotpService/TotpService.ts | 51 +++++++++++++++++++ 3 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 src/services/customer/TotpService/TotpService.ts diff --git a/src/common/repositories/TotpCodesRepository.ts b/src/common/repositories/TotpCodesRepository.ts index 8d9d9c33..8d1ebf48 100644 --- a/src/common/repositories/TotpCodesRepository.ts +++ b/src/common/repositories/TotpCodesRepository.ts @@ -67,4 +67,11 @@ export default class TotpCodesRepository extends BaseRepository { }, }); } + + /** + * Delete many totp codes + */ + public async deleteMany(query: Prisma.TotpCodesDeleteManyArgs) { + return this.model.deleteMany({ ...query }); + } } diff --git a/src/services/common/CronService/CronService.ts b/src/services/common/CronService/CronService.ts index 4b46e962..94584f54 100644 --- a/src/services/common/CronService/CronService.ts +++ b/src/services/common/CronService/CronService.ts @@ -6,6 +6,7 @@ import IdNotService from "../IdNotService/IdNotService"; import { PrismaClient } from "@prisma/client"; import NotificationBuilder from "@Common/notifications/NotificationBuilder"; import EmailBuilder from "@Common/emails/EmailBuilder"; +import TotpService from "@Services/customer/TotpService/TotpService"; // import { PrismaClient } from "@prisma/client"; @Service() @@ -16,6 +17,7 @@ export default class CronService { private idNotService: IdNotService, private notificationBuilder: NotificationBuilder, private emailBuilder: EmailBuilder, + private totpService: TotpService, ) {} public async sendMails() { @@ -86,8 +88,8 @@ export default class CronService { // Once a day at midnight try { const prisma = new PrismaClient(); - const expiringDocuments15Days: [{uid: string, expiration_date: Date}] = await prisma.$queryRaw - `SELECT distinct o.uid, f.created_at as expiration_date + const expiringDocuments15Days: [{ uid: string; expiration_date: Date }] = + await prisma.$queryRaw`SELECT distinct o.uid, f.created_at as expiration_date FROM documents d JOIN files f ON d.uid=f.document_uid JOIN office_folders o ON o.uid=d.folder_uid @@ -95,11 +97,24 @@ export default class CronService { AND d.document_status = 'DEPOSITED' AND current_date = (DATE(f.created_at) + interval '3 months' - interval '2 days');`; - expiringDocuments15Days.forEach(expiringFolder => { + expiringDocuments15Days.forEach((expiringFolder) => { this.notificationBuilder.sendDocumentExpiringSoonNotification(expiringFolder.uid, expiringFolder.expiration_date); }); + } catch (e) { + console.error(e); + } + }); + // Start job + if (!cronJob.running) { + cronJob.start(); + } + } - + public async cleanExpiredTotpCodes() { + const cronJob = new CronJob("0 0 * * *", async () => { + // Once a day at midnight + try { + await this.totpService.cleanExpiredTotpCodes(); } catch (e) { console.error(e); } diff --git a/src/services/customer/TotpService/TotpService.ts b/src/services/customer/TotpService/TotpService.ts new file mode 100644 index 00000000..ed8a9b20 --- /dev/null +++ b/src/services/customer/TotpService/TotpService.ts @@ -0,0 +1,51 @@ +import { Prisma, TotpCodes } from "@prisma/client"; +import TotpCodesRepository from "@Repositories/TotpCodesRepository"; +import BaseService from "@Services/BaseService"; +import { Service } from "typedi"; + +@Service() +export default class TotpService extends BaseService { + constructor(private totpCodesRepository: TotpCodesRepository) { + super(); + } + + /** + * @description : Get all totp codes + * @throws {Error} If totp codes cannot be get + */ + public async get(query: Prisma.TotpCodesFindManyArgs): Promise { + return this.totpCodesRepository.findMany(query); + } + + /** + * @description : Get one totp code + * @throws {Error} If totp code cannot be get + */ + public async getOne(query: Prisma.TotpCodesFindFirstArgs): Promise { + return this.totpCodesRepository.findOne(query); + } + + /** + * @description : Delete many totp codes + * @throws {Error} If totp code cannot be deleted + */ + public async deleteMany(query: Prisma.TotpCodesDeleteManyArgs): Promise { + return this.totpCodesRepository.deleteMany(query); + } + + /** + * @description : Delete every totp code expired for more than 30 days + * @throws {Error} If totp codes cannot be deleted + */ + public async cleanExpiredTotpCodes() { + const query: Prisma.TotpCodesDeleteManyArgs = { + where: { + expire_at: { + lte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), + }, + }, + }; + + return this.totpCodesRepository.deleteMany(query); + } +} From 7c07b5674d2556be2c0a37a245cc298ebadd11d8 Mon Sep 17 00:00:00 2001 From: Vins Date: Fri, 1 Dec 2023 11:56:50 +0100 Subject: [PATCH 32/46] using IDNOT_REDIRECT_URL instead of APP_HOST --- src/app/api/idnot/UserController.ts | 2 +- src/services/common/IdNotService/IdNotService.ts | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/app/api/idnot/UserController.ts b/src/app/api/idnot/UserController.ts index cac75ffc..60d13059 100644 --- a/src/app/api/idnot/UserController.ts +++ b/src/app/api/idnot/UserController.ts @@ -22,7 +22,7 @@ export default class UserController extends ApiController { protected async getUserInfosFromIdnot(req: Request, response: Response) { try { const code = req.params["code"]; - if (!code) throw new Error("code is required"); + if (!code) throw new Error("code is required"); const idNotToken = await this.idNotService.getIdNotToken(code); if(!idNotToken) { diff --git a/src/services/common/IdNotService/IdNotService.ts b/src/services/common/IdNotService/IdNotService.ts index 7b403836..dd15912e 100644 --- a/src/services/common/IdNotService/IdNotService.ts +++ b/src/services/common/IdNotService/IdNotService.ts @@ -117,11 +117,14 @@ export default class IdNotService extends BaseService { const query = new URLSearchParams({ client_id: this.variables.IDNOT_CLIENT_ID, client_secret: this.variables.IDNOT_CLIENT_SECRET, - redirect_uri: `${this.variables.APP_HOST}/authorized-client`, + redirect_uri: this.variables.IDNOT_REDIRECT_URL, code: code, grant_type: "authorization_code", - }); + }); + const token = await fetch(this.variables.IDNOT_BASE_URL + this.variables.IDNOT_CONNEXION_URL + "?" + query, { method: "POST" }); + if(token.status !== 200) console.error(await token.text()); + const decodedToken = (await token.json()) as IIdNotToken; const decodedIdToken = jwt.decode(decodedToken.id_token) as IdNotJwtPayload; From a2f1ed3943f009c02989fc34984876c830612426 Mon Sep 17 00:00:00 2001 From: Vins Date: Fri, 1 Dec 2023 12:03:20 +0100 Subject: [PATCH 33/46] disabled auto migrate --- devops/stg.values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devops/stg.values.yaml b/devops/stg.values.yaml index 74a13fa8..e2ce7a4b 100644 --- a/devops/stg.values.yaml +++ b/devops/stg.values.yaml @@ -5,7 +5,7 @@ scwSecretKey: AgChoEnPitXp4Ny/rVMEcevaWKNVpyj2cJYAcq+yFqKwVwnLB+ffDvwqz9XBHu+6d4 lecoffreBack: serviceAccountName: lecoffre-back-sa envSecrets: stg-env - command: "'sh', '-c', 'export $(xargs Date: Fri, 1 Dec 2023 12:11:50 +0100 Subject: [PATCH 34/46] debug log --- src/services/common/IdNotService/IdNotService.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/services/common/IdNotService/IdNotService.ts b/src/services/common/IdNotService/IdNotService.ts index dd15912e..a19c18de 100644 --- a/src/services/common/IdNotService/IdNotService.ts +++ b/src/services/common/IdNotService/IdNotService.ts @@ -122,6 +122,8 @@ export default class IdNotService extends BaseService { grant_type: "authorization_code", }); + console.log(this.variables.IDNOT_BASE_URL + this.variables.IDNOT_CONNEXION_URL + "?" + query); + const token = await fetch(this.variables.IDNOT_BASE_URL + this.variables.IDNOT_CONNEXION_URL + "?" + query, { method: "POST" }); if(token.status !== 200) console.error(await token.text()); From 8ee88e2ffd30a750aac5d38752699a73cfde17b9 Mon Sep 17 00:00:00 2001 From: Vins Date: Fri, 1 Dec 2023 15:22:16 +0100 Subject: [PATCH 35/46] Migrations --- src/common/databases/migrations/20230621092719_init/migration.sql | 0 src/common/databases/migrations/20230621100427_v1/migration.sql | 0 src/common/databases/migrations/20230622172124_v2/migration.sql | 0 src/common/databases/migrations/20230622172838_v3/migration.sql | 0 src/common/databases/migrations/20230623070044_v4/migration.sql | 0 src/common/databases/migrations/20230623072820_v5/migration.sql | 0 src/common/databases/migrations/20230627155351_v6/migration.sql | 0 src/common/databases/migrations/20230628100711_v7/migration.sql | 0 src/common/databases/migrations/20230703120600_v9/migration.sql | 0 src/common/databases/migrations/20230703122557_v10/migration.sql | 0 src/common/databases/migrations/20230703123246_v11/migration.sql | 0 src/common/databases/migrations/20230711134012_v12/migration.sql | 0 src/common/databases/migrations/20230713145026_v12/migration.sql | 0 src/common/databases/migrations/20230725084826_v12/migration.sql | 0 src/common/databases/migrations/20230725151748_v13/migration.sql | 0 src/common/databases/migrations/20230726095252_v25/migration.sql | 0 src/common/databases/migrations/20230801083702_v13/migration.sql | 0 src/common/databases/migrations/20230816144031_v22/migration.sql | 0 src/common/databases/migrations/20230907100058_v26/migration.sql | 0 src/common/databases/migrations/20230915113328_v26/migration.sql | 0 src/common/databases/migrations/20230920100341_v27/migration.sql | 0 21 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/common/databases/migrations/20230621092719_init/migration.sql create mode 100644 src/common/databases/migrations/20230621100427_v1/migration.sql create mode 100644 src/common/databases/migrations/20230622172124_v2/migration.sql create mode 100644 src/common/databases/migrations/20230622172838_v3/migration.sql create mode 100644 src/common/databases/migrations/20230623070044_v4/migration.sql create mode 100644 src/common/databases/migrations/20230623072820_v5/migration.sql create mode 100644 src/common/databases/migrations/20230627155351_v6/migration.sql create mode 100644 src/common/databases/migrations/20230628100711_v7/migration.sql create mode 100644 src/common/databases/migrations/20230703120600_v9/migration.sql create mode 100644 src/common/databases/migrations/20230703122557_v10/migration.sql create mode 100644 src/common/databases/migrations/20230703123246_v11/migration.sql create mode 100644 src/common/databases/migrations/20230711134012_v12/migration.sql create mode 100644 src/common/databases/migrations/20230713145026_v12/migration.sql create mode 100644 src/common/databases/migrations/20230725084826_v12/migration.sql create mode 100644 src/common/databases/migrations/20230725151748_v13/migration.sql create mode 100644 src/common/databases/migrations/20230726095252_v25/migration.sql create mode 100644 src/common/databases/migrations/20230801083702_v13/migration.sql create mode 100644 src/common/databases/migrations/20230816144031_v22/migration.sql create mode 100644 src/common/databases/migrations/20230907100058_v26/migration.sql create mode 100644 src/common/databases/migrations/20230915113328_v26/migration.sql create mode 100644 src/common/databases/migrations/20230920100341_v27/migration.sql diff --git a/src/common/databases/migrations/20230621092719_init/migration.sql b/src/common/databases/migrations/20230621092719_init/migration.sql new file mode 100644 index 00000000..e69de29b diff --git a/src/common/databases/migrations/20230621100427_v1/migration.sql b/src/common/databases/migrations/20230621100427_v1/migration.sql new file mode 100644 index 00000000..e69de29b diff --git a/src/common/databases/migrations/20230622172124_v2/migration.sql b/src/common/databases/migrations/20230622172124_v2/migration.sql new file mode 100644 index 00000000..e69de29b diff --git a/src/common/databases/migrations/20230622172838_v3/migration.sql b/src/common/databases/migrations/20230622172838_v3/migration.sql new file mode 100644 index 00000000..e69de29b diff --git a/src/common/databases/migrations/20230623070044_v4/migration.sql b/src/common/databases/migrations/20230623070044_v4/migration.sql new file mode 100644 index 00000000..e69de29b diff --git a/src/common/databases/migrations/20230623072820_v5/migration.sql b/src/common/databases/migrations/20230623072820_v5/migration.sql new file mode 100644 index 00000000..e69de29b diff --git a/src/common/databases/migrations/20230627155351_v6/migration.sql b/src/common/databases/migrations/20230627155351_v6/migration.sql new file mode 100644 index 00000000..e69de29b diff --git a/src/common/databases/migrations/20230628100711_v7/migration.sql b/src/common/databases/migrations/20230628100711_v7/migration.sql new file mode 100644 index 00000000..e69de29b diff --git a/src/common/databases/migrations/20230703120600_v9/migration.sql b/src/common/databases/migrations/20230703120600_v9/migration.sql new file mode 100644 index 00000000..e69de29b diff --git a/src/common/databases/migrations/20230703122557_v10/migration.sql b/src/common/databases/migrations/20230703122557_v10/migration.sql new file mode 100644 index 00000000..e69de29b diff --git a/src/common/databases/migrations/20230703123246_v11/migration.sql b/src/common/databases/migrations/20230703123246_v11/migration.sql new file mode 100644 index 00000000..e69de29b diff --git a/src/common/databases/migrations/20230711134012_v12/migration.sql b/src/common/databases/migrations/20230711134012_v12/migration.sql new file mode 100644 index 00000000..e69de29b diff --git a/src/common/databases/migrations/20230713145026_v12/migration.sql b/src/common/databases/migrations/20230713145026_v12/migration.sql new file mode 100644 index 00000000..e69de29b diff --git a/src/common/databases/migrations/20230725084826_v12/migration.sql b/src/common/databases/migrations/20230725084826_v12/migration.sql new file mode 100644 index 00000000..e69de29b diff --git a/src/common/databases/migrations/20230725151748_v13/migration.sql b/src/common/databases/migrations/20230725151748_v13/migration.sql new file mode 100644 index 00000000..e69de29b diff --git a/src/common/databases/migrations/20230726095252_v25/migration.sql b/src/common/databases/migrations/20230726095252_v25/migration.sql new file mode 100644 index 00000000..e69de29b diff --git a/src/common/databases/migrations/20230801083702_v13/migration.sql b/src/common/databases/migrations/20230801083702_v13/migration.sql new file mode 100644 index 00000000..e69de29b diff --git a/src/common/databases/migrations/20230816144031_v22/migration.sql b/src/common/databases/migrations/20230816144031_v22/migration.sql new file mode 100644 index 00000000..e69de29b diff --git a/src/common/databases/migrations/20230907100058_v26/migration.sql b/src/common/databases/migrations/20230907100058_v26/migration.sql new file mode 100644 index 00000000..e69de29b diff --git a/src/common/databases/migrations/20230915113328_v26/migration.sql b/src/common/databases/migrations/20230915113328_v26/migration.sql new file mode 100644 index 00000000..e69de29b diff --git a/src/common/databases/migrations/20230920100341_v27/migration.sql b/src/common/databases/migrations/20230920100341_v27/migration.sql new file mode 100644 index 00000000..e69de29b From 56fe8a43b4293e234498ebcb23d103c52d0b2d3d Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Fri, 1 Dec 2023 16:36:20 +0100 Subject: [PATCH 36/46] :sparkles: Can now resend sms right away --- package.json | 2 +- .../20231201150833_resent_totp/migration.sql | 2 + src/common/databases/schema.prisma | 1 + .../repositories/TotpCodesRepository.ts | 2 + .../CustomersService/CustomersService.ts | 42 +++++++++++-------- 5 files changed, 31 insertions(+), 18 deletions(-) create mode 100644 src/common/databases/migrations/20231201150833_resent_totp/migration.sql diff --git a/package.json b/package.json index 33f4337d..7d1cb4f0 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "file-type-checker": "^1.0.8", "fp-ts": "^2.16.1", "jsonwebtoken": "^9.0.0", - "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.99", + "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.102", "module-alias": "^2.2.2", "monocle-ts": "^2.3.13", "multer": "^1.4.5-lts.1", diff --git a/src/common/databases/migrations/20231201150833_resent_totp/migration.sql b/src/common/databases/migrations/20231201150833_resent_totp/migration.sql new file mode 100644 index 00000000..bf3805f6 --- /dev/null +++ b/src/common/databases/migrations/20231201150833_resent_totp/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "totp_codes" ADD COLUMN "resent" BOOLEAN NOT NULL DEFAULT false; diff --git a/src/common/databases/schema.prisma b/src/common/databases/schema.prisma index be5c663b..77d5f2d3 100644 --- a/src/common/databases/schema.prisma +++ b/src/common/databases/schema.prisma @@ -348,6 +348,7 @@ model TotpCodes { customer_uid String @db.VarChar(255) code String @db.VarChar(255) reason TotpCodesReasons @default(LOGIN) + resent Boolean @default(false) expire_at DateTime? @default(now()) created_at DateTime? @default(now()) updated_at DateTime? @updatedAt diff --git a/src/common/repositories/TotpCodesRepository.ts b/src/common/repositories/TotpCodesRepository.ts index 8d1ebf48..4a78e4e5 100644 --- a/src/common/repositories/TotpCodesRepository.ts +++ b/src/common/repositories/TotpCodesRepository.ts @@ -48,6 +48,7 @@ export default class TotpCodesRepository extends BaseRepository { }, }, expire_at: totpCode.expire_at!, + resent: totpCode.resent!, }, }; @@ -64,6 +65,7 @@ export default class TotpCodesRepository extends BaseRepository { }, data: { expire_at: new Date(), + resent: true, }, }); } diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index cf03f0c8..67dc09a5 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -1,10 +1,10 @@ import { BackendVariables } from "@Common/config/variables/Variables"; -import { Customers, Prisma } from "@prisma/client"; +import { Customers, Prisma, TotpCodes } from "@prisma/client"; import CustomersRepository from "@Repositories/CustomersRepository"; import TotpCodesRepository from "@Repositories/TotpCodesRepository"; import BaseService from "@Services/BaseService"; import AuthService from "@Services/common/AuthService/AuthService"; -import TotpCodes, { TotpCodesReasons } from "le-coffre-resources/dist/Customer/TotpCodes"; +import TotpCodesResource, { TotpCodesReasons } from "le-coffre-resources/dist/Customer/TotpCodes"; import { Customer } from "le-coffre-resources/dist/Notary"; import { Service } from "typedi"; import OvhService from "@Services/common/OvhService/OvhService"; @@ -88,7 +88,7 @@ export default class CustomersService extends BaseService { * 4: Save the SMS code in database * 5: Send the SMS code to the customer */ - public async verifyEmail2FASms(email: string): Promise<{ customer: Customer; totpCode: TotpCodes } | null> { + public async verifyEmail2FASms(email: string): Promise<{ customer: Customer; totpCode: TotpCodesResource } | null> { // 1: Check if the customer exists const customer = await this.getByEmail(email); if (!customer) return null; @@ -108,10 +108,16 @@ export default class CustomersService extends BaseService { const reason = customer.password ? TotpCodesReasons.LOGIN : TotpCodesReasons.FIRST_LOGIN; // 4: Save the SMS code in database const totpCode = await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60000), reason); - + if (!totpCode) return null; // 5: Send the SMS code to the customer await this.sendSmsCodeToCustomer(totpPin, customer); - return { customer, totpCode }; + return { + customer, + totpCode: TotpCodesResource.hydrate({ + ...totpCode, + reason: totpCode.reason as TotpCodesReasons, + }), + }; } /** @@ -273,7 +279,8 @@ export default class CustomersService extends BaseService { if (!totpCodeToResend) throw new TotpCodeExpiredError(); // 3: Check if it was created more than 30 seconds ago - if (totpCodeToResend.created_at && totpCodeToResend.created_at.getTime() > now - 30000) throw new TooSoonForNewCode(); + if (totpCodeToResend.created_at && totpCodeToResend.created_at.getTime() > now - 30000 && totpCodeToResend.resent) + throw new TooSoonForNewCode(); // 4: Generate a new SMS code const totpPin = this.generateTotp(); @@ -282,7 +289,7 @@ export default class CustomersService extends BaseService { await this.totpCodesRepository.disable(totpCodeToResend); // 6: Save the SMS code in database - const totpCode = await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60000), totpCodeToResend.reason!); + const totpCode = await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60000), totpCodeToResend.reason!, true); // 7: Send the SMS code to the customer await this.sendSmsCodeToCustomer(totpPin, customer); @@ -322,22 +329,23 @@ export default class CustomersService extends BaseService { /** * @description : Saves a TotpPin in database */ - private async saveTotpPin(customer: Customer, totpPin: number, expireAt: Date, reason: TotpCodesReasons) { + private async saveTotpPin( + customer: Customer, + totpPin: number, + expireAt: Date, + reason: TotpCodesReasons, + resent?: boolean, + ): Promise { // Create the totpCode in table using repository - await this.totpCodesRepository.create( - TotpCodes.hydrate({ + return await this.totpCodesRepository.create( + TotpCodesResource.hydrate({ reason, customer_uid: customer.uid, customer: Customer.hydrate(customer), created_at: new Date(), code: totpPin.toString(), expire_at: expireAt, - }), - ); - return await this.customerRepository.update( - customer.uid as string, - Customer.hydrate({ - ...customer, + resent: resent || false, }), ); } @@ -371,7 +379,7 @@ export default class CustomersService extends BaseService { * @param email * @returns */ - public async verifyTotpCode(totpCode: string, email: string): Promise { + public async verifyTotpCode(totpCode: string, email: string): Promise { // 1: Check if the customer exists const customer = await this.getByEmail(email); if (!customer) return null; From 869a30fac01aa61e7e94f1948e6a29c558fd8e69 Mon Sep 17 00:00:00 2001 From: Vins Date: Mon, 4 Dec 2023 02:30:31 +0100 Subject: [PATCH 37/46] Added ovh sms service name variable and cleaned code --- src/common/config/variables/Variables.ts | 4 +++ src/services/common/OvhService/OvhService.ts | 36 ++++++++----------- .../SmsFactorService/SmsFactorService.ts | 4 +-- .../CustomersService/CustomersService.ts | 2 +- 4 files changed, 21 insertions(+), 25 deletions(-) diff --git a/src/common/config/variables/Variables.ts b/src/common/config/variables/Variables.ts index 5a46fd26..1a618ab6 100644 --- a/src/common/config/variables/Variables.ts +++ b/src/common/config/variables/Variables.ts @@ -121,6 +121,9 @@ export class BackendVariables { @IsNotEmpty() public readonly OVH_CONSUMER_KEY!: string; + @IsNotEmpty() + public readonly OVH_SMS_SERVICE_NAME!: string; + @IsNotEmpty() public readonly SMS_FACTOR_TOKEN!: string; @@ -165,6 +168,7 @@ export class BackendVariables { this.OVH_APP_KEY = process.env["OVH_APP_KEY"]!; this.OVH_APP_SECRET = process.env["OVH_APP_SECRET"]!; this.OVH_CONSUMER_KEY = process.env["OVH_CONSUMER_KEY"]!; + this.OVH_SMS_SERVICE_NAME = process.env["OVH_SMS_SERVICE_NAME"]!; this.SMS_FACTOR_TOKEN = process.env["SMS_FACTOR_TOKEN"]!; diff --git a/src/services/common/OvhService/OvhService.ts b/src/services/common/OvhService/OvhService.ts index f70b59d2..94fb0e05 100644 --- a/src/services/common/OvhService/OvhService.ts +++ b/src/services/common/OvhService/OvhService.ts @@ -14,30 +14,22 @@ export default class OvhService extends BaseService { appSecret: this.variables.OVH_APP_SECRET, consumerKey: this.variables.OVH_CONSUMER_KEY, }); - - ovh.request("GET", "/sms", function (err: any, serviceName: string) { - if (err) { - console.log(err, serviceName); - return false; + + const serviceName = this.variables.OVH_SMS_SERVICE_NAME; + + ovh.request('POST', '/sms/' + serviceName + '/jobs/', { + message: message, + sender: "LeCoffre", + receivers: [phoneNumber], + }, (error: any, response: any) => { + if (error) { + console.error('Error sending Ovh Sms:', error); + return false; } else { - console.log("My account SMS is " + serviceName); - - // Send a simple SMS with a short number using your serviceName - ovh.request( - "POST", - "/sms/" + serviceName + "/jobs", - { - message: message, - senderForResponse: true, - receivers: [phoneNumber], - }, - function (errsend: any, result: any) { - console.log(errsend, result); - }, - ); - return true; + console.log('SMS sent successfully via Ovh:', response); + return true; } }); - return false; + return false; } } diff --git a/src/services/common/SmsFactorService/SmsFactorService.ts b/src/services/common/SmsFactorService/SmsFactorService.ts index 851152ed..32bd38ea 100644 --- a/src/services/common/SmsFactorService/SmsFactorService.ts +++ b/src/services/common/SmsFactorService/SmsFactorService.ts @@ -17,12 +17,12 @@ export default class SmsFactorService extends BaseService { to: phoneNumber, text: message, }).then(response => { - console.log('SMS sent successfully:', response.data); + console.log('SMS sent successfully via Sms Factor :', response.data); return true; }) .catch(error => { - console.error('Error sending SMS:', error.response.data); + console.error('Error sending Sms Factor SMS:', error.response.data); return false; }); return false; diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index 67dc09a5..46a0843b 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -360,7 +360,7 @@ export default class CustomersService extends BaseService { // Envoi du SMS if (!customer.contact?.phone_number) return; - let success = await selectedProvider.sendSms(customer.contact?.phone_number, totpPin.toString()); + let success = await selectedProvider.sendSms(customer.contact?.phone_number, totpPin.toString()); // Si l'envoi échoue, basculez automatiquement sur le second fournisseur if (!success) { From 70845595dfdd09531671661fd245494275c63593 Mon Sep 17 00:00:00 2001 From: Vins Date: Mon, 4 Dec 2023 03:07:56 +0100 Subject: [PATCH 38/46] Sms factor done in simulate mode --- .../SmsFactorService/SmsFactorService.ts | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/src/services/common/SmsFactorService/SmsFactorService.ts b/src/services/common/SmsFactorService/SmsFactorService.ts index 32bd38ea..14f93b6b 100644 --- a/src/services/common/SmsFactorService/SmsFactorService.ts +++ b/src/services/common/SmsFactorService/SmsFactorService.ts @@ -5,26 +5,29 @@ import axios from "axios"; @Service() export default class SmsFactorService extends BaseService { - - constructor(private variables: BackendVariables) { + constructor(private variables: BackendVariables) { super(); } - public async sendSms(phoneNumber: string, message: string): Promise { - axios.post('https://api.smsfactor.com/send/', { - token: this.variables.SMS_FACTOR_TOKEN, - sender: "LeCoffre", - to: phoneNumber, - text: message, - }).then(response => { - console.log('SMS sent successfully via Sms Factor :', response.data); - return true; - - }) - .catch(error => { - console.error('Error sending Sms Factor SMS:', error.response.data); - return false; - }); - return false; - } -} \ No newline at end of file + public async sendSms(phoneNumber: string, message: string): Promise { + axios + .get( + "https://api.smsfactor.com/send/simulate?to=" + + phoneNumber + + "&sender=LeCoffre&text=" + + message + + "token=" + + this.variables.SMS_FACTOR_TOKEN, + {}, + ) + .then((response) => { + console.log("SMS sent successfully via Sms Factor:", response.status); + return true; + }) + .catch((error) => { + console.error("Error sending Sms Factor SMS:", error); + return false; + }); + return false; + } +} From 1d8642b61fed3d3467463f805bc31ec49767cc08 Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Mon, 4 Dec 2023 11:17:31 +0100 Subject: [PATCH 39/46] :sparkles: Improving comprehension of code --- src/app/api/customer/AuthController.ts | 5 ++--- .../customer/CustomersService/CustomersService.ts | 8 ++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/app/api/customer/AuthController.ts b/src/app/api/customer/AuthController.ts index f4067dd9..55c922a2 100644 --- a/src/app/api/customer/AuthController.ts +++ b/src/app/api/customer/AuthController.ts @@ -6,7 +6,6 @@ import CustomersService, { InvalidPasswordError, InvalidTotpCodeError, NotRegisteredCustomerError, - PasswordAlreadySetError, SmsNotExpiredError, TooSoonForNewCode, TotpCodeExpiredError, @@ -141,7 +140,7 @@ export default class AuthController extends ApiController { } try { - const customer = await this.customerService.setFirstPassword(email, totpCode, password); + const customer = await this.customerService.setPassword(email, totpCode, password); if (!customer) { this.httpBadRequest(response, "Customer not found"); return; @@ -153,7 +152,7 @@ export default class AuthController extends ApiController { const refreshToken = this.authService.generateRefreshToken(payload); this.httpSuccess(response, { accessToken, refreshToken }); } catch (error) { - if (error instanceof TotpCodeExpiredError || error instanceof PasswordAlreadySetError) { + if (error instanceof TotpCodeExpiredError) { this.httpBadRequest(response, error.message); return; } diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index 46a0843b..922455f8 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -180,7 +180,7 @@ export default class CustomersService extends BaseService { * @param password * @returns */ - public async setFirstPassword(email: string, totpCode: string, password: string): Promise { + public async setPassword(email: string, totpCode: string, password: string): Promise { // 1: Check if the customer exists const customer = await this.getByEmail(email); if (!customer) return null; @@ -212,7 +212,7 @@ export default class CustomersService extends BaseService { const hashedPassword = await this.authService.hashPassword(password); // 7: Set the password in database and return the result of the update - return await this.setPassword(customer, hashedPassword); + return await this.setPasswordInDatabase(customer, hashedPassword); } /** @@ -300,7 +300,7 @@ export default class CustomersService extends BaseService { * @description : Set password for a customer * @throws {Error} If customer cannot be updated */ - private async setPassword(customer: Customer, password: string) { + private async setPasswordInDatabase(customer: Customer, password: string) { return await this.customerRepository.update( customer.uid as string, Customer.hydrate({ @@ -360,7 +360,7 @@ export default class CustomersService extends BaseService { // Envoi du SMS if (!customer.contact?.phone_number) return; - let success = await selectedProvider.sendSms(customer.contact?.phone_number, totpPin.toString()); + let success = await selectedProvider.sendSms(customer.contact?.phone_number, totpPin.toString()); // Si l'envoi échoue, basculez automatiquement sur le second fournisseur if (!success) { From cc316aeb2f472d6235bcd8ae6fed905acecd72f1 Mon Sep 17 00:00:00 2001 From: Vins Date: Mon, 4 Dec 2023 11:23:22 +0100 Subject: [PATCH 40/46] Sms feature finished + desactivate in dev --- src/services/common/OvhService/OvhService.ts | 1 + .../common/SmsFactorService/SmsFactorService.ts | 4 ++-- .../customer/CustomersService/CustomersService.ts | 11 ++++++----- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/services/common/OvhService/OvhService.ts b/src/services/common/OvhService/OvhService.ts index 94fb0e05..eaa9196b 100644 --- a/src/services/common/OvhService/OvhService.ts +++ b/src/services/common/OvhService/OvhService.ts @@ -20,6 +20,7 @@ export default class OvhService extends BaseService { ovh.request('POST', '/sms/' + serviceName + '/jobs/', { message: message, sender: "LeCoffre", + senderForResponse: true, receivers: [phoneNumber], }, (error: any, response: any) => { if (error) { diff --git a/src/services/common/SmsFactorService/SmsFactorService.ts b/src/services/common/SmsFactorService/SmsFactorService.ts index 14f93b6b..13332750 100644 --- a/src/services/common/SmsFactorService/SmsFactorService.ts +++ b/src/services/common/SmsFactorService/SmsFactorService.ts @@ -16,12 +16,12 @@ export default class SmsFactorService extends BaseService { phoneNumber + "&sender=LeCoffre&text=" + message + - "token=" + + "&token=" + this.variables.SMS_FACTOR_TOKEN, {}, ) .then((response) => { - console.log("SMS sent successfully via Sms Factor:", response.status); + console.log("SMS sent successfully via Sms Factor :" + response); return true; }) .catch((error) => { diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index 922455f8..7a5789f6 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -110,7 +110,7 @@ export default class CustomersService extends BaseService { const totpCode = await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60000), reason); if (!totpCode) return null; // 5: Send the SMS code to the customer - await this.sendSmsCodeToCustomer(totpPin, customer); + if(this.variables.ENV !== 'dev') await this.sendSmsCodeToCustomer(totpPin, customer); return { customer, totpCode: TotpCodesResource.hydrate({ @@ -162,7 +162,7 @@ export default class CustomersService extends BaseService { await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60000), TotpCodesReasons.RESET_PASSWORD); // 5: Send the SMS code to the customer - await this.sendSmsCodeToCustomer(totpPin, customer); + if(this.variables.ENV !== 'dev') await this.sendSmsCodeToCustomer(totpPin, customer); return customer; } @@ -292,7 +292,7 @@ export default class CustomersService extends BaseService { const totpCode = await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60000), totpCodeToResend.reason!, true); // 7: Send the SMS code to the customer - await this.sendSmsCodeToCustomer(totpPin, customer); + if(this.variables.ENV !== 'dev') await this.sendSmsCodeToCustomer(totpPin, customer); return { customer, totpCode }; } @@ -355,17 +355,18 @@ export default class CustomersService extends BaseService { } private async sendSmsCodeToCustomer(totpPin: number, customer: Customer) { + const message = "Votre code de vérification LEcoffre.io est : " + totpPin.toString(); // Sélectionnez le fournisseur de SMS en fonction de la variable d'environnement const selectedProvider = this.variables.SMS_PROVIDER === "OVH" ? this.ovhService : this.smsFactorService; // Envoi du SMS if (!customer.contact?.phone_number) return; - let success = await selectedProvider.sendSms(customer.contact?.phone_number, totpPin.toString()); + let success = await selectedProvider.sendSms(customer.contact?.phone_number, message); // Si l'envoi échoue, basculez automatiquement sur le second fournisseur if (!success) { const alternateProvider = this.variables.SMS_PROVIDER === "OVH" ? this.smsFactorService : this.ovhService; - success = await alternateProvider.sendSms(customer.contact?.phone_number, totpPin.toString()); + success = await alternateProvider.sendSms(customer.contact?.phone_number, message); } } From 542e4ae24cc30cdd6ed56664bfdac2ac40cf1797 Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Mon, 4 Dec 2023 11:30:05 +0100 Subject: [PATCH 41/46] :sparkles: Improving comprehension of code --- .../customer/CustomersService/CustomersService.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index 7a5789f6..8af9653e 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -107,7 +107,7 @@ export default class CustomersService extends BaseService { const reason = customer.password ? TotpCodesReasons.LOGIN : TotpCodesReasons.FIRST_LOGIN; // 4: Save the SMS code in database - const totpCode = await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60000), reason); + const totpCode = await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60 * 1000), reason); if (!totpCode) return null; // 5: Send the SMS code to the customer if(this.variables.ENV !== 'dev') await this.sendSmsCodeToCustomer(totpPin, customer); @@ -272,13 +272,13 @@ export default class CustomersService extends BaseService { const customerHydrated = Customer.hydrate(customer); - // 2: Get last code sent + // 2: Get last code sent and check if it's still valid const totpCodeToResend = customerHydrated.totpCodes?.find((totpCode) => { return totpCode.uid === totpCodeUid && totpCode.expire_at && totpCode.expire_at.getTime() > now; }); if (!totpCodeToResend) throw new TotpCodeExpiredError(); - // 3: Check if it was created more than 30 seconds ago + // 3: Check if it was created more than 30 seconds ago and hasn't been resent yet if (totpCodeToResend.created_at && totpCodeToResend.created_at.getTime() > now - 30000 && totpCodeToResend.resent) throw new TooSoonForNewCode(); @@ -288,8 +288,8 @@ export default class CustomersService extends BaseService { // 5: Disable the old code await this.totpCodesRepository.disable(totpCodeToResend); - // 6: Save the SMS code in database - const totpCode = await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60000), totpCodeToResend.reason!, true); + // 6: Save the SMS code in database with the same reason as the old one + const totpCode = await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60 * 1000), totpCodeToResend.reason!, true); // 7: Send the SMS code to the customer if(this.variables.ENV !== 'dev') await this.sendSmsCodeToCustomer(totpPin, customer); From ff60250d5ce2cdd138bca620fc20fa49edca940e Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Mon, 4 Dec 2023 11:38:11 +0100 Subject: [PATCH 42/46] :sparkles: Fix seeder with valid number --- src/common/databases/seeders/seeder.ts | 82 +++++++++++++------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/src/common/databases/seeders/seeder.ts b/src/common/databases/seeders/seeder.ts index 33f9ec37..2e7b3f30 100644 --- a/src/common/databases/seeders/seeder.ts +++ b/src/common/databases/seeders/seeder.ts @@ -173,8 +173,8 @@ export default async function main() { first_name: "Angela", last_name: "Dubois", email: "angela.dubois@gmail.com", - phone_number: "06 12 34 56 78", - cell_phone_number: "06 12 34 56 78", + phone_number: "+33785186013", + cell_phone_number: "+33785186013", birthdate: null, created_at: new Date(), updated_at: new Date(), @@ -185,8 +185,8 @@ export default async function main() { first_name: "Maxime", last_name: "Lalo", email: "maxime.lalo@smart-chain.fr", - phone_number: "06 23 45 67 89", - cell_phone_number: "06 23 45 67 89", + phone_number: "+33785186013", + cell_phone_number: "+33785186013", birthdate: null, created_at: new Date(), updated_at: new Date(), @@ -197,8 +197,8 @@ export default async function main() { first_name: "Vincent", last_name: "Alamelle", email: "vincent.alamelle@smart-chain.fr", - phone_number: "06 34 56 78 90", - cell_phone_number: "06 34 56 78 90", + phone_number: "+33785186013", + cell_phone_number: "+33785186013", birthdate: null, created_at: new Date(), updated_at: new Date(), @@ -209,8 +209,8 @@ export default async function main() { first_name: "Melissa", last_name: "Desde", email: "melissa.desde@smart-chain.fr", - phone_number: "06 45 67 89 01", - cell_phone_number: "06 45 67 89 01", + phone_number: "+33785186013", + cell_phone_number: "+33785186013", birthdate: null, created_at: new Date(), updated_at: new Date(), @@ -221,8 +221,8 @@ export default async function main() { first_name: "Maxime", last_name: "Leroy", email: "maxime.leroy@hotmail.fr", - phone_number: "06 56 78 90 12", - cell_phone_number: "06 56 78 90 12", + phone_number: "+33785186013", + cell_phone_number: "+33785186013", birthdate: null, created_at: new Date(), updated_at: new Date(), @@ -233,8 +233,8 @@ export default async function main() { first_name: "Paul", last_name: "Dupont", email: "paul.dupont@outlook.com", - phone_number: "06 67 89 01 23", - cell_phone_number: "06 67 89 01 23", + phone_number: "+33785186013", + cell_phone_number: "+33785186013", birthdate: null, created_at: new Date(), updated_at: new Date(), @@ -245,8 +245,8 @@ export default async function main() { first_name: "Jean", last_name: "Dubignot", email: "jean.dubignot@gmail.com", - phone_number: "06 78 90 12 34", - cell_phone_number: "06 78 90 12 34", + phone_number: "+33785186013", + cell_phone_number: "+33785186013", birthdate: null, created_at: new Date(), updated_at: new Date(), @@ -257,8 +257,8 @@ export default async function main() { first_name: "Vincent", last_name: "Martin", email: "vincent.martin@gmail.com", - phone_number: "06 89 01 23 45", - cell_phone_number: "06 89 01 23 45", + phone_number: "+33785186013", + cell_phone_number: "+33785186013", birthdate: null, created_at: new Date(), updated_at: new Date(), @@ -269,8 +269,8 @@ export default async function main() { first_name: "Lucie", last_name: "Chevalier", email: "lucie.chevalier@outlook.com", - phone_number: "07 12 34 56 78", - cell_phone_number: "07 12 34 56 78", + phone_number: "+33785186013", + cell_phone_number: "+33785186013", birthdate: null, created_at: new Date(), updated_at: new Date(), @@ -281,8 +281,8 @@ export default async function main() { first_name: "Sébastien", last_name: "Dubois", email: "sebastien.dubois@gmail.com", - phone_number: "07 23 45 67 89", - cell_phone_number: "07 23 45 67 89", + phone_number: "+33785186013", + cell_phone_number: "+33785186013", birthdate: null, created_at: new Date(), updated_at: new Date(), @@ -293,8 +293,8 @@ export default async function main() { first_name: "Mathilde", last_name: "Durand", email: "mathilde.durand@gmail.com", - phone_number: "07 34 56 78 90", - cell_phone_number: "07 34 56 78 90", + phone_number: "+33785186013", + cell_phone_number: "+33785186013", birthdate: null, created_at: new Date(), updated_at: new Date(), @@ -305,8 +305,8 @@ export default async function main() { first_name: "Antoine", last_name: "Bernard", email: "antoine.bernard@outlook.com", - phone_number: "07 45 67 89 01", - cell_phone_number: "07 45 67 89 01", + phone_number: "+33785186013", + cell_phone_number: "+33785186013", birthdate: null, created_at: new Date(), updated_at: new Date(), @@ -317,8 +317,8 @@ export default async function main() { first_name: "Camille", last_name: "Laurent", email: "camille.laurent@gmail.com", - phone_number: "07 56 78 90 12", - cell_phone_number: "07 56 78 90 12", + phone_number: "+33785186013", + cell_phone_number: "+33785186013", birthdate: null, created_at: new Date(), updated_at: new Date(), @@ -329,8 +329,8 @@ export default async function main() { first_name: "Julien", last_name: "Mercier", email: "julien.mercier@hotmail.fr", - phone_number: "07 67 89 01 23", - cell_phone_number: "07 67 89 01 23", + phone_number: "+33785186013", + cell_phone_number: "+33785186013", birthdate: null, created_at: new Date(), updated_at: new Date(), @@ -341,8 +341,8 @@ export default async function main() { first_name: "Charlotte", last_name: "Lefebvre", email: "charlotte.lefebvre@gmail.com", - phone_number: "07 78 90 12 34", - cell_phone_number: "07 78 90 12 34", + phone_number: "+33785186013", + cell_phone_number: "+33785186013", birthdate: null, created_at: new Date(), updated_at: new Date(), @@ -353,8 +353,8 @@ export default async function main() { first_name: "Caroline", last_name: "Pallut", email: "caroline.pallut@gmail.com", - phone_number: "07 89 01 23 45", - cell_phone_number: "07 89 01 23 45", + phone_number: "+33785186013", + cell_phone_number: "+33785186013", birthdate: null, created_at: new Date(), updated_at: new Date(), @@ -365,8 +365,8 @@ export default async function main() { first_name: "Nadège", last_name: "Gauchet", email: "nedege.gauchet@outlook.com", - phone_number: "06 11 22 33 44", - cell_phone_number: "06 11 22 33 44", + phone_number: "+33785186013", + cell_phone_number: "+33785186013", birthdate: null, created_at: new Date(), updated_at: new Date(), @@ -377,8 +377,8 @@ export default async function main() { first_name: "Matthieu", last_name: "Bougeard", email: "matthieu.bougeard@gmail.com", - phone_number: "07 22 33 44 55", - cell_phone_number: "07 22 33 44 55", + phone_number: "+33785186013", + cell_phone_number: "+33785186013", birthdate: null, created_at: new Date(), updated_at: new Date(), @@ -389,8 +389,8 @@ export default async function main() { first_name: "Cécile", last_name: "Celton", email: "cecile.celton@outlook.com", - phone_number: "06 55 66 77 88", - cell_phone_number: "06 55 66 77 88", + phone_number: "+33785186013", + cell_phone_number: "+33785186013", birthdate: null, created_at: new Date(), updated_at: new Date(), @@ -401,8 +401,8 @@ export default async function main() { first_name: "Gwendal", last_name: "Texier", email: "gwendal.texier@gmail.com", - phone_number: "07 88 99 00 11", - cell_phone_number: "07 88 99 00 11", + phone_number: "+33785186013", + cell_phone_number: "+33785186013", birthdate: null, created_at: new Date(), updated_at: new Date(), @@ -1548,7 +1548,7 @@ export default async function main() { office: offices[0], created_at: new Date(), updated_at: new Date(), - } + }, ]; const deedTypes: DeedType[] = [ From 63bb7e49357b8ff22feb4945dc651630e8075c94 Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Mon, 4 Dec 2023 15:59:39 +0100 Subject: [PATCH 43/46] :sparkles: Updating resources version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7d1cb4f0..e9f75855 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "file-type-checker": "^1.0.8", "fp-ts": "^2.16.1", "jsonwebtoken": "^9.0.0", - "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.102", + "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.104", "module-alias": "^2.2.2", "monocle-ts": "^2.3.13", "multer": "^1.4.5-lts.1", From 00b46a24078d42c14b284dd75a34792883bf7a93 Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Tue, 5 Dec 2023 09:49:48 +0100 Subject: [PATCH 44/46] :sparkles: Password verification --- src/app/api/customer/AuthController.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/app/api/customer/AuthController.ts b/src/app/api/customer/AuthController.ts index 55c922a2..ab47bf6e 100644 --- a/src/app/api/customer/AuthController.ts +++ b/src/app/api/customer/AuthController.ts @@ -125,17 +125,23 @@ export default class AuthController extends ApiController { const password = req.body["password"]; if (!email) { - this.httpBadRequest(response, "Email is required"); + this.httpBadRequest(response, "email is required"); return; } if (!totpCode) { - this.httpBadRequest(response, "Sms code is required"); + this.httpBadRequest(response, "totpCode is required"); return; } if (!password) { - this.httpBadRequest(response, "Password is required"); + this.httpBadRequest(response, "password is required"); + return; + } + + const passwordRegex = new RegExp(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z\d@$!%*?&]{8,}$/); + if (!passwordRegex.test(password)) { + this.httpBadRequest(response, "Password must contain at least 8 characters, 1 uppercase, 1 lowercase and 1 number"); return; } From 8f00d3e4bdd2a7127dba9bee007e899030636fbb Mon Sep 17 00:00:00 2001 From: Vins Date: Tue, 5 Dec 2023 10:56:22 +0100 Subject: [PATCH 45/46] Fixed cellphonenumber --- src/services/common/OvhService/OvhService.ts | 8 +++--- .../SmsFactorService/SmsFactorService.ts | 7 +++--- .../CustomersService/CustomersService.ts | 25 +++++++++++-------- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/services/common/OvhService/OvhService.ts b/src/services/common/OvhService/OvhService.ts index eaa9196b..b4868daf 100644 --- a/src/services/common/OvhService/OvhService.ts +++ b/src/services/common/OvhService/OvhService.ts @@ -17,20 +17,20 @@ export default class OvhService extends BaseService { const serviceName = this.variables.OVH_SMS_SERVICE_NAME; - ovh.request('POST', '/sms/' + serviceName + '/jobs/', { + await ovh.request('POST', '/sms/' + serviceName + '/jobs/', { message: message, sender: "LeCoffre", senderForResponse: true, receivers: [phoneNumber], }, (error: any, response: any) => { if (error) { - console.error('Error sending Ovh Sms:', error); + console.error('Error sending Ovh Sms'); return false; } else { - console.log('SMS sent successfully via Ovh:', response); + console.log('SMS sent successfully via Ovh'); return true; } }); - return false; + return true; } } diff --git a/src/services/common/SmsFactorService/SmsFactorService.ts b/src/services/common/SmsFactorService/SmsFactorService.ts index 13332750..1bc3f714 100644 --- a/src/services/common/SmsFactorService/SmsFactorService.ts +++ b/src/services/common/SmsFactorService/SmsFactorService.ts @@ -9,7 +9,7 @@ export default class SmsFactorService extends BaseService { super(); } - public async sendSms(phoneNumber: string, message: string): Promise { + public async sendSms(phoneNumber: string, message: string){ axios .get( "https://api.smsfactor.com/send/simulate?to=" + @@ -21,13 +21,12 @@ export default class SmsFactorService extends BaseService { {}, ) .then((response) => { - console.log("SMS sent successfully via Sms Factor :" + response); + console.log("SMS sent successfully via Sms Factor"); return true; }) .catch((error) => { - console.error("Error sending Sms Factor SMS:", error); + console.error("Error sending Sms Factor SMS"); return false; }); - return false; } } diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index 8af9653e..8870873d 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -1,4 +1,4 @@ -import { BackendVariables } from "@Common/config/variables/Variables"; +// import { BackendVariables } from "@Common/config/variables/Variables"; import { Customers, Prisma, TotpCodes } from "@prisma/client"; import CustomersRepository from "@Repositories/CustomersRepository"; import TotpCodesRepository from "@Repositories/TotpCodesRepository"; @@ -57,7 +57,7 @@ export default class CustomersService extends BaseService { private customerRepository: CustomersRepository, private authService: AuthService, private totpCodesRepository: TotpCodesRepository, - private variables: BackendVariables, + // private variables: BackendVariables, private ovhService: OvhService, private smsFactorService: SmsFactorService, ) { @@ -110,7 +110,8 @@ export default class CustomersService extends BaseService { const totpCode = await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60 * 1000), reason); if (!totpCode) return null; // 5: Send the SMS code to the customer - if(this.variables.ENV !== 'dev') await this.sendSmsCodeToCustomer(totpPin, customer); + // if(this.variables.ENV !== 'dev') + await this.sendSmsCodeToCustomer(totpPin, customer); return { customer, totpCode: TotpCodesResource.hydrate({ @@ -162,7 +163,8 @@ export default class CustomersService extends BaseService { await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60000), TotpCodesReasons.RESET_PASSWORD); // 5: Send the SMS code to the customer - if(this.variables.ENV !== 'dev') await this.sendSmsCodeToCustomer(totpPin, customer); + // if(this.variables.ENV !== 'dev') + await this.sendSmsCodeToCustomer(totpPin, customer); return customer; } @@ -292,7 +294,8 @@ export default class CustomersService extends BaseService { const totpCode = await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60 * 1000), totpCodeToResend.reason!, true); // 7: Send the SMS code to the customer - if(this.variables.ENV !== 'dev') await this.sendSmsCodeToCustomer(totpPin, customer); + // if(this.variables.ENV !== 'dev') + await this.sendSmsCodeToCustomer(totpPin, customer); return { customer, totpCode }; } @@ -356,17 +359,19 @@ export default class CustomersService extends BaseService { private async sendSmsCodeToCustomer(totpPin: number, customer: Customer) { const message = "Votre code de vérification LEcoffre.io est : " + totpPin.toString(); + // Sélectionnez le fournisseur de SMS en fonction de la variable d'environnement - const selectedProvider = this.variables.SMS_PROVIDER === "OVH" ? this.ovhService : this.smsFactorService; + //const selectedProvider = this.variables.SMS_PROVIDER === "OVH" ? this.ovhService : this.smsFactorService; // Envoi du SMS - if (!customer.contact?.phone_number) return; - let success = await selectedProvider.sendSms(customer.contact?.phone_number, message); + if (!customer.contact?.cell_phone_number) return; + let success = await this.ovhService.sendSms(customer.contact?.cell_phone_number, message); + // Si l'envoi échoue, basculez automatiquement sur le second fournisseur if (!success) { - const alternateProvider = this.variables.SMS_PROVIDER === "OVH" ? this.smsFactorService : this.ovhService; - success = await alternateProvider.sendSms(customer.contact?.phone_number, message); + //const alternateProvider = this.variables.SMS_PROVIDER === "OVH" ? this.smsFactorService : this.ovhService; + await this.smsFactorService.sendSms(customer.contact?.cell_phone_number, message); } } From d7e1a051bc20a8bd7c4b3074fbd5b18b55c7f281 Mon Sep 17 00:00:00 2001 From: Vins Date: Tue, 5 Dec 2023 11:19:26 +0100 Subject: [PATCH 46/46] fixed migrations --- devops/stg.values.yaml | 2 +- src/common/databases/migrations/20230711134012_/migration.sql | 0 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 src/common/databases/migrations/20230711134012_/migration.sql diff --git a/devops/stg.values.yaml b/devops/stg.values.yaml index e2ce7a4b..74a13fa8 100644 --- a/devops/stg.values.yaml +++ b/devops/stg.values.yaml @@ -5,7 +5,7 @@ scwSecretKey: AgChoEnPitXp4Ny/rVMEcevaWKNVpyj2cJYAcq+yFqKwVwnLB+ffDvwqz9XBHu+6d4 lecoffreBack: serviceAccountName: lecoffre-back-sa envSecrets: stg-env - command: "'sh', '-c', 'export $(xargs