import { MailService } from "#services/mail-service.js";
import { logger } from "#logger";
import { selPool, updPool } from "#db";
import { sql } from "slonik";
import { DbSchema } from "#db-schema";
import { z } from "zod";
// dayjs
import { dayjs, DayjsUtils } from "#dayjs";
import { config } from "#config";
class confirmPinsService {
// privalte
private genRandom4DigitNumber() {
return Math.floor(1000 + Math.random() * 9000);
}
private async deleteConfirmPin(transactionId: string) {
await updPool.query(
sql.unsafe`
delete from
usr.confirm_pins
where
transaction_id = ${transactionId}`,
);
}
private async pinTriesIncrement(transactionId: string) {
await updPool.query(
sql.unsafe`
update
usr.confirm_pins
set
wrong_pin_tries = wrong_pin_tries + 1
where
transaction_id = ${transactionId}`,
);
}
//
// public
//
async sendConfirmPin(transactionId: string, email: string) {
const confirmPin = this.genRandom4DigitNumber();
// удаляем если пин уже есть
await this.deleteConfirmPin(transactionId);
// отправка
const mailBody = `
`;
await MailService.sendMail(email, "Ваш код для EVENT", mailBody);
// бд
await updPool.query(
sql.unsafe`
insert into usr.confirm_pins (
transaction_id,
email,
confirm_pin,
create_time)
values (
${transactionId},
${email},
${confirmPin},
${DayjsUtils.createDayjsUtcWithoutOffset().toISOString()})`,
);
logger.info("Отправлен временный код: ", {
email,
confirmPin,
});
}
async checkConfirmPin(
transactionId: string,
confirmPin: string,
): Promise<
| { status: "correct"; email: string }
| { status: "rotten" }
| { status: "tooManyTries" }
| {
status: "wrong";
triesRemained: number;
}
> {
const pinInfo = await selPool.maybeOne(
sql.type(
z.object({
confirmPin: DbSchema.usr.confirmPins.confirmPin,
email: DbSchema.usr.confirmPins.email,
createTime: DbSchema.usr.confirmPins.createTime,
wrongPinTries: DbSchema.usr.confirmPins.wrongPinTries,
}),
)`
select
confirm_pin as "confirmPin",
email,
create_time as "createTime",
wrong_pin_tries as "wrongPinTries"
from
usr.confirm_pins
where
transaction_id = ${transactionId}`,
);
// не существует
if (!pinInfo) {
return { status: "rotten" };
}
// много попыток
if (pinInfo.wrongPinTries > config.CONFIRM_PIN_MAX_TRIES - 1) {
return { status: "tooManyTries" };
}
// просрочка
if (
DayjsUtils.createDayjsUtcWithoutOffset().isAfter(
dayjs
.utc(pinInfo.createTime)
.add(config.CONFIRM_PIN_LIFETIME_MINS, "minutes"),
)
) {
await this.deleteConfirmPin(transactionId);
return { status: "rotten" };
}
// неправильный
if (pinInfo.confirmPin !== confirmPin) {
await this.pinTriesIncrement(transactionId);
const triesRemained =
config.CONFIRM_PIN_MAX_TRIES - 1 - pinInfo.wrongPinTries;
return {
status: "wrong",
triesRemained,
};
}
// правильный
await this.deleteConfirmPin(transactionId);
return { status: "correct", email: pinInfo.email };
}
}
export const ConfirmPinsService = new confirmPinsService();