Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rate Limit API Calls #391

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/controllers/EventController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
} from 'routing-controllers';
import EventService from '../../services/EventService';
import { UserAuthentication, OptionalUserAuthentication } from '../middleware/UserAuthentication';
// import { RateLimiter } from '../middleware/RateLimiter';
import { AuthenticatedUser } from '../decorators/AuthenticatedUser';
import { UserModel } from '../../models/UserModel';
import PermissionsService from '../../services/PermissionsService';
Expand All @@ -28,6 +29,7 @@ import {
SubmitEventFeedbackRequest,
} from '../validators/EventControllerRequests';

// @UseBefore(RateLimiter)
@JsonController('/event')
export class EventController {
private eventService: EventService;
Expand Down
37 changes: 37 additions & 0 deletions api/controllers/MerchStoreController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
GetAllMerchCollectionsResponse,
CreateMerchCollectionResponse,
EditMerchCollectionResponse,
CreateCollectionPhotoResponse,
DeleteCollectionPhotoResponse,
GetOneMerchItemResponse,
DeleteMerchCollectionResponse,
CreateMerchItemResponse,
Expand Down Expand Up @@ -55,6 +57,7 @@ import MerchStoreService from '../../services/MerchStoreService';
import {
CreateMerchCollectionRequest,
EditMerchCollectionRequest,
CreateCollectionPhotoRequest,
CreateMerchItemRequest,
EditMerchItemRequest,
PlaceMerchOrderRequest,
Expand Down Expand Up @@ -124,6 +127,40 @@ export class MerchStoreController {
return { error: null };
}

@UseBefore(UserAuthentication)
@Post('/collection/picture/:uuid')
async createMerchCollectionPhoto(@UploadedFile('image',
{ options: StorageService.getFileOptions(MediaType.MERCH_PHOTO) }) file: File,
@Params() params: UuidParam,
@Body() createCollectionRequest: CreateCollectionPhotoRequest,
@AuthenticatedUser() user: UserModel): Promise<CreateCollectionPhotoResponse> {
if (!PermissionsService.canEditMerchStore(user)) throw new ForbiddenError();

// generate a random string for the uploaded photo url
const position = parseInt(createCollectionRequest.position, 10);
if (Number.isNaN(position)) throw new BadRequestError('Position must be a number');
const uniqueFileName = uuid();
const uploadedPhoto = await this.storageService.uploadToFolder(
file, MediaType.MERCH_PHOTO, uniqueFileName, params.uuid,
);
const collectionPhoto = await this.merchStoreService.createCollectionPhoto(
params.uuid, { uploadedPhoto, position },
);

return { error: null, collectionPhoto };
}

@UseBefore(UserAuthentication)
@Delete('/collection/picture/:uuid')
async deleteMerchCollectionPhoto(@Params() params: UuidParam, @AuthenticatedUser() user: UserModel):
Promise<DeleteCollectionPhotoResponse> {
if (!PermissionsService.canEditMerchStore(user)) throw new ForbiddenError();
const photoToDelete = await this.merchStoreService.getCollectionPhotoForDeletion(params.uuid);
await this.storageService.deleteAtUrl(photoToDelete.uploadedPhoto);
await this.merchStoreService.deleteCollectionPhoto(photoToDelete);
return { error: null };
}

@Get('/item/:uuid')
async getOneMerchItem(@Params() params: UuidParam,
@AuthenticatedUser() user: UserModel): Promise<GetOneMerchItemResponse> {
Expand Down
22 changes: 22 additions & 0 deletions api/middleware/RateLimiter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ExpressMiddlewareInterface, Middleware } from 'routing-controllers';
import * as express from 'express';
import { rateLimit } from 'express-rate-limit';
// import { Config } from '../../config';

@Middleware({ type: 'before' })
export class RateLimiter implements ExpressMiddlewareInterface {
private limiter = rateLimit({
windowMs: 900000,
max: 5,
message: 'Too many requests, please try again later.',
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
});

use(req: express.Request, res: express.Response, next: express.NextFunction) {
if (req.path === '/api/v2/user') {
return this.limiter(req, res, next);
}
return next();
}
}
2 changes: 2 additions & 0 deletions api/middleware/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { RequestLogger } from './RequestLogger';
import { ErrorHandler } from './ErrorHandler';
import { NotFoundHandler } from './NotFoundHandler';
import { MetricsRecorder } from './MetricsRecorder';
import { RateLimiter } from './RateLimiter';

export const middlewares = [
ErrorHandler,
NotFoundHandler,
RequestLogger,
MetricsRecorder,
RateLimiter,
];
32 changes: 32 additions & 0 deletions api/validators/MerchStoreRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { Type } from 'class-transformer';
import {
CreateMerchCollectionRequest as ICreateMerchCollectionRequest,
EditMerchCollectionRequest as IEditMerchCollectionRequest,
CreateCollectionPhotoRequest as ICreateCollectionPhotoRequest,
CreateMerchItemRequest as ICreateMerchItemRequest,
EditMerchItemRequest as IEditMerchItemRequest,
CreateMerchItemOptionRequest as ICreateMerchItemOptionRequest,
Expand All @@ -32,6 +33,8 @@ import {
OrderItemFulfillmentUpdate as IOrderItemFulfillmentUpdate,
MerchCollection as IMerchCollection,
MerchCollectionEdit as IMerchCollectionEdit,
MerchCollectionPhoto as IMerchCollectionPhoto,
MerchCollectionPhotoEdit as IMerchCollectionPhotoEdit,
MerchItem as IMerchItem,
MerchItemEdit as IMerchItemEdit,
MerchItemOption as IMerchItemOption,
Expand Down Expand Up @@ -60,6 +63,9 @@ export class MerchCollection implements IMerchCollection {

@Allow()
archived?: boolean;

@Allow()
collectionPhotos: MerchCollectionPhoto[];
}

export class MerchCollectionEdit implements IMerchCollectionEdit {
Expand All @@ -78,6 +84,26 @@ export class MerchCollectionEdit implements IMerchCollectionEdit {
@Min(0)
@Max(100)
discountPercentage?: number;

@Allow()
collectionPhotos?: MerchCollectionPhotoEdit[];
}

export class MerchCollectionPhoto implements IMerchCollectionPhoto {
@Allow()
uploadedPhoto: string;

@Allow()
position: number;
}

export class MerchCollectionPhotoEdit implements IMerchCollectionPhotoEdit {
@IsDefined()
@IsUUID()
uuid: string;

@Allow()
position: number;
}

export class MerchItemOptionMetadata implements IMerchItemOptionMetadata {
Expand Down Expand Up @@ -305,6 +331,12 @@ export class EditMerchCollectionRequest implements IEditMerchCollectionRequest {
collection: MerchCollectionEdit;
}

export class CreateCollectionPhotoRequest implements ICreateCollectionPhotoRequest {
@IsDefined()
@IsNumberString()
position: string;
}

export class CreateMerchItemRequest implements ICreateMerchItemRequest {
@Type(() => MerchItem)
@ValidateNested()
Expand Down
8 changes: 8 additions & 0 deletions config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const Config = {
MAX_EVENT_COVER_FILE_SIZE: Number(process.env.MAX_EVENT_COVER_FILE_SIZE) * BYTES_PER_KILOBYTE,
MAX_BANNER_FILE_SIZE: Number(process.env.MAX_BANNER_FILE_SIZE) * BYTES_PER_KILOBYTE,
MAX_MERCH_PHOTO_FILE_SIZE: Number(process.env.MAX_MERCH_PHOTO_FILE_SIZE) * BYTES_PER_KILOBYTE,
MAX_COLLECTION_PHOTO_FILE_SIZE: Number(process.env.MAX_MERCH_PHOTO_FILE_SIZE) * BYTES_PER_KILOBYTE,
MAX_RESUME_FILE_SIZE: Number(process.env.MAX_RESUME_FILE_SIZE) * BYTES_PER_KILOBYTE,
PROFILE_PICTURE_UPLOAD_PATH: process.env.BASE_UPLOAD_PATH + process.env.PROFILE_PICTURE_UPLOAD_PATH,
EVENT_COVER_UPLOAD_PATH: process.env.BASE_UPLOAD_PATH + process.env.EVENT_COVER_UPLOAD_PATH,
Expand All @@ -81,4 +82,11 @@ export const Config = {
FEEDBACK_POINT_REWARD: Number(process.env.FEEDBACK_POINT_REWARD),
EVENT_FEEDBACK_POINT_REWARD: Number(process.env.EVENT_FEEDBACK_POINT_REWARD),
},

rateLimits: {
default: {
windowMs: Number(process.env.DEFAULT_RATE_LIMIT_WINDOW_MS),
max: Number(process.env.DEFAULT_RATE_LIMIT_MAX_REQUESTS),
},
},
};
2 changes: 2 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,6 @@ const app = createExpressServer({
defaultErrorHandler: false,
});

app.set('trust proxy', 1);

app.listen(Config.port);
58 changes: 58 additions & 0 deletions migrations/0039-add-collection-image-table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm';

const TABLE_NAME = 'MerchCollectionPhotos';
const COLLECTION_TABLE_NAME = 'MerchandiseCollections';

export class AddCollectionImageTable1696990832868 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(new Table({
name: TABLE_NAME,
columns: [
{
name: 'uuid',
type: 'uuid',
isGenerated: true,
isPrimary: true,
generationStrategy: 'uuid',
},
{
name: 'merchCollection',
type: 'uuid',
},
{
name: 'uploadedPhoto',
type: 'varchar(255)',
},
{
name: 'uploadedAt',
type: 'timestamptz',
default: 'CURRENT_TIMESTAMP(6)',
},
{
name: 'position',
type: 'integer',
},
],
// cascade delete
foreignKeys: [
{
columnNames: ['merchCollection'],
referencedTableName: COLLECTION_TABLE_NAME,
referencedColumnNames: ['uuid'],
onDelete: 'CASCADE',
},
],
}));

await queryRunner.createIndices(TABLE_NAME, [
new TableIndex({
name: 'images_by_collection_index',
columnNames: ['merchCollection'],
}),
]);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable(TABLE_NAME);
}
}
33 changes: 33 additions & 0 deletions models/MerchCollectionPhotoModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { BaseEntity, Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { PublicMerchCollectionPhoto, Uuid } from '../types';
import { MerchandiseCollectionModel } from './MerchandiseCollectionModel';

@Entity('MerchCollectionPhotos')
export class MerchCollectionPhotoModel extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
uuid: Uuid;

@ManyToOne((type) => MerchandiseCollectionModel,
(merchCollection) => merchCollection.collectionPhotos, { nullable: false, onDelete: 'CASCADE' })
@JoinColumn({ name: 'merchCollection' })
@Index('images_by_collection_index')
merchCollection: MerchandiseCollectionModel;

@Column('varchar', { length: 255, nullable: false })
uploadedPhoto: string;

@Column('timestamptz', { default: () => 'CURRENT_TIMESTAMP(6)', nullable: false })
uploadedAt: Date;

@Column('integer')
position: number;

public getPublicMerchCollectionPhoto(): PublicMerchCollectionPhoto {
return {
uuid: this.uuid,
uploadedPhoto: this.uploadedPhoto,
uploadedAt: this.uploadedAt,
position: this.position,
};
}
}
5 changes: 5 additions & 0 deletions models/MerchandiseCollectionModel.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Entity, BaseEntity, Column, PrimaryGeneratedColumn, OneToMany, CreateDateColumn } from 'typeorm';
import { Uuid, PublicMerchCollection } from '../types';
import { MerchandiseItemModel } from './MerchandiseItemModel';
import { MerchCollectionPhotoModel } from './MerchCollectionPhotoModel';

@Entity('MerchandiseCollections')
export class MerchandiseCollectionModel extends BaseEntity {
Expand All @@ -25,10 +26,14 @@ export class MerchandiseCollectionModel extends BaseEntity {
@OneToMany((type) => MerchandiseItemModel, (item) => item.collection, { cascade: true })
items: MerchandiseItemModel[];

@OneToMany((type) => MerchCollectionPhotoModel, (photo) => photo.merchCollection, { cascade: true })
collectionPhotos: MerchCollectionPhotoModel[];

public getPublicMerchCollection(canSeeHiddenItems = false): PublicMerchCollection {
const baseMerchCollection: any = {
uuid: this.uuid,
title: this.title,
collectionPhotos: this.collectionPhotos,
themeColorHex: this.themeColorHex,
description: this.description,
createdAt: this.createdAt,
Expand Down
1 change: 1 addition & 0 deletions models/OrderItemModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export class OrderItemModel extends BaseEntity {
notes: string;

public getPublicOrderItem(): PublicOrderItem {
// console.log(this);
return {
uuid: this.uuid,
option: this.option.getPublicOrderMerchItemOption(),
Expand Down
2 changes: 2 additions & 0 deletions models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ActivityModel } from './ActivityModel';
import { EventModel } from './EventModel';
import { AttendanceModel } from './AttendanceModel';
import { MerchandiseCollectionModel } from './MerchandiseCollectionModel';
import { MerchCollectionPhotoModel } from './MerchCollectionPhotoModel';
import { MerchandiseItemModel } from './MerchandiseItemModel';
import { MerchandiseItemPhotoModel } from './MerchandiseItemPhotoModel';
import { OrderModel } from './OrderModel';
Expand All @@ -20,6 +21,7 @@ export const models = [
EventModel,
AttendanceModel,
MerchandiseCollectionModel,
MerchCollectionPhotoModel,
MerchandiseItemModel,
MerchandiseItemPhotoModel,
MerchandiseItemOptionModel,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"dotenv": "^8.2.0",
"ejs": "^3.1.3",
"express": "^4.17.1",
"express-rate-limit": "6.11.2",
"faker": "^5.5.3",
"jsonwebtoken": "^8.5.1",
"moment": "^2.27.0",
Expand Down
Loading
Loading