From 7b6ffb5010b0eceb268ad3c500ee098a05dea35b Mon Sep 17 00:00:00 2001 From: rubikscraft Date: Sat, 17 Sep 2022 19:46:53 +0200 Subject: [PATCH] add ratelimits --- backend/src/layers/PicsurLayers.module.ts | 5 +++- .../layers/throttler/PicsurThrottler.guard.ts | 2 +- .../routes/api/apikeys/apikeys.controller.ts | 6 +++-- .../api/experiment/experiment.controller.ts | 26 ++++++++----------- .../routes/api/pref/sys-pref.controller.ts | 5 +++- .../routes/api/pref/usr-pref.controller.ts | 11 +++++--- .../src/routes/api/roles/roles.controller.ts | 7 +++-- .../src/routes/api/usage/usage.controller.ts | 2 ++ .../routes/api/user/user-manage.controller.ts | 7 +++-- .../src/routes/api/user/user.controller.ts | 10 +++++-- .../routes/image/image-manage.controller.ts | 8 +++--- shared/src/types/failable.ts | 6 +++++ 12 files changed, 62 insertions(+), 33 deletions(-) diff --git a/backend/src/layers/PicsurLayers.module.ts b/backend/src/layers/PicsurLayers.module.ts index 121f309..d146384 100644 --- a/backend/src/layers/PicsurLayers.module.ts +++ b/backend/src/layers/PicsurLayers.module.ts @@ -6,7 +6,10 @@ import { PicsurThrottlerGuard } from './throttler/PicsurThrottler.guard'; import { ZodValidationPipe } from './validate/zod-validator.pipe'; @Module({ - imports: [ThrottlerModule.forRoot()], + imports: [ThrottlerModule.forRoot({ + ttl: 60, + limit: 60, + })], providers: [ PicsurThrottlerGuard, MainExceptionFilter, diff --git a/backend/src/layers/throttler/PicsurThrottler.guard.ts b/backend/src/layers/throttler/PicsurThrottler.guard.ts index 3f1bb0f..bbfe2c7 100644 --- a/backend/src/layers/throttler/PicsurThrottler.guard.ts +++ b/backend/src/layers/throttler/PicsurThrottler.guard.ts @@ -5,6 +5,6 @@ import { Fail, FT } from 'picsur-shared/dist/types'; @Injectable() export class PicsurThrottlerGuard extends ThrottlerGuard { protected override throwThrottlingException(context: ExecutionContext): void { - throw Fail(FT.Permission, undefined, 'Too many requests'); + throw Fail(FT.RateLimit); } } diff --git a/backend/src/routes/api/apikeys/apikeys.controller.ts b/backend/src/routes/api/apikeys/apikeys.controller.ts index 0c15fde..7ea7a82 100644 --- a/backend/src/routes/api/apikeys/apikeys.controller.ts +++ b/backend/src/routes/api/apikeys/apikeys.controller.ts @@ -1,4 +1,5 @@ import { Body, Controller, Post } from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; import { ApiKeyCreateResponse, ApiKeyDeleteRequest, @@ -8,14 +9,14 @@ import { ApiKeyListRequest, ApiKeyListResponse, ApiKeyUpdateRequest, - ApiKeyUpdateResponse, + ApiKeyUpdateResponse } from 'picsur-shared/dist/dto/api/apikeys.dto'; import { Permission } from 'picsur-shared/dist/dto/permissions.enum'; import { ThrowIfFailed } from 'picsur-shared/dist/types'; import { ApiKeyDbService } from '../../../collections/apikey-db/apikey-db.service'; import { HasPermission, - RequiredPermissions, + RequiredPermissions } from '../../../decorators/permissions.decorator'; import { ReqUserID } from '../../../decorators/request-user.decorator'; import { Returns } from '../../../decorators/returns.decorator'; @@ -53,6 +54,7 @@ export class ApiKeysController { @Post('create') @Returns(ApiKeyCreateResponse) + @Throttle(10) async createApiKey( @ReqUserID() userID: string, ): Promise { diff --git a/backend/src/routes/api/experiment/experiment.controller.ts b/backend/src/routes/api/experiment/experiment.controller.ts index b43a2c1..4aeaeb4 100644 --- a/backend/src/routes/api/experiment/experiment.controller.ts +++ b/backend/src/routes/api/experiment/experiment.controller.ts @@ -1,22 +1,18 @@ -import { Controller, Get, Request, Response } from '@nestjs/common'; -import type { FastifyReply } from 'fastify'; -import { UserInfoResponse } from 'picsur-shared/dist/dto/api/user-manage.dto'; +import { Controller } from '@nestjs/common'; import { NoPermissions } from '../../../decorators/permissions.decorator'; -import { Returns } from '../../../decorators/returns.decorator'; -import type AuthFastifyRequest from '../../../models/interfaces/authrequest.dto'; @Controller('api/experiment') @NoPermissions() export class ExperimentController { constructor() {} - @Get() - @Returns(UserInfoResponse) - async testRoute( - @Request() req: AuthFastifyRequest, - @Response({ passthrough: true }) res: FastifyReply, - ): Promise { - res.header('Location', '/error/delete-success'); - res.code(302); - return req.user; - } + // @Get() + // @Returns(UserInfoResponse) + // async testRoute( + // @Request() req: AuthFastifyRequest, + // @Response({ passthrough: true }) res: FastifyReply, + // ): Promise { + // res.header('Location', '/error/delete-success'); + // res.code(302); + // return req.user; + // } } diff --git a/backend/src/routes/api/pref/sys-pref.controller.ts b/backend/src/routes/api/pref/sys-pref.controller.ts index 8c5e448..fb9c35f 100644 --- a/backend/src/routes/api/pref/sys-pref.controller.ts +++ b/backend/src/routes/api/pref/sys-pref.controller.ts @@ -1,9 +1,10 @@ import { Body, Controller, Get, Logger, Param, Post } from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; import { GetPreferenceResponse, MultiplePreferencesResponse, UpdatePreferenceRequest, - UpdatePreferenceResponse, + UpdatePreferenceResponse } from 'picsur-shared/dist/dto/api/pref.dto'; import { ThrowIfFailed } from 'picsur-shared/dist/types'; import { SysPreferenceDbService } from '../../../collections/preference-db/sys-preference-db.service'; @@ -20,6 +21,7 @@ export class SysPrefController { @Get() @Returns(MultiplePreferencesResponse) + @Throttle(20) async getAllSysPrefs(): Promise { const prefs = ThrowIfFailed(await this.prefService.getAllPreferences()); @@ -39,6 +41,7 @@ export class SysPrefController { @Post(':key') @Returns(UpdatePreferenceResponse) + @Throttle(30) async setSysPref( @Param('key') key: string, @Body() body: UpdatePreferenceRequest, diff --git a/backend/src/routes/api/pref/usr-pref.controller.ts b/backend/src/routes/api/pref/usr-pref.controller.ts index 33f33ff..2c8d854 100644 --- a/backend/src/routes/api/pref/usr-pref.controller.ts +++ b/backend/src/routes/api/pref/usr-pref.controller.ts @@ -1,9 +1,10 @@ import { Body, Controller, Get, Logger, Param, Post } from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; import { GetPreferenceResponse, MultiplePreferencesResponse, UpdatePreferenceRequest, - UpdatePreferenceResponse, + UpdatePreferenceResponse } from 'picsur-shared/dist/dto/api/pref.dto'; import { ThrowIfFailed } from 'picsur-shared/dist/types'; import { UsrPreferenceDbService } from '../../../collections/preference-db/usr-preference-db.service'; @@ -21,7 +22,8 @@ export class UsrPrefController { @Get() @Returns(MultiplePreferencesResponse) - async getAllSysPrefs( + @Throttle(20) + async getAllUsrPrefs( @ReqUserID() userid: string, ): Promise { const prefs = ThrowIfFailed( @@ -36,7 +38,7 @@ export class UsrPrefController { @Get(':key') @Returns(GetPreferenceResponse) - async getSysPref( + async getUsrPref( @Param('key') key: string, @ReqUserID() userid: string, ): Promise { @@ -49,7 +51,8 @@ export class UsrPrefController { @Post(':key') @Returns(UpdatePreferenceResponse) - async setSysPref( + @Throttle(30) + async setUsrPref( @Param('key') key: string, @ReqUserID() userid: string, @Body() body: UpdatePreferenceRequest, diff --git a/backend/src/routes/api/roles/roles.controller.ts b/backend/src/routes/api/roles/roles.controller.ts index 9b4d71d..ea529c8 100644 --- a/backend/src/routes/api/roles/roles.controller.ts +++ b/backend/src/routes/api/roles/roles.controller.ts @@ -1,4 +1,5 @@ import { Body, Controller, Get, Logger, Post } from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; import { RoleCreateRequest, RoleCreateResponse, @@ -9,7 +10,7 @@ import { RoleListResponse, RoleUpdateRequest, RoleUpdateResponse, - SpecialRolesResponse, + SpecialRolesResponse } from 'picsur-shared/dist/dto/api/roles.dto'; import { Fail, FT, ThrowIfFailed } from 'picsur-shared/dist/types'; import { RoleDbService } from '../../../collections/role-db/role-db.service'; @@ -21,7 +22,7 @@ import { DefaultRolesList, ImmutableRolesList, SoulBoundRolesList, - UndeletableRolesList, + UndeletableRolesList } from '../../../models/constants/roles.const'; import { isPermissionsArray } from '../../../models/validators/permissions.validator'; @@ -56,6 +57,7 @@ export class RolesController { @Post('update') @Returns(RoleUpdateResponse) + @Throttle(20) async updateRole( @Body() body: RoleUpdateRequest, ): Promise { @@ -73,6 +75,7 @@ export class RolesController { @Post('create') @Returns(RoleCreateResponse) + @Throttle(10) async createRole( @Body() role: RoleCreateRequest, ): Promise { diff --git a/backend/src/routes/api/usage/usage.controller.ts b/backend/src/routes/api/usage/usage.controller.ts index 34803ed..3321836 100644 --- a/backend/src/routes/api/usage/usage.controller.ts +++ b/backend/src/routes/api/usage/usage.controller.ts @@ -1,4 +1,5 @@ import { Controller, Logger, Post, Req, Res } from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; import type { FastifyReply, FastifyRequest } from 'fastify'; import { Fail, FT, ThrowIfFailed } from 'picsur-shared/dist/types'; import { UsageConfigService } from '../../../config/late/usage.config.service'; @@ -14,6 +15,7 @@ export class UsageController { @Post(['report', 'report/*']) @ReturnsAnything() + @Throttle(120) async deleteRole( @Req() req: FastifyRequest, @Res({ diff --git a/backend/src/routes/api/user/user-manage.controller.ts b/backend/src/routes/api/user/user-manage.controller.ts index 7bbb0c9..7b4053e 100644 --- a/backend/src/routes/api/user/user-manage.controller.ts +++ b/backend/src/routes/api/user/user-manage.controller.ts @@ -1,4 +1,5 @@ import { Body, Controller, Get, Logger, Post } from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; import { GetSpecialUsersResponse, UserCreateRequest, @@ -10,7 +11,7 @@ import { UserListRequest, UserListResponse, UserUpdateRequest, - UserUpdateResponse, + UserUpdateResponse } from 'picsur-shared/dist/dto/api/user-manage.dto'; import { ThrowIfFailed } from 'picsur-shared/dist/types'; import { UserDbService } from '../../../collections/user-db/user-db.service'; @@ -20,7 +21,7 @@ import { Permission } from '../../../models/constants/permissions.const'; import { ImmutableUsersList, LockedLoginUsersList, - UndeletableUsersList, + UndeletableUsersList } from '../../../models/constants/special-users.const'; import { EUserBackend2EUser } from '../../../models/transformers/user.transformer'; @@ -46,6 +47,7 @@ export class UserAdminController { @Post('create') @Returns(UserCreateResponse) + @Throttle(10) async register( @Body() create: UserCreateRequest, ): Promise { @@ -78,6 +80,7 @@ export class UserAdminController { @Post('update') @Returns(UserUpdateResponse) + @Throttle(20) async setPermissions( @Body() body: UserUpdateRequest, ): Promise { diff --git a/backend/src/routes/api/user/user.controller.ts b/backend/src/routes/api/user/user.controller.ts index c587b88..4b2fb96 100644 --- a/backend/src/routes/api/user/user.controller.ts +++ b/backend/src/routes/api/user/user.controller.ts @@ -1,4 +1,5 @@ import { Body, Controller, Get, Logger, Post } from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; import { UserCheckNameRequest, UserCheckNameResponse, @@ -6,7 +7,7 @@ import { UserMePermissionsResponse, UserMeResponse, UserRegisterRequest, - UserRegisterResponse, + UserRegisterResponse } from 'picsur-shared/dist/dto/api/user.dto'; import type { EUser } from 'picsur-shared/dist/entities/user.entity'; import { ThrowIfFailed } from 'picsur-shared/dist/types'; @@ -14,7 +15,7 @@ import { UserDbService } from '../../../collections/user-db/user-db.service'; import { NoPermissions, RequiredPermissions, - UseLocalAuth, + UseLocalAuth } from '../../../decorators/permissions.decorator'; import { ReqUser, ReqUserID } from '../../../decorators/request-user.decorator'; import { Returns } from '../../../decorators/returns.decorator'; @@ -34,6 +35,7 @@ export class UserController { @Post('login') @Returns(UserLoginResponse) @UseLocalAuth(Permission.UserLogin) + @Throttle(30, 300) async login(@ReqUser() user: EUser): Promise { const jwt_token = ThrowIfFailed(await this.authService.createToken(user)); @@ -43,6 +45,7 @@ export class UserController { @Post('register') @Returns(UserRegisterResponse) @RequiredPermissions(Permission.UserRegister) + @Throttle(5, 300) async register( @Body() register: UserRegisterRequest, ): Promise { @@ -56,6 +59,7 @@ export class UserController { @Post('checkname') @Returns(UserCheckNameResponse) @RequiredPermissions(Permission.UserRegister) + @Throttle(20) async checkName( @Body() checkName: UserCheckNameRequest, ): Promise { @@ -67,6 +71,7 @@ export class UserController { @Get('me') @Returns(UserMeResponse) @RequiredPermissions(Permission.UserKeepLogin) + @Throttle(10) async me(@ReqUserID() userid: string): Promise { const backenduser = ThrowIfFailed(await this.usersService.findOne(userid)); @@ -81,6 +86,7 @@ export class UserController { @Get('me/permissions') @Returns(UserMePermissionsResponse) @NoPermissions() + @Throttle(20) async refresh( @ReqUserID() userid: string, ): Promise { diff --git a/backend/src/routes/image/image-manage.controller.ts b/backend/src/routes/image/image-manage.controller.ts index cece5a5..a0a18ca 100644 --- a/backend/src/routes/image/image-manage.controller.ts +++ b/backend/src/routes/image/image-manage.controller.ts @@ -5,8 +5,9 @@ import { Logger, Param, Post, - Res, + Res } from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; import type { FastifyReply } from 'fastify'; import { ImageDeleteRequest, @@ -17,14 +18,14 @@ import { ImageListResponse, ImageUpdateRequest, ImageUpdateResponse, - ImageUploadResponse, + ImageUploadResponse } from 'picsur-shared/dist/dto/api/image-manage.dto'; import { Permission } from 'picsur-shared/dist/dto/permissions.enum'; import { HasFailed, ThrowIfFailed } from 'picsur-shared/dist/types'; import { MultiPart } from '../../decorators/multipart/multipart.decorator'; import { HasPermission, - RequiredPermissions, + RequiredPermissions } from '../../decorators/permissions.decorator'; import { ReqUserID } from '../../decorators/request-user.decorator'; import { Returns } from '../../decorators/returns.decorator'; @@ -39,6 +40,7 @@ export class ImageManageController { @Post('upload') @Returns(ImageUploadResponse) + @Throttle(20) async uploadImage( @MultiPart() multipart: ImageUploadDto, @ReqUserID() userid: string, diff --git a/shared/src/types/failable.ts b/shared/src/types/failable.ts index eacc900..a7d3ad0 100644 --- a/shared/src/types/failable.ts +++ b/shared/src/types/failable.ts @@ -10,6 +10,7 @@ export enum FT { SysValidation = 'sysvalidation', UsrValidation = 'usrvalidation', Permission = 'permission', + RateLimit = 'ratelimit', NotFound = 'notfound', RouteNotFound = 'routenotfound', Conflict = 'conflict', @@ -63,6 +64,11 @@ const FTProps: { code: 403, message: 'Permission denied', }, + [FT.RateLimit]: { + important: false, + code: 429, + message: 'Rate limit exceeded', + }, [FT.NotFound]: { important: false, code: 404,