From 220a77e06373eb4a75dc4f469ecc1b1e6f4097b2 Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Thu, 23 Nov 2023 15:54:04 +0100 Subject: [PATCH 01/24] :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/24] :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/24] :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/24] :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/24] :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/24] :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/24] :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/24] :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/24] :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/24] :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/24] :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/24] :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/24] :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/24] :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/24] :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/24] 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/24] 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/24] :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/24] :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/24] :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/24] :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/24] :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/24] 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/24] 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) {