Merge branch 'dev' into staging
This commit is contained in:
commit
f76856813d
@ -30,17 +30,16 @@ export default class AuthController extends ApiController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const customer = await this.customerService.verifyEmail2FASms(email);
|
const res = await this.customerService.verifyEmail2FASms(email);
|
||||||
if (!customer) {
|
if (!res) {
|
||||||
this.httpNotFoundRequest(response, "Customer not found");
|
this.httpNotFoundRequest(response, "Customer not found");
|
||||||
return;
|
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) {
|
} catch (error) {
|
||||||
if (error instanceof SmsNotExpiredError) {
|
|
||||||
this.httpTooEarlyRequest(response, error.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log(error);
|
console.log(error);
|
||||||
this.httpInternalError(response);
|
this.httpInternalError(response);
|
||||||
}
|
}
|
||||||
@ -207,18 +206,27 @@ export default class AuthController extends ApiController {
|
|||||||
@Post("/api/v1/customer/auth/send-another-code")
|
@Post("/api/v1/customer/auth/send-another-code")
|
||||||
protected async sendAnotherCode(req: Request, response: Response) {
|
protected async sendAnotherCode(req: Request, response: Response) {
|
||||||
const email = req.body["email"];
|
const email = req.body["email"];
|
||||||
|
const totpCodeUid = req.body["totpCodeUid"];
|
||||||
if (!email) {
|
if (!email) {
|
||||||
this.httpBadRequest(response, "email is required");
|
this.httpBadRequest(response, "email is required");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!totpCodeUid) {
|
||||||
|
this.httpBadRequest(response, "totpCodeUid is required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const customer = await this.customerService.askAnotherCode(email);
|
const res = await this.customerService.askAnotherCode(email, totpCodeUid);
|
||||||
if (!customer) {
|
if (!res) {
|
||||||
this.httpNotFoundRequest(response, "Customer not found");
|
this.httpNotFoundRequest(response, "Customer not found");
|
||||||
return;
|
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) {
|
} catch (error) {
|
||||||
if (error instanceof TooSoonForNewCode || error instanceof TotpCodeExpiredError) {
|
if (error instanceof TooSoonForNewCode || error instanceof TotpCodeExpiredError) {
|
||||||
this.httpUnauthorized(response, error.message);
|
this.httpUnauthorized(response, error.message);
|
||||||
|
@ -22,7 +22,7 @@ export default class UserController extends ApiController {
|
|||||||
protected async getUserInfosFromIdnot(req: Request, response: Response) {
|
protected async getUserInfosFromIdnot(req: Request, response: Response) {
|
||||||
try {
|
try {
|
||||||
const code = req.params["code"];
|
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);
|
const idNotToken = await this.idNotService.getIdNotToken(code);
|
||||||
if(!idNotToken) {
|
if(!idNotToken) {
|
||||||
|
@ -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 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import IdNotService from "../IdNotService/IdNotService";
|
|||||||
import { PrismaClient } from "@prisma/client";
|
import { PrismaClient } from "@prisma/client";
|
||||||
import NotificationBuilder from "@Common/notifications/NotificationBuilder";
|
import NotificationBuilder from "@Common/notifications/NotificationBuilder";
|
||||||
import EmailBuilder from "@Common/emails/EmailBuilder";
|
import EmailBuilder from "@Common/emails/EmailBuilder";
|
||||||
|
import TotpService from "@Services/customer/TotpService/TotpService";
|
||||||
// import { PrismaClient } from "@prisma/client";
|
// import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
@ -16,6 +17,7 @@ export default class CronService {
|
|||||||
private idNotService: IdNotService,
|
private idNotService: IdNotService,
|
||||||
private notificationBuilder: NotificationBuilder,
|
private notificationBuilder: NotificationBuilder,
|
||||||
private emailBuilder: EmailBuilder,
|
private emailBuilder: EmailBuilder,
|
||||||
|
private totpService: TotpService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async sendMails() {
|
public async sendMails() {
|
||||||
@ -86,8 +88,8 @@ export default class CronService {
|
|||||||
// Once a day at midnight
|
// Once a day at midnight
|
||||||
try {
|
try {
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
const expiringDocuments15Days: [{uid: string, expiration_date: Date}] = await prisma.$queryRaw
|
const expiringDocuments15Days: [{ uid: string; expiration_date: Date }] =
|
||||||
`SELECT distinct o.uid, f.created_at as expiration_date
|
await prisma.$queryRaw`SELECT distinct o.uid, f.created_at as expiration_date
|
||||||
FROM documents d
|
FROM documents d
|
||||||
JOIN files f ON d.uid=f.document_uid
|
JOIN files f ON d.uid=f.document_uid
|
||||||
JOIN office_folders o ON o.uid=d.folder_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 d.document_status = 'DEPOSITED'
|
||||||
AND current_date = (DATE(f.created_at) + interval '3 months' - interval '2 days');`;
|
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);
|
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) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
@ -117,11 +117,14 @@ export default class IdNotService extends BaseService {
|
|||||||
const query = new URLSearchParams({
|
const query = new URLSearchParams({
|
||||||
client_id: this.variables.IDNOT_CLIENT_ID,
|
client_id: this.variables.IDNOT_CLIENT_ID,
|
||||||
client_secret: this.variables.IDNOT_CLIENT_SECRET,
|
client_secret: this.variables.IDNOT_CLIENT_SECRET,
|
||||||
redirect_uri: `${this.variables.APP_HOST}/authorized-client`,
|
redirect_uri: this.variables.IDNOT_REDIRECT_URL,
|
||||||
code: code,
|
code: code,
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
});
|
});
|
||||||
|
|
||||||
const token = await fetch(this.variables.IDNOT_BASE_URL + this.variables.IDNOT_CONNEXION_URL + "?" + query, { method: "POST" });
|
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 decodedToken = (await token.json()) as IIdNotToken;
|
||||||
const decodedIdToken = jwt.decode(decodedToken.id_token) as IdNotJwtPayload;
|
const decodedIdToken = jwt.decode(decodedToken.id_token) as IdNotJwtPayload;
|
||||||
|
|
||||||
|
@ -88,7 +88,7 @@ export default class CustomersService extends BaseService {
|
|||||||
* 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: Customer; totpCode: 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;
|
||||||
@ -100,18 +100,18 @@ export default class CustomersService extends BaseService {
|
|||||||
const validTotpCode = customerHydrated.totpCodes?.find((totpCode) => {
|
const validTotpCode = customerHydrated.totpCodes?.find((totpCode) => {
|
||||||
return totpCode.expire_at && totpCode.expire_at.getTime() > now;
|
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
|
// 3: Generate a new SMS code
|
||||||
const totpPin = this.generateTotp();
|
const totpPin = this.generateTotp();
|
||||||
|
|
||||||
const reason = customer.password ? TotpCodesReasons.LOGIN : TotpCodesReasons.FIRST_LOGIN;
|
const reason = customer.password ? TotpCodesReasons.LOGIN : TotpCodesReasons.FIRST_LOGIN;
|
||||||
// 4: Save the SMS code in database
|
// 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
|
// 5: Send the SMS code to the customer
|
||||||
await this.sendSmsCodeToCustomer(totpPin, 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<Customer | null> {
|
public async askAnotherCode(email: string, totpCodeUid: string): Promise<{ customer: Customer; totpCode: 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;
|
||||||
@ -267,26 +267,26 @@ export default class CustomersService extends BaseService {
|
|||||||
const customerHydrated = Customer.hydrate<Customer>(customer);
|
const customerHydrated = Customer.hydrate<Customer>(customer);
|
||||||
|
|
||||||
// 2: Get last code sent
|
// 2: Get last code sent
|
||||||
const lastCode = customerHydrated.totpCodes?.find((totpCode) => {
|
const totpCodeToResend = customerHydrated.totpCodes?.find((totpCode) => {
|
||||||
return totpCode.expire_at && totpCode.expire_at.getTime() > now;
|
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
|
// 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
|
// 4: Generate a new SMS code
|
||||||
const totpPin = this.generateTotp();
|
const totpPin = this.generateTotp();
|
||||||
|
|
||||||
// 5: Disable the old code
|
// 5: Disable the old code
|
||||||
await this.totpCodesRepository.disable(lastCode);
|
await this.totpCodesRepository.disable(totpCodeToResend);
|
||||||
|
|
||||||
// 6: Save the SMS code in database
|
// 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
|
// 7: Send the SMS code to the customer
|
||||||
await this.sendSmsCodeToCustomer(totpPin, customer);
|
await this.sendSmsCodeToCustomer(totpPin, customer);
|
||||||
return customer;
|
return { customer, totpCode };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
51
src/services/customer/TotpService/TotpService.ts
Normal file
51
src/services/customer/TotpService/TotpService.ts
Normal file
@ -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<TotpCodes[]> {
|
||||||
|
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<TotpCodes | null> {
|
||||||
|
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<Prisma.BatchPayload> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user