Skip to content

Commit

Permalink
Merge pull request #91 from VictoriqueMoe/express-typeorm-store
Browse files Browse the repository at this point in the history
Express typeorm store
  • Loading branch information
VictoriqueMoe authored Mar 4, 2024
2 parents a6cd18c + 1042d23 commit 20e17fc
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 15 deletions.
34 changes: 19 additions & 15 deletions src/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import rateLimit from "express-rate-limit";
import { LRUCache } from "lru-cache";
import { filesDir, FileUtils, NetworkUtils } from "./utils/Utils.js";
import { fileURLToPath } from "node:url";
import { ExpressRateLimitTypeOrmStore } from "./extensions/expressRateLimit/stores/ExpressRateLimitTypeOrmStore.js";

const opts: Partial<TsED.Configuration> = {
...config,
Expand Down Expand Up @@ -119,21 +120,6 @@ const opts: Partial<TsED.Configuration> = {
extended: true,
}),
compression(),
rateLimit({
windowMs: 1000,
limit: 1,
message: "You have exceeded your 1 request a second.",
standardHeaders: true,
skip: request => {
if (request?.$ctx?.request?.request?.session?.passport) {
return true;
}
return request.path.includes("/admin") ? true : !request.path.includes("/rest");
},
keyGenerator: request => {
return NetworkUtils.getIp(request);
},
}),
...Object.values(globalMiddleware),
],
views: {
Expand Down Expand Up @@ -166,6 +152,7 @@ export class Server implements BeforeRoutesInit {
public constructor(
@Inject() private app: PlatformApplication,
@Inject(SQLITE_DATA_SOURCE) private ds: DataSource,
@Inject() private expressRateLimitTypeOrmStore: ExpressRateLimitTypeOrmStore,
) {}

@Configuration()
Expand Down Expand Up @@ -200,5 +187,22 @@ export class Server implements BeforeRoutesInit {
}),
);
}
this.app.use(
rateLimit({
windowMs: 1000,
limit: 1,
standardHeaders: true,
skip: request => {
if (request?.$ctx?.request?.request?.session?.passport) {
return true;
}
return request.path.includes("/admin") ? true : !request.path.includes("/rest");
},
keyGenerator: request => {
return NetworkUtils.getIp(request);
},
store: this.expressRateLimitTypeOrmStore,
}),
);
}
}
111 changes: 111 additions & 0 deletions src/extensions/expressRateLimit/stores/ExpressRateLimitTypeOrmStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import type { ClientRateLimitInfo, IncrementResponse, Options, Store } from "express-rate-limit";
import { DataSource, LessThan, Repository } from "typeorm";
import { ExpressRateLimitStoreModel } from "../../../model/db/ExpressRateLimitStore.model.js";
import { Builder } from "builder-pattern";
import { Inject, Injectable } from "@tsed/di";
import { SQLITE_DATA_SOURCE } from "../../../model/di/tokens.js";
import { ScheduleService } from "../../../services/ScheduleService.js";

@Injectable()
export class ExpressRateLimitTypeOrmStore implements Store {
private windowMs: number;

private repo: Repository<ExpressRateLimitStoreModel>;

public constructor(
@Inject(SQLITE_DATA_SOURCE) ds: DataSource,
@Inject() private scheduleService: ScheduleService,
) {
this.repo = ds.getRepository(ExpressRateLimitStoreModel);
}

public init(options: Options): void {
this.windowMs = options.windowMs;
this.scheduleService.scheduleJobInterval(
{
milliseconds: this.windowMs,
},
this.clearExpired,
"rateLimiter",
this,
);
}

private async clearExpired(): Promise<void> {
await this.repo.delete({
resetTime: LessThan(new Date()),
});
}

public async get(key: string): Promise<ClientRateLimitInfo | undefined> {
const fromDb = await this.getFromDb(key);
if (fromDb) {
return this.transform(fromDb);
}
}

private async getResponse(key: string): Promise<ExpressRateLimitStoreModel> {
const fromDb = await this.getFromDb(key);
if (fromDb) {
return fromDb;
}
const newModel = Builder(ExpressRateLimitStoreModel)
.key(key)
.resetTime(new Date(Date.now() + this.windowMs))
.totalHits(0)
.build();
return this.repo.save(newModel);
}

public async increment(key: string): Promise<IncrementResponse> {
const resp = await this.getResponse(key);
const now = Date.now();
if (resp.resetTime && resp.resetTime.getTime() <= now) {
this.resetClient(resp, now);
}
resp.totalHits++;
return this.transform(await this.repo.save(resp));
}

private resetClient(client: ExpressRateLimitStoreModel, now = Date.now()): IncrementResponse {
client.totalHits = 0;
client.resetTime.setTime(now + this.windowMs);
return client;
}

public async decrement(key: string): Promise<void> {
const fromDb = await this.getFromDb(key);
if (!fromDb) {
return;
}
fromDb.totalHits--;
await this.repo.save(fromDb);
}

public async resetKey(key: string): Promise<void> {
await this.repo.delete({
key,
});
}

public async resetAll(): Promise<void> {
await this.repo.clear();
}

private transform(model: ExpressRateLimitStoreModel): ClientRateLimitInfo {
return {
totalHits: model.totalHits,
resetTime: model.resetTime,
};
}

private getFromDb(key: string): Promise<ExpressRateLimitStoreModel | null> {
return this.repo.findOneBy({
key,
});
}

public get localKeys(): boolean {
return false;
}
}
14 changes: 14 additions & 0 deletions src/migrations/1709586577877-express_typeorm_rate_table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class ExpressTypeormRateTable1709586577877 implements MigrationInterface {
name = 'ExpressTypeormRateTable1709586577877'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "express_rate_limit_store_model" ("key" varchar PRIMARY KEY NOT NULL, "totalHits" integer NOT NULL, "resetTime" datetime NOT NULL)`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "express_rate_limit_store_model"`);
}

}
13 changes: 13 additions & 0 deletions src/model/db/ExpressRateLimitStore.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Column, Entity, PrimaryColumn } from "typeorm";

@Entity()
export class ExpressRateLimitStoreModel {
@PrimaryColumn()
public key: string;

@Column()
public totalHits: number;

@Column()
public resetTime: Date;
}

0 comments on commit 20e17fc

Please sign in to comment.