Merge branch 'dev' into sendSms
This commit is contained in:
commit
20478738ca
@ -56,7 +56,7 @@
|
|||||||
"file-type-checker": "^1.0.8",
|
"file-type-checker": "^1.0.8",
|
||||||
"fp-ts": "^2.16.1",
|
"fp-ts": "^2.16.1",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
"le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.98",
|
"le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.99",
|
||||||
"module-alias": "^2.2.2",
|
"module-alias": "^2.2.2",
|
||||||
"monocle-ts": "^2.3.13",
|
"monocle-ts": "^2.3.13",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
@ -8,6 +8,7 @@ import CustomersService, {
|
|||||||
NotRegisteredCustomerError,
|
NotRegisteredCustomerError,
|
||||||
PasswordAlreadySetError,
|
PasswordAlreadySetError,
|
||||||
SmsNotExpiredError,
|
SmsNotExpiredError,
|
||||||
|
TooSoonForNewCode,
|
||||||
TotpCodeExpiredError,
|
TotpCodeExpiredError,
|
||||||
} from "@Services/customer/CustomersService/CustomersService";
|
} from "@Services/customer/CustomersService/CustomersService";
|
||||||
import AuthService from "@Services/common/AuthService/AuthService";
|
import AuthService from "@Services/common/AuthService/AuthService";
|
||||||
@ -45,6 +46,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")
|
@Post("/api/v1/customer/auth/login")
|
||||||
protected async login(req: Request, response: Response) {
|
protected async login(req: Request, response: Response) {
|
||||||
const email = req.body["email"];
|
const email = req.body["email"];
|
||||||
@ -177,4 +203,29 @@ export default class AuthController extends ApiController {
|
|||||||
this.httpInternalError(response);
|
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 TooSoonForNewCode || error instanceof TotpCodeExpiredError) {
|
||||||
|
this.httpUnauthorized(response, error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(error);
|
||||||
|
this.httpInternalError(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,8 @@ CREATE TABLE "totp_codes" (
|
|||||||
"code" VARCHAR(255) NOT NULL,
|
"code" VARCHAR(255) NOT NULL,
|
||||||
"reason" "TotpCodesReasons" NOT NULL DEFAULT 'LOGIN',
|
"reason" "TotpCodesReasons" NOT NULL DEFAULT 'LOGIN',
|
||||||
"expire_at" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP,
|
"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")
|
CONSTRAINT "totp_codes_pkey" PRIMARY KEY ("uid")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -349,7 +349,8 @@ model TotpCodes {
|
|||||||
code String @db.VarChar(255)
|
code String @db.VarChar(255)
|
||||||
reason TotpCodesReasons @default(LOGIN)
|
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")
|
@@map("totp_codes")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,11 +4,6 @@ import { Service } from "typedi";
|
|||||||
import { Prisma, TotpCodes } from "@prisma/client";
|
import { Prisma, TotpCodes } from "@prisma/client";
|
||||||
import { TotpCodes as TotpCode } from "le-coffre-resources/dist/Customer";
|
import { TotpCodes as TotpCode } from "le-coffre-resources/dist/Customer";
|
||||||
|
|
||||||
type IExcludedTotpCodesVars = {
|
|
||||||
code?: string;
|
|
||||||
expire_at?: Date;
|
|
||||||
};
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class TotpCodesRepository extends BaseRepository {
|
export default class TotpCodesRepository extends BaseRepository {
|
||||||
constructor(private database: Database) {
|
constructor(private database: Database) {
|
||||||
@ -40,22 +35,36 @@ export default class TotpCodesRepository extends BaseRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description : Create a customer
|
* @description : Create a totp code
|
||||||
*/
|
*/
|
||||||
public async create(totpCode: TotpCode, excludedVars: IExcludedTotpCodesVars): Promise<TotpCodes> {
|
public async create(totpCode: TotpCode): Promise<TotpCodes> {
|
||||||
const createArgs: Prisma.TotpCodesCreateArgs = {
|
const createArgs: Prisma.TotpCodesCreateArgs = {
|
||||||
data: {
|
data: {
|
||||||
code: excludedVars.code!,
|
code: totpCode.code!,
|
||||||
reason: totpCode.reason!,
|
reason: totpCode.reason!,
|
||||||
customer: {
|
customer: {
|
||||||
connect: {
|
connect: {
|
||||||
uid: totpCode.customer_uid!,
|
uid: totpCode.customer_uid!,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expire_at: excludedVars.expire_at!,
|
expire_at: totpCode.expire_at!,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.model.create({ ...createArgs });
|
return this.model.create({ ...createArgs });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable a totp code
|
||||||
|
*/
|
||||||
|
public async disable(totpCode: TotpCode): Promise<TotpCodes> {
|
||||||
|
return this.model.update({
|
||||||
|
where: {
|
||||||
|
uid: totpCode.uid!,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
expire_at: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,12 @@ export class PasswordAlreadySetError extends Error {
|
|||||||
super("Password already set");
|
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()
|
@Service()
|
||||||
export default class CustomersService extends BaseService {
|
export default class CustomersService extends BaseService {
|
||||||
constructor(
|
constructor(
|
||||||
@ -106,14 +112,61 @@ export default class CustomersService extends BaseService {
|
|||||||
return customer;
|
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<Customer | null> {
|
||||||
|
// 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>(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 && 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();
|
||||||
|
|
||||||
|
// 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
|
* @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 a totp code is existing and is not expired in the array
|
||||||
* 3: 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 SMS code is valid
|
* 4: Check the totpcode reason is valid
|
||||||
* 5: Hash the password
|
* 5: Disable the totp code used
|
||||||
* 6: Set the password in database and return the result of the update
|
* 6: Hash the password
|
||||||
|
* 7: Set the password in database and return the result of the update
|
||||||
* @param email
|
* @param email
|
||||||
* @param totpCode
|
* @param totpCode
|
||||||
* @param password
|
* @param password
|
||||||
@ -124,23 +177,33 @@ export default class CustomersService extends BaseService {
|
|||||||
const customer = await this.getByEmail(email);
|
const customer = await this.getByEmail(email);
|
||||||
if (!customer) return null;
|
if (!customer) return null;
|
||||||
|
|
||||||
// 2: Check if the password is already set
|
|
||||||
if (customer.password) throw new PasswordAlreadySetError();
|
|
||||||
|
|
||||||
const customerHydrated = Customer.hydrate<Customer>(customer);
|
const customerHydrated = Customer.hydrate<Customer>(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) => {
|
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();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!validTotpCode) throw new TotpCodeExpiredError();
|
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();
|
if (validTotpCode.code !== totpCode) throw new InvalidTotpCodeError();
|
||||||
|
|
||||||
// 5: Hash the password
|
// 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);
|
||||||
|
|
||||||
|
// 6: Hash the password
|
||||||
const hashedPassword = await this.authService.hashPassword(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);
|
return await this.setPassword(customer, hashedPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,7 +215,8 @@ export default class CustomersService extends BaseService {
|
|||||||
* 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
|
||||||
* 6: Return the customer
|
* 6: Disable the totp code used
|
||||||
|
* 7: Return the customer
|
||||||
* @param email
|
* @param email
|
||||||
* @param totpCode
|
* @param totpCode
|
||||||
* @param password
|
* @param password
|
||||||
@ -166,7 +230,7 @@ export default class CustomersService extends BaseService {
|
|||||||
|
|
||||||
// 2: 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) => {
|
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();
|
if (!validTotpCode) throw new TotpCodeExpiredError();
|
||||||
|
|
||||||
@ -180,6 +244,10 @@ 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();
|
||||||
|
|
||||||
|
// 6: Disable the totp code used
|
||||||
|
await this.totpCodesRepository.disable(validTotpCode);
|
||||||
|
|
||||||
|
// 7: Return the customer
|
||||||
return await this.customerRepository.update(
|
return await this.customerRepository.update(
|
||||||
customer.uid as string,
|
customer.uid as string,
|
||||||
Customer.hydrate<Customer>({
|
Customer.hydrate<Customer>({
|
||||||
@ -188,6 +256,37 @@ export default class CustomersService extends BaseService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async askAnotherCode(email: string): Promise<Customer | null> {
|
||||||
|
// 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>(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 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();
|
||||||
|
|
||||||
|
// 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
|
* @description : Set password for a customer
|
||||||
* @throws {Error} If customer cannot be updated
|
* @throws {Error} If customer cannot be updated
|
||||||
@ -228,11 +327,10 @@ export default class CustomersService extends BaseService {
|
|||||||
reason,
|
reason,
|
||||||
customer_uid: customer.uid,
|
customer_uid: customer.uid,
|
||||||
customer: Customer.hydrate<Customer>(customer),
|
customer: Customer.hydrate<Customer>(customer),
|
||||||
}),
|
created_at: new Date(),
|
||||||
{
|
|
||||||
code: totpPin.toString(),
|
code: totpPin.toString(),
|
||||||
expire_at: expireAt,
|
expire_at: expireAt,
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
return await this.customerRepository.update(
|
return await this.customerRepository.update(
|
||||||
customer.uid as string,
|
customer.uid as string,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user