diff --git a/apps/api/lib/helpers/verifyApiKey.ts b/apps/api/lib/helpers/verifyApiKey.ts index 85bb98ad7c..725971daf7 100644 --- a/apps/api/lib/helpers/verifyApiKey.ts +++ b/apps/api/lib/helpers/verifyApiKey.ts @@ -1,3 +1,4 @@ +import type { Prisma } from "@prisma/client"; import type { NextMiddleware } from "next-api-middleware"; import { hashAPIKey } from "@calcom/features/ee/api-keys/lib/apiKeys"; @@ -39,6 +40,7 @@ export const verifyApiKey: NextMiddleware = async (req, res, next) => { return res.status(401).json({ error: "This apiKey is expired" }); } if (!apiKey.userId) return res.status(404).json({ error: "No user found for this apiKey" }); + await hardRateLimit(apiKey)(req, res, next); // save the user id in the request for later use req.userId = apiKey.userId; // save the isAdmin boolean here for later use @@ -46,3 +48,20 @@ export const verifyApiKey: NextMiddleware = async (req, res, next) => { req.isCustomPrisma = false; await next(); }; + +const hardRateLimit = + (apiKey: Prisma.ApiKeyGetPayload): NextMiddleware => + async (req, res) => { + const { prisma } = req; + if (!IS_PRODUCTION) return; + if (apiKey.remainingCalls < 1) { + return res.status(429).json({ + error: "API_CALLS_LIMIT_REACHED", + message: "You have reached your daily limit of API calls. Book cal.com/sales to increase your limit", + }); + } + await prisma.apiKey.update({ + where: { id: apiKey.id }, + data: { lastUsedAt: new Date(), remainingCalls: apiKey.remainingCalls - 1 }, + }); + }; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 78c0ce069c..c45779f094 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -584,18 +584,19 @@ model Impersonations { } model ApiKey { - id String @id @unique @default(cuid()) - userId Int - teamId Int? - note String? - createdAt DateTime @default(now()) - expiresAt DateTime? - lastUsedAt DateTime? - hashedKey String @unique() - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) - team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade) - app App? @relation(fields: [appId], references: [slug], onDelete: Cascade) - appId String? + id String @id @unique @default(cuid()) + userId Int + teamId Int? + note String? + createdAt DateTime @default(now()) + expiresAt DateTime? + lastUsedAt DateTime? + hashedKey String @unique() + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) + team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade) + app App? @relation(fields: [appId], references: [slug], onDelete: Cascade) + appId String? + remainingCalls Int? @@index([userId]) }