From ba28a174aeff2c99fb243be093a197de5da734ed Mon Sep 17 00:00:00 2001 From: Janderson Souza Matias Date: Mon, 17 Jun 2024 20:44:51 -0300 Subject: [PATCH] adjust add limit to OPT attempts --- .../migrations/1718665191767-migrations.ts | 19 ++++++++++++++ .../migrations/1718665323971-migrations.ts | 26 +++++++++++++++++++ .../migrations/1718665677392-migrations.ts | 23 ++++++++++++++++ src/modules/auth/controller/index.ts | 14 ++++++++++ .../auth/entity/login_attempt.entity.ts | 22 ++++++++++++++++ src/modules/auth/entity/otp.entity.ts | 9 +------ src/modules/auth/service/index.ts | 25 ++++++++++++++++++ src/modules/logs/service/index.ts | 2 +- src/modules/user/controller/index.ts | 6 ++++- src/modules/user/entity/user.entity.ts | 3 +++ src/modules/user/service/index.ts | 5 ++-- 11 files changed, 142 insertions(+), 12 deletions(-) create mode 100644 src/database/migrations/1718665191767-migrations.ts create mode 100644 src/database/migrations/1718665323971-migrations.ts create mode 100644 src/database/migrations/1718665677392-migrations.ts create mode 100644 src/modules/auth/entity/login_attempt.entity.ts diff --git a/src/database/migrations/1718665191767-migrations.ts b/src/database/migrations/1718665191767-migrations.ts new file mode 100644 index 0000000..c2d93c6 --- /dev/null +++ b/src/database/migrations/1718665191767-migrations.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Migrations1718665191767 implements MigrationInterface { + name = "Migrations1718665191767"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "login_attempt" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "email" character varying NOT NULL, "created_at" bigint NOT NULL, CONSTRAINT "PK_72829cd4f7424e3cdfd46c476c0" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `ALTER TABLE "user" ADD "force_logout" boolean NOT NULL DEFAULT false` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "force_logout"`); + await queryRunner.query(`DROP TABLE "login_attempt"`); + } +} diff --git a/src/database/migrations/1718665323971-migrations.ts b/src/database/migrations/1718665323971-migrations.ts new file mode 100644 index 0000000..92db6a0 --- /dev/null +++ b/src/database/migrations/1718665323971-migrations.ts @@ -0,0 +1,26 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Migrations1718665323971 implements MigrationInterface { + name = "Migrations1718665323971"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "login_attempt" DROP COLUMN "created_at"` + ); + await queryRunner.query( + `ALTER TABLE "login_attempt" ADD "created_at" TIMESTAMP NOT NULL DEFAULT now()` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "session" DROP CONSTRAINT "FK_5351647a4a00ccbca90adb057f6"` + ); + await queryRunner.query( + `ALTER TABLE "login_attempt" DROP COLUMN "created_at"` + ); + await queryRunner.query( + `ALTER TABLE "login_attempt" ADD "created_at" bigint NOT NULL` + ); + } +} diff --git a/src/database/migrations/1718665677392-migrations.ts b/src/database/migrations/1718665677392-migrations.ts new file mode 100644 index 0000000..2f25e8f --- /dev/null +++ b/src/database/migrations/1718665677392-migrations.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Migrations1718665677392 implements MigrationInterface { + name = "Migrations1718665677392"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "login_attempt" DROP COLUMN "created_at"` + ); + await queryRunner.query( + `ALTER TABLE "login_attempt" ADD "created_at" bigint NOT NULL` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "login_attempt" DROP COLUMN "created_at"` + ); + await queryRunner.query( + `ALTER TABLE "login_attempt" ADD "created_at" TIMESTAMP NOT NULL DEFAULT now()` + ); + } +} diff --git a/src/modules/auth/controller/index.ts b/src/modules/auth/controller/index.ts index 9bdc684..3c713b3 100644 --- a/src/modules/auth/controller/index.ts +++ b/src/modules/auth/controller/index.ts @@ -82,6 +82,13 @@ export default class AuthenticationController { if (!user?.email) { return res.status(404).send({ user }); } + const loginAttempts = await Authentication.countLoginAttempt(user.email); + + if (loginAttempts >= 10) { + return res + .status(400) + .send({ error: "Number of attempts exceeded, try again later" }); + } const otp = await Authentication.verifyOTP(user.email, code); @@ -134,6 +141,13 @@ export default class AuthenticationController { if (!coach?.email) { return res.status(404).send({ coach }); } + const loginAttempts = await Authentication.countLoginAttempt(coach.email); + + if (loginAttempts >= 10) { + return res + .status(400) + .send({ error: "Number of attempts exceeded, try again later" }); + } const otp = await Authentication.verifyOTP(coach.email, code); diff --git a/src/modules/auth/entity/login_attempt.entity.ts b/src/modules/auth/entity/login_attempt.entity.ts new file mode 100644 index 0000000..3203cc4 --- /dev/null +++ b/src/modules/auth/entity/login_attempt.entity.ts @@ -0,0 +1,22 @@ +import { + Column, + CreateDateColumn, + Entity, + PrimaryGeneratedColumn, +} from "typeorm"; + +@Entity() +export class LoginAttempt { + constructor(otp?: Partial) { + Object.assign(this, otp); + } + + @PrimaryGeneratedColumn("uuid") + id?: string; + + @Column() + email?: string; + + @Column({ type: "bigint" }) + created_at?: number; +} diff --git a/src/modules/auth/entity/otp.entity.ts b/src/modules/auth/entity/otp.entity.ts index 9dbe1d3..782c275 100644 --- a/src/modules/auth/entity/otp.entity.ts +++ b/src/modules/auth/entity/otp.entity.ts @@ -1,11 +1,4 @@ -import { Coach } from "../../coach/entity/coach.entity"; -import { - Column, - Entity, - ManyToOne, - JoinColumn, - PrimaryGeneratedColumn, -} from "typeorm"; +import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; @Entity() export class Otp { diff --git a/src/modules/auth/service/index.ts b/src/modules/auth/service/index.ts index 9fb11a6..fc07a5b 100644 --- a/src/modules/auth/service/index.ts +++ b/src/modules/auth/service/index.ts @@ -10,6 +10,8 @@ import sgMail from "@sendgrid/mail"; import { OTP_EMAIL_NP, OTP_EMAIL_SL } from "./template-email"; import { Otp } from "../entity/otp.entity"; import { MoreThan } from "typeorm"; +import { UserService } from "../../user/service"; +import { LoginAttempt } from "../entity/login_attempt.entity"; const { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_UNAUTHORIZED } = constants; @@ -49,6 +51,11 @@ export default class Authentication { throw new UnauthorizedException("Invalid token"); } + if (user.force_logout) { + await UserService.updateAdmin(user.id, { ...user, force_logout: false }); + throw new UnauthorizedException("This user was updated"); + } + const { ...userWithoutPassword } = user; return new User(userWithoutPassword); @@ -133,6 +140,22 @@ export default class Authentication { }); }; + public static saveLoginAttempt = async (email: string) => { + const otpRepository = await dataSource.getRepository(LoginAttempt); + + await otpRepository.save({ email, created_at: new Date().getTime() }); + }; + + public static countLoginAttempt = async (email: string) => { + const otpRepository = await dataSource.getRepository(LoginAttempt); + const tenMinutesAgo = new Date().getTime() - 10 * 60 * 1000; + + return await otpRepository.countBy({ + email, + created_at: MoreThan(tenMinutesAgo), + }); + }; + public static verifyOTP = async (email: string, code: string) => { const otpRepository = await dataSource.getRepository(Otp); const tenMinutesAgo = new Date().getTime() - 10 * 60 * 1000; @@ -144,6 +167,8 @@ export default class Authentication { created_at: MoreThan(tenMinutesAgo), }); + await this.saveLoginAttempt(email); + if (otpCode?.id) { await otpRepository.update(otpCode?.id, { used: true }); } diff --git a/src/modules/logs/service/index.ts b/src/modules/logs/service/index.ts index 80a552e..845aa55 100644 --- a/src/modules/logs/service/index.ts +++ b/src/modules/logs/service/index.ts @@ -7,7 +7,7 @@ export class LogsService { static findAll = async (): Promise => { const userRepository = await dataSource.getRepository(Log); - return userRepository.find(); + return userRepository.find({ take: 50 }); }; static create = async (user: User, description: string) => { diff --git a/src/modules/user/controller/index.ts b/src/modules/user/controller/index.ts index 872d595..1e835f8 100644 --- a/src/modules/user/controller/index.ts +++ b/src/modules/user/controller/index.ts @@ -67,7 +67,11 @@ export default class UserController { throw new Error("You can not do that."); } - await UserService.updateAdmin(user_id, newUser); + await UserService.updateAdmin( + user_id, + newUser, + user_id === currentUser.id + ); return res.status(HTTP_STATUS_OK).send({}); } catch (error) { diff --git a/src/modules/user/entity/user.entity.ts b/src/modules/user/entity/user.entity.ts index 365b361..1490336 100644 --- a/src/modules/user/entity/user.entity.ts +++ b/src/modules/user/entity/user.entity.ts @@ -28,6 +28,9 @@ export class User { @Column({ nullable: true }) region_id?: string; + @Column({ default: false }) + force_logout?: boolean; + @ManyToOne(() => Region, (region) => region.id) @JoinColumn({ name: "region_id" }) region?: Region; diff --git a/src/modules/user/service/index.ts b/src/modules/user/service/index.ts index 3a24f37..497e66a 100644 --- a/src/modules/user/service/index.ts +++ b/src/modules/user/service/index.ts @@ -42,7 +42,8 @@ export class UserService { static updateAdmin = async ( user_id: User["id"], - newUser: Partial + newUser: Partial, + force_logout = false ): Promise => { const userRepository = dataSource.getRepository(User); @@ -52,6 +53,7 @@ export class UserService { name, email, role, + force_logout, region_id: role === "admin" ? null : region_id, }); }; @@ -72,7 +74,6 @@ export class UserService { static removeAdmin = async (id: User["id"]): Promise => { const userRepository = dataSource.getRepository(User); await userRepository.delete({ id }); - return; }; private static sendAdminWelcome = async (user: User) => {