TotpCodes in tables instead of in customer

This commit is contained in:
Maxime Lalo 2023-11-27 17:36:46 +01:00
parent cdbb3e3257
commit b6e1b2ff62
6 changed files with 192 additions and 59 deletions

View File

@ -159,12 +159,15 @@ export default class AuthController extends ApiController {
} }
try { try {
const customer = await this.customerService.verifyTotpCode(totpCode, email); const code = await this.customerService.verifyTotpCode(totpCode, email);
if (!customer) { if (!code) {
this.httpNotFoundRequest(response, "Customer not found"); this.httpNotFoundRequest(response, "Customer not found");
return; return;
} }
this.httpSuccess(response, { validCode: true, firstConnection: customer.password === null }); this.httpSuccess(response, {
validCode: true,
reason: code.reason,
});
} catch (error) { } catch (error) {
if (error instanceof InvalidTotpCodeError || error instanceof TotpCodeExpiredError) { if (error instanceof InvalidTotpCodeError || error instanceof TotpCodeExpiredError) {
this.httpUnauthorized(response, error.message); this.httpUnauthorized(response, error.message);

View File

@ -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;

View File

@ -102,10 +102,7 @@ model Customers {
office_folders OfficeFolders[] @relation("OfficeFolderHasCustomers") office_folders OfficeFolders[] @relation("OfficeFolderHasCustomers")
documents Documents[] documents Documents[]
password String? @db.VarChar(255) password String? @db.VarChar(255)
totpCode String? @db.VarChar(255) totpCodes TotpCodes[]
totpCodeExpire DateTime? @default(now())
@@map("customers") @@map("customers")
} }
@ -345,6 +342,23 @@ model Votes {
@@map("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 { enum ECivility {
MALE MALE
FEMALE FEMALE

View File

@ -5,7 +5,6 @@ import { Customers, ECivility, ECustomerStatus, Prisma } from "@prisma/client";
import { Customer } from "le-coffre-resources/dist/SuperAdmin"; import { Customer } from "le-coffre-resources/dist/SuperAdmin";
type IExcludedCustomerVars = { type IExcludedCustomerVars = {
totpCode?: string | null;
totpCodeExpire?: Date | null; totpCodeExpire?: Date | null;
password?: string; password?: string;
}; };
@ -36,7 +35,7 @@ export default class CustomersRepository extends BaseRepository {
public async findOne(query: Prisma.CustomersFindFirstArgs) { public async findOne(query: Prisma.CustomersFindFirstArgs) {
query.take = Math.min(query.take || this.defaultFetchRows, this.maxFetchRows); query.take = Math.min(query.take || this.defaultFetchRows, this.maxFetchRows);
if (!query.include) return this.model.findFirst({ ...query, include: { contact: true } }); 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: {}, address: {},
}, },
}, },
totpCode: excludedVars && excludedVars.totpCode,
totpCodeExpire: excludedVars && excludedVars.totpCodeExpire,
password: excludedVars && excludedVars.password, password: excludedVars && excludedVars.password,
}, },
}; };

View File

@ -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<TotpCodes> {
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 });
}
}

View File

@ -1,7 +1,9 @@
import { Customers, Prisma } from "@prisma/client"; import { Customers, Prisma } from "@prisma/client";
import CustomersRepository from "@Repositories/CustomersRepository"; import CustomersRepository from "@Repositories/CustomersRepository";
import TotpCodesRepository from "@Repositories/TotpCodesRepository";
import BaseService from "@Services/BaseService"; import BaseService from "@Services/BaseService";
import AuthService from "@Services/common/AuthService/AuthService"; 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 { Customer } from "le-coffre-resources/dist/Notary";
import { Service } from "typedi"; import { Service } from "typedi";
@ -42,7 +44,11 @@ export class PasswordAlreadySetError extends Error {
} }
@Service() @Service()
export default class CustomersService extends BaseService { export default class CustomersService extends BaseService {
constructor(private customerRepository: CustomersRepository, private authService: AuthService) { constructor(
private customerRepository: CustomersRepository,
private authService: AuthService,
private totpCodesRepository: TotpCodesRepository,
) {
super(); super();
} }
@ -65,23 +71,33 @@ export default class CustomersService extends BaseService {
/** /**
* @description : Send SMS to verify the email of a customer (2FA) * @description : Send SMS to verify the email of a customer (2FA)
* 1: Check if the customer exists * 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 * 3: Generate a new SMS code
* 4: Save the SMS code in database * 4: Save the SMS code in database
* 5: Send the SMS code to the customer * 5: Send the SMS code to the customer
*/ */
public async verifyEmail2FASms(email: string): Promise<Customer | null> { public async verifyEmail2FASms(email: string): Promise<Customer | null> {
// 1: Check if the customer exists
const customer = await this.getByEmail(email); const customer = await this.getByEmail(email);
if (!customer) return null; if (!customer) return null;
const now = new Date().getTime(); 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>(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(); 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); await this.sendSmsCodeToCustomer(totpPin, customer);
return 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 * @description : Set the password of a customer when it's the first time they connect
* 1: Check if the customer exists * 1: Check if the customer exists
* 2: Check if the password is already set * 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 * 4: Check if the SMS code is valid
* 5: Hash the password * 5: Hash the password
* 6: Set the password in database * 6: Set the password in database and return the result of the update
* 7: Returns the customer
* @param email * @param email
* @param totpCode * @param totpCode
* @param password * @param password
@ -108,28 +123,28 @@ export default class CustomersService extends BaseService {
// 2: Check if the password is already set // 2: Check if the password is already set
if (customer.password) throw new PasswordAlreadySetError(); if (customer.password) throw new PasswordAlreadySetError();
// 3: Check if the SMS code is existing and is not expired const customerHydrated = Customer.hydrate<Customer>(customer);
if (!customer.totpCode || !customer.totpCodeExpire || new Date().getTime() > customer.totpCodeExpire.getTime()) // 3: Check if a totp code is existing and is not expired in the array
throw new TotpCodeExpiredError(); 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 // 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 // 5: Hash the password
const hashedPassword = await this.authService.hashPassword(password); const hashedPassword = await this.authService.hashPassword(password);
// 6: Set the password in database // 6: Set the password in database and return the result of the update
await this.setPassword(customer, hashedPassword); return await this.setPassword(customer, hashedPassword);
// 7: Returns the customer
return customer;
} }
/** /**
* *
* @description : Login a customer * @description : Login a customer
* 1: Check if the customer exists * 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 * 3: Check if the SMS code is valid
* 4: Check if the user has a password or it's their first login * 4: Check if the user has a password or it's their first login
* 5: Check if the password is valid * 5: Check if the password is valid
@ -143,13 +158,16 @@ export default class CustomersService extends BaseService {
// 1: Check if the customer exists // 1: Check if the customer exists
const customer = await this.getByEmail(email); const customer = await this.getByEmail(email);
if (!customer) return null; if (!customer) return null;
const customerHydrated = Customer.hydrate<Customer>(customer);
// 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
if (!customer.totpCode || !customer.totpCodeExpire || new Date().getTime() > customer.totpCodeExpire.getTime()) const validTotpCode = customerHydrated.totpCodes?.find((totpCode) => {
throw new TotpCodeExpiredError(); return totpCode.expire_at && new Date().getTime() < totpCode.expire_at.getTime();
});
if (!validTotpCode) throw new TotpCodeExpiredError();
// 3: Check if the SMS code is valid // 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 // 4: Check if the user has a password or it's their first login
if (!customer.password) throw new NotRegisteredCustomerError(); 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); const isPasswordValid = await this.authService.comparePassword(password, customer.password);
if (!isPasswordValid) throw new InvalidPasswordError(); if (!isPasswordValid) throw new InvalidPasswordError();
await this.customerRepository.update( return await this.customerRepository.update(
customer.uid as string, customer.uid as string,
Customer.hydrate<Customer>({ Customer.hydrate<Customer>({
...customer, ...customer,
}), }),
{
totpCode: null,
totpCodeExpire: null,
},
); );
// 6: Return the customer
return customer;
} }
/** /**
@ -183,8 +195,6 @@ export default class CustomersService extends BaseService {
...customer, ...customer,
}), }),
{ {
totpCode: null,
totpCodeExpire: null,
password, password,
}, },
); );
@ -199,6 +209,7 @@ export default class CustomersService extends BaseService {
}, },
include: { include: {
contact: true, contact: true,
totpCodes: true,
}, },
}); });
} }
@ -206,16 +217,24 @@ export default class CustomersService extends BaseService {
/** /**
* @description : Saves a TotpPin in database * @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<TotpCodes>({
reason,
customer_uid: customer.uid,
customer: Customer.hydrate<Customer>(customer),
}),
{
code: totpPin.toString(),
expire_at: expireAt,
},
);
return await this.customerRepository.update( return await this.customerRepository.update(
customer.uid as string, customer.uid as string,
Customer.hydrate<Customer>({ Customer.hydrate<Customer>({
...customer, ...customer,
}), }),
{
totpCode: totpPin.toString(),
totpCodeExpire: expireAt,
},
); );
} }
@ -227,24 +246,33 @@ export default class CustomersService extends BaseService {
console.log(totpPin); console.log(totpPin);
} }
public async verifyTotpCode(totpCode: string, email: string): Promise<Customer | null> { /**
// 1: Check if the customer exists *
// 2: Check if the SMS code is existing and is not expired * 1: Check if the customer exists
// 3: Check if the SMS code is valid * 2: Check if a totp code is existing and is not expired in the array
// 4: Return the customer * 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<TotpCodes | null> {
// 1: Check if the customer exists // 1: Check if the customer exists
const customer = await this.getByEmail(email); const customer = await this.getByEmail(email);
if (!customer) return null; if (!customer) return null;
// 2: Check if the SMS code is existing and is not expired const customerHydrated = Customer.hydrate<Customer>(customer);
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 // 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 // 4: Return the customer
return customer; return validTotpCode;
} }
} }