diff --git a/src/app/api/customer/AuthController.ts b/src/app/api/customer/AuthController.ts index 631dce4c..f4067dd9 100644 --- a/src/app/api/customer/AuthController.ts +++ b/src/app/api/customer/AuthController.ts @@ -30,17 +30,16 @@ export default class AuthController extends ApiController { } try { - const customer = await this.customerService.verifyEmail2FASms(email); - if (!customer) { + const res = await this.customerService.verifyEmail2FASms(email); + if (!res) { this.httpNotFoundRequest(response, "Customer not found"); return; } - this.httpSuccess(response, { partialPhoneNumber: customer.contact?.cell_phone_number.replace(/\s/g, "").slice(-4) }); + this.httpSuccess(response, { + partialPhoneNumber: res.customer.contact?.cell_phone_number.replace(/\s/g, "").slice(-4), + totpCodeUid: res.totpCode.uid, + }); } catch (error) { - if (error instanceof SmsNotExpiredError) { - this.httpTooEarlyRequest(response, error.message); - return; - } console.log(error); this.httpInternalError(response); } @@ -207,18 +206,27 @@ export default class AuthController extends ApiController { @Post("/api/v1/customer/auth/send-another-code") protected async sendAnotherCode(req: Request, response: Response) { const email = req.body["email"]; + const totpCodeUid = req.body["totpCodeUid"]; if (!email) { this.httpBadRequest(response, "email is required"); return; } + if (!totpCodeUid) { + this.httpBadRequest(response, "totpCodeUid is required"); + return; + } + try { - const customer = await this.customerService.askAnotherCode(email); - if (!customer) { + const res = await this.customerService.askAnotherCode(email, totpCodeUid); + if (!res) { this.httpNotFoundRequest(response, "Customer not found"); return; } - this.httpSuccess(response, { partialPhoneNumber: customer.contact?.cell_phone_number.replace(/\s/g, "").slice(-4) }); + this.httpSuccess(response, { + partialPhoneNumber: res.customer.contact?.cell_phone_number.replace(/\s/g, "").slice(-4), + totpCodeUid: res.totpCode.uid, + }); } catch (error) { if (error instanceof TooSoonForNewCode || error instanceof TotpCodeExpiredError) { this.httpUnauthorized(response, error.message); diff --git a/src/app/api/idnot/UserController.ts b/src/app/api/idnot/UserController.ts index cac75ffc..60d13059 100644 --- a/src/app/api/idnot/UserController.ts +++ b/src/app/api/idnot/UserController.ts @@ -22,7 +22,7 @@ export default class UserController extends ApiController { protected async getUserInfosFromIdnot(req: Request, response: Response) { try { const code = req.params["code"]; - if (!code) throw new Error("code is required"); + if (!code) throw new Error("code is required"); const idNotToken = await this.idNotService.getIdNotToken(code); if(!idNotToken) { diff --git a/src/common/repositories/TotpCodesRepository.ts b/src/common/repositories/TotpCodesRepository.ts index 8d9d9c33..8d1ebf48 100644 --- a/src/common/repositories/TotpCodesRepository.ts +++ b/src/common/repositories/TotpCodesRepository.ts @@ -67,4 +67,11 @@ export default class TotpCodesRepository extends BaseRepository { }, }); } + + /** + * Delete many totp codes + */ + public async deleteMany(query: Prisma.TotpCodesDeleteManyArgs) { + return this.model.deleteMany({ ...query }); + } } diff --git a/src/services/common/CronService/CronService.ts b/src/services/common/CronService/CronService.ts index 4b46e962..94584f54 100644 --- a/src/services/common/CronService/CronService.ts +++ b/src/services/common/CronService/CronService.ts @@ -6,6 +6,7 @@ import IdNotService from "../IdNotService/IdNotService"; import { PrismaClient } from "@prisma/client"; import NotificationBuilder from "@Common/notifications/NotificationBuilder"; import EmailBuilder from "@Common/emails/EmailBuilder"; +import TotpService from "@Services/customer/TotpService/TotpService"; // import { PrismaClient } from "@prisma/client"; @Service() @@ -16,6 +17,7 @@ export default class CronService { private idNotService: IdNotService, private notificationBuilder: NotificationBuilder, private emailBuilder: EmailBuilder, + private totpService: TotpService, ) {} public async sendMails() { @@ -86,8 +88,8 @@ export default class CronService { // Once a day at midnight try { const prisma = new PrismaClient(); - const expiringDocuments15Days: [{uid: string, expiration_date: Date}] = await prisma.$queryRaw - `SELECT distinct o.uid, f.created_at as expiration_date + const expiringDocuments15Days: [{ uid: string; expiration_date: Date }] = + await prisma.$queryRaw`SELECT distinct o.uid, f.created_at as expiration_date FROM documents d JOIN files f ON d.uid=f.document_uid JOIN office_folders o ON o.uid=d.folder_uid @@ -95,11 +97,24 @@ export default class CronService { AND d.document_status = 'DEPOSITED' AND current_date = (DATE(f.created_at) + interval '3 months' - interval '2 days');`; - expiringDocuments15Days.forEach(expiringFolder => { + expiringDocuments15Days.forEach((expiringFolder) => { this.notificationBuilder.sendDocumentExpiringSoonNotification(expiringFolder.uid, expiringFolder.expiration_date); }); + } catch (e) { + console.error(e); + } + }); + // Start job + if (!cronJob.running) { + cronJob.start(); + } + } - + public async cleanExpiredTotpCodes() { + const cronJob = new CronJob("0 0 * * *", async () => { + // Once a day at midnight + try { + await this.totpService.cleanExpiredTotpCodes(); } catch (e) { console.error(e); } diff --git a/src/services/common/IdNotService/IdNotService.ts b/src/services/common/IdNotService/IdNotService.ts index 7b403836..dd15912e 100644 --- a/src/services/common/IdNotService/IdNotService.ts +++ b/src/services/common/IdNotService/IdNotService.ts @@ -117,11 +117,14 @@ export default class IdNotService extends BaseService { const query = new URLSearchParams({ client_id: this.variables.IDNOT_CLIENT_ID, client_secret: this.variables.IDNOT_CLIENT_SECRET, - redirect_uri: `${this.variables.APP_HOST}/authorized-client`, + redirect_uri: this.variables.IDNOT_REDIRECT_URL, code: code, grant_type: "authorization_code", - }); + }); + const token = await fetch(this.variables.IDNOT_BASE_URL + this.variables.IDNOT_CONNEXION_URL + "?" + query, { method: "POST" }); + if(token.status !== 200) console.error(await token.text()); + const decodedToken = (await token.json()) as IIdNotToken; const decodedIdToken = jwt.decode(decodedToken.id_token) as IdNotJwtPayload; diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index 0a81ee35..cf03f0c8 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -88,7 +88,7 @@ export default class CustomersService extends BaseService { * 4: Save the SMS code in database * 5: Send the SMS code to the customer */ - public async verifyEmail2FASms(email: string): Promise { + public async verifyEmail2FASms(email: string): Promise<{ customer: Customer; totpCode: TotpCodes } | null> { // 1: Check if the customer exists const customer = await this.getByEmail(email); if (!customer) return null; @@ -100,18 +100,18 @@ export default class CustomersService extends BaseService { const validTotpCode = customerHydrated.totpCodes?.find((totpCode) => { return totpCode.expire_at && totpCode.expire_at.getTime() > now; }); - if (validTotpCode) throw new SmsNotExpiredError(); + if (validTotpCode) return { customer, totpCode: validTotpCode }; // 3: Generate a new SMS code const totpPin = this.generateTotp(); const reason = customer.password ? TotpCodesReasons.LOGIN : TotpCodesReasons.FIRST_LOGIN; // 4: Save the SMS code in database - await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60000), reason); + const totpCode = await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60000), reason); // 5: Send the SMS code to the customer await this.sendSmsCodeToCustomer(totpPin, customer); - return customer; + return { customer, totpCode }; } /** @@ -258,7 +258,7 @@ export default class CustomersService extends BaseService { ); } - public async askAnotherCode(email: string): Promise { + public async askAnotherCode(email: string, totpCodeUid: string): Promise<{ customer: Customer; totpCode: TotpCodes } | null> { // 1: Check if the customer exists const customer = await this.getByEmail(email); if (!customer) return null; @@ -267,26 +267,26 @@ export default class CustomersService extends BaseService { const customerHydrated = Customer.hydrate(customer); // 2: Get last code sent - const lastCode = customerHydrated.totpCodes?.find((totpCode) => { - return totpCode.expire_at && totpCode.expire_at.getTime() > now; + const totpCodeToResend = customerHydrated.totpCodes?.find((totpCode) => { + return totpCode.uid === totpCodeUid && totpCode.expire_at && totpCode.expire_at.getTime() > now; }); - if (!lastCode) throw new TotpCodeExpiredError(); + if (!totpCodeToResend) throw new TotpCodeExpiredError(); // 3: Check if it was created more than 30 seconds ago - if (lastCode.created_at && lastCode.created_at.getTime() > now - 30000) throw new TooSoonForNewCode(); + if (totpCodeToResend.created_at && totpCodeToResend.created_at.getTime() > now - 30000) throw new TooSoonForNewCode(); // 4: Generate a new SMS code const totpPin = this.generateTotp(); // 5: Disable the old code - await this.totpCodesRepository.disable(lastCode); + await this.totpCodesRepository.disable(totpCodeToResend); // 6: Save the SMS code in database - await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60000), lastCode.reason!); + const totpCode = await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60000), totpCodeToResend.reason!); // 7: Send the SMS code to the customer await this.sendSmsCodeToCustomer(totpPin, customer); - return customer; + return { customer, totpCode }; } /** diff --git a/src/services/customer/TotpService/TotpService.ts b/src/services/customer/TotpService/TotpService.ts new file mode 100644 index 00000000..ed8a9b20 --- /dev/null +++ b/src/services/customer/TotpService/TotpService.ts @@ -0,0 +1,51 @@ +import { Prisma, TotpCodes } from "@prisma/client"; +import TotpCodesRepository from "@Repositories/TotpCodesRepository"; +import BaseService from "@Services/BaseService"; +import { Service } from "typedi"; + +@Service() +export default class TotpService extends BaseService { + constructor(private totpCodesRepository: TotpCodesRepository) { + super(); + } + + /** + * @description : Get all totp codes + * @throws {Error} If totp codes cannot be get + */ + public async get(query: Prisma.TotpCodesFindManyArgs): Promise { + return this.totpCodesRepository.findMany(query); + } + + /** + * @description : Get one totp code + * @throws {Error} If totp code cannot be get + */ + public async getOne(query: Prisma.TotpCodesFindFirstArgs): Promise { + return this.totpCodesRepository.findOne(query); + } + + /** + * @description : Delete many totp codes + * @throws {Error} If totp code cannot be deleted + */ + public async deleteMany(query: Prisma.TotpCodesDeleteManyArgs): Promise { + return this.totpCodesRepository.deleteMany(query); + } + + /** + * @description : Delete every totp code expired for more than 30 days + * @throws {Error} If totp codes cannot be deleted + */ + public async cleanExpiredTotpCodes() { + const query: Prisma.TotpCodesDeleteManyArgs = { + where: { + expire_at: { + lte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), + }, + }, + }; + + return this.totpCodesRepository.deleteMany(query); + } +}