diff --git a/apps/web/prisma/migrations/20230901064929_add_requests/migration.sql b/apps/web/prisma/migrations/20230901064929_add_requests/migration.sql new file mode 100644 index 00000000..a6a490ac --- /dev/null +++ b/apps/web/prisma/migrations/20230901064929_add_requests/migration.sql @@ -0,0 +1,12 @@ +-- CreateTable +CREATE TABLE `ApiRequest` ( + `id` VARCHAR(191) NOT NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `type` ENUM('GET_CONFIG', 'TRACK_VIEW') NOT NULL, + `hashedIp` VARCHAR(191) NOT NULL, + `durationInMs` INTEGER NOT NULL, + `projectId` VARCHAR(191) NOT NULL, + + INDEX `ApiRequest_projectId_idx`(`projectId`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/apps/web/prisma/migrations/20230901065851_remove_hashed_ip_field/migration.sql b/apps/web/prisma/migrations/20230901065851_remove_hashed_ip_field/migration.sql new file mode 100644 index 00000000..8992aa99 --- /dev/null +++ b/apps/web/prisma/migrations/20230901065851_remove_hashed_ip_field/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the column `hashedIp` on the `ApiRequest` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE `ApiRequest` DROP COLUMN `hashedIp`; diff --git a/apps/web/prisma/schema.prisma b/apps/web/prisma/schema.prisma index 8e28f8d2..84b66094 100644 --- a/apps/web/prisma/schema.prisma +++ b/apps/web/prisma/schema.prisma @@ -98,6 +98,8 @@ model Project { stripeSubscriptionId String? @unique stripePriceId String? currentPeriodEnd DateTime @default(dbgenerated("(CURRENT_TIMESTAMP(3) + INTERVAL 30 DAY)")) + + apiRequests ApiRequest[] } model ProjectUser { @@ -272,3 +274,20 @@ model ApiKey { @@index([userId]) } + +enum ApiRequestType { + GET_CONFIG + TRACK_VIEW +} + +model ApiRequest { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + type ApiRequestType + durationInMs Int + + projectId String + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + + @@index([projectId]) +} diff --git a/apps/web/src/env/schema.mjs b/apps/web/src/env/schema.mjs index 82ef86dd..0f0c71bc 100644 --- a/apps/web/src/env/schema.mjs +++ b/apps/web/src/env/schema.mjs @@ -30,6 +30,7 @@ export const serverSchema = z.object({ GITHUB_OAUTH_TOKEN: z.string().optional(), GOOGLE_CLIENT_ID: z.string().optional(), GOOGLE_CLIENT_SECRET: z.string().optional(), + HASHING_SECRET: z.string().min(1), }); /** diff --git a/apps/web/src/pages/api/dashboard/[projectId]/data.ts b/apps/web/src/pages/api/dashboard/[projectId]/data.ts index f9505a92..adea1cb7 100644 --- a/apps/web/src/pages/api/dashboard/[projectId]/data.ts +++ b/apps/web/src/pages/api/dashboard/[projectId]/data.ts @@ -8,6 +8,7 @@ import { RequestCache } from "server/services/RequestCache"; import { transformFlagValue } from "lib/flags"; import { LegacyAbbyDataResponse } from "@tryabby/core"; import { PlausibleService } from "server/services/PlausibleService"; +import { RequestService } from "server/services/RequestService"; const incomingQuerySchema = z.object({ projectId: z.string(), @@ -18,6 +19,8 @@ export default async function getWeightsHandler( req: NextApiRequest, res: NextApiResponse ) { + const now = performance.now(); + await NextCors(req, res, { methods: ["GET"], origin: "*", @@ -84,13 +87,13 @@ export default async function getWeightsHandler( await RequestCache.increment(projectId); - PlausibleService.trackPlausibleGoal( - "API Project Data Retrieved", - { projectId: projectId }, - req.url - ).catch((e) => - console.error("Error while sending tracking data to Plausible: ", e) - ); + RequestService.storeRequest({ + projectId, + type: "GET_CONFIG", + durationInMs: performance.now() - now, + }).then((e) => { + console.error("Unable to store request", e); + }); return; } catch (e) { diff --git a/apps/web/src/pages/api/data/index.ts b/apps/web/src/pages/api/data/index.ts index af4b6d3a..6f8fbed2 100644 --- a/apps/web/src/pages/api/data/index.ts +++ b/apps/web/src/pages/api/data/index.ts @@ -7,11 +7,13 @@ import isBot from "isbot"; import { Ratelimit } from "@upstash/ratelimit"; // for deno: see above import { Redis } from "@upstash/redis"; import { RequestCache } from "server/services/RequestCache"; +import { RequestService } from "server/services/RequestService"; export default async function incomingDataHandler( req: NextApiRequest, res: NextApiResponse ) { + const now = performance.now(); await NextCors(req, res, { methods: ["POST"], origin: "*", @@ -75,6 +77,13 @@ export default async function incomingDataHandler( } await RequestCache.increment(event.projectId); + RequestService.storeRequest({ + projectId: event.projectId, + type: "TRACK_VIEW", + durationInMs: performance.now() - now, + }).then((e) => { + console.error("Unable to store request", e); + }); } catch (err) { console.error(err); res.status(500).end(); diff --git a/apps/web/src/pages/api/v1/config/[projectId]/index.ts b/apps/web/src/pages/api/v1/config/[projectId]/index.ts index f6172231..6282a780 100644 --- a/apps/web/src/pages/api/v1/config/[projectId]/index.ts +++ b/apps/web/src/pages/api/v1/config/[projectId]/index.ts @@ -3,7 +3,7 @@ import { NextApiRequest, NextApiResponse } from "next"; import NextCors from "nextjs-cors"; import { prisma } from "server/db/client"; import * as ConfigService from "server/services/ConfigService"; -import { hashApiKey } from "utils/apiKey"; +import { hashString } from "utils/apiKey"; import { z } from "zod"; const incomingQuerySchema = z.object({ @@ -36,7 +36,7 @@ export default async function handler( return; } - const hashedApiKey = hashApiKey(apiKey); + const hashedApiKey = hashString(apiKey); const apiKeyEntry = await prisma.apiKey.findUnique({ where: { diff --git a/apps/web/src/pages/api/v1/data/[projectId].ts b/apps/web/src/pages/api/v1/data/[projectId].ts index f85d1b02..ed804f8c 100644 --- a/apps/web/src/pages/api/v1/data/[projectId].ts +++ b/apps/web/src/pages/api/v1/data/[projectId].ts @@ -8,6 +8,7 @@ import { trackPlanOverage } from "lib/logsnag"; import { RequestCache } from "server/services/RequestCache"; import { transformFlagValue } from "lib/flags"; import { PlausibleService } from "server/services/PlausibleService"; +import { RequestService } from "server/services/RequestService"; const incomingQuerySchema = z.object({ projectId: z.string(), @@ -18,6 +19,8 @@ export default async function getWeightsHandler( req: NextApiRequest, res: NextApiResponse ) { + const now = performance.now(); + await NextCors(req, res, { methods: ["GET"], origin: "*", @@ -82,13 +85,13 @@ export default async function getWeightsHandler( await RequestCache.increment(projectId); - PlausibleService.trackPlausibleGoal( - "API Project Data Retrieved", - { projectId }, - req.url - ).catch((e) => - console.error("Error while sending tracking data to Plausible: ", e) - ); + RequestService.storeRequest({ + projectId, + type: "GET_CONFIG", + durationInMs: performance.now() - now, + }).then((e) => { + console.error("Unable to store request", e); + }); return; } catch (e) { diff --git a/apps/web/src/server/services/RequestService.ts b/apps/web/src/server/services/RequestService.ts new file mode 100644 index 00000000..c08e89fe --- /dev/null +++ b/apps/web/src/server/services/RequestService.ts @@ -0,0 +1,13 @@ +import { ApiRequest } from "@prisma/client"; +import { hashString } from "utils/apiKey"; +import { prisma } from "server/db/client"; + +export abstract class RequestService { + static async storeRequest(request: Omit) { + await prisma.apiRequest.create({ + data: { + ...request, + }, + }); + } +} diff --git a/apps/web/src/server/trpc/router/apikey.ts b/apps/web/src/server/trpc/router/apikey.ts index bc3d43c5..f650635d 100644 --- a/apps/web/src/server/trpc/router/apikey.ts +++ b/apps/web/src/server/trpc/router/apikey.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { protectedProcedure, router } from "../trpc"; -import { generateRandomString, hashApiKey } from "utils/apiKey"; +import { generateRandomString, hashString } from "utils/apiKey"; import dayjs from "dayjs"; export const apiKeyRouter = router({ @@ -12,7 +12,7 @@ export const apiKeyRouter = router({ ) .mutation(async ({ ctx, input }) => { const apiKey = generateRandomString(); - const hashedApiKey = hashApiKey(apiKey); + const hashedApiKey = hashString(apiKey); await ctx.prisma.apiKey.create({ data: { diff --git a/apps/web/src/utils/apiKey.ts b/apps/web/src/utils/apiKey.ts index fb80c310..bcc1def7 100644 --- a/apps/web/src/utils/apiKey.ts +++ b/apps/web/src/utils/apiKey.ts @@ -1,16 +1,14 @@ import { createHmac, randomBytes } from "crypto"; +import { env } from "env/server.mjs"; export function generateRandomString(length = 32): string { const apiKey = randomBytes(length).toString("hex"); return apiKey; } -export function hashApiKey(apiKey: string): string { - const hmac = createHmac( - "sha256", - "dieserkeyistsupergeheimbittenichtweitergebendanke" - ); - hmac.update(apiKey); +export function hashString(data: string): string { + const hmac = createHmac("sha256", env.HASHING_SECRET); + hmac.update(data); const hashKey = hmac.digest("hex"); return hashKey; }