diff --git a/apps/api/src/app/auth/api-key.strategy.ts b/apps/api/src/app/auth/api-key.strategy.ts index f9937aaa7a..232a272bcd 100644 --- a/apps/api/src/app/auth/api-key.strategy.ts +++ b/apps/api/src/app/auth/api-key.strategy.ts @@ -35,6 +35,13 @@ export class ApiKeyStrategy extends PassportStrategy( ); } + if (await this.userService.isDailyRequestLimitExceeded({ user })) { + throw new HttpException( + getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS), + StatusCodes.TOO_MANY_REQUESTS + ); + } + await this.prismaService.analytics.upsert({ create: { user: { connect: { id: user.id } } }, update: { diff --git a/apps/api/src/app/auth/jwt.strategy.ts b/apps/api/src/app/auth/jwt.strategy.ts index c70e8fb60c..189389a860 100644 --- a/apps/api/src/app/auth/jwt.strategy.ts +++ b/apps/api/src/app/auth/jwt.strategy.ts @@ -42,6 +42,13 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { ); } + if (await this.userService.isDailyRequestLimitExceeded({ user })) { + throw new HttpException( + getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS), + StatusCodes.TOO_MANY_REQUESTS + ); + } + const country = countriesAndTimezones.getCountryForTimezone(timezone)?.id; diff --git a/apps/api/src/app/portfolio/portfolio.service.spec.ts b/apps/api/src/app/portfolio/portfolio.service.spec.ts index c16590cceb..d85258f7ef 100644 --- a/apps/api/src/app/portfolio/portfolio.service.spec.ts +++ b/apps/api/src/app/portfolio/portfolio.service.spec.ts @@ -92,6 +92,7 @@ describe('PortfolioService', () => { null, null, null, + null, null ); diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 7f3631c54d..55670bb6e0 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -31,9 +31,12 @@ import { DEFAULT_LOCALE, PROPERTY_API_KEY_GHOSTFOLIO, PROPERTY_IS_READ_ONLY_MODE, + PROPERTY_MAX_DAILY_REQUESTS, PROPERTY_REFERRAL_PARTNERS, PROPERTY_SYSTEM_MESSAGE, - TAG_ID_EXCLUDE_FROM_ANALYSIS + TAG_ID_EXCLUDE_FROM_ANALYSIS, + THROTTLE_DAILY_KEY, + THROTTLE_DAILY_TTL } from '@ghostfolio/common/config'; import { SubscriptionType } from '@ghostfolio/common/enums'; import { @@ -52,6 +55,7 @@ import { PerformanceCalculationType } from '@ghostfolio/common/types/performance import { Injectable } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; +import { InjectThrottlerStorage, ThrottlerStorage } from '@nestjs/throttler'; import { Prisma, Role, Settings, User } from '@prisma/client'; import { differenceInDays, subDays } from 'date-fns'; import { without } from 'lodash'; @@ -67,7 +71,9 @@ export class UserService { private readonly prismaService: PrismaService, private readonly propertyService: PropertyService, private readonly subscriptionService: SubscriptionService, - private readonly tagService: TagService + private readonly tagService: TagService, + @InjectThrottlerStorage() + private readonly throttlerStorage: ThrottlerStorage ) {} public async count(args?: Prisma.UserCountArgs) { @@ -228,6 +234,32 @@ export class UserService { return usersWithAdminRole.length > 0; } + public async isDailyRequestLimitExceeded({ + user + }: { + user: UserWithSettings; + }) { + if (user.subscription?.type === SubscriptionType.Premium) { + return false; + } + + const maxDailyRequests = await this.getMaxDailyRequests(); + + if (!maxDailyRequests) { + return false; + } + + const { isBlocked } = await this.throttlerStorage.increment( + `${THROTTLE_DAILY_KEY}-${user.id}`, + THROTTLE_DAILY_TTL, + maxDailyRequests, + THROTTLE_DAILY_TTL, + THROTTLE_DAILY_KEY + ); + + return isBlocked; + } + public async user( userWhereUniqueInput: Prisma.UserWhereUniqueInput ): Promise { @@ -782,4 +814,12 @@ export class UserService { return settings; } + + private async getMaxDailyRequests() { + const value = await this.propertyService.getByKey( + PROPERTY_MAX_DAILY_REQUESTS + ); + + return Number(value) || undefined; + } }